1use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, VecDeque};
8use std::sync::{Arc, Mutex};
9use std::time::SystemTime;
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum LoggingError {
15 #[error("Failed to setup logging: {0}")]
17 SetupError(String),
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct LogEntry {
23 #[serde(rename = "@timestamp")]
25 pub timestamp: String,
26
27 pub level: String,
29
30 pub target: String,
32
33 pub message: String,
35
36 pub service: String,
38
39 pub environment: String,
41
42 pub hostname: String,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub span_id: Option<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub trace_id: Option<String>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub request_id: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub user_id: Option<String>,
60
61 #[serde(flatten)]
63 pub fields: HashMap<String, serde_json::Value>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct LoggingConfig {
69 pub json_format: bool,
71
72 pub include_timestamp: bool,
74
75 pub include_hostname: bool,
77
78 pub include_spans: bool,
80
81 pub extract_fields: Vec<String>,
83
84 pub buffer_size: usize,
86}
87
88impl Default for LoggingConfig {
89 fn default() -> Self {
90 Self {
91 json_format: true,
92 include_timestamp: true,
93 include_hostname: true,
94 include_spans: true,
95 extract_fields: vec![
96 "user_id".to_string(),
97 "request_id".to_string(),
98 "module".to_string(),
99 "action".to_string(),
100 ],
101 buffer_size: 1000,
102 }
103 }
104}
105
106pub struct LogBuffer {
108 buffer: Arc<Mutex<VecDeque<LogEntry>>>,
109 config: LoggingConfig,
110}
111
112impl LogBuffer {
113 pub fn new(config: LoggingConfig) -> Self {
115 Self {
116 buffer: Arc::new(Mutex::new(VecDeque::with_capacity(config.buffer_size))),
117 config,
118 }
119 }
120
121 pub fn push(&self, entry: LogEntry) {
123 let mut buffer = self.buffer.lock().unwrap();
124 if buffer.len() >= self.config.buffer_size {
126 buffer.pop_front();
127 }
128 buffer.push_back(entry);
129 }
130
131 pub fn drain(&self) -> Vec<LogEntry> {
133 let mut buffer = self.buffer.lock().unwrap();
134 let entries: Vec<LogEntry> = buffer.drain(..).collect();
135 entries
136 }
137
138 pub fn len(&self) -> usize {
140 let buffer = self.buffer.lock().unwrap();
141 buffer.len()
142 }
143}
144
145pub fn create_log_entry(
147 level: &str,
148 target: &str,
149 message: &str,
150 service: &str,
151 environment: &str,
152) -> LogEntry {
153 let timestamp = format_timestamp(SystemTime::now());
154
155 let current_span = tracing::Span::current();
157 let span_id = if current_span.is_none() {
158 None
159 } else {
160 Some(format!("{:?}", current_span.id()))
161 };
162
163 let trace_id = span_id.clone(); LogEntry {
167 timestamp,
168 level: level.to_string(),
169 target: target.to_string(),
170 message: message.to_string(),
171 service: service.to_string(),
172 environment: environment.to_string(),
173 hostname: get_hostname(),
174 span_id,
175 trace_id,
176 request_id: None, user_id: None, fields: HashMap::new(),
179 }
180}
181
182fn get_hostname() -> String {
184 std::env::var("HOSTNAME")
185 .unwrap_or_else(|_| "localhost".to_string())
186}
187
188fn format_timestamp(time: SystemTime) -> String {
190 use std::time::UNIX_EPOCH;
191
192 let duration = time
193 .duration_since(UNIX_EPOCH)
194 .unwrap_or_default();
195
196 let secs = duration.as_secs();
197 let millis = duration.as_millis();
198
199 format!(
201 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
202 1970 + (secs / 31536000) % 10000,
203 (secs / 2592000) % 12 + 1,
204 (secs / 86400) % 31 + 1,
205 (secs / 3600) % 24,
206 (secs / 60) % 60,
207 secs % 60,
208 millis % 1000
209 )
210}
211
212pub fn init_structured_logging(
238 config: &LoggingConfig,
239 service_name: &str,
240 environment: &str,
241 log_level: &str,
242) -> Result<LogBuffer, LoggingError> {
243 use tracing_subscriber::{Registry, EnvFilter, layer::SubscriberExt};
244 use tracing_subscriber::fmt;
245 use tracing_subscriber::Layer;
246
247 let filter = EnvFilter::try_from_default_env()
248 .unwrap_or_else(|_| EnvFilter::new(log_level));
249
250 if config.json_format {
251 let json_layer = fmt::layer()
253 .json()
254 .with_target(true)
255 .with_thread_ids(false)
256 .with_thread_names(false)
257 .with_file(false)
258 .with_line_number(false)
259 .with_span_events(fmt::format::FmtSpan::CLOSE)
260 .boxed();
261
262 let subscriber = Registry::default()
263 .with(filter)
264 .with(json_layer);
265
266 tracing::subscriber::set_global_default(subscriber)
267 .map_err(|e| LoggingError::SetupError(e.to_string()))?;
268 } else {
269 let fmt_layer = fmt::layer()
271 .with_target(true)
272 .with_thread_ids(false)
273 .with_span_events(fmt::format::FmtSpan::CLOSE)
274 .boxed();
275
276 let subscriber = Registry::default()
277 .with(filter)
278 .with(fmt_layer);
279
280 tracing::subscriber::set_global_default(subscriber)
281 .map_err(|e| LoggingError::SetupError(e.to_string()))?;
282 }
283
284 tracing::info!(
285 service = service_name,
286 environment = environment,
287 format = if config.json_format { "json" } else { "pretty" },
288 "Structured logging initialized"
289 );
290
291 Ok(LogBuffer::new(config.clone()))
292}
293
294pub fn get_aggregated_logs(buffer: &LogBuffer) -> Vec<LogEntry> {
298 buffer.drain()
299}
300
301pub fn create_request_id() -> String {
305 use std::time::SystemTime;
306 format!("{:x}", SystemTime::now()
308 .duration_since(SystemTime::UNIX_EPOCH)
309 .unwrap_or_default()
310 .as_nanos())
311}
312
313pub trait WithRequestId {
317 fn with_request_id(self, request_id: &str) -> Self;
319}
320
321#[macro_export]
323macro_rules! request_span {
324 ($name:expr, $request_id:expr) => {
325 tracing::info_span!(
326 $name,
327 request_id = %$request_id,
328 )
329 };
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_log_entry_serialization() {
338 let entry = LogEntry {
339 timestamp: "2024-01-01T00:00:00.000Z".to_string(),
340 level: "INFO".to_string(),
341 target: "test".to_string(),
342 message: "Test message".to_string(),
343 service: "test-service".to_string(),
344 environment: "test".to_string(),
345 hostname: "localhost".to_string(),
346 span_id: None,
347 trace_id: None,
348 request_id: None,
349 user_id: None,
350 fields: HashMap::new(),
351 };
352
353 let json = serde_json::to_string(&entry).unwrap();
354 assert!(json.contains("Test message"));
355 assert!(json.contains("INFO"));
356 assert!(json.contains("@timestamp"));
357 }
358
359 #[test]
360 fn test_logging_config_defaults() {
361 let config = LoggingConfig::default();
362 assert!(config.json_format);
363 assert!(config.include_timestamp);
364 assert!(config.include_hostname);
365 assert!(config.include_spans);
366 assert_eq!(config.buffer_size, 1000);
367 }
368
369 #[test]
370 fn test_log_buffer() {
371 let config = LoggingConfig::default();
372 let buffer = LogBuffer::new(config);
373
374 assert_eq!(buffer.len(), 0);
375
376 let entry = LogEntry {
377 timestamp: "2024-01-01T00:00:00.000Z".to_string(),
378 level: "INFO".to_string(),
379 target: "test".to_string(),
380 message: "Test message".to_string(),
381 service: "test-service".to_string(),
382 environment: "test".to_string(),
383 hostname: "localhost".to_string(),
384 span_id: None,
385 trace_id: None,
386 request_id: None,
387 user_id: None,
388 fields: HashMap::new(),
389 };
390
391 buffer.push(entry.clone());
392 assert_eq!(buffer.len(), 1);
393
394 let entries = buffer.drain();
395 assert_eq!(entries.len(), 1);
396 assert_eq!(entries[0].message, "Test message");
397 assert_eq!(buffer.len(), 0);
398 }
399
400 #[test]
401 fn test_create_request_id() {
402 let id = create_request_id();
403 assert!(!id.is_empty());
404 }
405
406 #[test]
407 fn test_timestamp_format() {
408 let time = SystemTime::UNIX_EPOCH;
409 let timestamp = format_timestamp(time);
410 assert!(timestamp.starts_with("1970-01-01T00:00:00."));
411 }
412
413 #[test]
414 fn test_create_log_entry_with_defaults() {
415 let entry = create_log_entry("INFO", "test_target", "test message", "test-service", "dev");
416
417 assert_eq!(entry.level, "INFO");
418 assert_eq!(entry.target, "test_target");
419 assert_eq!(entry.message, "test message");
420 assert_eq!(entry.service, "test-service");
421 assert_eq!(entry.environment, "dev");
422 assert_eq!(entry.hostname, "localhost");
423 assert!(entry.fields.is_empty());
424 }
425
426 #[test]
427 fn test_log_buffer_overflow() {
428 let config = LoggingConfig {
429 buffer_size: 3,
430 ..Default::default()
431 };
432 let buffer = LogBuffer::new(config);
433
434 for i in 0..5 {
435 buffer.push(LogEntry {
436 timestamp: format!("2024-01-01T00:00:{}.000Z", i),
437 level: "INFO".to_string(),
438 target: "test".to_string(),
439 message: format!("Message {}", i),
440 service: "test-service".to_string(),
441 environment: "test".to_string(),
442 hostname: "localhost".to_string(),
443 span_id: None,
444 trace_id: None,
445 request_id: None,
446 user_id: None,
447 fields: HashMap::new(),
448 });
449 }
450
451 assert_eq!(buffer.len(), 3);
453
454 let entries = buffer.drain();
455 assert_eq!(entries.len(), 3);
456 assert_eq!(entries[0].message, "Message 2");
457 assert_eq!(entries[1].message, "Message 3");
458 assert_eq!(entries[2].message, "Message 4");
459 }
460}