From 200ec859c770070e8fa6e8eee9608d1ddc280c2a Mon Sep 17 00:00:00 2001 From: Jeremy Kallman Date: Tue, 30 Jun 2026 12:49:42 -0700 Subject: [PATCH 1/2] Map the exception method name to ERROR severity structlog.stdlib.BoundLogger.exception() logs with the method name "exception", which isn't in SEVERITY_MAPPING, so it fell back to the DEFAULT severity. The default BoundLogger is unaffected (it uses "error"). --- structlog_gcp/constants.py | 1 + tests/test_log.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/structlog_gcp/constants.py b/structlog_gcp/constants.py index 13df942..44d8bf6 100644 --- a/structlog_gcp/constants.py +++ b/structlog_gcp/constants.py @@ -16,6 +16,7 @@ "warn": "WARNING", # Warning events might cause problems. "warning": "WARNING", # Warning events might cause problems. "error": "ERROR", # Error events are likely to cause problems. + "exception": "ERROR", # structlog.stdlib.BoundLogger.exception() method name. "critical": "CRITICAL", # Critical events cause more severe problems or outages. # "alert": "ALERT", # A person must take an action immediately. # "emergency": "EMERGENCY", # One or more systems are unusable. diff --git a/tests/test_log.py b/tests/test_log.py index 8ff8229..e522104 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -6,6 +6,8 @@ from structlog.typing import WrappedLogger import structlog_gcp +from structlog_gcp.constants import CLOUD_LOGGING_KEY +from structlog_gcp.processors import LogSeverity from .conftest import T_stdout @@ -63,6 +65,16 @@ def test_exception(stdout: T_stdout, logger: WrappedLogger) -> None: assert msg == expected +def test_exception_method_maps_to_error_severity() -> None: + """`structlog.stdlib.BoundLogger.exception()` logs with the "exception" + method name, unlike the default ``BoundLogger`` (which uses "error"). It + must still map to ERROR severity. + """ + event_dict = LogSeverity()(None, "exception", {CLOUD_LOGGING_KEY: {}}) + + assert event_dict[CLOUD_LOGGING_KEY]["severity"] == "ERROR" + + def test_service_context_default(stdout: T_stdout, logger: WrappedLogger) -> None: try: 1 / 0 From a1d266acd5eabfe75d12fbf504b552f4d47b62ab Mon Sep 17 00:00:00 2001 From: Jeremy Kallman Date: Mon, 6 Jul 2026 15:09:43 -0400 Subject: [PATCH 2/2] Update testing approach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parametrize `test_exception` so a real `.exception()` call runs through the full pipeline and both method names are asserted to produce identical ERROR output — reusing the existing assertion, no new fixture or duplicated payload. --- tests/conftest.py | 14 +++++++++++--- tests/test_log.py | 21 +++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8527848..3f2583d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,14 +31,22 @@ def mock_logger_env() -> Generator[None, None, None]: @pytest.fixture -def logger(mock_logger_env: None) -> Generator[WrappedLogger, None, None]: - """Setup a logger for testing and return it""" +def logger( + request: pytest.FixtureRequest, mock_logger_env: None +) -> Generator[WrappedLogger, None, None]: + """Setup a logger for testing and return it. + + Indirectly parametrize with a ``wrapper_class`` (e.g. + ``structlog.stdlib.BoundLogger``) to bind a non-default logger. + """ structlog.reset_defaults() structlog.contextvars.clear_contextvars() processors = structlog_gcp.build_processors() - structlog.configure(processors=processors) + structlog.configure( + processors=processors, wrapper_class=getattr(request, "param", None) + ) logger = structlog.get_logger() yield logger diff --git a/tests/test_log.py b/tests/test_log.py index e522104..d7be34c 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -1,13 +1,12 @@ import datetime from unittest.mock import patch +import pytest import structlog from _pytest.capture import CaptureFixture from structlog.typing import WrappedLogger import structlog_gcp -from structlog_gcp.constants import CLOUD_LOGGING_KEY -from structlog_gcp.processors import LogSeverity from .conftest import T_stdout @@ -30,7 +29,15 @@ def test_normal(stdout: T_stdout, logger: WrappedLogger) -> None: assert msg == expected +@pytest.mark.parametrize( + "logger", + [None, structlog.stdlib.BoundLogger], + indirect=True, + ids=["default", "stdlib"], +) def test_exception(stdout: T_stdout, logger: WrappedLogger) -> None: + # The stdlib logger's exception() logs with the "exception" method name + # (the default one uses "error"); both must map to ERROR severity. try: 1 / 0 except ZeroDivisionError: @@ -65,16 +72,6 @@ def test_exception(stdout: T_stdout, logger: WrappedLogger) -> None: assert msg == expected -def test_exception_method_maps_to_error_severity() -> None: - """`structlog.stdlib.BoundLogger.exception()` logs with the "exception" - method name, unlike the default ``BoundLogger`` (which uses "error"). It - must still map to ERROR severity. - """ - event_dict = LogSeverity()(None, "exception", {CLOUD_LOGGING_KEY: {}}) - - assert event_dict[CLOUD_LOGGING_KEY]["severity"] == "ERROR" - - def test_service_context_default(stdout: T_stdout, logger: WrappedLogger) -> None: try: 1 / 0