Files
agent_delta/backend/app/core/logging.py
T
2026-06-15 14:22:07 +08:00

79 lines
2.9 KiB
Python

"""Logging utilities — PII redaction filter for GDPR/compliance safety."""
import logging
import re
from typing import ClassVar
class PiiRedactionFilter(logging.Filter):
"""Logging filter that redacts personally identifiable information.
Automatically scrubs email addresses, JWT tokens, API keys, bearer tokens,
and password-like values from log messages to prevent PII leaks to
log aggregators (Datadog, CloudWatch, Logfire, etc.).
Usage:
logging.getLogger().addFilter(PiiRedactionFilter())
"""
PATTERNS: ClassVar[list[tuple[re.Pattern[str], str]]] = [
# Email addresses
(re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"), "[EMAIL_REDACTED]"),
# JWT tokens (header.payload.signature)
(
re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+"),
"[JWT_REDACTED]",
),
# OpenAI API keys
(re.compile(r"sk-[a-zA-Z0-9]{20,}"), "[API_KEY_REDACTED]"),
# Anthropic API keys
(re.compile(r"sk-ant-[a-zA-Z0-9_-]{20,}"), "[API_KEY_REDACTED]"),
# Generic long hex/base64 secrets (40+ chars, likely tokens)
(
re.compile(
r"(?:token|key|secret|password|authorization)[=: ]+['\"]?([A-Za-z0-9_/+=.-]{40,})",
re.IGNORECASE,
),
"[SECRET_REDACTED]",
),
# Bearer tokens in headers
(re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{10,}"), "Bearer [TOKEN_REDACTED]"),
# Password/secret in key=value or key: value patterns
(
re.compile(
r"(password|passwd|pwd|secret_key|api_key|apikey|auth_token|access_token|refresh_token)"
r"[\s]*[=:]\s*['\"]?\S+['\"]?",
re.IGNORECASE,
),
r"\1=[REDACTED]",
),
]
def filter(self, record: logging.LogRecord) -> bool:
"""Redact PII from log record message and args."""
if isinstance(record.msg, str):
record.msg = self._redact(record.msg)
if record.args:
if isinstance(record.args, dict):
record.args = {
k: self._redact(v) if isinstance(v, str) else v for k, v in record.args.items()
}
elif isinstance(record.args, tuple):
record.args = tuple(
self._redact(a) if isinstance(a, str) else a for a in record.args
)
return True
def _redact(self, value: str) -> str:
for pattern, replacement in self.PATTERNS:
value = pattern.sub(replacement, value)
return value
def setup_logging() -> None:
"""Configure root logger with PII redaction filter."""
root_logger = logging.getLogger()
# Avoid adding duplicate filters
if not any(isinstance(f, PiiRedactionFilter) for f in root_logger.filters):
root_logger.addFilter(PiiRedactionFilter())