from __future__ import annotations import json import logging import re from datetime import datetime from logging.handlers import RotatingFileHandler from pathlib import Path from typing import Any from zoneinfo import ZoneInfo SENSITIVE_KEYS = ( "api_key", "authorization", "cookie", "password", "refresh_token", "secret", "token", ) SENSITIVE_VALUE_PATTERN = re.compile( r"(?i)\b(api[_-]?key|authorization|password|refresh[_-]?token|secret|token)" r"\s*[:=]\s*[^\s,;]+" ) BEARER_PATTERN = re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/-]{8,}") SHANGHAI_TIMEZONE = ZoneInfo("Asia/Shanghai") def redact(value: Any) -> Any: if isinstance(value, dict): return { str(key): "[REDACTED]" if _is_sensitive(str(key)) else redact(item) for key, item in value.items() } if isinstance(value, (list, tuple)): return [redact(item) for item in value] if isinstance(value, str): safe = SENSITIVE_VALUE_PATTERN.sub(lambda match: f"{match.group(1)}=[REDACTED]", value) return BEARER_PATTERN.sub("Bearer [REDACTED]", safe) return value def _is_sensitive(key: str) -> bool: normalized = key.lower().replace("-", "_") return any(marker in normalized for marker in SENSITIVE_KEYS) class JsonFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: payload: dict[str, Any] = { "timestamp": datetime.now(SHANGHAI_TIMEZONE).isoformat(timespec="milliseconds"), "level": record.levelname.lower(), "logger": record.name, "message": record.getMessage(), } for field in ("request_id", "event", "context"): if hasattr(record, field): payload[field] = redact(getattr(record, field)) if record.exc_info: payload["exception"] = record.exc_info[0].__name__ return json.dumps(redact(payload), ensure_ascii=False, separators=(",", ":")) def configure_logging(level: str, log_file: Path | None) -> None: handlers: list[logging.Handler] = [logging.StreamHandler()] if log_file is not None: log_file.parent.mkdir(parents=True, exist_ok=True) handlers.append( RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8", ) ) formatter = JsonFormatter() for handler in handlers: handler.setFormatter(formatter) logging.basicConfig(level=level, handlers=handlers, force=True)