rebuild(stage-2): establish runtime and persistence foundations
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user