# HTTPX Logging Handler Example Use this reference when an application should emit JSON logs to an HTTP collector while keeping startup logging configuration declarative. This page follows the top-level skill pattern: - define one `LOGGING` dictionary - apply it once with `logging.config.dictConfig(LOGGING)` - keep modules focused on logger calls Source docs to keep nearby: - [HTTPX clients](https://www.python-httpx.org/advanced/clients/) - [HTTPX timeouts](https://www.python-httpx.org/advanced/timeouts/) - [`logging.config.dictConfig`](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) ## Minimal Topology ```text application code -> named logger -> HttpxJsonLogHandler -> HTTP collector ``` ## Reusable Handler Type Keep transport behavior in one handler class and wire it declaratively through `dictConfig`. ```python title="httpx_json_handler.py" import logging import httpx class HttpxJsonLogHandler(logging.Handler): def __init__(self, collector_url: str, timeout_seconds: float = 2.0, token: str | None = None) -> None: super().__init__() headers = {"content-type": "application/json"} if token is not None: headers["authorization"] = f"Bearer {token}" timeout = httpx.Timeout(timeout_seconds) self.client = httpx.Client(base_url=collector_url, headers=headers, timeout=timeout) def emit(self, record: logging.LogRecord) -> None: payload = { "name": record.name, "levelname": record.levelname, "levelno": record.levelno, "pathname": record.pathname, "lineno": record.lineno, "funcName": record.funcName, "created": record.created, "message": record.getMessage(), } try: response = self.client.post("/logs", json=payload) response.raise_for_status() except httpx.HTTPError: self.handleError(record) def close(self) -> None: self.client.close() super().close() ``` ## Application Logging Configuration (Declarative) ```python title="logging_config.py" import logging.config LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": { "httpx": { "class": "httpx_json_handler.HttpxJsonLogHandler", "collector_url": "http://127.0.0.1:9021", "timeout_seconds": 2.0, "token": None, }, "console": { "class": "logging.StreamHandler", "level": "INFO", "stream": "ext://sys.stdout", }, }, "root": { "level": "INFO", "handlers": ["httpx", "console"], }, } def configure_logging() -> None: logging.config.dictConfig(LOGGING) ``` ```python title="feature.py" import logging logger = logging.getLogger(__name__) def sync_customer(customer_id: str) -> None: logger.info("Syncing customer %s", customer_id) ``` ```python title="main.py" from feature import sync_customer from logging_config import configure_logging configure_logging() sync_customer("C-101") ``` ## Collector-Side Configuration (Declarative) Whether you use an internal HTTP endpoint or a managed collector, keep receiver-side formatting and routing declared on the receiver side, not in application modules. ## Why This Pattern - Logging wiring is declared once and applied once. - Runtime behavior changes by editing config fields, not scattered root mutations. - Feature modules stay independent from transport details. - HTTP connection details remain encapsulated in one handler type. ## Review Checklist 1. Is there one `LOGGING` dict for the application process? 2. Is `dictConfig` called once at startup? 3. Are module loggers created via `logging.getLogger(__name__)`? 4. Are HTTP endpoint, timeout, and auth token inputs declared in handler config? 5. Are final routing/retention decisions handled by the collector side?