trace4rs_config/
config.rs

1//! Configuration structures which can be used for file based `trace4rs` config.
2
3use std::{
4    borrow::Cow,
5    collections::{
6        HashMap,
7        HashSet,
8    },
9    result,
10    str::FromStr,
11};
12
13#[cfg(feature = "schemars")]
14use schemars::JsonSchema;
15#[cfg(feature = "serde")]
16use serde::{
17    Deserialize,
18    Deserializer,
19    Serialize,
20    Serializer,
21};
22use smart_default::SmartDefault;
23
24use crate::error::{
25    Error,
26    Result,
27};
28
29/// The root configuration object containing everything necessary to build a
30/// `trace4rs::Handle`.
31#[derive(PartialEq, Eq, Clone, Debug)]
32#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33#[cfg_attr(feature = "schemars", derive(JsonSchema))]
34pub struct Config {
35    /// The default logger, which must be configured.
36    #[cfg_attr(feature = "serde", serde(rename = "root", alias = "default"))]
37    pub default:   Logger,
38    /// Appenders are assigned an id of your choice and configure actual log
39    /// message output.
40    #[cfg_attr(
41        feature = "in-order-serialization",
42        serde(serialize_with = "ordered_map")
43    )]
44    pub appenders: HashMap<AppenderId, Appender>,
45    /// Loggers receive events which match their target and may filter by
46    /// message level.
47    #[cfg_attr(
48        feature = "in-order-serialization",
49        serde(serialize_with = "ordered_map")
50    )]
51    pub loggers:   HashMap<Target, Logger>,
52}
53
54/// # Errors
55/// Returns an error if serialization fails
56#[cfg(feature = "in-order-serialization")]
57pub fn ordered_map<K, V, S>(
58    value: &HashMap<K, V>,
59    serializer: S,
60) -> std::result::Result<S::Ok, S::Error>
61where
62    K: Ord + Serialize,
63    V: Serialize,
64    S: Serializer,
65{
66    let ordered: std::collections::BTreeMap<_, _> = value.iter().collect();
67    ordered.serialize(serializer)
68}
69
70/// # Errors
71/// Returns an error if serialization fails
72#[cfg(feature = "in-order-serialization")]
73pub fn ordered_set<K, S>(value: &HashSet<K>, serializer: S) -> std::result::Result<S::Ok, S::Error>
74where
75    K: Ord + Serialize,
76    S: Serializer,
77{
78    let ordered: std::collections::BTreeSet<_> = value.iter().collect();
79    ordered.serialize(serializer)
80}
81
82impl Default for Config {
83    fn default() -> Self {
84        Self::console_config()
85    }
86}
87
88impl Config {
89    /// A configuration for `INFO` and above to be logged to stdout.
90    fn console_config() -> Config {
91        use literally::{
92            hmap,
93            hset,
94        };
95
96        Config {
97            default:   Logger {
98                level:     LevelFilter::INFO,
99                appenders: hset! { "stdout" },
100                format:    Format::default(),
101            },
102            loggers:   hmap! {},
103            appenders: hmap! {
104                "stdout" => Appender::Console
105            },
106        }
107    }
108}
109/// A log target, for example to capture all log messages in `trace4rs::config`
110/// the target would be `trace4rs::config`.
111#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113#[cfg_attr(feature = "schemars", derive(JsonSchema))]
114pub struct Target(pub String);
115impl Target {
116    #[must_use]
117    pub fn as_str(&self) -> &str {
118        &self.0
119    }
120}
121impl From<&str> for Target {
122    fn from(s: &str) -> Self {
123        Target(s.to_string())
124    }
125}
126impl ToString for Target {
127    fn to_string(&self) -> String {
128        self.0.clone()
129    }
130}
131
132/// An `AppenderId` is an arbitrary string which in the context of a config must
133/// be unique.
134#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
135#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
136#[cfg_attr(feature = "schemars", derive(JsonSchema))]
137pub struct AppenderId(pub String);
138
139/// A logger allows for filtering events and delegating to multiple appenders.
140#[derive(PartialEq, Eq, Clone, Debug)]
141#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
142#[cfg_attr(feature = "schemars", derive(JsonSchema))]
143pub struct Logger {
144    #[cfg_attr(
145        feature = "in-order-serialization",
146        serde(serialize_with = "ordered_set")
147    )]
148    pub appenders: HashSet<AppenderId>,
149    pub level:     LevelFilter,
150    #[cfg_attr(
151        feature = "serde",
152        serde(default = "Format::default", skip_serializing_if = "Format::is_normal")
153    )]
154    pub format:    Format,
155}
156
157#[cfg(feature = "serde")]
158macro_rules! named_unit_variant {
159    ($variant:ident) => {
160        pub mod $variant {
161            pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
162            where
163                S: serde::Serializer,
164            {
165                serializer.serialize_str(stringify!($variant))
166            }
167
168            pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
169            where
170                D: serde::Deserializer<'de>,
171            {
172                struct V;
173                impl<'de> serde::de::Visitor<'de> for V {
174                    type Value = ();
175
176                    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177                        f.write_str(concat!("\"", stringify!($variant), "\""))
178                    }
179
180                    fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
181                        if value == stringify!($variant) {
182                            Ok(())
183                        } else {
184                            Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
185                        }
186                    }
187                }
188                deserializer.deserialize_str(V)
189            }
190        }
191    };
192}
193
194#[cfg(feature = "serde")]
195mod format {
196    named_unit_variant!(normal);
197    named_unit_variant!(messageonly);
198
199    pub mod custom {
200        pub fn serialize<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
201        where
202            S: serde::Serializer,
203        {
204            serializer.serialize_str(value)
205        }
206
207        pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
208        where
209            D: serde::Deserializer<'de>,
210        {
211            struct V;
212            impl<'de> serde::de::Visitor<'de> for V {
213                type Value = String;
214
215                fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
216                    f.write_str(concat!(r#"{ "custom": "<format string>" }"#))
217                }
218
219                fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
220                    if value != "messageonly" && value != "normal" {
221                        Ok(value.to_string())
222                    } else {
223                        Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
224                    }
225                }
226            }
227            deserializer.deserialize_str(V)
228        }
229    }
230}
231
232#[derive(PartialEq, Eq, Clone, Debug, SmartDefault)]
233#[cfg_attr(
234    feature = "serde",
235    derive(Serialize, Deserialize),
236    serde(untagged, rename_all = "lowercase")
237)]
238#[cfg_attr(feature = "schemars", derive(JsonSchema))]
239pub enum Format {
240    #[default]
241    #[cfg_attr(feature = "serde", serde(with = "format::normal"))]
242    #[cfg_attr(feature = "schemars", schemars(with = "String"))]
243    Normal,
244    #[cfg_attr(feature = "serde", serde(with = "format::messageonly"))]
245    #[cfg_attr(feature = "schemars", schemars(with = "String"))]
246    MessageOnly,
247    #[cfg_attr(feature = "serde", serde(with = "format::custom"))]
248    #[cfg_attr(feature = "schemars", schemars(with = "String"))]
249    Custom(String),
250}
251impl Format {
252    #[cfg(feature = "serde")]
253    #[allow(clippy::trivially_copy_pass_by_ref)]
254    fn is_normal(&self) -> bool {
255        matches!(self, Self::Normal)
256    }
257}
258
259/// Simply a wrapper around `tracing::LevelFilter` such that it can be used by
260/// `serde`.
261#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
262#[cfg_attr(feature = "schemars", derive(JsonSchema), schemars(transparent))]
263pub struct LevelFilter(
264    #[cfg_attr(feature = "schemars", schemars(with = "String"))]
265    tracing::level_filters::LevelFilter,
266);
267impl From<LevelFilter> for tracing::level_filters::LevelFilter {
268    fn from(l: LevelFilter) -> Self {
269        l.0
270    }
271}
272
273#[rustfmt::skip] // eas: retain order
274impl LevelFilter {
275    pub const TRACE: Self = LevelFilter(tracing::level_filters::LevelFilter::TRACE);
276    pub const DEBUG: Self = LevelFilter(tracing::level_filters::LevelFilter::DEBUG);
277    pub const INFO: Self = LevelFilter(tracing::level_filters::LevelFilter::INFO);
278    pub const WARN: Self = LevelFilter(tracing::level_filters::LevelFilter::WARN);
279    pub const ERROR: Self = LevelFilter(tracing::level_filters::LevelFilter::ERROR);
280    pub const OFF: Self = LevelFilter(tracing::level_filters::LevelFilter::OFF);
281    #[must_use] pub const fn maximum() -> Self {
282        Self::TRACE
283    }
284}
285
286#[cfg(feature = "serde")]
287impl Serialize for LevelFilter {
288    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
289    where
290        S: Serializer,
291    {
292        serializer.serialize_str(&self.0.to_string().to_ascii_uppercase())
293    }
294}
295#[cfg(feature = "serde")]
296impl<'de> Deserialize<'de> for LevelFilter {
297    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
298    where
299        D: Deserializer<'de>,
300    {
301        let s = String::deserialize(deserializer)?;
302        FromStr::from_str(&s)
303            .map(Self)
304            .map_err(serde::de::Error::custom)
305    }
306}
307impl FromStr for LevelFilter {
308    type Err = <tracing::level_filters::LevelFilter as FromStr>::Err;
309
310    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
311        Ok(Self(FromStr::from_str(s)?))
312    }
313}
314
315/// An Appender specifies a single event sink.
316#[derive(Clone, Debug, PartialEq, Eq)]
317#[cfg_attr(feature = "schemars", derive(JsonSchema))]
318#[cfg_attr(
319    feature = "serde",
320    derive(Serialize, Deserialize),
321    serde(tag = "kind", rename_all = "lowercase")
322)]
323pub enum Appender {
324    Null,
325    Console,
326    File {
327        path: String,
328    },
329    RollingFile {
330        path:   String,
331        #[cfg_attr(feature = "serde", serde(rename = "rolloverPolicy"))]
332        policy: Policy,
333    },
334}
335
336impl Appender {
337    pub fn file(path: impl Into<String>) -> Self {
338        Self::File { path: path.into() }
339    }
340
341    pub fn console() -> Self {
342        Self::Console
343    }
344}
345impl From<&str> for AppenderId {
346    fn from(s: &str) -> Self {
347        AppenderId(s.to_string())
348    }
349}
350
351/// A Policy specifies how a `RollingFile` appender should be rolled.
352#[derive(Clone, Debug, PartialEq, Eq)]
353#[cfg_attr(feature = "schemars", derive(JsonSchema))]
354#[cfg_attr(
355    feature = "serde",
356    derive(Serialize, Deserialize),
357    serde(rename_all = "camelCase")
358)]
359pub struct Policy {
360    pub maximum_file_size: String,
361    pub max_size_roll_backups: u32,
362    #[cfg_attr(
363        feature = "serde",
364        serde(default, skip_serializing_if = "Option::is_none")
365    )]
366    pub pattern: Option<String>,
367}
368
369impl Policy {
370    /// Takes a string like 10kb and returns the number of bytes as a u64.
371    ///
372    /// # Examples
373    ///
374    /// ```text
375    /// 10, 10b
376    /// 10kb 10kib
377    /// 10mb 10mib
378    /// 10gb 10gib
379    /// 10tb 10tib // please no
380    /// ```
381    ///
382    /// # Errors
383    /// If the size is not of the aforementioned form we will fail to parse.
384    pub fn calculate_maximum_file_size(size: &str) -> Result<u64> {
385        const KB: u64 = 1024;
386        const MB: u64 = KB * 1024;
387        const GB: u64 = MB * 1024;
388        const TB: u64 = GB * 1024;
389
390        // This is lifted from log4rs. We need to replace this..or something.
391        let (number, unit) = match size.find(|c: char| !c.is_ascii_digit()) {
392            Some(n) => {
393                let mut chars = size.chars();
394                let (first, rest) = (
395                    chars.by_ref().take(n).collect::<String>(),
396                    chars.collect::<String>(),
397                );
398                (
399                    Cow::Owned(first.trim().to_string()),
400                    Some(rest.trim().to_string()),
401                )
402            },
403            None => (Cow::Borrowed(size.trim()), None),
404        };
405
406        let number = match number.parse::<u64>() {
407            Ok(n) => n,
408            Err(e) => return Err(e.into()),
409        };
410
411        let unit = match unit {
412            Some(u) => u,
413            None => return Ok(number),
414        };
415
416        let bytes_number = if unit.eq_ignore_ascii_case("b") {
417            Some(number)
418        } else if unit.eq_ignore_ascii_case("kb") || unit.eq_ignore_ascii_case("kib") {
419            number.checked_mul(KB)
420        } else if unit.eq_ignore_ascii_case("mb") || unit.eq_ignore_ascii_case("mib") {
421            number.checked_mul(MB)
422        } else if unit.eq_ignore_ascii_case("gb") || unit.eq_ignore_ascii_case("gib") {
423            number.checked_mul(GB)
424        } else if unit.eq_ignore_ascii_case("tb") || unit.eq_ignore_ascii_case("tib") {
425            number.checked_mul(TB)
426        } else {
427            return Err(Error::UnexpectedUnit(unit));
428        };
429
430        match bytes_number {
431            Some(n) => Ok(n),
432            None => Err(Error::Overflow { number, unit }),
433        }
434    }
435}
436
437#[cfg(all(test, feature = "serde"))]
438mod test {
439    use literally::hset;
440
441    use super::{
442        LevelFilter,
443        Logger,
444    };
445    use crate::config::Format;
446
447    #[test]
448    fn test_format_serde() {
449        let lgr = Logger {
450            appenders: hset! {},
451            level:     LevelFilter::OFF,
452            format:    Format::Normal,
453        };
454        let lgr_value = dbg!(serde_json::to_value(&lgr).unwrap());
455        assert!(lgr_value.get("format").is_none());
456        let lgr_parsed: Logger = serde_json::from_value(lgr_value).unwrap();
457        assert_eq!(lgr_parsed.format, Format::Normal);
458
459        let lgr = Logger {
460            appenders: hset! {},
461            level:     LevelFilter::OFF,
462            format:    Format::MessageOnly,
463        };
464        let lgr_value = dbg!(serde_json::to_value(&lgr).unwrap());
465        let fmt = lgr_value.get("format").unwrap().as_str().unwrap();
466        assert_eq!(fmt, "messageonly");
467        let lgr_parsed: Logger = serde_json::from_value(lgr_value).unwrap();
468        assert_eq!(lgr_parsed.format, Format::MessageOnly);
469
470        let lgr = Logger {
471            appenders: hset! {},
472            level:     LevelFilter::OFF,
473            format:    Format::Custom("foobar".to_string()),
474        };
475        let lgr_value = dbg!(serde_json::to_value(&lgr).unwrap());
476        let fmt = lgr_value.get("format").unwrap().as_str().unwrap();
477        assert_eq!(fmt, "foobar");
478        let lgr_parsed: Logger = serde_json::from_value(lgr_value).unwrap();
479        assert_eq!(lgr_parsed.format, Format::Custom("foobar".to_string()));
480    }
481}