use chrono::Offset;
use rocketmq_error::RocketMQError;
use rocketmq_error::RocketMQResult;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use std::sync::OnceLock;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_appender::rolling;
use tracing_subscriber::fmt::writer::MakeWriterExt;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Layer;
use chrono_tz::Tz;
use time::UtcOffset;
use tracing_subscriber::fmt::time::OffsetTime;
static WORKER_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
static LOCAL_TIMEZONE: OnceLock<Tz> = OnceLock::new();
pub fn init_logger() -> RocketMQResult<()> {
let info_level = std::env::var("RUST_LOG").unwrap_or_else(|_| String::from("INFO"));
let level = tracing::Level::from_str(&info_level)
.map_err(|_| RocketMQError::illegal_argument(format!("Invalid log level: {}", info_level)))?;
tracing_subscriber::fmt()
.with_timer(get_timer_from_env())
.with_thread_names(true)
.with_level(true)
.with_line_number(true)
.with_thread_ids(true)
.with_max_level(level)
.try_init()
.map_err(|e| RocketMQError::Internal(format!("Failed to initialize logger: {}", e)))?;
Ok(())
}
pub fn init_logger_with_level(level: Level) -> RocketMQResult<()> {
let tracing_level = level.to_tracing_level()?;
tracing_subscriber::fmt()
.with_timer(get_timer_from_env())
.with_thread_names(true)
.with_level(true)
.with_line_number(true)
.with_thread_ids(true)
.with_max_level(tracing_level)
.try_init()
.map_err(|e| RocketMQError::Internal(format!("Failed to initialize logger: {}", e)))?;
Ok(())
}
pub fn init_logger_with_file(
level: Level,
directory: impl AsRef<Path>,
file_name_prefix: impl AsRef<Path>,
) -> RocketMQResult<()> {
let max_level = tracing_subscriber::filter::LevelFilter::from_str(level.as_str())
.map_err(|_| RocketMQError::illegal_argument(format!("Invalid log level: {}", level)))?;
let file_appender = rolling::daily(directory, file_name_prefix);
let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
let console_layer = tracing_subscriber::fmt::layer()
.with_timer(get_timer_from_env())
.with_writer(std::io::stdout)
.with_thread_names(true)
.with_thread_ids(true)
.with_line_number(true)
.with_level(true)
.with_ansi(true)
.with_filter(max_level);
let file_layer = tracing_subscriber::fmt::layer()
.with_timer(get_timer_from_env())
.with_writer(file_writer)
.with_thread_names(true)
.with_thread_ids(true)
.with_line_number(true)
.with_level(true)
.with_ansi(false)
.with_filter(max_level);
tracing_subscriber::registry()
.with(console_layer)
.with(file_layer)
.try_init()
.map_err(|e| RocketMQError::Internal(format!("Failed to initialize logger: {}", e)))?;
WORKER_GUARD
.set(guard)
.map_err(|_| RocketMQError::Internal("Logger already initialized".to_string()))?;
Ok(())
}
pub fn get_timer_from_env() -> OffsetTime<time::format_description::well_known::Rfc3339> {
let tz: Tz = if let Ok(tz_str) = std::env::var("LOG_TIMEZONE") {
if tz_str.eq_ignore_ascii_case("Local") {
get_local_timezone()
} else {
tz_str.parse().unwrap_or_else(|e| {
eprintln!(
"Warning: Invalid timezone '{}': {}. Falling back to local timezone.",
tz_str, e
);
get_local_timezone()
})
}
} else {
get_local_timezone()
};
let now = chrono::Utc::now().with_timezone(&tz);
let offset_seconds = now.offset().fix().local_minus_utc();
let offset = UtcOffset::from_whole_seconds(offset_seconds).unwrap_or_else(|e| {
eprintln!(
"Warning: Invalid offset {} seconds: {}. Falling back to UTC.",
offset_seconds, e
);
UtcOffset::UTC
});
OffsetTime::new(offset, time::format_description::well_known::Rfc3339)
}
fn get_local_timezone() -> Tz {
*LOCAL_TIMEZONE.get_or_init(|| {
if let Ok(tz_name) = iana_time_zone::get_timezone() {
if let Ok(tz) = tz_name.parse::<Tz>() {
return tz;
} else {
eprintln!(
"Warning: System returned timezone '{}' but it could not be parsed. Trying fallback methods.",
tz_name
);
}
}
if let Ok(tz_name) = std::env::var("TZ") {
if let Ok(tz) = tz_name.parse::<Tz>() {
return tz;
}
}
let local_now = chrono::Local::now();
let offset_seconds = local_now.offset().fix().local_minus_utc();
let offset_hours = offset_seconds / 3600;
eprintln!(
"Warning: Could not determine system timezone. Guessing based on UTC offset ({} hours).",
offset_hours
);
match offset_hours {
-12 => chrono_tz::Pacific::Fiji,
-11 => chrono_tz::Pacific::Midway,
-10 => chrono_tz::Pacific::Honolulu,
-9 => chrono_tz::America::Anchorage,
-8 => chrono_tz::America::Los_Angeles,
-7 => chrono_tz::America::Denver,
-6 => chrono_tz::America::Chicago,
-5 => chrono_tz::America::New_York,
-4 => chrono_tz::America::Halifax,
-3 => chrono_tz::America::Sao_Paulo,
-2 => chrono_tz::Atlantic::South_Georgia,
-1 => chrono_tz::Atlantic::Azores,
0 => chrono_tz::UTC,
1 => chrono_tz::Europe::Paris,
2 => chrono_tz::Europe::Helsinki,
3 => chrono_tz::Europe::Moscow,
4 => chrono_tz::Asia::Dubai,
5 => chrono_tz::Asia::Karachi,
6 => chrono_tz::Asia::Dhaka,
7 => chrono_tz::Asia::Bangkok,
8 => chrono_tz::Asia::Shanghai,
9 => chrono_tz::Asia::Tokyo,
10 => chrono_tz::Australia::Sydney,
11 => chrono_tz::Pacific::Noumea,
12 => chrono_tz::Pacific::Auckland,
_ => {
eprintln!(
"Warning: Unsupported local timezone offset {} hours. Falling back to UTC.",
offset_hours
);
chrono_tz::UTC
}
}
})
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Level(&'static str);
impl Level {
pub const ERROR: Level = Level("ERROR");
pub const WARN: Level = Level("WARN");
pub const INFO: Level = Level("INFO");
pub const DEBUG: Level = Level("DEBUG");
pub const TRACE: Level = Level("TRACE");
#[inline]
pub fn as_str(&self) -> &'static str {
self.0
}
pub fn to_tracing_level(&self) -> RocketMQResult<tracing::Level> {
tracing::Level::from_str(self.0)
.map_err(|_| RocketMQError::illegal_argument(format!("Invalid log level: {}", self.0)))
}
pub fn try_from_str(level: &str) -> Result<Self, String> {
level.parse()
}
}
impl From<&'static str> for Level {
fn from(level: &'static str) -> Self {
Level::try_from_str(level).unwrap_or(Level::INFO)
}
}
impl FromStr for Level {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ERROR" => Ok(Level::ERROR),
"WARN" => Ok(Level::WARN),
"INFO" => Ok(Level::INFO),
"DEBUG" => Ok(Level::DEBUG),
"TRACE" => Ok(Level::TRACE),
_ => Err(format!("Invalid log level: {s}")),
}
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_as_str_returns_correct_value() {
assert_eq!(Level::ERROR.as_str(), "ERROR");
assert_eq!(Level::WARN.as_str(), "WARN");
assert_eq!(Level::INFO.as_str(), "INFO");
assert_eq!(Level::DEBUG.as_str(), "DEBUG");
assert_eq!(Level::TRACE.as_str(), "TRACE");
}
#[test]
fn level_from_str_creates_correct_level() {
assert_eq!(Level::from("ERROR"), Level::ERROR);
assert_eq!(Level::from("WARN"), Level::WARN);
assert_eq!(Level::from("INFO"), Level::INFO);
assert_eq!(Level::from("DEBUG"), Level::DEBUG);
assert_eq!(Level::from("TRACE"), Level::TRACE);
}
#[test]
fn level_from_str_trait_works() {
assert_eq!("ERROR".parse::<Level>().unwrap(), Level::ERROR);
assert_eq!("INFO".parse::<Level>().unwrap(), Level::INFO);
assert!("invalid".parse::<Level>().is_err());
}
#[test]
fn level_try_from_str_reports_invalid_level() {
assert_eq!(Level::try_from_str("WARN").unwrap(), Level::WARN);
assert_eq!(
Level::try_from_str("invalid").expect_err("invalid level should be reported"),
"Invalid log level: invalid"
);
}
#[test]
fn level_from_static_str_does_not_panic_on_invalid_level() {
assert_eq!(Level::from("invalid"), Level::INFO);
}
#[test]
fn level_display_formats_correctly() {
assert_eq!(format!("{}", Level::ERROR), "ERROR");
assert_eq!(format!("{}", Level::WARN), "WARN");
assert_eq!(format!("{}", Level::INFO), "INFO");
assert_eq!(format!("{}", Level::DEBUG), "DEBUG");
assert_eq!(format!("{}", Level::TRACE), "TRACE");
}
#[test]
fn level_to_tracing_level_success() {
assert!(Level::ERROR.to_tracing_level().is_ok());
assert!(Level::WARN.to_tracing_level().is_ok());
assert!(Level::INFO.to_tracing_level().is_ok());
assert!(Level::DEBUG.to_tracing_level().is_ok());
assert!(Level::TRACE.to_tracing_level().is_ok());
}
#[test]
fn level_clone_works() {
let level = Level::INFO;
let cloned = level.clone();
assert_eq!(level, cloned);
}
#[test]
fn level_hash_works() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Level::ERROR);
set.insert(Level::INFO);
assert!(set.contains(&Level::ERROR));
assert!(!set.contains(&Level::DEBUG));
}
}