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