80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
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)
|