Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pub struct Formatter to defmt_decoder::log. #781

Merged
merged 15 commits into from
Oct 4, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

- [#781]: `defmt-decoder`: Add `pub struct Formatter` to `defmt_decoder::log`
- [#778]: `defmt-decoder`: Add support for nested log formatting
- [#777]: `defmt-decoder`: Simplify StdoutLogger
- [#775]: `defmt-decoder`: Ignore AArch64 mapping symbols
- [#771]: `defmt-macros`: Ignore empty items in DEFMT_LOG
- [#769]: `defmt-decoder`: Add support for color, style, width and alignment to format

[#781]: https://github.com/knurling-rs/defmt/pull/781
[#778]: https://github.com/knurling-rs/defmt/pull/778
[#777]: https://github.com/knurling-rs/defmt/pull/777
[#775]: https://github.com/knurling-rs/defmt/pull/775
Expand Down
12 changes: 7 additions & 5 deletions decoder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ use std::{
};

use byteorder::{ReadBytesExt, LE};
use decoder::Decoder;
use defmt_parser::Level;
use elf2table::parse_impl;

pub use elf2table::{Location, Locations};
pub use frame::Frame;
pub use stream::StreamDecoder;
use crate::{decoder::Decoder, elf2table::parse_impl};

pub use crate::{
elf2table::{Location, Locations},
frame::Frame,
stream::StreamDecoder,
};

/// Specifies the origin of a format string
#[derive(PartialEq, Eq, Debug)]
Expand Down
101 changes: 85 additions & 16 deletions decoder/src/log/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ mod format;
mod json_logger;
mod stdout_logger;

use std::fmt;

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

use std::fmt;

use self::{json_logger::JsonLogger, stdout_logger::StdoutLogger};
use self::{
format::{LogMetadata, LogSegment},
json_logger::JsonLogger,
stdout_logger::{Printer, StdoutLogger},
};
use crate::Frame;

const DEFMT_TARGET_MARKER: &str = "defmt@";
Expand All @@ -27,18 +31,7 @@ pub fn log_defmt(
line: Option<u32>,
module_path: Option<&str>,
) {
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,
});
let (timestamp, level) = timestamp_and_level_from_frame(frame);

let target = format!(
"{}{}",
Expand All @@ -64,7 +57,7 @@ pub fn is_defmt_frame(metadata: &Metadata) -> bool {
}

/// A `log` record representing a defmt log frame.
pub struct DefmtRecord<'a> {
struct DefmtRecord<'a> {
log_record: &'a Record<'a>,
payload: Payload,
}
Expand Down Expand Up @@ -170,3 +163,79 @@ impl DefmtLoggerInfo {
self.has_timestamp
}
}

/// Format [`Frame`]s according to a `log_format`.
///
/// The `log_format` makes it possible to customize the defmt output.
///
/// The `log_format` is specified here: TODO
// TODO:
// - use two Formatter in StdoutLogger instead of the log format
// - add fn format_to_sink
// - specify log format
// - clarify relationship between Formatter and Printer (https://github.com/knurling-rs/defmt/pull/781#discussion_r1343000073)
#[derive(Debug)]
pub struct Formatter {
format: Vec<LogSegment>,
}

impl Formatter {
pub fn new(log_format: &str) -> Self {
let format = format::parse(log_format)
.unwrap_or_else(|_| panic!("log format is invalid '{log_format}'"));
Urhengulas marked this conversation as resolved.
Show resolved Hide resolved
Self { format }
}

pub fn format_to_string(
&self,
frame: Frame<'_>,
file: Option<&str>,
line: Option<u32>,
module_path: Option<&str>,
) -> String {
let (timestamp, level) = timestamp_and_level_from_frame(&frame);

// HACK: use match instead of let, because otherwise compilation fails
#[allow(clippy::match_single_binding)]
match format_args!("{}", frame.display_message()) {
args => {
let log_record = &Record::builder()
.args(args)
.module_path(module_path)
.file(file)
.line(line)
.build();

let record = DefmtRecord {
log_record,
payload: Payload { level, timestamp },
};

match level {
Some(_) => Printer::new_defmt(&record, &self.format),
Urhengulas marked this conversation as resolved.
Show resolved Hide resolved
None => {
// handle defmt::println separately
const RAW_FORMAT: &[LogSegment] = &[LogSegment::new(LogMetadata::Log)];
Printer::new_defmt(&record, RAW_FORMAT)
}
}
.format_frame()
}
}
}
}

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)
}
50 changes: 33 additions & 17 deletions decoder/src/log/stdout_logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,50 +131,66 @@ impl StdoutLogger {
}

/// Printer for `DefmtRecord`s.
struct Printer<'a> {
pub(super) struct Printer<'a> {
record: Record<'a>,
format: &'a [LogSegment],
min_timestamp_width: usize,
}

impl<'a> Printer<'a> {
pub fn new(record: Record<'a>, format: &'a [LogSegment]) -> Self {
fn new(record: Record<'a>, format: &'a [LogSegment]) -> Self {
Self {
record,
format,
min_timestamp_width: 0,
}
}

pub fn new_defmt(record: &'a DefmtRecord<'a>, format: &'a [LogSegment]) -> Self {
Urhengulas marked this conversation as resolved.
Show resolved Hide resolved
Self::new(Record::Defmt(record), format)
}

/// Pads the defmt timestamp to take up at least the given number of characters.
/// TODO: Remove this, shouldn't be needed now that we have width field support
pub fn min_timestamp_width(&mut self, min_timestamp_width: usize) -> &mut Self {
fn min_timestamp_width(&mut self, min_timestamp_width: usize) -> &mut Self {
self.min_timestamp_width = min_timestamp_width;
self
}

/// Prints the formatted log frame to `sink`.
pub fn print_frame<W: io::Write>(&self, sink: &mut W) -> io::Result<()> {
for segment in self.format {
let s = match &segment.metadata {
LogMetadata::String(s) => s.to_string(),
LogMetadata::Timestamp => self.build_timestamp(&segment.format),
LogMetadata::FileName => self.build_file_name(&segment.format),
LogMetadata::FilePath => self.build_file_path(&segment.format),
LogMetadata::ModulePath => self.build_module_path(&segment.format),
LogMetadata::LineNumber => self.build_line_number(&segment.format),
LogMetadata::LogLevel => self.build_log_level(&segment.format),
LogMetadata::Log => self.build_log(&segment.format),
LogMetadata::NestedLogSegments(segments) => {
self.build_nested(segments, &segment.format)
}
};

let s = self.build_segment(segment);
write!(sink, "{s}")?;
}
writeln!(sink)
}

pub(super) fn format_frame(&self) -> String {
let mut buf = String::new();
for segment in self.format {
let s = self.build_segment(segment);
write!(buf, "{s}").expect("writing to String cannot fail");
}
buf
}

fn build_segment(&self, segment: &LogSegment) -> String {
match &segment.metadata {
LogMetadata::String(s) => s.to_string(),
LogMetadata::Timestamp => self.build_timestamp(&segment.format),
LogMetadata::FileName => self.build_file_name(&segment.format),
LogMetadata::FilePath => self.build_file_path(&segment.format),
LogMetadata::ModulePath => self.build_module_path(&segment.format),
LogMetadata::LineNumber => self.build_line_number(&segment.format),
LogMetadata::LogLevel => self.build_log_level(&segment.format),
LogMetadata::Log => self.build_log(&segment.format),
LogMetadata::NestedLogSegments(segments) => {
self.build_nested(segments, &segment.format)
}
}
}

fn build_nested(&self, segments: &[LogSegment], format: &LogFormat) -> String {
let mut result = String::new();
for segment in segments {
Expand Down