1use std::collections::HashMap;
6use std::fmt;
7use std::sync::{Arc, RwLock};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::security::{sanitize_field_key, sanitize_field_value, sanitize_text};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum LogLevel {
15 Trace,
17 Debug,
19 Info,
21 Warn,
23 Error,
25}
26
27impl fmt::Display for LogLevel {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 LogLevel::Trace => write!(f, "TRACE"),
31 LogLevel::Debug => write!(f, "DEBUG"),
32 LogLevel::Info => write!(f, "INFO"),
33 LogLevel::Warn => write!(f, "WARN"),
34 LogLevel::Error => write!(f, "ERROR"),
35 }
36 }
37}
38
39impl LogLevel {
40 pub fn from_str(s: &str) -> Option<Self> {
42 match s.to_uppercase().as_str() {
43 "TRACE" => Some(LogLevel::Trace),
44 "DEBUG" => Some(LogLevel::Debug),
45 "INFO" => Some(LogLevel::Info),
46 "WARN" | "WARNING" => Some(LogLevel::Warn),
47 "ERROR" => Some(LogLevel::Error),
48 _ => None,
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct LogConfig {
56 pub level: LogLevel,
58 pub format: LogFormat,
60 pub include_timestamp: bool,
62 pub include_location: bool,
64 pub service_name: String,
66}
67
68impl Default for LogConfig {
69 fn default() -> Self {
70 Self {
71 level: LogLevel::Info,
72 format: LogFormat::Json,
73 include_timestamp: true,
74 include_location: false,
75 service_name: "dcp-server".to_string(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum LogFormat {
83 Json,
85 Text,
87}
88
89#[derive(Debug, Clone)]
91pub enum LogValue {
92 String(String),
93 Int(i64),
94 Float(f64),
95 Bool(bool),
96 Null,
97}
98
99impl From<&str> for LogValue {
100 fn from(s: &str) -> Self {
101 LogValue::String(s.to_string())
102 }
103}
104
105impl From<String> for LogValue {
106 fn from(s: String) -> Self {
107 LogValue::String(s)
108 }
109}
110
111impl From<i64> for LogValue {
112 fn from(v: i64) -> Self {
113 LogValue::Int(v)
114 }
115}
116
117impl From<i32> for LogValue {
118 fn from(v: i32) -> Self {
119 LogValue::Int(v as i64)
120 }
121}
122
123impl From<u64> for LogValue {
124 fn from(v: u64) -> Self {
125 LogValue::Int(v as i64)
126 }
127}
128
129impl From<f64> for LogValue {
130 fn from(v: f64) -> Self {
131 LogValue::Float(v)
132 }
133}
134
135impl From<bool> for LogValue {
136 fn from(v: bool) -> Self {
137 LogValue::Bool(v)
138 }
139}
140
141impl fmt::Display for LogValue {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 LogValue::String(s) => write!(f, "{}", s),
145 LogValue::Int(i) => write!(f, "{}", i),
146 LogValue::Float(fl) => write!(f, "{}", fl),
147 LogValue::Bool(b) => write!(f, "{}", b),
148 LogValue::Null => write!(f, "null"),
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct LogEntry {
156 pub level: LogLevel,
158 pub message: String,
160 pub timestamp: u64,
162 pub request_id: Option<String>,
164 pub trace_id: Option<String>,
166 pub fields: HashMap<String, LogValue>,
168 pub file: Option<String>,
170 pub line: Option<u32>,
172}
173
174impl LogEntry {
175 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
177 let timestamp = SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .unwrap_or_default()
180 .as_millis() as u64;
181 let message = message.into();
182
183 Self {
184 level,
185 message: sanitize_text(&message),
186 timestamp,
187 request_id: None,
188 trace_id: None,
189 fields: HashMap::new(),
190 file: None,
191 line: None,
192 }
193 }
194
195 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
197 let request_id = request_id.into();
198 self.request_id = Some(sanitize_text(&request_id));
199 self
200 }
201
202 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
204 let trace_id = trace_id.into();
205 self.trace_id = Some(sanitize_text(&trace_id));
206 self
207 }
208
209 pub fn with_field(mut self, key: impl Into<String>, value: impl Into<LogValue>) -> Self {
211 let key = key.into();
212 let value = sanitize_log_value(&key, value.into());
213 self.fields.insert(sanitize_field_key(&key), value);
214 self
215 }
216
217 pub fn with_location(mut self, file: impl Into<String>, line: u32) -> Self {
219 self.file = Some(file.into());
220 self.line = Some(line);
221 self
222 }
223
224 fn sanitized_copy(&self) -> Self {
225 let mut fields = HashMap::with_capacity(self.fields.len());
226 for (key, value) in &self.fields {
227 fields.insert(
228 sanitize_field_key(key),
229 sanitize_log_value(key, value.clone()),
230 );
231 }
232
233 Self {
234 level: self.level,
235 message: sanitize_text(&self.message),
236 timestamp: self.timestamp,
237 request_id: self.request_id.as_ref().map(|id| sanitize_text(id)),
238 trace_id: self.trace_id.as_ref().map(|id| sanitize_text(id)),
239 fields,
240 file: self.file.as_ref().map(|file| sanitize_text(file)),
241 line: self.line,
242 }
243 }
244
245 pub fn to_json(&self, service_name: &str) -> String {
247 let entry = self.sanitized_copy();
248 let mut json = String::from("{");
249
250 json.push_str(&format!("\"timestamp\":{},", entry.timestamp));
252 json.push_str(&format!("\"level\":\"{}\",", entry.level));
253 json.push_str(&format!(
254 "\"message\":{},",
255 escape_json_string(&entry.message)
256 ));
257 json.push_str(&format!(
258 "\"service\":{},",
259 escape_json_string(&sanitize_text(service_name))
260 ));
261
262 if let Some(ref request_id) = entry.request_id {
264 json.push_str(&format!(
265 "\"request_id\":{},",
266 escape_json_string(request_id)
267 ));
268 }
269 if let Some(ref trace_id) = entry.trace_id {
270 json.push_str(&format!("\"trace_id\":{},", escape_json_string(trace_id)));
271 }
272 if let Some(ref file) = entry.file {
273 json.push_str(&format!("\"file\":{},", escape_json_string(file)));
274 }
275 if let Some(line) = entry.line {
276 json.push_str(&format!("\"line\":{},", line));
277 }
278
279 for (safe_key, value) in &entry.fields {
281 let value_str = match value {
282 LogValue::String(s) => escape_json_string(s),
283 LogValue::Int(i) => i.to_string(),
284 LogValue::Float(f) => f.to_string(),
285 LogValue::Bool(b) => b.to_string(),
286 LogValue::Null => "null".to_string(),
287 };
288 json.push_str(&format!("{}:{},", escape_json_string(&safe_key), value_str));
289 }
290
291 if json.ends_with(',') {
293 json.pop();
294 }
295 json.push('}');
296
297 json
298 }
299
300 pub fn to_text(&self) -> String {
302 let entry = self.sanitized_copy();
303 let mut text = format!(
304 "{} [{}] {}",
305 format_timestamp(entry.timestamp),
306 entry.level,
307 entry.message
308 );
309
310 if let Some(ref request_id) = entry.request_id {
311 text.push_str(&format!(" request_id={}", request_id));
312 }
313
314 for (key, value) in &entry.fields {
315 text.push_str(&format!(" {}={}", key, value));
316 }
317
318 text
319 }
320}
321
322fn escape_json_string(s: &str) -> String {
324 let mut result = String::with_capacity(s.len() + 2);
325 result.push('"');
326 for c in s.chars() {
327 match c {
328 '"' => result.push_str("\\\""),
329 '\\' => result.push_str("\\\\"),
330 '\n' => result.push_str("\\n"),
331 '\r' => result.push_str("\\r"),
332 '\t' => result.push_str("\\t"),
333 c if c.is_control() => {
334 result.push_str(&format!("\\u{:04x}", c as u32));
335 }
336 c => result.push(c),
337 }
338 }
339 result.push('"');
340 result
341}
342
343fn sanitize_log_value(key: &str, value: LogValue) -> LogValue {
344 match value {
345 LogValue::String(value) => LogValue::String(sanitize_field_value(key, &value)),
346 other => {
347 if crate::security::is_sensitive_key(key) {
348 LogValue::String(crate::security::REDACTED.to_string())
349 } else {
350 other
351 }
352 }
353 }
354}
355
356fn format_timestamp(millis: u64) -> String {
358 let secs = millis / 1000;
359 let ms = millis % 1000;
360
361 let days_since_epoch = secs / 86400;
363 let time_of_day = secs % 86400;
364 let hours = time_of_day / 3600;
365 let minutes = (time_of_day % 3600) / 60;
366 let seconds = time_of_day % 60;
367
368 let (year, month, day) = days_to_ymd(days_since_epoch as i64);
370
371 format!(
372 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
373 year, month, day, hours, minutes, seconds, ms
374 )
375}
376
377fn days_to_ymd(days: i64) -> (i32, u32, u32) {
379 let mut remaining = days;
381 let mut year = 1970i32;
382
383 loop {
384 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
385 if remaining < days_in_year {
386 break;
387 }
388 remaining -= days_in_year;
389 year += 1;
390 }
391
392 let days_in_months: [i64; 12] = if is_leap_year(year) {
393 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
394 } else {
395 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
396 };
397
398 let mut month = 1u32;
399 for days_in_month in days_in_months.iter() {
400 if remaining < *days_in_month {
401 break;
402 }
403 remaining -= days_in_month;
404 month += 1;
405 }
406
407 (year, month, (remaining + 1) as u32)
408}
409
410fn is_leap_year(year: i32) -> bool {
411 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
412}
413
414pub struct StructuredLogger {
416 config: LogConfig,
417 sink: Arc<RwLock<Vec<LogEntry>>>,
419}
420
421impl StructuredLogger {
422 pub fn new(config: LogConfig) -> Self {
424 Self {
425 config,
426 sink: Arc::new(RwLock::new(Vec::new())),
427 }
428 }
429
430 pub fn with_defaults() -> Self {
432 Self::new(LogConfig::default())
433 }
434
435 pub fn trace(&self, message: impl Into<String>) -> LogEntryBuilder<'_> {
437 LogEntryBuilder::new(self, LogLevel::Trace, message.into())
438 }
439
440 pub fn debug(&self, message: impl Into<String>) -> LogEntryBuilder<'_> {
442 LogEntryBuilder::new(self, LogLevel::Debug, message.into())
443 }
444
445 pub fn info(&self, message: impl Into<String>) -> LogEntryBuilder<'_> {
447 LogEntryBuilder::new(self, LogLevel::Info, message.into())
448 }
449
450 pub fn warn(&self, message: impl Into<String>) -> LogEntryBuilder<'_> {
452 LogEntryBuilder::new(self, LogLevel::Warn, message.into())
453 }
454
455 pub fn error(&self, message: impl Into<String>) -> LogEntryBuilder<'_> {
457 LogEntryBuilder::new(self, LogLevel::Error, message.into())
458 }
459
460 pub fn is_enabled(&self, level: LogLevel) -> bool {
462 level >= self.config.level
463 }
464
465 pub fn config(&self) -> &LogConfig {
467 &self.config
468 }
469
470 pub fn set_level(&mut self, level: LogLevel) {
472 self.config.level = level;
473 }
474
475 pub fn emit(&self, entry: LogEntry) {
477 let entry = entry.sanitized_copy();
478 if entry.level < self.config.level {
479 return;
480 }
481
482 self.sink.write().unwrap().push(entry.clone());
484
485 let output = match self.config.format {
487 LogFormat::Json => entry.to_json(&self.config.service_name),
488 LogFormat::Text => entry.to_text(),
489 };
490
491 let _ = output;
494 }
495
496 pub fn entries(&self) -> Vec<LogEntry> {
498 self.sink.read().unwrap().clone()
499 }
500
501 pub fn clear(&self) {
503 self.sink.write().unwrap().clear();
504 }
505}
506
507impl Default for StructuredLogger {
508 fn default() -> Self {
509 Self::with_defaults()
510 }
511}
512
513pub struct LogEntryBuilder<'a> {
515 logger: &'a StructuredLogger,
516 entry: LogEntry,
517}
518
519impl<'a> LogEntryBuilder<'a> {
520 fn new(logger: &'a StructuredLogger, level: LogLevel, message: String) -> Self {
521 Self {
522 logger,
523 entry: LogEntry::new(level, message),
524 }
525 }
526
527 pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
529 let request_id = request_id.into();
530 self.entry.request_id = Some(sanitize_text(&request_id));
531 self
532 }
533
534 pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
536 let trace_id = trace_id.into();
537 self.entry.trace_id = Some(sanitize_text(&trace_id));
538 self
539 }
540
541 pub fn field(mut self, key: impl Into<String>, value: impl Into<LogValue>) -> Self {
543 let key = key.into();
544 let value = sanitize_log_value(&key, value.into());
545 self.entry.fields.insert(sanitize_field_key(&key), value);
546 self
547 }
548
549 pub fn emit(self) {
551 self.logger.emit(self.entry);
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558
559 #[test]
560 fn test_log_level_ordering() {
561 assert!(LogLevel::Trace < LogLevel::Debug);
562 assert!(LogLevel::Debug < LogLevel::Info);
563 assert!(LogLevel::Info < LogLevel::Warn);
564 assert!(LogLevel::Warn < LogLevel::Error);
565 }
566
567 #[test]
568 fn test_log_level_from_str() {
569 assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info));
570 assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
571 assert_eq!(LogLevel::from_str("WARNING"), Some(LogLevel::Warn));
572 assert_eq!(LogLevel::from_str("invalid"), None);
573 }
574
575 #[test]
576 fn test_log_entry_json() {
577 let entry = LogEntry::new(LogLevel::Info, "Test message")
578 .with_request_id("req-123")
579 .with_field("user_id", "user-456");
580
581 let json = entry.to_json("test-service");
582
583 assert!(json.contains("\"level\":\"INFO\""));
584 assert!(json.contains("\"message\":\"Test message\""));
585 assert!(json.contains("\"request_id\":\"req-123\""));
586 assert!(json.contains("\"user_id\":\"user-456\""));
587 assert!(json.contains("\"service\":\"test-service\""));
588 assert!(json.contains("\"timestamp\":"));
589 }
590
591 #[test]
592 fn test_log_entry_text() {
593 let entry =
594 LogEntry::new(LogLevel::Error, "Something went wrong").with_request_id("req-789");
595
596 let text = entry.to_text();
597
598 assert!(text.contains("[ERROR]"));
599 assert!(text.contains("Something went wrong"));
600 assert!(text.contains("request_id=req-789"));
601 }
602
603 #[test]
604 fn test_json_escaping() {
605 let entry = LogEntry::new(LogLevel::Info, "Message with \"quotes\" and \nnewline");
606 let json = entry.to_json("test");
607
608 assert!(json.contains("\\\"quotes\\\""));
609 assert!(json.contains("\\n"));
610 }
611
612 #[test]
613 fn test_structured_logger() {
614 let logger = StructuredLogger::with_defaults();
615
616 logger
617 .info("Test info message")
618 .request_id("req-001")
619 .field("method", "tools/call")
620 .emit();
621
622 let entries = logger.entries();
623 assert_eq!(entries.len(), 1);
624 assert_eq!(entries[0].level, LogLevel::Info);
625 assert_eq!(entries[0].message, "Test info message");
626 assert_eq!(entries[0].request_id, Some("req-001".to_string()));
627 }
628
629 #[test]
630 fn test_log_level_filtering() {
631 let mut logger = StructuredLogger::new(LogConfig {
632 level: LogLevel::Warn,
633 ..Default::default()
634 });
635
636 logger.debug("Debug message").emit();
637 logger.info("Info message").emit();
638 logger.warn("Warn message").emit();
639 logger.error("Error message").emit();
640
641 let entries = logger.entries();
642 assert_eq!(entries.len(), 2);
643 assert_eq!(entries[0].level, LogLevel::Warn);
644 assert_eq!(entries[1].level, LogLevel::Error);
645 }
646
647 #[test]
648 fn test_timestamp_formatting() {
649 let formatted = format_timestamp(1705321845123);
652 assert!(formatted.contains("2024-01-15"));
653 assert!(formatted.contains("12:30:45.123Z"));
654 }
655
656 #[test]
657 fn test_log_value_types() {
658 let entry = LogEntry::new(LogLevel::Info, "Test")
659 .with_field("string", "value")
660 .with_field("int", 42i64)
661 .with_field("float", 3.14f64)
662 .with_field("bool", true);
663
664 assert_eq!(entry.fields.len(), 4);
665 }
666}