In Part 3, we gave MicrOS logging compile-time filtering and per-module verbosity control. That already makes it a powerful tool for day-to-day development. But what if we want to push further?
In this final part of the series, I’ll explore runtime flexibility, multiple backends, timestamps, and structured logs. These are the kinds of features that turn a logging system from a developer aid into a serious instrumentation layer for your embedded OS.
Runtime Log Level Control
Right now, MicrOS log levels are fixed at compile time. That’s great for efficiency, but sometimes you need to turn up the volume without rebuilding the firmware.
A common approach is to expose a simple command interface over UART:
void shell_cmd_log_level(const char* module, const char* level) {
micros_log_level_t new_level = parse_level(level);
log_set_module_level(module, new_level);
I("Log level for %s set to %s", module, level);
}
This requires a runtime table of modules:
typedef struct {
const char* name;
micros_log_level_t level;
} micros_log_module_t;
static micros_log_module_t modules[] = {
{ "main", MICROS_LOG_LEVEL_INFO },
{ "boot", MICROS_LOG_LEVEL_INFO },
{ "scheduler", MICROS_LOG_LEVEL_DEBUG },
};
Now you can type:
log set scheduler info
and the scheduler module will quiet down without reflashing.
Multiple Backends
Today, _log() always writes to stdout/stderr. But what if you want:
- UART output for normal use,
- SEGGER RTT for fast debugging,
- or a ring buffer in RAM that you can dump on a crash?
We can abstract the backend:
typedef void (*log_backend_fn)(const char* formatted);
static log_backend_fn current_backend = uart_backend;
void log_set_backend(log_backend_fn backend) {
current_backend = backend;
}
Now _log() just formats the string and hands it off:
current_backend(buffer);
Switching from UART to RTT is a single call.
Adding Timestamps
Logs are much more useful if you know when they happened. Since MicrOS already has a system tick counter, we can prepend it:
uint32_t micros_log_timestamp(void) {
extern volatile uint32_t tick_counter;
return tick_counter;
}
fprintf(output, "[%s] [%s] [%lu ms] ", level_str, module_name,
micros_log_timestamp());
Output in QEMU might look like:
[I] [main] [123 ms] System is up and running!
Structured Logs
For humans, colors and text are great. But for automated testing, it’s often better to have structured logs.
Imagine this in JSON:
{
"level": "INFO",
"module": "boot",
"file": "boot.c",
"line": 120,
"time_ms": 456,
"message": "Main function returned with code 0"
}
This could be streamed over UART, captured in CI, and parsed by test scripts.
You might even use CBOR for a binary-compact version.
Vision: Logging as Instrumentation
At this point, MicrOS logging isn’t just about debugging. It becomes:
- A diagnostic tool during bring-up.
- A runtime control knob for adjusting verbosity.
- A data channel for structured system telemetry.
For a small RTOS, this is incredibly powerful — and all of it builds on the simple foundations we laid in Part 1.
Wrapping Up the Series
Over four parts, we went from nothing to a flexible, colorful logging framework:
- Why Logging Matters – log levels, module registration.
- Coloring and Structuring – severity tags, VT100 colors, file/line info.
- Filtering – compile-time ceiling and per-module verbosity.
- Extending – runtime control, backends, timestamps, structured logs.
The best part is: the core API is still lightweight. On a Cortex-M, if you set CONFIG_MICROS_LOG_LEVEL=ERROR, all the debug/info/warn macros disappear completely.
What’s Next?
- Add a simple UART shell for changing log levels at runtime.
- Implement a ring buffer backend for post-mortem debugging.
- Experiment with structured JSON/CBOR logs for CI integration.
And of course — keep testing in QEMU and on real boards to refine performance.
👉 That’s the end of this series, but the beginning of MicrOS logging as a serious tool. If you want to dig into the code, experiment with your own backends, or suggest features, contributions are welcome!