File size: 3,573 Bytes
936f0bf
 
 
 
c5638b0
936f0bf
 
 
 
 
c5638b0
 
 
 
 
 
 
 
 
 
936f0bf
c5638b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936f0bf
c5638b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936f0bf
 
 
 
 
 
 
 
 
 
 
c5638b0
 
936f0bf
 
 
 
 
 
 
 
 
 
 
 
c5638b0
 
936f0bf
 
 
 
 
 
 
 
 
 
 
c5638b0
936f0bf
c5638b0
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from __future__ import annotations

import logging
import sys
from typing import Any, MutableMapping

import structlog

from app.core.config import settings

_NOISY_LOGGERS: tuple[str, ...] = (
    "uvicorn.access",
    "uvicorn.error",
    "multipart",
    "httpx",
    "httpcore",
    "redis",
    "PIL",
    "asyncio",
)

_BASE_PRE_CHAIN: tuple = (
    structlog.contextvars.merge_contextvars,
    structlog.stdlib.add_log_level,
    structlog.processors.TimeStamper(fmt="iso"),
)


def _scrub_value(value: Any) -> Any:
    """Return a size placeholder for binary values, otherwise pass through."""
    if isinstance(value, (bytes, bytearray, memoryview)):
        return f"<{len(bytes(value))} bytes>"
    return value


def _scrub_binary(_logger: Any, _method: str, event_dict: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
    """Replace any bytes / bytearray values in event_dict with a size placeholder.

    structlog will happily serialise raw bytes to JSON (escaped) or to the
    console renderer, but a single misrouted PDF or image stream can balloon
    a log line into megabytes. This guard catches the leak at the boundary.
    """
    for key, value in list(event_dict.items()):
        event_dict[key] = _scrub_value(value)
    return event_dict


def _consume_positional_args(_logger: Any, _method: str, event_dict: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
    """Format `logger.info('msg %s', arg)` style calls safely.

    structlog stores the trailing args under `positional_args` and leaves
    the format string in `event` until a later renderer (e.g.
    `PositionalArgumentsFormatter`) consumes it. We do that here, scrubbing
    bytes so a stray PDF stream can never end up rendered as `b'\\x89PNG...'`.
    """
    pos_args = event_dict.pop("positional_args", None)
    if not pos_args:
        return event_dict

    event = event_dict.get("event")
    if not isinstance(event, str) or "%" not in event:
        return event_dict

    safe_args = tuple(_scrub_value(a) for a in pos_args)
    try:
        event_dict["event"] = event % safe_args
    except (TypeError, ValueError):
        # Fall back to the raw event if the format string is malformed.
        pass
    return event_dict


def configure_logging() -> None:
    pre_chain = list(_BASE_PRE_CHAIN)
    is_prod = settings.ENV == "production"

    if is_prod:
        renderer = structlog.processors.JSONRenderer()
    else:
        renderer = structlog.dev.ConsoleRenderer(colors=True)
        pre_chain.insert(0, structlog.stdlib.add_logger_name)

    structlog.configure(
        processors=[
            structlog.stdlib.filter_by_level,
            _consume_positional_args,
            _scrub_binary,
            *pre_chain,
            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
        ],
        logger_factory=structlog.stdlib.LoggerFactory(),
        wrapper_class=structlog.stdlib.BoundLogger,
        cache_logger_on_first_use=True,
    )

    formatter = structlog.stdlib.ProcessorFormatter(
        foreign_pre_chain=pre_chain,
        processors=[
            structlog.stdlib.ProcessorFormatter.remove_processors_meta,
            _consume_positional_args,
            _scrub_binary,
            renderer,
        ],
    )

    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(formatter)

    root_logger = logging.getLogger()
    root_logger.handlers = [handler]
    root_logger.setLevel(settings.LOG_LEVEL.upper())

    for lib in _NOISY_LOGGERS:
        logging.getLogger(lib).setLevel(logging.WARNING)