strut_tracing/
config.rs

1use crate::{FormatFlavor, Verbosity};
2use serde::de::{MapAccess, Visitor};
3use serde::{Deserialize, Deserializer};
4use std::collections::BTreeMap;
5use std::fmt::Formatter;
6use strut_factory::impl_deserialize_field;
7
8pub mod flavor;
9pub mod verbosity;
10
11/// Represents the application-level configuration section that covers everything
12/// related to pre-configuring the [formatted layer](tracing_subscriber::fmt::Layer)
13/// provided by the `tracing` crate. In essence, this is the application
14/// **logging** configuration.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct TracingConfig {
17    verbosity: Verbosity,
18    flavor: FormatFlavor,
19    color: bool,
20    show_timestamp: bool,
21    show_target: bool,
22    show_file: bool,
23    show_line_number: bool,
24    show_level: bool,
25    show_thread_id: bool,
26    show_thread_name: bool,
27    #[cfg(feature = "json")]
28    flatten_json: bool,
29    targets: BTreeMap<String, Verbosity>,
30}
31
32impl TracingConfig {
33    /// Merges an extra per-target [`Verbosity`] level into this config.
34    pub fn with_target(
35        mut self,
36        target: impl Into<String>,
37        verbosity: impl Into<Verbosity>,
38    ) -> Self {
39        self.targets.insert(target.into(), verbosity.into());
40
41        self
42    }
43
44    /// Merges extra per-target [`Verbosity`] levels into this config.
45    pub fn with_targets<T, L>(mut self, targets: impl IntoIterator<Item = (T, L)>) -> Self
46    where
47        T: Into<String>,
48        L: Into<Verbosity>,
49    {
50        for (target, verbosity) in targets.into_iter() {
51            self.targets.insert(target.into(), verbosity.into());
52        }
53
54        self
55    }
56}
57
58impl TracingConfig {
59    /// Reports the root [verbosity level](Verbosity) for this logging
60    /// configuration.
61    pub fn verbosity(&self) -> Verbosity {
62        self.verbosity
63    }
64
65    /// Reports the [formatting flavor](FormatFlavor) for this logging
66    /// configuration.
67    pub fn flavor(&self) -> FormatFlavor {
68        self.flavor
69    }
70
71    /// Reports whether this logging configuration enables
72    /// [colored](tracing_subscriber::fmt::Layer::with_ansi) output.
73    pub fn color(&self) -> bool {
74        self.color
75    }
76
77    /// Reports whether this logging configuration includes the
78    /// [timestamp](tracing_subscriber::fmt::Layer::without_time) in the output.
79    pub fn show_timestamp(&self) -> bool {
80        self.show_timestamp
81    }
82
83    /// Reports whether this logging configuration includes the
84    /// [target](tracing_subscriber::fmt::Layer::with_target) in the output.
85    pub fn show_target(&self) -> bool {
86        self.show_target
87    }
88
89    /// Reports whether this logging configuration includes the
90    /// [file](tracing_subscriber::fmt::Layer::with_file) in the output.
91    pub fn show_file(&self) -> bool {
92        self.show_file
93    }
94
95    /// Reports whether this logging configuration includes the
96    /// [line number](tracing_subscriber::fmt::Layer::with_line_number) in the
97    /// output.
98    pub fn show_line_number(&self) -> bool {
99        self.show_line_number
100    }
101
102    /// Reports whether this logging configuration includes the
103    /// [level](tracing_subscriber::fmt::Layer::with_level) in the output.
104    pub fn show_level(&self) -> bool {
105        self.show_level
106    }
107
108    /// Reports whether this logging configuration includes the
109    /// [thread ID](tracing_subscriber::fmt::Layer::with_thread_ids) in the
110    /// output.
111    pub fn show_thread_id(&self) -> bool {
112        self.show_thread_id
113    }
114
115    /// Reports whether this logging configuration includes the
116    /// [thread name](tracing_subscriber::fmt::Layer::with_thread_names) in the
117    /// output.
118    pub fn show_thread_name(&self) -> bool {
119        self.show_thread_name
120    }
121
122    /// Reports whether this logging configuration flattens the JSON output.
123    #[cfg(feature = "json")]
124    pub fn flatten_json(&self) -> bool {
125        self.flatten_json
126    }
127
128    /// Reports the
129    /// [customized](tracing_subscriber::filter::targets::Targets::with_targets)
130    /// per-[target](tracing_subscriber::filter::targets::Targets) verbosity for
131    /// this logging configuration.
132    pub fn targets(&self) -> &BTreeMap<String, Verbosity> {
133        &self.targets
134    }
135}
136
137impl Default for TracingConfig {
138    fn default() -> Self {
139        Self {
140            verbosity: Verbosity::default(),
141            flavor: FormatFlavor::default(),
142            color: Self::default_color(),
143            show_timestamp: Self::default_show_timestamp(),
144            show_target: Self::default_show_target(),
145            show_file: Self::default_show_file(),
146            show_line_number: Self::default_show_line_number(),
147            show_level: Self::default_show_level(),
148            show_thread_id: Self::default_show_thread_id(),
149            show_thread_name: Self::default_show_thread_name(),
150            #[cfg(feature = "json")]
151            flatten_json: Self::default_flatten_json(),
152            targets: BTreeMap::default(),
153        }
154    }
155}
156
157impl TracingConfig {
158    fn default_color() -> bool {
159        true
160    }
161
162    fn default_show_timestamp() -> bool {
163        true
164    }
165
166    fn default_show_target() -> bool {
167        true
168    }
169
170    fn default_show_file() -> bool {
171        false
172    }
173
174    fn default_show_line_number() -> bool {
175        false
176    }
177
178    fn default_show_level() -> bool {
179        true
180    }
181
182    fn default_show_thread_id() -> bool {
183        true
184    }
185
186    fn default_show_thread_name() -> bool {
187        false
188    }
189
190    #[cfg(feature = "json")]
191    fn default_flatten_json() -> bool {
192        true
193    }
194}
195
196impl AsRef<TracingConfig> for TracingConfig {
197    fn as_ref(&self) -> &TracingConfig {
198        self
199    }
200}
201
202const _: () = {
203    impl<'de> Deserialize<'de> for TracingConfig {
204        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
205        where
206            D: Deserializer<'de>,
207        {
208            deserializer.deserialize_map(TracingConfigVisitor)
209        }
210    }
211
212    struct TracingConfigVisitor;
213
214    impl<'de> Visitor<'de> for TracingConfigVisitor {
215        type Value = TracingConfig;
216
217        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
218            formatter.write_str("a map of tracing (logging) configuration")
219        }
220
221        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
222        where
223            A: MapAccess<'de>,
224        {
225            let mut level = None;
226            let mut flavor = None;
227            let mut color = None;
228            let mut show_timestamp = None;
229            let mut show_target = None;
230            let mut show_file = None;
231            let mut show_line_number = None;
232            let mut show_level = None;
233            let mut show_thread_id = None;
234            let mut show_thread_name = None;
235            #[cfg(feature = "json")]
236            let mut flatten_json = None;
237            let mut targets = None;
238
239            while let Some(key) = map.next_key()? {
240                match key {
241                    TracingConfigField::verbosity => key.poll(&mut map, &mut level)?,
242                    TracingConfigField::flavor => key.poll(&mut map, &mut flavor)?,
243                    TracingConfigField::color => key.poll(&mut map, &mut color)?,
244                    TracingConfigField::show_timestamp => {
245                        key.poll(&mut map, &mut show_timestamp)?
246                    }
247                    TracingConfigField::show_target => key.poll(&mut map, &mut show_target)?,
248                    TracingConfigField::show_file => key.poll(&mut map, &mut show_file)?,
249                    TracingConfigField::show_line_number => {
250                        key.poll(&mut map, &mut show_line_number)?
251                    }
252                    TracingConfigField::show_level => key.poll(&mut map, &mut show_level)?,
253                    TracingConfigField::show_thread_id => {
254                        key.poll(&mut map, &mut show_thread_id)?
255                    }
256                    TracingConfigField::show_thread_name => {
257                        key.poll(&mut map, &mut show_thread_name)?
258                    }
259                    #[cfg(feature = "json")]
260                    TracingConfigField::flatten_json => key.poll(&mut map, &mut flatten_json)?,
261                    #[cfg(not(feature = "json"))]
262                    TracingConfigField::flatten_json => map.next_value()?,
263                    TracingConfigField::targets => key.poll(&mut map, &mut targets)?,
264                    TracingConfigField::__ignore => map.next_value()?,
265                };
266            }
267
268            Ok(TracingConfig {
269                verbosity: level.unwrap_or_default(),
270                flavor: flavor.unwrap_or_default(),
271                color: color.unwrap_or_else(TracingConfig::default_color),
272                show_timestamp: show_timestamp
273                    .unwrap_or_else(TracingConfig::default_show_timestamp),
274                show_target: show_target.unwrap_or_else(TracingConfig::default_show_target),
275                show_file: show_file.unwrap_or_else(TracingConfig::default_show_file),
276                show_line_number: show_line_number
277                    .unwrap_or_else(TracingConfig::default_show_line_number),
278                show_level: show_level.unwrap_or_else(TracingConfig::default_show_level),
279                show_thread_id: show_thread_id
280                    .unwrap_or_else(TracingConfig::default_show_thread_id),
281                show_thread_name: show_thread_name
282                    .unwrap_or_else(TracingConfig::default_show_thread_name),
283                #[cfg(feature = "json")]
284                flatten_json: flatten_json.unwrap_or_else(TracingConfig::default_flatten_json),
285                targets: targets.unwrap_or_default(),
286            })
287        }
288    }
289
290    impl_deserialize_field!(
291        TracingConfigField,
292        strut_deserialize::Slug::eq_as_slugs,
293        verbosity | level,
294        flavor | flavour,
295        color
296            | with_color
297            | colour
298            | with_colour
299            | show_color
300            | show_colour
301            | show_colors
302            | show_colours,
303        show_timestamp | with_timestamp,
304        show_target | with_target,
305        show_file | with_file,
306        show_line_number | show_line | with_line | with_line_number,
307        show_level | with_level,
308        show_thread_id | with_thread_id,
309        show_thread_name | with_thread_name,
310        flatten_json | flat_json | with_flat_json,
311        targets | custom_targets | target_verbosity,
312    );
313};
314
315#[cfg(test)]
316mod tests {
317    use crate::{FormatFlavor, TracingConfig, Verbosity};
318    use pretty_assertions::assert_eq;
319    use std::collections::BTreeMap;
320
321    #[test]
322    fn from_empty() {
323        // Given
324        let input = "{}";
325        let expected_output = TracingConfig::default();
326
327        // When
328        let actual_output = serde_yml::from_str::<TracingConfig>(input).unwrap();
329
330        // Then
331        assert_eq!(expected_output, actual_output);
332    }
333
334    #[test]
335    fn from_map_sparse() {
336        // Given
337        let input = r#"
338verbosity: off
339"#;
340        let expected_output = TracingConfig {
341            verbosity: Verbosity::Off,
342            ..TracingConfig::default()
343        };
344
345        // When
346        let actual_output = serde_yml::from_str::<TracingConfig>(input).unwrap();
347
348        // Then
349        assert_eq!(expected_output, actual_output);
350    }
351
352    #[test]
353    fn from_map_full() {
354        // Given
355        let input = r#"
356verbosity: warn
357flavor: pretty
358show_color: false
359show_timestamp: false
360show_target: false
361show_file: true
362show_line_number: true
363show_level: false
364show_thread_id: false
365show_thread_name: true
366flatten_json: true
367targets:
368    crate_a: off
369    crate_b::module: error
370"#;
371        let expected_output = TracingConfig {
372            verbosity: Verbosity::Warn,
373            flavor: FormatFlavor::Pretty,
374            color: false,
375            show_timestamp: false,
376            show_target: false,
377            show_file: true,
378            show_line_number: true,
379            show_level: false,
380            show_thread_id: false,
381            show_thread_name: true,
382            #[cfg(feature = "json")]
383            flatten_json: true,
384            targets: BTreeMap::from([
385                ("crate_a".to_string(), Verbosity::Off),
386                ("crate_b::module".to_string(), Verbosity::Error),
387            ]),
388        };
389
390        // When
391        let actual_output = serde_yml::from_str::<TracingConfig>(input).unwrap();
392
393        // Then
394        assert_eq!(expected_output, actual_output);
395    }
396}