Skip to content

logger: apply a log config file to the live logger without a restart - #1791

Open
cosmin-staicu wants to merge 1 commit into
livekit:mainfrom
UiPath:feat/logger-config-file-watch
Open

cosmin-staicu wants to merge 1 commit into
livekit:mainfrom
UiPath:feat/logger-config-file-watch

Conversation

@cosmin-staicu

@cosmin-staicu cosmin-staicu commented Sep 10, 2026 •

Copy link
Copy Markdown

Set LK_LOG_CONFIG_PATH to a YAML file and the logger applies its level and component_levels to the running process, so a log level can change without a restart.

Why

The parts for this are already in logger. The root level and every component level are zap.AtomicLevel, the root ComponentLeveler resolves them through Config, and FromZapLogger registers a Config observer that refreshes the leveler. Nothing ever changed a level on a running process, though, so a level change still meant restarting it and losing the state you wanted to look at.

What

WatchConfigFile polls the file every 30s (LK_LOG_CONFIG_INTERVAL overrides that) and applies it to the Config the service holds. The hook is in FromZapLogger, which every consumer reaches (livekit-server through InitFromConfig, livekit-sip through NewZapLogger), so no binary needs its own flag or call site. It starts once, for the first logger the process builds. With the env var unset, nothing changes.

It polls instead of using fsnotify because the usual target is a mounted ConfigMap: kubelet swaps the ..data symlink rather than rewriting the file, so a watch on the file never fires.

Only level and component_levels are taken. The rest of the logging block (json, sample and the sampler settings) is read once when a logger is built, so changing it on a running process would do nothing. The file is decoded strictly, and a file that names any other key is rejected with a warning instead of being half applied.

The file is an overlay on the startup config, not on whatever was applied last. WatchConfigFile snapshots the config once at startup and applies every file over that baseline. A level the file omits falls back to its startup value, and emptying the file to {} restores the levels the process booted with. component_levels entries merge over the startup ones, so an entry the file doesn't name, such as the pion_level livekit-server sets, keeps its startup value, and an entry that disappears from the file stops applying.

Worth a look

Levels are written through a new Config.updateLevels rather than Config.Update. Update assigns every field, so applying a partial file through it would reset the rest, and it would rewrite the item sampler settings that WithItemSampler reads without Config's lock. updateLevels sets the two level fields under the lock and notifies the same observers. The read side needs no change: the leveler resolves through Config.ResolveComponentLevel, which takes the same lock.

An unreadable file (an optional ConfigMap that isn't mounted yet), unchanged bytes and a rejected file all leave the config in force alone. A rejected file is reported once when it appears, not on every poll. A file that goes away isn't treated as a reset, because a transient read error would then flap levels on a live process. To reset, empty the file to {}.

Tests are in logger/configwatch_test.go.

@changeset-bot

changeset-bot Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 907d9f7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
github.com/livekit/protocol Patch
@livekit/protocol Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…start

The pieces for changing levels at runtime were already here — every component
level is a zap.AtomicLevel held by the root ComponentLeveler, and FromZapLogger
registers a Config observer that refreshes the leveler — but nothing ever
changed a level on a running process, so a level change meant restarting it.

Add the missing trigger: when LK_LOG_CONFIG_PATH is set, poll that file and
apply its level and component_levels to the Config the service is holding
(LK_LOG_CONFIG_INTERVAL overrides the 30s default). The hook sits in
FromZapLogger, which is the one path every consumer reaches — livekit-server
via InitFromConfig, livekit-sip via NewZapLogger — so no binary needs its own
flag or call site.

Polling rather than fsnotify because the target is a mounted ConfigMap: kubelet
swaps the ..data symlink instead of rewriting the file, so a watch on the file
never fires.

Only those two keys are taken. The rest of the logging block (json, sample, the
sampler settings) is read once when a logger is built, and WithItemSampler reads
the item sampler settings without Config's lock, so rewriting them on a running
process would change nothing and race that read. The file is decoded strictly,
so one that names any other key is rejected with a warning rather than half
applied, and the levels are written through Config.updateLevels, which touches
nothing else. The read side needs nothing new: the leveler resolves through
Config.ResolveComponentLevel, which takes the same lock.

The file is a declarative overlay on the startup config, not on whatever was
applied last: WatchConfigFile snapshots the config once at startup and every
file is applied over that baseline, so a level the file omits falls back to its
startup value and emptying the file to `{}` restores the levels the process
booted with. component_levels entries merge over the startup ones, so an entry
the file does not name — including pion_level, which livekit-server puts there —
keeps its startup value, and one that disappears from the file stops applying.

An unreadable file (an optional ConfigMap not yet mounted), unchanged bytes and
a rejected file all leave the config in force untouched, and a rejected file is
reported once when it appears rather than on every poll. A file that goes away
is deliberately not a reset — a transient read error would otherwise flap levels
on a live process; emptying it to `{}` is the reset.

Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
@cosmin-staicu
cosmin-staicu force-pushed the feat/logger-config-file-watch branch from 907d9f7 to 7948c85 Compare September 24, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant