rustkernel_core/observability/
mod.rs1pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ObservabilityConfig {
46 pub metrics: Option<MetricsConfig>,
48 pub tracing: Option<TracingConfig>,
50 pub logging: LogConfig,
52 pub alerting: Option<AlertConfig>,
54 pub service_name: String,
56 pub service_version: String,
58 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 pub fn builder() -> ObservabilityConfigBuilder {
79 ObservabilityConfigBuilder::default()
80 }
81
82 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 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 #[cfg(feature = "metrics")]
113 pub async fn init(&self) -> crate::error::Result<()> {
114 self.logging.init()?;
116
117 if let Some(ref metrics) = self.metrics {
119 metrics.init().await?;
120 }
121
122 if let Some(ref tracing) = self.tracing {
124 tracing.init().await?;
125 }
126
127 Ok(())
128 }
129
130 #[cfg(not(feature = "metrics"))]
132 pub async fn init(&self) -> crate::error::Result<()> {
133 self.logging.init()?;
134 Ok(())
135 }
136}
137
138#[derive(Default)]
140pub struct ObservabilityConfigBuilder {
141 config: ObservabilityConfig,
142}
143
144impl ObservabilityConfigBuilder {
145 pub fn with_metrics(mut self, config: MetricsConfig) -> Self {
147 self.config.metrics = Some(config);
148 self
149 }
150
151 pub fn with_tracing(mut self, config: TracingConfig) -> Self {
153 self.config.tracing = Some(config);
154 self
155 }
156
157 pub fn with_logging(mut self, config: LogConfig) -> Self {
159 self.config.logging = config;
160 self
161 }
162
163 pub fn with_alerting(mut self, config: AlertConfig) -> Self {
165 self.config.alerting = Some(config);
166 self
167 }
168
169 pub fn service_name(mut self, name: impl Into<String>) -> Self {
171 self.config.service_name = name.into();
172 self
173 }
174
175 pub fn service_version(mut self, version: impl Into<String>) -> Self {
177 self.config.service_version = version.into();
178 self
179 }
180
181 pub fn environment(mut self, env: impl Into<String>) -> Self {
183 self.config.environment = env.into();
184 self
185 }
186
187 pub fn build(self) -> ObservabilityConfig {
189 self.config
190 }
191}
192
193#[derive(Debug, Clone, Default)]
195pub struct MetricLabels {
196 pub kernel_id: Option<String>,
198 pub domain: Option<String>,
200 pub tenant_id: Option<String>,
202 pub extra: std::collections::HashMap<String, String>,
204}
205
206impl MetricLabels {
207 pub fn new() -> Self {
209 Self::default()
210 }
211
212 pub fn with_kernel(mut self, id: impl Into<String>) -> Self {
214 self.kernel_id = Some(id.into());
215 self
216 }
217
218 pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
220 self.domain = Some(domain.into());
221 self
222 }
223
224 pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
226 self.tenant_id = Some(tenant.into());
227 self
228 }
229
230 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 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}