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