Comprehensive OpenTelemetry observability for Django with traces, logs, metrics, and profiling
Project description
Django O11y
OpenTelemetry observability for Django with traces, logs, metrics, and profiling.
This package is based on configurations from these blog posts:
- Django Monitoring with Prometheus and Grafana
- Django Development and Production Logging
- Celery Monitoring with Prometheus and Grafana
Features
- Distributed Tracing - OpenTelemetry traces for requests, database, cache, and Celery tasks
- Structured Logging - Structlog with colorized dev logs, JSON prod logs, and OTLP export
- Hybrid Metrics - django-prometheus (infrastructure) + OpenTelemetry (business metrics with exemplars)
- Profiling - Pyroscope continuous profiling (optional)
- Celery Integration - Full observability for async tasks with tracing, logging, and metrics
- Grafana Dashboards - Pre-built dashboards from blog posts work without changes
- Zero config - Works with sensible defaults, customizable via Django settings
- Trace correlation - Automatic trace_id and span_id injection in logs
Quick start
Installation
Recommended for most users:
pip install django-o11y[all]
Or choose specific features:
| Installation Command | Includes | When to Use |
|---|---|---|
pip install django-o11y |
Core (tracing + logging) | Minimal setup |
pip install django-o11y[celery] |
+ Celery instrumentation | Async task observability |
pip install django-o11y[prometheus] |
+ django-prometheus | Infrastructure metrics |
pip install django-o11y[profiling] |
+ pyroscope-io | Continuous profiling |
pip install django-o11y[all] |
Everything | Development & full features |
Production recommendation:
pip install django-o11y[celery,prometheus]
Basic setup
Add to your Django settings:
# settings.py
INSTALLED_APPS = [
'django_o11y', # Add this
'django.contrib.admin',
# ... other apps
]
MIDDLEWARE = [
'django_o11y.middleware.TracingMiddleware', # Add this
'django_o11y.middleware.LoggingMiddleware', # Add this
# ... other middleware
]
django-o11y will automatically:
- Set up OpenTelemetry tracing
- Configure structured logging (Structlog + OTLP)
- Instrument Django, database, cache, and HTTP clients
- Export traces and logs to
http://localhost:4317(OTLP)
Configuration
Customize via Django settings (all optional):
# settings.py
DJANGO_O11Y = {
'SERVICE_NAME': 'my-django-app',
# Tracing
'TRACING': {
'ENABLED': True,
'OTLP_ENDPOINT': 'http://localhost:4317',
'SAMPLE_RATE': 1.0, # 100% sampling (use 0.1 for 10% in prod)
},
# Logging (based on blog post)
'LOGGING': {
'FORMAT': 'json', # 'console' in dev, 'json' in prod
'LEVEL': 'INFO',
'REQUEST_LEVEL': 'INFO',
'DATABASE_LEVEL': 'WARNING',
'COLORIZED': True, # Colorized logs in dev
'RICH_EXCEPTIONS': True, # Beautiful exceptions in dev
'OTLP_ENABLED': True, # Export logs to OTLP
},
# Metrics (hybrid: django-prometheus + OpenTelemetry)
'METRICS': {
'PROMETHEUS_ENABLED': True, # Expose /metrics endpoint
'OTLP_ENABLED': False, # Push metrics via OTLP (disabled by default)
},
# Celery integration (disabled by default, enable if using Celery)
'CELERY': {
'ENABLED': False,
'TRACING_ENABLED': True,
'LOGGING_ENABLED': True,
'METRICS_ENABLED': True,
},
# Profiling (optional)
'PROFILING': {
'ENABLED': False,
'PYROSCOPE_URL': 'http://localhost:4040',
},
}
Or use environment variables:
# Service name
export OTEL_SERVICE_NAME=my-django-app
# Tracing
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_TRACES_SAMPLER_ARG=1.0
# Logging
export DJANGO_LOG_LEVEL=INFO
export DJANGO_LOG_FORMAT=json
Hybrid metrics
Django Observability uses a hybrid metrics approach:
Infrastructure Metrics (django-prometheus)
Uses django-prometheus for infrastructure metrics:
- Request/response metrics (req/s, latency, status codes)
- Database operations (queries/s, latency, connection pool)
- Cache hit rates
- Migration status
Existing Grafana dashboards from the blog posts work without modification.
Business Metrics (OpenTelemetry with Exemplars)
Use OpenTelemetry for custom business metrics with trace correlation:
from django_o11y.metrics import counter, histogram
# Counter with labels
payment_counter = counter("payments.processed", "Total payments processed")
payment_counter.add(1, {"status": "success", "method": "card"})
# Histogram with automatic timing and exemplars (links to traces!)
payment_latency = histogram("payments.latency", "Payment processing time", "s")
with payment_latency.time({"method": "card"}):
result = process_payment() # This span is automatically linked as exemplar
Exemplars let you click on a metric spike in Grafana and jump directly to the trace that caused it.
Structured logging
Based on the Django Development and Production Logging blog post.
Development (colorized console)
import structlog
logger = structlog.get_logger(__name__)
logger.info("Payment processed", amount=100, user_id=123)
Output:
2026-02-12T10:30:45 [info ] Payment processed amount=100 user_id=123 [views.py:42]
Production (JSON + OTLP)
{
"event": "Payment processed",
"amount": 100,
"user_id": 123,
"trace_id": "a1b2c3d4e5f6g7h8",
"span_id": "i9j0k1l2m3n4",
"timestamp": "2026-02-12T10:30:45.123Z",
"level": "info",
"logger": "myapp.views",
"filename": "views.py",
"func_name": "process_payment",
"lineno": 42
}
Logs automatically include trace_id and span_id - click on a log in Grafana Loki and jump to its trace in Tempo!
Celery integration
Zero-config Celery observability. Enable it in settings:
# settings.py
DJANGO_O11Y = {
'CELERY': {
'ENABLED': True, # Auto-instruments when worker starts
},
}
When your Celery worker starts, observability is automatically set up via signals. No manual function calls needed!
Manual setup (optional)
For advanced use cases or backwards compatibility:
# celery_app.py
from celery import Celery
from django_o11y.celery import setup_celery_o11y
app = Celery('myapp')
app.config_from_object('django.conf:settings', namespace='CELERY')
# Optional: Manual setup (auto-called via signals if CELERY.ENABLED=True)
setup_celery_o11y(app)
What you get
Every Celery task automatically includes:
# tasks.py
import structlog
logger = structlog.get_logger(__name__)
@app.task
def process_order(order_id):
# Automatic observability:
# Distributed tracing span (linked to parent request if triggered by API)
# Task lifecycle logs (received, started, succeeded/failed, retried)
# Structured logs with trace_id and span_id
# Task metrics (duration, success rate)
logger.info("Processing order", order_id=order_id)
return process(order_id)
Celery dashboards from the blog work without modification.
Verification
Check that Celery observability is working:
python manage.py o11y check
Quick local testing
Start the full observability stack with one command:
python manage.py o11y stack start
This starts all services with Docker Compose and automatically imports Grafana dashboards:
- Grafana (http://localhost:3000) - Pre-configured dashboards
- Tempo - Distributed tracing backend
- Loki - Log aggregation
- Prometheus - Metrics collection
- Pyroscope - Continuous profiling
- Alloy - OTLP receiver (port 4317)
Then start your Django app:
python manage.py runserver
Generate some traffic and explore in Grafana:
- Dashboards → Django Overview, Django Requests, Celery Tasks
- Explore → Tempo (view traces)
- Explore → Loki (view logs with trace correlation)
- Click on a log → "Tempo" button → See the full trace
- Click on a metric spike → See linked traces via exemplars
Custom app URL
If your app runs in Docker or on a different port:
# App in Docker network
python manage.py o11y stack start --app-url django-app:8000
# App on different port
python manage.py o11y stack start --app-url host.docker.internal:3000
Grafana dashboards
This package works with dashboards from the blog posts:
- Django Overview - Request metrics, database ops, cache hit rate
- Django Requests Overview - Per-view breakdown, error rates
- Django Requests by View - Detailed per-view latency analysis
- Celery Tasks Overview - Task states, queue length, worker status
- Celery Tasks by Task - Per-task metrics and failures
All dashboards are included in the demo project.
Development
Local development
# Clone repo
git clone https://github.com/adinhodovic/django-o11y
cd django-o11y
# Install with uv
uv sync --all-extras
# Run tests
uv run pytest
# Run linting
uv run ruff check .
uv run pylint src/django_o11y
# Run with tox (test matrix)
uv run tox
Contributing
Contributions are welcome! Please:
- Fork the repo
- Create a feature branch (
git checkout -b feat/my-feature) - Commit with conventional commits (
feat:,fix:, etc.) - Push and create a PR
- CI will run tests and linting
Verification and troubleshooting
Health check
Verify your setup with the built-in health check command:
python manage.py o11y check
This will:
- Check configuration is valid
- Test OTLP endpoint connectivity
- Verify required packages are installed
- Create a test trace and show how to view it in Tempo
Common issues
Silent Celery instrumentation failure
Problem: Celery tasks aren't traced despite CELERY.ENABLED = True
Solution: Install the required package:
pip install opentelemetry-instrumentation-celery
The system will warn you at startup if this package is missing.
Configuration errors
Problem: Django won't start with configuration error
Solution: Configuration is validated at startup. Read the error message carefully:
ImproperlyConfigured: Django O11y configuration errors:
• TRACING.SAMPLE_RATE must be between 0.0 and 1.0, got 1.5
Please fix these issues in your DJANGO_O11Y setting.
Fix the issues in your settings and restart.
No traces appearing
Problem: Application runs but no traces in Tempo
Check:
- OTLP endpoint is reachable:
python manage.py o11y check - Sampling rate isn't 0: Check
TRACING.SAMPLE_RATE - Tracing is enabled: Check
TRACING.ENABLED - OTLP receiver is running:
docker ps | grep tempo
Logs not structured
Problem: Logs appear as plain text instead of structured JSON
Solution: Use structlog.get_logger() instead of logging.getLogger():
# Wrong
import logging
logger = logging.getLogger(__name__)
# Correct
import structlog
logger = structlog.get_logger(__name__)
Documentation
License
MIT License - see LICENSE
Acknowledgments
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_o11y-0.1.0.tar.gz.
File metadata
- Download URL: django_o11y-0.1.0.tar.gz
- Upload date:
- Size: 126.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa31d5451fd759099f62ea9a2374b92eb8b92c4095e5a40888cff98027adfdb7
|
|
| MD5 |
e89e2d7cc0e8f3d7d996f1c17e701d9f
|
|
| BLAKE2b-256 |
d224182bbd57ca3bdd56d504a16fd4f9c235cf9a0cd72bc8aed9f4170580cadd
|
Provenance
The following attestation bundles were made for django_o11y-0.1.0.tar.gz:
Publisher:
ci-cd.yml on adinhodovic/django-o11y
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_o11y-0.1.0.tar.gz -
Subject digest:
aa31d5451fd759099f62ea9a2374b92eb8b92c4095e5a40888cff98027adfdb7 - Sigstore transparency entry: 969613371
- Sigstore integration time:
-
Permalink:
adinhodovic/django-o11y@6a83d93cff6b76ec2c529a5ba466ce6cc56525ad -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/adinhodovic
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-cd.yml@6a83d93cff6b76ec2c529a5ba466ce6cc56525ad -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_o11y-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_o11y-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f42622690e321eb403ef8c5b68441bda2f0dacf67158649b49a1909d7eb5dfc9
|
|
| MD5 |
3ca2e81de9fddf09a2e39673c865da79
|
|
| BLAKE2b-256 |
513a82f7994d54b5fc21ac1e31c3abe8e8021dd3f992a0c7950fd0f89c4b97a2
|
Provenance
The following attestation bundles were made for django_o11y-0.1.0-py3-none-any.whl:
Publisher:
ci-cd.yml on adinhodovic/django-o11y
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_o11y-0.1.0-py3-none-any.whl -
Subject digest:
f42622690e321eb403ef8c5b68441bda2f0dacf67158649b49a1909d7eb5dfc9 - Sigstore transparency entry: 969613391
- Sigstore integration time:
-
Permalink:
adinhodovic/django-o11y@6a83d93cff6b76ec2c529a5ba466ce6cc56525ad -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/adinhodovic
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci-cd.yml@6a83d93cff6b76ec2c529a5ba466ce6cc56525ad -
Trigger Event:
release
-
Statement type: