Skip to content

Commit

Permalink
fix: logging to file with structlog (#3425)
Browse files Browse the repository at this point in the history
fix: structlog file logging

Fixes issue where adding a file handle to the config object would cause it to fail on call to `configure()`.

- add e2e test for logging to file w/ structlog
  • Loading branch information
peterschutt authored Apr 28, 2024
1 parent edf9400 commit cc6d55b
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 4 deletions.
11 changes: 7 additions & 4 deletions litestar/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
Empty file.
85 changes: 85 additions & 0 deletions tests/e2e/test_logging/test_structlog_to_file.py
Original file line number Diff line number Diff line change
@@ -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,
},
]

0 comments on commit cc6d55b

Please sign in to comment.