use crate::LogLevel;
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogRecord {
pub level: LogLevel,
pub target: String,
pub message: String,
pub timestamp: DateTime<Utc>,
pub fields: HashMap<String, String>,
}
impl LogRecord {
pub fn new(level: LogLevel, target: impl Into<String>, message: impl Into<String>) -> Self {
Self {
level,
target: target.into(),
message: message.into(),
timestamp: Utc::now(),
fields: HashMap::new(),
}
}
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 with_fields(mut self, fields: HashMap<String, String>) -> Self {
self.fields.extend(fields);
self
}
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = timestamp;
self
}
pub fn level_at_least(&self, threshold: LogLevel) -> bool {
self.level >= threshold
}
}
impl fmt::Display for LogRecord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} {} - {}",
self.level.as_str(),
self.timestamp.to_rfc3339(),
self.target,
self.message
)
}
}
pub trait LogFilter: Send + Sync {
fn should_keep(&self, record: &LogRecord) -> bool;
fn name(&self) -> &str {
"filter"
}
}
#[derive(Debug, Clone)]
pub struct LevelThresholdFilter {
pub threshold: LogLevel,
}
impl LevelThresholdFilter {
pub fn new(threshold: LogLevel) -> Self {
Self { threshold }
}
}
impl LogFilter for LevelThresholdFilter {
fn should_keep(&self, record: &LogRecord) -> bool {
record.level >= self.threshold
}
fn name(&self) -> &str {
"level_threshold"
}
}
#[derive(Debug, Clone)]
pub struct TargetFilter {
pub targets: HashSet<String>,
pub allow: bool,
}
impl TargetFilter {
pub fn allowlist(targets: &[&str]) -> Self {
Self {
targets: targets.iter().map(|s| s.to_string()).collect(),
allow: true,
}
}
pub fn blocklist(targets: &[&str]) -> Self {
Self {
targets: targets.iter().map(|s| s.to_string()).collect(),
allow: false,
}
}
}
impl LogFilter for TargetFilter {
fn should_keep(&self, record: &LogRecord) -> bool {
let contains = self.targets.contains(&record.target);
if self.allow {
contains
} else {
!contains
}
}
fn name(&self) -> &str {
if self.allow {
"target_allowlist"
} else {
"target_blocklist"
}
}
}
#[derive(Debug, Clone)]
pub struct ContainsFilter {
pub pattern: String,
pub include: bool,
}
impl ContainsFilter {
pub fn include(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
include: true,
}
}
pub fn exclude(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
include: false,
}
}
}
impl LogFilter for ContainsFilter {
fn should_keep(&self, record: &LogRecord) -> bool {
let contains = record.message.contains(self.pattern.as_str());
if self.include {
contains
} else {
!contains
}
}
fn name(&self) -> &str {
"contains"
}
}
pub struct AllFilter {
filters: Vec<Box<dyn LogFilter>>,
}
impl AllFilter {
pub fn new(filters: Vec<Box<dyn LogFilter>>) -> Self {
Self { filters }
}
}
impl fmt::Debug for AllFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AllFilter")
.field("filter_count", &self.filters.len())
.finish()
}
}
impl LogFilter for AllFilter {
fn should_keep(&self, record: &LogRecord) -> bool {
self.filters.iter().all(|f| f.should_keep(record))
}
fn name(&self) -> &str {
"all"
}
}
pub struct AnyFilter {
filters: Vec<Box<dyn LogFilter>>,
}
impl AnyFilter {
pub fn new(filters: Vec<Box<dyn LogFilter>>) -> Self {
Self { filters }
}
}
impl fmt::Debug for AnyFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AnyFilter")
.field("filter_count", &self.filters.len())
.finish()
}
}
impl LogFilter for AnyFilter {
fn should_keep(&self, record: &LogRecord) -> bool {
self.filters.iter().any(|f| f.should_keep(record))
}
fn name(&self) -> &str {
"any"
}
}
pub trait LogFormatter: Send + Sync {
fn format(&self, record: &LogRecord) -> String;
fn name(&self) -> &str {
"formatter"
}
}
#[derive(Debug, Default)]
pub struct JsonFormatter;
impl LogFormatter for JsonFormatter {
fn format(&self, record: &LogRecord) -> String {
serde_json::to_string(record).unwrap_or_else(|_| "{}".to_string())
}
fn name(&self) -> &str {
"json"
}
}
#[derive(Debug, Clone)]
pub struct TextFormatter {
pub template: String,
}
impl TextFormatter {
pub fn new() -> Self {
Self {
template: "[{level}] {timestamp} {target} - {message}".to_string(),
}
}
pub fn with_template(template: impl Into<String>) -> Self {
Self {
template: template.into(),
}
}
fn render(&self, record: &LogRecord) -> String {
self.template
.replace("{level}", record.level.as_str())
.replace("{timestamp}", &record.timestamp.to_rfc3339())
.replace("{target}", &record.target)
.replace("{message}", &record.message)
}
}
impl Default for TextFormatter {
fn default() -> Self {
Self::new()
}
}
impl LogFormatter for TextFormatter {
fn format(&self, record: &LogRecord) -> String {
self.render(record)
}
fn name(&self) -> &str {
"text"
}
}
#[derive(Debug, Default)]
pub struct StructuredFormatter;
impl StructuredFormatter {
pub fn new() -> Self {
Self
}
}
impl LogFormatter for StructuredFormatter {
fn format(&self, record: &LogRecord) -> String {
let mut parts = Vec::with_capacity(4 + record.fields.len());
parts.push(format!("level={}", record.level.as_str()));
parts.push(format!("ts={}", record.timestamp.to_rfc3339()));
parts.push(format!("target={}", record.target));
parts.push(format!("msg={}", record.message));
let mut field_keys: Vec<&String> = record.fields.keys().collect();
field_keys.sort();
for key in field_keys {
parts.push(format!("{}={}", key, record.fields[key]));
}
parts.join(" ")
}
fn name(&self) -> &str {
"structured"
}
}
pub trait LogOutput: Send + Sync {
fn write(&self, formatted: &str);
fn flush(&self) {}
fn name(&self) -> &str {
"output"
}
}
pub struct MemoryOutput {
buffer: Mutex<Vec<String>>,
}
impl MemoryOutput {
pub fn new() -> Self {
Self {
buffer: Mutex::new(Vec::new()),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
buffer: Mutex::new(Vec::with_capacity(capacity)),
}
}
pub fn handle(&self) -> MemoryOutputHandle {
MemoryOutputHandle {
buffer: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl Default for MemoryOutput {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for MemoryOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemoryOutput")
.field("count", &self.buffer.lock().len())
.finish()
}
}
impl LogOutput for MemoryOutput {
fn write(&self, formatted: &str) {
self.buffer.lock().push(formatted.to_string());
}
fn name(&self) -> &str {
"memory"
}
}
pub struct MemoryOutputHandle {
buffer: Arc<Mutex<Vec<String>>>,
}
impl MemoryOutputHandle {
pub fn new() -> (Self, MemoryOutputShared) {
let buffer = Arc::new(Mutex::new(Vec::new()));
let handle = Self {
buffer: buffer.clone(),
};
let output = MemoryOutputShared { buffer };
(handle, output)
}
pub fn entries(&self) -> Vec<String> {
self.buffer.lock().clone()
}
pub fn count(&self) -> usize {
self.buffer.lock().len()
}
pub fn clear(&self) {
self.buffer.lock().clear();
}
}
impl Default for MemoryOutputHandle {
fn default() -> Self {
Self {
buffer: Arc::new(Mutex::new(Vec::new())),
}
}
}
pub struct MemoryOutputShared {
buffer: Arc<Mutex<Vec<String>>>,
}
impl fmt::Debug for MemoryOutputShared {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemoryOutputShared")
.field("count", &self.buffer.lock().len())
.finish()
}
}
impl LogOutput for MemoryOutputShared {
fn write(&self, formatted: &str) {
self.buffer.lock().push(formatted.to_string());
}
fn name(&self) -> &str {
"memory_shared"
}
}
pub struct CallbackOutput {
callback: Box<dyn Fn(&str) + Send + Sync>,
}
impl CallbackOutput {
pub fn new(callback: impl Fn(&str) + Send + Sync + 'static) -> Self {
Self {
callback: Box::new(callback),
}
}
}
impl fmt::Debug for CallbackOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CallbackOutput").finish()
}
}
impl LogOutput for CallbackOutput {
fn write(&self, formatted: &str) {
(self.callback)(formatted);
}
fn name(&self) -> &str {
"callback"
}
}
pub struct CountingOutput {
count: std::sync::atomic::AtomicU64,
}
impl CountingOutput {
pub fn new() -> Self {
Self {
count: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn count(&self) -> u64 {
self.count.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for CountingOutput {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for CountingOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CountingOutput")
.field("count", &self.count())
.finish()
}
}
impl LogOutput for CountingOutput {
fn write(&self, _formatted: &str) {
self.count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn name(&self) -> &str {
"counting"
}
}
pub struct LogPipeline {
filters: Vec<Box<dyn LogFilter>>,
formatter: Box<dyn LogFormatter>,
outputs: Vec<Box<dyn LogOutput>>,
processed_count: std::sync::atomic::AtomicU64,
dropped_count: std::sync::atomic::AtomicU64,
}
impl LogPipeline {
pub fn new(
filters: Vec<Box<dyn LogFilter>>,
formatter: Box<dyn LogFormatter>,
outputs: Vec<Box<dyn LogOutput>>,
) -> Self {
Self {
filters,
formatter,
outputs,
processed_count: std::sync::atomic::AtomicU64::new(0),
dropped_count: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn process(&self, record: &LogRecord) {
for filter in &self.filters {
if !filter.should_keep(record) {
self.dropped_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return;
}
}
let formatted = self.formatter.format(record);
for output in &self.outputs {
output.write(&formatted);
}
self.processed_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn process_batch(&self, records: &[LogRecord]) {
for record in records {
self.process(record);
}
}
pub fn processed_count(&self) -> u64 {
self.processed_count
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn dropped_count(&self) -> u64 {
self.dropped_count
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn filter_count(&self) -> usize {
self.filters.len()
}
pub fn output_count(&self) -> usize {
self.outputs.len()
}
pub fn flush(&self) {
for output in &self.outputs {
output.flush();
}
}
}
impl fmt::Debug for LogPipeline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LogPipeline")
.field("filters", &self.filters.len())
.field("outputs", &self.outputs.len())
.field("processed", &self.processed_count())
.field("dropped", &self.dropped_count())
.finish()
}
}
pub struct LogPipelineBuilder {
filters: Vec<Box<dyn LogFilter>>,
formatter: Option<Box<dyn LogFormatter>>,
outputs: Vec<Box<dyn LogOutput>>,
}
impl LogPipelineBuilder {
pub fn new() -> Self {
Self {
filters: Vec::new(),
formatter: None,
outputs: Vec::new(),
}
}
pub fn filter(mut self, filter: Box<dyn LogFilter>) -> Self {
self.filters.push(filter);
self
}
pub fn filters(mut self, filters: Vec<Box<dyn LogFilter>>) -> Self {
self.filters.extend(filters);
self
}
pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
self.formatter = Some(formatter);
self
}
pub fn output(mut self, output: Box<dyn LogOutput>) -> Self {
self.outputs.push(output);
self
}
pub fn outputs(mut self, outputs: Vec<Box<dyn LogOutput>>) -> Self {
self.outputs.extend(outputs);
self
}
pub fn build(self) -> LogPipeline {
let formatter = self.formatter.unwrap_or_else(|| Box::new(JsonFormatter));
LogPipeline::new(self.filters, formatter, self.outputs)
}
}
impl Default for LogPipelineBuilder {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for LogPipelineBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LogPipelineBuilder")
.field("filters", &self.filters.len())
.field("has_formatter", &self.formatter.is_some())
.field("outputs", &self.outputs.len())
.finish()
}
}
pub struct RoutingRule {
pub filter: Box<dyn LogFilter>,
pub pipeline: LogPipeline,
pub name: String,
}
impl fmt::Debug for RoutingRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RoutingRule")
.field("name", &self.name)
.field("filter", &self.filter.name())
.finish()
}
}
pub struct LogRouter {
rules: Vec<RoutingRule>,
default: Option<LogPipeline>,
routed_count: std::sync::atomic::AtomicU64,
unmatched_count: std::sync::atomic::AtomicU64,
}
impl LogRouter {
pub fn new() -> Self {
Self {
rules: Vec::new(),
default: None,
routed_count: std::sync::atomic::AtomicU64::new(0),
unmatched_count: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn route(mut self, rule: RoutingRule) -> Self {
self.rules.push(rule);
self
}
pub fn default_pipeline(mut self, pipeline: LogPipeline) -> Self {
self.default = Some(pipeline);
self
}
pub fn route_record(&self, record: &LogRecord) {
for rule in &self.rules {
if rule.filter.should_keep(record) {
rule.pipeline.process(record);
self.routed_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return;
}
}
if let Some(default) = &self.default {
default.process(record);
self.routed_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
} else {
self.unmatched_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
pub fn route_batch(&self, records: &[LogRecord]) {
for record in records {
self.route_record(record);
}
}
pub fn routed_count(&self) -> u64 {
self.routed_count.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn unmatched_count(&self) -> u64 {
self.unmatched_count
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn has_default(&self) -> bool {
self.default.is_some()
}
}
impl Default for LogRouter {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for LogRouter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LogRouter")
.field("rules", &self.rules.len())
.field("has_default", &self.default.is_some())
.field("routed", &self.routed_count())
.field("unmatched", &self.unmatched_count())
.finish()
}
}
pub struct RateLimitFilter {
window_ms: u64,
max_count: u32,
window_start: parking_lot::Mutex<Option<std::time::Instant>>,
count: parking_lot::Mutex<u32>,
}
impl RateLimitFilter {
pub fn new(window_ms: u64, max_count: u32) -> Self {
Self {
window_ms,
max_count,
window_start: parking_lot::Mutex::new(None),
count: parking_lot::Mutex::new(0),
}
}
pub fn per_second(max_per_sec: u32) -> Self {
Self::new(1000, max_per_sec)
}
}
impl LogFilter for RateLimitFilter {
fn should_keep(&self, _record: &LogRecord) -> bool {
let now = std::time::Instant::now();
let mut start = self.window_start.lock();
let mut count = self.count.lock();
match *start {
None => {
*start = Some(now);
*count = 1;
true
}
Some(s) => {
let elapsed = now.duration_since(s);
if elapsed.as_millis() as u64 >= self.window_ms {
*start = Some(now);
*count = 1;
true
} else {
*count += 1;
*count <= self.max_count
}
}
}
}
}
pub struct SamplingFilter {
rate: f64,
state: parking_lot::Mutex<u64>,
}
impl SamplingFilter {
pub fn new(rate: f64) -> Self {
Self {
rate: rate.clamp(0.0, 1.0),
state: parking_lot::Mutex::new(0x12345678),
}
}
pub fn ten_percent() -> Self {
Self::new(0.1)
}
pub fn one_percent() -> Self {
Self::new(0.01)
}
pub fn rate(&self) -> f64 {
self.rate
}
fn next_random(&self) -> f64 {
let mut state = self.state.lock();
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(*state >> 11) as f64 / (1u64 << 53) as f64
}
}
impl LogFilter for SamplingFilter {
fn should_keep(&self, _record: &LogRecord) -> bool {
self.next_random() < self.rate
}
}
pub struct LogAggregator {
window_ms: u64,
entries: parking_lot::Mutex<HashMap<String, (std::time::Instant, u32)>>,
}
impl LogAggregator {
pub fn new(window_ms: u64) -> Self {
Self {
window_ms,
entries: parking_lot::Mutex::new(HashMap::new()),
}
}
pub fn should_output(&self, message: &str) -> bool {
let now = std::time::Instant::now();
let mut entries = self.entries.lock();
match entries.get_mut(message) {
Some((first_seen, count)) => {
let elapsed = now.duration_since(*first_seen);
if elapsed.as_millis() as u64 >= self.window_ms {
*first_seen = now;
*count = 1;
true
} else {
*count += 1;
false
}
}
None => {
entries.insert(message.to_string(), (now, 1));
true
}
}
}
pub fn count(&self, message: &str) -> u32 {
self.entries
.lock()
.get(message)
.map(|(_, c)| *c)
.unwrap_or(0)
}
pub fn clear(&self) {
self.entries.lock().clear();
}
}
pub struct LogBuffer {
capacity: usize,
buffer: parking_lot::Mutex<Vec<LogRecord>>,
}
impl LogBuffer {
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
buffer: parking_lot::Mutex::new(Vec::new()),
}
}
pub fn push(&self, record: LogRecord) -> bool {
let mut buf = self.buffer.lock();
buf.push(record);
buf.len() >= self.capacity
}
pub fn flush(&self) -> Vec<LogRecord> {
let mut buf = self.buffer.lock();
std::mem::take(&mut *buf)
}
pub fn len(&self) -> usize {
self.buffer.lock().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn capacity(&self) -> usize {
self.capacity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_record_new() {
let record = LogRecord::new(LogLevel::Info, "app", "hello");
assert_eq!(record.level, LogLevel::Info);
assert_eq!(record.target, "app");
assert_eq!(record.message, "hello");
assert!(record.fields.is_empty());
}
#[test]
fn test_log_record_with_field() {
let record = LogRecord::new(LogLevel::Warn, "db", "slow query")
.with_field("duration", "150ms")
.with_field("sql", "SELECT * FROM users");
assert_eq!(record.fields.get("duration"), Some(&"150ms".to_string()));
assert_eq!(
record.fields.get("sql"),
Some(&"SELECT * FROM users".to_string())
);
}
#[test]
fn test_log_record_level_at_least() {
let record = LogRecord::new(LogLevel::Warn, "app", "msg");
assert!(record.level_at_least(LogLevel::Warn));
assert!(record.level_at_least(LogLevel::Info));
assert!(!record.level_at_least(LogLevel::Error));
}
#[test]
fn test_log_record_display() {
let record = LogRecord::new(LogLevel::Error, "app", "crash");
let s = format!("{}", record);
assert!(s.contains("ERROR"));
assert!(s.contains("app"));
assert!(s.contains("crash"));
}
#[test]
fn test_level_threshold_filter_passes() {
let filter = LevelThresholdFilter::new(LogLevel::Info);
let record = LogRecord::new(LogLevel::Info, "app", "msg");
assert!(filter.should_keep(&record));
}
#[test]
fn test_level_threshold_filter_blocks() {
let filter = LevelThresholdFilter::new(LogLevel::Warn);
let record = LogRecord::new(LogLevel::Debug, "app", "msg");
assert!(!filter.should_keep(&record));
}
#[test]
fn test_level_threshold_filter_boundary() {
let filter = LevelThresholdFilter::new(LogLevel::Warn);
assert!(filter.should_keep(&LogRecord::new(LogLevel::Warn, "a", "m")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "a", "m")));
assert!(filter.should_keep(&LogRecord::new(LogLevel::Error, "a", "m")));
}
#[test]
fn test_target_filter_allowlist() {
let filter = TargetFilter::allowlist(&["app", "db"]);
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "app", "m")));
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "db", "m")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "cache", "m")));
}
#[test]
fn test_target_filter_blocklist() {
let filter = TargetFilter::blocklist(&["debug", "trace"]);
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "debug", "m")));
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "app", "m")));
}
#[test]
fn test_contains_filter_include() {
let filter = ContainsFilter::include("error");
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "a", "an error occurred")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "a", "all good")));
}
#[test]
fn test_contains_filter_exclude() {
let filter = ContainsFilter::exclude("password");
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "a", "user password leaked")));
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "a", "user logged in")));
}
#[test]
fn test_all_filter() {
let filter = AllFilter::new(vec![
Box::new(LevelThresholdFilter::new(LogLevel::Info)),
Box::new(TargetFilter::allowlist(&["app"])),
]);
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "app", "m")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Debug, "app", "m")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "db", "m")));
}
#[test]
fn test_any_filter() {
let filter = AnyFilter::new(vec![
Box::new(TargetFilter::allowlist(&["app"])),
Box::new(LevelThresholdFilter::new(LogLevel::Error)),
]);
assert!(filter.should_keep(&LogRecord::new(LogLevel::Info, "app", "m")));
assert!(filter.should_keep(&LogRecord::new(LogLevel::Error, "db", "m")));
assert!(!filter.should_keep(&LogRecord::new(LogLevel::Info, "db", "m")));
}
#[test]
fn test_json_formatter() {
let formatter = JsonFormatter;
let record = LogRecord::new(LogLevel::Info, "app", "hello");
let json = formatter.format(&record);
assert!(json.contains("\"level\":\"Info\""));
assert!(json.contains("\"target\":\"app\""));
assert!(json.contains("\"message\":\"hello\""));
}
#[test]
fn test_json_formatter_with_fields() {
let formatter = JsonFormatter;
let record = LogRecord::new(LogLevel::Warn, "db", "slow").with_field("duration", "100ms");
let json = formatter.format(&record);
assert!(json.contains("duration"));
assert!(json.contains("100ms"));
}
#[test]
fn test_text_formatter_default() {
let formatter = TextFormatter::new();
let record = LogRecord::new(LogLevel::Error, "app", "crash");
let text = formatter.format(&record);
assert!(text.contains("ERROR"));
assert!(text.contains("app"));
assert!(text.contains("crash"));
}
#[test]
fn test_text_formatter_custom_template() {
let formatter = TextFormatter::with_template("{level} - {message}");
let record = LogRecord::new(LogLevel::Info, "app", "hello");
let text = formatter.format(&record);
assert_eq!(text, "INFO - hello");
}
#[test]
fn test_structured_formatter() {
let formatter = StructuredFormatter::new();
let record = LogRecord::new(LogLevel::Info, "app", "hello").with_field("key", "value");
let text = formatter.format(&record);
assert!(text.contains("level=INFO"));
assert!(text.contains("target=app"));
assert!(text.contains("msg=hello"));
assert!(text.contains("key=value"));
}
#[test]
fn test_memory_output_write() {
let output = MemoryOutput::new();
output.write("line 1");
output.write("line 2");
let entries = output.buffer.lock().clone();
assert_eq!(entries, vec!["line 1", "line 2"]);
}
#[test]
fn test_memory_output_handle() {
let (handle, output) = MemoryOutputHandle::new();
output.write("test line");
assert_eq!(handle.count(), 1);
assert_eq!(handle.entries(), vec!["test line"]);
}
#[test]
fn test_memory_output_handle_clear() {
let (handle, output) = MemoryOutputHandle::new();
output.write("a");
output.write("b");
assert_eq!(handle.count(), 2);
handle.clear();
assert_eq!(handle.count(), 0);
}
#[test]
fn test_counting_output() {
let output = CountingOutput::new();
output.write("a");
output.write("b");
output.write("c");
assert_eq!(output.count(), 3);
}
#[test]
fn test_callback_output() {
let counter = Arc::new(std::sync::atomic::AtomicU64::new(0));
let counter_clone = counter.clone();
let output = CallbackOutput::new(move |_s| {
counter_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
output.write("a");
output.write("b");
assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn test_log_pipeline_basic() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output)],
);
let record = LogRecord::new(LogLevel::Info, "app", "hello");
pipeline.process(&record);
assert_eq!(handle.count(), 1);
assert_eq!(pipeline.processed_count(), 1);
assert_eq!(pipeline.dropped_count(), 0);
}
#[test]
fn test_log_pipeline_filter_drops() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipeline::new(
vec![Box::new(LevelThresholdFilter::new(LogLevel::Warn))],
Box::new(TextFormatter::new()),
vec![Box::new(output)],
);
let record = LogRecord::new(LogLevel::Debug, "app", "debug msg");
pipeline.process(&record);
assert_eq!(handle.count(), 0);
assert_eq!(pipeline.processed_count(), 0);
assert_eq!(pipeline.dropped_count(), 1);
}
#[test]
fn test_log_pipeline_multiple_filters() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipeline::new(
vec![
Box::new(LevelThresholdFilter::new(LogLevel::Info)),
Box::new(TargetFilter::allowlist(&["app"])),
],
Box::new(JsonFormatter),
vec![Box::new(output)],
);
pipeline.process(&LogRecord::new(LogLevel::Info, "app", "ok"));
pipeline.process(&LogRecord::new(LogLevel::Info, "db", "filtered"));
pipeline.process(&LogRecord::new(LogLevel::Debug, "app", "filtered"));
assert_eq!(handle.count(), 1);
assert_eq!(pipeline.processed_count(), 1);
assert_eq!(pipeline.dropped_count(), 2);
}
#[test]
fn test_log_pipeline_multiple_outputs() {
let counter = CountingOutput::new();
let counter_ref = Arc::new(CountingOutput::new());
let count2 = Arc::new(std::sync::atomic::AtomicU64::new(0));
let count2_clone = count2.clone();
let callback = CallbackOutput::new(move |_| {
count2_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
let pipeline = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(counter), Box::new(callback)],
);
pipeline.process(&LogRecord::new(LogLevel::Info, "a", "m"));
pipeline.process(&LogRecord::new(LogLevel::Info, "a", "m"));
assert_eq!(count2.load(std::sync::atomic::Ordering::Relaxed), 2);
let _ = counter_ref;
}
#[test]
fn test_log_pipeline_batch() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output)],
);
let records = vec![
LogRecord::new(LogLevel::Info, "a", "1"),
LogRecord::new(LogLevel::Warn, "a", "2"),
LogRecord::new(LogLevel::Error, "a", "3"),
];
pipeline.process_batch(&records);
assert_eq!(handle.count(), 3);
assert_eq!(pipeline.processed_count(), 3);
}
#[test]
fn test_pipeline_builder_basic() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipelineBuilder::new()
.filter(Box::new(LevelThresholdFilter::new(LogLevel::Info)))
.formatter(Box::new(TextFormatter::new()))
.output(Box::new(output))
.build();
pipeline.process(&LogRecord::new(LogLevel::Info, "app", "hello"));
pipeline.process(&LogRecord::new(LogLevel::Debug, "app", "dropped"));
assert_eq!(handle.count(), 1);
assert_eq!(pipeline.processed_count(), 1);
assert_eq!(pipeline.dropped_count(), 1);
}
#[test]
fn test_pipeline_builder_default_formatter() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipelineBuilder::new().output(Box::new(output)).build();
pipeline.process(&LogRecord::new(LogLevel::Info, "app", "hello"));
assert_eq!(handle.count(), 1);
assert!(handle.entries()[0].contains("\"level\""));
}
#[test]
fn test_pipeline_builder_multiple_outputs() {
let (handle1, output1) = MemoryOutputHandle::new();
let (handle2, output2) = MemoryOutputHandle::new();
let pipeline = LogPipelineBuilder::new()
.formatter(Box::new(TextFormatter::new()))
.outputs(vec![Box::new(output1), Box::new(output2)])
.build();
pipeline.process(&LogRecord::new(LogLevel::Info, "a", "m"));
assert_eq!(handle1.count(), 1);
assert_eq!(handle2.count(), 1);
}
#[test]
fn test_log_router_basic() {
let (handle1, output1) = MemoryOutputHandle::new();
let (handle2, output2) = MemoryOutputHandle::new();
let pipeline1 = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output1)],
);
let pipeline2 = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output2)],
);
let router = LogRouter::new()
.route(RoutingRule {
filter: Box::new(TargetFilter::allowlist(&["app"])),
pipeline: pipeline1,
name: "app_rule".to_string(),
})
.route(RoutingRule {
filter: Box::new(TargetFilter::allowlist(&["db"])),
pipeline: pipeline2,
name: "db_rule".to_string(),
});
router.route_record(&LogRecord::new(LogLevel::Info, "app", "app msg"));
router.route_record(&LogRecord::new(LogLevel::Info, "db", "db msg"));
router.route_record(&LogRecord::new(LogLevel::Info, "cache", "unmatched"));
assert_eq!(handle1.count(), 1);
assert_eq!(handle2.count(), 1);
assert_eq!(router.routed_count(), 2);
assert_eq!(router.unmatched_count(), 1);
}
#[test]
fn test_log_router_default_pipeline() {
let (handle, output) = MemoryOutputHandle::new();
let default_pipeline = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output)],
);
let router = LogRouter::new().default_pipeline(default_pipeline);
router.route_record(&LogRecord::new(LogLevel::Info, "any", "msg"));
assert_eq!(handle.count(), 1);
assert_eq!(router.routed_count(), 1);
assert_eq!(router.unmatched_count(), 0);
}
#[test]
fn test_log_router_no_match_no_default() {
let router = LogRouter::new();
router.route_record(&LogRecord::new(LogLevel::Info, "any", "msg"));
assert_eq!(router.routed_count(), 0);
assert_eq!(router.unmatched_count(), 1);
}
#[test]
fn test_log_router_batch() {
let (handle, output) = MemoryOutputHandle::new();
let pipeline = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output)],
);
let router = LogRouter::new().default_pipeline(pipeline);
let records = vec![
LogRecord::new(LogLevel::Info, "a", "1"),
LogRecord::new(LogLevel::Warn, "b", "2"),
];
router.route_batch(&records);
assert_eq!(handle.count(), 2);
assert_eq!(router.routed_count(), 2);
}
#[test]
fn test_log_router_first_match_wins() {
let (handle1, output1) = MemoryOutputHandle::new();
let (handle2, output2) = MemoryOutputHandle::new();
let pipeline1 = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output1)],
);
let pipeline2 = LogPipeline::new(
vec![],
Box::new(TextFormatter::new()),
vec![Box::new(output2)],
);
let router = LogRouter::new()
.route(RoutingRule {
filter: Box::new(LevelThresholdFilter::new(LogLevel::Warn)),
pipeline: pipeline1,
name: "warn_plus".to_string(),
})
.route(RoutingRule {
filter: Box::new(LevelThresholdFilter::new(LogLevel::Error)),
pipeline: pipeline2,
name: "error_only".to_string(),
});
router.route_record(&LogRecord::new(LogLevel::Error, "a", "m"));
assert_eq!(handle1.count(), 1);
assert_eq!(handle2.count(), 0);
}
#[test]
fn test_rate_limit_allows_within_limit() {
let filter = RateLimitFilter::per_second(5);
let record = LogRecord::new(LogLevel::Info, "app", "msg");
for _ in 0..5 {
assert!(filter.should_keep(&record));
}
}
#[test]
fn test_rate_limit_blocks_over_limit() {
let filter = RateLimitFilter::per_second(3);
let record = LogRecord::new(LogLevel::Info, "app", "msg");
for _ in 0..3 {
assert!(filter.should_keep(&record));
}
assert!(!filter.should_keep(&record));
}
#[test]
fn test_rate_limit_per_second_constructor() {
let f = RateLimitFilter::per_second(10);
assert_eq!(f.max_count, 10);
assert_eq!(f.window_ms, 1000);
}
#[test]
fn test_sampling_full_rate() {
let filter = SamplingFilter::new(1.0);
let record = LogRecord::new(LogLevel::Info, "app", "msg");
for _ in 0..100 {
assert!(filter.should_keep(&record));
}
}
#[test]
fn test_sampling_zero_rate() {
let filter = SamplingFilter::new(0.0);
let record = LogRecord::new(LogLevel::Info, "app", "msg");
for _ in 0..100 {
assert!(!filter.should_keep(&record));
}
}
#[test]
fn test_sampling_rate_clamped() {
let filter = SamplingFilter::new(2.0);
assert_eq!(filter.rate(), 1.0);
let filter2 = SamplingFilter::new(-1.0);
assert_eq!(filter2.rate(), 0.0);
}
#[test]
fn test_sampling_ten_percent() {
let filter = SamplingFilter::ten_percent();
assert!((filter.rate() - 0.1).abs() < 1e-10);
}
#[test]
fn test_sampling_one_percent() {
let filter = SamplingFilter::one_percent();
assert!((filter.rate() - 0.01).abs() < 1e-10);
}
#[test]
fn test_aggregator_first_output() {
let agg = LogAggregator::new(1000);
assert!(agg.should_output("error: db connection failed"));
}
#[test]
fn test_aggregator_suppresses_duplicates() {
let agg = LogAggregator::new(1000);
assert!(agg.should_output("error: timeout"));
assert!(!agg.should_output("error: timeout"));
assert!(!agg.should_output("error: timeout"));
assert_eq!(agg.count("error: timeout"), 3);
}
#[test]
fn test_aggregator_different_messages() {
let agg = LogAggregator::new(1000);
assert!(agg.should_output("error A"));
assert!(agg.should_output("error B"));
assert!(!agg.should_output("error A"));
assert!(!agg.should_output("error B"));
}
#[test]
fn test_aggregator_clear() {
let agg = LogAggregator::new(1000);
agg.should_output("msg");
assert_eq!(agg.count("msg"), 1);
agg.clear();
assert_eq!(agg.count("msg"), 0);
}
#[test]
fn test_aggregator_count_unknown() {
let agg = LogAggregator::new(1000);
assert_eq!(agg.count("unknown"), 0);
}
#[test]
fn test_log_buffer_push_and_flush() {
let buf = LogBuffer::new(3);
assert!(buf.is_empty());
buf.push(LogRecord::new(LogLevel::Info, "a", "1"));
buf.push(LogRecord::new(LogLevel::Info, "a", "2"));
assert_eq!(buf.len(), 2);
let flushed = buf.flush();
assert_eq!(flushed.len(), 2);
assert!(buf.is_empty());
}
#[test]
fn test_log_buffer_threshold() {
let buf = LogBuffer::new(2);
assert!(!buf.push(LogRecord::new(LogLevel::Info, "a", "1")));
assert!(buf.push(LogRecord::new(LogLevel::Info, "a", "2")));
}
#[test]
fn test_log_buffer_capacity() {
let buf = LogBuffer::new(5);
assert_eq!(buf.capacity(), 5);
}
#[test]
fn test_log_buffer_capacity_clamped() {
let buf = LogBuffer::new(0);
assert_eq!(buf.capacity(), 1);
}
}