rustkernel_core/observability/
mod.rs

1//! Observability Infrastructure
2//!
3//! This module provides production-grade observability for RustKernels:
4//!
5//! - **Metrics**: Prometheus-compatible metrics for kernel performance
6//! - **Tracing**: Distributed tracing with OpenTelemetry support
7//! - **Logging**: Structured logging with context propagation
8//! - **Alerting**: Alert rules and routing
9//!
10//! # Feature Flags
11//!
12//! - `metrics`: Enable Prometheus metrics export
13//! - `otlp`: Enable OpenTelemetry Protocol export
14//! - `structured-logging`: Enable JSON structured logging
15//! - `alerting`: Enable alert rule engine
16//!
17//! # Example
18//!
19//! ```rust,ignore
20//! use rustkernel_core::observability::{MetricsConfig, ObservabilityConfig};
21//!
22//! let config = ObservabilityConfig::builder()
23//!     .with_metrics(MetricsConfig::prometheus(9090))
24//!     .with_tracing(TracingConfig::otlp("http://jaeger:4317"))
25//!     .build();
26//!
27//! // Initialize observability
28//! config.init().await?;
29//! ```
30
31pub mod alerting;
32pub mod logging;
33pub mod metrics;
34pub mod tracing;
35
36pub use alerting::{AlertConfig, AlertRule, AlertSeverity, AlertState};
37pub use logging::{LogConfig, LogLevel, StructuredLogger};
38pub use metrics::{KernelMetrics, MetricsConfig, MetricsExporter};
39pub use tracing::{KernelSpan, SpanContext, TracingConfig};
40
41use serde::{Deserialize, Serialize};
42
43/// Unified observability configuration
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ObservabilityConfig {
46    /// Metrics configuration
47    pub metrics: Option<MetricsConfig>,
48    /// Tracing configuration
49    pub tracing: Option<TracingConfig>,
50    /// Logging configuration
51    pub logging: LogConfig,
52    /// Alerting configuration
53    pub alerting: Option<AlertConfig>,
54    /// Service name for all telemetry
55    pub service_name: String,
56    /// Service version
57    pub service_version: String,
58    /// Environment (dev, staging, prod)
59    pub environment: String,
60}
61
62impl Default for ObservabilityConfig {
63    fn default() -> Self {
64        Self {
65            metrics: None,
66            tracing: None,
67            logging: LogConfig::default(),
68            alerting: None,
69            service_name: "rustkernels".to_string(),
70            service_version: env!("CARGO_PKG_VERSION").to_string(),
71            environment: "development".to_string(),
72        }
73    }
74}
75
76impl ObservabilityConfig {
77    /// Create a new builder
78    pub fn builder() -> ObservabilityConfigBuilder {
79        ObservabilityConfigBuilder::default()
80    }
81
82    /// Development configuration - verbose logging, no external exports
83    pub fn development() -> Self {
84        Self {
85            logging: LogConfig {
86                level: LogLevel::Debug,
87                structured: false,
88                ..Default::default()
89            },
90            environment: "development".to_string(),
91            ..Default::default()
92        }
93    }
94
95    /// Production configuration - structured logging, metrics enabled
96    pub fn production() -> Self {
97        Self {
98            metrics: Some(MetricsConfig::default()),
99            tracing: Some(TracingConfig::default()),
100            logging: LogConfig {
101                level: LogLevel::Info,
102                structured: true,
103                ..Default::default()
104            },
105            alerting: Some(AlertConfig::default()),
106            environment: "production".to_string(),
107            ..Default::default()
108        }
109    }
110
111    /// Initialize all observability components
112    #[cfg(feature = "metrics")]
113    pub async fn init(&self) -> crate::error::Result<()> {
114        // Initialize logging
115        self.logging.init()?;
116
117        // Initialize metrics
118        if let Some(ref metrics) = self.metrics {
119            metrics.init().await?;
120        }
121
122        // Initialize tracing
123        if let Some(ref tracing) = self.tracing {
124            tracing.init().await?;
125        }
126
127        Ok(())
128    }
129
130    /// Initialize without metrics feature
131    #[cfg(not(feature = "metrics"))]
132    pub async fn init(&self) -> crate::error::Result<()> {
133        self.logging.init()?;
134        Ok(())
135    }
136}
137
138/// Builder for observability configuration
139#[derive(Default)]
140pub struct ObservabilityConfigBuilder {
141    config: ObservabilityConfig,
142}
143
144impl ObservabilityConfigBuilder {
145    /// Set metrics configuration
146    pub fn with_metrics(mut self, config: MetricsConfig) -> Self {
147        self.config.metrics = Some(config);
148        self
149    }
150
151    /// Set tracing configuration
152    pub fn with_tracing(mut self, config: TracingConfig) -> Self {
153        self.config.tracing = Some(config);
154        self
155    }
156
157    /// Set logging configuration
158    pub fn with_logging(mut self, config: LogConfig) -> Self {
159        self.config.logging = config;
160        self
161    }
162
163    /// Set alerting configuration
164    pub fn with_alerting(mut self, config: AlertConfig) -> Self {
165        self.config.alerting = Some(config);
166        self
167    }
168
169    /// Set service name
170    pub fn service_name(mut self, name: impl Into<String>) -> Self {
171        self.config.service_name = name.into();
172        self
173    }
174
175    /// Set service version
176    pub fn service_version(mut self, version: impl Into<String>) -> Self {
177        self.config.service_version = version.into();
178        self
179    }
180
181    /// Set environment
182    pub fn environment(mut self, env: impl Into<String>) -> Self {
183        self.config.environment = env.into();
184        self
185    }
186
187    /// Build the configuration
188    pub fn build(self) -> ObservabilityConfig {
189        self.config
190    }
191}
192
193/// Common labels for all metrics
194#[derive(Debug, Clone, Default)]
195pub struct MetricLabels {
196    /// Kernel ID
197    pub kernel_id: Option<String>,
198    /// Domain
199    pub domain: Option<String>,
200    /// Tenant ID
201    pub tenant_id: Option<String>,
202    /// Additional labels
203    pub extra: std::collections::HashMap<String, String>,
204}
205
206impl MetricLabels {
207    /// Create new metric labels
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Set kernel ID
213    pub fn with_kernel(mut self, id: impl Into<String>) -> Self {
214        self.kernel_id = Some(id.into());
215        self
216    }
217
218    /// Set domain
219    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
220        self.domain = Some(domain.into());
221        self
222    }
223
224    /// Set tenant ID
225    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
226        self.tenant_id = Some(tenant.into());
227        self
228    }
229
230    /// Add extra label
231    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
232        self.extra.insert(key.into(), value.into());
233        self
234    }
235
236    /// Convert to label pairs
237    pub fn to_pairs(&self) -> Vec<(&str, String)> {
238        let mut pairs = Vec::new();
239        if let Some(ref id) = self.kernel_id {
240            pairs.push(("kernel_id", id.clone()));
241        }
242        if let Some(ref domain) = self.domain {
243            pairs.push(("domain", domain.clone()));
244        }
245        if let Some(ref tenant) = self.tenant_id {
246            pairs.push(("tenant_id", tenant.clone()));
247        }
248        for (k, v) in &self.extra {
249            pairs.push((k.as_str(), v.clone()));
250        }
251        pairs
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_default_config() {
261        let config = ObservabilityConfig::default();
262        assert_eq!(config.service_name, "rustkernels");
263        assert_eq!(config.environment, "development");
264        assert!(config.metrics.is_none());
265    }
266
267    #[test]
268    fn test_production_config() {
269        let config = ObservabilityConfig::production();
270        assert_eq!(config.environment, "production");
271        assert!(config.metrics.is_some());
272        assert!(config.tracing.is_some());
273    }
274
275    #[test]
276    fn test_builder() {
277        let config = ObservabilityConfig::builder()
278            .service_name("test-service")
279            .environment("testing")
280            .build();
281
282        assert_eq!(config.service_name, "test-service");
283        assert_eq!(config.environment, "testing");
284    }
285
286    #[test]
287    fn test_metric_labels() {
288        let labels = MetricLabels::new()
289            .with_kernel("graph/pagerank")
290            .with_domain("GraphAnalytics")
291            .with_tenant("tenant-123");
292
293        let pairs = labels.to_pairs();
294        assert_eq!(pairs.len(), 3);
295    }
296}