spdlog/formatter/
full_formatter.rs

1//! Provides a full info formatter.
2
3use std::fmt::{self, Write};
4
5use crate::{
6    formatter::{fmt_with_time, Formatter, FormatterContext, TimeDate},
7    Error, Record, StringBuf, __EOL,
8};
9
10#[rustfmt::skip]
11/// Full information logs formatter.
12///
13/// It is the default formatter for most sinks. By default, all fields are
14/// enabled. Use [`FullFormatter::builder`] to opt-out of fields as needed, and
15/// corresponding information for disabled fields will be removed from the
16/// formatted output.
17///
18/// ## Examples
19/// 
20/// - If all fields are enabled, log messages formatted by it look like:
21///
22///    - Default:
23///
24///      <pre>
25///      [2022-11-02 09:23:12.263] [<font color="#0DBC79">info</font>] hello, world! { key1=value1 key2=value2 }
26///      </pre>
27///
28///    - If the logger has a name:
29///
30///      <pre>
31///      [2022-11-02 09:23:12.263] [logger-name] [<font color="#0DBC79">info</font>] hello, world! { key1=value1 key2=value2 }
32///      </pre>
33/// 
34///    - If crate feature `source-location` is enabled:
35///
36///      <pre>
37///      [2022-11-02 09:23:12.263] [logger-name] [<font color="#0DBC79">info</font>] [mod::path, src/main.rs:4] hello, world! { key1=value1 key2=value2 }
38///      </pre>
39/// 
40/// - Disabling some fields will remove corresponding information:
41///
42/// ```
43/// use spdlog::formatter::FullFormatter;
44/// # use spdlog::info;
45#[doc = include_str!(concat!(env!("OUT_DIR"), "/test_utils/common_for_doc_test.rs"))]
46/// #
47///
48/// let formatter = FullFormatter::builder()
49///     .time(false)
50///     .source_location(false)
51///     .build();
52/// // ... Setting up sinks with the formatter
53/// # let (doctest, sink) = test_utils::echo_logger_from_formatter(formatter, None);
54/// info!(logger: doctest, "Interesting log message");
55/// # assert_eq!(
56/// #     sink.clone_string().replace("\r", ""),
57/// /* Output */ "[info] Interesting log message\n"
58/// # );
59///
60/// let formatter = FullFormatter::builder()
61///     .time(false)
62///     .level(false)
63///     .source_location(false)
64///     .build();
65/// // ... Setting up sinks with the formatter
66/// # let (doctest, sink) = test_utils::echo_logger_from_formatter(formatter, None);
67/// info!(logger: doctest, "Interesting log message");
68/// # assert_eq!(
69/// #     sink.clone_string().replace("\r", ""),
70/// /* Output */ "Interesting log message\n"
71/// # );
72/// ```
73#[derive(Clone)]
74pub struct FullFormatter {
75    options: FormattingOptions,
76}
77
78impl FullFormatter {
79    /// Constructs a `FullFormatter`.
80    ///
81    /// See [`FullFormatter::builder`] for the default parameters will be used.
82    #[must_use]
83    pub fn new() -> FullFormatter {
84        Self::builder().build()
85    }
86
87    /// Gets a builder of `FullFormatter` with default parameters:
88    ///
89    /// | Parameter         | Default Value |
90    /// |-------------------|---------------|
91    /// | [time]            | `true`        |
92    /// | [logger_name]     | `true`        |
93    /// | [level]           | `true`        |
94    /// | [source_location] | `true`        |
95    /// | [kv]              | `true`        |
96    /// | [eol]             | `true`        |
97    ///
98    /// [time]: FullFormatterBuilder::time
99    /// [logger_name]: FullFormatterBuilder::logger_name
100    /// [level]: FullFormatterBuilder::level
101    /// [source_location]: FullFormatterBuilder::source_location
102    /// [kv]: FullFormatterBuilder::kv
103    /// [eol]: FullFormatterBuilder::eol
104    #[must_use]
105    pub fn builder() -> FullFormatterBuilder {
106        FullFormatterBuilder(FormattingOptions {
107            time: true,
108            logger_name: true,
109            level: true,
110            source_location: true,
111            kv: true,
112            eol: true,
113        })
114    }
115
116    fn format_impl(
117        &self,
118        record: &Record,
119        dest: &mut StringBuf,
120        ctx: &mut FormatterContext,
121    ) -> Result<(), fmt::Error> {
122        #[cfg(not(feature = "flexible-string"))]
123        dest.reserve(crate::string_buf::RESERVE_SIZE);
124
125        let mut spacer = AutoSpacer::new();
126
127        spacer.write_if(self.options.time, dest, |dest| {
128            fmt_with_time(
129                ctx,
130                record,
131                |mut time: TimeDate| -> Result<(), fmt::Error> {
132                    dest.write_str("[")?;
133                    dest.write_str(time.full_second_str())?;
134                    dest.write_str(".")?;
135                    write!(dest, "{:03}", time.millisecond())?;
136                    dest.write_str("]")?;
137                    Ok(())
138                },
139            )
140        })?;
141        spacer.write_if_opt(
142            self.options.logger_name,
143            record.logger_name(),
144            dest,
145            |dest, logger_name| {
146                dest.write_str("[")?;
147                dest.write_str(logger_name)?;
148                dest.write_str("]")
149            },
150        )?;
151        let mut style_range = None;
152        spacer.write_if(self.options.level, dest, |dest| {
153            dest.write_str("[")?;
154            let style_range_begin = dest.len();
155            dest.write_str(record.level().as_str())?;
156            let style_range_end = dest.len();
157            dest.write_str("]")?;
158            style_range = Some(style_range_begin..style_range_end);
159            Ok(())
160        })?;
161        spacer.write_if_opt(
162            self.options.source_location,
163            record.source_location(),
164            dest,
165            |dest, srcloc| {
166                dest.write_str("[")?;
167                dest.write_str(srcloc.module_path())?;
168                dest.write_str(", ")?;
169                dest.write_str(srcloc.file())?;
170                dest.write_str(":")?;
171                write!(dest, "{}", srcloc.line())?;
172                dest.write_str("]")
173            },
174        )?;
175        spacer.write_always(dest, |dest| dest.write_str(record.payload()))?;
176
177        let key_values = record.key_values();
178        spacer.write_if(self.options.kv && !key_values.is_empty(), dest, |dest| {
179            dest.write_str("{ ")?;
180            key_values.write_to(dest, false)?;
181            dest.write_str(" }")
182        })?;
183
184        if self.options.eol {
185            dest.write_str(__EOL)?;
186        }
187
188        ctx.set_style_range(style_range);
189        Ok(())
190    }
191}
192
193impl Formatter for FullFormatter {
194    fn format(
195        &self,
196        record: &Record,
197        dest: &mut StringBuf,
198        ctx: &mut FormatterContext,
199    ) -> crate::Result<()> {
200        self.format_impl(record, dest, ctx)
201            .map_err(Error::FormatRecord)
202    }
203}
204
205impl Default for FullFormatter {
206    fn default() -> FullFormatter {
207        FullFormatter::new()
208    }
209}
210
211#[allow(missing_docs)]
212pub struct FullFormatterBuilder(FormattingOptions);
213
214impl FullFormatterBuilder {
215    /// Specify whether to enable time field.
216    ///
217    /// Example of this field: `[2022-11-02 09:23:12.263]`
218    #[must_use]
219    pub fn time(&mut self, value: bool) -> &mut Self {
220        self.0.time = value;
221        self
222    }
223
224    /// Specify whether to enable logger name field.
225    ///
226    /// Example of this field: `[logger-name]`
227    #[must_use]
228    pub fn logger_name(&mut self, value: bool) -> &mut Self {
229        self.0.logger_name = value;
230        self
231    }
232
233    /// Specify whether to enable level field.
234    ///
235    /// Note that disabling this field will also remove the style from the
236    /// formatted result.
237    ///
238    /// Example of this field: <code>[<font color="#0DBC79">info</font>]</code>
239    #[must_use]
240    pub fn level(&mut self, value: bool) -> &mut Self {
241        self.0.level = value;
242        self
243    }
244
245    /// Specify whether to enable source location field.
246    ///
247    /// Example of this field: `[mod::path, src/main.rs:4]`
248    #[must_use]
249    pub fn source_location(&mut self, value: bool) -> &mut Self {
250        self.0.source_location = value;
251        self
252    }
253
254    /// Specify whether to enable kv field.
255    ///
256    /// Example of this field: `{ key1=value1 key2=value2 }`
257    #[must_use]
258    pub fn kv(&mut self, value: bool) -> &mut Self {
259        self.0.kv = value;
260        self
261    }
262
263    /// Specify whether to enable eol field.
264    ///
265    /// Example of this field: `\n` or `\r\n` on Windows.
266    #[must_use]
267    pub fn eol(&mut self, value: bool) -> &mut Self {
268        self.0.eol = value;
269        self
270    }
271
272    /// Builds a `FullFormatter`.
273    #[must_use]
274    pub fn build(&mut self) -> FullFormatter {
275        FullFormatter {
276            options: self.0.clone(),
277        }
278    }
279}
280
281#[derive(Clone)]
282struct FormattingOptions {
283    time: bool,
284    logger_name: bool,
285    level: bool,
286    source_location: bool,
287    kv: bool,
288    eol: bool,
289}
290
291struct AutoSpacer(bool);
292
293impl AutoSpacer {
294    fn new() -> Self {
295        Self(false)
296    }
297
298    fn write_always(
299        &mut self,
300        dest: &mut StringBuf,
301        f: impl FnOnce(&mut StringBuf) -> fmt::Result,
302    ) -> fmt::Result {
303        if self.0 {
304            dest.write_str(" ")?;
305        } else {
306            self.0 = true;
307        }
308        f(dest)?;
309        Ok(())
310    }
311
312    fn write_if(
313        &mut self,
314        conf: bool,
315        dest: &mut StringBuf,
316        f: impl FnOnce(&mut StringBuf) -> fmt::Result,
317    ) -> fmt::Result {
318        if conf {
319            if self.0 {
320                dest.write_str(" ")?;
321            } else {
322                self.0 = true;
323            }
324            f(dest)?;
325        }
326        Ok(())
327    }
328
329    fn write_if_opt<O>(
330        &mut self,
331        conf: bool,
332        option: Option<O>,
333        dest: &mut StringBuf,
334        f: impl FnOnce(&mut StringBuf, O) -> fmt::Result,
335    ) -> fmt::Result {
336        if conf {
337            if let Some(option) = option {
338                if self.0 {
339                    dest.write_str(" ")?;
340                } else {
341                    self.0 = true;
342                }
343                f(dest, option)?;
344            }
345        }
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use chrono::prelude::*;
353
354    use super::*;
355    use crate::{kv, Level, RecordOwned, __EOL};
356
357    fn record() -> RecordOwned {
358        let kvs = [
359            (kv::Key::__from_static_str("k1"), kv::Value::from(114)),
360            (kv::Key::__from_static_str("k2"), kv::Value::from("514")),
361        ];
362        Record::new(Level::Warn, "test log content", None, Some("logger"), &kvs).to_owned()
363    }
364
365    #[test]
366    fn format() {
367        let record = record();
368        let record = record.as_ref();
369        let mut buf = StringBuf::new();
370        let mut ctx = FormatterContext::new();
371        FullFormatter::new()
372            .format(&record, &mut buf, &mut ctx)
373            .unwrap();
374
375        let local_time: DateTime<Local> = record.time().into();
376        assert_eq!(
377            format!(
378                "[{}] [logger] [warn] test log content {{ k1=114 k2=514 }}{}",
379                local_time.format("%Y-%m-%d %H:%M:%S.%3f"),
380                __EOL
381            ),
382            buf
383        );
384        assert_eq!(Some(36..40), ctx.style_range());
385    }
386
387    #[test]
388    fn no_time() {
389        let record = record();
390        let mut buf = StringBuf::new();
391        let mut ctx = FormatterContext::new();
392        FullFormatter::builder()
393            .time(false)
394            .build()
395            .format(&record.as_ref(), &mut buf, &mut ctx)
396            .unwrap();
397
398        assert_eq!(
399            buf,
400            format!("[logger] [warn] test log content {{ k1=114 k2=514 }}{__EOL}")
401        );
402        assert_eq!(ctx.style_range(), Some(10..14));
403    }
404
405    #[test]
406    fn no_time_logger_name() {
407        let record = record();
408        let mut buf = StringBuf::new();
409        let mut ctx = FormatterContext::new();
410        FullFormatter::builder()
411            .time(false)
412            .logger_name(false)
413            .build()
414            .format(&record.as_ref(), &mut buf, &mut ctx)
415            .unwrap();
416
417        assert_eq!(
418            buf,
419            format!("[warn] test log content {{ k1=114 k2=514 }}{__EOL}")
420        );
421        assert_eq!(ctx.style_range(), Some(1..5));
422    }
423
424    #[test]
425    fn no_time_logger_name_level() {
426        let record = record();
427        let mut buf = StringBuf::new();
428        let mut ctx = FormatterContext::new();
429        FullFormatter::builder()
430            .time(false)
431            .logger_name(false)
432            .level(false)
433            .build()
434            .format(&record.as_ref(), &mut buf, &mut ctx)
435            .unwrap();
436
437        assert_eq!(buf, format!("test log content {{ k1=114 k2=514 }}{__EOL}"));
438        assert!(ctx.style_range().is_none());
439    }
440
441    #[test]
442    fn no_time_logger_name_level_kv() {
443        let record = record();
444        let mut buf = StringBuf::new();
445        let mut ctx = FormatterContext::new();
446        FullFormatter::builder()
447            .time(false)
448            .logger_name(false)
449            .level(false)
450            .kv(false)
451            .build()
452            .format(&record.as_ref(), &mut buf, &mut ctx)
453            .unwrap();
454
455        assert_eq!(buf, format!("test log content{__EOL}"));
456        assert!(ctx.style_range().is_none());
457    }
458
459    #[test]
460    fn no_time_eol() {
461        let record = record();
462        let mut buf = StringBuf::new();
463        let mut ctx = FormatterContext::new();
464        FullFormatter::builder()
465            .time(false)
466            .eol(false)
467            .build()
468            .format(&record.as_ref(), &mut buf, &mut ctx)
469            .unwrap();
470
471        assert_eq!(buf, "[logger] [warn] test log content { k1=114 k2=514 }");
472        assert_eq!(ctx.style_range(), Some(10..14));
473    }
474}