use crate::{LogEntry, LogLevel, Logger};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub enum RotationPolicy {
Size(u64),
Time(Duration),
SizeOrTime(u64, Duration),
}
impl RotationPolicy {
fn should_rotate(&self, current_size: u64, elapsed: Duration) -> bool {
match self {
RotationPolicy::Size(max_bytes) => current_size >= *max_bytes,
RotationPolicy::Time(max_age) => elapsed >= *max_age,
RotationPolicy::SizeOrTime(max_bytes, max_age) => {
current_size >= *max_bytes || elapsed >= *max_age
}
}
}
}
pub struct LogRotator {
policy: RotationPolicy,
max_files: usize,
current: Mutex<Vec<u8>>,
rotated: Mutex<Vec<Vec<u8>>>,
started_at: Mutex<Instant>,
rotation_count: Mutex<u64>,
}
impl LogRotator {
pub fn new(policy: RotationPolicy, max_files: usize) -> Self {
Self {
policy,
max_files,
current: Mutex::new(Vec::new()),
rotated: Mutex::new(Vec::new()),
started_at: Mutex::new(Instant::now()),
rotation_count: Mutex::new(0),
}
}
pub fn write(&self, data: &[u8]) {
let should_rotate = {
let mut current = self.current.lock();
current.extend_from_slice(data);
let started_at = self.started_at.lock();
self.policy
.should_rotate(current.len() as u64, started_at.elapsed())
};
if should_rotate {
self.rotate();
}
}
pub fn rotate(&self) {
let mut current = self.current.lock();
let mut rotated = self.rotated.lock();
let mut started_at = self.started_at.lock();
let mut count = self.rotation_count.lock();
let old_buffer = std::mem::take(&mut *current);
if !old_buffer.is_empty() {
rotated.insert(0, old_buffer);
}
while rotated.len() > self.max_files {
rotated.pop();
}
*started_at = Instant::now();
*count += 1;
}
pub fn current_size(&self) -> usize {
self.current.lock().len()
}
pub fn rotated_count(&self) -> usize {
self.rotated.lock().len()
}
pub fn total_rotations(&self) -> u64 {
*self.rotation_count.lock()
}
pub fn current_content(&self) -> Vec<u8> {
self.current.lock().clone()
}
pub fn rotated_content(&self, index: usize) -> Option<Vec<u8>> {
let rotated = self.rotated.lock();
rotated.get(index).cloned()
}
pub fn current_age(&self) -> Duration {
self.started_at.lock().elapsed()
}
pub fn policy(&self) -> &RotationPolicy {
&self.policy
}
pub fn max_files(&self) -> usize {
self.max_files
}
pub fn clear(&self) {
self.current.lock().clear();
self.rotated.lock().clear();
*self.started_at.lock() = Instant::now();
}
}
pub trait LogSink: Send + Sync {
fn write(&self, entry: &LogEntry);
fn name(&self) -> &str;
}
pub struct MemorySink {
name: String,
entries: Mutex<Vec<LogEntry>>,
}
impl MemorySink {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
entries: Mutex::new(Vec::new()),
}
}
pub fn entries(&self) -> Vec<LogEntry> {
self.entries.lock().clone()
}
pub fn len(&self) -> usize {
self.entries.lock().len()
}
pub fn is_empty(&self) -> bool {
self.entries.lock().is_empty()
}
pub fn clear(&self) {
self.entries.lock().clear();
}
}
impl LogSink for MemorySink {
fn write(&self, entry: &LogEntry) {
let mut entries = self.entries.lock();
entries.push(entry.clone());
}
fn name(&self) -> &str {
&self.name
}
}
pub struct ConsoleSink {
name: String,
}
impl ConsoleSink {
pub fn new() -> Self {
Self {
name: "console".to_string(),
}
}
pub fn with_name(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
impl Default for ConsoleSink {
fn default() -> Self {
Self::new()
}
}
impl LogSink for ConsoleSink {
fn write(&self, entry: &LogEntry) {
println!(
"[{}] {} - {}",
entry.level.as_str(),
entry.timestamp,
entry.message
);
}
fn name(&self) -> &str {
&self.name
}
}
pub struct CallbackSink<F>
where
F: Fn(&LogEntry) + Send + Sync,
{
name: String,
callback: F,
}
impl<F> CallbackSink<F>
where
F: Fn(&LogEntry) + Send + Sync,
{
pub fn new(name: impl Into<String>, callback: F) -> Self {
Self {
name: name.into(),
callback,
}
}
}
impl<F> LogSink for CallbackSink<F>
where
F: Fn(&LogEntry) + Send + Sync,
{
fn write(&self, entry: &LogEntry) {
(self.callback)(entry);
}
fn name(&self) -> &str {
&self.name
}
}
pub struct MultiOutputLogger {
level: LogLevel,
sinks: Vec<Arc<dyn LogSink>>,
}
impl MultiOutputLogger {
pub fn new(level: LogLevel) -> Self {
Self {
level,
sinks: Vec::new(),
}
}
pub fn add_sink(&mut self, sink: Arc<dyn LogSink>) -> &mut Self {
self.sinks.push(sink);
self
}
pub fn sink_names(&self) -> Vec<String> {
self.sinks.iter().map(|s| s.name().to_string()).collect()
}
pub fn sink_count(&self) -> usize {
self.sinks.len()
}
pub fn level(&self) -> LogLevel {
self.level
}
}
impl Logger for MultiOutputLogger {
fn log(&self, level: LogLevel, msg: &str) {
if level < self.level {
return;
}
let entry = LogEntry {
level,
message: msg.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
};
for sink in &self.sinks {
sink.write(&entry);
}
}
}
#[derive(Debug, Clone)]
pub struct LevelFilter {
default_level: LogLevel,
target_levels: HashMap<String, LogLevel>,
}
impl LevelFilter {
pub fn new(default_level: LogLevel) -> Self {
Self {
default_level,
target_levels: HashMap::new(),
}
}
pub fn with_target_level(mut self, target: impl Into<String>, level: LogLevel) -> Self {
self.target_levels.insert(target.into(), level);
self
}
pub fn remove_target(&mut self, target: &str) -> Option<LogLevel> {
self.target_levels.remove(target)
}
pub fn level_for(&self, target: &str) -> LogLevel {
self.target_levels
.get(target)
.copied()
.unwrap_or(self.default_level)
}
pub fn default_level(&self) -> LogLevel {
self.default_level
}
pub fn set_default_level(&mut self, level: LogLevel) {
self.default_level = level;
}
pub fn should_log(&self, target: &str, level: LogLevel) -> bool {
level >= self.level_for(target)
}
pub fn target_count(&self) -> usize {
self.target_levels.len()
}
pub fn targets(&self) -> Vec<String> {
self.target_levels.keys().cloned().collect()
}
}
impl Default for LevelFilter {
fn default() -> Self {
Self::new(LogLevel::Info)
}
}
pub type StructuredFields = HashMap<String, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuredLogEntry {
pub level: LogLevel,
pub message: String,
pub timestamp: String,
pub target: Option<String>,
pub fields: StructuredFields,
}
impl StructuredLogEntry {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
Self {
level,
message: message.into(),
timestamp: chrono::Utc::now().to_rfc3339(),
target: None,
fields: HashMap::new(),
}
}
pub fn with_target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.fields.insert(key.into(), value.into());
self
}
pub fn from_log_entry(entry: &LogEntry) -> Self {
Self {
level: entry.level,
message: entry.message.clone(),
timestamp: entry.timestamp.clone(),
target: None,
fields: HashMap::new(),
}
}
pub fn to_log_entry(&self) -> LogEntry {
LogEntry {
level: self.level,
message: self.message.clone(),
timestamp: self.timestamp.clone(),
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn format_fields(&self) -> String {
let mut pairs: Vec<String> = self
.fields
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
pairs.sort(); pairs.join(" ")
}
}
pub struct StructuredLogWriter {
filter: LevelFilter,
sinks: Vec<Arc<dyn StructuredSink>>,
}
pub trait StructuredSink: Send + Sync {
fn write(&self, entry: &StructuredLogEntry);
fn name(&self) -> &str;
}
pub struct MemoryStructuredSink {
name: String,
entries: Mutex<Vec<StructuredLogEntry>>,
}
impl MemoryStructuredSink {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
entries: Mutex::new(Vec::new()),
}
}
pub fn entries(&self) -> Vec<StructuredLogEntry> {
self.entries.lock().clone()
}
pub fn len(&self) -> usize {
self.entries.lock().len()
}
pub fn is_empty(&self) -> bool {
self.entries.lock().is_empty()
}
pub fn clear(&self) {
self.entries.lock().clear();
}
}
impl StructuredSink for MemoryStructuredSink {
fn write(&self, entry: &StructuredLogEntry) {
let mut entries = self.entries.lock();
entries.push(entry.clone());
}
fn name(&self) -> &str {
&self.name
}
}
impl StructuredLogWriter {
pub fn new(filter: LevelFilter) -> Self {
Self {
filter,
sinks: Vec::new(),
}
}
pub fn add_sink(&mut self, sink: Arc<dyn StructuredSink>) -> &mut Self {
self.sinks.push(sink);
self
}
pub fn sink_count(&self) -> usize {
self.sinks.len()
}
pub fn sink_names(&self) -> Vec<String> {
self.sinks.iter().map(|s| s.name().to_string()).collect()
}
pub fn filter(&self) -> &LevelFilter {
&self.filter
}
pub fn filter_mut(&mut self) -> &mut LevelFilter {
&mut self.filter
}
pub fn log(&self, entry: &StructuredLogEntry) {
let target = entry.target.as_deref().unwrap_or("");
if !self.filter.should_log(target, entry.level) {
return;
}
for sink in &self.sinks {
sink.write(entry);
}
}
pub fn log_with_fields(
&self,
target: impl Into<String>,
level: LogLevel,
message: impl Into<String>,
fields: StructuredFields,
) {
let entry = StructuredLogEntry::new(level, message)
.with_target(target)
.with_fields(fields);
self.log(&entry);
}
}
impl StructuredLogEntry {
pub fn with_fields(mut self, fields: StructuredFields) -> Self {
self.fields.extend(fields);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_rotation_policy_size_met() {
let policy = RotationPolicy::Size(100);
assert!(policy.should_rotate(100, Duration::from_secs(0)));
assert!(policy.should_rotate(101, Duration::from_secs(0)));
assert!(!policy.should_rotate(99, Duration::from_secs(0)));
}
#[test]
fn test_rotation_policy_time_met() {
let policy = RotationPolicy::Time(Duration::from_secs(60));
assert!(policy.should_rotate(0, Duration::from_secs(60)));
assert!(policy.should_rotate(0, Duration::from_secs(61)));
assert!(!policy.should_rotate(0, Duration::from_secs(59)));
}
#[test]
fn test_rotation_policy_size_or_time_either() {
let policy = RotationPolicy::SizeOrTime(100, Duration::from_secs(60));
assert!(policy.should_rotate(100, Duration::from_secs(0)));
assert!(policy.should_rotate(0, Duration::from_secs(60)));
assert!(!policy.should_rotate(99, Duration::from_secs(59)));
assert!(policy.should_rotate(200, Duration::from_secs(120)));
}
#[test]
fn test_log_rotator_new_empty() {
let rotator = LogRotator::new(RotationPolicy::Size(1024), 3);
assert_eq!(rotator.current_size(), 0);
assert_eq!(rotator.rotated_count(), 0);
assert_eq!(rotator.total_rotations(), 0);
assert_eq!(rotator.max_files(), 3);
}
#[test]
fn test_log_rotator_write_accumulates() {
let rotator = LogRotator::new(RotationPolicy::Size(1024), 3);
rotator.write(b"hello");
rotator.write(b" world");
assert_eq!(rotator.current_size(), 11);
assert_eq!(rotator.current_content(), b"hello world");
}
#[test]
fn test_log_rotator_size_triggers_rotation() {
let rotator = LogRotator::new(RotationPolicy::Size(10), 3);
rotator.write(b"12345"); assert_eq!(rotator.rotated_count(), 0);
rotator.write(b"67890"); assert_eq!(rotator.rotated_count(), 1);
assert_eq!(rotator.total_rotations(), 1);
assert_eq!(rotator.current_size(), 0);
}
#[test]
fn test_log_rotator_rotation_preserves_content() {
let rotator = LogRotator::new(RotationPolicy::Size(10), 3);
rotator.write(b"hello world"); assert_eq!(rotator.rotated_count(), 1);
let rotated = rotator.rotated_content(0).expect("rotated[0] must exist");
assert_eq!(rotated, b"hello world");
}
#[test]
fn test_log_rotator_max_files_drops_oldest() {
let rotator = LogRotator::new(RotationPolicy::Size(5), 2);
rotator.write(b"AAAAAA"); rotator.write(b"BBBBBB"); rotator.write(b"CCCCCC"); assert_eq!(rotator.rotated_count(), 2);
assert_eq!(rotator.total_rotations(), 3);
assert_eq!(rotator.rotated_content(0), Some(b"CCCCCC".to_vec()));
assert_eq!(rotator.rotated_content(1), Some(b"BBBBBB".to_vec()));
assert_eq!(rotator.rotated_content(2), None);
}
#[test]
fn test_log_rotator_manual_rotate() {
let rotator = LogRotator::new(RotationPolicy::Size(1024), 3);
rotator.write(b"some data");
rotator.rotate();
assert_eq!(rotator.rotated_count(), 1);
assert_eq!(rotator.current_size(), 0);
}
#[test]
fn test_log_rotator_manual_rotate_empty_no_op() {
let rotator = LogRotator::new(RotationPolicy::Size(1024), 3);
rotator.rotate(); assert_eq!(rotator.rotated_count(), 0);
assert_eq!(rotator.total_rotations(), 1); }
#[test]
fn test_log_rotator_clear() {
let rotator = LogRotator::new(RotationPolicy::Size(5), 3);
rotator.write(b"hello world"); rotator.write(b"more");
assert!(!rotator.current_content().is_empty());
assert_eq!(rotator.rotated_count(), 1);
rotator.clear();
assert_eq!(rotator.current_size(), 0);
assert_eq!(rotator.rotated_count(), 0);
}
#[test]
fn test_log_rotator_policy_accessor() {
let rotator = LogRotator::new(RotationPolicy::Size(256), 5);
match rotator.policy() {
RotationPolicy::Size(n) => assert_eq!(*n, 256),
_ => panic!("expected Size policy"),
}
}
#[test]
fn test_log_rotator_time_based_rotation() {
let rotator = LogRotator::new(RotationPolicy::Time(Duration::from_millis(50)), 3);
rotator.write(b"data");
assert_eq!(rotator.rotated_count(), 0);
thread::sleep(Duration::from_millis(60));
rotator.write(b"more"); assert_eq!(rotator.rotated_count(), 1);
}
#[test]
fn test_log_rotator_current_age_increases() {
let rotator = LogRotator::new(RotationPolicy::Size(1024), 3);
let age1 = rotator.current_age();
thread::sleep(Duration::from_millis(10));
let age2 = rotator.current_age();
assert!(age2 > age1);
}
#[test]
fn test_memory_sink_new_empty() {
let sink = MemorySink::new("test");
assert_eq!(sink.name(), "test");
assert!(sink.is_empty());
assert_eq!(sink.len(), 0);
}
#[test]
fn test_memory_sink_write_stores_entry() {
let sink = MemorySink::new("mem");
let entry = LogEntry {
level: LogLevel::Info,
message: "hello".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
sink.write(&entry);
assert_eq!(sink.len(), 1);
assert_eq!(sink.entries()[0].message, "hello");
}
#[test]
fn test_memory_sink_clear() {
let sink = MemorySink::new("mem");
let entry = LogEntry {
level: LogLevel::Info,
message: "x".to_string(),
timestamp: "t".to_string(),
};
sink.write(&entry);
sink.write(&entry);
assert_eq!(sink.len(), 2);
sink.clear();
assert!(sink.is_empty());
}
#[test]
fn test_console_sink_name_default() {
let sink = ConsoleSink::new();
assert_eq!(sink.name(), "console");
}
#[test]
fn test_console_sink_custom_name() {
let sink = ConsoleSink::with_name("stdout");
assert_eq!(sink.name(), "stdout");
}
#[test]
fn test_console_sink_write_does_not_panic() {
let sink = ConsoleSink::new();
let entry = LogEntry {
level: LogLevel::Info,
message: "test".to_string(),
timestamp: "t".to_string(),
};
sink.write(&entry); }
#[test]
fn test_callback_sink_invokes_closure() {
let counter = Arc::new(Mutex::new(0u32));
let c = counter.clone();
let sink = CallbackSink::new("cb", move |_entry| {
*c.lock() += 1;
});
let entry = LogEntry {
level: LogLevel::Info,
message: "x".to_string(),
timestamp: "t".to_string(),
};
sink.write(&entry);
sink.write(&entry);
assert_eq!(*counter.lock(), 2);
}
#[test]
fn test_multi_output_logger_fans_out_to_all_sinks() {
let mut logger = MultiOutputLogger::new(LogLevel::Debug);
let sink1 = Arc::new(MemorySink::new("s1"));
let sink2 = Arc::new(MemorySink::new("s2"));
logger.add_sink(sink1.clone());
logger.add_sink(sink2.clone());
logger.log(LogLevel::Info, "hello");
assert_eq!(sink1.len(), 1);
assert_eq!(sink2.len(), 1);
assert_eq!(sink1.entries()[0].message, "hello");
assert_eq!(sink2.entries()[0].message, "hello");
}
#[test]
fn test_multi_output_logger_respects_level_filter() {
let mut logger = MultiOutputLogger::new(LogLevel::Warn);
let sink = Arc::new(MemorySink::new("s"));
logger.add_sink(sink.clone());
logger.log(LogLevel::Debug, "debug"); logger.log(LogLevel::Info, "info"); logger.log(LogLevel::Warn, "warn"); logger.log(LogLevel::Error, "error");
assert_eq!(sink.len(), 2);
}
#[test]
fn test_multi_output_logger_sink_names() {
let mut logger = MultiOutputLogger::new(LogLevel::Info);
logger.add_sink(Arc::new(MemorySink::new("alpha")));
logger.add_sink(Arc::new(MemorySink::new("beta")));
let names = logger.sink_names();
assert_eq!(names, vec!["alpha", "beta"]);
assert_eq!(logger.sink_count(), 2);
}
#[test]
fn test_multi_output_logger_empty_sinks_no_error() {
let logger = MultiOutputLogger::new(LogLevel::Info);
logger.log(LogLevel::Info, "msg"); assert_eq!(logger.sink_count(), 0);
}
#[test]
fn test_multi_output_logger_level_accessor() {
let logger = MultiOutputLogger::new(LogLevel::Error);
assert_eq!(logger.level(), LogLevel::Error);
}
#[test]
fn test_level_filter_default_level() {
let filter = LevelFilter::new(LogLevel::Info);
assert_eq!(filter.default_level(), LogLevel::Info);
assert_eq!(filter.level_for("anything"), LogLevel::Info);
assert!(filter.should_log("anything", LogLevel::Info));
assert!(!filter.should_log("anything", LogLevel::Debug));
}
#[test]
fn test_level_filter_target_override() {
let filter = LevelFilter::new(LogLevel::Info)
.with_target_level("database", LogLevel::Debug)
.with_target_level("http", LogLevel::Warn);
assert!(filter.should_log("database", LogLevel::Debug));
assert_eq!(filter.level_for("database"), LogLevel::Debug);
assert!(!filter.should_log("http", LogLevel::Info));
assert!(filter.should_log("http", LogLevel::Warn));
assert_eq!(filter.level_for("http"), LogLevel::Warn);
assert_eq!(filter.level_for("cache"), LogLevel::Info);
assert!(filter.should_log("cache", LogLevel::Info));
assert!(!filter.should_log("cache", LogLevel::Debug));
}
#[test]
fn test_level_filter_remove_target() {
let mut filter = LevelFilter::new(LogLevel::Info).with_target_level("db", LogLevel::Debug);
assert_eq!(filter.level_for("db"), LogLevel::Debug);
let removed = filter.remove_target("db");
assert_eq!(removed, Some(LogLevel::Debug));
assert_eq!(filter.level_for("db"), LogLevel::Info); }
#[test]
fn test_level_filter_remove_missing_target_returns_none() {
let mut filter = LevelFilter::new(LogLevel::Info);
assert_eq!(filter.remove_target("never"), None);
}
#[test]
fn test_level_filter_set_default_level() {
let mut filter = LevelFilter::new(LogLevel::Info);
filter.set_default_level(LogLevel::Debug);
assert_eq!(filter.default_level(), LogLevel::Debug);
assert!(filter.should_log("any", LogLevel::Debug));
}
#[test]
fn test_level_filter_target_count_and_names() {
let filter = LevelFilter::new(LogLevel::Info)
.with_target_level("a", LogLevel::Debug)
.with_target_level("b", LogLevel::Warn);
assert_eq!(filter.target_count(), 2);
let mut targets = filter.targets();
targets.sort();
assert_eq!(targets, vec!["a", "b"]);
}
#[test]
fn test_level_filter_default_impl() {
let filter = LevelFilter::default();
assert_eq!(filter.default_level(), LogLevel::Info);
}
#[test]
fn test_structured_log_entry_new() {
let entry = StructuredLogEntry::new(LogLevel::Info, "hello");
assert_eq!(entry.level, LogLevel::Info);
assert_eq!(entry.message, "hello");
assert!(entry.target.is_none());
assert!(entry.fields.is_empty());
assert!(!entry.timestamp.is_empty());
}
#[test]
fn test_structured_log_entry_with_target() {
let entry = StructuredLogEntry::new(LogLevel::Info, "msg").with_target("database");
assert_eq!(entry.target, Some("database".to_string()));
}
#[test]
fn test_structured_log_entry_with_field() {
let entry = StructuredLogEntry::new(LogLevel::Info, "msg")
.with_field("user_id", "12345")
.with_field("action", "login");
assert_eq!(entry.fields.get("user_id"), Some(&"12345".to_string()));
assert_eq!(entry.fields.get("action"), Some(&"login".to_string()));
assert_eq!(entry.fields.len(), 2);
}
#[test]
fn test_structured_log_entry_with_fields_batch() {
let mut fields = StructuredFields::new();
fields.insert("k1".to_string(), "v1".to_string());
fields.insert("k2".to_string(), "v2".to_string());
let entry = StructuredLogEntry::new(LogLevel::Info, "msg").with_fields(fields);
assert_eq!(entry.fields.len(), 2);
}
#[test]
fn test_structured_log_entry_from_log_entry() {
let original = LogEntry {
level: LogLevel::Warn,
message: "warning msg".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
let structured = StructuredLogEntry::from_log_entry(&original);
assert_eq!(structured.level, LogLevel::Warn);
assert_eq!(structured.message, "warning msg");
assert_eq!(structured.timestamp, "2024-01-01T00:00:00Z");
assert!(structured.target.is_none());
assert!(structured.fields.is_empty());
}
#[test]
fn test_structured_log_entry_to_log_entry() {
let structured = StructuredLogEntry::new(LogLevel::Error, "err")
.with_target("db")
.with_field("code", "500");
let plain = structured.to_log_entry();
assert_eq!(plain.level, LogLevel::Error);
assert_eq!(plain.message, "err");
}
#[test]
fn test_structured_log_entry_json_roundtrip() {
let entry = StructuredLogEntry::new(LogLevel::Info, "test")
.with_target("app")
.with_field("key", "value");
let json = entry.to_json().expect("serialize");
let back = StructuredLogEntry::from_json(&json).expect("deserialize");
assert_eq!(back.level, entry.level);
assert_eq!(back.message, entry.message);
assert_eq!(back.target, entry.target);
assert_eq!(back.fields, entry.fields);
}
#[test]
fn test_structured_log_entry_format_fields_sorted() {
let entry = StructuredLogEntry::new(LogLevel::Info, "msg")
.with_field("zebra", "1")
.with_field("alpha", "2")
.with_field("middle", "3");
let formatted = entry.format_fields();
assert_eq!(formatted, "alpha=2 middle=3 zebra=1");
}
#[test]
fn test_structured_log_entry_format_fields_empty() {
let entry = StructuredLogEntry::new(LogLevel::Info, "msg");
assert_eq!(entry.format_fields(), "");
}
#[test]
fn test_memory_structured_sink_new_empty() {
let sink = MemoryStructuredSink::new("s");
assert_eq!(sink.name(), "s");
assert!(sink.is_empty());
}
#[test]
fn test_memory_structured_sink_stores_entry() {
let sink = MemoryStructuredSink::new("s");
let entry = StructuredLogEntry::new(LogLevel::Info, "hello").with_field("k", "v");
sink.write(&entry);
assert_eq!(sink.len(), 1);
assert_eq!(sink.entries()[0].message, "hello");
assert_eq!(sink.entries()[0].fields.get("k"), Some(&"v".to_string()));
}
#[test]
fn test_memory_structured_sink_clear() {
let sink = MemoryStructuredSink::new("s");
sink.write(&StructuredLogEntry::new(LogLevel::Info, "x"));
sink.clear();
assert!(sink.is_empty());
}
#[test]
fn test_structured_log_writer_filters_by_target() {
let filter = LevelFilter::new(LogLevel::Info).with_target_level("verbose", LogLevel::Debug);
let mut writer = StructuredLogWriter::new(filter);
let sink = Arc::new(MemoryStructuredSink::new("mem"));
writer.add_sink(sink.clone());
let debug_entry = StructuredLogEntry::new(LogLevel::Debug, "dbg").with_target("verbose");
writer.log(&debug_entry);
assert_eq!(sink.len(), 1);
let filtered = StructuredLogEntry::new(LogLevel::Debug, "filtered");
writer.log(&filtered);
assert_eq!(sink.len(), 1); }
#[test]
fn test_structured_log_writer_fans_out_to_multiple_sinks() {
let writer = StructuredLogWriter::new(LevelFilter::new(LogLevel::Debug));
let sink1 = Arc::new(MemoryStructuredSink::new("s1"));
let sink2 = Arc::new(MemoryStructuredSink::new("s2"));
let mut writer = writer;
writer.add_sink(sink1.clone());
writer.add_sink(sink2.clone());
let entry = StructuredLogEntry::new(LogLevel::Info, "hello");
writer.log(&entry);
assert_eq!(sink1.len(), 1);
assert_eq!(sink2.len(), 1);
}
#[test]
fn test_structured_log_writer_log_with_fields() {
let mut writer = StructuredLogWriter::new(LevelFilter::new(LogLevel::Info));
let sink = Arc::new(MemoryStructuredSink::new("mem"));
writer.add_sink(sink.clone());
let mut fields = StructuredFields::new();
fields.insert("user_id".to_string(), "42".to_string());
writer.log_with_fields("api", LogLevel::Info, "request", fields);
assert_eq!(sink.len(), 1);
let entry = &sink.entries()[0];
assert_eq!(entry.target, Some("api".to_string()));
assert_eq!(entry.fields.get("user_id"), Some(&"42".to_string()));
}
#[test]
fn test_structured_log_writer_sink_names() {
let mut writer = StructuredLogWriter::new(LevelFilter::new(LogLevel::Info));
writer.add_sink(Arc::new(MemoryStructuredSink::new("alpha")));
writer.add_sink(Arc::new(MemoryStructuredSink::new("beta")));
let names = writer.sink_names();
assert_eq!(names, vec!["alpha", "beta"]);
assert_eq!(writer.sink_count(), 2);
}
#[test]
fn test_structured_log_writer_filter_mut() {
let mut writer = StructuredLogWriter::new(LevelFilter::new(LogLevel::Info));
writer.filter_mut().set_default_level(LogLevel::Error);
assert_eq!(writer.filter().default_level(), LogLevel::Error);
}
#[test]
fn test_structured_log_writer_empty_sinks_no_error() {
let writer = StructuredLogWriter::new(LevelFilter::new(LogLevel::Debug));
let entry = StructuredLogEntry::new(LogLevel::Info, "msg");
writer.log(&entry); assert_eq!(writer.sink_count(), 0);
}
#[test]
fn test_multi_output_logger_implements_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<MultiOutputLogger>();
assert_send_sync::<MemorySink>();
assert_send_sync::<ConsoleSink>();
}
}