"""Uvicorn access log에서 인증 콜백 비밀값을 제거한다. Uvicorn은 ASGI scope의 path와 query string을 합쳐 ``uvicorn.access``의 세 번째 포맷 인자로 기록한다. Google OAuth 콜백에는 일회용 authorization code와 CSRF state가 query에 있으므로, 애플리케이션 라우터가 안전하게 로깅해도 기본 access log에서 먼저 노출될 수 있다. """ from __future__ import annotations import logging from typing import Any OAUTH_CALLBACK_PATH = "/auth/callback" def redact_access_log_target(target: Any) -> Any: """OAuth callback의 query 전체를 제거하고 다른 request target은 보존한다.""" if not isinstance(target, str): return target path, separator, _query = target.partition("?") if separator and path.rstrip("/") == OAUTH_CALLBACK_PATH: return path return target class UvicornAccessLogRedactionFilter(logging.Filter): """Uvicorn의 구조화된 access-log 인자에서 request target만 정제한다.""" def filter(self, record: logging.LogRecord) -> bool: args = record.args # Uvicorn 0.30.x access 포맷: # (client_addr, method, path_with_query, http_version, status_code) if isinstance(args, tuple) and len(args) >= 3: target = args[2] redacted = redact_access_log_target(target) if redacted != target: record.args = (*args[:2], redacted, *args[3:]) return True _ACCESS_LOG_FILTER = UvicornAccessLogRedactionFilter() def install_uvicorn_access_log_redaction() -> None: """현재 프로세스의 Uvicorn access logger에 필터를 한 번만 설치한다.""" access_logger = logging.getLogger("uvicorn.access") if not any( isinstance(existing, UvicornAccessLogRedactionFilter) for existing in access_logger.filters ): access_logger.addFilter(_ACCESS_LOG_FILTER) __all__ = [ "UvicornAccessLogRedactionFilter", "install_uvicorn_access_log_redaction", "redact_access_log_target", ]