rs-zero 0.2.8

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

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

/// 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,

    /// The exporter pipeline could not be installed or flushed.
    #[error("observability exporter error: {0}")]
    ExporterInstall(String),
}

/// Initializes tracing according to the OpenTelemetry configuration.
pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
    init_opentelemetry_tracing_with_handle(config).map(|_| ())
}

/// Initializes tracing and returns a shutdown handle when an exporter is installed.
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);
    }
}