use std::env;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn from_env() -> Self {
env::var("RUST_LOG")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(LogLevel::Info)
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Trace => write!(f, "trace"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Info => write!(f, "info"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::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(LogLevel::Trace),
"debug" => Ok(LogLevel::Debug),
"info" => Ok(LogLevel::Info),
"warn" | "warning" => Ok(LogLevel::Warn),
"error" => Ok(LogLevel::Error),
_ => Err(format!("Invalid log level: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Text,
Json,
Compact,
}
impl LogFormat {
pub fn from_env() -> Self {
env::var("LOG_FORMAT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(LogFormat::Text)
}
}
impl fmt::Display for LogFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogFormat::Text => write!(f, "text"),
LogFormat::Json => write!(f, "json"),
LogFormat::Compact => write!(f, "compact"),
}
}
}
impl std::str::FromStr for LogFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" | "pretty" | "human" => Ok(LogFormat::Text),
"json" => Ok(LogFormat::Json),
"compact" => Ok(LogFormat::Compact),
_ => Err(format!("Invalid log format: {}", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct LogConfig {
pub level: LogLevel,
pub format: LogFormat,
pub timestamps: bool,
pub source_location: bool,
pub thread_ids: bool,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: LogLevel::Info,
format: LogFormat::Text,
timestamps: true,
source_location: false,
thread_ids: false,
}
}
}
impl LogConfig {
pub fn from_env() -> Self {
Self {
level: LogLevel::from_env(),
format: LogFormat::from_env(),
timestamps: env::var("LOG_TIMESTAMPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(true),
source_location: env::var("LOG_SOURCE_LOCATION")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(false),
thread_ids: env::var("LOG_THREAD_IDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(false),
}
}
}
pub fn init_logging(config: LogConfig) -> Result<(), LoggingError> {
if env::var("RUST_LOG").is_err() {
unsafe {
env::set_var("RUST_LOG", format!("thread_flow={}", config.level));
}
}
let mut builder = env_logger::builder();
builder.parse_env("RUST_LOG");
if let Some(precision) = if config.timestamps {
Some(env_logger::fmt::TimestampPrecision::Millis)
} else {
None
} {
builder.format_timestamp(Some(precision));
} else {
builder.format_timestamp(None);
}
builder.format_module_path(config.source_location);
builder
.try_init()
.map_err(|e| LoggingError::InitializationFailed(e.to_string()))?;
Ok(())
}
pub fn init_cli_logging() -> Result<(), LoggingError> {
init_logging(LogConfig {
level: LogLevel::from_env(),
format: LogFormat::Text,
timestamps: true,
source_location: false,
thread_ids: false,
})
}
pub fn init_production_logging() -> Result<(), LoggingError> {
init_logging(LogConfig {
level: LogLevel::Info,
format: LogFormat::Json,
timestamps: true,
source_location: true,
thread_ids: true,
})
}
#[derive(Debug, thiserror::Error)]
pub enum LoggingError {
#[error("Failed to initialize logging: {0}")]
InitializationFailed(String),
#[error("Invalid log configuration: {0}")]
InvalidConfiguration(String),
}
#[macro_export]
macro_rules! timed_operation {
($name:expr, $($key:ident = $value:expr),*, $block:block) => {{
let _start = std::time::Instant::now();
$(
println!("[DEBUG] {}: {} = {:?}", $name, stringify!($key), $value);
)*
let result = $block;
let _duration = _start.elapsed();
println!("[INFO] {} completed in {:?}", $name, _duration);
result
}};
}
pub mod structured {
use thread_utilities::RapidMap;
pub struct LogContext {
fields: RapidMap<String, String>,
}
impl LogContext {
pub fn new() -> Self {
Self {
fields: thread_utilities::get_map(),
}
}
pub fn field(mut self, key: impl Into<String>, value: impl ToString) -> Self {
self.fields.insert(key.into(), value.to_string());
self
}
pub fn info(self, message: &str) {
println!("[INFO] {} {:?}", message, self.fields);
}
pub fn warn(self, message: &str) {
eprintln!("[WARN] {} {:?}", message, self.fields);
}
pub fn error(self, message: &str) {
eprintln!("[ERROR] {} {:?}", message, self.fields);
}
}
impl Default for LogContext {
fn default() -> Self {
Self::new()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_parsing() {
assert_eq!("trace".parse::<LogLevel>().unwrap(), LogLevel::Trace);
assert_eq!("debug".parse::<LogLevel>().unwrap(), LogLevel::Debug);
assert_eq!("info".parse::<LogLevel>().unwrap(), LogLevel::Info);
assert_eq!("warn".parse::<LogLevel>().unwrap(), LogLevel::Warn);
assert_eq!("error".parse::<LogLevel>().unwrap(), LogLevel::Error);
}
#[test]
fn test_log_format_parsing() {
assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Text);
assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
assert_eq!("compact".parse::<LogFormat>().unwrap(), LogFormat::Compact);
}
#[test]
fn test_log_config_default() {
let config = LogConfig::default();
assert_eq!(config.level, LogLevel::Info);
assert_eq!(config.format, LogFormat::Text);
assert!(config.timestamps);
assert!(!config.source_location);
assert!(!config.thread_ids);
}
}