From ff29e0532211d0ecf456694de41f4aecd04431af Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 29 Jun 2026 17:11:45 -0400 Subject: [PATCH] fix: JSON extra cannot clobber canonical time/level/module fields (v0.3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 2 +- pyproject.toml | 2 +- src/log_setup/__init__.py | 2 +- src/log_setup/formats.py | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25463e7..efc4e67 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ and emit; their records flow into the handlers `log_setup` wired. ## Install ``` -log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.3.0 +log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.3.1 ``` No dependencies — stdlib only. diff --git a/pyproject.toml b/pyproject.toml index 411437d..498a259 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "log_setup" -version = "0.3.0" +version = "0.3.1" description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format" requires-python = ">=3.10" dependencies = [] diff --git a/src/log_setup/__init__.py b/src/log_setup/__init__.py index c8eb6d6..8633ee5 100644 --- a/src/log_setup/__init__.py +++ b/src/log_setup/__init__.py @@ -19,4 +19,4 @@ from .setup import setup_logging __all__ = ["setup_logging"] -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/src/log_setup/formats.py b/src/log_setup/formats.py index ddd8769..a0e079b 100644 --- a/src/log_setup/formats.py +++ b/src/log_setup/formats.py @@ -15,6 +15,11 @@ DEFAULT_FORMAT = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S" _RESERVED = frozenset(vars(logging.makeLogRecord({})).keys()) | {"message", "asctime"} +# this formatter's own canonical output keys — stdlib's LogRecord rejects `extra` keys +# colliding with real attribute names (e.g. `module`), but `time`/`level` are NOT +# LogRecord attrs, so a caller's extra={"time":...}/{"level":...} would otherwise +# overwrite the UTC timestamp / levelname. guard them explicitly +_OUTPUT_KEYS = frozenset({"time", "level", "module", "message"}) class JsonLinesFormatter(logging.Formatter): @@ -38,7 +43,7 @@ class JsonLinesFormatter(logging.Formatter): "message": record.getMessage(), } for key, value in record.__dict__.items(): - if key not in _RESERVED and not key.startswith("_"): + if key not in _RESERVED and key not in _OUTPUT_KEYS and not key.startswith("_"): payload[key] = value if record.exc_info: payload["exc_info"] = self.formatException(record.exc_info)