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

decoder: add support for customizable logger formatting #765

Merged
merged 20 commits into from
Aug 1, 2023

Conversation

andresovela
Copy link
Contributor

@andresovela andresovela commented Jul 15, 2023

A pain point for me when using defmt is that I can't easily customize the format in which the logs are printed.

This PR replaces the current PrettyLogger with a new StdoutLogger (seemed like a better name to me since the other logger is called JsonLogger), that supports an optional format string. If no format string is provided, a default format is used that looks pretty much the same as the current format, with the exception that the location is not "dimmed".

The formatting options I added are completely arbitrary and not set in stone. I just chose what made sense to me. This is from what I wrote in the docs:

 /// An optional `log_format` string can be provided to format the way
 /// logs are printed. A format string could look as follows:
 /// "{t} [{L}] Location<{f}:{l}> {s}"
 ///
 /// The arguments between curly braces are placeholders for log metadata.
 /// The following arguments are supported:
 /// - {t} : log timestamp
 /// - {f} : file name (e.g. "main.rs")
 /// - {F} : file path (e.g. "home/app/main.rs")
 /// - {m} : module path (e.g. "foo::bar::some_function")
 /// - {l} : line number
 /// - {L} : log level (e.g. "INFO", "DEBUG", etc)
 /// - {s} : the actual log
 ///
 /// For example, with the log format shown above, a log would look like this:
 /// "23124 [INFO] Location<main.rs:23> Hello, world!"

The new format is easily changeable and extendable. Some things that I think could be implemented but I chose not to waste time on before getting community feedback are:

  • Support for a new formatting option to set/reset colors
  • Support for escaped curly braces (the parser currently does not support random curly braces in the format string)
  • Support for field widths (e.g. {t:8} or something like that to specify that the timestamp must be at least 8 characters wide)

This PR is also an alternative to #762 (I think)

@Urhengulas
Copy link
Member

Thank you for the PR. I am looking into it

Copy link
Member

@Urhengulas Urhengulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like the idea!

Such a mechanism is common practice in many other logging systems, so I don't know why we did not think about it before.

Left a few comments.

decoder/src/log/json_logger.rs Show resolved Hide resolved
decoder/src/log/stdout_logger.rs Outdated Show resolved Hide resolved
decoder/src/log/stdout_logger.rs Show resolved Hide resolved
decoder/src/log/format.rs Outdated Show resolved Hide resolved
decoder/src/log/format.rs Outdated Show resolved Hide resolved
decoder/src/log/format.rs Outdated Show resolved Hide resolved
decoder/src/log/stdout_logger.rs Show resolved Hide resolved
decoder/src/log/stdout_logger.rs Outdated Show resolved Hide resolved
@andresovela
Copy link
Contributor Author

I made the requested changes and I added an extra host_log_format option. It made sense to implement it after reading some of the issues in probe-run.

decoder/src/log/json_logger.rs Outdated Show resolved Hide resolved
Copy link
Member

@Urhengulas Urhengulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I played around with the feature and really like it. It gives users all the flexibility they need.

I've noticed a few details which changed with your PR. Some of these changes might be okay, but I wanted to make sure none of them happen accidentally, and some plainly need to be fixed.

