use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trace => write!(f, "trace"),
Self::Debug => write!(f, "debug"),
Self::Info => write!(f, "info"),
Self::Warn => write!(f, "warn"),
Self::Error => write!(f, "error"),
}
}
}
impl std::str::FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"trace" => Ok(Self::Trace),
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" | "warning" => Ok(Self::Warn),
"error" => Ok(Self::Error),
_ => Err(format!("Invalid log level: {}", s)),
}
}
}
impl From<LogLevel> for tracing::Level {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Trace => tracing::Level::TRACE,
LogLevel::Debug => tracing::Level::DEBUG,
LogLevel::Info => tracing::Level::INFO,
LogLevel::Warn => tracing::Level::WARN,
LogLevel::Error => tracing::Level::ERROR,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub level: LogLevel,
pub structured: bool,
pub include_timestamps: bool,
pub include_location: bool,
pub include_thread_ids: bool,
pub domain_levels: std::collections::HashMap<String, LogLevel>,
pub output: LogOutput,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: LogLevel::Info,
structured: false,
include_timestamps: true,
include_location: false,
include_thread_ids: false,
domain_levels: std::collections::HashMap::new(),
output: LogOutput::Stdout,
}
}
}
impl LogConfig {
pub fn development() -> Self {
Self {
level: LogLevel::Debug,
structured: false,
include_location: true,
..Default::default()
}
}
pub fn production() -> Self {
Self {
level: LogLevel::Info,
structured: true,
include_timestamps: true,
include_thread_ids: true,
..Default::default()
}
}
pub fn with_domain_level(mut self, domain: impl Into<String>, level: LogLevel) -> Self {
self.domain_levels.insert(domain.into(), level);
self
}
pub fn init(&self) -> crate::error::Result<()> {
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(self.level.to_string()));
let subscriber = tracing_subscriber::registry().with(filter);
if self.structured {
let layer = fmt::layer()
.json()
.with_thread_ids(self.include_thread_ids)
.with_file(self.include_location)
.with_line_number(self.include_location);
subscriber.with(layer).try_init().ok();
} else {
let layer = fmt::layer()
.with_thread_ids(self.include_thread_ids)
.with_file(self.include_location)
.with_line_number(self.include_location);
subscriber.with(layer).try_init().ok();
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogOutput {
#[default]
Stdout,
Stderr,
File(String),
}
pub struct StructuredLogger {
level: LogLevel,
message: Option<String>,
kernel_id: Option<String>,
domain: Option<String>,
tenant_id: Option<String>,
trace_id: Option<String>,
span_id: Option<String>,
fields: std::collections::HashMap<String, serde_json::Value>,
}
impl StructuredLogger {
pub fn trace() -> Self {
Self::new(LogLevel::Trace)
}
pub fn debug() -> Self {
Self::new(LogLevel::Debug)
}
pub fn info() -> Self {
Self::new(LogLevel::Info)
}
pub fn warn() -> Self {
Self::new(LogLevel::Warn)
}
pub fn error() -> Self {
Self::new(LogLevel::Error)
}
fn new(level: LogLevel) -> Self {
Self {
level,
message: None,
kernel_id: None,
domain: None,
tenant_id: None,
trace_id: None,
span_id: None,
fields: std::collections::HashMap::new(),
}
}
pub fn message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
pub fn kernel(mut self, id: impl Into<String>) -> Self {
self.kernel_id = Some(id.into());
self
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self
}
pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
self.tenant_id = Some(tenant.into());
self
}
pub fn trace_context(
mut self,
trace_id: impl Into<String>,
span_id: impl Into<String>,
) -> Self {
self.trace_id = Some(trace_id.into());
self.span_id = Some(span_id.into());
self
}
pub fn field(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.fields.insert(key.into(), json_value);
}
self
}
pub fn log(self) {
let msg = self.message.unwrap_or_default();
match self.level {
LogLevel::Trace => tracing::trace!(
kernel_id = ?self.kernel_id,
domain = ?self.domain,
tenant_id = ?self.tenant_id,
trace_id = ?self.trace_id,
span_id = ?self.span_id,
"{}",
msg
),
LogLevel::Debug => tracing::debug!(
kernel_id = ?self.kernel_id,
domain = ?self.domain,
tenant_id = ?self.tenant_id,
trace_id = ?self.trace_id,
span_id = ?self.span_id,
"{}",
msg
),
LogLevel::Info => tracing::info!(
kernel_id = ?self.kernel_id,
domain = ?self.domain,
tenant_id = ?self.tenant_id,
trace_id = ?self.trace_id,
span_id = ?self.span_id,
"{}",
msg
),
LogLevel::Warn => tracing::warn!(
kernel_id = ?self.kernel_id,
domain = ?self.domain,
tenant_id = ?self.tenant_id,
trace_id = ?self.trace_id,
span_id = ?self.span_id,
"{}",
msg
),
LogLevel::Error => tracing::error!(
kernel_id = ?self.kernel_id,
domain = ?self.domain,
tenant_id = ?self.tenant_id,
trace_id = ?self.trace_id,
span_id = ?self.span_id,
"{}",
msg
),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AuditLog {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub event_type: AuditEventType,
pub actor: String,
pub resource: String,
pub action: String,
pub result: AuditResult,
pub details: Option<serde_json::Value>,
pub tenant_id: Option<String>,
pub request_id: Option<String>,
}
impl AuditLog {
pub fn new(
event_type: AuditEventType,
actor: impl Into<String>,
resource: impl Into<String>,
action: impl Into<String>,
) -> Self {
Self {
timestamp: chrono::Utc::now(),
event_type,
actor: actor.into(),
resource: resource.into(),
action: action.into(),
result: AuditResult::Success,
details: None,
tenant_id: None,
request_id: None,
}
}
pub fn with_result(mut self, result: AuditResult) -> Self {
self.result = result;
self
}
pub fn with_details(mut self, details: impl Serialize) -> Self {
self.details = serde_json::to_value(details).ok();
self
}
pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
self.tenant_id = Some(tenant.into());
self
}
pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = Some(id.into());
self
}
pub fn emit(self) {
tracing::info!(
target: "audit",
event_type = ?self.event_type,
actor = %self.actor,
resource = %self.resource,
action = %self.action,
result = ?self.result,
tenant_id = ?self.tenant_id,
request_id = ?self.request_id,
"AUDIT"
);
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
Authentication,
Authorization,
KernelAccess,
ConfigChange,
DataAccess,
AdminAction,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditResult {
Success,
Failure,
Denied,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_parsing() {
assert_eq!("debug".parse::<LogLevel>().unwrap(), LogLevel::Debug);
assert_eq!("INFO".parse::<LogLevel>().unwrap(), LogLevel::Info);
assert_eq!("warning".parse::<LogLevel>().unwrap(), LogLevel::Warn);
}
#[test]
fn test_log_config() {
let config = LogConfig::production();
assert!(config.structured);
assert_eq!(config.level, LogLevel::Info);
let dev_config = LogConfig::development();
assert!(!dev_config.structured);
assert_eq!(dev_config.level, LogLevel::Debug);
}
#[test]
fn test_structured_logger() {
let logger = StructuredLogger::info()
.message("Test message")
.kernel("graph/pagerank")
.tenant("tenant-123")
.field("latency_us", 150);
assert!(logger.message.is_some());
assert!(logger.kernel_id.is_some());
}
#[test]
fn test_audit_log() {
let audit = AuditLog::new(
AuditEventType::KernelAccess,
"user-123",
"graph/pagerank",
"execute",
)
.with_result(AuditResult::Success)
.with_tenant("tenant-456");
assert_eq!(audit.actor, "user-123");
assert!(audit.tenant_id.is_some());
}
}