use crate::base::entity::message::{Location, Message, MessagePriority};
use crate::base::entity::node::Node;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
#[default]
Info = 2,
Warn = 3,
Error = 4,
Fatal = 5,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Fatal => "FATAL",
}
}
pub fn parse_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"TRACE" => Some(LogLevel::Trace),
"DEBUG" => Some(LogLevel::Debug),
"INFO" => Some(LogLevel::Info),
"WARN" | "WARNING" => Some(LogLevel::Warn),
"ERROR" => Some(LogLevel::Error),
"FATAL" => Some(LogLevel::Fatal),
_ => None,
}
}
pub fn to_priority(&self) -> MessagePriority {
match self {
LogLevel::Trace | LogLevel::Debug => MessagePriority::Low,
LogLevel::Info => MessagePriority::Normal,
LogLevel::Warn => MessagePriority::High,
LogLevel::Error | LogLevel::Fatal => MessagePriority::Critical,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LogDestination {
System,
Access,
Error,
Security,
Performance,
Custom(String),
}
impl LogDestination {
pub fn to_location(&self) -> Location {
match self {
LogDestination::System => Location::service("system-log"),
LogDestination::Access => Location::service("access-log"),
LogDestination::Error => Location::service("error-log"),
LogDestination::Security => Location::service("security-log"),
LogDestination::Performance => Location::service("performance-log"),
LogDestination::Custom(name) => Location::service(&format!("custom-log-{}", name)),
}
}
pub fn name(&self) -> String {
match self {
LogDestination::System => "system".to_string(),
LogDestination::Access => "access".to_string(),
LogDestination::Error => "error".to_string(),
LogDestination::Security => "security".to_string(),
LogDestination::Performance => "performance".to_string(),
LogDestination::Custom(name) => name.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LogMessage {
pub level: LogLevel,
pub message: String,
pub module: Option<String>,
pub function: Option<String>,
pub location: Option<String>,
pub context: Option<serde_json::Value>,
}
impl LogMessage {
pub fn new(level: LogLevel, message: String) -> Self {
Self {
level,
message,
module: None,
function: None,
location: None,
context: None,
}
}
pub fn with_module(mut self, module: String) -> Self {
self.module = Some(module);
self
}
pub fn with_function(mut self, function: String) -> Self {
self.function = Some(function);
self
}
pub fn with_location(mut self, location: String) -> Self {
self.location = Some(location);
self
}
pub fn with_context(mut self, context: serde_json::Value) -> Self {
self.context = Some(context);
self
}
pub fn formatted(&self) -> String {
let mut parts = vec![format!("[{}]", self.level.as_str()), self.message.clone()];
if let Some(module) = &self.module {
parts.push(format!("module:{}", module));
}
if let Some(function) = &self.function {
parts.push(format!("fn:{}", function));
}
if let Some(location) = &self.location {
parts.push(format!("at:{}", location));
}
parts.join(" ")
}
}
pub type LogEntry = Message<LogMessage>;
pub struct LogEntryBuilder;
impl LogEntryBuilder {
pub fn new_entry(
source: Location,
destination: LogDestination,
level: LogLevel,
message: String,
) -> LogEntry {
let log_message = LogMessage::new(level, message);
let priority = level.to_priority();
Message::with_priority(source, destination.to_location(), log_message, priority)
}
#[allow(clippy::too_many_arguments)]
pub fn new_entry_with_context(
source: Location,
destination: LogDestination,
level: LogLevel,
message: String,
module: Option<String>,
function: Option<String>,
location: Option<String>,
context: Option<serde_json::Value>,
) -> LogEntry {
let mut log_message = LogMessage::new(level, message);
if let Some(module) = module {
log_message = log_message.with_module(module);
}
if let Some(function) = function {
log_message = log_message.with_function(function);
}
if let Some(location) = location {
log_message = log_message.with_location(location);
}
if let Some(context) = context {
log_message = log_message.with_context(context);
}
let priority = level.to_priority();
Message::with_priority(source, destination.to_location(), log_message, priority)
}
}
pub trait LogEntryExt {
fn level(&self) -> LogLevel;
fn formatted_message(&self) -> String;
fn matches_level(&self, min_level: LogLevel) -> bool;
}
impl LogEntryExt for LogEntry {
fn level(&self) -> LogLevel {
self.message.level
}
fn formatted_message(&self) -> String {
self.message.formatted()
}
fn matches_level(&self, min_level: LogLevel) -> bool {
self.message.level >= min_level
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogContainer {
pub name: String,
pub destination: LogDestination,
pub min_level: LogLevel,
pub max_entries: usize,
pub entries: Vec<LogEntry>,
pub active: bool,
}
impl Hash for LogContainer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.destination.hash(state);
}
}
impl LogContainer {
pub fn new(name: String, destination: LogDestination, min_level: LogLevel) -> Self {
Self {
name,
destination,
min_level,
max_entries: 0, entries: Vec::new(),
active: true,
}
}
pub fn with_max_entries(mut self, max_entries: usize) -> Self {
self.max_entries = max_entries;
self
}
pub fn add_entry(&mut self, entry: LogEntry) -> bool {
if !self.active || !entry.matches_level(self.min_level) {
return false;
}
self.entries.push(entry);
if self.max_entries > 0 && self.entries.len() > self.max_entries {
self.entries.remove(0); }
true
}
pub fn get_entries(&self, min_level: Option<LogLevel>) -> Vec<&LogEntry> {
match min_level {
Some(level) => self
.entries
.iter()
.filter(|e| e.matches_level(level))
.collect(),
None => self.entries.iter().collect(),
}
}
pub fn get_entries_since(&self, since: DateTime<Utc>) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|e| e.timestamp >= since)
.collect()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn set_min_level(&mut self, level: LogLevel) {
self.min_level = level;
}
}
pub type Log = Node<LogContainer>;
pub trait LogExt {
fn new_log(name: String, destination: LogDestination, min_level: LogLevel) -> Self;
fn add_entry(&mut self, entry: LogEntry) -> bool;
fn destination(&self) -> &LogDestination;
fn min_level(&self) -> LogLevel;
}
impl LogExt for Log {
fn new_log(name: String, destination: LogDestination, min_level: LogLevel) -> Self {
let container = LogContainer::new(name.clone(), destination, min_level);
Node::new(container, Some(name))
}
fn add_entry(&mut self, entry: LogEntry) -> bool {
let result = self.node.add_entry(entry);
if result {
self.modified = Utc::now();
}
result
}
fn destination(&self) -> &LogDestination {
&self.node.destination
}
fn min_level(&self) -> LogLevel {
self.node.min_level
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_conversion() {
assert_eq!(LogLevel::parse_str("INFO"), Some(LogLevel::Info));
assert_eq!(LogLevel::parse_str("error"), Some(LogLevel::Error));
assert_eq!(LogLevel::parse_str("INVALID"), None);
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::Error.to_priority(), MessagePriority::Critical);
}
#[test]
fn test_log_message_creation() {
let msg = LogMessage::new(LogLevel::Info, "Test message".to_string())
.with_module("test_module".to_string())
.with_function("test_function".to_string());
assert_eq!(msg.level, LogLevel::Info);
assert_eq!(msg.message, "Test message");
assert_eq!(msg.module, Some("test_module".to_string()));
assert_eq!(msg.function, Some("test_function".to_string()));
}
#[test]
fn test_log_entry_creation() {
let source = Location::service("test-service");
let entry = LogEntryBuilder::new_entry(
source,
LogDestination::System,
LogLevel::Info,
"Test log entry".to_string(),
);
assert_eq!(entry.level(), LogLevel::Info);
assert_eq!(entry.message.message, "Test log entry");
assert_eq!(entry.destination, LogDestination::System.to_location());
}
#[test]
fn test_log_container() {
let mut container = LogContainer::new(
"test-log".to_string(),
LogDestination::System,
LogLevel::Info,
);
let entry = LogEntryBuilder::new_entry(
Location::service("test"),
LogDestination::System,
LogLevel::Info,
"Test message".to_string(),
);
assert!(container.add_entry(entry));
assert_eq!(container.entry_count(), 1);
let debug_entry = LogEntryBuilder::new_entry(
Location::service("test"),
LogDestination::System,
LogLevel::Debug,
"Debug message".to_string(),
);
assert!(!container.add_entry(debug_entry)); assert_eq!(container.entry_count(), 1);
}
#[test]
fn test_log_node() {
let mut log = Log::new_log(
"system-log".to_string(),
LogDestination::System,
LogLevel::Info,
);
let entry = LogEntryBuilder::new_entry(
Location::service("test"),
LogDestination::System,
LogLevel::Warn,
"Warning message".to_string(),
);
let initial_modified = log.modified;
assert!(log.add_entry(entry));
assert!(log.modified > initial_modified);
assert_eq!(log.node.entry_count(), 1);
}
}