Please have a look at these probe-run invocations as context
$ # before PR without timestamp
$ DEFMT_LOG=trace cargo run -q --bin levels
(HOST) INFO  flashing program (2 pages / 8.00 KiB)
(HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
INFO  info
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:10
TRACE trace
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:11
WARN  warn
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:12
DEBUG debug
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:13
ERROR error
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:14
println
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:15
────────────────────────────────────────────────────────────────────────────────
(HOST) INFO  device halted without error

$ # before PR with timestamp
$ DEFMT_LOG=trace cargo run -q --bin levels
0 INFO  info
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:10
1 TRACE trace
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:11
2 WARN  warn
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:12
3 DEBUG debug
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:13
4 ERROR error
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:14
5 println
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:15

$ # after PR without timestamp
$ DEFMT_LOG=trace cargo run -q --bin levels
(HOST) INFO flashing program (2 pages / 8.00 KiB)
(HOST) INFO success!
────────────────────────────────────────────────────────────────────────────────
 INFO info
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:10
 TRACE trace
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:11
 WARN warn
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:12
 DEBUG debug
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:13
 ERROR error
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:14
 <lvl> println
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:15
────────────────────────────────────────────────────────────────────────────────
(HOST) INFO device halted without error

$ # after PR with timestamp
$ DEFMT_LOG=trace cargo run -q --bin levels
0 INFO info
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:10
1 TRACE trace
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:11
2 WARN warn
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:12
3 DEBUG debug
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:13
4 ERROR error
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:14
5 <lvl> println
└─ levels::__cortex_m_rt_main @ src/bin/levels.rs:15

timestamp

With your PR and the default format, if there is no timestamp, the first character is a space. Before this was not the case1, but the log line just started with the log level. This should be fixed.

println

The idea of defmt::println is that the log message is printed without the log level and timestamp. With your PR it emits a <lvl> as log level. This should be fixed.

bold messages

With your PR all messages are bold. Before only log messages were bold, but println and host messages were normal. This is okay if it changes, but I would probably err for making all log messages normal (and the metadata dimmed).

space between timestamp and log message

Before, there were enough spaces between the log level and the log message so the beginning of the log message was aligned. With the PR there is always just one space. I think this is okay if it changes.

Footnotes

  1. Actually there was a similar bug https://github.com/knurling-rs/defmt/issues/602

decoder/src/log/stdout_logger.rs Outdated Show resolved Hide resolved
@andresovela
Copy link
Contributor Author

andresovela commented Jul 19, 2023

With your PR and the default format, if there is no timestamp, the first character is a space. Before this was not the case, but the log line just started with the log level. This should be fixed.

Unless I'm misunderstanding the circumstances under which the timestamp would not be available, I'd argue that the previous behavior is not the right behavior in the context of the new formatting system in this PR.

I'm assuming that a timestamp is not available if the user does not provide a defmt::timestamp!() implementation. If the format specifies that a timestamp should be printed, I think printing 0 illustrates the issue better than just not printing it.

I also just don't see how I can implement the previous behavior nicely in the current system. If the user does not intend to provide a defmt::timestamp!() implementation, I think the right call would be to guide the user to remove the timestamp from the log format.

For example if I want the following format:

T1234 | INFO | Hello

I think it'd be better to print

T0 | INFO | Hello

than

T| INFO | Hello

@andresovela
Copy link
Contributor Author

The idea of defmt::println is that the log message is printed without the log level and timestamp. With your PR it emits a <lvl> as log level. This should be fixed.

I've fixed this. I think I hadn't understood the purpose of that println function before. I actually think that whole thing should be revamped, and we should instead have info_raw!(), debug_raw!(), ... macros, that print logs without formatting but preserve log level information for filtering purposes.

I see the value for a println!() macro that always prints regardless of the configured log level, but I still think these logs should have format. I think this is a subject for future discussion and should be addressed in a future PR if there's a desire for such a thing.

@andresovela
Copy link
Contributor Author

andresovela commented Jul 19, 2023

With your PR all messages are bold. Before only log messages were bold, but println and host messages were normal. This is okay if it changes, but I would probably err for making all log messages normal (and the metadata dimmed).

I've fixed this, with the exception of the dimmed metadata. I think further formatting options should be added in future PRs.

@andresovela
Copy link
Contributor Author

Before, there were enough spaces between the log level and the log message so the beginning of the log message was aligned. With the PR there is always just one space. I think this is okay if it changes.

I didn't understand this comment, but I think we should extend the formatting options with a field width specifier in a future PR. I think that would address this in a better way.

Then you could have something like "{L} {f:25} {s}" that would print something like:

INFO main.rs                   Hello, world!
INFO decoder.rs                Decoding...

without the need to special-case everything internally and allowing for better user-customization.

@andresovela
Copy link
Contributor Author

space between timestamp and log message

Ah I see what you mean now, but I don't understand why this is not working. The width specifier is already there. Any ideas?

@Urhengulas
Copy link
Member

Unless I'm misunderstanding the circumstances under which the timestamp would not be available, I'd argue that the previous behavior is not the right behavior in the context of the new formatting system in this PR.

I'm assuming that a timestamp is not available if the user does not provide a defmt::timestamp!() implementation. If the format specifies that a timestamp should be printed, I think printing 0 illustrates the issue better than just not printing it.

Or we have a <time> default.

I also just don't see how I can implement the previous behavior nicely in the current system. If the user does not intend to provide a defmt::timestamp!() implementation, I think the right call would be to guide the user to remove the timestamp from the log format.

I agree. I am unsure if we easily know if there is a timestamp implementation or not.

@Urhengulas
Copy link
Member

Urhengulas commented Jul 24, 2023

I am unsure if we easily know if there is a timestamp implementation or not.

It seems that adding a defmt_decoder::Table::has_timestamp() method does the job:

diff --git a/decoder/src/lib.rs b/decoder/src/lib.rs
index cf7fa12..7da00bf 100644
--- a/decoder/src/lib.rs
+++ b/decoder/src/lib.rs
@@ -271,6 +271,10 @@ impl Table {
     pub fn encoding(&self) -> Encoding {
         self.encoding
     }
+
+    pub fn has_timestamp(&self) -> bool {
+        self.timestamp.is_some()
+    }
 }
 
 // NOTE follows `parser::Type`

I tested it with a small project and adding and removing the timestamp implementation.

@andresovela
Copy link
Contributor Author

So what would you like me to do about this?

@Urhengulas
Copy link
Member

So what would you like me to do about this?

I propose the following:

timestamp implementation timestamp in log_format action
yes yes just display it
yes no just not display it
no yes warn that there is no timestamp implementation and that it should be added or the timestamp should be removed from the log_format
no no just not display it

Do you agree?

@andresovela
Copy link
Contributor Author

I agree with the warning, but what do I print with the logs in that case? ""? " "? "0"?

@andresovela
Copy link
Contributor Author

I added an info struct to the defmt logger API, and I added the warning in probe-run

@Urhengulas
Copy link
Member

I agree with the warning, but what do I print with the logs in that case? ""? " "? "0"?

<time>?

@andresovela
Copy link
Contributor Author

Done

@Urhengulas Urhengulas added this pull request to the merge queue Aug 1, 2023
Merged via the queue into knurling-rs:main with commit 85c4073 Aug 1, 2023
15 checks passed
@andresovela andresovela deleted the customizable-logger branch August 1, 2023 09:44
@ia0
Copy link
Contributor

ia0 commented Aug 23, 2023

My understanding is that with this PR, the println format is hard-coded and it is not possible to add the timestamp as it used to be the case (IIRC). Is this by design or it would be possible to let the user customize the println format too? (like for the log and host log format)

@andresovela
Copy link
Contributor Author

Hi @ia0,

According to @Urhengulas this is by design:

println

The idea of defmt::println is that the log message is printed without the log level and timestamp. With your PR it emits a <lvl> as log level. This should be fixed.

I also argued that a discussion should be had about what we want println to do:

I've fixed this. I think I hadn't understood the purpose of that println function before. I actually think that whole thing should be revamped, and we should instead have info_raw!(), debug_raw!(), ... macros, that print logs without formatting but preserve log level information for filtering purposes.

I see the value for a println!() macro that always prints regardless of the configured log level, but I still think these logs should have format. I think this is a subject for future discussion and should be addressed in a future PR if there's a desire for such a thing.

@ia0
Copy link
Contributor

ia0 commented Aug 28, 2023

Thanks for the response! I'm working around by adding the timestamp directly in the println call. However, if the goal is for println to not show any timestamp information, then target timestamp should not be sent for those messages. When I use probe-run --json I can see the target_timestamp field being populated on println calls.

@Urhengulas
Copy link
Member

Thanks for the response! I'm working around by adding the timestamp directly in the println call. However, if the goal is for println to not show any timestamp information, then target timestamp should not be sent for those messages. When I use probe-run --json I can see the target_timestamp field being populated on println calls.

Hei 👋🏾

I agree that we need to make the logging story more clear, I am gonna put some time into that soon™️

I would be interested why you cannot use the logging macros (e.g. info, debug) if you want the timestamp?

@ia0
Copy link
Contributor

ia0 commented Aug 28, 2023

I would be interested why you cannot use the logging macros (e.g. info, debug) if you want the timestamp?

Because that's something I want always printed, regardless of log level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants