Spaces:
Sleeping
Sleeping
| 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) | |