use std::fmt;
use std::sync::OnceLock;
use std::time::SystemTime;
use serde::{Serialize, Serializer};
use crate::RunId;
static GLOBAL_LOGGER: OnceLock<StructuredLogger> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
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 Serialize for LogLevel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
pub timestamp: String,
pub level: LogLevel,
pub message: String,
pub target: String,
#[serde(flatten)]
pub fields: serde_json::Map<String, serde_json::Value>,
}
impl LogEntry {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
LogEntry {
timestamp: Self::current_timestamp(),
level,
message: message.into(),
target: "bzzz".to_string(),
fields: serde_json::Map::new(),
}
}
pub fn with_target(mut self, target: impl Into<String>) -> Self {
self.target = target.into();
self
}
pub fn with_run_id(mut self, run_id: &RunId) -> Self {
self.fields.insert(
"run_id".into(),
serde_json::Value::String(run_id.as_str().to_string()),
);
self
}
pub fn with_step_id(mut self, step_id: &str) -> Self {
self.fields.insert(
"step_id".into(),
serde_json::Value::String(step_id.to_string()),
);
self
}
pub fn with_worker(mut self, worker: &str) -> Self {
self.fields.insert(
"worker".into(),
serde_json::Value::String(worker.to_string()),
);
self
}
pub fn with_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 with_fields(
mut self,
fields: impl IntoIterator<Item = (String, serde_json::Value)>,
) -> Self {
for (key, value) in fields {
self.fields.insert(key, value);
}
self
}
fn current_timestamp() -> String {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let nanos = now.subsec_nanos();
chrono_builder(secs, nanos)
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
format!(
r#"{{"timestamp":"{}","level":"{}","message":"{}"}}"#,
self.timestamp, self.level, self.message
)
})
}
}
fn chrono_builder(secs: u64, nanos: u32) -> String {
const SECS_PER_DAY: u64 = 86400;
const SECS_PER_HOUR: u64 = 3600;
const SECS_PER_MIN: u64 = 60;
let days_since_epoch = secs / SECS_PER_DAY;
let secs_remaining = secs % SECS_PER_DAY;
let hour = secs_remaining / SECS_PER_HOUR;
let mins_remaining = secs_remaining % SECS_PER_HOUR;
let minute = mins_remaining / SECS_PER_MIN;
let second = mins_remaining % SECS_PER_MIN;
let (year, month, day) = days_to_ymd(days_since_epoch as i64);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
year,
month,
day,
hour,
minute,
second,
nanos / 1000
)
}
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
let year = 1970 + (days / 365) as i32;
let day_of_year = (days % 365) as u32;
let (month, day) = if day_of_year < 31 {
(1, day_of_year + 1)
} else if day_of_year < 59 {
(2, day_of_year - 30)
} else if day_of_year < 90 {
(3, day_of_year - 58)
} else if day_of_year < 120 {
(4, day_of_year - 89)
} else if day_of_year < 151 {
(5, day_of_year - 119)
} else if day_of_year < 181 {
(6, day_of_year - 150)
} else if day_of_year < 212 {
(7, day_of_year - 180)
} else if day_of_year < 243 {
(8, day_of_year - 211)
} else if day_of_year < 273 {
(9, day_of_year - 242)
} else if day_of_year < 304 {
(10, day_of_year - 272)
} else if day_of_year < 334 {
(11, day_of_year - 303)
} else {
(12, day_of_year - 333)
};
(year, month, day)
}
pub struct StructuredLogger {
level: LogLevel,
}
impl StructuredLogger {
pub fn new(level: LogLevel) -> Self {
StructuredLogger { level }
}
pub fn info_level() -> Self {
Self::new(LogLevel::Info)
}
pub fn log(&self, level: LogLevel, message: impl Into<String>) {
if level as u8 >= self.level as u8 {
let entry = LogEntry::new(level, message);
println!("{}", entry.to_json());
}
}
pub fn log_with_fields(
&self,
level: LogLevel,
message: impl Into<String>,
fields: impl IntoIterator<Item = (String, serde_json::Value)>,
) {
if level as u8 >= self.level as u8 {
let entry = LogEntry::new(level, message).with_fields(fields);
println!("{}", entry.to_json());
}
}
pub fn trace(&self, message: impl Into<String>) {
self.log(LogLevel::Trace, message);
}
pub fn debug(&self, message: impl Into<String>) {
self.log(LogLevel::Debug, message);
}
pub fn info(&self, message: impl Into<String>) {
self.log(LogLevel::Info, message);
}
pub fn warn(&self, message: impl Into<String>) {
self.log(LogLevel::Warn, message);
}
pub fn error(&self, message: impl Into<String>) {
self.log(LogLevel::Error, message);
}
}
impl Default for StructuredLogger {
fn default() -> Self {
Self::info_level()
}
}
pub fn global_logger() -> &'static StructuredLogger {
GLOBAL_LOGGER.get_or_init(StructuredLogger::info_level)
}
pub fn init_global_logger(level: LogLevel) {
let _ = GLOBAL_LOGGER.get_or_init(|| StructuredLogger::new(level));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_display() {
assert_eq!(LogLevel::Info.to_string(), "INFO");
assert_eq!(LogLevel::Error.to_string(), "ERROR");
}
#[test]
fn test_log_entry_creation() {
let entry = LogEntry::new(LogLevel::Info, "test message");
assert_eq!(entry.message, "test message");
assert_eq!(entry.level, LogLevel::Info);
}
#[test]
fn test_log_entry_with_field() {
let entry = LogEntry::new(LogLevel::Info, "test").with_field("key", "value");
assert!(entry.fields.contains_key("key"));
}
#[test]
fn test_log_entry_with_run_id() {
let run_id = RunId::new();
let entry = LogEntry::new(LogLevel::Info, "test").with_run_id(&run_id);
assert!(entry.fields.contains_key("run_id"));
assert_eq!(
entry.fields.get("run_id").unwrap().as_str().unwrap(),
run_id.as_str()
);
}
#[test]
fn test_log_entry_with_worker() {
let entry = LogEntry::new(LogLevel::Info, "test").with_worker("w1");
assert!(entry.fields.contains_key("worker"));
assert_eq!(entry.fields.get("worker").unwrap().as_str().unwrap(), "w1");
}
#[test]
fn test_global_logger() {
let logger = global_logger();
logger.info("test message");
}
#[test]
fn test_log_entry_to_json() {
let entry = LogEntry::new(LogLevel::Info, "test message");
let json = entry.to_json();
assert!(json.contains("\"level\":\"INFO\""));
assert!(json.contains("\"message\":\"test message\""));
}
#[tokio::test]
async fn test_structured_logger() {
let logger = StructuredLogger::info_level();
logger.info("info message");
logger.warn("warning message");
logger.error("error message");
}
}