defmt_decoder/log/
mod.rs

1//! Handles the conversion of defmt frames to log messages (as strings).
2//!
3//! Also provides interoperability utilities between [`defmt`] and the [`log`] crate.
4//!
5//! If you are implementing a custom defmt decoding tool, this module can make it easier to
6//! integrate it with logs produced with the [`log`] crate.
7//!
8//! [`log`]: https://crates.io/crates/log
9//! [`defmt`]: https://crates.io/crates/defmt
10
11pub mod format;
12mod json_logger;
13mod stdout_logger;
14
15use std::fmt;
16
17use log::{Level, LevelFilter, Log, Metadata, Record as LogRecord};
18use serde::{Deserialize, Serialize};
19
20use self::{
21    format::{Formatter, HostFormatter},
22    json_logger::JsonLogger,
23    stdout_logger::StdoutLogger,
24};
25use crate::Frame;
26
27const DEFMT_TARGET_MARKER: &str = "defmt@";
28
29/// Logs a defmt frame using the `log` facade.
30pub fn log_defmt(
31    frame: &Frame<'_>,
32    file: Option<&str>,
33    line: Option<u32>,
34    module_path: Option<&str>,
35) {
36    let (timestamp, level) = timestamp_and_level_from_frame(frame);
37
38    let target = format!(
39        "{}{}",
40        DEFMT_TARGET_MARKER,
41        serde_json::to_value(Payload { timestamp, level }).unwrap()
42    );
43
44    log::logger().log(
45        &LogRecord::builder()
46            .args(format_args!("{}", frame.display_message()))
47            // .level(level) // no need to set the level, since it is transferred via payload
48            .target(&target)
49            .module_path(module_path)
50            .file(file)
51            .line(line)
52            .build(),
53    );
54}
55
56/// Determines whether `metadata` belongs to a log record produced by [`log_defmt`].
57pub fn is_defmt_frame(metadata: &Metadata) -> bool {
58    metadata.target().starts_with(DEFMT_TARGET_MARKER)
59}
60
61/// A `log` record representing a defmt log frame.
62struct DefmtRecord<'a> {
63    log_record: &'a LogRecord<'a>,
64    payload: Payload,
65}
66
67#[derive(Clone, Copy, Debug)]
68pub enum DefmtLoggerType {
69    Stdout,
70    Json,
71}
72
73pub struct DefmtLoggerConfig {
74    pub formatter: Formatter,
75    pub host_formatter: HostFormatter,
76    pub logger_type: DefmtLoggerType,
77}
78
79#[derive(Deserialize, Serialize)]
80struct Payload {
81    level: Option<Level>,
82    timestamp: String,
83}
84
85impl<'a> DefmtRecord<'a> {
86    /// If `record` was produced by [`log_defmt`], returns the corresponding `DefmtRecord`.
87    pub fn new(log_record: &'a LogRecord<'a>) -> Option<Self> {
88        let target = log_record.metadata().target();
89        target
90            .strip_prefix(DEFMT_TARGET_MARKER)
91            .map(|payload| Self {
92                log_record,
93                payload: serde_json::from_str(payload).expect("malformed 'payload'"),
94            })
95    }
96
97    /// Returns the formatted defmt timestamp.
98    pub fn timestamp(&self) -> &str {
99        self.payload.timestamp.as_str()
100    }
101
102    pub fn level(&self) -> Option<Level> {
103        self.payload.level
104    }
105
106    pub fn args(&self) -> &fmt::Arguments<'a> {
107        self.log_record.args()
108    }
109
110    pub fn module_path(&self) -> Option<&'a str> {
111        self.log_record.module_path()
112    }
113
114    pub fn file(&self) -> Option<&'a str> {
115        self.log_record.file()
116    }
117
118    pub fn line(&self) -> Option<u32> {
119        self.log_record.line()
120    }
121}
122
123/// Initializes a `log` sink that handles defmt frames.
124///
125/// Defmt frames will be printed to stdout, other logs to stderr.
126///
127/// The caller has to provide a `should_log` closure that determines whether a log record should be
128/// printed.
129pub fn init_logger(
130    formatter: Formatter,
131    host_formatter: HostFormatter,
132    logger_type: DefmtLoggerType,
133    should_log: impl Fn(&Metadata) -> bool + Sync + Send + 'static,
134) {
135    let logger: Box<dyn Log> = match logger_type {
136        DefmtLoggerType::Stdout => StdoutLogger::new(formatter, host_formatter, should_log),
137        DefmtLoggerType::Json => {
138            JsonLogger::print_schema_version();
139            JsonLogger::new(formatter, host_formatter, should_log)
140        }
141    };
142    alterable_logger::set_boxed_logger(logger);
143    alterable_logger::set_max_level(LevelFilter::Trace);
144}
145
146fn timestamp_and_level_from_frame(frame: &Frame<'_>) -> (String, Option<Level>) {
147    let timestamp = frame
148        .display_timestamp()
149        .map(|ts| ts.to_string())
150        .unwrap_or_default();
151    let level = frame.level().map(|level| match level {
152        crate::Level::Trace => Level::Trace,
153        crate::Level::Debug => Level::Debug,
154        crate::Level::Info => Level::Info,
155        crate::Level::Warn => Level::Warn,
156        crate::Level::Error => Level::Error,
157    });
158    (timestamp, level)
159}