use serde::Serialize;
use tracing_subscriber::{
filter::EnvFilter,
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
};
#[derive(Debug, Clone)]
pub struct LogConfig {
pub format: LogFormat,
pub level: String,
pub detailed: bool,
pub json_output: bool,
pub include_timestamp: bool,
pub include_target: bool,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
format: LogFormat::Compact,
level: "info".to_string(),
detailed: false,
json_output: false,
include_timestamp: true,
include_target: false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum LogFormat {
Compact,
Pretty,
Json,
}
pub fn init_logging(config: LogConfig) -> Result<(), Box<dyn std::error::Error>> {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("kindly_guard={},warn", config.level)));
if config.json_output || matches!(config.format, LogFormat::Json) {
let fmt_layer = fmt::layer()
.with_target(config.include_target)
.with_thread_ids(false)
.with_thread_names(false)
.with_span_events(if config.detailed {
FmtSpan::FULL
} else {
FmtSpan::NONE
});
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
} else {
let fmt_layer = fmt::layer()
.with_target(config.include_target)
.with_thread_ids(false)
.with_thread_names(false)
.with_ansi(!cfg!(windows)) .with_span_events(if config.detailed {
FmtSpan::FULL
} else {
FmtSpan::NONE
});
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
}
Ok(())
}
pub fn init_logging_simple(detailed: bool) -> Result<(), Box<dyn std::error::Error>> {
init_logging(LogConfig {
detailed,
..Default::default()
})
}
#[macro_export]
macro_rules! log_security_event {
($level:expr, event_type:$event_type:expr, client:$client:expr, $($field:tt)*) => {
match $level {
Level::ERROR => tracing::error!(
event_type = $event_type,
client = $client,
category = "security",
$($field)*
),
Level::WARN => tracing::warn!(
event_type = $event_type,
client = $client,
category = "security",
$($field)*
),
Level::INFO => tracing::info!(
event_type = $event_type,
client = $client,
category = "security",
$($field)*
),
Level::DEBUG => tracing::debug!(
event_type = $event_type,
client = $client,
category = "security",
$($field)*
),
_ => tracing::trace!(
event_type = $event_type,
client = $client,
category = "security",
$($field)*
),
}
};
}
#[macro_export]
macro_rules! log_performance {
($operation:expr, $duration_ms:expr, $($field:tt)*) => {
tracing::debug!(
operation = $operation,
duration_ms = $duration_ms,
category = "performance",
$($field)*
);
};
}
pub mod event_types {
pub const AUTH_SUCCESS: &str = "authentication.success";
pub const AUTH_FAILURE: &str = "authentication.failure";
pub const RATE_LIMIT_EXCEEDED: &str = "rate_limit.exceeded";
pub const THREAT_DETECTED: &str = "threat.detected";
pub const THREAT_BLOCKED: &str = "threat.blocked";
pub const REQUEST_PROCESSED: &str = "request.processed";
pub const CIRCUIT_BREAKER_OPEN: &str = "protection.activated";
pub const CIRCUIT_BREAKER_CLOSE: &str = "protection.restored";
pub const PATTERN_DETECTED: &str = "pattern.detected";
pub const PERFORMANCE_OPTIMIZED: &str = "performance.optimized";
}
pub mod field_names {
pub const CLIENT_ID: &str = "client.id";
pub const METHOD: &str = "request.method";
pub const THREAT_TYPE: &str = "threat.type";
pub const THREAT_SEVERITY: &str = "threat.severity";
pub const RESPONSE_TIME_MS: &str = "response.time_ms";
pub const TOKENS_REMAINING: &str = "rate_limit.tokens_remaining";
pub const PATTERN_CONFIDENCE: &str = "pattern.confidence";
pub const OPTIMIZATION_LEVEL: &str = "performance.level";
}
pub struct SemanticLogger;
impl SemanticLogger {
pub fn auth_event(success: bool, client_id: &str, method: Option<&str>) {
let event_type = if success {
event_types::AUTH_SUCCESS
} else {
event_types::AUTH_FAILURE
};
if let Some(method) = method {
tracing::info!(
event_type = event_type,
client.id = client_id,
auth.method = method,
category = "security"
);
} else {
tracing::info!(
event_type = event_type,
client.id = client_id,
category = "security"
);
}
}
pub fn threat_detected(client_id: &str, threat_type: &str, severity: &str) {
tracing::warn!(
event_type = event_types::THREAT_DETECTED,
client.id = client_id,
threat.type = threat_type,
threat.severity = severity,
category = "security"
);
}
pub fn rate_limit_event(client_id: &str, allowed: bool, tokens: f64) {
if allowed {
tracing::debug!(
event_type = "rate_limit.check",
client.id = client_id,
rate_limit.allowed = allowed,
rate_limit.tokens_remaining = tokens,
category = "security"
);
} else {
tracing::warn!(
event_type = event_types::RATE_LIMIT_EXCEEDED,
client.id = client_id,
rate_limit.allowed = allowed,
rate_limit.tokens_remaining = tokens,
category = "security"
);
}
}
pub fn performance_metric(operation: &str, duration_ms: u64, enhanced: bool) {
let level = if enhanced { "optimized" } else { "standard" };
tracing::debug!(
event_type = "performance.metric",
operation = operation,
duration_ms = duration_ms,
performance.mode = level,
category = "performance"
);
}
pub fn circuit_breaker_event(endpoint: &str, open: bool) {
let event_type = if open {
event_types::CIRCUIT_BREAKER_OPEN
} else {
event_types::CIRCUIT_BREAKER_CLOSE
};
tracing::info!(
event_type = event_type,
endpoint = endpoint,
protection.active = open,
category = "security"
);
}
}
pub fn sanitize_for_log(input: &str) -> String {
let sanitized = input
.replace("enhanced", "optimized")
.replace("standard", "normal");
if sanitized.len() > 200 {
format!("{}...", &sanitized[..200])
} else {
sanitized
}
}
#[macro_export]
macro_rules! request_span {
($method:expr, $request_id:expr, $client_id:expr) => {
tracing::info_span!(
"request",
request.id = $request_id,
request.method = $method,
client.id = $client_id,
request.start_time = %chrono::Utc::now(),
)
};
}
#[macro_export]
macro_rules! log_request_complete {
($span:expr, $success:expr, $duration_ms:expr) => {
tracing::info!(
parent: &$span,
event_type = "request.complete",
request.success = $success,
request.duration_ms = $duration_ms,
category = "performance"
);
};
}
#[derive(Debug, Serialize)]
pub struct ErrorLog {
pub error_type: String,
pub error_code: i32,
pub message: String,
pub severity: String,
pub retryable: bool,
pub context: Option<serde_json::Value>,
}
impl ErrorLog {
pub fn from_kindly_error(error: &crate::error::KindlyError) -> Self {
Self {
error_type: format!("{error:?}"),
error_code: error.to_protocol_code(),
message: error.to_string(),
severity: format!("{:?}", error.severity()),
retryable: error.is_retryable(),
context: None,
}
}
pub fn log(&self) {
match self.severity.as_str() {
"Critical" => tracing::error!(
error = serde_json::to_string(self).unwrap_or_default(),
category = "error"
),
"High" => tracing::warn!(
error = serde_json::to_string(self).unwrap_or_default(),
category = "error"
),
_ => tracing::info!(
error = serde_json::to_string(self).unwrap_or_default(),
category = "error"
),
}
}
}