1use std::borrow::Cow;
4
5#[cfg(feature = "colors")]
6pub(crate) mod color;
7pub(crate) mod event;
8#[cfg(feature = "json")]
9pub(crate) mod json;
10pub(crate) mod span_chain;
11
12#[cfg(feature = "colors")]
13pub use color::{ColorMode, ColorTheme};
14
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20pub enum TimestampFormat {
21 #[default]
23 None,
24 UnixSeconds,
27 Uptime,
30 Rfc3339,
35}
36
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub(crate) enum RenderMode {
43 #[default]
44 Pretty,
45 #[cfg(feature = "json")]
46 Json,
47}
48
49#[derive(Debug, Clone)]
54#[allow(clippy::struct_excessive_bools)] pub(crate) struct FormatConfig {
56 #[cfg_attr(not(feature = "json"), allow(dead_code))]
57 pub mode: RenderMode,
58 pub show_target: bool,
59 pub show_thread_id: bool,
60 pub show_timestamp: bool,
61 pub timestamp_format: TimestampFormat,
62 pub use_level_prefix: bool,
63 pub span_separator: Cow<'static, str>,
64 pub message_separator: Cow<'static, str>,
65 pub level_separator: Cow<'static, str>,
66 pub function_bracket_left: Cow<'static, str>,
67 pub function_bracket_right: Cow<'static, str>,
68 pub arguments_equality: Cow<'static, str>,
69 pub arguments_separator: Cow<'static, str>,
70 pub thread_id_prefix: Cow<'static, str>,
71 pub thread_id_suffix: Cow<'static, str>,
72}
73
74impl Default for FormatConfig {
75 fn default() -> Self {
76 Self {
77 mode: RenderMode::Pretty,
78 show_target: false,
79 show_thread_id: false,
80 show_timestamp: false,
81 timestamp_format: TimestampFormat::None,
82 use_level_prefix: true,
83 span_separator: Cow::Borrowed("::"),
84 message_separator: Cow::Borrowed(": "),
85 level_separator: Cow::Borrowed(" "),
86 function_bracket_left: Cow::Borrowed("("),
87 function_bracket_right: Cow::Borrowed(")"),
88 arguments_equality: Cow::Borrowed(": "),
89 arguments_separator: Cow::Borrowed(", "),
90 thread_id_prefix: Cow::Borrowed("["),
91 thread_id_suffix: Cow::Borrowed("] "),
92 }
93 }
94}
95
96pub(crate) fn syslog_prefix(level: tracing::Level) -> &'static str {
100 match level {
101 tracing::Level::ERROR => "<3>",
102 tracing::Level::WARN => "<4>",
103 tracing::Level::INFO => "<5>",
104 tracing::Level::DEBUG => "<6>",
105 tracing::Level::TRACE => "<7>",
106 }
107}
108
109pub(crate) fn current_thread_id_int() -> String {
114 let id = format!("{:?}", std::thread::current().id());
115 id.split_once('(')
117 .and_then(|(_, rest)| rest.split_once(')'))
118 .map_or_else(|| id.clone(), |(digits, _)| digits.to_owned())
119}
120
121pub(crate) fn format_timestamp(format: TimestampFormat) -> String {
124 use std::time::{SystemTime, UNIX_EPOCH};
125
126 match format {
127 TimestampFormat::None => String::new(),
128 TimestampFormat::UnixSeconds => SystemTime::now()
129 .duration_since(UNIX_EPOCH)
130 .map_or_else(|_| String::from("0.000"), |d| {
131 format!("{}.{:03}", d.as_secs(), d.subsec_millis())
132 }),
133 TimestampFormat::Uptime => {
134 let elapsed = process_start().elapsed();
135 format!("{}.{:03}", elapsed.as_secs(), elapsed.subsec_millis())
136 }
137 TimestampFormat::Rfc3339 => format_rfc3339(SystemTime::now()),
138 }
139}
140
141fn format_rfc3339(now: std::time::SystemTime) -> String {
145 use std::time::UNIX_EPOCH;
146 let Ok(dur) = now.duration_since(UNIX_EPOCH) else {
147 return String::from("1970-01-01T00:00:00.000Z");
148 };
149 let total_secs = dur.as_secs();
150 let millis = dur.subsec_millis();
151 #[allow(clippy::cast_possible_wrap)]
153 let days = (total_secs / 86_400) as i64;
154 let sod = total_secs % 86_400;
155 let hour = sod / 3_600;
156 let minute = (sod % 3_600) / 60;
157 let second = sod % 60;
158 let (year, month, day) = civil_from_days(days);
159 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
160}
161
162#[allow(
167 clippy::cast_possible_wrap,
168 clippy::cast_sign_loss,
169 clippy::cast_possible_truncation
170)]
171fn civil_from_days(z: i64) -> (i64, u32, u32) {
172 let z = z + 719_468;
173 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
174 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;
177 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 };
182 (year, m as u32, d as u32)
183}
184
185fn process_start() -> std::time::Instant {
186 use std::sync::OnceLock;
187 static START: OnceLock<std::time::Instant> = OnceLock::new();
188 *START.get_or_init(std::time::Instant::now)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn syslog_prefix_maps_levels() {
197 assert_eq!(syslog_prefix(tracing::Level::ERROR), "<3>");
198 assert_eq!(syslog_prefix(tracing::Level::WARN), "<4>");
199 assert_eq!(syslog_prefix(tracing::Level::INFO), "<5>");
200 assert_eq!(syslog_prefix(tracing::Level::DEBUG), "<6>");
201 assert_eq!(syslog_prefix(tracing::Level::TRACE), "<7>");
202 }
203
204 #[test]
205 fn current_thread_id_int_is_numeric() {
206 let s = current_thread_id_int();
207 if let Ok(n) = s.parse::<u64>() {
211 assert!(n > 0);
213 }
214 }
215
216 #[test]
217 fn format_timestamp_none_is_empty() {
218 assert_eq!(format_timestamp(TimestampFormat::None), "");
219 }
220
221 #[test]
222 fn format_timestamp_unix_has_dot() {
223 let s = format_timestamp(TimestampFormat::UnixSeconds);
224 assert!(s.contains('.'), "unexpected {s:?}");
225 }
226
227 #[test]
228 fn format_timestamp_uptime_has_dot() {
229 let s = format_timestamp(TimestampFormat::Uptime);
230 assert!(s.contains('.'), "unexpected {s:?}");
231 }
232
233 #[test]
234 fn rfc3339_known_epochs() {
235 use std::time::{Duration, UNIX_EPOCH};
236 assert_eq!(
238 format_rfc3339(UNIX_EPOCH),
239 "1970-01-01T00:00:00.000Z"
240 );
241 let t = UNIX_EPOCH + Duration::new(1_777_991_025, 123_000_000);
246 assert_eq!(format_rfc3339(t), "2026-05-05T14:23:45.123Z");
247 let t = UNIX_EPOCH + Duration::new(951_782_400, 0);
250 assert_eq!(format_rfc3339(t), "2000-02-29T00:00:00.000Z");
251 let t = UNIX_EPOCH + Duration::new(946_684_799, 999_000_000);
253 assert_eq!(format_rfc3339(t), "1999-12-31T23:59:59.999Z");
254 }
255
256 #[test]
257 fn format_timestamp_rfc3339_shape() {
258 let s = format_timestamp(TimestampFormat::Rfc3339);
259 assert_eq!(s.len(), 24, "got {s:?}");
261 assert!(s.ends_with('Z'), "got {s:?}");
262 assert!(s.contains('T'), "got {s:?}");
263 }
264}