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}