Skip to main content

forge_core/observability/
log.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6/// Log level.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum LogLevel {
10    /// Trace level (most verbose).
11    Trace,
12    /// Debug level.
13    Debug,
14    /// Info level.
15    #[default]
16    Info,
17    /// Warning level.
18    Warn,
19    /// Error level.
20    Error,
21}
22
23/// Error for parsing LogLevel from string.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ParseLogLevelError(pub String);
26
27impl std::fmt::Display for ParseLogLevelError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "invalid log level: {}", self.0)
30    }
31}
32
33impl std::error::Error for ParseLogLevelError {}
34
35impl FromStr for LogLevel {
36    type Err = ParseLogLevelError;
37
38    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
39        match s.to_lowercase().as_str() {
40            "trace" => Ok(Self::Trace),
41            "debug" => Ok(Self::Debug),
42            "info" => Ok(Self::Info),
43            "warn" | "warning" => Ok(Self::Warn),
44            "error" => Ok(Self::Error),
45            _ => Err(ParseLogLevelError(s.to_string())),
46        }
47    }
48}
49
50impl std::fmt::Display for LogLevel {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Self::Trace => write!(f, "trace"),
54            Self::Debug => write!(f, "debug"),
55            Self::Info => write!(f, "info"),
56            Self::Warn => write!(f, "warn"),
57            Self::Error => write!(f, "error"),
58        }
59    }
60}
61
62/// A structured log entry.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct LogEntry {
65    /// Log level.
66    pub level: LogLevel,
67    /// Log message.
68    pub message: String,
69    /// Target (module path).
70    pub target: Option<String>,
71    /// Structured fields.
72    pub fields: HashMap<String, serde_json::Value>,
73    /// Timestamp.
74    pub timestamp: chrono::DateTime<chrono::Utc>,
75    /// Trace ID if part of a trace.
76    pub trace_id: Option<String>,
77    /// Span ID if part of a span.
78    pub span_id: Option<String>,
79    /// Node ID that generated this log.
80    pub node_id: Option<uuid::Uuid>,
81}
82
83impl LogEntry {
84    /// Create a new log entry.
85    pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
86        Self {
87            level,
88            message: message.into(),
89            target: None,
90            fields: HashMap::new(),
91            timestamp: chrono::Utc::now(),
92            trace_id: None,
93            span_id: None,
94            node_id: None,
95        }
96    }
97
98    /// Create a trace log.
99    pub fn trace(message: impl Into<String>) -> Self {
100        Self::new(LogLevel::Trace, message)
101    }
102
103    /// Create a debug log.
104    pub fn debug(message: impl Into<String>) -> Self {
105        Self::new(LogLevel::Debug, message)
106    }
107
108    /// Create an info log.
109    pub fn info(message: impl Into<String>) -> Self {
110        Self::new(LogLevel::Info, message)
111    }
112
113    /// Create a warn log.
114    pub fn warn(message: impl Into<String>) -> Self {
115        Self::new(LogLevel::Warn, message)
116    }
117
118    /// Create an error log.
119    pub fn error(message: impl Into<String>) -> Self {
120        Self::new(LogLevel::Error, message)
121    }
122
123    /// Set the target.
124    pub fn with_target(mut self, target: impl Into<String>) -> Self {
125        self.target = Some(target.into());
126        self
127    }
128
129    /// Add a field.
130    pub fn with_field(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
131        if let Ok(v) = serde_json::to_value(value) {
132            self.fields.insert(key.into(), v);
133        }
134        self
135    }
136
137    /// Add multiple fields.
138    pub fn with_fields(mut self, fields: HashMap<String, serde_json::Value>) -> Self {
139        self.fields.extend(fields);
140        self
141    }
142
143    /// Set the trace ID.
144    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
145        self.trace_id = Some(trace_id.into());
146        self
147    }
148
149    /// Set the span ID.
150    pub fn with_span_id(mut self, span_id: impl Into<String>) -> Self {
151        self.span_id = Some(span_id.into());
152        self
153    }
154
155    /// Set the node ID.
156    pub fn with_node_id(mut self, node_id: uuid::Uuid) -> Self {
157        self.node_id = Some(node_id);
158        self
159    }
160
161    /// Check if this log matches a minimum level filter.
162    pub fn matches_level(&self, min_level: LogLevel) -> bool {
163        self.level >= min_level
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_log_level_from_str() {
173        assert_eq!("info".parse::<LogLevel>(), Ok(LogLevel::Info));
174        assert_eq!("WARNING".parse::<LogLevel>(), Ok(LogLevel::Warn));
175        assert_eq!("warn".parse::<LogLevel>(), Ok(LogLevel::Warn));
176        assert!("unknown".parse::<LogLevel>().is_err());
177    }
178
179    #[test]
180    fn test_log_level_ordering() {
181        assert!(LogLevel::Trace < LogLevel::Debug);
182        assert!(LogLevel::Debug < LogLevel::Info);
183        assert!(LogLevel::Info < LogLevel::Warn);
184        assert!(LogLevel::Warn < LogLevel::Error);
185    }
186
187    #[test]
188    fn test_log_entry_creation() {
189        let entry = LogEntry::info("Request processed")
190            .with_target("forge::gateway")
191            .with_field("duration_ms", 42)
192            .with_field("status", 200);
193
194        assert_eq!(entry.level, LogLevel::Info);
195        assert_eq!(entry.message, "Request processed");
196        assert_eq!(entry.target, Some("forge::gateway".to_string()));
197        assert_eq!(
198            entry.fields.get("duration_ms"),
199            Some(&serde_json::json!(42))
200        );
201    }
202
203    #[test]
204    fn test_log_level_filter() {
205        let debug_log = LogEntry::debug("Debug message");
206        let info_log = LogEntry::info("Info message");
207        let error_log = LogEntry::error("Error message");
208
209        assert!(!debug_log.matches_level(LogLevel::Info));
210        assert!(info_log.matches_level(LogLevel::Info));
211        assert!(error_log.matches_level(LogLevel::Info));
212    }
213}