use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[macro_export]
macro_rules! log_request {
($method:expr, $path:expr, $status:expr, $duration:expr) => {
tracing::info!(
method = $method,
path = $path,
status = $status,
duration_ms = $duration,
"HTTP request completed"
);
};
($method:expr, $path:expr, $status:expr, $duration:expr, $($key:ident = $value:expr),+) => {
tracing::info!(
method = $method,
path = $path,
status = $status,
duration_ms = $duration,
$($key = $value),+,
"HTTP request completed"
);
};
}
#[macro_export]
macro_rules! log_error {
($error_code:expr, $message:expr) => {
tracing::error!(
error_code = $error_code,
$message
);
};
($error_code:expr, $message:expr, $($key:ident = $value:expr),+) => {
tracing::error!(
error_code = $error_code,
$($key = $value),+,
$message
);
};
}
pub fn init() {
let env_filter = match std::env::var("RUST_LOG") {
Ok(val) => EnvFilter::new(val.trim()),
Err(_) => EnvFilter::new("info"),
};
let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
let enable_spans =
std::env::var("LOG_ENABLE_SPANS").unwrap_or_else(|_| "true".to_string()) == "true";
let registry = tracing_subscriber::registry().with(env_filter);
match (log_file_dir, file_only) {
(Some(log_dir), false) => {
let mut console_layer = fmt::layer()
.json()
.with_current_span(enable_spans)
.with_span_list(false);
if enable_spans {
console_layer = console_layer
.with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
}
let file_appender =
RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
let mut file_layer = fmt::layer()
.json()
.with_current_span(enable_spans)
.with_span_list(false)
.with_writer(file_appender);
if enable_spans {
file_layer = file_layer
.with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
}
let _ = registry.with(console_layer).with(file_layer).try_init();
}
(Some(log_dir), true) => {
let file_appender =
RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
let mut file_layer = fmt::layer()
.json()
.with_current_span(enable_spans)
.with_span_list(false)
.with_writer(file_appender);
if enable_spans {
file_layer = file_layer
.with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
}
let _ = registry.with(file_layer).try_init();
}
(None, _) => {
let mut console_layer = fmt::layer()
.json()
.with_current_span(enable_spans)
.with_span_list(false);
if enable_spans {
console_layer = console_layer
.with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
}
let _ = registry.with(console_layer).try_init();
}
}
}
pub fn validate_config() -> Result<String, String> {
let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
let enable_spans =
std::env::var("LOG_ENABLE_SPANS").unwrap_or_else(|_| "true".to_string()) == "true";
if let Err(e) = EnvFilter::try_new(rust_log.trim()) {
return Err(format!("Invalid RUST_LOG format: {}", e));
}
if let Some(ref dir) = log_file_dir {
if let Err(e) = std::fs::create_dir_all(dir) {
return Err(format!("Cannot create log directory '{}': {}", dir, e));
}
}
let config = match (log_file_dir.as_ref(), file_only) {
(Some(dir), false) => format!(
"Console + File logging to {}/{}.YYYY-MM-DD",
dir, log_file_prefix
),
(Some(dir), true) => format!(
"File-only logging to {}/{}.YYYY-MM-DD",
dir, log_file_prefix
),
(None, _) => "Console-only logging".to_string(),
};
let spans_status = if enable_spans { "enabled" } else { "disabled" };
Ok(format!(
"✓ RUST_LOG: {}\n✓ Mode: {}\n✓ Spans: {}",
rust_log, config, spans_status
))
}
pub fn print_config() {
match validate_config() {
Ok(config) => println!("Logging Configuration:\n{}", config),
Err(error) => eprintln!("Logging Configuration Error: {}", error),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_does_not_panic() {
let _ = std::panic::catch_unwind(|| {
init();
});
}
#[test]
fn test_env_var_parsing() {
std::env::set_var("LOG_FILE_PREFIX", "test");
let prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
assert_eq!(prefix, "test");
std::env::remove_var("LOG_FILE_PREFIX");
}
}
pub mod structured {
use tracing::{error, info};
pub fn http_request(method: &str, path: &str, status: u16, duration_ms: u64) {
info!(
method = method,
path = path,
status = status,
duration_ms = duration_ms,
"HTTP request completed"
);
}
pub fn database_op(operation: &str, table: &str, duration_ms: u64, rows_affected: Option<u64>) {
info!(
operation = operation,
table = table,
duration_ms = duration_ms,
rows_affected = rows_affected,
"Database operation completed"
);
}
pub fn user_action(user_id: u64, action: &str, resource: Option<&str>) {
info!(
user_id = user_id,
action = action,
resource = resource,
"User action performed"
);
}
pub fn error_with_context(error_code: &str, message: &str) {
error!(error_code = error_code, "{}" = message);
}
}