better_tracing/fmt/time/
time_crate.rs

1use crate::fmt::{format::Writer, time::FormatTime, writer::WriteAdaptor};
2use std::fmt;
3use time::{
4    format_description::parse,
5    format_description::{well_known, FormatItem},
6    formatting::Formattable,
7    OffsetDateTime, UtcOffset,
8};
9
10/// Formats the current [local time] using a [formatter] from the [`time` crate].
11///
12/// To format the current [UTC time] instead, use the [`UtcTime`] type.
13///
14/// <div class="example-wrap" style="display:inline-block">
15/// <pre class="compile_fail" style="white-space:normal;font:inherit;">
16///     <strong>Warning</strong>: The <a href = "https://docs.rs/time/0.3/time/"><code>time</code>
17///     crate</a> must be compiled with <code>--cfg unsound_local_offset</code> in order to use
18///     local timestamps. When this cfg is not enabled, local timestamps cannot be recorded, and
19///     events will be logged without timestamps.
20///
21///    Alternatively, [`OffsetTime`] can log with a local offset if it is initialized early.
22///
23///    See the <a href="https://docs.rs/time/0.3.4/time/#feature-flags"><code>time</code>
24///    documentation</a> for more details.
25/// </pre></div>
26///
27/// [local time]: time::OffsetDateTime::now_local
28/// [UTC time]:     time::OffsetDateTime::now_utc
29/// [formatter]:    time::formatting::Formattable
30/// [`time` crate]: time
31#[derive(Clone, Debug)]
32#[cfg_attr(
33    docsrs,
34    doc(cfg(all(unsound_local_offset, feature = "time", feature = "local-time")))
35)]
36#[cfg(feature = "local-time")]
37pub struct LocalTime<F> {
38    format: F,
39}
40
41/// Formats the current [UTC time] using a [formatter] from the [`time` crate].
42///
43/// To format the current [local time] instead, use the [`LocalTime`] type.
44///
45/// [local time]: time::OffsetDateTime::now_local
46/// [UTC time]:     time::OffsetDateTime::now_utc
47/// [formatter]:    time::formatting::Formattable
48/// [`time` crate]: time
49#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
50#[derive(Clone, Debug)]
51pub struct UtcTime<F> {
52    format: F,
53}
54
55/// Formats the current time using a fixed offset and a [formatter] from the [`time` crate].
56///
57/// This is typically used as an alternative to [`LocalTime`]. `LocalTime` determines the offset
58/// every time it formats a message, which may be unsound or fail. With `OffsetTime`, the offset is
59/// determined once. This makes it possible to do so while the program is still single-threaded and
60/// handle any errors. However, this also means the offset cannot change while the program is
61/// running (the offset will not change across DST changes).
62///
63/// [formatter]: time::formatting::Formattable
64/// [`time` crate]: time
65#[derive(Clone, Debug)]
66#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
67pub struct OffsetTime<F> {
68    offset: time::UtcOffset,
69    format: F,
70}
71
72// === impl LocalTime ===
73
74#[cfg(feature = "local-time")]
75impl LocalTime<well_known::Rfc3339> {
76    /// Returns a formatter that formats the current [local time] in the
77    /// [RFC 3339] format (a subset of the [ISO 8601] timestamp format).
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use better_tracing::fmt::{self, time};
83    ///
84    /// let subscriber = better_tracing::fmt()
85    ///     .with_timer(time::LocalTime::rfc_3339());
86    /// # drop(subscriber);
87    /// ```
88    ///
89    /// [local time]: time::OffsetDateTime::now_local
90    /// [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339
91    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
92    pub fn rfc_3339() -> Self {
93        Self::new(well_known::Rfc3339)
94    }
95}
96
97#[cfg(feature = "local-time")]
98impl<F: Formattable> LocalTime<F> {
99    /// Returns a formatter that formats the current [local time] using the
100    /// [`time` crate] with the provided provided format. The format may be any
101    /// type that implements the [`Formattable`] trait.
102    ///
103    ///
104    /// <div class="example-wrap" style="display:inline-block">
105    /// <pre class="compile_fail" style="white-space:normal;font:inherit;">
106    ///     <strong>Warning</strong>: The <a href = "https://docs.rs/time/0.3/time/">
107    ///     <code>time</code> crate</a> must be compiled with <code>--cfg
108    ///     unsound_local_offset</code> in order to use local timestamps. When this
109    ///     cfg is not enabled, local timestamps cannot be recorded, and
110    ///     events will be logged without timestamps.
111    ///
112    ///    See the <a href="https://docs.rs/time/0.3.4/time/#feature-flags">
113    ///    <code>time</code> documentation</a> for more details.
114    /// </pre></div>
115    ///
116    /// Typically, the format will be a format description string, or one of the
117    /// `time` crate's [well-known formats].
118    ///
119    /// If the format description is statically known, then the
120    /// [`format_description!`] macro should be used. This is identical to the
121    /// [`time::format_description::parse`] method, but runs at compile-time,
122    /// throwing an error if the format description is invalid. If the desired format
123    /// is not known statically (e.g., a user is providing a format string), then the
124    /// [`time::format_description::parse`] method should be used. Note that this
125    /// method is fallible.
126    ///
127    /// See the [`time` book] for details on the format description syntax.
128    ///
129    /// # Examples
130    ///
131    /// Using the [`format_description!`] macro:
132    ///
133    /// ```
134    /// use better_tracing::fmt::{self, time::LocalTime};
135    /// use time::macros::format_description;
136    ///
137    /// let timer = LocalTime::new(format_description!("[hour]:[minute]:[second]"));
138    /// let subscriber = better_tracing::fmt()
139    ///     .with_timer(timer);
140    /// # drop(subscriber);
141    /// ```
142    ///
143    /// Using [`time::format_description::parse`]:
144    ///
145    /// ```
146    /// use better_tracing::fmt::{self, time::LocalTime};
147    ///
148    /// let time_format = time::format_description::parse("[hour]:[minute]:[second]")
149    ///     .expect("format string should be valid!");
150    /// let timer = LocalTime::new(time_format);
151    /// let subscriber = better_tracing::fmt()
152    ///     .with_timer(timer);
153    /// # drop(subscriber);
154    /// ```
155    ///
156    /// Using the [`format_description!`] macro requires enabling the `time`
157    /// crate's "macros" feature flag.
158    ///
159    /// Using a [well-known format][well-known formats] (this is equivalent to
160    /// [`LocalTime::rfc_3339`]):
161    ///
162    /// ```
163    /// use better_tracing::fmt::{self, time::LocalTime};
164    ///
165    /// let timer = LocalTime::new(time::format_description::well_known::Rfc3339);
166    /// let subscriber = better_tracing::fmt()
167    ///     .with_timer(timer);
168    /// # drop(subscriber);
169    /// ```
170    ///
171    /// [local time]: time::OffsetDateTime::now_local()
172    /// [`time` crate]: time
173    /// [`Formattable`]: time::formatting::Formattable
174    /// [well-known formats]: time::format_description::well_known
175    /// [`format_description!`]: https://docs.rs/time/0.3/time/macros/macro.format_description.html
176    /// [`time::format_description::parse`]: time::format_description::parse()
177    /// [`time` book]: https://time-rs.github.io/book/api/format-description.html
178    pub fn new(format: F) -> Self {
179        Self { format }
180    }
181}
182
183#[cfg(feature = "local-time")]
184impl<F> FormatTime for LocalTime<F>
185where
186    F: Formattable,
187{
188    fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
189        let now = OffsetDateTime::now_local().map_err(|_| fmt::Error)?;
190        format_datetime(now, w, &self.format)
191    }
192}
193
194#[cfg(feature = "local-time")]
195impl<F> Default for LocalTime<F>
196where
197    F: Formattable + Default,
198{
199    fn default() -> Self {
200        Self::new(F::default())
201    }
202}
203
204// === impl UtcTime ===
205
206impl UtcTime<well_known::Rfc3339> {
207    /// Returns a formatter that formats the current [UTC time] in the
208    /// [RFC 3339] format, which is a subset of the [ISO 8601] timestamp format.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use better_tracing::fmt::{self, time};
214    ///
215    /// let subscriber = better_tracing::fmt()
216    ///     .with_timer(time::UtcTime::rfc_3339());
217    /// # drop(subscriber);
218    /// ```
219    ///
220    /// [local time]: time::OffsetDateTime::now_utc
221    /// [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339
222    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
223    pub fn rfc_3339() -> Self {
224        Self::new(well_known::Rfc3339)
225    }
226}
227
228#[cfg(feature = "local-time")]
229impl LocalTime<Vec<FormatItem<'static>>> {
230    /// Time-of-day with whole seconds, no suffix: HH:MM:SS
231    pub fn time_only_secs() -> Self {
232        let fmt = parse("[hour]:[minute]:[second]").expect("static format string must be valid");
233        Self::new(fmt)
234    }
235
236    /// Time-of-day with milliseconds, no suffix: HH:MM:SS.mmm
237    pub fn time_only_millis() -> Self {
238        let fmt = parse("[hour]:[minute]:[second].[subsecond digits:3]")
239            .expect("static format string must be valid");
240        Self::new(fmt)
241    }
242
243    /// Time-of-day with microseconds, no suffix: HH:MM:SS.uuuuuu
244    pub fn time_only_micros() -> Self {
245        let fmt = parse("[hour]:[minute]:[second].[subsecond digits:6]")
246            .expect("static format string must be valid");
247        Self::new(fmt)
248    }
249}
250
251// Convenience helpers using parsed format descriptions for varying precision and time-only.
252impl UtcTime<Vec<FormatItem<'static>>> {
253    /// RFC3339 with no fractional seconds and 'Z'.
254    pub fn rfc3339_seconds() -> Self {
255        let fmt = parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z")
256            .expect("static format string must be valid");
257        Self::new(fmt)
258    }
259
260    /// RFC3339 with 3 fractional digits (milliseconds) and 'Z'.
261    pub fn rfc3339_millis() -> Self {
262        let fmt = parse("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z")
263            .expect("static format string must be valid");
264        Self::new(fmt)
265    }
266
267    /// RFC3339 with 9 fractional digits (nanoseconds) and 'Z'.
268    pub fn rfc3339_nanos() -> Self {
269        let fmt = parse("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:9]Z")
270            .expect("static format string must be valid");
271        Self::new(fmt)
272    }
273
274    /// Time-of-day with whole seconds, no suffix: HH:MM:SS
275    pub fn time_only_secs() -> Self {
276        let fmt = parse("[hour]:[minute]:[second]").expect("static format string must be valid");
277        Self::new(fmt)
278    }
279
280    /// Time-of-day with milliseconds, no suffix: HH:MM:SS.mmm
281    pub fn time_only_millis() -> Self {
282        let fmt = parse("[hour]:[minute]:[second].[subsecond digits:3]")
283            .expect("static format string must be valid");
284        Self::new(fmt)
285    }
286
287    /// Time-of-day with microseconds, no suffix: HH:MM:SS.uuuuuu
288    pub fn time_only_micros() -> Self {
289        let fmt = parse("[hour]:[minute]:[second].[subsecond digits:6]")
290            .expect("static format string must be valid");
291        Self::new(fmt)
292    }
293}
294
295impl<F: Formattable> UtcTime<F> {
296    /// Returns a formatter that formats the current [UTC time] using the
297    /// [`time` crate], with the provided provided format. The format may be any
298    /// type that implements the [`Formattable`] trait.
299    ///
300    /// Typically, the format will be a format description string, or one of the
301    /// `time` crate's [well-known formats].
302    ///
303    /// If the format description is statically known, then the
304    /// [`format_description!`] macro should be used. This is identical to the
305    /// [`time::format_description::parse`] method, but runs at compile-time,
306    /// failing  an error if the format description is invalid. If the desired format
307    /// is not known statically (e.g., a user is providing a format string), then the
308    /// [`time::format_description::parse`] method should be used. Note that this
309    /// method is fallible.
310    ///
311    /// See the [`time` book] for details on the format description syntax.
312    ///
313    /// # Examples
314    ///
315    /// Using the [`format_description!`] macro:
316    ///
317    /// ```
318    /// use better_tracing::fmt::{self, time::UtcTime};
319    /// use time::macros::format_description;
320    ///
321    /// let timer = UtcTime::new(format_description!("[hour]:[minute]:[second]"));
322    /// let subscriber = better_tracing::fmt()
323    ///     .with_timer(timer);
324    /// # drop(subscriber);
325    /// ```
326    ///
327    /// Using the [`format_description!`] macro requires enabling the `time`
328    /// crate's "macros" feature flag.
329    ///
330    /// Using [`time::format_description::parse`]:
331    ///
332    /// ```
333    /// use better_tracing::fmt::{self, time::UtcTime};
334    ///
335    /// let time_format = time::format_description::parse("[hour]:[minute]:[second]")
336    ///     .expect("format string should be valid!");
337    /// let timer = UtcTime::new(time_format);
338    /// let subscriber = better_tracing::fmt()
339    ///     .with_timer(timer);
340    /// # drop(subscriber);
341    /// ```
342    ///
343    /// Using a [well-known format][well-known formats] (this is equivalent to
344    /// [`UtcTime::rfc_3339`]):
345    ///
346    /// ```
347    /// use better_tracing::fmt::{self, time::UtcTime};
348    ///
349    /// let timer = UtcTime::new(time::format_description::well_known::Rfc3339);
350    /// let subscriber = better_tracing::fmt()
351    ///     .with_timer(timer);
352    /// # drop(subscriber);
353    /// ```
354    ///
355    /// [UTC time]: time::OffsetDateTime::now_utc()
356    /// [`time` crate]: time
357    /// [`Formattable`]: time::formatting::Formattable
358    /// [well-known formats]: time::format_description::well_known
359    /// [`format_description!`]: https://docs.rs/time/0.3/time/macros/macro.format_description.html
360    /// [`time::format_description::parse`]: time::format_description::parse
361    /// [`time` book]: https://time-rs.github.io/book/api/format-description.html
362    pub fn new(format: F) -> Self {
363        Self { format }
364    }
365}
366
367impl<F> FormatTime for UtcTime<F>
368where
369    F: Formattable,
370{
371    fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
372        format_datetime(OffsetDateTime::now_utc(), w, &self.format)
373    }
374}
375
376impl<F> Default for UtcTime<F>
377where
378    F: Formattable + Default,
379{
380    fn default() -> Self {
381        Self::new(F::default())
382    }
383}
384
385// === impl OffsetTime ===
386
387#[cfg(feature = "local-time")]
388impl OffsetTime<well_known::Rfc3339> {
389    /// Returns a formatter that formats the current time using the [local time offset] in the [RFC
390    /// 3339] format (a subset of the [ISO 8601] timestamp format).
391    ///
392    /// Returns an error if the local time offset cannot be determined. This typically occurs in
393    /// multithreaded programs. To avoid this problem, initialize `OffsetTime` before forking
394    /// threads. When using Tokio, this means initializing `OffsetTime` before the Tokio runtime.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use better_tracing::fmt::{self, time};
400    ///
401    /// let subscriber = better_tracing::fmt()
402    ///     .with_timer(time::OffsetTime::local_rfc_3339().expect("could not get local offset!"));
403    /// # drop(subscriber);
404    /// ```
405    ///
406    /// Using `OffsetTime` with Tokio:
407    ///
408    /// ```
409    /// use better_tracing::fmt::time::OffsetTime;
410    ///
411    /// #[tokio::main]
412    /// async fn run() {
413    ///     tracing::info!("runtime initialized");
414    ///
415    ///     // At this point the Tokio runtime is initialized, and we can use both Tokio and Tracing
416    ///     // normally.
417    /// }
418    ///
419    /// fn main() {
420    ///     // Because we need to get the local offset before Tokio spawns any threads, our `main`
421    ///     // function cannot use `tokio::main`.
422    ///     better_tracing::fmt()
423    ///         .with_timer(OffsetTime::local_rfc_3339().expect("could not get local time offset"))
424    ///         .init();
425    ///
426    ///     // Even though `run` is written as an `async fn`, because we used `tokio::main` on it
427    ///     // we can call it as a synchronous function.
428    ///     run();
429    /// }
430    /// ```
431    ///
432    /// [local time offset]: time::UtcOffset::current_local_offset
433    /// [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339
434    /// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
435    pub fn local_rfc_3339() -> Result<Self, time::error::IndeterminateOffset> {
436        Ok(Self::new(
437            UtcOffset::current_local_offset()?,
438            well_known::Rfc3339,
439        ))
440    }
441}
442
443impl<F: time::formatting::Formattable> OffsetTime<F> {
444    /// Returns a formatter that formats the current time using the [`time` crate] with the provided
445    /// provided format and [timezone offset]. The format may be any type that implements the
446    /// [`Formattable`] trait.
447    ///
448    ///
449    /// Typically, the offset will be the [local offset], and format will be a format description
450    /// string, or one of the `time` crate's [well-known formats].
451    ///
452    /// If the format description is statically known, then the
453    /// [`format_description!`] macro should be used. This is identical to the
454    /// [`time::format_description::parse`] method, but runs at compile-time,
455    /// throwing an error if the format description is invalid. If the desired format
456    /// is not known statically (e.g., a user is providing a format string), then the
457    /// [`time::format_description::parse`] method should be used. Note that this
458    /// method is fallible.
459    ///
460    /// See the [`time` book] for details on the format description syntax.
461    ///
462    /// # Examples
463    ///
464    /// Using the [`format_description!`] macro:
465    ///
466    /// ```
467    /// use better_tracing::fmt::{self, time::OffsetTime};
468    /// use time::macros::format_description;
469    /// use time::UtcOffset;
470    ///
471    /// let offset = UtcOffset::current_local_offset().expect("should get local offset!");
472    /// let timer = OffsetTime::new(offset, format_description!("[hour]:[minute]:[second]"));
473    /// let subscriber = better_tracing::fmt()
474    ///     .with_timer(timer);
475    /// # drop(subscriber);
476    /// ```
477    ///
478    /// Using [`time::format_description::parse`]:
479    ///
480    /// ```
481    /// use better_tracing::fmt::{self, time::OffsetTime};
482    /// use time::UtcOffset;
483    ///
484    /// let offset = UtcOffset::current_local_offset().expect("should get local offset!");
485    /// let time_format = time::format_description::parse("[hour]:[minute]:[second]")
486    ///     .expect("format string should be valid!");
487    /// let timer = OffsetTime::new(offset, time_format);
488    /// let subscriber = better_tracing::fmt()
489    ///     .with_timer(timer);
490    /// # drop(subscriber);
491    /// ```
492    ///
493    /// Using the [`format_description!`] macro requires enabling the `time`
494    /// crate's "macros" feature flag.
495    ///
496    /// Using a [well-known format][well-known formats] (this is equivalent to
497    /// [`OffsetTime::local_rfc_3339`]):
498    ///
499    /// ```
500    /// use better_tracing::fmt::{self, time::OffsetTime};
501    /// use time::UtcOffset;
502    ///
503    /// let offset = UtcOffset::current_local_offset().expect("should get local offset!");
504    /// let timer = OffsetTime::new(offset, time::format_description::well_known::Rfc3339);
505    /// let subscriber = better_tracing::fmt()
506    ///     .with_timer(timer);
507    /// # drop(subscriber);
508    /// ```
509    ///
510    /// [`time` crate]: time
511    /// [timezone offset]: time::UtcOffset
512    /// [`Formattable`]: time::formatting::Formattable
513    /// [local offset]: time::UtcOffset::current_local_offset()
514    /// [well-known formats]: time::format_description::well_known
515    /// [`format_description!`]: https://docs.rs/time/0.3/time/macros/macro.format_description.html
516    /// [`time::format_description::parse`]: time::format_description::parse
517    /// [`time` book]: https://time-rs.github.io/book/api/format-description.html
518    pub fn new(offset: time::UtcOffset, format: F) -> Self {
519        Self { offset, format }
520    }
521}
522
523impl<F> FormatTime for OffsetTime<F>
524where
525    F: time::formatting::Formattable,
526{
527    fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
528        let now = OffsetDateTime::now_utc().to_offset(self.offset);
529        format_datetime(now, w, &self.format)
530    }
531}
532
533fn format_datetime(
534    now: OffsetDateTime,
535    into: &mut Writer<'_>,
536    fmt: &impl Formattable,
537) -> fmt::Result {
538    let mut into = WriteAdaptor::new(into);
539    now.format_into(&mut into, fmt)
540        .map_err(|_| fmt::Error)
541        .map(|_| ())
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::fmt::format::Writer;
548
549    #[test]
550    fn utc_time_rfc3339_seconds_formats() {
551        let timer = UtcTime::rfc3339_seconds();
552        let mut s = String::new();
553        let mut w = Writer::new(&mut s);
554        timer.format_time(&mut w).unwrap();
555        // Expect pattern: YYYY-MM-DDTHH:MM:SSZ (no frac)
556        assert!(s.ends_with('Z'));
557        assert_eq!(s.len(), "YYYY-MM-DDTHH:MM:SSZ".len());
558    }
559
560    #[test]
561    fn utc_time_rfc3339_millis_formats() {
562        let timer = UtcTime::rfc3339_millis();
563        let mut s = String::new();
564        let mut w = Writer::new(&mut s);
565        timer.format_time(&mut w).unwrap();
566        // Rough shape contains .mmmZ
567        let (pre, suf) = s.split_at(s.len() - 4);
568        assert!(pre.contains('.'));
569        assert!(suf.ends_with('Z'));
570    }
571
572    #[test]
573    fn utc_time_time_only_sec_formats() {
574        let timer = UtcTime::time_only_secs();
575        let mut s = String::new();
576        let mut w = Writer::new(&mut s);
577        timer.format_time(&mut w).unwrap();
578        // Expect HH:MM:SS
579        assert_eq!(s.chars().filter(|&c| c == ':').count(), 2);
580        assert_eq!(s.len(), 8);
581    }
582
583    #[cfg(feature = "local-time")]
584    #[test]
585    fn local_time_time_only_ms_formats() {
586        let timer = LocalTime::time_only_millis();
587        let mut s = String::new();
588        let mut w = Writer::new(&mut s);
589        timer.format_time(&mut w).unwrap();
590        // Expect HH:MM:SS.mmm
591        assert!(s.len() >= 12);
592        assert!(s.contains('.'));
593    }
594}