diff --git a/litestar/logging/config.py b/litestar/logging/config.py index 3187fb1a57..c084c5d034 100644 --- a/litestar/logging/config.py +++ b/litestar/logging/config.py @@ -2,13 +2,14 @@ import sys from abc import ABC, abstractmethod -from dataclasses import asdict, dataclass, field, fields +from dataclasses import dataclass, field, fields from importlib.util import find_spec from logging import INFO from typing import TYPE_CHECKING, Any, Callable, Literal, cast from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException from litestar.serialization.msgspec_hooks import _msgspec_json_encoder +from litestar.utils.dataclass import simple_asdict from litestar.utils.deprecation import deprecated __all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig") @@ -313,7 +314,9 @@ def stdlib_json_serializer(value: EventDict, **_: Any) -> str: # pragma: no cov return _msgspec_json_encoder.encode(value).decode("utf-8") -def default_structlog_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore +def default_structlog_processors( + as_json: bool = True, json_serializer: Callable[[Any], Any] = default_json_serializer +) -> list[Processor]: # pyright: ignore """Set the default processors for structlog. Returns: @@ -329,7 +332,7 @@ def default_structlog_processors(as_json: bool = True) -> list[Processor]: # py structlog.processors.add_log_level, structlog.processors.format_exc_info, structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer(serializer=default_json_serializer), + structlog.processors.JSONRenderer(serializer=json_serializer), ] return [ structlog.contextvars.merge_contextvars, @@ -465,7 +468,7 @@ def configure(self) -> GetLogger: structlog.configure( **{ k: v - for k, v in asdict(self).items() + for k, v in simple_asdict(self).items() if k not in ( "standard_lib_logging_config", diff --git a/tests/e2e/test_logging/__init__.py b/tests/e2e/test_logging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/test_logging/test_structlog_to_file.py b/tests/e2e/test_logging/test_structlog_to_file.py new file mode 100644 index 0000000000..8e3e3f205f --- /dev/null +++ b/tests/e2e/test_logging/test_structlog_to_file.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import TYPE_CHECKING +from unittest.mock import ANY + +import pytest +import structlog + +from litestar import Litestar, get +from litestar.logging import StructLoggingConfig +from litestar.logging.config import default_json_serializer, default_structlog_processors +from litestar.plugins.structlog import StructlogConfig, StructlogPlugin +from litestar.testing import TestClient + +if TYPE_CHECKING: + from collections.abc import Iterator + + +@pytest.fixture(autouse=True) +def structlog_reset() -> Iterator[None]: + try: + yield + finally: + structlog.reset_defaults() + + +def test_structlog_to_file(tmp_path: Path) -> None: + log_file = tmp_path / "log.log" + + logging_config = StructlogConfig( + structlog_logging_config=StructLoggingConfig( + logger_factory=structlog.WriteLoggerFactory(file=log_file.open("wt")), + processors=default_structlog_processors( + json_serializer=lambda v, **_: str(default_json_serializer(v), "utf-8") + ), + ), + ) + + logger = structlog.get_logger() + + @get("/") + def handler() -> str: + logger.info("handled", hello="world") + return "hello" + + app = Litestar(route_handlers=[handler], plugins=[StructlogPlugin(config=logging_config)], debug=True) + + with TestClient(app) as client: + resp = client.get("/") + assert resp.text == "hello" + + logged_data = [json.loads(line) for line in log_file.read_text().splitlines()] + assert logged_data == [ + { + "path": "/", + "method": "GET", + "content_type": ["", {}], + "headers": { + "host": "testserver.local", + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "connection": "keep-alive", + "user-agent": "testclient", + }, + "cookies": {}, + "query": {}, + "path_params": {}, + "body": None, + "event": "HTTP Request", + "level": "info", + "timestamp": ANY, + }, + {"hello": "world", "event": "handled", "level": "info", "timestamp": ANY}, + { + "status_code": 200, + "cookies": {}, + "headers": {"content-type": "text/plain; charset=utf-8", "content-length": "5"}, + "body": "hello", + "event": "HTTP Response", + "level": "info", + "timestamp": ANY, + }, + ]