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-tokenOTEL_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_ENABLEDis what flips the bridge on. Without it, the logging instrumentation only injectstrace_id/span_idinto 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.envloaded 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)})
raiseOpen 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 theextradict on aLogRecordand attaches each key as an attribute on the OTel log record. Sologger.info("order received", extra={"order.id": "ord_123"})produces an OTel log with attributeorder.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
OTLPHandlermanually. WhenOTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true, the agent already attaches aLoggingHandlerto 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 Level | Numeric | OTLP Severity Number | Traceway Severity |
|---|---|---|---|
DEBUG | 10 | 5 | DEBUG |
INFO | 20 | 9 | INFO |
WARNING | 30 | 13 | WARN |
ERROR | 40 | 17 | ERROR |
CRITICAL | 50 | 21 | FATAL |
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})
raiseNote 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
- Exceptions — record caught exceptions with context
- Metrics — custom counters, histograms, and gauges
- OTel Logs reference — full OTLP logs mapping and severity details