Skip to main content
Navigation
HomeTechnical ReferenceJournalGitHubGitHub
Sidebar — toggle document categories via the logo
Categories

Observability Stack — Prometheus, Grafana, Loki, Mimir

Overview​

The Grafana observability stack provides a unified pipeline for metrics (Prometheus/Mimir), logs (Loki), and visualization (Grafana). This reference covers each component's role, common deployment patterns, and the architectural tradeoffs between monolithic and microservice deployments.

Component roles​

ComponentPurposePort
PrometheusMetrics collection, short-term storage, alerting9090
GrafanaDashboards, visualization, alerting UI3000
LokiLog aggregation, storage, querying3100
Promtail / AlloyLog collection agent (ships logs to Loki)9080
MimirLong-term, horizontally scalable metrics storage9009
AlertmanagerAlert routing, grouping, silencing9093

Prometheus​

Architecture​

Prometheus scrapes metrics from instrumented targets via HTTP, stores them in a local time-series database, and evaluates alerting rules. For high availability, run two identical instances scraping the same targets — use an HA pair, not a cluster.

# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: "node"
static_configs:
- targets: ["localhost:9100", "node-2:9100"]

- 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: (.+)

rule_files:
- "alerts/*.yml"

alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]

Alerting rules​

# alerts/node.yml
groups:
- name: node
rules:
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage is above 80% for 10 minutes (current: {{ $value }}%)"

- alert: InstanceDown
expr: up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} is down"

PromQL quick reference​

# Rate (per-second average over time window)
rate(http_requests_total[5m])

# Increase (total increase over time window)
increase(http_requests_total[1h])

# Histogram quantile
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# Aggregation
sum(rate(http_requests_total[5m])) by (status_code)
avg(node_cpu_seconds_total) by (instance)

# Top K
topk(5, rate(http_requests_total[5m]))

# Arithmetic
rate(http_errors_total[5m]) / rate(http_requests_total[5m]) * 100

Loki​

Loki indexes log metadata (labels) but not the log content itself, keeping storage efficient.

LogQL quick reference​

# Basic query
{app="myapp"} |= "error"

# Filter with regex
{app="myapp"} |~ "ERROR|FATAL"

# Exclude pattern
{app="myapp"} != "DEBUG"

# Parse JSON logs and filter
{app="myapp"} | json | status_code >= 500

# Aggregate
sum(count_over_time({app="myapp"} |= "error" [5m]))

# Rate of errors over time
rate({app="myapp"} |= "error" [5m])

Promtail configuration​

