data-extract / app /core /logging.py
validops-east-1's picture
feat: optimize code + add json key extractor
c5638b0
Raw
History Blame Contribute Delete
3.57 kB
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)