use std::borrow::Cow;
#[cfg(feature = "colors")]
pub(crate) mod color;
pub(crate) mod event;
#[cfg(feature = "json")]
pub(crate) mod json;
pub(crate) mod span_chain;
#[cfg(feature = "colors")]
pub use color::{ColorMode, ColorTheme};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TimestampFormat {
#[default]
None,
UnixSeconds,
Uptime,
Rfc3339,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum RenderMode {
#[default]
Pretty,
#[cfg(feature = "json")]
Json,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub(crate) struct FormatConfig {
#[cfg_attr(not(feature = "json"), allow(dead_code))]
pub mode: RenderMode,
pub show_target: bool,
pub show_thread_id: bool,
pub show_timestamp: bool,
pub timestamp_format: TimestampFormat,
pub use_level_prefix: bool,
pub span_separator: Cow<'static, str>,
pub message_separator: Cow<'static, str>,
pub level_separator: Cow<'static, str>,
pub function_bracket_left: Cow<'static, str>,
pub function_bracket_right: Cow<'static, str>,
pub arguments_equality: Cow<'static, str>,
pub arguments_separator: Cow<'static, str>,
pub thread_id_prefix: Cow<'static, str>,
pub thread_id_suffix: Cow<'static, str>,
}
impl Default for FormatConfig {
fn default() -> Self {
Self {
mode: RenderMode::Pretty,
show_target: false,
show_thread_id: false,
show_timestamp: false,
timestamp_format: TimestampFormat::None,
use_level_prefix: true,
span_separator: Cow::Borrowed("::"),
message_separator: Cow::Borrowed(": "),
level_separator: Cow::Borrowed(" "),
function_bracket_left: Cow::Borrowed("("),
function_bracket_right: Cow::Borrowed(")"),
arguments_equality: Cow::Borrowed(": "),
arguments_separator: Cow::Borrowed(", "),
thread_id_prefix: Cow::Borrowed("["),
thread_id_suffix: Cow::Borrowed("] "),
}
}
}
pub(crate) fn syslog_prefix(level: tracing::Level) -> &'static str {
match level {
tracing::Level::ERROR => "<3>",
tracing::Level::WARN => "<4>",
tracing::Level::INFO => "<5>",
tracing::Level::DEBUG => "<6>",
tracing::Level::TRACE => "<7>",
}
}
pub(crate) fn current_thread_id_int() -> String {
let id = format!("{:?}", std::thread::current().id());
id.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.map_or_else(|| id.clone(), |(digits, _)| digits.to_owned())
}
pub(crate) fn format_timestamp(format: TimestampFormat) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
match format {
TimestampFormat::None => String::new(),
TimestampFormat::UnixSeconds => SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or_else(|_| String::from("0.000"), |d| {
format!("{}.{:03}", d.as_secs(), d.subsec_millis())
}),
TimestampFormat::Uptime => {
let elapsed = process_start().elapsed();
format!("{}.{:03}", elapsed.as_secs(), elapsed.subsec_millis())
}
TimestampFormat::Rfc3339 => format_rfc3339(SystemTime::now()),
}
}
fn format_rfc3339(now: std::time::SystemTime) -> String {
use std::time::UNIX_EPOCH;
let Ok(dur) = now.duration_since(UNIX_EPOCH) else {
return String::from("1970-01-01T00:00:00.000Z");
};
let total_secs = dur.as_secs();
let millis = dur.subsec_millis();
#[allow(clippy::cast_possible_wrap)]
let days = (total_secs / 86_400) as i64;
let sod = total_secs % 86_400;
let hour = sod / 3_600;
let minute = (sod % 3_600) / 60;
let second = sod % 60;
let (year, month, day) = civil_from_days(days);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
}
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if m <= 2 { y + 1 } else { y };
(year, m as u32, d as u32)
}
fn process_start() -> std::time::Instant {
use std::sync::OnceLock;
static START: OnceLock<std::time::Instant> = OnceLock::new();
*START.get_or_init(std::time::Instant::now)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn syslog_prefix_maps_levels() {
assert_eq!(syslog_prefix(tracing::Level::ERROR), "<3>");
assert_eq!(syslog_prefix(tracing::Level::WARN), "<4>");
assert_eq!(syslog_prefix(tracing::Level::INFO), "<5>");
assert_eq!(syslog_prefix(tracing::Level::DEBUG), "<6>");
assert_eq!(syslog_prefix(tracing::Level::TRACE), "<7>");
}
#[test]
fn current_thread_id_int_is_numeric() {
let s = current_thread_id_int();
if let Ok(n) = s.parse::<u64>() {
assert!(n > 0);
}
}
#[test]
fn format_timestamp_none_is_empty() {
assert_eq!(format_timestamp(TimestampFormat::None), "");
}
#[test]
fn format_timestamp_unix_has_dot() {
let s = format_timestamp(TimestampFormat::UnixSeconds);
assert!(s.contains('.'), "unexpected {s:?}");
}
#[test]
fn format_timestamp_uptime_has_dot() {
let s = format_timestamp(TimestampFormat::Uptime);
assert!(s.contains('.'), "unexpected {s:?}");
}
#[test]
fn rfc3339_known_epochs() {
use std::time::{Duration, UNIX_EPOCH};
assert_eq!(
format_rfc3339(UNIX_EPOCH),
"1970-01-01T00:00:00.000Z"
);
let t = UNIX_EPOCH + Duration::new(1_777_991_025, 123_000_000);
assert_eq!(format_rfc3339(t), "2026-05-05T14:23:45.123Z");
let t = UNIX_EPOCH + Duration::new(951_782_400, 0);
assert_eq!(format_rfc3339(t), "2000-02-29T00:00:00.000Z");
let t = UNIX_EPOCH + Duration::new(946_684_799, 999_000_000);
assert_eq!(format_rfc3339(t), "1999-12-31T23:59:59.999Z");
}
#[test]
fn format_timestamp_rfc3339_shape() {
let s = format_timestamp(TimestampFormat::Rfc3339);
assert_eq!(s.len(), 24, "got {s:?}");
assert!(s.ends_with('Z'), "got {s:?}");
assert!(s.contains('T'), "got {s:?}");
}
}