1#![warn(missing_docs)]
9
10pub mod health;
11
12#[cfg(feature = "metrics")]
13pub mod metrics;
14
15#[cfg(feature = "otlp")]
16pub mod tracing;
17
18#[cfg(feature = "profiling")]
19pub mod profiling;
20
21#[cfg(feature = "json-log")]
22pub mod logging;
23
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use wae_types::WaeError;
27
28#[cfg(feature = "json-log")]
29use ::tracing::Level;
30
31#[cfg(any(feature = "metrics", feature = "health", feature = "profiling"))]
32use std::sync::Arc;
33
34pub type ObservabilityResult<T> = Result<T, WaeError>;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
39pub enum HealthStatus {
40 #[default]
42 Passing,
43 Warning,
45 Critical,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HealthCheck {
52 pub name: String,
54 pub status: HealthStatus,
56 pub details: Option<String>,
58 pub timestamp: i64,
60}
61
62impl HealthCheck {
63 pub fn new(name: String, status: HealthStatus) -> Self {
65 Self { name, status, details: None, timestamp: chrono::Utc::now().timestamp() }
66 }
67
68 pub fn with_details(mut self, details: String) -> Self {
70 self.details = Some(details);
71 self
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct HealthReport {
78 pub overall: HealthStatus,
80 pub checks: Vec<HealthCheck>,
82}
83
84impl HealthReport {
85 pub fn new(checks: Vec<HealthCheck>) -> Self {
87 let overall = checks.iter().fold(HealthStatus::Passing, |acc, check| match (acc, check.status) {
88 (HealthStatus::Critical, _) | (_, HealthStatus::Critical) => HealthStatus::Critical,
89 (HealthStatus::Warning, _) | (_, HealthStatus::Warning) => HealthStatus::Warning,
90 _ => HealthStatus::Passing,
91 });
92 Self { overall, checks }
93 }
94}
95
96#[async_trait::async_trait]
98pub trait HealthChecker: Send + Sync {
99 async fn check(&self) -> HealthCheck;
101}
102
103#[cfg(feature = "json-log")]
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum LogOutput {
107 #[default]
109 Stdout,
110 Stderr,
112 Test,
114}
115
116#[cfg(feature = "json-log")]
118#[derive(Debug, Clone)]
119pub struct JsonLogConfig {
120 pub level: Level,
122 pub output: LogOutput,
124 pub include_target: bool,
126 pub include_timestamp: bool,
128}
129
130#[cfg(feature = "json-log")]
131impl Default for JsonLogConfig {
132 fn default() -> Self {
133 Self { level: Level::INFO, output: LogOutput::Stdout, include_target: true, include_timestamp: true }
134 }
135}
136
137#[cfg(feature = "otlp")]
139#[derive(Debug, Clone)]
140pub struct OtlpConfig {
141 pub endpoint: String,
143 pub service_name: String,
145 pub service_version: String,
147 pub sample_ratio: f64,
149 pub enabled: bool,
151}
152
153#[cfg(feature = "otlp")]
154impl Default for OtlpConfig {
155 fn default() -> Self {
156 Self {
157 endpoint: "http://localhost:4317".to_string(),
158 service_name: "wae-service".to_string(),
159 service_version: "0.1.0".to_string(),
160 sample_ratio: 1.0,
161 enabled: false,
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct ObservabilityConfig {
169 pub service_name: String,
171 pub service_version: String,
173 pub enable_health: bool,
175 pub enable_metrics: bool,
177 pub enable_json_log: bool,
179 pub enable_otlp: bool,
181 pub enable_profiling: bool,
183 #[cfg(feature = "json-log")]
185 pub json_log_config: JsonLogConfig,
186 #[cfg(feature = "otlp")]
188 pub otlp_config: OtlpConfig,
189 #[cfg(feature = "profiling")]
191 pub profiling_config: profiling::ConsoleConfig,
192}
193
194impl Default for ObservabilityConfig {
195 fn default() -> Self {
196 Self {
197 service_name: "wae-service".to_string(),
198 service_version: "0.1.0".to_string(),
199 enable_health: true,
200 enable_metrics: false,
201 enable_json_log: false,
202 enable_otlp: false,
203 enable_profiling: false,
204 #[cfg(feature = "json-log")]
205 json_log_config: JsonLogConfig::default(),
206 #[cfg(feature = "otlp")]
207 otlp_config: OtlpConfig::default(),
208 #[cfg(feature = "profiling")]
209 profiling_config: profiling::ConsoleConfig::default(),
210 }
211 }
212}
213
214impl ObservabilityConfig {
215 pub fn new(service_name: String, service_version: String) -> Self {
217 Self { service_name, service_version, ..Default::default() }
218 }
219
220 pub fn with_health(mut self) -> Self {
222 self.enable_health = true;
223 self
224 }
225
226 #[cfg(feature = "metrics")]
228 pub fn with_metrics(mut self) -> Self {
229 self.enable_metrics = true;
230 self
231 }
232
233 #[cfg(feature = "json-log")]
235 pub fn with_json_log(mut self) -> Self {
236 self.enable_json_log = true;
237 self
238 }
239
240 #[cfg(feature = "otlp")]
242 pub fn with_otlp(mut self) -> Self {
243 self.enable_otlp = true;
244 self
245 }
246
247 #[cfg(feature = "profiling")]
249 pub fn with_profiling(mut self) -> Self {
250 self.enable_profiling = true;
251 self
252 }
253
254 #[cfg(feature = "json-log")]
256 pub fn with_json_log_config(mut self, config: JsonLogConfig) -> Self {
257 self.json_log_config = config;
258 self
259 }
260
261 #[cfg(feature = "otlp")]
263 pub fn with_otlp_config(mut self, config: OtlpConfig) -> Self {
264 self.otlp_config = config;
265 self
266 }
267
268 #[cfg(feature = "profiling")]
270 pub fn with_profiling_config(mut self, config: profiling::ConsoleConfig) -> Self {
271 self.profiling_config = config;
272 self
273 }
274}
275
276pub struct ObservabilityService {
278 config: ObservabilityConfig,
280 health_checkers: Vec<Box<dyn HealthChecker>>,
282 #[cfg(feature = "health")]
284 health_registry: Option<Arc<health::HealthRegistry>>,
285 #[cfg(feature = "metrics")]
287 metrics_registry: Option<Arc<metrics::MetricsRegistry>>,
288}
289
290impl ObservabilityService {
291 pub fn new(config: ObservabilityConfig) -> Self {
293 Self {
294 config,
295 health_checkers: Vec::new(),
296 #[cfg(feature = "health")]
297 health_registry: None,
298 #[cfg(feature = "metrics")]
299 metrics_registry: None,
300 }
301 }
302
303 pub fn add_health_checker(&mut self, checker: Box<dyn HealthChecker>) {
305 self.health_checkers.push(checker);
306 }
307
308 pub async fn health_check(&self) -> ObservabilityResult<HealthReport> {
310 let mut checks = Vec::with_capacity(self.health_checkers.len());
311 for checker in &self.health_checkers {
312 checks.push(checker.check().await);
313 }
314 Ok(HealthReport::new(checks))
315 }
316
317 pub fn config(&self) -> &ObservabilityConfig {
319 &self.config
320 }
321
322 #[cfg(feature = "health")]
324 pub fn health_registry(&self) -> Option<&Arc<health::HealthRegistry>> {
325 self.health_registry.as_ref()
326 }
327
328 #[cfg(feature = "metrics")]
330 pub fn metrics_registry(&self) -> Option<&Arc<metrics::MetricsRegistry>> {
331 self.metrics_registry.as_ref()
332 }
333}
334
335pub fn init_observability(config: ObservabilityConfig) -> ObservabilityResult<ObservabilityService> {
347 #[cfg(feature = "json-log")]
348 let _ = config.enable_json_log;
349
350 #[cfg(feature = "otlp")]
351 let _ = config.enable_otlp;
352
353 #[cfg(feature = "profiling")]
354 if config.enable_profiling {
355 let console = profiling::TokioConsole::new();
356 console.init(&config.profiling_config);
357 }
358
359 #[allow(unused_mut)]
360 let mut service = ObservabilityService::new(config);
361
362 #[cfg(feature = "health")]
363 {
364 let registry = Arc::new(health::HealthRegistry::new());
365 service.health_registry = Some(registry);
366 }
367
368 #[cfg(feature = "metrics")]
369 if service.config.enable_metrics {
370 let registry = Arc::new(metrics::MetricsRegistry::new());
371 service.metrics_registry = Some(registry);
372 }
373
374 Ok(service)
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct MetricLabels {
380 labels: HashMap<String, String>,
382}
383
384impl MetricLabels {
385 pub fn new() -> Self {
387 Self { labels: HashMap::new() }
388 }
389
390 pub fn insert(&mut self, key: String, value: String) {
392 self.labels.insert(key, value);
393 }
394
395 pub fn get(&self, key: &str) -> Option<&String> {
397 self.labels.get(key)
398 }
399}
400
401impl Default for MetricLabels {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407pub struct HttpHealthChecker {
409 name: String,
411 url: String,
413}
414
415impl HttpHealthChecker {
416 pub fn new(name: String, url: String) -> Self {
418 Self { name, url }
419 }
420}
421
422#[async_trait::async_trait]
423impl HealthChecker for HttpHealthChecker {
424 async fn check(&self) -> HealthCheck {
425 HealthCheck::new(self.name.clone(), HealthStatus::Passing).with_details(format!("HTTP check for {}", self.url))
426 }
427}