Skip to main content

fraiseql_server/observability/
logging.rs

1//! Structured logging with trace context
2//!
3//! Provides JSON-formatted logs with trace ID correlation
4
5use std::collections::HashMap;
6
7/// Initialize structured logging
8pub fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
9    // Initialize tracing-subscriber with JSON formatting
10    // In GREEN phase, this is a no-op
11    Ok(())
12}
13
14/// Log level
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
16pub enum LogLevel {
17    /// Debug level
18    Debug = 0,
19    /// Info level
20    Info  = 1,
21    /// Warning level
22    Warn  = 2,
23    /// Error level
24    Error = 3,
25}
26
27impl LogLevel {
28    /// Get string representation
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            Self::Debug => "DEBUG",
32            Self::Info => "INFO",
33            Self::Warn => "WARN",
34            Self::Error => "ERROR",
35        }
36    }
37}
38
39/// Structured log entry
40#[derive(Clone, Debug)]
41pub struct LogEntry {
42    /// Timestamp
43    pub timestamp: String,
44    /// Log level
45    pub level:     LogLevel,
46    /// Log message
47    pub message:   String,
48    /// Trace ID
49    pub trace_id:  Option<String>,
50    /// Span ID
51    pub span_id:   Option<String>,
52    /// Additional fields
53    pub fields:    HashMap<String, String>,
54}
55
56impl LogEntry {
57    /// Create a new log entry
58    pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
59        Self {
60            timestamp: chrono::Utc::now().to_rfc3339(),
61            level,
62            message: message.into(),
63            trace_id: None,
64            span_id: None,
65            fields: HashMap::new(),
66        }
67    }
68
69    /// Set trace ID
70    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
71        self.trace_id = Some(trace_id.into());
72        self
73    }
74
75    /// Set span ID
76    pub fn with_span_id(mut self, span_id: impl Into<String>) -> Self {
77        self.span_id = Some(span_id.into());
78        self
79    }
80
81    /// Add a field
82    pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
83        self.fields.insert(key.into(), value.into());
84        self
85    }
86
87    /// Format as JSON
88    pub fn as_json(&self) -> Result<serde_json::Value, serde_json::Error> {
89        let mut json = serde_json::json!({
90            "timestamp": self.timestamp,
91            "level": self.level.as_str(),
92            "message": self.message,
93        });
94
95        if let Some(ref trace_id) = self.trace_id {
96            json["trace_id"] = serde_json::Value::String(trace_id.clone());
97        }
98
99        if let Some(ref span_id) = self.span_id {
100            json["span_id"] = serde_json::Value::String(span_id.clone());
101        }
102
103        for (key, value) in &self.fields {
104            json[key] = serde_json::Value::String(value.clone());
105        }
106
107        Ok(json)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_log_entry() {
117        let entry = LogEntry::new(LogLevel::Info, "Test message")
118            .with_trace_id("trace-123")
119            .with_span_id("span-456")
120            .with_field("user_id", "user-123");
121
122        assert_eq!(entry.level, LogLevel::Info);
123        assert_eq!(entry.message, "Test message");
124        assert_eq!(entry.trace_id, Some("trace-123".to_string()));
125        assert_eq!(entry.fields.len(), 1);
126    }
127
128    #[test]
129    fn test_log_levels() {
130        assert!(LogLevel::Debug < LogLevel::Info);
131        assert!(LogLevel::Info < LogLevel::Warn);
132        assert!(LogLevel::Warn < LogLevel::Error);
133    }
134}