# 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

```mermaid
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:

- 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

> **WARNING**
>
> **Do not call `printf` or `KLOG_x` from an interrupt or a QP assert.** stdio takes a lock there.
>
> - In an interrupt, call `kode_log_emergency()`. `kode_log()` also falls back to it, but it formats first and is not allowed in an interrupt that runs with the flash cache off.
> - In an assert, call `khal_panic_print()`, which handles only `%s %d %u %x %c`, with no floats and no widths.

> **NOTE**
>
> **A line below the build ceiling still runs its arguments.** The macro turns into `kode_log_nop()`, which drops the format string from the image, but the compiler keeps evaluating and type-checking the arguments. Put no side effect in a log argument.

> **WARNING**
>
> **A tag longer than 15 characters sets nothing.** `kode_log_set_level()` refuses it, because a cut tag would never match the tag it came from, and logs a warning under the tag `klog`. The console's `log_level` still answers `>ok`, so that warning is the only sign.

> **NOTE**
>
> **A hold protects a secret, and nothing else lowers it.** At DEBUG, ESP-IDF's HTTP client writes the headers of a request, the bearer token among them. `kdrv_net` holds `HTTP_CLIENT` at INFO for each `PUT`. Neither `config key="log"` nor `log_level` goes under a hold. ESP-IDF forgets its per-tag levels when the global level changes, but a hold belongs to klog and survives that.

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

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/klog): the whole layer.
- [`kode_log.h`](https://github.com/kodediy/kode-sdk/blob/main/components/klog/include/kode_log.h): the interface, with the contract of every call.
- [`components/kaos/log`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/log): AO_Log, the owner of the sink and the console.
