Files
prompts/docs/skills/python-logging/references/httpx-logging-handler-example.md
2026-07-08 22:39:46 -05:00

132 lines
3.9 KiB
Markdown

# 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?