sh_layer1/observability/
mod.rs1pub 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
18pub struct Observability {
20 config: ObservabilityConfig,
21 metrics_storage: Arc<MetricsStorage>,
22}
23
24impl Observability {
25 pub fn new(config: ObservabilityConfig) -> ShResult<Self> {
27 if config.tracing_enabled {
29 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 pub fn with_defaults() -> ShResult<Self> {
42 Self::new(ObservabilityConfig::default())
43 }
44
45 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 pub fn counter(&self, name: &str) -> Counter {
61 Counter::new(name, Arc::clone(&self.metrics_storage))
62 }
63
64 pub fn histogram(&self, name: &str) -> Histogram {
66 Histogram::new(name, Arc::clone(&self.metrics_storage))
67 }
68
69 pub fn gauge(&self, name: &str) -> Gauge {
71 Gauge::new(name, Arc::clone(&self.metrics_storage))
72 }
73
74 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 pub fn get_metric(&self, name: &str) -> Option<MetricValue> {
83 self.metrics_storage.get(name)
84 }
85
86 pub fn list_metrics(&self) -> Vec<String> {
88 self.metrics_storage.list_names()
89 }
90
91 pub fn config(&self) -> &ObservabilityConfig {
93 &self.config
94 }
95
96 pub fn shutdown(self) -> ShResult<()> {
98 #[cfg(feature = "otel")]
99 {
100 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 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 }
208}