Skip to main content

barbacane_telemetry/
lib.rs

1//! Observability infrastructure for Barbacane API Gateway.
2//!
3//! This crate provides:
4//! - Structured JSON logging with trace correlation
5//! - Prometheus metrics registry and exposition
6//! - Distributed tracing with W3C Trace Context
7//! - OTLP export to OpenTelemetry Collector
8//!
9//! # Usage
10//!
11//! ```ignore
12//! use barbacane_telemetry::{TelemetryConfig, Telemetry};
13//!
14//! let config = TelemetryConfig::new()
15//!     .with_log_level("info")
16//!     .with_otlp_endpoint("http://localhost:4317");
17//!
18//! let telemetry = Telemetry::init(config)?;
19//! ```
20
21pub mod config;
22pub mod export;
23pub mod logging;
24pub mod metrics;
25pub mod prometheus;
26pub mod tracing;
27
28pub use config::{LogFormat, OtlpProtocol, TelemetryConfig};
29pub use logging::events;
30pub use metrics::MetricsRegistry;
31pub use prometheus::PROMETHEUS_CONTENT_TYPE;
32pub use tracing::{attributes, spans, TracingContext};
33
34use std::sync::Arc;
35use thiserror::Error;
36
37/// Telemetry errors.
38#[derive(Debug, Error)]
39pub enum TelemetryError {
40    /// Failed to initialize logging.
41    #[error("failed to initialize logging: {0}")]
42    LoggingInit(String),
43
44    /// Failed to initialize tracing.
45    #[error("failed to initialize tracing: {0}")]
46    TracingInit(String),
47
48    /// Failed to initialize OTLP exporter.
49    #[error("failed to initialize OTLP exporter: {0}")]
50    OtlpInit(String),
51}
52
53/// Main telemetry handle.
54///
55/// Holds references to the metrics registry and provides methods for
56/// trace context propagation.
57pub struct Telemetry {
58    config: TelemetryConfig,
59    metrics: Arc<MetricsRegistry>,
60}
61
62impl Telemetry {
63    /// Initialize telemetry with the given configuration.
64    ///
65    /// This sets up:
66    /// - Structured logging (JSON or pretty format)
67    /// - Metrics registry
68    /// - OTLP exporter (if endpoint configured)
69    pub fn init(config: TelemetryConfig) -> Result<Self, TelemetryError> {
70        // Initialize structured logging
71        logging::init_logging(&config)?;
72
73        // Initialize metrics registry
74        let metrics = Arc::new(MetricsRegistry::new());
75
76        // Initialize tracer - OTLP if endpoint configured, basic otherwise
77        if config.otlp_endpoint.is_some() {
78            export::init_otlp_tracer(&config)?;
79        } else {
80            tracing::init_basic_tracer(&config.service_name, config.trace_sampling);
81        }
82
83        Ok(Self { config, metrics })
84    }
85
86    /// Initialize telemetry without setting up logging.
87    ///
88    /// Use this when logging is already initialized (e.g., in tests).
89    pub fn init_without_logging(config: TelemetryConfig) -> Result<Self, TelemetryError> {
90        // Initialize metrics registry
91        let metrics = Arc::new(MetricsRegistry::new());
92
93        // Initialize tracer - OTLP if endpoint configured, basic otherwise
94        if config.otlp_endpoint.is_some() {
95            export::init_otlp_tracer(&config)?;
96        } else {
97            tracing::init_basic_tracer(&config.service_name, config.trace_sampling);
98        }
99
100        Ok(Self { config, metrics })
101    }
102
103    /// Shutdown telemetry gracefully.
104    ///
105    /// Flushes any remaining spans before shutdown.
106    pub fn shutdown(&self) {
107        if self.config.otlp_endpoint.is_some() {
108            export::shutdown_otlp();
109        } else {
110            tracing::shutdown_tracer();
111        }
112    }
113
114    /// Get the telemetry configuration.
115    pub fn config(&self) -> &TelemetryConfig {
116        &self.config
117    }
118
119    /// Get the metrics registry.
120    pub fn metrics(&self) -> &Arc<MetricsRegistry> {
121        &self.metrics
122    }
123
124    /// Get a cloned Arc reference to the metrics registry.
125    pub fn metrics_clone(&self) -> Arc<MetricsRegistry> {
126        Arc::clone(&self.metrics)
127    }
128
129    /// Render metrics in Prometheus text format.
130    pub fn render_prometheus(&self) -> String {
131        prometheus::render_metrics(&self.metrics)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_default_config() {
141        let config = TelemetryConfig::default();
142        assert_eq!(config.service_name, "barbacane");
143        assert_eq!(config.log_level, "info");
144        assert_eq!(config.log_format, LogFormat::Json);
145        assert!(config.otlp_endpoint.is_none());
146        assert_eq!(config.trace_sampling, 1.0);
147    }
148
149    #[test]
150    fn test_config_builder() {
151        let config = TelemetryConfig::new()
152            .with_service_name("test-service")
153            .with_log_level("debug")
154            .with_log_format(LogFormat::Pretty)
155            .with_otlp_endpoint("http://localhost:4317")
156            .with_trace_sampling(0.5);
157
158        assert_eq!(config.service_name, "test-service");
159        assert_eq!(config.log_level, "debug");
160        assert_eq!(config.log_format, LogFormat::Pretty);
161        assert_eq!(
162            config.otlp_endpoint,
163            Some("http://localhost:4317".to_string())
164        );
165        assert_eq!(config.trace_sampling, 0.5);
166    }
167
168    #[test]
169    fn test_trace_sampling_clamped() {
170        let config = TelemetryConfig::new().with_trace_sampling(1.5);
171        assert_eq!(config.trace_sampling, 1.0);
172
173        let config = TelemetryConfig::new().with_trace_sampling(-0.5);
174        assert_eq!(config.trace_sampling, 0.0);
175    }
176
177    #[test]
178    fn test_telemetry_init_without_logging() {
179        let config = TelemetryConfig::default();
180        let result = Telemetry::init_without_logging(config);
181        assert!(result.is_ok());
182    }
183}