# promtail-config.yaml
server:
http_listen_port: 9080

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: varlogs
__path__: /var/log/*.log

- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
relabel_configs:
- source_labels: [__meta_docker_container_name]
target_label: container

Mimir​

Mimir is a horizontally scalable, highly available, long-term storage backend for Prometheus metrics. It accepts Prometheus remote write and serves PromQL queries.

When to use Mimir​

NeedPrometheusMimir
Short-term metrics (hours/days)✓✓
Long-term retention (months/years)✗✓
High availability (multi-node)HA pair only✓ (clustered)
Multi-tenancy✗✓
Global view (multi-cluster)Federation only✓

Key architecture components​

ComponentPurpose
DistributorValidates and distributes incoming samples to ingesters
IngesterBuffers recent data in memory, flushes to object storage
QuerierHandles PromQL queries across ingesters and store-gateways
Store-gatewayServes long-term data from object storage (S3, GCS)
CompactorMerges and deduplicates blocks in object storage
RulerEvaluates recording and alerting rules

OpenTelemetry​

OpenTelemetry (OTel) is the CNCF standard for generating, collecting, and exporting telemetry data. It unifies traces, metrics, and logs into a single framework, replacing vendor-specific agents with a vendor-neutral pipeline.

Architecture​

┌───────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Traces │ │ Metrics │ │ Logs │ │
│ │ (SDK) │ │ (SDK) │ │ (SDK/Bridge)│ │
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
│ └──────────────┼─────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ OTel Collector │ (agent/sidecar/gateway)│
│ │ - Receivers │ │
│ │ - Processors │ │
│ │ - Exporters │ │
│ └───────┬─────────┘ │
└────────────────────┼───────────────────────────────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌───────┐ ┌─────────┐
│ Tempo │ │ Mimir │ │ Loki │
│ (traces)│ │(metrics)│ │ (logs) │
└─────────┘ └───────┘ └─────────┘

Collector configuration​

The collector runs as a DaemonSet (agent) or Deployment (gateway) and routes telemetry to backends:

# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

processors:
batch:
timeout: 5s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512

exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
prometheusremotewrite/mimir:
endpoint: http://mimir:9009/api/v1/push
loki:
endpoint: http://loki:3100/loki/api/v1/push

service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite/mimir]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]

Auto-instrumentation (Node.js)​

# No code changes — just require the module at startup
node --require @opentelemetry/auto-instrumentations-node app.js
// tracing.js — minimal manual setup
const { NodeSDK } = require("@opentelemetry/sdk-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");

const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({ url: "http://localhost:4317" }),
});
sdk.start();

Tempo — Distributed Tracing​

Tempo is Grafana's distributed tracing backend. Unlike Jaeger or Zipkin, Tempo is designed to be cost-effective at scale by separating search from storage — it only indexes trace IDs and service names, keeping trace data in cheap object storage.

When to use Tempo​

NeedTempoJaeger
Massive trace volumes (billions/day)✓ (S3-backed)Limited by Cassandra/ES
Full-text search on spans✗ (use Loki for logs)✓ (Elasticsearch)
Tight Grafana integration (TraceQL, Exemplars)✓Partial
Simple single-binary deployment✓ (monolithic mode)✓

TraceQL quick reference​

# Find traces where a service errored
{ .http.status_code >= 500 }

# Find slow traces
{ duration > 3s }

# Trace with specific attribute
{ resource.service.name = "checkout" && .http.method = "POST" }

# Aggregate: count of spans by service
{ } | count() by resource.service.name

# Histogram of span durations
{ } | histogram(duration) by name

Tempo deployment modes​

ModeDescriptionWhen
MonolithicSingle binary, local storageDev, small-scale (< 10M spans/day)
Scalable monolithicSingle binary, S3 backendMedium scale, simpler than microservices
MicroservicesSeparate scaling per componentLarge scale (> 100M spans/day)
# Start Tempo in monolithic mode with S3 backend
docker run -d --name tempo \
-p 3200:3200 \
-v ./tempo.yaml:/etc/tempo.yaml \
grafana/tempo:latest -config.file=/etc/tempo.yaml

Tempo + Loki + Grafana integration​

Grafana auto-correlates traces with logs when both are configured as data sources. Add a traceID to your structured logs and Grafana shows a "Tempo" button next to log lines — click it to see the full trace for that request.

Grafana Alloy​

Alloy is Grafana's unified agent — the successor to Promtail, Grafana Agent, and the OpenTelemetry Collector. It collects logs, metrics, and traces using a single binary and pipeline-based configuration language.

Replacing Promtail with Alloy​

# alloy-config.alloy — replaces promtail-config.yaml
logging {
level = "info"
}

loki.source.file varlogs {
targets = [
{ __path__ = "/var/log/*.log", job = "varlogs" },
{ __path__ = "/var/log/nginx/*.log", job = "nginx" },
]
forward_to = [loki.write.default.receiver]
}

loki.source.docker containers {
host = "unix:///var/run/docker.sock"
labels = {
job = "docker",
}
forward_to = [loki.write.default.receiver]
}

loki.write default {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}

Alloy + OpenTelemetry (metrics + traces)​

// otelcol.receiver.otlp receives OTLP data from instrumented apps
otelcol.receiver.otlp default {
grpc { endpoint = "0.0.0.0:4317" }
http { endpoint = "0.0.0.0:4318" }
output {
metrics = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
}
}

otelcol.processor.batch default {
output {
metrics = [otelcol.exporter.prometheus.default.input]
traces = [otelcol.exporter.otlp.tempo.input]
}
}

otelcol.exporter.prometheus default {
forward_to = [prometheus.remote_write.mimir.receiver]
}

prometheus.remote_write mimir {
endpoint {
url = "http://mimir:9009/api/v1/push"
}
}

otelcol.exporter.otlp tempo {
client {
endpoint = "tempo:4317"
tls { insecure = true }
}
}

End-to-end deployment (docker-compose)​

A complete observability stack in a single docker-compose.yml — suitable for development and small production environments:

# docker-compose.yml — full LGTM stack (Loki, Grafana, Tempo, Mimir) + Alloy
version: "3.8"

services:
# ── Metrics ──────────────────────────────────────────
mimir:
image: grafana/mimir:latest
command: ["-ingester.ring.replication-factor=1", "-target=all", "-config.file=/etc/mimir.yaml"]
ports: ["9009:9009"]
volumes:
- ./mimir.yaml:/etc/mimir.yaml
- mimir-data:/data

# ── Logs ─────────────────────────────────────────────
loki:
image: grafana/loki:latest
ports: ["3100:3100"]
command: -config.file=/etc/loki/local-config.yaml

# ── Traces ───────────────────────────────────────────
tempo:
image: grafana/tempo:latest
command: ["-config.file=/etc/tempo.yaml"]
ports: ["3200:3200", "4317:4317"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
- tempo-data:/var/tempo

# ── Agent (logs + traces collection) ─────────────────
alloy:
image: grafana/alloy:latest
ports: ["12345:12345"]
volumes:
- ./alloy-config.alloy:/etc/alloy/config.alloy
- /var/log:/var/log:ro
- /var/run/docker.sock:/var/run/docker.sock

# ── Visualization ────────────────────────────────────
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_INSTALL_PLUGINS: grafana-lokiexplore-app
volumes:
- ./datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
- grafana-data:/var/lib/grafana

volumes:
mimir-data:
tempo-data:
grafana-data:
# Start the full stack
docker compose up -d

# Verify all components
curl http://localhost:3000 # Grafana UI
curl http://localhost:3100/ready # Loki
curl http://localhost:3200/ready # Tempo
curl http://localhost:9009/ready # Mimir

Architecture tradeoffs​

Monolithic deployment​

Best for: Small teams, single cluster, simple operations.

ProsCons
Simple to deploy and debugNo high availability (single points of failure)
Low resource overheadNo horizontal scaling
Single config fileCannot isolate noisy neighbors

Deploy: Single Prometheus, Loki, Grafana instance per cluster. Use docker-compose or a single Helm chart.

Microservice / HA deployment​

Best for: Multi-cluster, multi-tenant, production at scale.

ProsCons
Horizontal scaling per componentComplex deployment and networking
Fault isolation (component failure doesn't take down everything)Higher resource overhead
Multi-tenancy with isolationRequires object storage (S3/GCS)
Long-term retention via MimirOperational complexity

Deploy: Mimir in microservice mode, Loki in scalable mode (separate read/write paths), Grafana with PostgreSQL backend.

┌─────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Promtail │ │ Node │ │ App │ │
│ │(DaemonSet)│ │ Exporter │ │ /metrics│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │ Loki │ │ Prometheus (HA pair) │ │
│ │(scalable)│ │ → remote_write → Mimir │ │
│ └────┬────┘ └────────────────────────┘ │
│ │ │
└───────┼─────────────────────────────────────────┘
│
▼
┌───────────────┐ ┌──────────────┐
│ Grafana │────▶│ Mimir (S3) │
│ Dashboards │ │ Long-term │
│ Alerts │ │ metrics │
└───────────────┘ └──────────────┘

Grafana​

Data source setup​

# datasource.yml (provisioning)
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
access: proxy
isDefault: true

- name: Loki
type: loki
url: http://loki:3100
access: proxy

- name: Mimir
type: prometheus
url: http://mimir-nginx:9009/prometheus
access: proxy

Dashboard JSON model essentials​

{
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{status_code}}"
}
],
"gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }
}
],
"templating": {
"list": [
{
"name": "instance",
"type": "query",
"query": "label_values(node_cpu_seconds_total, instance)"
}
]
}
}

Alertmanager​

# alertmanager.yml
route:
group_by: ["alertname", "severity"]
group_wait: 10s
group_interval: 10s
repeat_interval: 4h
receiver: "slack-critical"
routes:
- match:
severity: critical
receiver: "pagerduty"
- match:
severity: warning
receiver: "slack-warnings"

receivers:
- name: "slack-critical"
slack_configs:
- channel: "#alerts-critical"
api_url: "https://hooks.slack.com/services/..."
title: "{{ .GroupLabels.alertname }}"
text: "{{ range .Alerts }}{{ .Annotations.description }}\n{{ end }}"

- name: "pagerduty"
pagerduty_configs:
- routing_key: "<pagerduty-key>"
severity: critical

- name: "slack-warnings"
slack_configs:
- channel: "#alerts-warnings"
api_url: "https://hooks.slack.com/services/..."

See also​