rs-zero 0.1.1

Rust-first microservice framework inspired by go-zero engineering practices
Documentation
use std::{collections::BTreeMap, time::Duration};

use crate::observability::{ObservabilityError, ObservabilityResult};

/// OTLP transport protocol.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OtlpProtocol {
    /// gRPC OTLP.
    Grpc,
    /// HTTP/protobuf OTLP.
    HttpProtobuf,
}

/// OTLP traces exporter configuration.
#[derive(Debug, Clone, PartialEq)]
pub struct OtlpTraceConfig {
    /// Collector endpoint.
    pub endpoint: String,
    /// Transport protocol.
    pub protocol: OtlpProtocol,
    /// Request headers.
    pub headers: BTreeMap<String, String>,
    /// Resource attributes.
    pub resource: BTreeMap<String, String>,
    /// Sampling ratio in `[0.0, 1.0]`.
    pub sampling_ratio: f64,
    /// Export timeout.
    pub timeout: Duration,
}

impl Default for OtlpTraceConfig {
    fn default() -> Self {
        Self {
            endpoint: "http://127.0.0.1:4317".to_string(),
            protocol: OtlpProtocol::Grpc,
            headers: BTreeMap::new(),
            resource: BTreeMap::new(),
            sampling_ratio: 1.0,
            timeout: Duration::from_secs(5),
        }
    }
}

/// Handle used to flush and shut down tracing exporters.
#[derive(Debug, Clone, Default)]
pub struct TraceShutdownHandle {
    installed: bool,
}

impl TraceShutdownHandle {
    /// Creates a handle representing an installed exporter.
    pub fn installed() -> Self {
        Self { installed: true }
    }

    /// Returns whether this handle owns an installed exporter.
    pub fn is_installed(&self) -> bool {
        self.installed
    }

    /// Flushes pending spans. The default build has no external collector.
    pub fn flush(&self) -> ObservabilityResult<()> {
        Ok(())
    }

    /// Shuts down the exporter. The default build has no external collector.
    pub fn shutdown(self) -> ObservabilityResult<()> {
        Ok(())
    }
}

/// Validates and normalizes OTLP trace config.
pub fn build_otlp_trace_config(config: OtlpTraceConfig) -> ObservabilityResult<OtlpTraceConfig> {
    if config.endpoint.trim().is_empty() {
        return Err(ObservabilityError::MissingOtlpEndpoint);
    }
    Ok(config)
}

#[cfg(test)]
mod tests {
    use super::{OtlpTraceConfig, TraceShutdownHandle, build_otlp_trace_config};
    use crate::observability::ObservabilityError;

    #[test]
    fn otlp_config_requires_endpoint() {
        let error = build_otlp_trace_config(OtlpTraceConfig {
            endpoint: String::new(),
            ..OtlpTraceConfig::default()
        })
        .expect_err("endpoint");
        assert_eq!(error, ObservabilityError::MissingOtlpEndpoint);
    }

    #[test]
    fn shutdown_handle_flushes_without_collector() {
        let handle = TraceShutdownHandle::installed();
        assert!(handle.is_installed());
        handle.flush().expect("flush");
        handle.shutdown().expect("shutdown");
    }
}