1use parking_lot::RwLock;
25use std::collections::HashMap;
26use std::fmt;
27use std::io::Write;
28use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
29use std::sync::Arc;
30use std::time::{Instant, SystemTime, UNIX_EPOCH};
31
32use crate::observability::{SpanId, TraceId};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
40pub enum LogLevel {
41 Trace = 0,
43 Debug = 1,
45 #[default]
47 Info = 2,
48 Warn = 3,
50 Error = 4,
52 Fatal = 5,
54}
55
56impl LogLevel {
57 pub fn as_str(&self) -> &'static str {
59 match self {
60 LogLevel::Trace => "TRACE",
61 LogLevel::Debug => "DEBUG",
62 LogLevel::Info => "INFO",
63 LogLevel::Warn => "WARN",
64 LogLevel::Error => "ERROR",
65 LogLevel::Fatal => "FATAL",
66 }
67 }
68
69 pub fn parse(s: &str) -> Option<Self> {
71 match s.to_uppercase().as_str() {
72 "TRACE" => Some(LogLevel::Trace),
73 "DEBUG" => Some(LogLevel::Debug),
74 "INFO" => Some(LogLevel::Info),
75 "WARN" | "WARNING" => Some(LogLevel::Warn),
76 "ERROR" => Some(LogLevel::Error),
77 "FATAL" | "CRITICAL" => Some(LogLevel::Fatal),
78 _ => None,
79 }
80 }
81}
82
83impl fmt::Display for LogLevel {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, "{}", self.as_str())
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95pub enum LogOutput {
96 #[default]
98 Text,
99 Json,
101 Compact,
103 Pretty,
105}
106
107#[derive(Debug, Clone)]
113pub struct TraceContext {
114 pub trace_id: Option<TraceId>,
116 pub span_id: Option<SpanId>,
118 pub parent_span_id: Option<SpanId>,
120 pub fields: HashMap<String, String>,
122}
123
124impl TraceContext {
125 pub fn new() -> Self {
127 Self {
128 trace_id: None,
129 span_id: None,
130 parent_span_id: None,
131 fields: HashMap::new(),
132 }
133 }
134
135 pub fn with_new_trace() -> Self {
137 Self {
138 trace_id: Some(TraceId::new()),
139 span_id: Some(SpanId::new()),
140 parent_span_id: None,
141 fields: HashMap::new(),
142 }
143 }
144
145 pub fn with_trace_id(mut self, trace_id: TraceId) -> Self {
147 self.trace_id = Some(trace_id);
148 self
149 }
150
151 pub fn with_span_id(mut self, span_id: SpanId) -> Self {
153 self.span_id = Some(span_id);
154 self
155 }
156
157 pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
159 self.fields.insert(key.into(), value.into());
160 self
161 }
162}
163
164impl Default for TraceContext {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[derive(Debug, Clone)]
176pub struct LogConfig {
177 pub level: LogLevel,
179 pub output: LogOutput,
181 pub trace_correlation: bool,
183 pub include_timestamps: bool,
185 pub include_location: bool,
187 pub include_thread_id: bool,
189 pub module_levels: HashMap<String, LogLevel>,
191 pub service_name: String,
193 pub environment: String,
195 pub global_fields: HashMap<String, String>,
197}
198
199impl Default for LogConfig {
200 fn default() -> Self {
201 Self {
202 level: LogLevel::Info,
203 output: LogOutput::Text,
204 trace_correlation: true,
205 include_timestamps: true,
206 include_location: false,
207 include_thread_id: false,
208 module_levels: HashMap::new(),
209 service_name: "ringkernel".to_string(),
210 environment: "development".to_string(),
211 global_fields: HashMap::new(),
212 }
213 }
214}
215
216impl LogConfig {
217 pub fn builder() -> LogConfigBuilder {
219 LogConfigBuilder::new()
220 }
221
222 pub fn development() -> Self {
224 Self {
225 level: LogLevel::Debug,
226 output: LogOutput::Pretty,
227 trace_correlation: true,
228 include_timestamps: true,
229 include_location: true,
230 include_thread_id: false,
231 environment: "development".to_string(),
232 ..Default::default()
233 }
234 }
235
236 pub fn production() -> Self {
238 Self {
239 level: LogLevel::Info,
240 output: LogOutput::Json,
241 trace_correlation: true,
242 include_timestamps: true,
243 include_location: false,
244 include_thread_id: true,
245 environment: "production".to_string(),
246 ..Default::default()
247 }
248 }
249
250 pub fn effective_level(&self, module: &str) -> LogLevel {
252 if let Some(&level) = self.module_levels.get(module) {
254 return level;
255 }
256
257 let mut best_match: Option<(&str, LogLevel)> = None;
259 for (prefix, &level) in &self.module_levels {
260 if module.starts_with(prefix) {
261 match best_match {
262 None => best_match = Some((prefix, level)),
263 Some((best_prefix, _)) if prefix.len() > best_prefix.len() => {
264 best_match = Some((prefix, level));
265 }
266 _ => {}
267 }
268 }
269 }
270
271 best_match.map(|(_, level)| level).unwrap_or(self.level)
272 }
273}
274
275#[derive(Debug, Default)]
277pub struct LogConfigBuilder {
278 config: LogConfig,
279}
280
281impl LogConfigBuilder {
282 pub fn new() -> Self {
284 Self {
285 config: LogConfig::default(),
286 }
287 }
288
289 pub fn level(mut self, level: LogLevel) -> Self {
291 self.config.level = level;
292 self
293 }
294
295 pub fn output(mut self, output: LogOutput) -> Self {
297 self.config.output = output;
298 self
299 }
300
301 pub fn with_trace_correlation(mut self, enabled: bool) -> Self {
303 self.config.trace_correlation = enabled;
304 self
305 }
306
307 pub fn with_timestamps(mut self, enabled: bool) -> Self {
309 self.config.include_timestamps = enabled;
310 self
311 }
312
313 pub fn with_location(mut self, enabled: bool) -> Self {
315 self.config.include_location = enabled;
316 self
317 }
318
319 pub fn with_thread_id(mut self, enabled: bool) -> Self {
321 self.config.include_thread_id = enabled;
322 self
323 }
324
325 pub fn service_name(mut self, name: impl Into<String>) -> Self {
327 self.config.service_name = name.into();
328 self
329 }
330
331 pub fn environment(mut self, env: impl Into<String>) -> Self {
333 self.config.environment = env.into();
334 self
335 }
336
337 pub fn module_level(mut self, module: impl Into<String>, level: LogLevel) -> Self {
339 self.config.module_levels.insert(module.into(), level);
340 self
341 }
342
343 pub fn global_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
345 self.config.global_fields.insert(key.into(), value.into());
346 self
347 }
348
349 pub fn build(self) -> LogConfig {
351 self.config
352 }
353}
354
355#[derive(Debug, Clone)]
361pub struct LogEntry {
362 pub level: LogLevel,
364 pub message: String,
366 pub timestamp: SystemTime,
368 pub target: Option<String>,
370 pub file: Option<String>,
372 pub line: Option<u32>,
374 pub thread_id: Option<u64>,
376 pub thread_name: Option<String>,
378 pub trace_id: Option<TraceId>,
380 pub span_id: Option<SpanId>,
382 pub fields: HashMap<String, LogValue>,
384}
385
386#[derive(Debug, Clone)]
388pub enum LogValue {
389 String(String),
391 Int(i64),
393 Uint(u64),
395 Float(f64),
397 Bool(bool),
399}
400
401impl fmt::Display for LogValue {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 LogValue::String(s) => write!(f, "{}", s),
405 LogValue::Int(i) => write!(f, "{}", i),
406 LogValue::Uint(u) => write!(f, "{}", u),
407 LogValue::Float(fl) => write!(f, "{}", fl),
408 LogValue::Bool(b) => write!(f, "{}", b),
409 }
410 }
411}
412
413impl From<&str> for LogValue {
414 fn from(s: &str) -> Self {
415 LogValue::String(s.to_string())
416 }
417}
418
419impl From<String> for LogValue {
420 fn from(s: String) -> Self {
421 LogValue::String(s)
422 }
423}
424
425impl From<i64> for LogValue {
426 fn from(i: i64) -> Self {
427 LogValue::Int(i)
428 }
429}
430
431impl From<u64> for LogValue {
432 fn from(u: u64) -> Self {
433 LogValue::Uint(u)
434 }
435}
436
437impl From<f64> for LogValue {
438 fn from(f: f64) -> Self {
439 LogValue::Float(f)
440 }
441}
442
443impl From<bool> for LogValue {
444 fn from(b: bool) -> Self {
445 LogValue::Bool(b)
446 }
447}
448
449impl LogEntry {
450 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
452 Self {
453 level,
454 message: message.into(),
455 timestamp: SystemTime::now(),
456 target: None,
457 file: None,
458 line: None,
459 thread_id: None,
460 thread_name: None,
461 trace_id: None,
462 span_id: None,
463 fields: HashMap::new(),
464 }
465 }
466
467 pub fn with_target(mut self, target: impl Into<String>) -> Self {
469 self.target = Some(target.into());
470 self
471 }
472
473 pub fn with_trace_context(mut self, ctx: &TraceContext) -> Self {
475 self.trace_id = ctx.trace_id;
476 self.span_id = ctx.span_id;
477 for (k, v) in &ctx.fields {
478 self.fields.insert(k.clone(), LogValue::String(v.clone()));
479 }
480 self
481 }
482
483 pub fn with_field(mut self, key: impl Into<String>, value: impl Into<LogValue>) -> Self {
485 self.fields.insert(key.into(), value.into());
486 self
487 }
488
489 pub fn to_json(&self, config: &LogConfig) -> String {
491 let mut json = String::with_capacity(512);
492 json.push('{');
493
494 if config.include_timestamps {
496 let ts = self
497 .timestamp
498 .duration_since(UNIX_EPOCH)
499 .map(|d| d.as_millis())
500 .unwrap_or(0);
501 json.push_str(&format!(r#""timestamp":{},"#, ts));
502 }
503
504 json.push_str(&format!(r#""level":"{}","#, self.level.as_str()));
506
507 let escaped_msg = self.message.replace('\\', "\\\\").replace('"', "\\\"");
509 json.push_str(&format!(r#""message":"{}","#, escaped_msg));
510
511 json.push_str(&format!(r#""service":"{}","#, config.service_name));
513 json.push_str(&format!(r#""environment":"{}","#, config.environment));
514
515 if let Some(ref target) = self.target {
517 json.push_str(&format!(r#""target":"{}","#, target));
518 }
519
520 if config.include_location {
522 if let Some(ref file) = self.file {
523 json.push_str(&format!(r#""file":"{}","#, file));
524 }
525 if let Some(line) = self.line {
526 json.push_str(&format!(r#""line":{},"#, line));
527 }
528 }
529
530 if config.include_thread_id {
532 if let Some(tid) = self.thread_id {
533 json.push_str(&format!(r#""thread_id":{},"#, tid));
534 }
535 if let Some(ref name) = self.thread_name {
536 json.push_str(&format!(r#""thread_name":"{}","#, name));
537 }
538 }
539
540 if config.trace_correlation {
542 if let Some(trace_id) = self.trace_id {
543 json.push_str(&format!(r#""trace_id":"{:032x}","#, trace_id.0));
544 }
545 if let Some(span_id) = self.span_id {
546 json.push_str(&format!(r#""span_id":"{:016x}","#, span_id.0));
547 }
548 }
549
550 for (k, v) in &config.global_fields {
552 json.push_str(&format!(r#""{}":"{}","#, k, v));
553 }
554
555 if !self.fields.is_empty() {
557 json.push_str(r#""fields":{"#);
558 let mut first = true;
559 for (k, v) in &self.fields {
560 if !first {
561 json.push(',');
562 }
563 first = false;
564 match v {
565 LogValue::String(s) => {
566 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
567 json.push_str(&format!(r#""{}":"{}""#, k, escaped));
568 }
569 LogValue::Int(i) => json.push_str(&format!(r#""{}":{}""#, k, i)),
570 LogValue::Uint(u) => json.push_str(&format!(r#""{}":{}""#, k, u)),
571 LogValue::Float(f) => json.push_str(&format!(r#""{}":{}""#, k, f)),
572 LogValue::Bool(b) => json.push_str(&format!(r#""{}":{}""#, k, b)),
573 }
574 }
575 json.push_str("},");
576 }
577
578 if json.ends_with(',') {
580 json.pop();
581 }
582 json.push('}');
583
584 json
585 }
586
587 pub fn to_text(&self, config: &LogConfig) -> String {
589 let mut text = String::with_capacity(256);
590
591 if config.include_timestamps {
593 let ts = self
594 .timestamp
595 .duration_since(UNIX_EPOCH)
596 .map(|d| {
597 let secs = d.as_secs();
598 let millis = d.subsec_millis();
599 format!(
600 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
601 1970 + secs / 31536000, ((secs % 31536000) / 2592000) + 1, ((secs % 2592000) / 86400) + 1, (secs % 86400) / 3600, (secs % 3600) / 60, secs % 60, millis
608 )
609 })
610 .unwrap_or_else(|_| "1970-01-01T00:00:00.000Z".to_string());
611 text.push_str(&ts);
612 text.push(' ');
613 }
614
615 text.push_str(&format!("{:5} ", self.level.as_str()));
617
618 if let Some(ref target) = self.target {
620 text.push_str(&format!("[{}] ", target));
621 }
622
623 if config.trace_correlation {
625 if let Some(trace_id) = self.trace_id {
626 text.push_str(&format!("trace={:032x} ", trace_id.0));
627 }
628 }
629
630 text.push_str(&self.message);
632
633 if !self.fields.is_empty() {
635 text.push_str(" {");
636 let mut first = true;
637 for (k, v) in &self.fields {
638 if !first {
639 text.push_str(", ");
640 }
641 first = false;
642 text.push_str(&format!("{}={}", k, v));
643 }
644 text.push('}');
645 }
646
647 text
648 }
649}
650
651pub struct StructuredLogger {
657 config: RwLock<LogConfig>,
659 context: RwLock<TraceContext>,
661 log_count: AtomicU64,
663 error_count: AtomicU64,
665 enabled: AtomicBool,
667 start_time: Instant,
669 sinks: RwLock<Vec<Arc<dyn LogSink>>>,
671}
672
673impl StructuredLogger {
674 pub fn new(config: LogConfig) -> Self {
676 Self {
677 config: RwLock::new(config),
678 context: RwLock::new(TraceContext::new()),
679 log_count: AtomicU64::new(0),
680 error_count: AtomicU64::new(0),
681 enabled: AtomicBool::new(true),
682 start_time: Instant::now(),
683 sinks: RwLock::new(vec![]),
684 }
685 }
686
687 pub fn default_logger() -> Self {
689 Self::new(LogConfig::default())
690 }
691
692 pub fn development() -> Self {
694 Self::new(LogConfig::development())
695 }
696
697 pub fn production() -> Self {
699 Self::new(LogConfig::production())
700 }
701
702 pub fn set_enabled(&self, enabled: bool) {
704 self.enabled.store(enabled, Ordering::SeqCst);
705 }
706
707 pub fn is_enabled(&self) -> bool {
709 self.enabled.load(Ordering::SeqCst)
710 }
711
712 pub fn set_config(&self, config: LogConfig) {
714 *self.config.write() = config;
715 }
716
717 pub fn config(&self) -> LogConfig {
719 self.config.read().clone()
720 }
721
722 pub fn set_context(&self, context: TraceContext) {
724 *self.context.write() = context;
725 }
726
727 pub fn context(&self) -> TraceContext {
729 self.context.read().clone()
730 }
731
732 pub fn start_trace(&self) -> TraceContext {
734 let ctx = TraceContext::with_new_trace();
735 *self.context.write() = ctx.clone();
736 ctx
737 }
738
739 pub fn add_sink(&self, sink: Arc<dyn LogSink>) {
741 self.sinks.write().push(sink);
742 }
743
744 pub fn log(&self, level: LogLevel, message: &str, fields: &[(&str, &str)]) {
746 if !self.enabled.load(Ordering::SeqCst) {
747 return;
748 }
749
750 let config = self.config.read();
751 if level < config.level {
752 return;
753 }
754
755 let ctx = self.context.read();
756 let mut entry = LogEntry::new(level, message).with_trace_context(&ctx);
757
758 for (k, v) in fields {
759 entry = entry.with_field(*k, *v);
760 }
761
762 self.log_count.fetch_add(1, Ordering::Relaxed);
763 if level >= LogLevel::Error {
764 self.error_count.fetch_add(1, Ordering::Relaxed);
765 }
766
767 let output = match config.output {
769 LogOutput::Json => entry.to_json(&config),
770 LogOutput::Text | LogOutput::Compact | LogOutput::Pretty => entry.to_text(&config),
771 };
772
773 drop(config);
774
775 let sinks = self.sinks.read();
777 for sink in sinks.iter() {
778 let _ = sink.write(&entry, &output);
779 }
780
781 if sinks.is_empty() {
783 let _ = writeln!(std::io::stderr(), "{}", output);
784 }
785 }
786
787 pub fn trace(&self, message: &str, fields: &[(&str, &str)]) {
789 self.log(LogLevel::Trace, message, fields);
790 }
791
792 pub fn debug(&self, message: &str, fields: &[(&str, &str)]) {
794 self.log(LogLevel::Debug, message, fields);
795 }
796
797 pub fn info(&self, message: &str, fields: &[(&str, &str)]) {
799 self.log(LogLevel::Info, message, fields);
800 }
801
802 pub fn warn(&self, message: &str, fields: &[(&str, &str)]) {
804 self.log(LogLevel::Warn, message, fields);
805 }
806
807 pub fn error(&self, message: &str, fields: &[(&str, &str)]) {
809 self.log(LogLevel::Error, message, fields);
810 }
811
812 pub fn fatal(&self, message: &str, fields: &[(&str, &str)]) {
814 self.log(LogLevel::Fatal, message, fields);
815 }
816
817 pub fn stats(&self) -> LoggerStats {
819 LoggerStats {
820 log_count: self.log_count.load(Ordering::Relaxed),
821 error_count: self.error_count.load(Ordering::Relaxed),
822 uptime: self.start_time.elapsed(),
823 sink_count: self.sinks.read().len(),
824 }
825 }
826}
827
828impl Default for StructuredLogger {
829 fn default() -> Self {
830 Self::default_logger()
831 }
832}
833
834#[derive(Debug, Clone)]
836pub struct LoggerStats {
837 pub log_count: u64,
839 pub error_count: u64,
841 pub uptime: std::time::Duration,
843 pub sink_count: usize,
845}
846
847pub trait LogSink: Send + Sync {
853 fn write(&self, entry: &LogEntry, formatted: &str) -> Result<(), LogSinkError>;
855
856 fn flush(&self) -> Result<(), LogSinkError> {
858 Ok(())
859 }
860
861 fn name(&self) -> &str;
863}
864
865#[derive(Debug)]
867pub struct LogSinkError {
868 pub message: String,
870}
871
872impl fmt::Display for LogSinkError {
873 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
874 write!(f, "LogSinkError: {}", self.message)
875 }
876}
877
878impl std::error::Error for LogSinkError {}
879
880pub struct ConsoleSink {
882 use_stderr: bool,
884}
885
886impl ConsoleSink {
887 pub fn new() -> Self {
889 Self { use_stderr: true }
890 }
891
892 pub fn stdout() -> Self {
894 Self { use_stderr: false }
895 }
896
897 pub fn stderr() -> Self {
899 Self { use_stderr: true }
900 }
901}
902
903impl Default for ConsoleSink {
904 fn default() -> Self {
905 Self::new()
906 }
907}
908
909impl LogSink for ConsoleSink {
910 fn write(&self, _entry: &LogEntry, formatted: &str) -> Result<(), LogSinkError> {
911 let result = if self.use_stderr {
912 writeln!(std::io::stderr(), "{}", formatted)
913 } else {
914 writeln!(std::io::stdout(), "{}", formatted)
915 };
916 result.map_err(|e| LogSinkError {
917 message: e.to_string(),
918 })
919 }
920
921 fn name(&self) -> &str {
922 if self.use_stderr {
923 "console:stderr"
924 } else {
925 "console:stdout"
926 }
927 }
928}
929
930pub struct MemoryLogSink {
932 logs: RwLock<Vec<String>>,
934 capacity: usize,
936}
937
938impl MemoryLogSink {
939 pub fn new(capacity: usize) -> Self {
941 Self {
942 logs: RwLock::new(Vec::with_capacity(capacity)),
943 capacity,
944 }
945 }
946
947 pub fn logs(&self) -> Vec<String> {
949 self.logs.read().clone()
950 }
951
952 pub fn clear(&self) {
954 self.logs.write().clear();
955 }
956
957 pub fn len(&self) -> usize {
959 self.logs.read().len()
960 }
961
962 pub fn is_empty(&self) -> bool {
964 self.logs.read().is_empty()
965 }
966}
967
968impl LogSink for MemoryLogSink {
969 fn write(&self, _entry: &LogEntry, formatted: &str) -> Result<(), LogSinkError> {
970 let mut logs = self.logs.write();
971 if logs.len() >= self.capacity {
972 logs.remove(0);
973 }
974 logs.push(formatted.to_string());
975 Ok(())
976 }
977
978 fn name(&self) -> &str {
979 "memory"
980 }
981}
982
983pub struct FileLogSink {
985 path: String,
987 file: RwLock<Option<std::fs::File>>,
989}
990
991impl FileLogSink {
992 pub fn new(path: impl Into<String>) -> Result<Self, LogSinkError> {
994 let path = path.into();
995 let file = std::fs::OpenOptions::new()
996 .create(true)
997 .append(true)
998 .open(&path)
999 .map_err(|e| LogSinkError {
1000 message: format!("Failed to open log file: {}", e),
1001 })?;
1002
1003 Ok(Self {
1004 path,
1005 file: RwLock::new(Some(file)),
1006 })
1007 }
1008}
1009
1010impl LogSink for FileLogSink {
1011 fn write(&self, _entry: &LogEntry, formatted: &str) -> Result<(), LogSinkError> {
1012 let mut guard = self.file.write();
1013 if let Some(ref mut file) = *guard {
1014 writeln!(file, "{}", formatted).map_err(|e| LogSinkError {
1015 message: e.to_string(),
1016 })?;
1017 }
1018 Ok(())
1019 }
1020
1021 fn flush(&self) -> Result<(), LogSinkError> {
1022 let mut guard = self.file.write();
1023 if let Some(ref mut file) = *guard {
1024 file.flush().map_err(|e| LogSinkError {
1025 message: e.to_string(),
1026 })?;
1027 }
1028 Ok(())
1029 }
1030
1031 fn name(&self) -> &str {
1032 &self.path
1033 }
1034}
1035
1036use std::sync::OnceLock;
1041
1042static GLOBAL_LOGGER: OnceLock<StructuredLogger> = OnceLock::new();
1043
1044pub fn init(config: LogConfig) {
1046 let _ = GLOBAL_LOGGER.set(StructuredLogger::new(config));
1047}
1048
1049pub fn logger() -> &'static StructuredLogger {
1051 GLOBAL_LOGGER.get_or_init(StructuredLogger::default_logger)
1052}
1053
1054pub fn trace(message: &str, fields: &[(&str, &str)]) {
1056 logger().trace(message, fields);
1057}
1058
1059pub fn debug(message: &str, fields: &[(&str, &str)]) {
1061 logger().debug(message, fields);
1062}
1063
1064pub fn info(message: &str, fields: &[(&str, &str)]) {
1066 logger().info(message, fields);
1067}
1068
1069pub fn warn(message: &str, fields: &[(&str, &str)]) {
1071 logger().warn(message, fields);
1072}
1073
1074pub fn error(message: &str, fields: &[(&str, &str)]) {
1076 logger().error(message, fields);
1077}
1078
1079pub fn fatal(message: &str, fields: &[(&str, &str)]) {
1081 logger().fatal(message, fields);
1082}
1083
1084#[cfg(test)]
1089mod tests {
1090 use super::*;
1091
1092 #[test]
1093 fn test_log_level_ordering() {
1094 assert!(LogLevel::Trace < LogLevel::Debug);
1095 assert!(LogLevel::Debug < LogLevel::Info);
1096 assert!(LogLevel::Info < LogLevel::Warn);
1097 assert!(LogLevel::Warn < LogLevel::Error);
1098 assert!(LogLevel::Error < LogLevel::Fatal);
1099 }
1100
1101 #[test]
1102 fn test_log_level_from_str() {
1103 assert_eq!(LogLevel::parse("trace"), Some(LogLevel::Trace));
1104 assert_eq!(LogLevel::parse("DEBUG"), Some(LogLevel::Debug));
1105 assert_eq!(LogLevel::parse("Info"), Some(LogLevel::Info));
1106 assert_eq!(LogLevel::parse("WARNING"), Some(LogLevel::Warn));
1107 assert_eq!(LogLevel::parse("error"), Some(LogLevel::Error));
1108 assert_eq!(LogLevel::parse("FATAL"), Some(LogLevel::Fatal));
1109 assert_eq!(LogLevel::parse("CRITICAL"), Some(LogLevel::Fatal));
1110 assert_eq!(LogLevel::parse("invalid"), None);
1111 }
1112
1113 #[test]
1114 fn test_log_config_builder() {
1115 let config = LogConfig::builder()
1116 .level(LogLevel::Debug)
1117 .output(LogOutput::Json)
1118 .with_trace_correlation(true)
1119 .with_timestamps(true)
1120 .with_location(true)
1121 .service_name("test-service")
1122 .environment("test")
1123 .module_level("ringkernel::k2k", LogLevel::Trace)
1124 .global_field("version", "1.0.0")
1125 .build();
1126
1127 assert_eq!(config.level, LogLevel::Debug);
1128 assert_eq!(config.output, LogOutput::Json);
1129 assert!(config.trace_correlation);
1130 assert!(config.include_timestamps);
1131 assert!(config.include_location);
1132 assert_eq!(config.service_name, "test-service");
1133 assert_eq!(config.environment, "test");
1134 assert_eq!(
1135 config.effective_level("ringkernel::k2k::broker"),
1136 LogLevel::Trace
1137 );
1138 }
1139
1140 #[test]
1141 fn test_log_config_effective_level() {
1142 let config = LogConfig::builder()
1143 .level(LogLevel::Info)
1144 .module_level("ringkernel", LogLevel::Debug)
1145 .module_level("ringkernel::k2k", LogLevel::Trace)
1146 .build();
1147
1148 assert_eq!(config.effective_level("other::module"), LogLevel::Info);
1149 assert_eq!(config.effective_level("ringkernel::core"), LogLevel::Debug);
1150 assert_eq!(config.effective_level("ringkernel::k2k"), LogLevel::Trace);
1151 assert_eq!(
1152 config.effective_level("ringkernel::k2k::broker"),
1153 LogLevel::Trace
1154 );
1155 }
1156
1157 #[test]
1158 fn test_trace_context() {
1159 let ctx = TraceContext::with_new_trace()
1160 .with_field("user_id", "123")
1161 .with_field("request_id", "abc");
1162
1163 assert!(ctx.trace_id.is_some());
1164 assert!(ctx.span_id.is_some());
1165 assert_eq!(ctx.fields.get("user_id"), Some(&"123".to_string()));
1166 }
1167
1168 #[test]
1169 fn test_log_entry_json() {
1170 let config = LogConfig::builder()
1171 .service_name("test")
1172 .environment("dev")
1173 .with_timestamps(false)
1174 .with_trace_correlation(false)
1175 .build();
1176
1177 let entry = LogEntry::new(LogLevel::Info, "Test message").with_field("key", "value");
1178
1179 let json = entry.to_json(&config);
1180 assert!(json.contains(r#""level":"INFO""#));
1181 assert!(json.contains(r#""message":"Test message""#));
1182 assert!(json.contains(r#""service":"test""#));
1183 }
1184
1185 #[test]
1186 fn test_log_entry_text() {
1187 let config = LogConfig::builder()
1188 .with_timestamps(false)
1189 .with_trace_correlation(false)
1190 .build();
1191
1192 let entry = LogEntry::new(LogLevel::Warn, "Warning!").with_target("test::module");
1193
1194 let text = entry.to_text(&config);
1195 assert!(text.contains("WARN"));
1196 assert!(text.contains("[test::module]"));
1197 assert!(text.contains("Warning!"));
1198 }
1199
1200 #[test]
1201 fn test_structured_logger() {
1202 let logger = StructuredLogger::new(LogConfig::builder().level(LogLevel::Debug).build());
1203
1204 let sink = Arc::new(MemoryLogSink::new(100));
1205 logger.add_sink(sink.clone());
1206
1207 logger.info("Test message", &[("key", "value")]);
1208 logger.debug("Debug message", &[]);
1209 logger.trace("Trace message", &[]); assert_eq!(sink.len(), 2);
1212 }
1213
1214 #[test]
1215 fn test_memory_sink_capacity() {
1216 let sink = MemoryLogSink::new(3);
1217 let entry = LogEntry::new(LogLevel::Info, "msg");
1218
1219 sink.write(&entry, "log1").unwrap();
1220 sink.write(&entry, "log2").unwrap();
1221 sink.write(&entry, "log3").unwrap();
1222 sink.write(&entry, "log4").unwrap();
1223
1224 let logs = sink.logs();
1225 assert_eq!(logs.len(), 3);
1226 assert_eq!(logs[0], "log2");
1227 assert_eq!(logs[2], "log4");
1228 }
1229
1230 #[test]
1231 fn test_logger_stats() {
1232 let logger = StructuredLogger::new(LogConfig::default());
1233 let sink = Arc::new(MemoryLogSink::new(100));
1234 logger.add_sink(sink);
1235
1236 logger.info("info", &[]);
1237 logger.error("error", &[]);
1238 logger.warn("warn", &[]);
1239
1240 let stats = logger.stats();
1241 assert_eq!(stats.log_count, 3);
1242 assert_eq!(stats.error_count, 1);
1243 assert_eq!(stats.sink_count, 1);
1244 }
1245
1246 #[test]
1247 fn test_logger_disable() {
1248 let logger = StructuredLogger::new(LogConfig::default());
1249 let sink = Arc::new(MemoryLogSink::new(100));
1250 logger.add_sink(sink.clone());
1251
1252 logger.info("before", &[]);
1253 logger.set_enabled(false);
1254 logger.info("during", &[]);
1255 logger.set_enabled(true);
1256 logger.info("after", &[]);
1257
1258 assert_eq!(sink.len(), 2);
1259 }
1260
1261 #[test]
1262 fn test_log_value_display() {
1263 assert_eq!(LogValue::String("test".to_string()).to_string(), "test");
1264 assert_eq!(LogValue::Int(-42).to_string(), "-42");
1265 assert_eq!(LogValue::Uint(42).to_string(), "42");
1266 assert_eq!(LogValue::Bool(true).to_string(), "true");
1267 }
1268
1269 #[test]
1270 fn test_console_sink() {
1271 let sink = ConsoleSink::stderr();
1272 assert_eq!(sink.name(), "console:stderr");
1273
1274 let sink = ConsoleSink::stdout();
1275 assert_eq!(sink.name(), "console:stdout");
1276 }
1277
1278 #[test]
1279 fn test_log_config_presets() {
1280 let dev = LogConfig::development();
1281 assert_eq!(dev.level, LogLevel::Debug);
1282 assert_eq!(dev.output, LogOutput::Pretty);
1283 assert!(dev.include_location);
1284
1285 let prod = LogConfig::production();
1286 assert_eq!(prod.level, LogLevel::Info);
1287 assert_eq!(prod.output, LogOutput::Json);
1288 assert!(prod.include_thread_id);
1289 }
1290}