1use crate::error::{CloudError, Result};
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::time::SystemTime;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum LogLevel {
16 Trace,
18 Debug,
20 Info,
22 Warn,
24 Error,
26 Fatal,
28}
29
30impl LogLevel {
31 pub fn as_str(&self) -> &str {
33 match self {
34 LogLevel::Trace => "TRACE",
35 LogLevel::Debug => "DEBUG",
36 LogLevel::Info => "INFO",
37 LogLevel::Warn => "WARN",
38 LogLevel::Error => "ERROR",
39 LogLevel::Fatal => "FATAL",
40 }
41 }
42
43 pub fn to_severity(&self) -> u8 {
45 match self {
46 LogLevel::Trace => 0,
47 LogLevel::Debug => 1,
48 LogLevel::Info => 2,
49 LogLevel::Warn => 3,
50 LogLevel::Error => 4,
51 LogLevel::Fatal => 5,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct LogEntry {
59 pub timestamp: SystemTime,
61
62 pub level: LogLevel,
64
65 pub message: String,
67
68 pub labels: HashMap<String, String>,
70
71 pub trace_id: Option<String>,
73
74 pub span_id: Option<String>,
76
77 pub source: Option<String>,
79}
80
81impl LogEntry {
82 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
84 Self {
85 timestamp: SystemTime::now(),
86 level,
87 message: message.into(),
88 labels: HashMap::new(),
89 trace_id: None,
90 span_id: None,
91 source: None,
92 }
93 }
94
95 pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
97 self.labels.insert(key.into(), value.into());
98 self
99 }
100
101 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
103 self.trace_id = Some(trace_id.into());
104 self
105 }
106
107 pub fn with_span_id(mut self, span_id: impl Into<String>) -> Self {
109 self.span_id = Some(span_id.into());
110 self
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct Metric {
117 pub name: String,
119
120 pub value: f64,
122
123 pub timestamp: u64,
125
126 pub dimensions: HashMap<String, String>,
128
129 pub unit: Option<String>,
131}
132
133impl Metric {
134 pub fn new(name: impl Into<String>, value: f64) -> Self {
136 Self {
137 name: name.into(),
138 value,
139 timestamp: SystemTime::now()
140 .duration_since(std::time::UNIX_EPOCH)
141 .unwrap()
142 .as_secs(),
143 dimensions: HashMap::new(),
144 unit: None,
145 }
146 }
147
148 pub fn with_dimension(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
150 self.dimensions.insert(key.into(), value.into());
151 self
152 }
153
154 pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
156 self.unit = Some(unit.into());
157 self
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct Span {
164 pub name: String,
166
167 pub span_id: String,
169
170 pub trace_id: String,
172
173 pub parent_span_id: Option<String>,
175
176 pub start_time: SystemTime,
178
179 pub end_time: Option<SystemTime>,
181
182 pub attributes: HashMap<String, String>,
184
185 pub status: Option<String>,
187}
188
189impl Span {
190 pub fn new(name: impl Into<String>, trace_id: impl Into<String>) -> Self {
192 Self {
193 name: name.into(),
194 span_id: uuid::Uuid::new_v4().to_string(),
195 trace_id: trace_id.into(),
196 parent_span_id: None,
197 start_time: SystemTime::now(),
198 end_time: None,
199 attributes: HashMap::new(),
200 status: None,
201 }
202 }
203
204 pub fn with_parent(mut self, parent_span_id: impl Into<String>) -> Self {
206 self.parent_span_id = Some(parent_span_id.into());
207 self
208 }
209
210 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
212 self.attributes.insert(key.into(), value.into());
213 self
214 }
215
216 pub fn end_with_status(mut self, status: impl Into<String>) -> Self {
218 self.end_time = Some(SystemTime::now());
219 self.status = Some(status.into());
220 self
221 }
222
223 pub fn duration(&self) -> Option<std::time::Duration> {
225 self.end_time
226 .and_then(|end| end.duration_since(self.start_time).ok())
227 }
228}
229
230#[async_trait]
232pub trait CloudMetrics: Send + Sync {
233 async fn export_metrics(&self, metrics: &[Metric]) -> Result<()>;
243
244 async fn export_metric(&self, metric: &Metric) -> Result<()> {
254 self.export_metrics(&[metric.clone()]).await
255 }
256}
257
258#[async_trait]
260pub trait CloudLogger: Send + Sync {
261 async fn log(&self, message: &str, level: LogLevel) -> Result<()>;
272
273 async fn log_structured(&self, entry: &LogEntry) -> Result<()>;
283
284 async fn log_batch(&self, entries: &[LogEntry]) -> Result<()> {
294 for entry in entries {
296 self.log_structured(entry).await?;
297 }
298 Ok(())
299 }
300}
301
302#[async_trait]
304pub trait CloudTracer: Send + Sync {
305 fn start_span(&self, name: &str) -> Span {
315 Span::new(name, uuid::Uuid::new_v4().to_string())
316 }
317
318 fn start_child_span(&self, name: &str, parent: &Span) -> Span {
329 Span::new(name, parent.trace_id.clone())
330 .with_parent(parent.span_id.clone())
331 }
332
333 async fn end_span(&self, span: Span) -> Result<()>;
343
344 async fn export_spans(&self, spans: &[Span]) -> Result<()> {
354 for span in spans {
356 self.end_span(span.clone()).await?;
357 }
358 Ok(())
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_log_level_as_str() {
368 assert_eq!(LogLevel::Info.as_str(), "INFO");
369 assert_eq!(LogLevel::Error.as_str(), "ERROR");
370 }
371
372 #[test]
373 fn test_log_level_severity() {
374 assert!(LogLevel::Fatal.to_severity() > LogLevel::Error.to_severity());
375 assert!(LogLevel::Error.to_severity() > LogLevel::Warn.to_severity());
376 assert!(LogLevel::Warn.to_severity() > LogLevel::Info.to_severity());
377 }
378
379 #[test]
380 fn test_log_entry_builder() {
381 let entry = LogEntry::new(LogLevel::Info, "Test message")
382 .with_label("service", "llm-shield")
383 .with_trace_id("trace-123")
384 .with_span_id("span-456");
385
386 assert_eq!(entry.message, "Test message");
387 assert_eq!(entry.level, LogLevel::Info);
388 assert_eq!(entry.labels.get("service"), Some(&"llm-shield".to_string()));
389 assert_eq!(entry.trace_id, Some("trace-123".to_string()));
390 assert_eq!(entry.span_id, Some("span-456".to_string()));
391 }
392
393 #[test]
394 fn test_metric_builder() {
395 let metric = Metric::new("http_requests_total", 100.0)
396 .with_dimension("method", "POST")
397 .with_dimension("status", "200")
398 .with_unit("Count");
399
400 assert_eq!(metric.name, "http_requests_total");
401 assert_eq!(metric.value, 100.0);
402 assert_eq!(metric.dimensions.get("method"), Some(&"POST".to_string()));
403 assert_eq!(metric.unit, Some("Count".to_string()));
404 }
405
406 #[test]
407 fn test_span_creation() {
408 let span = Span::new("test_operation", "trace-abc")
409 .with_attribute("http.method", "GET")
410 .with_attribute("http.status_code", "200");
411
412 assert_eq!(span.name, "test_operation");
413 assert_eq!(span.trace_id, "trace-abc");
414 assert!(span.parent_span_id.is_none());
415 assert!(span.end_time.is_none());
416 assert_eq!(span.attributes.len(), 2);
417 }
418
419 #[test]
420 fn test_span_child() {
421 let parent = Span::new("parent", "trace-123");
422 let child = Span::new("child", parent.trace_id.clone())
423 .with_parent(parent.span_id.clone());
424
425 assert_eq!(child.trace_id, parent.trace_id);
426 assert_eq!(child.parent_span_id, Some(parent.span_id));
427 }
428
429 #[test]
430 fn test_span_duration() {
431 let span = Span::new("test", "trace-1");
432 assert!(span.duration().is_none());
433
434 let ended_span = span.end_with_status("OK");
435 assert!(ended_span.duration().is_some());
436 assert_eq!(ended_span.status, Some("OK".to_string()));
437 }
438}