forge_core/observability/
log.rs1use std::collections::HashMap;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum LogLevel {
10 Trace,
12 Debug,
14 #[default]
16 Info,
17 Warn,
19 Error,
21}
22
23#[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#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct LogEntry {
65 pub level: LogLevel,
67 pub message: String,
69 pub target: Option<String>,
71 pub fields: HashMap<String, serde_json::Value>,
73 pub timestamp: chrono::DateTime<chrono::Utc>,
75 pub trace_id: Option<String>,
77 pub span_id: Option<String>,
79 pub node_id: Option<uuid::Uuid>,
81}
82
83impl LogEntry {
84 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 pub fn trace(message: impl Into<String>) -> Self {
100 Self::new(LogLevel::Trace, message)
101 }
102
103 pub fn debug(message: impl Into<String>) -> Self {
105 Self::new(LogLevel::Debug, message)
106 }
107
108 pub fn info(message: impl Into<String>) -> Self {
110 Self::new(LogLevel::Info, message)
111 }
112
113 pub fn warn(message: impl Into<String>) -> Self {
115 Self::new(LogLevel::Warn, message)
116 }
117
118 pub fn error(message: impl Into<String>) -> Self {
120 Self::new(LogLevel::Error, message)
121 }
122
123 pub fn with_target(mut self, target: impl Into<String>) -> Self {
125 self.target = Some(target.into());
126 self
127 }
128
129 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 pub fn with_fields(mut self, fields: HashMap<String, serde_json::Value>) -> Self {
139 self.fields.extend(fields);
140 self
141 }
142
143 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 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 pub fn with_node_id(mut self, node_id: uuid::Uuid) -> Self {
157 self.node_id = Some(node_id);
158 self
159 }
160
161 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}