use thiserror::Error;
use tracing_subscriber::{EnvFilter, fmt, fmt::format::FmtSpan};
use crate::observability::{OpenTelemetryConfig, TraceExporter, TraceShutdownHandle};
pub type ObservabilityResult<T> = Result<T, ObservabilityError>;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ObservabilityError {
#[error("tracing subscriber is already initialized")]
SubscriberAlreadyInitialized,
#[error("otlp exporter endpoint is required")]
MissingOtlpEndpoint,
#[error("observability exporter error: {0}")]
ExporterInstall(String),
}
pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
init_opentelemetry_tracing_with_handle(config).map(|_| ())
}
pub fn init_opentelemetry_tracing_with_handle(
config: OpenTelemetryConfig,
) -> ObservabilityResult<TraceShutdownHandle> {
match config.exporter {
TraceExporter::Disabled => Ok(TraceShutdownHandle::disabled()),
TraceExporter::Stdout => {
let filter =
EnvFilter::try_new(config.filter).unwrap_or_else(|_| EnvFilter::new("info"));
fmt()
.with_env_filter(filter)
.with_span_events(FmtSpan::CLOSE)
.try_init()
.map_err(|_| ObservabilityError::SubscriberAlreadyInitialized)?;
Ok(TraceShutdownHandle::installed())
}
TraceExporter::Otlp { endpoint } => init_otlp(endpoint, config.filter, config.timeout),
}
}
#[cfg(feature = "otlp")]
fn init_otlp(
endpoint: String,
filter: String,
timeout: std::time::Duration,
) -> ObservabilityResult<TraceShutdownHandle> {
crate::observability::install_otlp_tracing(
crate::observability::OtlpTraceConfig {
endpoint,
timeout,
..crate::observability::OtlpTraceConfig::default()
},
filter,
)
}
#[cfg(not(feature = "otlp"))]
fn init_otlp(
endpoint: String,
_filter: String,
_timeout: std::time::Duration,
) -> ObservabilityResult<TraceShutdownHandle> {
if endpoint.trim().is_empty() {
Err(ObservabilityError::MissingOtlpEndpoint)
} else {
Err(ObservabilityError::ExporterInstall(
"enable the `otlp` feature to install an OTLP exporter".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::{ObservabilityError, init_opentelemetry_tracing};
use crate::observability::{OpenTelemetryConfig, TraceExporter};
#[test]
fn disabled_exporter_does_not_install_subscriber() {
init_opentelemetry_tracing(OpenTelemetryConfig::default()).expect("disabled");
}
#[test]
fn otlp_requires_endpoint() {
let error = init_opentelemetry_tracing(OpenTelemetryConfig {
exporter: TraceExporter::Otlp {
endpoint: String::new(),
},
..OpenTelemetryConfig::default()
})
.expect_err("missing endpoint");
assert_eq!(error, ObservabilityError::MissingOtlpEndpoint);
}
}