"""JSON formatter for stdlib logging that preserves extra fields.

A single logging.Formatter subclass that serialises any keyword arguments
passed via ``extra=`` into the JSON output alongside the standard record
attributes.
"""

from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from typing import Any

# Attributes that belong to the standard LogRecord and should NOT be
# treated as user-supplied extra fields.
_STD_RECORD_ATTRS: frozenset[str] = frozenset(
    {
        "name",
        "msg",
        "args",
        "levelname",
        "levelno",
        "pathname",
        "filename",
        "module",
        "exc_info",
        "exc_text",
        "stack_info",
        "lineno",
        "funcName",
        "created",
        "msecs",
        "relativeCreated",
        "thread",
        "threadName",
        "processName",
        "process",
        "message",
        "asctime",
        "taskName",
    }
)


class JSONFormatter(logging.Formatter):
    """Format log records as JSON lines, including extra fields.

    Usage::

        handler = logging.StreamHandler()
        handler.setFormatter(JSONFormatter())
        logging.getLogger().addHandler(handler)

    Output keys:
        - ``event``      – the log message
        - ``level``      – lower-cased level name
        - ``timestamp``  – ISO-8601 UTC timestamp
        - ``logger``     – logger name
        - any ``extra`` fields supplied by the caller
    """

    def format(self, record: logging.LogRecord) -> str:
        """Return a JSON string for *record*."""
        log_dict: dict[str, Any] = {
            "event": record.getMessage(),
            "level": record.levelname.lower(),
            "timestamp": (
                datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat()
            ),
            "logger": record.name,
        }

        # Merge any extra fields attached to the record.
        for key, value in record.__dict__.items():
            if key not in _STD_RECORD_ATTRS:
                log_dict[key] = value

        # Include exception info when present.
        if record.exc_info and not record.exc_text:
            record.exc_text = self.formatException(record.exc_info)
        if record.exc_text:
            log_dict["exception"] = record.exc_text

        return json.dumps(log_dict, default=str)
