A self-contained demonstration of structured application logging and log-based observability for a Django backend. The entire backend and monitoring stack runs locally with one Docker Compose command.
This project shows how a Python web application can emit structured JSON logs, collect them directly from Docker, store them in Loki, and turn them into useful Grafana dashboards without adding a separate metrics system.
The sample API intentionally introduces variable response times and occasional HTTP errors. This makes it possible to generate realistic traffic and explore:
- request throughput;
- response status distribution;
- 5xx error ratio;
- p50, p95, and p99 request latency;
- traffic and average latency by endpoint;
- individual requests using fields such as
request_id,path, andstatus_code.
HTTP client
|
v
Django + Gunicorn
|
| structured JSON on stdout/stderr
v
Docker logging stream
|
v
Grafana Alloy ---> Grafana Loki <--- Grafana
discovery log storage queries and dashboards
The stack contains four services:
| Service | Role | Published port |
|---|---|---|
backend |
Django REST API served by Gunicorn | 8000 |
alloy |
Discovers the backend container and forwards its logs | Internal only |
loki |
Stores and queries logs with 30-day retention | Internal only |
grafana |
Provides log exploration and dashboards | 3000 |
Alloy collects only the backend service from this Compose project. Loki and
Alloy are not exposed to the host network. Grafana's Loki datasource and the
Backend HTTP Overview dashboard are provisioned automatically.
- Docker Engine
- Docker Compose v2
curland Bash for the optional traffic generator
Create the local environment file:
cp .env.example .envReplace at least these placeholder values in .env:
SECRET_KEY=replace-with-a-long-random-secret
GRAFANA_ADMIN_PASSWORD=replace-with-a-strong-passwordBuild and start the complete stack:
docker compose up -d --buildCheck that every service is running:
docker compose psThe services are available at:
- API health endpoint: http://localhost:8000/api/health/
- Grafana: http://localhost:3000
Sign in to Grafana with GRAFANA_ADMIN_USER and
GRAFANA_ADMIN_PASSWORD from .env.
The included script rotates between the health, users, and products endpoints. The second argument is the number of requests and the third is concurrency:
./generate-traffic.sh http://127.0.0.1:8000 1000 20The API deliberately returns an error for roughly 10% of requests so that error panels and status-code series contain meaningful data.
Open Grafana, select Explore, choose the Loki datasource, and run:
{service_name="backend"}
To see only successfully parsed HTTP request events:
{service_name="backend"}
| json
| message="http_request_finished"
| __error__=""
The provisioned dashboard is available under:
Dashboards > Observability > Backend HTTP Overview
It includes request rate, status-code rate, latency percentiles, 5xx ratio, top endpoints, and average endpoint latency.
Each completed request produces one JSON event similar to:
{
"levelname": "INFO",
"name": "api.middleware",
"message": "http_request_finished",
"request_id": "7f46d6a8-0f2a-4ecb-aadc-ae18af65e93c",
"ip_addr": "172.21.0.1",
"duration_ms": 77,
"status_code": 200,
"method": "GET",
"path": "/api/health/"
}High-cardinality values such as request_id remain inside the JSON body and
are extracted at query time. Only bounded fields such as service, environment,
platform, and log level are stored as Loki stream labels.
Named Docker volumes persist data between container replacements:
| Volume | Contents |
|---|---|
backend-data |
SQLite database |
loki-data |
Loki indexes, chunks, and WAL |
alloy-data |
Log-reading positions |
grafana-data |
Grafana database and user state |
Stop the stack without deleting its data:
docker compose downReset the project and permanently delete all persisted data:
docker compose down -v
docker compose up -d --buildFollow logs from the complete stack:
docker compose logs -f backend alloy loki grafanaFollow the backend logs:
docker compose logs -f backendRestart services without deleting data:
docker compose restartRecreate a service after changing its configuration:
docker compose up -d --force-recreate alloyAlloy requires access to /var/run/docker.sock for Docker discovery and log
collection. Access to this socket is effectively privileged host access, even
when it is mounted read-only. Use only trusted images and configuration, never
publish Alloy publicly, and consider a restricted Docker socket proxy for a
production deployment.
The default setup is intended for local development and demonstration. Before exposing it publicly, use strong secrets, HTTPS, a production database, proper host validation, backups, and network-level access controls.