1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! This module provides interoperability utilities between [`defmt`] and the [`log`] crate.
//!
//! If you are implementing a custom defmt decoding tool, this module can make it easier to
//! integrate it with logs produced with the [`log`] crate.
//!
//! [`log`]: https://crates.io/crates/log
//! [`defmt`]: https://crates.io/crates/defmt

pub mod format;
mod json_logger;
mod stdout_logger;

use std::fmt;

use log::{Level, LevelFilter, Log, Metadata, Record as LogRecord};
use serde::{Deserialize, Serialize};

use self::{
    format::{Formatter, HostFormatter},
    json_logger::JsonLogger,
    stdout_logger::StdoutLogger,
};
use crate::Frame;

const DEFMT_TARGET_MARKER: &str = "defmt@";

/// Logs a defmt frame using the `log` facade.
pub fn log_defmt(
    frame: &Frame<'_>,
    file: Option<&str>,
    line: Option<u32>,
    module_path: Option<&str>,
) {
    let (timestamp, level) = timestamp_and_level_from_frame(frame);

    let target = format!(
        "{}{}",
        DEFMT_TARGET_MARKER,
        serde_json::to_value(Payload { timestamp, level }).unwrap()
    );

    log::logger().log(
        &LogRecord::builder()
            .args(format_args!("{}", frame.display_message()))
            // .level(level) // no need to set the level, since it is transferred via payload
            .target(&target)
            .module_path(module_path)
            .file(file)
            .line(line)
            .build(),
    );
}

/// Determines whether `metadata` belongs to a log record produced by [`log_defmt`].
pub fn is_defmt_frame(metadata: &Metadata) -> bool {
    metadata.target().starts_with(DEFMT_TARGET_MARKER)
}

/// A `log` record representing a defmt log frame.
struct DefmtRecord<'a> {
    log_record: &'a LogRecord<'a>,
    payload: Payload,
}

#[derive(Clone, Copy, Debug)]
pub enum DefmtLoggerType {
    Stdout,
    Json,
}

pub struct DefmtLoggerConfig {
    pub formatter: Formatter,
    pub host_formatter: HostFormatter,
    pub logger_type: DefmtLoggerType,
}

#[derive(Deserialize, Serialize)]
struct Payload {
    level: Option<Level>,
    timestamp: String,
}

impl<'a> DefmtRecord<'a> {
    /// If `record` was produced by [`log_defmt`], returns the corresponding `DefmtRecord`.
    pub fn new(log_record: &'a LogRecord<'a>) -> Option<Self> {
        let target = log_record.metadata().target();
        target
            .strip_prefix(DEFMT_TARGET_MARKER)
            .map(|payload| Self {
                log_record,
                payload: serde_json::from_str(payload).expect("malformed 'payload'"),
            })
    }

    /// Returns the formatted defmt timestamp.
    pub fn timestamp(&self) -> &str {
        self.payload.timestamp.as_str()
    }

    pub fn level(&self) -> Option<Level> {
        self.payload.level
    }

    pub fn args(&self) -> &fmt::Arguments<'a> {
        self.log_record.args()
    }

    pub fn module_path(&self) -> Option<&'a str> {
        self.log_record.module_path()
    }

    pub fn file(&self) -> Option<&'a str> {
        self.log_record.file()
    }

    pub fn line(&self) -> Option<u32> {
        self.log_record.line()
    }
}

/// Initializes a `log` sink that handles defmt frames.
///
/// Defmt frames will be printed to stdout, other logs to stderr.
///
/// The caller has to provide a `should_log` closure that determines whether a log record should be
/// printed.
pub fn init_logger(
    formatter: Formatter,
    host_formatter: HostFormatter,
    logger_type: DefmtLoggerType,
    should_log: impl Fn(&Metadata) -> bool + Sync + Send + 'static,
) {
    let logger: Box<dyn Log> = match logger_type {
        DefmtLoggerType::Stdout => StdoutLogger::new(formatter, host_formatter, should_log),
        DefmtLoggerType::Json => {
            JsonLogger::print_schema_version();
            JsonLogger::new(formatter, host_formatter, should_log)
        }
    };
    log::set_boxed_logger(logger).unwrap();
    log::set_max_level(LevelFilter::Trace);
}

fn timestamp_and_level_from_frame(frame: &Frame<'_>) -> (String, Option<Level>) {
    let timestamp = frame
        .display_timestamp()
        .map(|ts| ts.to_string())
        .unwrap_or_default();
    let level = frame.level().map(|level| match level {
        crate::Level::Trace => Level::Trace,
        crate::Level::Debug => Level::Debug,
        crate::Level::Info => Level::Info,
        crate::Level::Warn => Level::Warn,
        crate::Level::Error => Level::Error,
    });
    (timestamp, level)
}