Skip to main content

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
41// Re-export ringkernel-core 0.4.2 observability primitives for deep integration.
42pub use ringkernel_core::alerting as ring_alerting;
43pub use ringkernel_core::logging as ring_logging;
44pub use ringkernel_core::observability as ring_observability;
45pub use ringkernel_core::telemetry as ring_telemetry;
46pub use ringkernel_core::telemetry_pipeline as ring_telemetry_pipeline;
47
48use serde::{Deserialize, Serialize};
49
50/// Unified observability configuration
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ObservabilityConfig {
53    /// Metrics configuration
54    pub metrics: Option<MetricsConfig>,
55    /// Tracing configuration
56    pub tracing: Option<TracingConfig>,
57    /// Logging configuration
58    pub logging: LogConfig,
59    /// Alerting configuration
60    pub alerting: Option<AlertConfig>,
61    /// Service name for all telemetry
62    pub service_name: String,
63    /// Service version
64    pub service_version: String,
65    /// Environment (dev, staging, prod)
66    pub environment: String,
67}
68
69impl Default for ObservabilityConfig {
70    fn default() -> Self {
71        Self {
72            metrics: None,
73            tracing: None,
74            logging: LogConfig::default(),
75            alerting: None,
76            service_name: "rustkernels".to_string(),
77            service_version: env!("CARGO_PKG_VERSION").to_string(),
78            environment: "development".to_string(),
79        }
80    }
81}
82
83impl ObservabilityConfig {
84    /// Create a new builder
85    pub fn builder() -> ObservabilityConfigBuilder {
86        ObservabilityConfigBuilder::default()
87    }
88
89    /// Development configuration - verbose logging, no external exports
90    pub fn development() -> Self {
91        Self {
92            logging: LogConfig {
93                level: LogLevel::Debug,
94                structured: false,
95                ..Default::default()
96            },
97            environment: "development".to_string(),
98            ..Default::default()
99        }
100    }
101
102    /// Production configuration - structured logging, metrics enabled
103    pub fn production() -> Self {
104        Self {
105            metrics: Some(MetricsConfig::default()),
106            tracing: Some(TracingConfig::default()),
107            logging: LogConfig {
108                level: LogLevel::Info,
109                structured: true,
110                ..Default::default()
111            },
112            alerting: Some(AlertConfig::default()),
113            environment: "production".to_string(),
114            ..Default::default()
115        }
116    }
117
118    /// Initialize all observability components
119    #[cfg(feature = "metrics")]
120    pub async fn init(&self) -> crate::error::Result<()> {
121        // Initialize logging
122        self.logging.init()?;
123
124        // Initialize metrics
125        if let Some(ref metrics) = self.metrics {
126            metrics.init().await?;
127        }
128
129        // Initialize tracing
130        if let Some(ref tracing) = self.tracing {
131            tracing.init().await?;
132        }
133
134        Ok(())
135    }
136
137    /// Initialize without metrics feature
138    #[cfg(not(feature = "metrics"))]
139    pub async fn init(&self) -> crate::error::Result<()> {
140        self.logging.init()?;
141        Ok(())
142    }
143}
144
145/// Builder for observability configuration
146#[derive(Default)]
147pub struct ObservabilityConfigBuilder {
148    config: ObservabilityConfig,
149}
150
151impl ObservabilityConfigBuilder {
152    /// Set metrics configuration
153    pub fn with_metrics(mut self, config: MetricsConfig) -> Self {
154        self.config.metrics = Some(config);
155        self
156    }
157
158    /// Set tracing configuration
159    pub fn with_tracing(mut self, config: TracingConfig) -> Self {
160        self.config.tracing = Some(config);
161        self
162    }
163
164    /// Set logging configuration
165    pub fn with_logging(mut self, config: LogConfig) -> Self {
166        self.config.logging = config;
167        self
168    }
169
170    /// Set alerting configuration
171    pub fn with_alerting(mut self, config: AlertConfig) -> Self {
172        self.config.alerting = Some(config);
173        self
174    }
175
176    /// Set service name
177    pub fn service_name(mut self, name: impl Into<String>) -> Self {
178        self.config.service_name = name.into();
179        self
180    }
181
182    /// Set service version
183    pub fn service_version(mut self, version: impl Into<String>) -> Self {
184        self.config.service_version = version.into();
185        self
186    }
187
188    /// Set environment
189    pub fn environment(mut self, env: impl Into<String>) -> Self {
190        self.config.environment = env.into();
191        self
192    }
193
194    /// Build the configuration
195    pub fn build(self) -> ObservabilityConfig {
196        self.config
197    }
198}
199
200/// Common labels for all metrics
201#[derive(Debug, Clone, Default)]
202pub struct MetricLabels {
203    /// Kernel ID
204    pub kernel_id: Option<String>,
205    /// Domain
206    pub domain: Option<String>,
207    /// Tenant ID
208    pub tenant_id: Option<String>,
209    /// Additional labels
210    pub extra: std::collections::HashMap<String, String>,
211}
212
213impl MetricLabels {
214    /// Create new metric labels
215    pub fn new() -> Self {
216        Self::default()
217    }
218
219    /// Set kernel ID
220    pub fn with_kernel(mut self, id: impl Into<String>) -> Self {
221        self.kernel_id = Some(id.into());
222        self
223    }
224
225    /// Set domain
226    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
227        self.domain = Some(domain.into());
228        self
229    }
230
231    /// Set tenant ID
232    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
233        self.tenant_id = Some(tenant.into());
234        self
235    }
236
237    /// Add extra label
238    pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
239        self.extra.insert(key.into(), value.into());
240        self
241    }
242
243    /// Convert to label pairs
244    pub fn to_pairs(&self) -> Vec<(&str, String)> {
245        let mut pairs = Vec::new();
246        if let Some(ref id) = self.kernel_id {
247            pairs.push(("kernel_id", id.clone()));
248        }
249        if let Some(ref domain) = self.domain {
250            pairs.push(("domain", domain.clone()));
251        }
252        if let Some(ref tenant) = self.tenant_id {
253            pairs.push(("tenant_id", tenant.clone()));
254        }
255        for (k, v) in &self.extra {
256            pairs.push((k.as_str(), v.clone()));
257        }
258        pairs
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_default_config() {
268        let config = ObservabilityConfig::default();
269        assert_eq!(config.service_name, "rustkernels");
270        assert_eq!(config.environment, "development");
271        assert!(config.metrics.is_none());
272    }
273
274    #[test]
275    fn test_production_config() {
276        let config = ObservabilityConfig::production();
277        assert_eq!(config.environment, "production");
278        assert!(config.metrics.is_some());
279        assert!(config.tracing.is_some());
280    }
281
282    #[test]
283    fn test_builder() {
284        let config = ObservabilityConfig::builder()
285            .service_name("test-service")
286            .environment("testing")
287            .build();
288
289        assert_eq!(config.service_name, "test-service");
290        assert_eq!(config.environment, "testing");
291    }
292
293    #[test]
294    fn test_metric_labels() {
295        let labels = MetricLabels::new()
296            .with_kernel("graph/pagerank")
297            .with_domain("GraphAnalytics")
298            .with_tenant("tenant-123");
299
300        let pairs = labels.to_pairs();
301        assert_eq!(pairs.len(), 3);
302    }
303}