use std::collections::HashMap;
use opentelemetry_otlp::{LogExporter, MetricExporter, Protocol};
use opentelemetry_sdk::{
logs::LogExporter as LogExporterTrait, metrics::exporter::PushMetricExporter,
trace::SpanExporter,
};
use crate::{
ConfigureError, internal::env::get_optional_env,
internal::exporters::remove_pending::RemovePendingSpansExporter,
};
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 (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")?;
let builder = opentelemetry_otlp::SpanExporter::builder();
let span_exporter =
match protocol {
Protocol::Grpc => {
feature_required!("export-grpc", source, {
use opentelemetry_otlp::WithTonicConfig;
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.as_ref())?)
.build()?
})
}
Protocol::HttpBinary => {
feature_required!("export-http-protobuf", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
builder
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/traces"))
.build()?
})
}
Protocol::HttpJson => {
feature_required!("export-http-json", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
builder
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/traces"))
.build()?
})
}
};
Ok(RemovePendingSpansExporter::new(span_exporter))
}
pub fn metric_exporter(
endpoint: &str,
headers: Option<HashMap<String, String>>,
) -> Result<impl PushMetricExporter + use<>, ConfigureError> {
let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")?;
let builder =
MetricExporter::builder().with_temporality(opentelemetry_sdk::metrics::Temporality::Delta);
match protocol {
Protocol::Grpc => {
feature_required!("export-grpc", source, {
use opentelemetry_otlp::WithTonicConfig;
Ok(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.as_ref())?)
.build()?)
})
}
Protocol::HttpBinary => {
feature_required!("export-http-protobuf", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(builder
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/metrics"))
.build()?)
})
}
Protocol::HttpJson => {
feature_required!("export-http-json", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(builder
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/metrics"))
.build()?)
})
}
}
}
pub fn log_exporter(
endpoint: &str,
headers: Option<HashMap<String, String>>,
) -> Result<impl LogExporterTrait + use<>, ConfigureError> {
let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")?;
let builder = LogExporter::builder();
match protocol {
Protocol::Grpc => {
feature_required!("export-grpc", source, {
use opentelemetry_otlp::WithTonicConfig;
Ok(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.as_ref())?)
.build()?)
})
}
Protocol::HttpBinary => {
feature_required!("export-http-protobuf", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(builder
.with_http()
.with_protocol(Protocol::HttpBinary)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/logs"))
.build()?)
})
}
Protocol::HttpJson => {
feature_required!("export-http-json", source, {
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
Ok(builder
.with_http()
.with_protocol(Protocol::HttpJson)
.with_headers(headers.unwrap_or_default())
.with_endpoint(format!("{endpoint}/v1/logs"))
.build()?)
})
}
}
}
#[cfg(feature = "export-grpc")]
fn build_metadata_from_headers(
headers: Option<&HashMap<String, String>>,
) -> Result<tonic::metadata::MetadataMap, ConfigureError> {
let Some(headers) = headers else {
return Ok(tonic::metadata::MetadataMap::new());
};
let mut header_map = http::HeaderMap::new();
for (key, value) in 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))
}
const DEFAULT_LOGFIRE_PROTOCOL: Protocol = Protocol::HttpBinary;
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_str(value: &str) -> Result<Protocol, ConfigureError> {
match value {
OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => Ok(Protocol::Grpc),
OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => Ok(Protocol::HttpBinary),
OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => Ok(Protocol::HttpJson),
_ => Err(ConfigureError::Other(
format!("unsupported protocol: {value}").into(),
)),
}
}
fn protocol_from_env(data_env_var: &str) -> Result<(String, Protocol), ConfigureError> {
[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_else(
|| {
Ok((
"the default logfire export protocol".to_string(),
DEFAULT_LOGFIRE_PROTOCOL,
))
},
|(var_name, value)| Ok((format!("`{var_name}={value}`"), protocol_from_str(&value)?)),
)
}