Skip to main content

rs_zero/observability/
otel.rs

1use thiserror::Error;
2use tracing_subscriber::{EnvFilter, fmt, fmt::format::FmtSpan};
3
4use crate::observability::{OpenTelemetryConfig, TraceExporter, TraceShutdownHandle};
5
6/// Result type used by observability setup.
7pub type ObservabilityResult<T> = Result<T, ObservabilityError>;
8
9/// Errors returned by observability setup.
10#[derive(Debug, Error, PartialEq, Eq)]
11pub enum ObservabilityError {
12    /// A global tracing subscriber has already been installed.
13    #[error("tracing subscriber is already initialized")]
14    SubscriberAlreadyInitialized,
15
16    /// The OTLP exporter configuration is incomplete.
17    #[error("otlp exporter endpoint is required")]
18    MissingOtlpEndpoint,
19
20    /// The exporter pipeline could not be installed or flushed.
21    #[error("observability exporter error: {0}")]
22    ExporterInstall(String),
23}
24
25/// Initializes tracing according to the OpenTelemetry configuration.
26pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
27    init_opentelemetry_tracing_with_handle(config).map(|_| ())
28}
29
30/// Initializes tracing and returns a shutdown handle when an exporter is installed.
31pub fn init_opentelemetry_tracing_with_handle(
32    config: OpenTelemetryConfig,
33) -> ObservabilityResult<TraceShutdownHandle> {
34    match config.exporter {
35        TraceExporter::Disabled => Ok(TraceShutdownHandle::disabled()),
36        TraceExporter::Stdout => {
37            let filter =
38                EnvFilter::try_new(config.filter).unwrap_or_else(|_| EnvFilter::new("info"));
39            fmt()
40                .with_env_filter(filter)
41                .with_span_events(FmtSpan::CLOSE)
42                .try_init()
43                .map_err(|_| ObservabilityError::SubscriberAlreadyInitialized)?;
44            Ok(TraceShutdownHandle::installed())
45        }
46        TraceExporter::Otlp { endpoint } => init_otlp(endpoint, config.filter, config.timeout),
47    }
48}
49
50#[cfg(feature = "otlp")]
51fn init_otlp(
52    endpoint: String,
53    filter: String,
54    timeout: std::time::Duration,
55) -> ObservabilityResult<TraceShutdownHandle> {
56    crate::observability::install_otlp_tracing(
57        crate::observability::OtlpTraceConfig {
58            endpoint,
59            timeout,
60            ..crate::observability::OtlpTraceConfig::default()
61        },
62        filter,
63    )
64}
65
66#[cfg(not(feature = "otlp"))]
67fn init_otlp(
68    endpoint: String,
69    _filter: String,
70    _timeout: std::time::Duration,
71) -> ObservabilityResult<TraceShutdownHandle> {
72    if endpoint.trim().is_empty() {
73        Err(ObservabilityError::MissingOtlpEndpoint)
74    } else {
75        Err(ObservabilityError::ExporterInstall(
76            "enable the `otlp` feature to install an OTLP exporter".to_string(),
77        ))
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::{ObservabilityError, init_opentelemetry_tracing};
84    use crate::observability::{OpenTelemetryConfig, TraceExporter};
85
86    #[test]
87    fn disabled_exporter_does_not_install_subscriber() {
88        init_opentelemetry_tracing(OpenTelemetryConfig::default()).expect("disabled");
89    }
90
91    #[test]
92    fn otlp_requires_endpoint() {
93        let error = init_opentelemetry_tracing(OpenTelemetryConfig {
94            exporter: TraceExporter::Otlp {
95                endpoint: String::new(),
96            },
97            ..OpenTelemetryConfig::default()
98        })
99        .expect_err("missing endpoint");
100        assert_eq!(error, ObservabilityError::MissingOtlpEndpoint);
101    }
102}