#![cfg_attr(
not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)),
expect(unreachable_code, unused_variables)
)]
use std::collections::HashMap;
use opentelemetry_otlp::Protocol;
use opentelemetry_sdk::{
logs::LogExporter, metrics::exporter::PushMetricExporter, trace::SpanExporter,
};
use crate::{ConfigureError, internal::env::get_optional_env};
#[cfg(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
))]
const USER_AGENT: &str = concat!("logfire-rust/", env!("CARGO_PKG_VERSION"));
#[cfg(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
))]
fn headers_with_user_agent(headers: Option<HashMap<String, String>>) -> HashMap<String, String> {
let mut headers = headers.unwrap_or_default();
if !headers
.keys()
.any(|name| name.eq_ignore_ascii_case("user-agent"))
{
headers.insert("User-Agent".to_string(), USER_AGENT.to_string());
}
headers
}
macro_rules! feature_required {
($feature_name:literal, $functionality:expr, $if_enabled:expr) => {{
#[cfg(feature = $feature_name)]
{
let _ = $functionality; $if_enabled
}
#[cfg(not(feature = $feature_name))]
{
return Err(ConfigureError::LogfireFeatureRequired {
feature_name: $feature_name,
functionality: $functionality,
});
}
}};
}
pub fn span_exporter(
endpoint: &str,
headers: Option<HashMap<String, String>>,
) -> Result<impl SpanExporter + use<>, ConfigureError> {
let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")?;
let span_exporter = match protocol {
#[cfg(feature = "export-grpc")]
Protocol::Grpc => {
use opentelemetry_otlp::WithTonicConfig;
opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_channel(
tonic::transport::Channel::builder(
endpoint
.try_into()
.map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?,
)
.connect_lazy(),
)
.with_metadata(build_metadata_from_headers(headers)?)
.build()?
}
#[cfg(feature = "export-http-protobuf")]
Protocol::HttpBinary => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/traces"))
.build()?
}
#[cfg(feature = "export-http-json")]
Protocol::HttpJson => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/traces"))
.build()?
}
};
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
{
Ok(UnreachableExporter)
}
#[cfg(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
))]
{
Ok(
crate::internal::exporters::remove_pending::RemovePendingSpansExporter::new(
span_exporter,
),
)
}
}
pub fn metric_exporter(
endpoint: &str,
headers: Option<HashMap<String, String>>,
) -> Result<impl PushMetricExporter + use<>, ConfigureError> {
let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")?;
match protocol {
#[cfg(feature = "export-grpc")]
Protocol::Grpc => {
use opentelemetry_otlp::WithTonicConfig;
Ok(opentelemetry_otlp::MetricExporter::builder()
.with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
.with_tonic()
.with_channel(
tonic::transport::Channel::builder(
endpoint
.try_into()
.map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?,
)
.connect_lazy(),
)
.with_metadata(build_metadata_from_headers(headers)?)
.build()?)
}
#[cfg(feature = "export-http-protobuf")]
Protocol::HttpBinary => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(opentelemetry_otlp::MetricExporter::builder()
.with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/metrics"))
.build()?)
}
#[cfg(feature = "export-http-json")]
Protocol::HttpJson => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(opentelemetry_otlp::MetricExporter::builder()
.with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/metrics"))
.build()?)
}
}
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
#[expect(unreachable_code)]
{
let _ = endpoint;
let _ = headers;
let _ = protocol;
Ok(UnreachableExporter)
}
}
pub fn log_exporter(
endpoint: &str,
headers: Option<HashMap<String, String>>,
) -> Result<impl LogExporter + use<>, ConfigureError> {
let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")?;
match protocol {
#[cfg(feature = "export-grpc")]
Protocol::Grpc => {
use opentelemetry_otlp::WithTonicConfig;
Ok(opentelemetry_otlp::LogExporter::builder()
.with_tonic()
.with_channel(
tonic::transport::Channel::builder(
endpoint
.try_into()
.map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?,
)
.connect_lazy(),
)
.with_metadata(build_metadata_from_headers(headers)?)
.build()?)
}
#[cfg(feature = "export-http-protobuf")]
Protocol::HttpBinary => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(opentelemetry_otlp::LogExporter::builder()
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/logs"))
.build()?)
}
#[cfg(feature = "export-http-json")]
Protocol::HttpJson => {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(opentelemetry_otlp::LogExporter::builder()
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers_with_user_agent(headers))
.with_endpoint(format!("{endpoint}/v1/logs"))
.build()?)
}
}
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
#[expect(unreachable_code)]
{
let _ = endpoint;
let _ = headers;
let _ = protocol;
Ok(UnreachableExporter)
}
}
#[cfg(feature = "export-grpc")]
fn build_metadata_from_headers(
headers: Option<HashMap<String, String>>,
) -> Result<tonic::metadata::MetadataMap, ConfigureError> {
let mut header_map = http::HeaderMap::new();
for (key, value) in headers_with_user_agent(headers) {
header_map.insert(
http::HeaderName::try_from(key).map_err(|e| ConfigureError::Other(e.into()))?,
http::HeaderValue::try_from(value).map_err(|e| ConfigureError::Other(e.into()))?,
);
}
Ok(tonic::metadata::MetadataMap::from_headers(header_map))
}
#[cfg(test)]
#[cfg(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
))]
mod tests {
use super::{USER_AGENT, headers_with_user_agent};
use std::collections::HashMap;
#[test]
fn user_agent_is_added_to_headers() {
assert_eq!(
headers_with_user_agent(None),
HashMap::from([("User-Agent".to_string(), USER_AGENT.to_string())])
);
let headers = headers_with_user_agent(Some(HashMap::from([(
"Authorization".to_string(),
"Bearer token".to_string(),
)])));
assert_eq!(
headers,
HashMap::from([
("Authorization".to_string(), "Bearer token".to_string()),
("User-Agent".to_string(), USER_AGENT.to_string())
])
);
}
#[test]
fn user_agent_set_by_caller_is_preserved() {
let headers = headers_with_user_agent(Some(HashMap::from([(
"user-agent".to_string(),
"my-app/1.2.3".to_string(),
)])));
assert_eq!(
headers,
HashMap::from([("user-agent".to_string(), "my-app/1.2.3".to_string())])
);
}
}
const DEFAULT_LOGFIRE_PROTOCOL: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF;
const OTEL_EXPORTER_OTLP_PROTOCOL_GRPC: &str = "grpc";
const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF: &str = "http/protobuf";
const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json";
fn protocol_from_env(data_env_var: &str) -> Result<Protocol, ConfigureError> {
let (source, value) = [data_env_var, "OTEL_EXPORTER_OTLP_PROTOCOL"]
.into_iter()
.find_map(|var_name| match get_optional_env(var_name, None) {
Ok(Some(value)) => Some(Ok((var_name, value))),
Ok(None) => None,
Err(e) => Some(Err(e)),
})
.transpose()?
.map_or(
(
"the default logfire export protocol".to_string(),
DEFAULT_LOGFIRE_PROTOCOL.to_string(),
),
|(var_name, value)| (format!("`{var_name}={value}`"), value),
);
match value.as_str() {
OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => {
feature_required!("export-grpc", source, Ok(Protocol::Grpc))
}
OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => {
feature_required!("export-http-protobuf", source, Ok(Protocol::HttpBinary))
}
OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => {
feature_required!("export-http-json", source, Ok(Protocol::HttpJson))
}
_ => Err(ConfigureError::Other(
format!("unsupported protocol: {value}").into(),
)),
}
}
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
#[derive(Debug)]
struct UnreachableExporter;
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
impl SpanExporter for UnreachableExporter {
fn export(
&self,
_batch: Vec<opentelemetry_sdk::trace::SpanData>,
) -> impl std::future::Future<Output = opentelemetry_sdk::error::OTelSdkResult> + Send {
async { unreachable!() }
}
}
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
impl PushMetricExporter for UnreachableExporter {
fn export(
&self,
_metrics: &opentelemetry_sdk::metrics::data::ResourceMetrics,
) -> impl std::future::Future<Output = opentelemetry_sdk::error::OTelSdkResult> + Send {
async { unreachable!() }
}
fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
unreachable!()
}
fn shutdown_with_timeout(
&self,
_timeout: std::time::Duration,
) -> opentelemetry_sdk::error::OTelSdkResult {
unreachable!()
}
fn temporality(&self) -> opentelemetry_sdk::metrics::Temporality {
unreachable!()
}
}
#[cfg(not(any(
feature = "export-grpc",
feature = "export-http-protobuf",
feature = "export-http-json"
)))]
impl LogExporter for UnreachableExporter {
fn export(
&self,
_batch: opentelemetry_sdk::logs::LogBatch<'_>,
) -> impl std::future::Future<Output = opentelemetry_sdk::error::OTelSdkResult> + Send {
async { unreachable!() }
}
}