use crate::value::Value;
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum TraceLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
impl TraceLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Debug => "DEBUG",
Self::Info => "INFO",
Self::Warn => "WARN",
Self::Error => "ERROR",
}
}
}
impl std::str::FromStr for TraceLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" | "warning" => Ok(Self::Warn),
"error" => Ok(Self::Error),
_ => Err(format!("Invalid trace level: {}", s)),
}
}
}
impl std::fmt::Display for TraceLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct TraceEntry {
pub timestamp: Instant,
pub level: TraceLevel,
pub category: String,
pub label: Option<String>,
pub values: Vec<Value>,
pub location: Option<String>,
}
impl TraceEntry {
pub fn new(level: TraceLevel, category: String, values: Vec<Value>) -> Self {
Self {
timestamp: Instant::now(),
level,
category,
label: None,
values,
location: None,
}
}
pub fn with_label(mut self, label: String) -> Self {
self.label = Some(label);
self
}
pub fn with_location(mut self, location: String) -> Self {
self.location = Some(location);
self
}
pub fn format(&self) -> String {
let values_str: Vec<String> = self.values.iter().map(|v| v.to_string()).collect();
let payload = values_str.join(" ");
match &self.label {
Some(l) => format!("[{}:{}:{}] {}", self.level, self.category, l, payload),
None => format!("[{}:{}] {}", self.level, self.category, payload),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct TraceFilter {
pub min_level: Option<TraceLevel>,
pub category: Option<String>,
pub label: Option<String>,
pub since: Option<Instant>,
}
impl TraceFilter {
pub fn new() -> Self {
Self::default()
}
pub fn with_min_level(mut self, level: TraceLevel) -> Self {
self.min_level = Some(level);
self
}
pub fn with_category(mut self, category: String) -> Self {
self.category = Some(category);
self
}
pub fn with_label(mut self, label: String) -> Self {
self.label = Some(label);
self
}
pub fn with_since(mut self, since: Instant) -> Self {
self.since = Some(since);
self
}
pub fn matches(&self, entry: &TraceEntry) -> bool {
if let Some(min_level) = self.min_level
&& entry.level < min_level
{
return false;
}
if let Some(ref category) = self.category
&& &entry.category != category
{
return false;
}
if let Some(ref label) = self.label
&& entry.label.as_ref() != Some(label)
{
return false;
}
if let Some(since) = self.since
&& entry.timestamp < since
{
return false;
}
true
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TraceStats {
pub total_entries: usize,
pub by_level: std::collections::HashMap<TraceLevel, usize>,
pub by_category: std::collections::HashMap<String, usize>,
pub buffer_size: usize,
pub buffer_full: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_trace_level_from_str() {
assert_eq!(TraceLevel::from_str("debug"), Ok(TraceLevel::Debug));
assert_eq!(TraceLevel::from_str("DEBUG"), Ok(TraceLevel::Debug));
assert_eq!(TraceLevel::from_str("info"), Ok(TraceLevel::Info));
assert_eq!(TraceLevel::from_str("warn"), Ok(TraceLevel::Warn));
assert_eq!(TraceLevel::from_str("warning"), Ok(TraceLevel::Warn));
assert_eq!(TraceLevel::from_str("error"), Ok(TraceLevel::Error));
assert!(TraceLevel::from_str("invalid").is_err());
}
#[test]
fn test_trace_level_display() {
assert_eq!(TraceLevel::Debug.to_string(), "DEBUG");
assert_eq!(TraceLevel::Info.to_string(), "INFO");
assert_eq!(TraceLevel::Warn.to_string(), "WARN");
assert_eq!(TraceLevel::Error.to_string(), "ERROR");
}
#[test]
fn test_trace_level_ordering() {
assert!(TraceLevel::Debug < TraceLevel::Info);
assert!(TraceLevel::Info < TraceLevel::Warn);
assert!(TraceLevel::Warn < TraceLevel::Error);
}
#[test]
fn test_trace_entry_creation() {
let entry = TraceEntry::new(
TraceLevel::Info,
"test_category".to_string(),
vec![Value::Number(42.0)],
);
assert_eq!(entry.level, TraceLevel::Info);
assert_eq!(entry.category, "test_category");
assert_eq!(entry.values.len(), 1);
assert!(entry.label.is_none());
assert!(entry.location.is_none());
}
#[test]
fn test_trace_entry_with_label() {
let entry = TraceEntry::new(TraceLevel::Info, "test_category".to_string(), vec![])
.with_label("test_label".to_string());
assert_eq!(entry.label, Some("test_label".to_string()));
}
#[test]
fn test_trace_entry_format() {
let entry1 = TraceEntry::new(
TraceLevel::Info,
"category1".to_string(),
vec![Value::Number(42.0)],
);
let formatted1 = entry1.format();
assert!(formatted1.contains("[INFO:category1]"));
assert!(formatted1.contains("42"));
let entry2 = TraceEntry::new(
TraceLevel::Error,
"category2".to_string(),
vec![Value::String("error_msg".to_string())],
)
.with_label("test_label".to_string());
let formatted2 = entry2.format();
assert!(formatted2.contains("[ERROR:category2:test_label]"));
assert!(formatted2.contains("error_msg"));
}
#[test]
fn test_trace_filter() {
let filter = TraceFilter::new().with_min_level(TraceLevel::Warn);
let debug_entry = TraceEntry::new(TraceLevel::Debug, "test".to_string(), vec![]);
let warn_entry = TraceEntry::new(TraceLevel::Warn, "test".to_string(), vec![]);
let error_entry = TraceEntry::new(TraceLevel::Error, "test".to_string(), vec![]);
assert!(!filter.matches(&debug_entry));
assert!(filter.matches(&warn_entry));
assert!(filter.matches(&error_entry));
}
#[test]
fn test_trace_filter_with_category() {
let filter = TraceFilter::new().with_category("api_call".to_string());
let api_entry = TraceEntry::new(TraceLevel::Info, "api_call".to_string(), vec![]);
let db_entry = TraceEntry::new(TraceLevel::Info, "database".to_string(), vec![]);
assert!(filter.matches(&api_entry));
assert!(!filter.matches(&db_entry));
}
#[test]
fn test_trace_filter_with_label() {
let filter = TraceFilter::new().with_label("slow_request".to_string());
let entry1 = TraceEntry::new(TraceLevel::Warn, "api".to_string(), vec![])
.with_label("slow_request".to_string());
let entry2 = TraceEntry::new(TraceLevel::Warn, "api".to_string(), vec![])
.with_label("fast_request".to_string());
assert!(filter.matches(&entry1));
assert!(!filter.matches(&entry2));
}
#[test]
fn test_trace_filter_combined() {
let filter = TraceFilter::new()
.with_min_level(TraceLevel::Warn)
.with_category("api".to_string());
let entry1 = TraceEntry::new(TraceLevel::Warn, "api".to_string(), vec![]);
let entry2 = TraceEntry::new(TraceLevel::Info, "api".to_string(), vec![]);
let entry3 = TraceEntry::new(TraceLevel::Warn, "database".to_string(), vec![]);
assert!(filter.matches(&entry1));
assert!(!filter.matches(&entry2));
assert!(!filter.matches(&entry3));
}
}