From 10a439369362f6645f9b8937e5f2d41243bce427 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:25:34 -0700 Subject: [PATCH 01/10] ci: test Python 3.14 and exercise framework smoke tests --- .github/workflows/code_quality.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 93d37ee..018732b 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v7 with: python-version: 3.9 - name: Install dependencies @@ -36,15 +36,15 @@ jobs: strategy: matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: "Git checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -60,14 +60,15 @@ jobs: strategy: matrix: backend: ["fastapi", "flask", "quart", "sanic"] + python-version: ["3.9", "3.14"] steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v7 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: "Git checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Install dependencies run: | python -m pip install --upgrade pip From 35de14150963f803f29d43985f1991914da51240 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:25:48 -0700 Subject: [PATCH 02/10] ci: collect results from all Python and backend matrix jobs --- .github/workflows/code_quality.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 018732b..fd1bce8 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -35,6 +35,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] @@ -58,6 +59,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: backend: ["fastapi", "flask", "quart", "sanic"] python-version: ["3.9", "3.14"] From 48b9b9d016b5cd1dcd35f22a3327c9c653059279 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:27:23 -0700 Subject: [PATCH 03/10] fix(smoketest): let Quart create its event loop on Python 3.14 --- tests/smoketests/quart/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/smoketests/quart/api.py b/tests/smoketests/quart/api.py index f7728fa..84e5767 100644 --- a/tests/smoketests/quart/api.py +++ b/tests/smoketests/quart/api.py @@ -1,4 +1,4 @@ -import asyncio, logging, sys, json_logging, quart +import logging, sys, json_logging, quart app = quart.Quart(__name__) json_logging.init_quart(enable_json=True) @@ -17,5 +17,5 @@ async def home(): return "Hello world" if __name__ == "__main__": - loop = asyncio.get_event_loop() - app.run(host='0.0.0.0', port=int(5000), use_reloader=False, loop=loop) + # Quart manages its own event loop; get_event_loop() raises without a loop on Python 3.14. + app.run(host='0.0.0.0', port=int(5000), use_reloader=False) From 5da967af1acfa66f9194b5ebbfedf10391d80912 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:29:39 -0700 Subject: [PATCH 04/10] ci: exercise Python 3.14 with supported Sanic dependency --- tests/smoketests/sanic/requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/smoketests/sanic/requirements.txt b/tests/smoketests/sanic/requirements.txt index 4e01c5e..06a78ea 100644 --- a/tests/smoketests/sanic/requirements.txt +++ b/tests/smoketests/sanic/requirements.txt @@ -1,4 +1,7 @@ -sanic==20.3.0 +# Preserve coverage of the legacy Sanic integration on Python 3.9. +sanic==20.3.0; python_version < "3.10" +# Sanic 25.12+ supports Python 3.14; 20.3.0 pulls in httpx importing removed cgi. +sanic>=25.12,<26; python_version >= "3.10" requests pytest -e . From 4042c8c6c66132083bec602c8d642649bc1c2a06 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:31:03 -0700 Subject: [PATCH 05/10] fix(sanic): tolerate missing format keys in modern logging defaults --- json_logging/framework/sanic/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/json_logging/framework/sanic/__init__.py b/json_logging/framework/sanic/__init__.py index 9edba8b..5724b00 100644 --- a/json_logging/framework/sanic/__init__.py +++ b/json_logging/framework/sanic/__init__.py @@ -39,12 +39,12 @@ def config(self): LOGGING_CONFIG_DEFAULTS["formatters"]["generic"][ "class" ] = "json_logging.JSONLogFormatter" - del LOGGING_CONFIG_DEFAULTS["formatters"]["generic"]["format"] + LOGGING_CONFIG_DEFAULTS["formatters"]["generic"].pop("format", None) LOGGING_CONFIG_DEFAULTS["formatters"]["access"][ "class" ] = "json_logging.JSONLogFormatter" - del LOGGING_CONFIG_DEFAULTS["formatters"]["access"]["format"] + LOGGING_CONFIG_DEFAULTS["formatters"]["access"].pop("format", None) # logging.config.dictConfig(LOGGING_CONFIG_DEFAULTS) From a70f63b543060f6fe371b55ab4bdbd02ee106756 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:32:37 -0700 Subject: [PATCH 06/10] docs: reflect Python 3.14 CI coverage --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3261866..0be1019 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,8 +53,9 @@ Key locations: ## Local checks Create and activate a virtual environment before installing test requirements. -The existing CI covers Python 3.9–3.13; this is not a claim that every framework -dependency supports every version. Verify actual installation and runtime. +The CI runs unit tests on Python 3.9–3.14 and framework smoke tests on Python 3.9 and 3.14; +this is not a claim that every framework dependency supports every version. Verify actual +installation and runtime. ```bash python -m pip install -r test-requirements.txt From 17b40989c04f8970abee1c51e6f01fad1d816bd4 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:32:49 -0700 Subject: [PATCH 07/10] docs: align maintenance guide with Python 3.14 matrix --- MAINTENANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 4b52648..f92e377 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -47,7 +47,7 @@ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics ``` The GitHub Actions workflow `.github/workflows/code_quality.yml` runs -unit tests on Python 3.9–3.13, lint, and separate smoke tests for FastAPI, +unit tests on Python 3.9–3.14, lint, and separate smoke tests on Python 3.9 and 3.14 for FastAPI, Flask, Quart, and Sanic. The shared smoke-test runner starts an example service on localhost:5000; that port must be available for local runs. From 382202c87a549104d9101fc96328f16624435778 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:55:46 -0700 Subject: [PATCH 08/10] test(sanic): guard JSON formatter compatibility across logging config shapes --- CHANGELOG.md | 3 ++ tests/test_sanic_logging_config.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_sanic_logging_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9f475..d43e624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). ## Unreleased +- fix: `init_sanic()` no longer raises `KeyError: 'format'` when Sanic 24.x and later omit + formatter `format` keys; older Sanic logging configuration remains supported (PR #133). + - fix: FastAPI application logs and `json_logging.get_correlation_id()` now use the request's correlation id instead of `-` in synchronous (`def`) endpoints (fixes #124, PR #123). When request instrumentation is enabled with `init_request_instrument(app)` (Python 3.9+): diff --git a/tests/test_sanic_logging_config.py b/tests/test_sanic_logging_config.py new file mode 100644 index 0000000..68805cd --- /dev/null +++ b/tests/test_sanic_logging_config.py @@ -0,0 +1,73 @@ +"""Regression tests for Sanic default logging formatter compatibility. + +Sanic 24+ removes the legacy 'format' keys from generic/access formatters. +Run each initialization in a fresh Python process because json_logging.init_sanic() +changes process-global state and intentionally cannot be called twice. +""" +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.parametrize("config_shape", ["legacy", "modern", "installed"]) +def test_init_sanic_handles_logging_formats_and_emits_json(config_shape): + """Both old and new Sanic config shapes initialize and produce parseable JSON records.""" + code = textwrap.dedent(""" + import io + import json + import logging + import sanic.log + import json_logging + + config_shape = %(config_shape)r + if config_shape != "installed": + formatters = { + "generic": {"format": "%%(message)s"} if config_shape == "legacy" else {}, + "access": {"format": "%%(message)s"} if config_shape == "legacy" else {}, + } + sanic.log.LOGGING_CONFIG_DEFAULTS = { + "disable_existing_loggers": True, + "formatters": formatters, + } + + json_logging.init_sanic(enable_json=True) + defaults = sanic.log.LOGGING_CONFIG_DEFAULTS + assert defaults["disable_existing_loggers"] is False + logger = logging.getLogger("sanic-formatter-test") + logger.setLevel(logging.INFO) + logger.propagate = False + + for name in ("generic", "access"): + formatter_config = defaults["formatters"][name] + assert formatter_config["class"] == "json_logging.JSONLogFormatter" + assert "format" not in formatter_config + + output = io.StringIO() + handler = logging.StreamHandler(output) + handler.setFormatter(json_logging.JSONLogFormatter()) + logger.addHandler(handler) + try: + logger.info("sanic json logging", extra={"service": name}) + finally: + logger.removeHandler(handler) + handler.close() + record = json.loads(output.getvalue().strip()) + assert record["type"] == "log" + assert record["msg"] == "sanic json logging" + assert record["service"] == name + print("PASS") + """ %% {"config_shape": config_shape}) + env = dict(os.environ, PYTHONWARNINGS="ignore") + result = subprocess.run( + [sys.executable, "-c", code], cwd=str(ROOT), env=env, + capture_output=True, text=True, timeout=40, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "PASS" in result.stdout From 1402278d38adb0a53d20cb0f0f7ca04a8f1ed69b Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:56:04 -0700 Subject: [PATCH 09/10] test: correct subprocess script interpolation --- tests/test_sanic_logging_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sanic_logging_config.py b/tests/test_sanic_logging_config.py index 68805cd..88ffc8c 100644 --- a/tests/test_sanic_logging_config.py +++ b/tests/test_sanic_logging_config.py @@ -63,7 +63,7 @@ def test_init_sanic_handles_logging_formats_and_emits_json(config_shape): assert record["msg"] == "sanic json logging" assert record["service"] == name print("PASS") - """ %% {"config_shape": config_shape}) + """ % {"config_shape": config_shape}) env = dict(os.environ, PYTHONWARNINGS="ignore") result = subprocess.run( [sys.executable, "-c", code], cwd=str(ROOT), env=env, From 2b29ec2f3cf8c40e555c35f1af2d9d38e9429815 Mon Sep 17 00:00:00 2001 From: suppagoddo Date: Tue, 20 Oct 2026 11:56:41 -0700 Subject: [PATCH 10/10] test(sanic): instantiate the formatter selected by Sanic config --- tests/test_sanic_logging_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_sanic_logging_config.py b/tests/test_sanic_logging_config.py index 88ffc8c..c9117b4 100644 --- a/tests/test_sanic_logging_config.py +++ b/tests/test_sanic_logging_config.py @@ -51,7 +51,8 @@ def test_init_sanic_handles_logging_formats_and_emits_json(config_shape): output = io.StringIO() handler = logging.StreamHandler(output) - handler.setFormatter(json_logging.JSONLogFormatter()) + formatter_class = getattr(json_logging, formatter_config["class"].rsplit(".", 1)[-1]) + handler.setFormatter(formatter_class()) logger.addHandler(handler) try: logger.info("sanic json logging", extra={"service": name})