1use serde::{Deserialize, Serialize};
9use tracing::Level;
10
11use crate::ConnectionId;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct StructuredLogEvent {
16 pub timestamp: u64,
18 pub level: LogLevel,
20 pub target: String,
22 pub message: String,
24 pub fields: Vec<(String, String)>,
26 pub span_id: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub trace_id: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub connection_id: Option<String>,
34}
35
36#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
38pub enum LogLevel {
39 ERROR,
41 WARN,
43 INFO,
45 DEBUG,
47 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 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 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 pub fn with_fields(mut self, fields: Vec<(String, String)>) -> Self {
86 self.fields.extend(fields);
87 self
88 }
89
90 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 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 pub fn with_connection_id(mut self, conn_id: &ConnectionId) -> Self {
104 self.connection_id = Some(format!("{conn_id:?}"));
105 self
106 }
107
108 pub fn to_json(&self) -> Result<String, serde_json::Error> {
110 serde_json::to_string(self)
111 }
112
113 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
115 serde_json::to_string_pretty(self)
116 }
117}
118
119pub struct StructuredEventBuilder {
121 event: StructuredLogEvent,
122}
123
124impl StructuredEventBuilder {
125 pub fn new(level: Level, target: &str, message: &str) -> Self {
127 Self {
128 event: StructuredLogEvent::new(level, target, message),
129 }
130 }
131
132 pub fn field(mut self, key: &str, value: &str) -> Self {
134 self.event = self.event.with_field(key, value);
135 self
136 }
137
138 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 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 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 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 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 pub fn build(self) -> StructuredLogEvent {
172 self.event
173 }
174}
175
176#[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
202pub 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 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 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}