Building a Lightweight Logging Framework for MicrOS – Part 1: Why Logging Matters in an RTOS

When working with microcontrollers, debugging can feel like peering through a keyhole. Unlike desktop development, there’s no rich OS to lean on, no debugger always attached, and no full-featured console to tell you what went wrong. If your system crashes, often the only trace left is whatever text you managed to push out before everything froze.

That’s why a good logging framework is one of the first things I wanted in MicrOS — my small embedded operating system. In this series, I’ll walk you through how I built a lightweight but flexible logging API for MicrOS, starting from the simplest ideas and growing it into a colorful, structured system with per-module filtering and runtime flexibility.


Why Not Just printf?

The simplest way to log on a microcontroller is to sprinkle printf() calls wherever you need to see something. And in very small projects, that works fine. But once you scale up, it quickly becomes a problem:

  • No severity levels: You can’t tell “debug spam” apart from real errors.
  • No filtering: When you want only errors, you still get everything.
  • Runtime cost: Even disabled printfs eat cycles and code space.
  • Poor readability: Raw strings are hard to scan when things get busy.

In an RTOS like MicrOS, I wanted something closer to what you’d expect in a professional system:

  • Control over how much detail I see (globally and per module).
  • Clear, structured messages with severity and source information.
  • Zero cost when logs are disabled.
  • The ability to eventually add color, timestamps, and different output backends.

Design Goals for MicrOS Logging

I set out a few rules for the system:

  1. Global configuration – one macro defines the maximum log level for the whole OS (CONFIG_MICROS_LOG_LEVEL).
  2. Per-module configuration – each source file can declare its own verbosity (MICROS_LOG_REGISTER(main, MICROS_LOG_LEVEL_INFO)).
  3. Convenient macros – log with E("..."), W("..."), I("..."), D("...").
  4. Minimal overhead – if a log is above the global level, it gets compiled out.
  5. Clarity – each message should clearly show severity, module, and (when needed) file/line.

The First Building Block: Log Levels

At the heart of the API is a simple enum:

typedef enum {
    MICROS_LOG_LEVEL_NONE  = 0,
    MICROS_LOG_LEVEL_ERROR = 1,
    MICROS_LOG_LEVEL_WARN  = 2,
    MICROS_LOG_LEVEL_INFO  = 3,
    MICROS_LOG_LEVEL_DEBUG = 4
} micros_log_level_t;

This gives us a spectrum from completely silent (NONE) to full verbosity (DEBUG).

A global ceiling is set at build time:

#define CONFIG_MICROS_LOG_LEVEL MICROS_LOG_LEVEL_DEBUG

So if you compile with INFO, you’ll never even build debug messages into your binary.


A Simple Logging Function

Before adding colors or fancy formatting, we just need a function to send messages out:

void _log(micros_log_level_t level,
          const char* module_name,
          const char* file,
          int line,
          const char* fmt,
          ...);

This function will later handle formatting and writing to stdout/stderr. For now, it’s just a placeholder we can build around.


Registering a Module

Each C file that wants to log needs to “introduce itself”:

MICROS_LOG_REGISTER(main, MICROS_LOG_LEVEL_INFO);

This macro stores the module’s name ("main") and its default verbosity. It means we can later filter logs differently per module.


Using the API

Once a module is registered, logging is as easy as:

I("System is starting up...");
E("Fatal error: %d", code);

The API will decide (based on global and module levels) whether to actually print the message.


What’s Next

So far we’ve defined:

  • The log levels.
  • The log function prototype.
  • The per-module registration.

That’s enough to sketch out the framework, but it doesn’t yet make logs easy to read. Everything is still monochrome, and debug logs don’t tell you where they came from.

In Part 2, we’ll fix that: we’ll add structured formatting and VT100 colors, so logs pop off the screen and errors can’t be missed.


👉 Stay tuned for Part 2: Coloring and Structuring Logs.

Grzegorz Grzęda
Grzegorz Grzęda

At G2Labs, Grzegorz Grzęda develops IoT and embedded platforms while building MicrOS in public and sharing hands-on tutorials.

Articles: 10

Newsletter Updates

Enter your email address below and subscribe to our newsletter