logger: apply a log config file to the live logger without a restart - #1791
Open
cosmin-staicu wants to merge 1 commit into
Open
cosmin-staicu wants to merge 1 commit into
cosmin-staicu wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 907d9f7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
cosmin-staicu
force-pushed
the
feat/logger-config-file-watch
branch
2 times, most recently
from
September 10, 2026 08:46
9d16f84 to
907d9f7
Compare
…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
force-pushed
the
feat/logger-config-file-watch
branch
from
September 24, 2026 05:35
907d9f7 to
7948c85
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Set
LK_LOG_CONFIG_PATHto a YAML file and the logger applies itslevelandcomponent_levelsto 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 arezap.AtomicLevel, the rootComponentLevelerresolves them throughConfig, andFromZapLoggerregisters aConfigobserver 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
WatchConfigFilepolls the file every 30s (LK_LOG_CONFIG_INTERVALoverrides that) and applies it to theConfigthe service holds. The hook is inFromZapLogger, which every consumer reaches (livekit-server throughInitFromConfig, livekit-sip throughNewZapLogger), 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
..datasymlink rather than rewriting the file, so a watch on the file never fires.Only
levelandcomponent_levelsare taken. The rest of the logging block (json,sampleand 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.
WatchConfigFilesnapshots 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_levelsentries merge over the startup ones, so an entry the file doesn't name, such as thepion_levellivekit-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.updateLevelsrather thanConfig.Update.Updateassigns every field, so applying a partial file through it would reset the rest, and it would rewrite the item sampler settings thatWithItemSamplerreads withoutConfig's lock.updateLevelssets the two level fields under the lock and notifies the same observers. The read side needs no change: the leveler resolves throughConfig.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.