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:

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 ao
The path of one log line

klog 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:

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:

PartMeaning
!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
LengthKLOG_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
ColourOff 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.

LevelValueLetterUse
KODE_LOG_DEBUG0DBench detail
KODE_LOG_INFO1IWhat happened, for someone reading later
KODE_LOG_NOTICE2NOutcomes, answers and state changes
KODE_LOG_WARN3WWrong but survivable
KODE_LOG_ERROR4EThe call failed and somebody has to know
KODE_LOG_OFF5noneA 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.

WhereWhat decidesSet by
Build timeCONFIG_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 onKconfig choice, "Most verbose klog level compiled in"
Start of a bootCONFIG_KLOG_DEFAULT_LEVEL, default INFO. It cannot be more verbose than the ceiling. OFF means answers onlyKconfig choice, "Level the console starts at"
Run time, globalkode_log_set_level(NULL, min), the console's config key="log". AO_Log stores it for the next bootThe console
Run time, one tagkode_log_set_level(tag, min), the console's log_level tag="…". Not storedThe console
A holdkode_log_hold(tag, min) puts a floor under one tag. The effective minimum is the stricter of the tag's level and its holdA 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.

CallWhat 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.

ConstantValueMeaning
KLOG_LINE_MAX256One formatted line
KLOG_TAG_TABLE_SZ16Per-tag overrides
KLOG_TAG_LEN16A tag in the table, terminator included: 15 characters
KODE_LOG_HOLD_MAX4Tags held at once
KLOG_STAMP_LEN28A timestamp. The ISO form needs 25
KLOG_ESP_TAG_LEN32A 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.

FileWhat it does
include/kode_log.hThe public interface: levels, macros, filter, hold, sink, rescue path and KODE_CLOCK_KNOWN_AFTER_S
src/klog.cThe formatter, the stamp, the per-tag table, the holds, the colour and the capture of the ESP-IDF stream
KconfigThe build ceiling and the starting level
CMakeLists.txtRegisters 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