From 53c4a607b16cd3b17c917fc91af9715d4735986d Mon Sep 17 00:00:00 2001 From: Gonghan-Princess <268298391+Gonghan-Princess@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:00:16 +0800 Subject: [PATCH] Add ASGI scope access log atoms --- docs/how_to_guides/logging.rst | 6 ++++++ src/hypercorn/logging.py | 5 +++++ tests/test_logging.py | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/docs/how_to_guides/logging.rst b/docs/how_to_guides/logging.rst index 9e16e079..b2860b38 100644 --- a/docs/how_to_guides/logging.rst +++ b/docs/how_to_guides/logging.rst @@ -66,8 +66,14 @@ p process ID {Header}i request header {Header}o response header {Variable}e environment variable +{Variable}s string or byte value from the ASGI scope =========== =========== +For example, ``%({myapp.trace_id}s)s`` logs a trace ID added to the +request's ASGI scope by the application. Byte values are decoded as +Latin-1; other value types and missing keys appear as ``-``. The +``{Variable}e`` atom continues to read process environment variables. + Customising the logger ---------------------- diff --git a/src/hypercorn/logging.py b/src/hypercorn/logging.py index 04f84ab4..e11a5317 100644 --- a/src/hypercorn/logging.py +++ b/src/hypercorn/logging.py @@ -140,6 +140,11 @@ def __init__( self[f"{{{name.decode('latin1').lower()}}}i"] = value.decode("latin1") for name, value in os.environ.items(): self[f"{{{name.lower()}}}e"] = value + for name, value in request.items(): + if isinstance(value, str): + self[f"{{{name.lower()}}}s"] = value + elif isinstance(value, bytes): + self[f"{{{name.lower()}}}s"] = value.decode("latin1") protocol = request.get("http_version", "ws") client = request.get("client") if client is None: diff --git a/tests/test_logging.py b/tests/test_logging.py index bb6f32b8..d5415913 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -3,6 +3,7 @@ import logging import os import time +from typing import cast import pytest @@ -110,6 +111,29 @@ def test_access_log_environ_atoms(http_scope: HTTPScope, response: ResponseSumma assert atoms["{random}e"] == "Environ" +def test_access_log_scope_atoms( + http_scope: HTTPScope, response: ResponseSummary, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("MYAPP_TRACE_ID", "process-value") + scope = cast( + HTTPScope, + { + **http_scope, + "myapp_trace_id": "request-value", + "myapp_bytes": b"binary-\xff", + "myapp_object": object(), + }, + ) + + atoms = AccessLogAtoms(scope, response, 0) + + assert atoms["{MYAPP_TRACE_ID}s"] == "request-value" + assert "%({myapp_trace_id}s)s" % atoms == "request-value" + assert atoms["{myapp_bytes}s"] == "binary-\xff" + assert atoms["{myapp_object}s"] == "-" + assert atoms["{myapp_trace_id}e"] == "process-value" + + def test_nonstandard_status_code(http_scope: HTTPScope) -> None: atoms = AccessLogAtoms(http_scope, {"status": 441, "headers": []}, 0) assert atoms["st"] == ""