Building a Lightweight Logging Framework for MicrOS – Part 2: Coloring and Structuring Logs

In Part 1 of this series, I explained why logging matters for MicrOS and how we started with a simple design: log levels, a global ceiling, and per-module registration. That gave us the skeleton of a logging system — but the output was still plain and hard to scan.

In this part, we’ll bring logs to life by adding VT100 colors, severity tags, and structured formatting. This way, you can spot errors immediately, follow debug traces line by line, and keep your terminal readable even when dozens of modules are talking at once.


Adding Color with VT100 Escape Codes

Most serial terminals and QEMU consoles understand VT100 escape codes. These are simple strings that tell the terminal to switch text color. For example:

  • \033[31m → red
  • \033[32m → green
  • \033[33m → yellow
  • \033[34m → blue
  • \033[0m → reset

We map each severity level to a color:

#define MICROS_LOG_ESCAPE_CODE_COLOR_RED    "\033[31m"
#define MICROS_LOG_ESCAPE_CODE_COLOR_GREEN  "\033[32m"
#define MICROS_LOG_ESCAPE_CODE_COLOR_YELLOW "\033[33m"
#define MICROS_LOG_ESCAPE_CODE_COLOR_BLUE   "\033[34m"
#define MICROS_LOG_ESCAPE_CODE_COLOR_RESET  "\033[0m"

Errors stand out in red, warnings in yellow, informational logs in green, and debug traces in blue.


Formatting Messages

The _log function now does three things:

  1. Selects the color and severity tag (E, W, I, D).
  2. Prints either a compact message (for info/warn/error) or a detailed trace (for debug).
  3. Resets the color so the terminal stays clean.

Here’s the core logic:

switch (level) {
    case MICROS_LOG_LEVEL_ERROR:
        color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_RED;
        level_str = "E";
        break;
    case MICROS_LOG_LEVEL_WARN:
        color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_YELLOW;
        level_str = "W";
        break;
    case MICROS_LOG_LEVEL_INFO:
        color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_GREEN;
        level_str = "I";
        break;
    case MICROS_LOG_LEVEL_DEBUG:
        color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_BLUE;
        level_str = "D";
        break;
}

For debug logs, we include file and line:

fprintf(output, "[%s] [%s] --%s:%d-- ",
        level_str, module_name, file, line);

For everything else:

fprintf(output, "[%s] [%s] ", level_str, module_name);

Finally, we call vfprintf() with the user’s message and reset the color:

vfprintf(output, fmt, args);
fprintf(output, MICROS_LOG_ESCAPE_CODE_COLOR_RESET "\n");

Real Logs in Action

Here’s what it looks like when running the init_functions sample in QEMU:

[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:22-- System clock initialized - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:26-- UART initialized - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:31-- System initialized successfully - simulated
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:35-- Very early init function in section .init_array.50 - simulated
[I] [main] MicrOS Initialization Framework Example
[I] [main] System is up and running!
[I] [main] Main function is exiting now.
[I] [boot] Main function returned with code 0
[D] [main] --/home/grzegorz/projects/micros/samples/init_functions/main.c:39-- Very late fini function in section .fini_array - simulated
[I] [boot] System halted after main return

In a terminal, those lines appear:

  • Blue for debug [D] (with file and line).
  • Green for info [I].
  • Yellow for warnings [W].
  • Red for errors [E].

This makes it instantly obvious what’s routine, what’s important, and what’s catastrophic.


Why File and Line for Debug?

You’ll notice that only debug logs show the full --file:line-- prefix. That’s intentional:

  • Debugging sessions often require pinpoint accuracy — “which path hit this code?”
  • Normal operation should remain readable and concise.

This balance keeps everyday logs short, while still letting you crank up detail when hunting a bug.


Where We Stand

At this point, our logging system has:

  • Severity levels (E, W, I, D).
  • VT100 colors for instant readability.
  • File/line context for debug logs.
  • Clean reset after each line.

Already, this makes MicrOS much easier to work with than raw printf. But we’re not done yet.


Coming Up Next

In Part 3, we’ll add compile-time and per-module filtering:

  • A global ceiling (CONFIG_MICROS_LOG_LEVEL).
  • MICROS_LOG_REGISTER(module, level) for per-module defaults.
  • Macros like D(), I(), W(), and E() that automatically check both.

That’s where logging really starts to feel powerful: each part of the OS can speak at its own volume, and you can globally tune the noise.


👉 Stay tuned for Part 3: Compile-Time and Per-Module Filtering — where we turn the logging API into something you can really depend on in production.

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