1use std::{fmt, sync::Arc};
15
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
21pub struct RequestId(pub(crate) Uuid);
22
23impl RequestId {
24 #[must_use]
26 pub fn new() -> Self {
27 Self(Uuid::new_v4())
28 }
29
30 #[must_use]
32 pub const fn from_uuid(uuid: Uuid) -> Self {
33 Self(uuid)
34 }
35}
36
37impl Default for RequestId {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl fmt::Display for RequestId {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "{}", self.0)
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct RequestContext {
52 pub request_id: RequestId,
54
55 pub operation: Option<String>,
57
58 pub user_id: Option<String>,
60
61 pub org_id: Option<String>,
63
64 pub client_ip: Option<String>,
66
67 pub api_version: Option<String>,
69}
70
71impl RequestContext {
72 #[must_use]
74 pub fn new() -> Self {
75 Self {
76 request_id: RequestId::new(),
77 operation: None,
78 user_id: None,
79 org_id: None,
80 client_ip: None,
81 api_version: None,
82 }
83 }
84
85 #[must_use]
87 pub fn with_operation(mut self, operation: String) -> Self {
88 self.operation = Some(operation);
89 self
90 }
91
92 #[must_use]
94 pub fn with_user_id(mut self, user_id: String) -> Self {
95 self.user_id = Some(user_id);
96 self
97 }
98
99 #[must_use]
101 pub fn with_org_id(mut self, org_id: String) -> Self {
102 self.org_id = Some(org_id);
103 self
104 }
105
106 #[must_use]
108 pub fn with_client_ip(mut self, ip: String) -> Self {
109 self.client_ip = Some(ip);
110 self
111 }
112
113 #[must_use]
115 pub fn with_api_version(mut self, version: String) -> Self {
116 self.api_version = Some(version);
117 self
118 }
119}
120
121impl Default for RequestContext {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "UPPERCASE")]
130#[non_exhaustive]
131pub enum LogLevel {
132 Trace,
134 Debug,
136 Info,
138 Warn,
140 Error,
142}
143
144impl fmt::Display for LogLevel {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::Trace => write!(f, "TRACE"),
148 Self::Debug => write!(f, "DEBUG"),
149 Self::Info => write!(f, "INFO"),
150 Self::Warn => write!(f, "WARN"),
151 Self::Error => write!(f, "ERROR"),
152 }
153 }
154}
155
156impl From<tracing::Level> for LogLevel {
157 fn from(level: tracing::Level) -> Self {
158 match level {
159 tracing::Level::TRACE => Self::Trace,
160 tracing::Level::DEBUG => Self::Debug,
161 tracing::Level::INFO => Self::Info,
162 tracing::Level::WARN => Self::Warn,
163 tracing::Level::ERROR => Self::Error,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct StructuredLogEntry {
171 pub timestamp: String,
173
174 pub level: LogLevel,
176
177 pub message: String,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub request_context: Option<RequestContext>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub metrics: Option<LogMetrics>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub error: Option<ErrorDetails>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub source: Option<SourceLocation>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub context: Option<serde_json::Value>,
199}
200
201impl StructuredLogEntry {
202 #[must_use]
204 pub fn new(level: LogLevel, message: String) -> Self {
205 Self {
206 timestamp: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
207 level,
208 message,
209 request_context: None,
210 metrics: None,
211 error: None,
212 source: None,
213 context: None,
214 }
215 }
216
217 #[must_use]
219 pub fn with_request_context(mut self, context: RequestContext) -> Self {
220 self.request_context = Some(context);
221 self
222 }
223
224 #[must_use]
226 pub const fn with_metrics(mut self, metrics: LogMetrics) -> Self {
227 self.metrics = Some(metrics);
228 self
229 }
230
231 #[must_use]
233 pub fn with_error(mut self, error: ErrorDetails) -> Self {
234 self.error = Some(error);
235 self
236 }
237
238 #[must_use]
240 pub fn with_source(mut self, source: SourceLocation) -> Self {
241 self.source = Some(source);
242 self
243 }
244
245 #[must_use]
247 pub fn with_context(mut self, context: serde_json::Value) -> Self {
248 self.context = Some(context);
249 self
250 }
251
252 #[must_use]
254 pub fn to_json_string(&self) -> String {
255 serde_json::to_string(self).unwrap_or_else(|_| {
256 format!(
257 r#"{{"level":"{}","message":"{}","error":"serialization failed"}}"#,
258 self.level, self.message
259 )
260 })
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct LogMetrics {
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub duration_ms: Option<f64>,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub complexity: Option<u32>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub items_processed: Option<u64>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub cache_hit: Option<bool>,
282
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub db_queries: Option<u32>,
286}
287
288impl LogMetrics {
289 #[must_use]
291 pub const fn new() -> Self {
292 Self {
293 duration_ms: None,
294 complexity: None,
295 items_processed: None,
296 cache_hit: None,
297 db_queries: None,
298 }
299 }
300
301 #[must_use]
303 pub const fn with_duration_ms(mut self, duration: f64) -> Self {
304 self.duration_ms = Some(duration);
305 self
306 }
307
308 #[must_use]
310 pub const fn with_complexity(mut self, complexity: u32) -> Self {
311 self.complexity = Some(complexity);
312 self
313 }
314
315 #[must_use]
317 pub const fn with_items_processed(mut self, count: u64) -> Self {
318 self.items_processed = Some(count);
319 self
320 }
321
322 #[must_use]
324 pub const fn with_cache_hit(mut self, hit: bool) -> Self {
325 self.cache_hit = Some(hit);
326 self
327 }
328
329 #[must_use]
331 pub const fn with_db_queries(mut self, count: u32) -> Self {
332 self.db_queries = Some(count);
333 self
334 }
335}
336
337impl Default for LogMetrics {
338 fn default() -> Self {
339 Self::new()
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ErrorDetails {
346 pub error_type: String,
348
349 pub message: String,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub code: Option<String>,
355
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub stack_trace: Option<String>,
359}
360
361impl ErrorDetails {
362 #[must_use]
364 pub const fn new(error_type: String, message: String) -> Self {
365 Self {
366 error_type,
367 message,
368 code: None,
369 stack_trace: None,
370 }
371 }
372
373 #[must_use]
375 pub fn with_code(mut self, code: String) -> Self {
376 self.code = Some(code);
377 self
378 }
379
380 #[must_use]
382 pub fn with_stack_trace(mut self, trace: String) -> Self {
383 self.stack_trace = Some(trace);
384 self
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct SourceLocation {
391 pub file: String,
393
394 pub line: u32,
396
397 pub module: String,
399}
400
401impl SourceLocation {
402 #[must_use]
404 pub const fn new(file: String, line: u32, module: String) -> Self {
405 Self { file, line, module }
406 }
407}
408
409#[derive(Debug, Clone)]
411pub struct RequestLogger {
412 context: Arc<RequestContext>,
413}
414
415impl RequestLogger {
416 #[must_use]
418 pub fn new(context: RequestContext) -> Self {
419 Self {
420 context: Arc::new(context),
421 }
422 }
423
424 #[must_use]
426 pub fn with_request_id(request_id: RequestId) -> Self {
427 Self::new(RequestContext {
428 request_id,
429 ..Default::default()
430 })
431 }
432
433 #[must_use]
435 pub fn context(&self) -> &RequestContext {
436 &self.context
437 }
438
439 pub fn info(&self, message: impl Into<String>) -> StructuredLogEntry {
441 StructuredLogEntry::new(LogLevel::Info, message.into())
442 .with_request_context((*self.context).clone())
443 }
444
445 pub fn warn(&self, message: impl Into<String>) -> StructuredLogEntry {
447 StructuredLogEntry::new(LogLevel::Warn, message.into())
448 .with_request_context((*self.context).clone())
449 }
450
451 pub fn error(&self, message: impl Into<String>) -> StructuredLogEntry {
453 StructuredLogEntry::new(LogLevel::Error, message.into())
454 .with_request_context((*self.context).clone())
455 }
456
457 pub fn debug(&self, message: impl Into<String>) -> StructuredLogEntry {
459 StructuredLogEntry::new(LogLevel::Debug, message.into())
460 .with_request_context((*self.context).clone())
461 }
462}