Skip to main content

sh_layer1/observability/
mod.rs

1//! Observability module.
2//!
3//! Provides tracing, metrics, and structured logging capabilities.
4
5pub mod config;
6pub mod logging;
7pub mod metrics;
8pub mod span;
9
10pub use config::{LogFormat, ObservabilityConfig};
11pub use logging::{log, LogLevel};
12pub use metrics::{Counter, Gauge, Histogram, MetricValue, MetricsStorage};
13pub use span::SpanGuard;
14
15use crate::error_handler::ShResult;
16use std::sync::Arc;
17
18/// Main observability manager.
19pub struct Observability {
20    config: ObservabilityConfig,
21    metrics_storage: Arc<MetricsStorage>,
22}
23
24impl Observability {
25    /// Create a new observability instance with the given configuration.
26    pub fn new(config: ObservabilityConfig) -> ShResult<Self> {
27        // Initialize tracing subscriber if enabled
28        if config.tracing_enabled {
29            // Note: Subscriber can only be set once per process
30            // We use try_init which handles the "already set" case gracefully
31            let _ = logging::init_subscriber(config.log_format);
32        }
33
34        Ok(Self {
35            config,
36            metrics_storage: Arc::new(MetricsStorage::new()),
37        })
38    }
39
40    /// Create a new observability instance with default configuration.
41    pub fn with_defaults() -> ShResult<Self> {
42        Self::new(ObservabilityConfig::default())
43    }
44
45    /// Create a new span for tracing.
46    pub fn span(&self, name: &str) -> SpanGuard {
47        if !self.config.tracing_enabled {
48            return SpanGuard::noop();
49        }
50
51        let span = tracing::info_span!(
52            "operation",
53            service = %self.config.service_name,
54            name = name
55        );
56        SpanGuard::new(span)
57    }
58
59    /// Get or create a counter metric.
60    pub fn counter(&self, name: &str) -> Counter {
61        Counter::new(name, Arc::clone(&self.metrics_storage))
62    }
63
64    /// Get or create a histogram metric.
65    pub fn histogram(&self, name: &str) -> Histogram {
66        Histogram::new(name, Arc::clone(&self.metrics_storage))
67    }
68
69    /// Get or create a gauge metric.
70    pub fn gauge(&self, name: &str) -> Gauge {
71        Gauge::new(name, Arc::clone(&self.metrics_storage))
72    }
73
74    /// Log a structured message.
75    pub fn log(&self, level: LogLevel, message: &str, attributes: &[(&str, &str)]) {
76        if self.config.tracing_enabled {
77            logging::log(level, message, attributes);
78        }
79    }
80
81    /// Get a metric value by name.
82    pub fn get_metric(&self, name: &str) -> Option<MetricValue> {
83        self.metrics_storage.get(name)
84    }
85
86    /// List all metric names.
87    pub fn list_metrics(&self) -> Vec<String> {
88        self.metrics_storage.list_names()
89    }
90
91    /// Get the configuration.
92    pub fn config(&self) -> &ObservabilityConfig {
93        &self.config
94    }
95
96    /// Graceful shutdown.
97    pub fn shutdown(self) -> ShResult<()> {
98        #[cfg(feature = "otel")]
99        {
100            // Flush pending traces
101            if self.config.tracing_enabled {
102                tracing::info!("Shutting down observability");
103            }
104        }
105
106        Ok(())
107    }
108}
109
110impl Default for Observability {
111    fn default() -> Self {
112        Self::with_defaults().expect("Failed to create default Observability")
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_observability_creation() {
122        let config = ObservabilityConfig::new("test-service");
123        let obs = Observability::new(config).expect("Failed to create observability");
124        assert_eq!(obs.config().service_name, "test-service");
125    }
126
127    #[test]
128    fn test_observability_default() {
129        let obs = Observability::default();
130        assert_eq!(obs.config().service_name, "continuum");
131    }
132
133    #[test]
134    fn test_span_creation() {
135        let obs = Observability::default();
136        let span = obs.span("test_operation");
137        span.set_attribute("key", "value");
138    }
139
140    #[test]
141    fn test_counter_operations() {
142        let obs = Observability::default();
143        let counter = obs.counter("requests");
144
145        counter.increment(1);
146        counter.increment(2);
147
148        let value = obs.get_metric("requests").expect("Counter should exist");
149        assert_eq!(value.as_counter(), 3);
150    }
151
152    #[test]
153    fn test_gauge_operations() {
154        let obs = Observability::default();
155        let gauge = obs.gauge("temperature");
156
157        gauge.set(25.5);
158
159        let value = obs.get_metric("temperature").expect("Gauge should exist");
160        assert_eq!(value.as_gauge(), 25.5);
161    }
162
163    #[test]
164    fn test_histogram_operations() {
165        let obs = Observability::default();
166        let histogram = obs.histogram("latency");
167
168        histogram.record(0.1);
169        histogram.record(0.2);
170        histogram.record(0.3);
171
172        let value = obs.get_metric("latency").expect("Histogram should exist");
173        let values = value.as_histogram();
174        assert_eq!(values.len(), 3);
175    }
176
177    #[test]
178    fn test_list_metrics() {
179        let obs = Observability::default();
180
181        obs.counter("c1").increment(1);
182        obs.gauge("g1").set(1.0);
183        obs.histogram("h1").record(1.0);
184
185        let names = obs.list_metrics();
186        assert_eq!(names.len(), 3);
187        assert!(names.contains(&"c1".to_string()));
188        assert!(names.contains(&"g1".to_string()));
189        assert!(names.contains(&"h1".to_string()));
190    }
191
192    #[test]
193    fn test_disabled_tracing() {
194        let config = ObservabilityConfig::default().without_tracing();
195        let obs = Observability::new(config).expect("Failed to create observability");
196
197        let span = obs.span("test");
198        // Span should be a no-op (no underlying span)
199        assert!(span.as_ref().is_none());
200    }
201
202    #[test]
203    fn test_log_message() {
204        let obs = Observability::default();
205        obs.log(LogLevel::Info, "test message", &[("key", "value")]);
206        // Should not panic
207    }
208}