Source code for logeverything.transport.http
"""
HTTP Transport Handler for LogEverything.
Ships structured log records to a dashboard or collector via HTTP POST
using only the standard library (``urllib.request``). Zero external deps.
Usage::
from logeverything.transport.http import HTTPTransportHandler
handler = HTTPTransportHandler("http://dashboard:8999/api/ingest/logs")
logger.addHandler(handler)
"""
import datetime
import json
import logging
import os
import urllib.error
import urllib.request
from typing import Any, Dict, List, Optional
from logeverything.transport.buffer import LogBuffer
[docs]
class HTTPTransportHandler(logging.Handler):
"""
Logging handler that batches records and POSTs them as JSON to an HTTP
endpoint.
Args:
endpoint: URL to POST batched logs to (e.g.
``http://localhost:8999/api/ingest/logs``).
api_key: Optional API key sent as ``Authorization: Bearer <key>``.
batch_size: Records per HTTP request.
flush_interval: Seconds between automatic flushes.
source_name: Identifier for this application / process (defaults to PID).
max_retries: Retry count for failed HTTP POSTs.
timeout: HTTP request timeout in seconds.
"""
def __init__(
self,
endpoint: str,
api_key: Optional[str] = None,
batch_size: int = 50,
flush_interval: float = 2.0,
source_name: Optional[str] = None,
max_retries: int = 3,
timeout: float = 10.0,
level: int = logging.NOTSET,
):
super().__init__(level)
self.endpoint = endpoint
self.api_key = api_key
self.source_name = source_name or f"pid-{os.getpid()}"
self.timeout = timeout
self._buffer = LogBuffer(
send_batch=self._send_batch,
batch_size=batch_size,
flush_interval=flush_interval,
max_retries=max_retries,
)
[docs]
def emit(self, record: logging.LogRecord) -> None:
"""Serialize the record and queue it for batched delivery."""
try:
entry = self._record_to_dict(record)
self._buffer.put(entry)
except Exception:
self.handleError(record)
[docs]
def flush(self) -> None:
"""Flush the internal buffer immediately."""
self._buffer.flush()
[docs]
def close(self) -> None:
"""Flush remaining records and release resources."""
self._buffer.close()
super().close()
# --- Internals ---
def _record_to_dict(self, record: logging.LogRecord) -> Dict[str, Any]:
entry: Dict[str, Any] = {
"timestamp": datetime.datetime.fromtimestamp(record.created).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"correlation_id": getattr(record, "correlation_id", ""),
"thread": record.thread,
"process": record.process,
"source": self.source_name,
}
if not entry["correlation_id"]:
try:
from logeverything.correlation import get_correlation_id
entry["correlation_id"] = get_correlation_id()
except Exception:
pass # nosec B110 -- best-effort correlation lookup
return entry
def _send_batch(self, batch: List[Dict[str, Any]]) -> None:
payload = json.dumps({"logs": batch, "source": self.source_name}).encode("utf-8")
req = urllib.request.Request(
self.endpoint,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
if self.api_key:
req.add_header("Authorization", f"Bearer {self.api_key}")
urllib.request.urlopen(req, timeout=self.timeout) # nosec B310 -- endpoint is configured, not user-controlled