1use std::num::NonZeroU8;
8
9use time::format_description;
10use web_time_compat::{SystemTime, SystemTimeExt};
11
12pub(super) fn new_formatter(
15 granularity: std::time::Duration,
16) -> impl tracing_subscriber::fmt::time::FormatTime {
17 LogPrecision::from_duration(granularity).timer()
18}
19
20#[derive(Clone, Debug)]
25#[cfg_attr(test, derive(Copy, Eq, PartialEq))]
26enum LogPrecision {
27 Subseconds(u8),
33 Seconds(u8),
38 Minutes(u8),
43
44 Hours,
46}
47
48fn ilog10_roundup(x: u32) -> u8 {
52 x.saturating_sub(1)
53 .checked_ilog10()
54 .map(|x| (x + 1) as u8)
55 .unwrap_or(0)
56}
57
58#[derive(Clone, Debug)]
60enum TimeRounder {
61 Verbatim,
64 RoundMinutes(NonZeroU8),
67 RoundSeconds(NonZeroU8),
70}
71
72struct LogTimer {
74 rounder: TimeRounder,
76 formatter: format_description::OwnedFormatItem,
79}
80
81impl LogPrecision {
82 fn from_duration(dur: std::time::Duration) -> Self {
90 let seconds = match (dur.as_secs(), dur.subsec_nanos()) {
92 (0, _) => 0,
93 (a, 0) => a,
94 (a, _) => a + 1,
95 };
96
97 if seconds >= 3541 {
99 LogPrecision::Hours
101 } else if seconds >= 60 {
102 let minutes = seconds.div_ceil(60);
103 assert!((1..=59).contains(&minutes));
104 LogPrecision::Minutes(minutes.try_into().expect("Math bug"))
105 } else if seconds >= 1 {
106 assert!((1..=59).contains(&seconds));
107 LogPrecision::Seconds(seconds.try_into().expect("Math bug"))
108 } else {
109 let ilog10 = ilog10_roundup(dur.subsec_nanos());
110 if ilog10 >= 9 {
111 LogPrecision::Seconds(1)
112 } else {
113 LogPrecision::Subseconds(9 - ilog10)
114 }
115 }
116 }
117
118 fn timer(&self) -> LogTimer {
121 use LogPrecision::*;
122 let format_str = match self {
123 Hours => "[year]-[month]-[day]T[hour repr:24]:00:00Z".to_string(),
124 Minutes(_) => "[year]-[month]-[day]T[hour repr:24]:[minute]:00Z".to_string(),
125 Seconds(_) => "[year]-[month]-[day]T[hour repr:24]:[minute]:[second]Z".to_string(),
126 Subseconds(significant_digits) => {
127 assert!(*significant_digits >= 1 && *significant_digits <= 9);
128 format!(
129 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:{}]Z",
130 significant_digits
131 )
132 }
133 };
134 let formatter = format_description::parse_owned::<2>(&format_str)
135 .expect("Couldn't parse a built-in time format string");
136 let rounder = match self {
137 Hours | Minutes(1) | Seconds(1) | Subseconds(_) => TimeRounder::Verbatim,
138 Minutes(granularity) => {
139 TimeRounder::RoundMinutes((*granularity).try_into().expect("Math bug"))
140 }
141 Seconds(granularity) => {
142 TimeRounder::RoundSeconds((*granularity).try_into().expect("Math bug"))
143 }
144 };
145
146 LogTimer { rounder, formatter }
147 }
148}
149
150#[derive(thiserror::Error, Debug)]
154#[non_exhaustive]
155enum TimeFmtError {
156 #[error("Internal error while trying to round the time.")]
161 Rounding(#[from] time::error::ComponentRange),
162
163 #[error("`time` couldn't format this time.")]
168 TimeFmt(#[from] time::error::Format),
169}
170
171impl TimeRounder {
172 fn round(&self, when: time::OffsetDateTime) -> Result<time::OffsetDateTime, TimeFmtError> {
177 use TimeRounder::*;
191 fn round_down(inp: u8, granularity: NonZeroU8) -> u8 {
193 inp - (inp % granularity)
194 }
195
196 Ok(match self {
197 Verbatim => when,
198 RoundMinutes(granularity) => {
199 when.replace_minute(round_down(when.minute(), *granularity))?
200 }
201 RoundSeconds(granularity) => {
202 when.replace_second(round_down(when.second(), *granularity))?
203 }
204 })
205 }
206}
207
208impl LogTimer {
209 fn time_to_string(&self, when: time::OffsetDateTime) -> Result<String, TimeFmtError> {
211 Ok(self.rounder.round(when)?.format(&self.formatter)?)
213 }
214}
215
216impl tracing_subscriber::fmt::time::FormatTime for LogTimer {
217 fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
218 let now_utc: time::OffsetDateTime = SystemTime::get().into();
220 w.write_str(&self.time_to_string(now_utc).map_err(|_| std::fmt::Error)?)
221 }
222}
223
224#[cfg(test)]
225mod test {
226 #![allow(clippy::bool_assert_comparison)]
228 #![allow(clippy::clone_on_copy)]
229 #![allow(clippy::dbg_macro)]
230 #![allow(clippy::mixed_attributes_style)]
231 #![allow(clippy::print_stderr)]
232 #![allow(clippy::print_stdout)]
233 #![allow(clippy::single_char_pattern)]
234 #![allow(clippy::unwrap_used)]
235 #![allow(clippy::unchecked_time_subtraction)]
236 #![allow(clippy::useless_vec)]
237 #![allow(clippy::needless_pass_by_value)]
238 use super::*;
241 use std::time::Duration;
242
243 #[test]
244 fn ilog() {
245 assert_eq!(ilog10_roundup(0), 0);
246 assert_eq!(ilog10_roundup(1), 0);
247 assert_eq!(ilog10_roundup(2), 1);
248 assert_eq!(ilog10_roundup(9), 1);
249 assert_eq!(ilog10_roundup(10), 1);
250 assert_eq!(ilog10_roundup(11), 2);
251 assert_eq!(ilog10_roundup(99), 2);
252 assert_eq!(ilog10_roundup(100), 2);
253 assert_eq!(ilog10_roundup(101), 3);
254 assert_eq!(ilog10_roundup(99_999_999), 8);
255 assert_eq!(ilog10_roundup(100_000_000), 8);
256 assert_eq!(ilog10_roundup(100_000_001), 9);
257 assert_eq!(ilog10_roundup(999_999_999), 9);
258 assert_eq!(ilog10_roundup(1_000_000_000), 9);
259 assert_eq!(ilog10_roundup(1_000_000_001), 10);
260
261 assert_eq!(ilog10_roundup(u32::MAX), 10);
262 }
263
264 #[test]
265 fn precision_from_duration() {
266 use LogPrecision::*;
267 fn check(sec: u64, nanos: u32, expected: LogPrecision) {
268 assert_eq!(
269 LogPrecision::from_duration(Duration::new(sec, nanos)),
270 expected,
271 );
272 }
273
274 check(0, 0, Subseconds(9));
275 check(0, 1, Subseconds(9));
276 check(0, 5, Subseconds(8));
277 check(0, 10, Subseconds(8));
278 check(0, 1_000, Subseconds(6));
279 check(0, 1_000_000, Subseconds(3));
280 check(0, 99_000_000, Subseconds(1));
281 check(0, 100_000_000, Subseconds(1));
282 check(0, 200_000_000, Seconds(1));
283
284 check(1, 0, Seconds(1));
285 check(1, 1, Seconds(2));
286 check(30, 0, Seconds(30));
287 check(59, 0, Seconds(59));
288
289 check(59, 1, Minutes(1));
290 check(60, 0, Minutes(1));
291 check(60, 1, Minutes(2));
292 check(60 * 59, 0, Minutes(59));
293
294 check(60 * 59, 1, Hours);
295 check(3600, 0, Hours);
296 check(86400 * 365, 0, Hours);
297 }
298
299 #[test]
300 fn test_formatting() {
301 let when = humantime::parse_rfc3339("2023-07-05T04:15:36.123456789Z")
302 .unwrap()
303 .into();
304 let check = |precision: LogPrecision, expected| {
305 assert_eq!(&precision.timer().time_to_string(when).unwrap(), expected);
306 };
307 check(LogPrecision::Hours, "2023-07-05T04:00:00Z");
308 check(LogPrecision::Minutes(15), "2023-07-05T04:15:00Z");
309 check(LogPrecision::Minutes(10), "2023-07-05T04:10:00Z");
310 check(LogPrecision::Minutes(4), "2023-07-05T04:12:00Z");
311 check(LogPrecision::Minutes(1), "2023-07-05T04:15:00Z");
312 check(LogPrecision::Seconds(50), "2023-07-05T04:15:00Z");
313 check(LogPrecision::Seconds(30), "2023-07-05T04:15:30Z");
314 check(LogPrecision::Seconds(20), "2023-07-05T04:15:20Z");
315 check(LogPrecision::Seconds(1), "2023-07-05T04:15:36Z");
316 check(LogPrecision::Subseconds(1), "2023-07-05T04:15:36.1Z");
317 check(LogPrecision::Subseconds(2), "2023-07-05T04:15:36.12Z");
318 check(LogPrecision::Subseconds(7), "2023-07-05T04:15:36.1234567Z");
319 cfg_if::cfg_if! {
320 if #[cfg(windows)] {
321 let expected = "2023-07-05T04:15:36.123456700Z";
324 } else {
325 let expected = "2023-07-05T04:15:36.123456789Z";
326 }
327 }
328 check(LogPrecision::Subseconds(9), expected);
329 }
330}