ant_quic/logging/
structured.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8use serde::{Deserialize, Serialize};
9use tracing::Level;
10
11use crate::ConnectionId;
12
13/// Structured log event with full metadata
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct StructuredLogEvent {
16    /// Timestamp in microseconds since epoch
17    pub timestamp: u64,
18    /// Log severity level
19    pub level: LogLevel,
20    /// Logical target of the log (module or subsystem)
21    pub target: String,
22    /// Human-readable message
23    pub message: String,
24    /// Structured key/value fields attached to the record
25    pub fields: Vec<(String, String)>,
26    /// Optional span identifier
27    pub span_id: Option<String>,
28    /// Optional trace identifier
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub trace_id: Option<String>,
31    /// Optional connection identifier
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub connection_id: Option<String>,
34}
35
36/// Serializable log level
37#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
38pub enum LogLevel {
39    /// Error conditions
40    ERROR,
41    /// Potential problems
42    WARN,
43    /// Informational messages
44    INFO,
45    /// Debug-level diagnostics
46    DEBUG,
47    /// Verbose tracing
48    TRACE,
49}
50
51impl From<Level> for LogLevel {
52    fn from(level: Level) -> Self {
53        match level {
54            Level::ERROR => Self::ERROR,
55            Level::WARN => Self::WARN,
56            Level::INFO => Self::INFO,
57            Level::DEBUG => Self::DEBUG,
58            Level::TRACE => Self::TRACE,
59        }
60    }
61}
62
63impl StructuredLogEvent {
64    /// Create a new structured log event
65    pub fn new(level: Level, target: impl Into<String>, message: impl Into<String>) -> Self {
66        Self {
67            timestamp: crate::tracing::timestamp_now(),
68            level: level.into(),
69            target: target.into(),
70            message: message.into(),
71            fields: Vec::new(),
72            span_id: None,
73            trace_id: None,
74            connection_id: None,
75        }
76    }
77
78    /// Add a field to the event
79    pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80        self.fields.push((key.into(), value.into()));
81        self
82    }
83
84    /// Add multiple fields
85    pub fn with_fields(mut self, fields: Vec<(String, String)>) -> Self {
86        self.fields.extend(fields);
87        self
88    }
89
90    /// Set the span ID
91    pub fn with_span_id(mut self, span_id: impl Into<String>) -> Self {
92        self.span_id = Some(span_id.into());
93        self
94    }
95
96    /// Set the trace ID
97    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
98        self.trace_id = Some(trace_id.into());
99        self
100    }
101
102    /// Set the connection ID
103    pub fn with_connection_id(mut self, conn_id: &ConnectionId) -> Self {
104        self.connection_id = Some(format!("{conn_id:?}"));
105        self
106    }
107
108    /// Convert to JSON
109    pub fn to_json(&self) -> Result<String, serde_json::Error> {
110        serde_json::to_string(self)
111    }
112
113    /// Convert to pretty JSON
114    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
115        serde_json::to_string_pretty(self)
116    }
117}
118
119/// Builder for structured events
120pub struct StructuredEventBuilder {
121    event: StructuredLogEvent,
122}
123
124impl StructuredEventBuilder {
125    /// Create a new builder
126    pub fn new(level: Level, target: &str, message: &str) -> Self {
127        Self {
128            event: StructuredLogEvent::new(level, target, message),
129        }
130    }
131
132    /// Add a string field
133    pub fn field(mut self, key: &str, value: &str) -> Self {
134        self.event = self.event.with_field(key, value);
135        self
136    }
137
138    /// Add a numeric field
139    pub fn field_num<T: std::fmt::Display>(mut self, key: &str, value: T) -> Self {
140        self.event = self.event.with_field(key, value.to_string());
141        self
142    }
143
144    /// Add a boolean field
145    pub fn field_bool(mut self, key: &str, value: bool) -> Self {
146        self.event = self.event.with_field(key, value.to_string());
147        self
148    }
149
150    /// Add an optional field
151    pub fn field_opt<T: std::fmt::Display>(mut self, key: &str, value: Option<T>) -> Self {
152        if let Some(v) = value {
153            self.event = self.event.with_field(key, v.to_string());
154        }
155        self
156    }
157
158    /// Set connection ID
159    pub fn connection_id(mut self, conn_id: &ConnectionId) -> Self {
160        self.event = self.event.with_connection_id(conn_id);
161        self
162    }
163
164    /// Set span ID
165    pub fn span_id(mut self, span_id: &str) -> Self {
166        self.event = self.event.with_span_id(span_id);
167        self
168    }
169
170    /// Build the event
171    pub fn build(self) -> StructuredLogEvent {
172        self.event
173    }
174}
175
176/// Format a structured event as JSON
177#[allow(dead_code)]
178pub(super) fn format_as_json(event: &super::LogEvent) -> String {
179    let structured = StructuredLogEvent {
180        timestamp: crate::tracing::timestamp_now(),
181        level: event.level.into(),
182        target: event.target.clone(),
183        message: event.message.clone(),
184        fields: event
185            .fields
186            .iter()
187            .map(|(k, v)| (k.clone(), v.clone()))
188            .collect(),
189        span_id: event.span_id.clone(),
190        trace_id: None,
191        connection_id: None,
192    };
193
194    structured.to_json().unwrap_or_else(|_| {
195        format!(
196            r#"{{"error":"failed to serialize event","message":"{}"}}"#,
197            event.message
198        )
199    })
200}
201
202/// Parse structured fields from a format string
203pub fn parse_structured_fields(
204    format_str: &str,
205    args: &[&dyn std::fmt::Display],
206) -> Vec<(String, String)> {
207    let mut fields = Vec::new();
208    let parts = format_str.split("{}");
209    let mut arg_idx = 0;
210
211    for (i, part) in parts.enumerate() {
212        if i > 0 && arg_idx < args.len() {
213            // Extract field name from the previous part
214            if let Some(field_name) = extract_field_name(part) {
215                fields.push((field_name, args[arg_idx].to_string()));
216            }
217            arg_idx += 1;
218        }
219    }
220
221    fields
222}
223
224fn extract_field_name(text: &str) -> Option<String> {
225    // Look for patterns like "field_name=" or "field_name:"
226    let trimmed = text.trim();
227    if let Some(idx) = trimmed.rfind('=') {
228        let name = trimmed[..idx].trim();
229        if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
230            return Some(name.to_string());
231        }
232    }
233    if let Some(idx) = trimmed.rfind(':') {
234        let name = trimmed[..idx].trim();
235        if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
236            return Some(name.to_string());
237        }
238    }
239    None
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_structured_event_builder() {
248        let event = StructuredEventBuilder::new(Level::INFO, "test", "Test message")
249            .field("key1", "value1")
250            .field_num("count", 42)
251            .field_bool("enabled", true)
252            .field_opt("optional", Some("present"))
253            .field_opt::<String>("missing", None)
254            .build();
255
256        assert_eq!(event.level, LogLevel::INFO);
257        assert_eq!(event.target, "test");
258        assert_eq!(event.message, "Test message");
259        assert_eq!(event.fields.len(), 4);
260        assert!(
261            event
262                .fields
263                .contains(&("key1".to_string(), "value1".to_string()))
264        );
265        assert!(
266            event
267                .fields
268                .contains(&("count".to_string(), "42".to_string()))
269        );
270        assert!(
271            event
272                .fields
273                .contains(&("enabled".to_string(), "true".to_string()))
274        );
275        assert!(
276            event
277                .fields
278                .contains(&("optional".to_string(), "present".to_string()))
279        );
280    }
281
282    #[test]
283    fn test_json_serialization() {
284        let event = StructuredLogEvent::new(Level::ERROR, "test::module", "Error occurred")
285            .with_field("error_code", "E001")
286            .with_field("details", "Connection timeout");
287
288        let json = event.to_json().unwrap();
289        assert!(json.contains(r#""level":"ERROR""#));
290        assert!(json.contains(r#""target":"test::module""#));
291        assert!(json.contains(r#""message":"Error occurred""#));
292        assert!(json.contains(r#""error_code","E001""#));
293    }
294}