rolled files land in log_dir under their basename (the namer/rotate_on_start basename
them), but prune()/retier() globbed the un-basenamed name. a name like 'sub/run' matched
nothing, so old .log/.gz files piled up forever (slow disk leak; the live file was fine).
basename the stem at the top of both prune() and retier(). regression-verified with
name='sub/run' (10 retained vs 12/dead before). bump v0.4.0 -> v0.4.1
Signed-off-by: disqualifier <dev@disqualifier.me>
- keep_uncompressed/keep_compressed: newest N rolled logs plain, next M gzipped, rest
deleted. applies to on_start/daily/size. opt-in by knob presence; without them the
legacy backup_count + gzip-on-roll path is unchanged (existing consumers unaffected).
- _normalize_name: 'latest' and 'latest.log' both -> live latest.log (no .log.log).
- _gzip_file preserves source mtime (stable tier ordering across re-tiers).
- rotate_on_start collision counter checks both .log and .log.gz (no duplicate logical
roll when a same-stamp file was already compressed).
execute-verified stdlib-only incl. a back-compat control proving the no-knobs path is
unchanged. bump v0.3.2 -> v0.4.0
Signed-off-by: disqualifier <dev@disqualifier.me>
- the non-compress rotator and on_start move fall back to shutil.move when os.replace
hits OSError(EXDEV) across filesystems, so rolls land on a separate logs volume /
container bind-mount instead of failing every rotation via the handler's silent
handleError (L18)
- on_start disambiguates a same-second restart with a numeric counter so a rapid
crash-restart loop doesn't clobber the earlier rolled file (L17)
- reject a bool root level (True==1) consistently with the per-module path; alias the
queue module import to drop the queue:bool param shadow; log (not swallow) a
handler.close failure during re-setup (nits).
Signed-off-by: disqualifier <dev@disqualifier.me>
guard the extra-merge loop with the formatter's own output keys (time/level/module/
message). stdlib LogRecord rejects extra keys colliding with real attribute names, but
time/level are NOT LogRecord attrs, so a caller's extra={"time":...}/{"level":...}
previously overwrote the UTC timestamp / levelname — the two fields Loki/Grafana alert
on. now those keys are reserved and a colliding extra is dropped.
Signed-off-by: disqualifier <dev@disqualifier.me>
add an optional module_levels={logger_name: level} param to setup_logging,
the ergonomic way to quiet noisy dependencies (motor/pymongo/aiohttp -> WARNING)
from the one entry-point call instead of scattering setLevel afterwards.
- exact logger-name match, no discovery; stdlib hierarchy applies so naming a
parent quiets its subtree
- str or int level per entry, same normalization as root level
- bad level for one entry is skipped + warned, never raises (never-crash rule)
- module_levels=None/{} (default) is byte-identical to prior behavior
additive, backwards-compatible -> v0.3.0.
Signed-off-by: disqualifier <dev@disqualifier.me>
add a selectable output format to setup_logging: text (default, human,
local time) stays unchanged; output="json" emits one-JSON-object-per-line
(JSON Lines) for the Grafana/Loki path. json fields are time (UTC ISO-8601
with Z), level, module, message, plus any extra={...} keys surfaced as
top-level fields and a rendered exc_info traceback on error records. both
file and console use the chosen format; the live-file name is unchanged so
the Promtail glob and tail command don't break across text/json. an unknown
output falls back to text and warns, never crashes. stdlib json only, zero
new deps. minor bump to v0.2.0.
Signed-off-by: disqualifier <dev@disqualifier.me>
_level_value used logging.getLevelName(name), which returns the string 'Level XXX'
for an unknown name; that string then reached setLevel() and raised ValueError,
violating the 'never crashes the app over logging' contract. validate the result is
an int and fall back to INFO otherwise.
verified: level='BOGUS' -> INFO (no crash); 'DEBUG' and int levels still honored.
Signed-off-by: disqualifier <dev@disqualifier.me>