1
//! Support logging the time with different levels of precision.
2
//
3
// TODO: We might want to move this to a lower-level crate if it turns out to be
4
// generally useful: and it might, if we are encouraging the use of `tracing`
5
// with arti!  If we do this, we need to clean up the API a little.
6

            
7
use std::num::NonZeroU8;
8

            
9
use time::format_description;
10
use web_time_compat::{SystemTime, SystemTimeExt};
11

            
12
/// Construct a new [`FormatTime`](tracing_subscriber::fmt::time::FormatTime)
13
/// from a given user-supplied description of the desired log granularity.
14
306
pub(super) fn new_formatter(
15
306
    granularity: std::time::Duration,
16
306
) -> impl tracing_subscriber::fmt::time::FormatTime {
17
306
    LogPrecision::from_duration(granularity).timer()
18
306
}
19

            
20
/// Instructions for what degree of precision to use for our log times.
21
//
22
// (This is a separate type from `LogTimer` so that we can test our parsing
23
// and our implementation independently.)
24
#[derive(Clone, Debug)]
25
#[cfg_attr(test, derive(Copy, Eq, PartialEq))]
26
enum LogPrecision {
27
    /// Display up to this many significant digits when logging.
28
    ///
29
    /// System limitations will also limit the number of digits displayed.
30
    ///
31
    /// Must be in range 1..9.
32
    Subseconds(u8),
33
    /// Before logging, round the number of seconds down to the nearest
34
    /// multiple of this number within the current minute.
35
    ///
36
    /// Must be in range 1..59.
37
    Seconds(u8),
38
    /// Before logging, round the number of minutes down to the nearest multiple
39
    /// of this number within the current hour.
40
    ///
41
    /// Must be in range 1..59.
42
    Minutes(u8),
43

            
44
    /// Before logging, round to down to the nearest hour.
45
    Hours,
46
}
47

            
48
/// Compute the smallest n such that 10^n >= x.
49
///
50
/// Since the input is a u32, this will return a value in the range 0..10.
51
50
fn ilog10_roundup(x: u32) -> u8 {
52
50
    x.saturating_sub(1)
53
50
        .checked_ilog10()
54
71
        .map(|x| (x + 1) as u8)
55
50
        .unwrap_or(0)
56
50
}
57

            
58
/// Describe how to compute the current time.
59
#[derive(Clone, Debug)]
60
enum TimeRounder {
61
    /// Just take the current time; any transformation will be done by the
62
    /// formatter.
63
    Verbatim,
64
    /// Round the minutes within the hours down to the nearest multiple of
65
    /// this granularity.
66
    RoundMinutes(NonZeroU8),
67
    /// Round the seconds within the minute down to the nearest multiple of
68
    /// this granularity.
69
    RoundSeconds(NonZeroU8),
70
}
71

            
72
/// Actual type to implement log formatting.
73
struct LogTimer {
74
    /// Source that knows how to compute a time, rounded as necessary.
75
    rounder: TimeRounder,
76
    /// Formatter that knows how to format the time, discarding fields as
77
    /// necessary.
78
    formatter: format_description::OwnedFormatItem,
79
}
80

            
81
impl LogPrecision {
82
    /// Convert a `Duration` into a LogPrecision that rounds the time displayed
83
    /// in log messages to intervals _no more precise_ than the interval
84
    /// specified in Duration.
85
    ///
86
    /// (As an exception, we do not support granularities greater than 1 hour.
87
    /// If you specify a granularity greater than an hour, we just give you a
88
    /// one-hour granularity.)
89
346
    fn from_duration(dur: std::time::Duration) -> Self {
90
        // Round any fraction greater than 1 up to next second.
91
346
        let seconds = match (dur.as_secs(), dur.subsec_nanos()) {
92
18
            (0, _) => 0,
93
320
            (a, 0) => a,
94
8
            (a, _) => a + 1,
95
        };
96

            
97
        // Anything above one hour minus one minute will round to one hour.
98
346
        if seconds >= 3541 {
99
            // This is the lowest precision we have.
100
6
            LogPrecision::Hours
101
340
        } else if seconds >= 60 {
102
8
            let minutes = seconds.div_ceil(60);
103
8
            assert!((1..=59).contains(&minutes));
104
8
            LogPrecision::Minutes(minutes.try_into().expect("Math bug"))
105
332
        } else if seconds >= 1 {
106
314
            assert!((1..=59).contains(&seconds));
107
314
            LogPrecision::Seconds(seconds.try_into().expect("Math bug"))
108
        } else {
109
18
            let ilog10 = ilog10_roundup(dur.subsec_nanos());
110
18
            if ilog10 >= 9 {
111
2
                LogPrecision::Seconds(1)
112
            } else {
113
16
                LogPrecision::Subseconds(9 - ilog10)
114
            }
115
        }
116
346
    }
117

            
118
    /// Convert a LogPrecision (which specifies the precision we want) into a
119
    /// LogTimer (which can be used to format times in the log)
120
332
    fn timer(&self) -> LogTimer {
121
        use LogPrecision::*;
122
332
        let format_str = match self {
123
2
            Hours => "[year]-[month]-[day]T[hour repr:24]:00:00Z".to_string(),
124
8
            Minutes(_) => "[year]-[month]-[day]T[hour repr:24]:[minute]:00Z".to_string(),
125
314
            Seconds(_) => "[year]-[month]-[day]T[hour repr:24]:[minute]:[second]Z".to_string(),
126
8
            Subseconds(significant_digits) => {
127
8
                assert!(*significant_digits >= 1 && *significant_digits <= 9);
128
8
                format!(
129
8
                    "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:{}]Z",
130
                    significant_digits
131
                )
132
            }
133
        };
134
332
        let formatter = format_description::parse_owned::<2>(&format_str)
135
332
            .expect("Couldn't parse a built-in time format string");
136
332
        let rounder = match self {
137
320
            Hours | Minutes(1) | Seconds(1) | Subseconds(_) => TimeRounder::Verbatim,
138
6
            Minutes(granularity) => {
139
6
                TimeRounder::RoundMinutes((*granularity).try_into().expect("Math bug"))
140
            }
141
6
            Seconds(granularity) => {
142
6
                TimeRounder::RoundSeconds((*granularity).try_into().expect("Math bug"))
143
            }
144
        };
145

            
146
332
        LogTimer { rounder, formatter }
147
332
    }
148
}
149

            
150
/// An error that occurs while trying to format the time.
151
///
152
/// Internal.
153
#[derive(thiserror::Error, Debug)]
154
#[non_exhaustive]
155
enum TimeFmtError {
156
    /// The time crate wouldn't let us replace a field.
157
    ///
158
    /// This indicates that the value we were trying to use there was invalid,
159
    /// and so our math must have been wrong.
160
    #[error("Internal error while trying to round the time.")]
161
    Rounding(#[from] time::error::ComponentRange),
162

            
163
    /// The time crate wouldn't let us format a value.
164
    ///
165
    /// This indicates that our formatters were busted, and so we probably have
166
    /// a programming error.
167
    #[error("`time` couldn't format this time.")]
168
    TimeFmt(#[from] time::error::Format),
169
}
170

            
171
impl TimeRounder {
172
    /// Round `when` down according to this `TimeRounder`.
173
    ///
174
    /// Note that we round fields minimally: we don't round any fields that the
175
    /// associated formatter will not display.
176
26
    fn round(&self, when: time::OffsetDateTime) -> Result<time::OffsetDateTime, TimeFmtError> {
177
        // NOTE: This function really mustn't panic.  We try to log any panics
178
        // that we encounter, and if logging itself can panic, we're in a
179
        // potential heap of trouble.
180
        //
181
        // This danger is somewhat ameliorated by the behavior of the default
182
        // panic handler, which detects nested panics and aborts in response.
183
        // Thus, if we ever discard that handler, we need to be sure to
184
        // reimplement nested panic detection.
185
        //
186
        // Alternatively, we _could_ nest this functionality within
187
        // `catch_unwind`.  But I'm not sure that the overhead there would be
188
        // acceptable: Logging can be performance sensitive.
189

            
190
        use TimeRounder::*;
191
        /// Round `inp` down to the nearest multiple of `granularity`.
192
12
        fn round_down(inp: u8, granularity: NonZeroU8) -> u8 {
193
12
            inp - (inp % granularity)
194
12
        }
195

            
196
26
        Ok(match self {
197
14
            Verbatim => when,
198
6
            RoundMinutes(granularity) => {
199
6
                when.replace_minute(round_down(when.minute(), *granularity))?
200
            }
201
6
            RoundSeconds(granularity) => {
202
6
                when.replace_second(round_down(when.second(), *granularity))?
203
            }
204
        })
205
26
    }
206
}
207

            
208
impl LogTimer {
209
    /// Convert `when` to a string with appropriate rounding.
210
26
    fn time_to_string(&self, when: time::OffsetDateTime) -> Result<String, TimeFmtError> {
211
        // See NOTE above: This function mustn't panic.
212
26
        Ok(self.rounder.round(when)?.format(&self.formatter)?)
213
26
    }
214
}
215

            
216
impl tracing_subscriber::fmt::time::FormatTime for LogTimer {
217
    fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
218
        // See NOTE above: This function mustn't panic.
219
        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)]
225
mod test {
226
    // @@ begin test lint list maintained by maint/add_warning @@
227
    #![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
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
239

            
240
    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
                // Windows has a 100-nanosecond precision, see
322
                // https://learn.microsoft.com/en-us/windows/win32/sysinfo/about-time
323
                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
}