1use serde::{Deserialize, Serialize};
2use tracing::Level;
3
4use crate::ConnectionId;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct StructuredLogEvent {
9 pub timestamp: u64, 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#[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 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 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 pub fn with_fields(mut self, fields: Vec<(String, String)>) -> Self {
66 self.fields.extend(fields);
67 self
68 }
69
70 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 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 pub fn with_connection_id(mut self, conn_id: &ConnectionId) -> Self {
84 self.connection_id = Some(format!("{conn_id:?}"));
85 self
86 }
87
88 pub fn to_json(&self) -> Result<String, serde_json::Error> {
90 serde_json::to_string(self)
91 }
92
93 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
95 serde_json::to_string_pretty(self)
96 }
97}
98
99pub struct StructuredEventBuilder {
101 event: StructuredLogEvent,
102}
103
104impl StructuredEventBuilder {
105 pub fn new(level: Level, target: &str, message: &str) -> Self {
107 Self {
108 event: StructuredLogEvent::new(level, target, message),
109 }
110 }
111
112 pub fn field(mut self, key: &str, value: &str) -> Self {
114 self.event = self.event.with_field(key, value);
115 self
116 }
117
118 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 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 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 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 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 pub fn build(self) -> StructuredLogEvent {
152 self.event
153 }
154}
155
156pub(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
181pub 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 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 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}