diff --git a/app/logging.py b/app/logging.py index 3a4264f..f9fa0db 100644 --- a/app/logging.py +++ b/app/logging.py @@ -2,16 +2,39 @@ import json import logging from urllib.parse import urlsplit, urlunsplit - -def redacted_url(url: str) -> str: - parsed = urlsplit(url) - host = parsed.hostname or "" - if ":" in host and not host.startswith("["): - host = f"[{host}]" - if parsed.port: - host = f"{host}:{parsed.port}" - return urlunsplit((parsed.scheme, host, parsed.path, "REDACTED" if parsed.query else "", "")) +_STANDARD = set(logging.makeLogRecord({}).__dict__) | {"message", "asctime"} -def log_event(logger: logging.Logger, event: str, **fields: object) -> None: - logger.info(json.dumps({"event": event, **fields}, default=str, sort_keys=True)) +def redact_url(value: str) -> str: + try: + parsed = urlsplit(value) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + return value + host = f"[{parsed.hostname}]" if ":" in parsed.hostname else parsed.hostname + port = f":{parsed.port}" if parsed.port else "" + query = "" if parsed.query else "" + fragment = "" if parsed.fragment else "" + return urlunsplit((parsed.scheme, host + port, parsed.path, query, fragment)) + except ValueError: + return "" + + +class StructuredJsonFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + payload: dict[str, object] = { + "level": record.levelname.lower(), + "logger": record.name, + "message": record.getMessage(), + } + for key, value in record.__dict__.items(): + if key not in _STANDARD and not key.startswith("_"): + payload[key] = value + return json.dumps(payload, default=str, separators=(",", ":")) + + +def configure_logging(level: str) -> None: + handler = logging.StreamHandler() + handler.setFormatter(StructuredJsonFormatter()) + root = logging.getLogger() + root.handlers[:] = [handler] + root.setLevel(level.upper())