rs-zero 0.1.1

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
use thiserror::Error;
use tracing_subscriber::{EnvFilter, fmt};

use crate::observability::{OpenTelemetryConfig, TraceExporter};

/// Result type used by observability setup.
pub type ObservabilityResult<T> = Result<T, ObservabilityError>;

/// Errors returned by observability setup.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum ObservabilityError {
    /// A global tracing subscriber has already been installed.
    #[error("tracing subscriber is already initialized")]
    SubscriberAlreadyInitialized,

    /// The OTLP exporter configuration is incomplete.
    #[error("otlp exporter endpoint is required")]
    MissingOtlpEndpoint,
}

/// Initializes tracing according to the OpenTelemetry configuration.
///
/// `TraceExporter::Stdout` installs a `tracing-subscriber` pipeline that is
/// useful for local OpenTelemetry-style span inspection. `TraceExporter::Otlp`
/// validates endpoint configuration and leaves transport installation explicit
/// for application code, so tests and local development do not require a
/// collector.
pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
    match config.exporter {
        TraceExporter::Disabled => Ok(()),
        TraceExporter::Stdout => {
            let filter =
                EnvFilter::try_new(config.filter).unwrap_or_else(|_| EnvFilter::new("info"));
            fmt()
                .with_env_filter(filter)
                .try_init()
                .map_err(|_| ObservabilityError::SubscriberAlreadyInitialized)
        }
        TraceExporter::Otlp { endpoint } => {
            if endpoint.trim().is_empty() {
                Err(ObservabilityError::MissingOtlpEndpoint)
            } else {
                Ok(())
            }
        }
    }
}

#[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);
    }
}