Simple diagnostic printf logging for Arduino-ish code on 32-bit+ processors, with support for tagging and per-tag level filtering.
You should first consider these more established libraries:
- Arduino-Log - level filtering, printf formatting, multiple
Printoutputs - arduino-logger - level filtering, printf formatting, ISR-safe, many implementation choices
- DebugLog - level filtering, argument formatting,
Streamand/or file output - EasyLogger - level filtering, tag ("service") filtering, any
Printoutput - ESPLogger - logger-object level filtering, no formatting, serial output
- Logger - level filtering, no formatting, serial output or custom function
- MycilaLogger - logger-object level filtering, printf formatting, multiple
Printoutputs, ESP32 only - roo_logging - level filtering, per-tag verbosity, iostreams formatting, serial output
- SerialLogger - combined data/diagnostic logging, no formatting, level filtering, serial output
This library (ok_arduino_logging) adds centrally configured string-tag log level filtering: Each source file define a tag string which is added to logs from that file. The main app/sketch is compiled with a filter expression for tags and log levels, so you can dial verbosity up and down for per subsystem without modifying library code or rebuilding the world.
Log tags are free form strings: they can be the source file name, a logical subsystem name, the name of a library, or whatever else makes sense for filtering. Multiple source files can use the same tag, and tags can be localized to classes or functions. Filter expressions can use * wildcards for tag matching to allow for structured tags (like network.http.parser).
Importantly, libraries can use tags (often the library name, maybe with suffixes) so the app can tweak per-library log verbosity without modifying library code.
Other niceties include:
- printf formatting (arguments aren't evaluated if the log is suppressed)
- assert-like
OK_ERROR_IF(...)andOK_FATAL_IF(...)macros for convenience - source file, line, and function reporting for fatal and assert-check errors, but not others
- compact emoji representations of log levels
- basic unit tests (!!) using wokwi
These would be nice but are NOT currently supported:
- custom formatting of log lines
- routing to multiple destinations (eg. sd card + serial)
- management/prevention of I/O blocking from logging
- management/prevention of repeated log-spam
- thread or ISR safety guarantees
#include <ok_logging.h>
static OkLoggingContext OK_CONTEXT("my_app");
static int loop_count;
void setup() {
Serial.begin(115200); // or whatever
OK_NOTE("In setup!");
OK_ERROR("Oh no, an error occurred!");
}
void loop() {
OK_NOTE("In loop! Counter=%d", loop_count);
delay(500);
++loop_count;
}
Note, an OkLoggingContext object named OK_CONTEXT must be in scope for any logging macro use. This is typically a file-scoped static global, as above. The constructor takes the log tag (any string, or nullptr) for logging statements in this scope.
This example generates output like the following:
0.524 [my_app] In setup!
0.525 ⚠️ [my_app] Oh no, an error happened!
0.526 [my_app] In loop! Counter=0
1.996 [my_app] In loop! Counter=1
Several macros and functions are defined in <ok_logging.h>
OK_DETAIL(fmt, ...)- lowest priority levelOK_NOTE(fmt, ...)- "normal" priority levelOK_ERROR(fmt, ...)- elevated priority, indicates a problemOK_FATAL(fmt, ...)- highest priority, aborts and rebootsOK_FATAL_IF(cond)- ifcondistrue, logs source location and#cond, aborts and rebootsOK_ERROR_IF(cond)- ifcondistrue, logs source location and#cond; returnscondeither wayset_ok_logging_output(output)- redirects logs to aPrint*stream (&Serialby default)set_ok_logging_global_minimum(level)- sets a global minimum level to print, and returns the old global minimum level (defaultOK_DETAIL_LEVEL)
Logs are filtered with a global configuration string, which can be set one of two ways:
- Define
char const* ok_logging_config = "...";in a source file - OR pass
-DOK_LOGGING_CONFIG=...to the compiler
Either way, the configuration string is a comma-separated series of tagpattern=level rules, where tagpattern is a log tag (as set in OK_CONTEXT) with optional * wildcards, and level is one of DETAIL, NOTE, ERROR, or FATAL. A bare level name with no = is equivalent to *=level. For each log tag, the first matching rule is used as the minimum level to print.
For example, the configuration "foo=DETAIL,bar=ERROR,baz.*=NOTE,FATAL":
- prints all logs tagged "
foo" - prints
ERRORorFATALlogs tagged "bar" - prints non-
DETAILlogs with a tag starting with "baz." (eg. "baz.blah" but not plain "baz"), - only prints
FATALlogs with any other tag
When no rule matches a tag, or there is no configuration, the default is NOTE (everything but DETAIL).
Logging macros synchronously write to the output object (Serial by default) which can mess with timing. On most Arduino runtimes, serial output is thread-safe but not interrupt-safe.
The logging formatter attempts to use a 128-byte stack buffer first (so 128 bytes should be available on the stack), if the message would exceed that size a dynamically allocated buffer is used, if allocation fails an error message is printed instead.