Python logging to UptimeEye

Python's standard logging module makes shipping a matter of one handler. In containers, emit JSON to stdout and let the platform agent ship it; without an agent, the handler below batches records and posts them as JSON lines.

Endpoint: https://logs.uptimeeye.com/v1/ingest/jsonline

1.A batching handler for the standard logging module

uptimeeye_logs.py
import atexit, json, logging, os, threading, time, urllib.request

ENDPOINT = "https://logs.uptimeeye.com/v1/ingest/jsonline?_stream_fields=service,env"
KEY = os.environ["UPTIMEEYE_INGEST_KEY"]

class UptimeEyeHandler(logging.Handler):
    """Buffers records and posts them as JSON lines every 2 s or 200 records."""

    def __init__(self, service, env="prod", batch=200, interval=2.0):
        super().__init__()
        self.base = {"service": service, "env": env}
        self.batch, self.interval = batch, interval
        self.buf, self.lock = [], threading.Lock()
        threading.Thread(target=self._loop, daemon=True).start()
        atexit.register(self.flush)

    def emit(self, record):
        entry = {
            **self.base,
            "_time": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created)) + f".{int(record.msecs):03d}Z",
            "_msg": record.getMessage(),
            "level": record.levelname.lower(),
            "logger": record.name,
            **getattr(record, "fields", {}),
        }
        if record.exc_info:
            entry["exception"] = self.formatter.formatException(record.exc_info) if self.formatter else logging.Formatter().formatException(record.exc_info)
        with self.lock:
            self.buf.append(json.dumps(entry, default=str))
            if len(self.buf) >= self.batch:
                self._post()

    def _loop(self):
        while True:
            time.sleep(self.interval)
            self.flush()

    def flush(self):
        with self.lock:
            self._post()

    def _post(self):
        if not self.buf:
            return
        body = ("\n".join(self.buf) + "\n").encode()
        self.buf = []
        req = urllib.request.Request(ENDPOINT, data=body, method="POST",
            headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/x-ndjson"})
        try:
            urllib.request.urlopen(req, timeout=10).read()
        except Exception as err:  # never let logging crash the app
            print("uptimeeye logs:", err)

# usage
log = logging.getLogger("checkout")
log.setLevel(logging.INFO)
log.addHandler(UptimeEyeHandler(service="checkout"))
log.info("payment authorized", extra={"fields": {"order_id": "4711", "duration_ms": 812}})
Note: Django: register the handler in LOGGING['handlers'] and attach it to the django and your app loggers.

2.Containers: JSON to stdout

python-json-logger or structlog give you one JSON object per line; the platform agent (Kubernetes, Docker) parses it.

logging setup
import logging, sys
from pythonjsonlogger.json import JsonFormatter

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter(
    "%(asctime)s %(levelname)s %(name)s %(message)s",
    rename_fields={"asctime": "time", "levelname": "level", "name": "logger"},
    static_fields={"service": "checkout", "env": "prod"},
    datefmt="%Y-%m-%dT%H:%M:%S%z",
))
logging.basicConfig(level=logging.INFO, handlers=[handler])

3.OpenTelemetry

shell
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http

export OTEL_SERVICE_NAME=checkout
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=prod
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.uptimeeye.com/v1/ingest/otlp/v1/logs
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="Authorization=Bearer%20ue_ingest_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

opentelemetry-instrument python app.py

Fields you get

These show up in the fields panel and can be used in every filter:

  • service, env
  • level
  • logger
  • exception with the formatted traceback
  • keys passed via extra

Tips

  • Pass structured data via extra={"fields": {...}} (handler above) or as keyword arguments in structlog — not as f-string fragments in the message.
  • Gunicorn/uvicorn access logs: attach the same handler to the gunicorn.access / uvicorn.access loggers.

FAQ

Will a slow network block my request handlers?
No — the handler only appends to an in-memory buffer; the POST happens on a daemon thread or when the buffer is full (bounded by batch).
Where do I get the ingest key?
In the app under API Keys → New API Key → type “Log ingest”. The key starts with ue_ingest_ and is shown once. Management keys (ue_live_) are refused by the ingest endpoint.
How do I check that logs arrive?
Open Logs in the app, pick the 15m range and search for service:=<your service>. New lines are searchable within about a second; Live tail shows them with a ~6 s delay.