{"id":161,"date":"2025-09-29T13:49:00","date_gmt":"2025-09-29T11:49:00","guid":{"rendered":"https:\/\/microsproject.dev\/?page_id=161"},"modified":"2025-09-25T13:53:19","modified_gmt":"2025-09-25T11:53:19","slug":"designing-a-lightweight-colorful-logging-framework-for-micros","status":"publish","type":"page","link":"https:\/\/microsproject.dev\/index.php\/tutorials\/designing-a-lightweight-colorful-logging-framework-for-micros\/","title":{"rendered":"Designing a Lightweight, Colorful Logging Framework for MicrOS"},"content":{"rendered":"\n<p>Logging is one of the most important tools in any embedded developer\u2019s toolbox. When you\u2019re working on a microcontroller, debugging often means staring at a serial console and hoping the right message appears before the system locks up. Unlike desktop systems, you don\u2019t have a rich OS with debugging tools, crash dumps, or stack traces. If something goes wrong, the only trace you might have is a few bytes printed out over UART.<\/p>\n\n\n\n<p>That\u2019s why, when building <strong>MicrOS<\/strong> \u2014 a lightweight embedded operating system I\u2019m developing for Cortex-M microcontrollers \u2014 I knew I needed a <strong>logging framework<\/strong>. Not just <code>printf<\/code>, but a system that\u2019s:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Configurable<\/strong>: set global and per-module verbosity.<\/li>\n\n\n\n<li><strong>Readable<\/strong>: color-coded, structured messages with severity levels.<\/li>\n\n\n\n<li><strong>Efficient<\/strong>: compile-time filtering so unused logs cost nothing.<\/li>\n\n\n\n<li><strong>Extensible<\/strong>: ready for timestamps, runtime configuration, and multiple backends.<\/li>\n<\/ul>\n\n\n\n<p>This article walks through the full design of the MicrOS logging system: from the simplest foundations to a colorful, flexible framework you can use on real hardware (or in QEMU). Along the way, I\u2019ll show design trade-offs, implementation details, and real log output captured from the MicrOS samples.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. Why Not Just <code>printf<\/code>?<\/h2>\n\n\n\n<p>The simplest way to get feedback from a microcontroller is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>printf(\"Hello, world!\\n\");\n<\/code><\/pre>\n\n\n\n<p>That works fine for tiny projects, but it doesn\u2019t scale. Some problems with raw <code>printf<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No severity levels<\/strong>: you can\u2019t distinguish debug noise from a fatal error.<\/li>\n\n\n\n<li><strong>No filtering<\/strong>: everything prints, even if you only care about errors.<\/li>\n\n\n\n<li><strong>Runtime overhead<\/strong>: every <code>printf<\/code> call consumes flash, cycles, and UART bandwidth.<\/li>\n\n\n\n<li><strong>Poor readability<\/strong>: long messages quickly become a wall of text.<\/li>\n<\/ul>\n\n\n\n<p>On an embedded OS, especially one that runs multiple modules (kernel, scheduler, drivers, app code), you need something better. You need <strong>structured, filterable logs<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Step One: Defining Log Levels<\/h2>\n\n\n\n<p>At the core of MicrOS logging is a simple enum:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>typedef enum {\n    MICROS_LOG_LEVEL_NONE  = 0,\n    MICROS_LOG_LEVEL_ERROR = 1,\n    MICROS_LOG_LEVEL_WARN  = 2,\n    MICROS_LOG_LEVEL_INFO  = 3,\n    MICROS_LOG_LEVEL_DEBUG = 4\n} micros_log_level_t;\n<\/code><\/pre>\n\n\n\n<p>This gives us a hierarchy:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>NONE<\/code>: silent.<\/li>\n\n\n\n<li><code>ERROR<\/code>: only critical problems.<\/li>\n\n\n\n<li><code>WARN<\/code>: problems that don\u2019t halt the system.<\/li>\n\n\n\n<li><code>INFO<\/code>: normal operational messages.<\/li>\n\n\n\n<li><code>DEBUG<\/code>: detailed tracing for development.<\/li>\n<\/ul>\n\n\n\n<p>At build time, you select a <strong>global ceiling<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#define CONFIG_MICROS_LOG_LEVEL MICROS_LOG_LEVEL_DEBUG\n<\/code><\/pre>\n\n\n\n<p>This ensures that higher-verbosity logs (<code>DEBUG<\/code>, <code>INFO<\/code>) can be completely compiled out if you don\u2019t need them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. Module Registration<\/h2>\n\n\n\n<p>Each module (source file) needs to \u201cintroduce itself\u201d to the logging system. For that, we use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#define MICROS_LOG_REGISTER(module_name, level)                \\\n    static const char* _micros_log_module_name = #module_name; \\\n    static const micros_log_level_t _micros_log_level = level\n<\/code><\/pre>\n\n\n\n<p>For example, in the MicrOS boot code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>MICROS_LOG_REGISTER(boot, MICROS_LOG_LEVEL_INFO);\n<\/code><\/pre>\n\n\n\n<p>This means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Logs from this file will be tagged with <code>boot<\/code>.<\/li>\n\n\n\n<li>The default verbosity for this file is <code>INFO<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Another module could set itself to <code>DEBUG<\/code> if it\u2019s more chatty.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. The Logging Function<\/h2>\n\n\n\n<p>All log macros eventually funnel into <code>_log<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void _log(micros_log_level_t level,\n          const char* module_name,\n          const char* file,\n          int line,\n          const char* fmt,\n          ...);\n<\/code><\/pre>\n\n\n\n<p>This function is responsible for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Formatting the message.<\/li>\n\n\n\n<li>Adding severity tags and module names.<\/li>\n\n\n\n<li>Coloring the output.<\/li>\n\n\n\n<li>Writing to <code>stdout<\/code> or <code>stderr<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Here\u2019s a simplified version of the implementation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#define MICROS_LOG_ESCAPE_CODE_COLOR_RED    \"\\033&#91;31m\"\n#define MICROS_LOG_ESCAPE_CODE_COLOR_GREEN  \"\\033&#91;32m\"\n#define MICROS_LOG_ESCAPE_CODE_COLOR_YELLOW \"\\033&#91;33m\"\n#define MICROS_LOG_ESCAPE_CODE_COLOR_BLUE   \"\\033&#91;34m\"\n#define MICROS_LOG_ESCAPE_CODE_COLOR_RESET  \"\\033&#91;0m\"\n\n#define MICROS_LOG_LEVEL_STRING_ERROR \"E\"\n#define MICROS_LOG_LEVEL_STRING_WARN  \"W\"\n#define MICROS_LOG_LEVEL_STRING_INFO  \"I\"\n#define MICROS_LOG_LEVEL_STRING_DEBUG \"D\"\n\nvoid _log(micros_log_level_t level,\n          const char* module_name,\n          const char* file,\n          int line,\n          const char* fmt,\n          ...) {\n    const char* level_str = \"\";\n    const char* color_escape_str = \"\";\n    FILE* output = (level == MICROS_LOG_LEVEL_ERROR) ? stderr : stdout;\n\n    switch (level) {\n        case MICROS_LOG_LEVEL_ERROR:\n            color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_RED;\n            level_str = MICROS_LOG_LEVEL_STRING_ERROR;\n            break;\n        case MICROS_LOG_LEVEL_WARN:\n            color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_YELLOW;\n            level_str = MICROS_LOG_LEVEL_STRING_WARN;\n            break;\n        case MICROS_LOG_LEVEL_INFO:\n            color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_GREEN;\n            level_str = MICROS_LOG_LEVEL_STRING_INFO;\n            break;\n        case MICROS_LOG_LEVEL_DEBUG:\n            color_escape_str = MICROS_LOG_ESCAPE_CODE_COLOR_BLUE;\n            level_str = MICROS_LOG_LEVEL_STRING_DEBUG;\n            break;\n        default:\n            return;  \/\/ Invalid log level\n    }\n\n    fprintf(output, color_escape_str);\n    if (level != MICROS_LOG_LEVEL_DEBUG) {\n        fprintf(output, \"&#91;%s] &#91;%s] \", level_str, module_name);\n    } else {\n        fprintf(output, \"&#91;%s] &#91;%s] --%s:%d-- \", level_str, module_name, file, line);\n    }\n\n    va_list args;\n    va_start(args, fmt);\n    vfprintf(output, fmt, args);\n    va_end(args);\n\n    fprintf(output, MICROS_LOG_ESCAPE_CODE_COLOR_RESET \"\\n\");\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. Logging Macros<\/h2>\n\n\n\n<p>To make logging ergonomic, we define macros:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#if CONFIG_MICROS_LOG_LEVEL &gt;= MICROS_LOG_LEVEL_DEBUG\n#define D(...) do { \\\n    if (_micros_log_level &gt;= MICROS_LOG_LEVEL_DEBUG) { \\\n        _log(MICROS_LOG_LEVEL_DEBUG, _micros_log_module_name, __FILE__, __LINE__, __VA_ARGS__); \\\n    } \\\n} while (0)\n#else\n#define D(...) do {} while (0)\n#endif\n<\/code><\/pre>\n\n\n\n<p>The same pattern applies to <code>I()<\/code>, <code>W()<\/code>, and <code>E()<\/code>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <strong>global level<\/strong> (<code>CONFIG_MICROS_LOG_LEVEL<\/code>) controls which macros even exist.<\/li>\n\n\n\n<li>The <strong>module level<\/strong> (<code>_micros_log_level<\/code>) controls runtime filtering within that range.<\/li>\n<\/ul>\n\n\n\n<p>This two-layer system gives you fine-grained control with zero overhead for disabled logs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. Real Output from QEMU<\/h2>\n\n\n\n<p>Here\u2019s a real run of the <strong><code>init_functions<\/code><\/strong> sample in QEMU:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;D] &#91;main] --\/home\/grzegorz\/projects\/micros\/samples\/init_functions\/main.c:22-- System clock initialized - simulated\n&#91;D] &#91;main] --\/home\/grzegorz\/projects\/micros\/samples\/init_functions\/main.c:26-- UART initialized - simulated\n&#91;D] &#91;main] --\/home\/grzegorz\/projects\/micros\/samples\/init_functions\/main.c:31-- System initialized successfully - simulated\n&#91;D] &#91;main] --\/home\/grzegorz\/projects\/micros\/samples\/init_functions\/main.c:35-- Very early init function in section .init_array.50 - simulated\n&#91;I] &#91;main] MicrOS Initialization Framework Example\n&#91;I] &#91;main] System is up and running!\n&#91;I] &#91;main] Main function is exiting now.\n&#91;I] &#91;boot] Main function returned with code 0\n&#91;D] &#91;main] --\/home\/grzegorz\/projects\/micros\/samples\/init_functions\/main.c:39-- Very late fini function in section .fini_array - simulated\n&#91;I] &#91;boot] System halted after main return\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Blue<\/strong> debug logs show full <code>--file:line--<\/code> context.<\/li>\n\n\n\n<li><strong>Green<\/strong> info logs are concise.<\/li>\n\n\n\n<li>Errors (red) and warnings (yellow) would stand out immediately.<\/li>\n<\/ul>\n\n\n\n<p>Even in a wall of logs, your eye is drawn to the right severity.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. Why File and Line for Debug?<\/h2>\n\n\n\n<p>One design decision: only <code>DEBUG<\/code> logs include the full <code>--file:line--<\/code> prefix.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Debug sessions demand pinpoint accuracy.<\/li>\n\n\n\n<li>Normal runtime logs should remain short and readable.<\/li>\n<\/ul>\n\n\n\n<p>This keeps output lean, but detailed when you need it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8. Future Extensions<\/h2>\n\n\n\n<p>The current MicrOS logging framework is simple and efficient, but there\u2019s plenty of room to grow.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Runtime Log Level Control<\/h3>\n\n\n\n<p>Add a UART shell command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>log set &lt;module&gt; &lt;level&gt;\n<\/code><\/pre>\n\n\n\n<p>This would let you crank up debug logging on a single module without reflashing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Backends<\/h3>\n\n\n\n<p>Support backends like:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>UART<\/strong> (default).<\/li>\n\n\n\n<li><strong>SEGGER RTT<\/strong> (fast debugging).<\/li>\n\n\n\n<li><strong>Ring buffer in RAM<\/strong> (post-mortem analysis).<\/li>\n<\/ul>\n\n\n\n<p>A simple function pointer swap can choose the backend:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>typedef void (*log_backend_fn)(const char* formatted);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Timestamps<\/h3>\n\n\n\n<p>Prefix logs with a tick counter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;I] &#91;main] &#91;123 ms] System is up and running!\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Structured Logs<\/h3>\n\n\n\n<p>Output JSON or CBOR for automated testing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"level\": \"INFO\",\n  \"module\": \"boot\",\n  \"file\": \"boot.c\",\n  \"line\": 120,\n  \"time_ms\": 456,\n  \"message\": \"Main function returned with code 0\"\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">9. Conclusion<\/h2>\n\n\n\n<p>Logging in an embedded OS isn\u2019t a luxury \u2014 it\u2019s your primary debugging tool. By designing MicrOS logging around <strong>levels, modules, compile-time filtering, and VT100 colors<\/strong>, we now have a system that\u2019s:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Readable<\/strong>: clear severity tags and colors.<\/li>\n\n\n\n<li><strong>Configurable<\/strong>: global and per-module control.<\/li>\n\n\n\n<li><strong>Efficient<\/strong>: disabled logs vanish at compile time.<\/li>\n\n\n\n<li><strong>Extensible<\/strong>: ready for runtime tuning, timestamps, and structured output.<\/li>\n<\/ul>\n\n\n\n<p>From a bare-bones enum to a colorful structured system, MicrOS logging has grown into a framework that makes debugging and development far smoother \u2014 whether on real hardware or inside QEMU.<\/p>\n\n\n\n<p>And the best part: the whole thing is still small, simple, and easy to adapt.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>\ud83d\udc49 Want to dive deeper? The full MicrOS logging code is available in the project repository, along with examples like <code>hello_world<\/code>, <code>init_functions<\/code>, and the boot code.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Logging is one of the most important tools in any embedded developer\u2019s toolbox. When you\u2019re working on a microcontroller, debugging often means staring at a serial console and hoping the right message appears before the system locks up. Unlike desktop systems, you don\u2019t have a rich OS with debugging tools, crash dumps, or stack traces. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":159,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-161","page","type-page","status-publish","hentry"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/pages\/161","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/comments?post=161"}],"version-history":[{"count":1,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/pages\/161\/revisions"}],"predecessor-version":[{"id":162,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/pages\/161\/revisions\/162"}],"up":[{"embeddable":true,"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/pages\/159"}],"wp:attachment":[{"href":"https:\/\/microsproject.dev\/index.php\/wp-json\/wp\/v2\/media?parent=161"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}