SDK
klog
The one log front end of the Kode Dot SDK: KLOG macros, five levels, per-tag filters, ESP-IDF capture, and the rescue path for interrupts and crashes.
What it is
klog is the one place where the SDK writes a log line. No component calls printf or ESP_LOGx. Code calls the KLOG_* macros, and klog decides the shape of the line, whether it shows, and where it goes. The log lines of ESP-IDF, LVGL and the vendor drivers are captured and printed again in the same shape, so every line on the console looks alike.
Each line has a level and a tag. The level says how important the line is, and the tag names the component that wrote it. A line shows when its level is at or above the minimum of its tag, which is one >= comparison and nothing else.
NOTICE sits between INFO and WARN. A NOTICE line is one that a person who only types commands wants to see:
- the outcome of a request (installed, mounted, joined)
- a reading that a command asked for
- a change in the state of the system, such as the boot banner
INFO keeps the rest. A console set to n shows the outcomes and the trouble without the chatter.
How it is connected
flowchart LR
subgraph src["Who logs"]
macro["KLOG_x macro"]
idf["ESP-IDF and<br/>LVGL logs"]
end
subgraph klog["Inside klog"]
va["kode_log_va<br/>filter by tag"]
cap["klog_espidf_vprintf<br/>re-parse"]
emit["klog_emit<br/>256-byte line"]
out{"klog_out<br/>can it post?"}
end
macro --> va
idf --> cap
cap --> emit
va --> emit
emit --> out
out -- "yes" --> sink["Sink<br/>LOG_LINE_SIG"]
out -- "no" --> rescue["kode_log_emergency<br/>esp_rom_printf"]
sink --> log["AO_Log"]
class macro app
class log aoklog formats the line in the task of the caller. AO_Log, the owner of the console, writes the bytes. The layers below the Active Objects also log, and a lower layer cannot call a higher one, so AO_Log hands klog the address of one function when it starts (kode_log_set_sink()). klog calls that function without knowing who owns it. sdk/kaos/log describes the other side.
klog_out() checks whether the code can post an event right now. If it cannot, the caller prints the line at once, with no event and no queue. That happens in three cases:
- No sink is registered yet (the first lines of the boot).
- The code runs in an interrupt.
- The scheduler is not running.
In all three the line still goes out.
klog depends on nothing of the SDK. It requires ESP-IDF's log and esp_timer privately. khal and the drivers log through it, which is why it sits below them.
Specifications
The line
A call such as KLOG_I(tag, "text") prints one line:
!<L> <when> <tag> <message>
The parts of that line:
| Part | Meaning |
|---|---|
! | The mark of a line the host did not ask for. Every reply to a command starts with > |
<L> | One of D I N W E |
<when> | Milliseconds since boot, as 32 bits, until the wall clock holds a time after 2026-01-01 (KODE_CLOCK_KNOWN_AFTER_S). From then on, UTC to the millisecond, such as 2026-09-03T21:09:44.585Z. Milliseconds wrap after about 49 days |
<tag> | The component that wrote it |
| Length | KLOG_LINE_MAX 256 bytes, terminator included. A longer line is cut, and it still ends in \n so the next record is not joined to it |
| Colour | Off by default. When on, one prefix and one suffix wrap the whole line: DEBUG magenta, INFO green, NOTICE cyan, WARN yellow, ERROR red. A cut line loses its colour reset |
The levels
The five levels, plus the filter-only value that drops everything.
| Level | Value | Letter | Use |
|---|---|---|---|
KODE_LOG_DEBUG | 0 | D | Bench detail |
KODE_LOG_INFO | 1 | I | What happened, for someone reading later |
KODE_LOG_NOTICE | 2 | N | Outcomes, answers and state changes |
KODE_LOG_WARN | 3 | W | Wrong but survivable |
KODE_LOG_ERROR | 4 | E | The call failed and somebody has to know |
KODE_LOG_OFF | 5 | none | A filter only: drops every line |
KODE_LOG_LEVEL_WORDS is the console's word for each level, d i n w e off, in the same order.
Where a line is filtered
A line meets five filters, from the build to the run time.
| Where | What decides | Set by |
|---|---|---|
| Build time | CONFIG_KLOG_MAXIMUM_LEVEL, default DEBUG. A KLOG_x below it compiles to kode_log_nop(): no format string in flash, and nothing can turn it back on | Kconfig choice, "Most verbose klog level compiled in" |
| Start of a boot | CONFIG_KLOG_DEFAULT_LEVEL, default INFO. It cannot be more verbose than the ceiling. OFF means answers only | Kconfig choice, "Level the console starts at" |
| Run time, global | kode_log_set_level(NULL, min), the console's config key="log". AO_Log stores it for the next boot | The console |
| Run time, one tag | kode_log_set_level(tag, min), the console's log_level tag="…". Not stored | The console |
| A hold | kode_log_hold(tag, min) puts a floor under one tag. The effective minimum is the stricter of the tag's level and its hold | A driver |
ESP-IDF's own tag filter moves with kode_log_set_level(). ESP-IDF has no NOTICE, so at NOTICE its filter is WARN: its INFO lines are hidden and its warnings show.
The DEBUG lines of ESP-IDF stay compiled in, so config key="log" value=d turns them on too. The bootloader prints nothing, because the build sets it silent.
Console replies, the > lines, never pass through the tag filter. That is why log_level can always answer.
The calls
The public calls and macros of klog.
| Call | What it does |
|---|---|
kode_log_init() | Captures the ESP-IDF log with esp_log_set_vprintf(). First in kode_init(), needs no scheduler. Logs kodeOS logging up (ESP-IDF stream captured) |
KLOG_D, KLOG_I, KLOG_N, KLOG_W, KLOG_E (tag, fmt, ...) | A line at one level. An expression, so it works wherever a call does |
kode_log(lvl, tag, fmt, ...), kode_log_va(lvl, tag, fmt, ap) | What the macros call. Drops the line for a NULL tag or format, a level past ERROR (OFF included), or a level under the tag's minimum |
kode_log_always(lvl, tag, fmt, ...) | A line the filters do not hold back, except a global OFF. For the boot banner (NOTICE) and the slow-step warning of the supervisor (WARN) |
kode_log_set_level(tag, min) | The filter of one tag, or the global one for NULL |
kode_log_get_level(), kode_log_build_level() | The global level now, and the level the build starts at. info compares them to print source=stored or source=default |
kode_log_hold(tag, min), kode_log_release(tag) | Holds a tag at min or quieter. false for a NULL tag or a full table. The tag must outlive the hold, so pass a literal |
kode_log_set_color(enable) | ANSI colour on or off |
kode_log_reply(line), kode_log_reply_src(line, src) | One reply line of the console protocol, verbatim, with no filter, stamp or colour. src is the port of the command answered, or KODE_LOG_SRC_NONE (0xFF) |
kode_log_set_sink(sink) | Registers the function that takes finished lines (line, len, src), or NULL to fall back to the rescue path. The line is valid only during the call |
kode_log_emergency(line) | The rescue path |
The rescue path
kode_log_emergency() writes through esp_rom_printf, the printf in the ROM of the chip. It uses no newlib, no lock and no allocation.
It exists for three cases: before QF runs, inside an interrupt, and inside a QP assert. A QP assert is raised from a critical section, where stdio takes a recursive lock and ESP-IDF answers with a bare abort(), so the message says nothing about the real failure. The assert hook in kqp calls khal_panic_print(), which prints the same way and formats the text.
The rescue path writes around AO_Log, so a line that goes out this way can land in the middle of the redraw of the prompt, or be cut in half, even when nothing is wrong.
Limits
The sizes klog is built with.
| Constant | Value | Meaning |
|---|---|---|
KLOG_LINE_MAX | 256 | One formatted line |
KLOG_TAG_TABLE_SZ | 16 | Per-tag overrides |
KLOG_TAG_LEN | 16 | A tag in the table, terminator included: 15 characters |
KODE_LOG_HOLD_MAX | 4 | Tags held at once |
KLOG_STAMP_LEN | 28 | A timestamp. The ISO form needs 25 |
KLOG_ESP_TAG_LEN | 32 | A tag parsed out of an ESP-IDF line |
A line from ESP-IDF is parsed back from the form I (1234) tag: message. The first letter gives the level (E, W, I, D, and V as DEBUG). A line that does not fit the form goes out at INFO under the tag esp, with the whole line as the message. Trailing \r and \n are removed.
The files
The four files of the layer.
| File | What it does |
|---|---|
include/kode_log.h | The public interface: levels, macros, filter, hold, sink, rescue path and KODE_CLOCK_KNOWN_AFTER_S |
src/klog.c | The formatter, the stamp, the per-tag table, the holds, the colour and the capture of the ESP-IDF stream |
Kconfig | The build ceiling and the starting level |
CMakeLists.txt | Registers klog.c; requires log and esp_timer privately |
Gotchas
Example code
Write one line at every level under one tag, then raise the filter of that tag to WARN and write them again. With the log left at its default level, the first round shows four lines (DEBUG is under INFO) and the second shows two.
/*
* log_levels.c - 2026-10-28 - from Kode with love
*
* Writes one line at every klog level under the tag "demo", raises the filter
* of that tag to WARN, and writes them again. At the default level (INFO) the
* serial console of the Kode Dot shows the I, N, W and E lines of the first
* round, then only the W and E lines of the second.
*/
#include "kode.h"
#define DEMO_TAG "demo"
/* Write one line at each of the five levels. */
static void log_every_level(const char *round)
{
KLOG_D(DEMO_TAG, "%s: debug, bench detail", round);
KLOG_I(DEMO_TAG, "%s: info, what happened", round);
KLOG_N(DEMO_TAG, "%s: notice, an outcome", round);
KLOG_W(DEMO_TAG, "%s: warn, wrong but survivable", round);
KLOG_E(DEMO_TAG, "%s: error, somebody has to know", round);
}
/* Bring the system up, log twice, and change the tag's filter in between. */
void app_main(void)
{
kode_init();
log_every_level("first");
kode_log_set_level(DEMO_TAG, KODE_LOG_WARN);
log_every_level("second");
}Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.
In the SDK
components/klog: the whole layer.kode_log.h: the interface, with the contract of every call.components/kaos/log: AO_Log, the owner of the sink and the console.