ant_quic/logging/
structured.rs

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