Skip to main content

rs_zero/observability/
otel.rs

1use thiserror::Error;
2use tracing_subscriber::{EnvFilter, fmt};
3
4use crate::observability::{OpenTelemetryConfig, TraceExporter};
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
21/// Initializes tracing according to the OpenTelemetry configuration.
22///
23/// `TraceExporter::Stdout` installs a `tracing-subscriber` pipeline that is
24/// useful for local OpenTelemetry-style span inspection. `TraceExporter::Otlp`
25/// validates endpoint configuration and leaves transport installation explicit
26/// for application code, so tests and local development do not require a
27/// collector.
28pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
29    match config.exporter {
30        TraceExporter::Disabled => Ok(()),
31        TraceExporter::Stdout => {
32            let filter =
33                EnvFilter::try_new(config.filter).unwrap_or_else(|_| EnvFilter::new("info"));
34            fmt()
35                .with_env_filter(filter)
36                .try_init()
37                .map_err(|_| ObservabilityError::SubscriberAlreadyInitialized)
38        }
39        TraceExporter::Otlp { endpoint } => {
40            if endpoint.trim().is_empty() {
41                Err(ObservabilityError::MissingOtlpEndpoint)
42            } else {
43                Ok(())
44            }
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::{ObservabilityError, init_opentelemetry_tracing};
52    use crate::observability::{OpenTelemetryConfig, TraceExporter};
53
54    #[test]
55    fn disabled_exporter_does_not_install_subscriber() {
56        init_opentelemetry_tracing(OpenTelemetryConfig::default()).expect("disabled");
57    }
58
59    #[test]
60    fn otlp_requires_endpoint() {
61        let error = init_opentelemetry_tracing(OpenTelemetryConfig {
62            exporter: TraceExporter::Otlp {
63                endpoint: String::new(),
64            },
65            ..OpenTelemetryConfig::default()
66        })
67        .expect_err("missing endpoint");
68        assert_eq!(error, ObservabilityError::MissingOtlpEndpoint);
69    }
70}