1use crate::error::ObservabilityResult;
4use std::collections::HashMap;
5use std::sync::Arc;
6use web_time::{Duration, Instant};
7
8#[cfg(feature = "structured-logging")]
9use serde_json::Value as JsonValue;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13#[cfg_attr(
14 feature = "structured-logging",
15 derive(serde::Serialize, serde::Deserialize)
16)]
17pub enum LogLevel {
18 Error = 0,
19 Warn = 1,
20 Info = 2,
21 Debug = 3,
22 Trace = 4,
23}
24
25impl LogLevel {
26 pub fn as_str(&self) -> &'static str {
27 match self {
28 LogLevel::Error => "ERROR",
29 LogLevel::Warn => "WARN",
30 LogLevel::Info => "INFO",
31 LogLevel::Debug => "DEBUG",
32 LogLevel::Trace => "TRACE",
33 }
34 }
35}
36
37pub struct SpanGuard {
39 span_id: String,
40 start_time: Instant,
41 plugin: Option<Arc<dyn ObservabilityPlugin>>,
42}
43
44impl SpanGuard {
45 pub fn new(span_id: String, plugin: Arc<dyn ObservabilityPlugin>) -> Self {
46 Self {
47 span_id,
48 start_time: Instant::now(),
49 plugin: Some(plugin),
50 }
51 }
52
53 pub fn no_op() -> Self {
54 Self {
55 span_id: String::new(),
56 start_time: Instant::now(),
57 plugin: None,
58 }
59 }
60
61 pub fn span_id(&self) -> &str {
62 &self.span_id
63 }
64
65 pub fn duration(&self) -> Duration {
66 self.start_time.elapsed()
67 }
68
69 pub fn add_attribute(&self, key: &str, value: &str) {
70 if let Some(plugin) = &self.plugin {
71 plugin.add_span_attribute(&self.span_id, key, value);
72 }
73 }
74
75 pub fn set_status(&self, status: SpanStatus) {
76 if let Some(plugin) = &self.plugin {
77 plugin.set_span_status(&self.span_id, status);
78 }
79 }
80}
81
82impl Drop for SpanGuard {
83 fn drop(&mut self) {
84 if let Some(plugin) = self.plugin.take() {
85 plugin.end_span(&self.span_id);
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy)]
92pub enum SpanStatus {
93 Ok,
94 Error,
95 Cancelled,
96}
97
98pub trait ObservabilityPlugin: Send + Sync {
100 fn start_span(&self, name: &str, attributes: &[(&str, &str)]) -> SpanGuard;
102
103 fn end_span(&self, span_id: &str);
105
106 fn add_span_attribute(&self, span_id: &str, key: &str, value: &str);
108
109 fn set_span_status(&self, span_id: &str, status: SpanStatus);
111
112 fn record_metric(&self, name: &str, value: f64, labels: &[(&str, &str)]);
114
115 fn increment_counter(&self, name: &str, labels: &[(&str, &str)]) {
117 self.record_metric(name, 1.0, labels);
118 }
119
120 fn record_histogram(&self, name: &str, value: f64, labels: &[(&str, &str)]) {
122 self.record_metric(name, value, labels);
123 }
124
125 #[cfg(feature = "structured-logging")]
127 fn log_structured(&self, level: LogLevel, message: &str, fields: &JsonValue);
128
129 fn log(&self, level: LogLevel, message: &str) {
131 #[cfg(feature = "structured-logging")]
132 self.log_structured(level, message, &serde_json::json!({}));
133
134 #[cfg(not(feature = "structured-logging"))]
135 {
136 let level_str = level.as_str();
138 let output = format!("[{}] {}", level_str, message);
139 self.write_log(&output);
140 }
141 }
142
143 fn write_log(&self, message: &str);
145
146 fn flush(&self) -> ObservabilityResult<()>;
148
149 fn is_enabled(&self) -> bool {
151 true
152 }
153
154 fn plugin_type(&self) -> &'static str {
156 "generic"
157 }
158}
159
160pub trait MetricsCollector: Send + Sync {
162 fn register_counter(
164 &mut self,
165 name: &str,
166 description: &str,
167 labels: &[&str],
168 ) -> ObservabilityResult<()>;
169
170 fn register_histogram(
172 &mut self,
173 name: &str,
174 description: &str,
175 buckets: &[f64],
176 labels: &[&str],
177 ) -> ObservabilityResult<()>;
178
179 fn register_gauge(
181 &mut self,
182 name: &str,
183 description: &str,
184 labels: &[&str],
185 ) -> ObservabilityResult<()>;
186
187 fn record_counter(
189 &self,
190 name: &str,
191 value: f64,
192 labels: &HashMap<String, String>,
193 ) -> ObservabilityResult<()>;
194
195 fn record_histogram(
197 &self,
198 name: &str,
199 value: f64,
200 labels: &HashMap<String, String>,
201 ) -> ObservabilityResult<()>;
202
203 fn set_gauge(
205 &self,
206 name: &str,
207 value: f64,
208 labels: &HashMap<String, String>,
209 ) -> ObservabilityResult<()>;
210
211 fn get_metrics(&self) -> HashMap<String, f64>;
213
214 fn clear(&mut self);
216}
217
218#[cfg(feature = "structured-logging")]
220pub trait StructuredLogger: Send + Sync {
221 fn log_with_trace(
223 &self,
224 level: LogLevel,
225 message: &str,
226 fields: &JsonValue,
227 trace_id: Option<&str>,
228 span_id: Option<&str>,
229 );
230
231 fn log_performance(
233 &self,
234 operation: &str,
235 duration: Duration,
236 success: bool,
237 additional_fields: &JsonValue,
238 );
239
240 fn log_error(&self, error: &dyn std::error::Error, context: &JsonValue);
242
243 fn set_level(&mut self, level: LogLevel);
245
246 fn is_level_enabled(&self, level: LogLevel) -> bool;
248}
249
250pub trait ObservabilityBuilder {
252 type Plugin: ObservabilityPlugin;
253
254 fn build(self) -> ObservabilityResult<Self::Plugin>;
256
257 fn with_name(self, name: impl Into<String>) -> Self;
259
260 fn enabled(self, enabled: bool) -> Self;
262}
263
264pub trait BatchingSupport {
266 fn batch_size(&self) -> usize;
268
269 fn set_batch_size(&mut self, size: usize);
271
272 fn flush_interval(&self) -> Duration;
274
275 fn set_flush_interval(&mut self, interval: Duration);
277
278 fn force_flush(&self) -> ObservabilityResult<()>;
280}
281
282pub const METRIC_LABEL_ALLOWLIST: &[&str] = &[
286 "app", "version", "namespace", "component", "operation", "status", "provider", "model", "direction", ];
299
300pub fn create_labels(pairs: &[(&str, &str)]) -> HashMap<String, String> {
302 pairs
303 .iter()
304 .map(|(k, v)| (k.to_string(), v.to_string()))
305 .collect()
306}
307
308pub fn validate_metric_label_allowlist(label_keys: &[&str]) -> bool {
310 label_keys
311 .iter()
312 .all(|label| METRIC_LABEL_ALLOWLIST.contains(label))
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use std::sync::Mutex;
319 use std::sync::atomic::{AtomicUsize, Ordering};
320
321 #[derive(Default)]
322 struct MockPlugin {
323 end_calls: AtomicUsize,
324 attrs: Mutex<Vec<(String, String, String)>>,
325 statuses: Mutex<Vec<(String, SpanStatus)>>,
326 }
327
328 impl ObservabilityPlugin for MockPlugin {
329 fn start_span(&self, _name: &str, _attributes: &[(&str, &str)]) -> SpanGuard {
330 SpanGuard::no_op()
332 }
333
334 fn end_span(&self, span_id: &str) {
335 self.end_calls.fetch_add(1, Ordering::SeqCst);
336 assert!(!span_id.is_empty());
338 }
339
340 fn add_span_attribute(&self, span_id: &str, key: &str, value: &str) {
341 self.attrs.lock().unwrap().push((
342 span_id.to_string(),
343 key.to_string(),
344 value.to_string(),
345 ));
346 }
347
348 fn set_span_status(&self, span_id: &str, status: SpanStatus) {
349 self.statuses
350 .lock()
351 .unwrap()
352 .push((span_id.to_string(), status));
353 }
354
355 fn record_metric(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {}
356
357 #[cfg(feature = "structured-logging")]
358 fn log_structured(&self, _level: LogLevel, _message: &str, _fields: &JsonValue) {}
359
360 fn write_log(&self, _message: &str) {}
361
362 fn flush(&self) -> ObservabilityResult<()> {
363 Ok(())
364 }
365 }
366
367 #[test]
368 fn span_guard_drop_calls_end_span_once() {
369 let typed = Arc::new(MockPlugin::default());
370 let plugin: Arc<dyn ObservabilityPlugin> = typed.clone();
371
372 {
373 let g = SpanGuard::new("span-1".to_string(), plugin);
374 g.add_attribute("k", "v");
375 g.set_status(SpanStatus::Error);
376 } assert_eq!(typed.end_calls.load(Ordering::SeqCst), 1);
379
380 let attrs = typed.attrs.lock().unwrap().clone();
381 assert_eq!(
382 attrs,
383 vec![("span-1".to_string(), "k".to_string(), "v".to_string())]
384 );
385
386 let statuses = typed.statuses.lock().unwrap().clone();
387 assert_eq!(statuses.len(), 1);
388 assert_eq!(statuses[0].0, "span-1");
389 assert!(matches!(statuses[0].1, SpanStatus::Error));
390 }
391
392 #[test]
393 fn span_guard_no_op_is_safe() {
394 let g = SpanGuard::no_op();
396 g.add_attribute("k", "v");
397 g.set_status(SpanStatus::Error);
398 drop(g);
399 }
400}