Django
Logs

Logs

opentelemetry-instrumentation-logging (auto-installed by opentelemetry-bootstrap) bridges Python's standard logging module to the OpenTelemetry Logs SDK and OTLP exporter. Once enabled, every logger.info(...) / logger.error(...) — including records emitted by Django itself, your views, your Celery tasks, and any third-party library that uses logging — is forwarded to Traceway and linked to the active trace and span.

Step 1: Enable OTLP Logs

Extend the env config from the Quick Start to also turn on the logs exporter and the Python logging auto-instrumentation:

# Existing traces + metrics config from the Quick Start
OTEL_SERVICE_NAME=my-django-app
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
 
# Enable OTLP logs
OTEL_LOGS_EXPORTER=otlp
 
# Auto-forward Python `logging` records as OTel log records
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true
 
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-traceway-instance.com/api/otel
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20your-project-token

OTEL_EXPORTER_OTLP_ENDPOINT is the base URL — the logs exporter appends /v1/logs automatically. No extra endpoint config is needed.

OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED is what flips the bridge on. Without it, the logging instrumentation only injects trace_id / span_id into your existing log formatters — your records do not get shipped to OTLP. The variable is read once at startup, so set it in your environment (not in a .env loaded after Django imports).

Step 2: Use Python's logging Module as Usual

Use the standard Python logger. Because the handler runs inside the request / Celery task span, every record emitted from there carries that span's trace_id and span_id automatically:

# myapp/views.py
import logging
 
from django.http import JsonResponse
 
logger = logging.getLogger(__name__)
 
 
def create_order(request):
    order_id = "ord_123"
    logger.info("order received", extra={"order.id": order_id})
 
    try:
        # ... business logic ...
        logger.info("order processed", extra={"order.id": order_id})
        return JsonResponse({"status": "ok"})
    except Exception as exc:
        logger.error("order failed", extra={"order.id": order_id, "exception": str(exc)})
        raise

Open the endpoint's trace in the Traceway dashboard and the Logs tab will show these records attached. The same is true for logs emitted inside Celery tasks, management commands, and any other code that runs under an active span.

Attributes via extra={...}: OTel's logging handler reads the extra dict on a LogRecord and attaches each key as an attribute on the OTel log record. So logger.info("order received", extra={"order.id": "ord_123"}) produces an OTel log with attribute order.id="ord_123". Use dotted keys (e.g. order.id, user.id) to match OTel semantic conventions.

Step 3 (optional): Inject trace_id Into Console / File Logs

If you also write logs to the console or a file (Django's default LOGGING config does this in dev), the logging instrumentation injects the active trace_id and span_id into the LogRecord itself. Add them to your formatter to see them in your tail / Cloud Logging / wherever you read raw logs:

# settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "with_trace": {
            "format": "[{levelname}] {message} | trace_id={otelTraceID} span_id={otelSpanID}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "with_trace",
        },
    },
    "root": {"handlers": ["console"], "level": "INFO"},
}

otelTraceID and otelSpanID are populated by opentelemetry-instrumentation-logging for every record emitted under an active span. Outside a span they are "0" placeholders.

Don't add a Python OTLPHandler manually. When OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true, the agent already attaches a LoggingHandler to the root logger. Adding another one yourself will produce duplicate log records in Traceway.

Severity Mapping

Python's logging levels are mapped to OpenTelemetry severity numbers (and Traceway's TRACE → FATAL labels) as follows:

Python LevelNumericOTLP Severity NumberTraceway Severity
DEBUG105DEBUG
INFO209INFO
WARNING3013WARN
ERROR4017ERROR
CRITICAL5021FATAL

Only records at or above the root logger's configured level are emitted to handlers — so set LOGGING["root"]["level"] = "INFO" (or whatever threshold you want) to control volume.

Logging Exceptions With Full Tracebacks

logger.exception(...) and logger.error(..., exc_info=True) both attach the traceback to the OTel log record's exception.stacktrace attribute, which Traceway extracts into the Logs view:

import logging
 
logger = logging.getLogger(__name__)
 
 
def withdraw(request):
    try:
        _withdraw(request.user, request.POST["amount"])
    except InsufficientFundsError:
        logger.exception("withdraw failed", extra={"user.id": request.user.pk})
        raise

Note that the unhandled exception is also captured on the request span by the Django middleware (see Exceptions). logger.exception(...) is for logging the same event into the Logs feed with structured attributes — not a substitute for re-raising.

Test Your Integration

Add a route that logs at multiple levels, hit it once, and check the Logs page in the Traceway dashboard:

# myapp/views.py
import logging
 
from django.http import JsonResponse
 
logger = logging.getLogger(__name__)
 
 
def log_test(request):
    logger.debug("debug sample")
    logger.info("info sample", extra={"request.id": "req_1"})
    logger.warning("warning sample")
    logger.error("error sample", extra={"order.id": "ord_1"})
    return JsonResponse({"emitted": 4})
# myproject/urls.py
urlpatterns = [
    # ...
    path("log-test/", views.log_test),
]

Visit /log-test/, then open Logs in the dashboard. You should see records at or above your root logger's level within a few seconds, each linked to the trace for that request.

Next Steps