use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Trace => "TRACE",
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
Self::Fatal => "FATAL",
}
}
pub fn severity(&self) -> u8 {
match self {
Self::Trace => 0,
Self::Debug => 1,
Self::Info => 2,
Self::Warn => 3,
Self::Error => 4,
Self::Fatal => 5,
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl PartialOrd for LogLevel {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LogLevel {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.severity().cmp(&other.severity())
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub source: String,
pub timestamp: String,
pub context: HashMap<String, Value>,
pub span_id: Option<String>,
}
impl LogEntry {
pub fn builder(
level: LogLevel,
message: impl Into<String>,
source: impl Into<String>,
) -> LogEntryBuilder {
LogEntryBuilder {
level,
message: message.into(),
source: source.into(),
timestamp: String::new(),
context: HashMap::new(),
span_id: None,
}
}
pub fn to_json(&self) -> Value {
serde_json::json!({
"level": self.level.as_str(),
"message": self.message,
"source": self.source,
"timestamp": self.timestamp,
"context": self.context,
"span_id": self.span_id,
})
}
pub fn to_formatted_string(&self) -> String {
format!(
"[{} {}] {}: {}",
self.timestamp, self.level, self.source, self.message,
)
}
}
pub struct LogEntryBuilder {
level: LogLevel,
message: String,
source: String,
timestamp: String,
context: HashMap<String, Value>,
span_id: Option<String>,
}
impl LogEntryBuilder {
pub fn timestamp(mut self, ts: impl Into<String>) -> Self {
self.timestamp = ts.into();
self
}
pub fn context(mut self, key: impl Into<String>, value: Value) -> Self {
self.context.insert(key.into(), value);
self
}
pub fn span_id(mut self, id: impl Into<String>) -> Self {
self.span_id = Some(id.into());
self
}
pub fn build(self) -> LogEntry {
LogEntry {
level: self.level,
message: self.message,
source: self.source,
timestamp: self.timestamp,
context: self.context,
span_id: self.span_id,
}
}
}
#[derive(Debug, Clone)]
pub struct LogFilter {
min_level: Option<LogLevel>,
source: Option<String>,
contains: Option<String>,
}
impl LogFilter {
pub fn new() -> Self {
Self {
min_level: None,
source: None,
contains: None,
}
}
pub fn with_min_level(mut self, level: LogLevel) -> Self {
self.min_level = Some(level);
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_contains(mut self, text: impl Into<String>) -> Self {
self.contains = Some(text.into());
self
}
pub fn matches(&self, entry: &LogEntry) -> bool {
if let Some(ref min) = self.min_level {
if entry.level < *min {
return false;
}
}
if let Some(ref src) = self.source {
if entry.source != *src {
return false;
}
}
if let Some(ref text) = self.contains {
if !entry.message.contains(text.as_str()) {
return false;
}
}
true
}
}
impl Default for LogFilter {
fn default() -> Self {
Self::new()
}
}
pub struct Logger {
pub name: String,
entries: Vec<LogEntry>,
}
impl Logger {
pub fn new(name: String) -> Self {
Self {
name,
entries: Vec::new(),
}
}
pub fn log(&mut self, level: LogLevel, message: impl Into<String>, source: impl Into<String>) {
let entry = LogEntry {
level,
message: message.into(),
source: source.into(),
timestamp: String::new(),
context: HashMap::new(),
span_id: None,
};
self.entries.push(entry);
}
pub fn trace(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.log(LogLevel::Trace, message, source);
}
pub fn debug(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.log(LogLevel::Debug, message, source);
}
pub fn info(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.log(LogLevel::Info, message, source);
}
pub fn warn(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.log(LogLevel::Warn, message, source);
}
pub fn error(&mut self, message: impl Into<String>, source: impl Into<String>) {
self.log(LogLevel::Error, message, source);
}
pub fn entries(&self) -> &[LogEntry] {
&self.entries
}
pub fn filtered(&self, filter: &LogFilter) -> Vec<&LogEntry> {
self.entries.iter().filter(|e| filter.matches(e)).collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
pub trait LogSink {
fn write(&mut self, entry: &LogEntry);
fn flush(&mut self);
fn name(&self) -> &str;
}
pub struct InMemorySink {
entries: Vec<LogEntry>,
}
impl InMemorySink {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn entries(&self) -> &[LogEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for InMemorySink {
fn default() -> Self {
Self::new()
}
}
impl LogSink for InMemorySink {
fn write(&mut self, entry: &LogEntry) {
self.entries.push(entry.clone());
}
fn flush(&mut self) {
}
fn name(&self) -> &str {
"in_memory"
}
}
pub struct JsonSink {
json_entries: Vec<String>,
}
impl JsonSink {
pub fn new() -> Self {
Self {
json_entries: Vec::new(),
}
}
pub fn json_entries(&self) -> &[String] {
&self.json_entries
}
}
impl Default for JsonSink {
fn default() -> Self {
Self::new()
}
}
impl LogSink for JsonSink {
fn write(&mut self, entry: &LogEntry) {
let json = entry.to_json();
self.json_entries
.push(serde_json::to_string(&json).unwrap_or_default());
}
fn flush(&mut self) {
}
fn name(&self) -> &str {
"json"
}
}
pub struct MultiLogger {
sinks: Vec<Box<dyn LogSink>>,
}
impl MultiLogger {
pub fn new() -> Self {
Self { sinks: Vec::new() }
}
pub fn add_sink(&mut self, sink: Box<dyn LogSink>) {
self.sinks.push(sink);
}
pub fn log(&mut self, entry: LogEntry) {
for sink in &mut self.sinks {
sink.write(&entry);
}
}
pub fn flush_all(&mut self) {
for sink in &mut self.sinks {
sink.flush();
}
}
pub fn sink_count(&self) -> usize {
self.sinks.len()
}
}
impl Default for MultiLogger {
fn default() -> Self {
Self::new()
}
}
pub struct LogAggregator {
entries: Vec<LogEntry>,
}
impl LogAggregator {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn ingest(&mut self, entries: &[LogEntry]) {
for entry in entries {
self.entries.push(entry.clone());
}
}
pub fn count_by_level(&self) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
for entry in &self.entries {
*counts.entry(entry.level.as_str().to_string()).or_insert(0) += 1;
}
counts
}
pub fn error_rate(&self) -> f64 {
if self.entries.is_empty() {
return 0.0;
}
let error_count = self
.entries
.iter()
.filter(|e| e.level == LogLevel::Error || e.level == LogLevel::Fatal)
.count();
error_count as f64 / self.entries.len() as f64
}
pub fn most_common_source(&self) -> Option<String> {
if self.entries.is_empty() {
return None;
}
let mut counts: HashMap<&str, usize> = HashMap::new();
for entry in &self.entries {
*counts.entry(&entry.source).or_insert(0) += 1;
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(source, _)| source.to_string())
}
pub fn to_json(&self) -> Value {
serde_json::json!({
"total_entries": self.entries.len(),
"count_by_level": self.count_by_level(),
"error_rate": self.error_rate(),
"most_common_source": self.most_common_source(),
})
}
}
impl Default for LogAggregator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_as_str() {
assert_eq!(LogLevel::Trace.as_str(), "TRACE");
assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::Warn.as_str(), "WARN");
assert_eq!(LogLevel::Error.as_str(), "ERROR");
assert_eq!(LogLevel::Fatal.as_str(), "FATAL");
}
#[test]
fn test_log_level_severity() {
assert_eq!(LogLevel::Trace.severity(), 0);
assert_eq!(LogLevel::Debug.severity(), 1);
assert_eq!(LogLevel::Info.severity(), 2);
assert_eq!(LogLevel::Warn.severity(), 3);
assert_eq!(LogLevel::Error.severity(), 4);
assert_eq!(LogLevel::Fatal.severity(), 5);
}
#[test]
fn test_log_level_display() {
assert_eq!(LogLevel::Info.to_string(), "INFO");
assert_eq!(LogLevel::Fatal.to_string(), "FATAL");
}
#[test]
fn test_log_level_ordering() {
assert!(LogLevel::Trace < LogLevel::Debug);
assert!(LogLevel::Debug < LogLevel::Info);
assert!(LogLevel::Info < LogLevel::Warn);
assert!(LogLevel::Warn < LogLevel::Error);
assert!(LogLevel::Error < LogLevel::Fatal);
}
#[test]
fn test_log_level_equality() {
assert_eq!(LogLevel::Info, LogLevel::Info);
assert_ne!(LogLevel::Info, LogLevel::Debug);
}
#[test]
fn test_log_level_sort() {
let mut levels = vec![
LogLevel::Error,
LogLevel::Trace,
LogLevel::Warn,
LogLevel::Info,
LogLevel::Fatal,
LogLevel::Debug,
];
levels.sort();
assert_eq!(
levels,
vec![
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
LogLevel::Fatal,
]
);
}
#[test]
fn test_log_entry_builder_basic() {
let entry = LogEntry::builder(LogLevel::Info, "hello", "main")
.timestamp("2024-01-01T00:00:00Z")
.build();
assert_eq!(entry.level, LogLevel::Info);
assert_eq!(entry.message, "hello");
assert_eq!(entry.source, "main");
assert_eq!(entry.timestamp, "2024-01-01T00:00:00Z");
assert!(entry.context.is_empty());
assert!(entry.span_id.is_none());
}
#[test]
fn test_log_entry_builder_with_context() {
let entry = LogEntry::builder(LogLevel::Debug, "test", "src")
.context("key", serde_json::json!("value"))
.context("num", serde_json::json!(42))
.build();
assert_eq!(entry.context.len(), 2);
assert_eq!(entry.context["key"], serde_json::json!("value"));
assert_eq!(entry.context["num"], serde_json::json!(42));
}
#[test]
fn test_log_entry_builder_with_span_id() {
let entry = LogEntry::builder(LogLevel::Warn, "msg", "src")
.span_id("span-123")
.build();
assert_eq!(entry.span_id, Some("span-123".to_string()));
}
#[test]
fn test_log_entry_to_json() {
let entry = LogEntry::builder(LogLevel::Error, "failure", "network")
.timestamp("2024-06-15T12:00:00Z")
.context("code", serde_json::json!(500))
.build();
let json = entry.to_json();
assert_eq!(json["level"], "ERROR");
assert_eq!(json["message"], "failure");
assert_eq!(json["source"], "network");
assert_eq!(json["timestamp"], "2024-06-15T12:00:00Z");
assert_eq!(json["context"]["code"], 500);
assert!(json["span_id"].is_null());
}
#[test]
fn test_log_entry_to_formatted_string() {
let entry = LogEntry::builder(LogLevel::Info, "Agent started", "main")
.timestamp("2024-01-01")
.build();
let formatted = entry.to_formatted_string();
assert_eq!(formatted, "[2024-01-01 INFO] main: Agent started");
}
#[test]
fn test_log_entry_to_formatted_string_error() {
let entry = LogEntry::builder(LogLevel::Error, "crash", "core")
.timestamp("2024-12-31")
.build();
assert_eq!(
entry.to_formatted_string(),
"[2024-12-31 ERROR] core: crash"
);
}
#[test]
fn test_log_filter_empty_matches_all() {
let filter = LogFilter::new();
let entry = LogEntry::builder(LogLevel::Trace, "x", "y").build();
assert!(filter.matches(&entry));
}
#[test]
fn test_log_filter_min_level() {
let filter = LogFilter::new().with_min_level(LogLevel::Warn);
let trace = LogEntry::builder(LogLevel::Trace, "x", "y").build();
let warn = LogEntry::builder(LogLevel::Warn, "x", "y").build();
let error = LogEntry::builder(LogLevel::Error, "x", "y").build();
assert!(!filter.matches(&trace));
assert!(filter.matches(&warn));
assert!(filter.matches(&error));
}
#[test]
fn test_log_filter_source() {
let filter = LogFilter::new().with_source("network");
let net = LogEntry::builder(LogLevel::Info, "x", "network").build();
let db = LogEntry::builder(LogLevel::Info, "x", "database").build();
assert!(filter.matches(&net));
assert!(!filter.matches(&db));
}
#[test]
fn test_log_filter_contains() {
let filter = LogFilter::new().with_contains("timeout");
let yes = LogEntry::builder(LogLevel::Error, "connection timeout", "net").build();
let no = LogEntry::builder(LogLevel::Error, "connection refused", "net").build();
assert!(filter.matches(&yes));
assert!(!filter.matches(&no));
}
#[test]
fn test_log_filter_combined() {
let filter = LogFilter::new()
.with_min_level(LogLevel::Warn)
.with_source("network")
.with_contains("timeout");
let match_all = LogEntry::builder(LogLevel::Error, "connection timeout", "network").build();
let wrong_level =
LogEntry::builder(LogLevel::Debug, "connection timeout", "network").build();
let wrong_source = LogEntry::builder(LogLevel::Error, "connection timeout", "db").build();
let wrong_msg = LogEntry::builder(LogLevel::Error, "success", "network").build();
assert!(filter.matches(&match_all));
assert!(!filter.matches(&wrong_level));
assert!(!filter.matches(&wrong_source));
assert!(!filter.matches(&wrong_msg));
}
#[test]
fn test_log_filter_default() {
let filter = LogFilter::default();
let entry = LogEntry::builder(LogLevel::Fatal, "x", "y").build();
assert!(filter.matches(&entry));
}
#[test]
fn test_logger_new() {
let logger = Logger::new("test".to_string());
assert_eq!(logger.name, "test");
assert!(logger.is_empty());
assert_eq!(logger.len(), 0);
}
#[test]
fn test_logger_log() {
let mut logger = Logger::new("test".to_string());
logger.log(LogLevel::Info, "hello", "main");
assert_eq!(logger.len(), 1);
assert_eq!(logger.entries()[0].message, "hello");
assert_eq!(logger.entries()[0].level, LogLevel::Info);
assert_eq!(logger.entries()[0].source, "main");
}
#[test]
fn test_logger_convenience_methods() {
let mut logger = Logger::new("test".to_string());
logger.trace("t", "s");
logger.debug("d", "s");
logger.info("i", "s");
logger.warn("w", "s");
logger.error("e", "s");
assert_eq!(logger.len(), 5);
assert_eq!(logger.entries()[0].level, LogLevel::Trace);
assert_eq!(logger.entries()[1].level, LogLevel::Debug);
assert_eq!(logger.entries()[2].level, LogLevel::Info);
assert_eq!(logger.entries()[3].level, LogLevel::Warn);
assert_eq!(logger.entries()[4].level, LogLevel::Error);
}
#[test]
fn test_logger_filtered() {
let mut logger = Logger::new("test".to_string());
logger.info("ok", "main");
logger.warn("caution", "main");
logger.error("fail", "main");
let filter = LogFilter::new().with_min_level(LogLevel::Warn);
let results = logger.filtered(&filter);
assert_eq!(results.len(), 2);
assert_eq!(results[0].level, LogLevel::Warn);
assert_eq!(results[1].level, LogLevel::Error);
}
#[test]
fn test_logger_filtered_empty_result() {
let mut logger = Logger::new("test".to_string());
logger.info("ok", "main");
let filter = LogFilter::new().with_min_level(LogLevel::Fatal);
assert!(logger.filtered(&filter).is_empty());
}
#[test]
fn test_logger_clear() {
let mut logger = Logger::new("test".to_string());
logger.info("a", "s");
logger.info("b", "s");
assert_eq!(logger.len(), 2);
logger.clear();
assert!(logger.is_empty());
assert_eq!(logger.len(), 0);
}
#[test]
fn test_logger_entries_order() {
let mut logger = Logger::new("test".to_string());
logger.info("first", "s");
logger.info("second", "s");
logger.info("third", "s");
assert_eq!(logger.entries()[0].message, "first");
assert_eq!(logger.entries()[1].message, "second");
assert_eq!(logger.entries()[2].message, "third");
}
#[test]
fn test_in_memory_sink_new() {
let sink = InMemorySink::new();
assert!(sink.is_empty());
assert_eq!(sink.len(), 0);
}
#[test]
fn test_in_memory_sink_write() {
let mut sink = InMemorySink::new();
let entry = LogEntry::builder(LogLevel::Info, "test", "src").build();
sink.write(&entry);
assert_eq!(sink.len(), 1);
assert_eq!(sink.entries()[0].message, "test");
}
#[test]
fn test_in_memory_sink_name() {
let sink = InMemorySink::new();
assert_eq!(sink.name(), "in_memory");
}
#[test]
fn test_in_memory_sink_multiple_writes() {
let mut sink = InMemorySink::new();
for i in 0..10 {
let entry = LogEntry::builder(LogLevel::Debug, format!("msg-{}", i), "src").build();
sink.write(&entry);
}
assert_eq!(sink.len(), 10);
}
#[test]
fn test_json_sink_new() {
let sink = JsonSink::new();
assert!(sink.json_entries().is_empty());
}
#[test]
fn test_json_sink_write() {
let mut sink = JsonSink::new();
let entry = LogEntry::builder(LogLevel::Warn, "warning", "net")
.timestamp("2024-01-01")
.build();
sink.write(&entry);
assert_eq!(sink.json_entries().len(), 1);
let parsed: Value = serde_json::from_str(&sink.json_entries()[0]).unwrap();
assert_eq!(parsed["level"], "WARN");
assert_eq!(parsed["message"], "warning");
}
#[test]
fn test_json_sink_name() {
let sink = JsonSink::new();
assert_eq!(sink.name(), "json");
}
#[test]
fn test_json_sink_produces_valid_json() {
let mut sink = JsonSink::new();
let entry = LogEntry::builder(LogLevel::Error, "err", "core")
.context("detail", serde_json::json!("something broke"))
.build();
sink.write(&entry);
let parsed: Value = serde_json::from_str(&sink.json_entries()[0]).unwrap();
assert_eq!(parsed["context"]["detail"], "something broke");
}
#[test]
fn test_multi_logger_new() {
let ml = MultiLogger::new();
assert_eq!(ml.sink_count(), 0);
}
#[test]
fn test_multi_logger_add_sink() {
let mut ml = MultiLogger::new();
ml.add_sink(Box::new(InMemorySink::new()));
ml.add_sink(Box::new(JsonSink::new()));
assert_eq!(ml.sink_count(), 2);
}
#[test]
fn test_multi_logger_dispatches_to_all_sinks() {
let mut ml = MultiLogger::new();
ml.add_sink(Box::new(InMemorySink::new()));
ml.add_sink(Box::new(JsonSink::new()));
let entry = LogEntry::builder(LogLevel::Info, "dispatch test", "ml").build();
ml.log(entry);
assert_eq!(ml.sink_count(), 2);
}
#[test]
fn test_multi_logger_flush_all() {
let mut ml = MultiLogger::new();
ml.add_sink(Box::new(InMemorySink::new()));
ml.flush_all(); }
#[test]
fn test_multi_logger_default() {
let ml = MultiLogger::default();
assert_eq!(ml.sink_count(), 0);
}
#[test]
fn test_multi_logger_log_no_sinks() {
let mut ml = MultiLogger::new();
let entry = LogEntry::builder(LogLevel::Fatal, "no sinks", "test").build();
ml.log(entry); }
#[test]
fn test_aggregator_new() {
let agg = LogAggregator::new();
assert!(agg.count_by_level().is_empty());
assert_eq!(agg.error_rate(), 0.0);
assert_eq!(agg.most_common_source(), None);
}
#[test]
fn test_aggregator_count_by_level() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Info, "a", "s").build(),
LogEntry::builder(LogLevel::Info, "b", "s").build(),
LogEntry::builder(LogLevel::Error, "c", "s").build(),
];
agg.ingest(&entries);
let counts = agg.count_by_level();
assert_eq!(counts["INFO"], 2);
assert_eq!(counts["ERROR"], 1);
}
#[test]
fn test_aggregator_error_rate() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Info, "a", "s").build(),
LogEntry::builder(LogLevel::Error, "b", "s").build(),
LogEntry::builder(LogLevel::Fatal, "c", "s").build(),
LogEntry::builder(LogLevel::Info, "d", "s").build(),
];
agg.ingest(&entries);
let rate = agg.error_rate();
assert!((rate - 0.5).abs() < 1e-10);
}
#[test]
fn test_aggregator_error_rate_zero() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Info, "a", "s").build(),
LogEntry::builder(LogLevel::Debug, "b", "s").build(),
];
agg.ingest(&entries);
assert_eq!(agg.error_rate(), 0.0);
}
#[test]
fn test_aggregator_error_rate_all_errors() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Error, "a", "s").build(),
LogEntry::builder(LogLevel::Fatal, "b", "s").build(),
];
agg.ingest(&entries);
assert!((agg.error_rate() - 1.0).abs() < 1e-10);
}
#[test]
fn test_aggregator_most_common_source() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Info, "a", "network").build(),
LogEntry::builder(LogLevel::Info, "b", "network").build(),
LogEntry::builder(LogLevel::Info, "c", "database").build(),
];
agg.ingest(&entries);
assert_eq!(agg.most_common_source(), Some("network".to_string()));
}
#[test]
fn test_aggregator_most_common_source_empty() {
let agg = LogAggregator::new();
assert_eq!(agg.most_common_source(), None);
}
#[test]
fn test_aggregator_to_json() {
let mut agg = LogAggregator::new();
let entries = vec![
LogEntry::builder(LogLevel::Info, "a", "src").build(),
LogEntry::builder(LogLevel::Error, "b", "src").build(),
];
agg.ingest(&entries);
let json = agg.to_json();
assert_eq!(json["total_entries"], 2);
assert_eq!(json["most_common_source"], "src");
assert!(json["error_rate"].is_number());
assert!(json["count_by_level"].is_object());
}
#[test]
fn test_aggregator_multiple_ingests() {
let mut agg = LogAggregator::new();
let batch1 = vec![LogEntry::builder(LogLevel::Info, "a", "s").build()];
let batch2 = vec![LogEntry::builder(LogLevel::Warn, "b", "s").build()];
agg.ingest(&batch1);
agg.ingest(&batch2);
let counts = agg.count_by_level();
assert_eq!(counts["INFO"], 1);
assert_eq!(counts["WARN"], 1);
}
#[test]
fn test_aggregator_default() {
let agg = LogAggregator::default();
assert_eq!(agg.error_rate(), 0.0);
}
#[test]
fn test_log_entry_empty_strings() {
let entry = LogEntry::builder(LogLevel::Info, "", "").build();
assert_eq!(entry.message, "");
assert_eq!(entry.source, "");
assert_eq!(entry.to_formatted_string(), "[ INFO] : ");
}
#[test]
fn test_logger_filter_by_source_and_level() {
let mut logger = Logger::new("test".to_string());
logger.info("ok", "api");
logger.warn("slow", "api");
logger.error("fail", "db");
logger.error("crash", "api");
let filter = LogFilter::new()
.with_min_level(LogLevel::Error)
.with_source("api");
let results = logger.filtered(&filter);
assert_eq!(results.len(), 1);
assert_eq!(results[0].message, "crash");
}
#[test]
fn test_log_level_clone() {
let level = LogLevel::Warn;
let cloned = level.clone();
assert_eq!(level, cloned);
}
#[test]
fn test_log_entry_clone() {
let entry = LogEntry::builder(LogLevel::Info, "clone me", "src")
.context("k", serde_json::json!("v"))
.span_id("s1")
.build();
let cloned = entry.clone();
assert_eq!(cloned.message, "clone me");
assert_eq!(cloned.span_id, Some("s1".to_string()));
assert_eq!(cloned.context["k"], serde_json::json!("v"));
}
#[test]
fn test_log_filter_contains_partial_match() {
let filter = LogFilter::new().with_contains("time");
let yes = LogEntry::builder(LogLevel::Info, "timeout error", "s").build();
let also_yes = LogEntry::builder(LogLevel::Info, "runtime", "s").build();
let no = LogEntry::builder(LogLevel::Info, "other", "s").build();
assert!(filter.matches(&yes));
assert!(filter.matches(&also_yes));
assert!(!filter.matches(&no));
}
}