The Modern DevOps Observability Stack: Grafana, Prometheus, Loki
Build a complete DevOps observability stack with Prometheus for metrics, Loki for logs, Tempo for traces, and Grafana dashboards—includes Docker Compose setup.
You cannot operate what you cannot observe. The Grafana stack—Prometheus, Loki, Tempo, and Grafana—has become the standard open-source observability solution for teams running containers and Kubernetes. It covers the three pillars of observability: metrics, logs, and traces.
This guide walks through what each component does, how they fit together, and how to get the full stack running with Docker Compose and Kubernetes.
The Three Pillars of Observability
Metrics tell you what is happening—counters, gauges, and histograms that describe the state of your system at any point in time. “The API is processing 1,200 requests/second with a p99 latency of 340ms.”
Logs tell you what happened—timestamped records of events, errors, and state transitions. “At 14:32:01, user 4892 failed authentication: invalid JWT signature.”
Traces tell you why it happened—the distributed journey of a single request across multiple services. “This request took 1.2 seconds because it waited 900ms for the payments service to call the fraud API.”
All three are necessary. Metrics surface the problem. Logs narrow the cause. Traces pinpoint it.
Component Overview
Prometheus — Metrics Collection
Prometheus is a time-series database that scrapes metrics from instrumented targets at configurable intervals. Applications expose a /metrics endpoint; Prometheus collects it.
Key concepts:
- Scrape targets: Services and infrastructure Prometheus polls for metrics
- PromQL: The query language for writing alerts and dashboards
- Alert rules: Expressions that fire when thresholds are crossed
- Alertmanager: Routes alerts to Slack, PagerDuty, email, etc.
Loki — Log Aggregation
Loki is a horizontally scalable log aggregation system that only indexes labels (not log content). This makes it dramatically cheaper than Elasticsearch at the cost of full-text search speed. It’s designed to work alongside Prometheus—you correlate metrics and logs by the same labels.
Tempo — Distributed Tracing
Tempo is a high-scale distributed tracing backend. It accepts traces in OpenTelemetry, Jaeger, and Zipkin formats. Like Loki, it stores traces in object storage and queries them by trace ID—making it very cost-efficient for high-volume trace data.
Grafana — Visualization and Dashboards
Grafana is the unified frontend for all three data sources. In 2026, Grafana Explore is the primary interface for ad-hoc investigation, while pre-built dashboards handle routine monitoring. Grafana Alerting manages alerts across all data sources from one interface.
Docker Compose Setup
The fastest way to get the full stack running locally or on a single server:
# docker-compose.yml
version: "3.8"
networks:
observability:
driver: bridge
volumes:
prometheus_data:
grafana_data:
loki_data:
tempo_data:
services:
# ─── Prometheus ──────────────────────────────────────────
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./config/alert_rules.yml:/etc/prometheus/alert_rules.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
- '--web.enable-remote-write-receiver'
ports:
- "9090:9090"
networks:
- observability
# ─── Alertmanager ────────────────────────────────────────
alertmanager:
image: prom/alertmanager:v0.27.0
container_name: alertmanager
restart: unless-stopped
volumes:
- ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports:
- "9093:9093"
networks:
- observability
# ─── Loki ────────────────────────────────────────────────
loki:
image: grafana/loki:3.0.0
container_name: loki
restart: unless-stopped
volumes:
- ./config/loki.yml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
networks:
- observability
# ─── Promtail (log shipper) ───────────────────────────────
promtail:
image: grafana/promtail:3.0.0
container_name: promtail
restart: unless-stopped
volumes:
- ./config/promtail.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
networks:
- observability
# ─── Tempo ───────────────────────────────────────────────
tempo:
image: grafana/tempo:2.4.0
container_name: tempo
restart: unless-stopped
volumes:
- ./config/tempo.yml:/etc/tempo.yml:ro
- tempo_data:/var/tempo
command: -config.file=/etc/tempo.yml
ports:
- "3200:3200" # Tempo API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
networks:
- observability
# ─── Grafana ─────────────────────────────────────────────
grafana:
image: grafana/grafana:10.4.0
container_name: grafana
restart: unless-stopped
volumes:
- grafana_data:/var/lib/grafana
- ./config/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro
- ./config/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml:ro
- ./dashboards:/var/lib/grafana/dashboards:ro
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-changeme}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
ports:
- "3000:3000"
depends_on:
- prometheus
- loki
- tempo
networks:
- observability
# ─── Node Exporter (host metrics) ────────────────────────
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
restart: unless-stopped
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
networks:
- observability
Configuration Files
Prometheus Configuration
# config/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'local-dev'
env: 'development'
rule_files:
- /etc/prometheus/alert_rules.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'my-api'
static_configs:
- targets: ['my-api:8080']
metrics_path: /metrics
scrape_interval: 10s
# Kubernetes pods with prometheus.io annotations
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
Alert Rules
# config/alert_rules.yml
groups:
- name: api_alerts
interval: 30s
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
> 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate on {{ $labels.service }}"
description: "Error rate is {{ $value | humanizePercentage }} (threshold: 5%)"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "High p99 latency on {{ $labels.service }}"
description: "p99 latency is {{ $value }}s"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[5m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"
Grafana Data Sources Provisioning
# config/grafana-datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prometheus
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: 15s
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: trace_id
- name: Loki
type: loki
uid: loki
url: http://loki:3100
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: '"traceId":"(\w+)"'
name: TraceID
url: '$${__value.raw}'
- name: Tempo
type: tempo
uid: tempo
url: http://tempo:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki
filterByTraceID: true
tracesToMetrics:
datasourceUid: prometheus
serviceMap:
datasourceUid: prometheus
search:
hide: false
nodeGraph:
enabled: true
Instrumenting Your Application
Python (with OpenTelemetry)
from prometheus_client import Counter, Histogram, start_http_server
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
import structlog
# Structured logging (ships to Loki via Promtail)
log = structlog.get_logger()
# Prometheus metrics
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds',
'HTTP request latency',
['method', 'endpoint'],
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)
# OpenTelemetry tracing
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://tempo:4317"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# Usage in a request handler
def handle_request(method, path):
with tracer.start_as_current_span(f"{method} {path}") as span:
trace_id = format(span.get_span_context().trace_id, '032x')
with REQUEST_LATENCY.labels(method, path).time():
try:
result = process(path)
REQUEST_COUNT.labels(method, path, "200").inc()
log.info("request_handled", path=path, trace_id=trace_id)
return result
except Exception as e:
REQUEST_COUNT.labels(method, path, "500").inc()
log.error("request_failed", path=path, error=str(e), trace_id=trace_id)
raise
# Expose metrics endpoint
start_http_server(9090)
Key Dashboards to Build
1. Service Health Dashboard
Every service should have:
- Request rate (RPS)
- Error rate (%)
- p50 / p95 / p99 latency
- Active pods and restarts
- CPU and memory usage
PromQL examples:
# Request rate
sum(rate(http_requests_total{service="my-api"}[5m]))
# Error rate percentage
sum(rate(http_requests_total{service="my-api",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{service="my-api"}[5m])) * 100
# p99 latency
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket{service="my-api"}[5m])) by (le)
)
2. Kubernetes Cluster Dashboard
Import the official Kubernetes Cluster dashboard (ID: 315) from Grafana Labs. It covers:
- Node CPU, memory, disk, network
- Pod status and restart counts
- Namespace resource utilization
3. Node Exporter Dashboard
Import Node Exporter Full (ID: 1860) for detailed host-level metrics.
4. Log-to-Trace Correlation
Configure Loki derived fields to extract trace IDs from structured logs and link directly to Tempo. When you see an error in logs, one click takes you to the full distributed trace.
Deploying to Kubernetes with Helm
The Grafana community maintains a Kube Prometheus Stack Helm chart that deploys everything:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm upgrade --install kube-prometheus-stack \
prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.adminPassword=your-secure-password \
--set prometheus.prometheusSpec.retention=30d \
--set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=50Gi
# Add Loki
helm repo add grafana https://grafana.github.io/helm-charts
helm upgrade --install loki grafana/loki-stack \
--namespace monitoring \
--set grafana.enabled=false \
--set prometheus.enabled=false \
--set loki.persistence.enabled=true \
--set loki.persistence.size=20Gi
Conclusion
The Grafana observability stack is mature, cost-effective, and deeply integrated. Prometheus, Loki, Tempo, and Grafana work together to give you the full picture: what’s happening, what happened, and why.
Start with Prometheus and a basic Grafana dashboard for your most critical service. Add Loki for log aggregation. Instrument your applications with OpenTelemetry and wire up Tempo for traces. Each addition compounds your ability to diagnose issues faster and with more confidence.
In 2026, teams that have invested in observability consistently outperform those that haven’t—not because observability prevents incidents, but because it dramatically reduces the time from “something is wrong” to “we know exactly why and have fixed it.”
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.