#![allow(missing_docs)]
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use paladin_core::platform::container::log::{LogDestination, LogEntry, LogLevel};
pub type LogResult<T> = Result<T, LogError>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum LogError {
#[error("IO error: {0}")]
IoError(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Log destination not found: {0}")]
DestinationNotFound(String),
#[error("Log buffer full")]
BufferFull,
#[error("Log service unavailable")]
ServiceUnavailable,
#[error("Invalid log format: {0}")]
InvalidFormat(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
#[derive(Debug, Clone)]
pub struct LogRotationConfig {
pub max_size: u64,
pub max_files: u32,
pub compress: bool,
}
impl Default for LogRotationConfig {
fn default() -> Self {
Self {
max_size: 10 * 1024 * 1024, max_files: 5,
compress: true,
}
}
}
#[derive(Debug, Clone)]
pub struct LogDestinationConfig {
pub destination: LogDestination,
pub min_level: LogLevel,
pub format: LogFormat,
pub enabled: bool,
pub rotation: Option<LogRotationConfig>,
pub settings: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum LogFormat {
#[default]
Text,
Json,
Structured(String),
}
#[derive(Debug, Clone, Default)]
pub struct LogStats {
pub entries_written: u64,
pub entries_by_level: HashMap<LogLevel, u64>,
pub errors: u64,
pub bytes_written: u64,
pub last_write: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone)]
pub struct LogQuery {
pub min_level: Option<LogLevel>,
pub max_level: Option<LogLevel>,
pub start_time: Option<DateTime<Utc>>,
pub end_time: Option<DateTime<Utc>>,
pub source: Option<String>,
pub module: Option<String>,
pub message_contains: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
impl Default for LogQuery {
fn default() -> Self {
Self {
min_level: None,
max_level: None,
start_time: None,
end_time: None,
source: None,
module: None,
message_contains: None,
limit: Some(1000), offset: None,
}
}
}
#[derive(Debug, Clone)]
pub struct BatchWriteRequest {
pub entries: Vec<LogEntry>,
pub atomic: bool,
pub timeout: Option<std::time::Duration>,
}
impl BatchWriteRequest {
pub fn new(entries: Vec<LogEntry>) -> Self {
Self {
entries,
atomic: true,
timeout: None,
}
}
pub fn with_atomic(mut self, atomic: bool) -> Self {
self.atomic = atomic;
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
#[derive(Debug, Clone)]
pub struct LogHealthCheck {
pub destination: LogDestination,
pub healthy: bool,
pub last_write: Option<DateTime<Utc>>,
pub error_message: Option<String>,
pub response_time: Option<std::time::Duration>,
}
#[async_trait]
pub trait LogPort: Send + Sync {
async fn write_entry(&self, entry: LogEntry) -> LogResult<()>;
async fn write_entries(&self, entries: Vec<LogEntry>) -> LogResult<()>;
async fn batch_write(&self, request: BatchWriteRequest) -> LogResult<()>;
async fn read_entries(&self, query: LogQuery) -> LogResult<Vec<LogEntry>>;
async fn count_entries(&self, query: LogQuery) -> LogResult<u64>;
async fn configure_destination(&self, config: LogDestinationConfig) -> LogResult<()>;
async fn remove_destination(&self, destination: LogDestination) -> LogResult<()>;
async fn list_destinations(&self) -> LogResult<Vec<LogDestination>>;
async fn flush(&self) -> LogResult<()>;
async fn flush_destination(&self, destination: LogDestination) -> LogResult<()>;
async fn rotate_logs(&self, destination: LogDestination) -> LogResult<()>;
async fn get_stats(&self) -> LogResult<LogStats>;
async fn get_destination_stats(&self, destination: LogDestination) -> LogResult<LogStats>;
async fn clear_logs(&self, destination: LogDestination) -> LogResult<()>;
async fn clear_logs_before(
&self,
destination: LogDestination,
before: DateTime<Utc>,
) -> LogResult<u64>;
async fn health_check(&self) -> LogResult<Vec<LogHealthCheck>>;
async fn health_check_destination(
&self,
destination: LogDestination,
) -> LogResult<LogHealthCheck>;
fn get_provider_name(&self) -> &'static str;
async fn test_connection(&self) -> LogResult<()>;
async fn archive_logs(
&self,
destination: LogDestination,
before: DateTime<Utc>,
) -> LogResult<String>;
fn supported_formats(&self) -> Vec<LogFormat>;
}
pub trait LogFormatter: Send + Sync {
fn format_entry(&self, entry: &LogEntry) -> LogResult<String>;
fn format_entries(&self, entries: &[LogEntry]) -> LogResult<Vec<String>> {
entries.iter().map(|e| self.format_entry(e)).collect()
}
fn format_type(&self) -> LogFormat;
}
pub struct TextLogFormatter;
impl LogFormatter for TextLogFormatter {
fn format_entry(&self, entry: &LogEntry) -> LogResult<String> {
Ok(format!(
"{} [{}] {:?} - {}",
entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
entry.message.level.as_str(),
entry.source,
entry.message.formatted()
))
}
fn format_type(&self) -> LogFormat {
LogFormat::Text
}
}
pub struct JsonLogFormatter;
impl LogFormatter for JsonLogFormatter {
fn format_entry(&self, entry: &LogEntry) -> LogResult<String> {
serde_json::to_string(entry).map_err(|e| LogError::SerializationError(e.to_string()))
}
fn format_type(&self) -> LogFormat {
LogFormat::Json
}
}
#[cfg(test)]
mod tests {
use super::*;
use paladin_core::base::entity::message::Location;
use paladin_core::platform::container::log::{LogDestination, LogEntryBuilder, LogLevel};
#[test]
fn test_log_query_default() {
let query = LogQuery::default();
assert_eq!(query.limit, Some(1000));
assert!(query.min_level.is_none());
}
#[test]
fn test_batch_write_request() {
let entries = vec![LogEntryBuilder::new_entry(
Location::service("test"),
LogDestination::System,
LogLevel::Info,
"Test message".to_string(),
)];
let request = BatchWriteRequest::new(entries.clone())
.with_atomic(false)
.with_timeout(std::time::Duration::from_secs(5));
assert_eq!(request.entries.len(), 1);
assert!(!request.atomic);
assert_eq!(request.timeout, Some(std::time::Duration::from_secs(5)));
}
#[test]
fn test_text_formatter() {
let formatter = TextLogFormatter;
let entry = LogEntryBuilder::new_entry(
Location::service("test-service"),
LogDestination::System,
LogLevel::Info,
"Test message".to_string(),
);
let formatted = formatter.format_entry(&entry).unwrap();
assert!(formatted.contains("[INFO]"));
assert!(formatted.contains("Test message"));
}
#[test]
fn test_json_formatter() {
let formatter = JsonLogFormatter;
let entry = LogEntryBuilder::new_entry(
Location::service("test-service"),
LogDestination::System,
LogLevel::Info,
"Test message".to_string(),
);
let formatted = formatter.format_entry(&entry).unwrap();
assert!(formatted.contains("\"Info\"") || formatted.contains("Info"));
assert!(formatted.contains("Test message"));
}
}