Skip to content

How It Works: Complete Guide

This guide explains every tool in the observability stack, how they work, how they're configured, and how they connect to each other.


Overview

The observability stack consists of 6 main tools:

Tool Role Purpose
Prometheus Metrics collection Collects and stores metrics from applications
Loki Log storage Stores and indexes log data
Promtail Log shipping Reads log files and sends to Loki
Alertmanager Alert routing Routes alerts to Slack channels
Grafana Visualization Displays dashboards from Prometheus and Loki
Node Exporter Host metrics Collects CPU, memory, disk metrics

Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        OBSERVABILITY STACK                                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚    β”‚   Browser    β”‚                    β”‚      Django App       β”‚            β”‚
β”‚    β”‚              │──── HTTP ────────▢│     obs-django:9000    β”‚            β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚                       β”‚            β”‚
β”‚                                        β”‚  - Serves pages       β”‚            β”‚
β”‚                                        β”‚  - Writes logs        β”‚            β”‚
β”‚                                        β”‚  - Exposes /metrics   β”‚            β”‚
β”‚                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                                    β”‚                        β”‚
β”‚                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                              β”‚                     β”‚                     β”‚  β”‚
β”‚                              β–Ό                     β–Ό                     β–Ό  β”‚
β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                   β”‚   Prometheus    β”‚  β”‚    Promtail     β”‚  β”‚   Sentry  β”‚  β”‚
β”‚                   β”‚ :9090           β”‚  β”‚  (log shipper)  β”‚  β”‚  (cloud)  β”‚  β”‚
β”‚                   β”‚                 β”‚  β”‚                 β”‚  β”‚           β”‚  β”‚
β”‚                   β”‚ Pulls /metrics  β”‚  β”‚ Tails log files β”‚  β”‚ Receives  β”‚  β”‚
β”‚                   β”‚ every 15s       β”‚  β”‚ β†’ pushes to Lokiβ”‚  β”‚ errors    β”‚  β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                            β”‚                    β”‚                          β”‚
β”‚                            β”‚ evaluate           β”‚ push                     β”‚
β”‚                            β”‚ rules              β”‚ logs                     β”‚
β”‚                            β–Ό                    β–Ό                          β”‚
β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚                   β”‚  Alertmanager   β”‚  β”‚      Loki       β”‚                 β”‚
β”‚                   β”‚ :9093           β”‚  β”‚ :3100           β”‚                 β”‚
β”‚                   β”‚                 β”‚  β”‚                 β”‚                 β”‚
β”‚                   β”‚ Routes alerts   β”‚  β”‚ Stores logs     β”‚                 β”‚
β”‚                   β”‚ to Slack        β”‚  β”‚ with labels     β”‚                 β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚                            β”‚                    β”‚                          β”‚
β”‚                            β”‚ notify             β”‚ query                    β”‚
β”‚                            β–Ό                    β–Ό                          β”‚
β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚                   β”‚     Slack       β”‚  β”‚    Grafana      β”‚                 β”‚
β”‚                   β”‚ #alerts-*       β”‚  β”‚ :3000           β”‚                 β”‚
β”‚                   β”‚                 β”‚  β”‚                 β”‚                 β”‚
β”‚                   β”‚ Receives alert  β”‚  β”‚ Dashboards &    β”‚                 β”‚
β”‚                   β”‚ notifications   β”‚  β”‚ log exploration β”‚                 β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Prometheus

What Is Prometheus?

Prometheus is an open-source monitoring system that collects metrics from your applications. It uses a "pull model" - meaning it scrapes (pulls) data from your app, rather than your app pushing data to it.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        PROMETHEUS WORKFLOW                                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Application exposes metrics                                        β”‚
β”‚  ─────────────────────────────────────                                      β”‚
β”‚                                                                             β”‚
β”‚      Django App                                                             β”‚
β”‚          β”‚                                                                  β”‚
β”‚          β”‚ Exposes /metrics endpoint                                        β”‚
β”‚          β–Ό                                                                  β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ GET /metrics                                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP django_http_requests_total              β”‚                   β”‚
β”‚      β”‚ # TYPE django_http_requests_total counter      β”‚                   β”‚
β”‚      β”‚ django_http_requests_total{method="GET"} 1234  β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP django_db_query_duration_seconds         β”‚                   β”‚
β”‚      β”‚ django_db_query_duration_seconds 0.005          β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Prometheus scrapes (pulls) metrics                                 β”‚
β”‚  ──────────────────────────────────────────                                 β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Prometheus                                      β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Every 15 seconds:                               β”‚                   β”‚
β”‚      β”‚ 1. HTTP GET to obs-django:9000/metrics         β”‚                   β”‚
β”‚      β”‚ 2. Parse response                               β”‚                   β”‚
β”‚      β”‚ 3. Store in TSDB                                β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Prometheus stores metrics                                          β”‚
β”‚  ─────────────────────────────────                                          β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Time Series Database (TSDB)                     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Metric Name          Timestamp    Value         β”‚                   β”‚
β”‚      β”‚ ─────────────────────────────────────────────  β”‚                   β”‚
β”‚      β”‚ django_http_...total  10:00:00     1234        β”‚                   β”‚
β”‚      β”‚ django_http_...total  10:00:15     1235        β”‚                   β”‚
β”‚      β”‚ django_http_...total  10:00:30     1236        β”‚                   β”‚
β”‚      β”‚ django_db_...seconds  10:00:00     0.005       β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 4: Prometheus evaluates alert rules                                   β”‚
β”‚  ─────────────────────────────────────────                                  β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Alert Rules                                     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ - alert: DjangoDown                             β”‚                   β”‚
β”‚      β”‚   expr: up{job="django"} == 0                   β”‚                   β”‚
β”‚      β”‚   for: 10s                                      β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Every 15 seconds:                               β”‚                   β”‚
β”‚      β”‚ 1. Evaluate expression                          β”‚                   β”‚
β”‚      β”‚ 2. If true for 10s β†’ send alert                β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Configuration Explained

File: prometheus/prometheus.yml

# Global settings
global:
  scrape_interval: 15s    # How often to scrape targets (default: 15 seconds)
  evaluation_interval: 15s  # How often to evaluate alert rules (default: 15 seconds)

# Alertmanager configuration
alerting:
  alertmanagers:
    - static_configs:
        - targets: ["obs-alertmanager:9093"]  # Where to send alerts

# Alert rules location
rule_files:
  - /etc/prometheus/rules/*.yml  # Load all .yml files in this directory

# What to scrape
scrape_configs:

  # Job 1: Prometheus itself (self-monitoring)
  - job_name: "prometheus"           # Job name
    static_configs:
      - targets: ["localhost:9090"]  # Scrape itself

  # Job 2: Django application
  - job_name: "django"
    static_configs:
      - targets: ["obs-django:9000"]  # Docker container name and port
    metrics_path: /metrics            # Endpoint to scrape

  # Job 3: Node Exporter (host metrics)
  - job_name: "node"
    static_configs:
      - targets: ["obs-node-exporter:9100"]

  # Job 4: Loki (log system metrics)
  - job_name: "loki"
    static_configs:
      - targets: ["obs-loki:3100"]
    metrics_path: /metrics

  # Job 5: Grafana (UI metrics)
  - job_name: "grafana"
    static_configs:
      - targets: ["obs-grafana:3000"]
    metrics_path: /metrics

Key Concepts

Concept Meaning
Scrape Pulling metrics from a target
Scrape interval How often to pull (15s)
Target Where to scrape (container:port)
Job Group of targets
Metric A measurement (e.g., request count)
Label Metadata for filtering (e.g., method=GET)
TSDB Time Series Database (storage)
Alert rule Condition that triggers alert

How to Test

# Check if Prometheus is running
curl http://localhost:9090/-/healthy

# Query a metric
curl 'http://localhost:9090/api/v1/query?query=up'

# View all targets
curl http://localhost:9090/api/v1/targets | jq

# View alert rules
curl http://localhost:9090/api/v1/rules | jq

Official Documentation


Loki

What Is Loki?

Loki is a log aggregation system designed to be cost-effective and easy to operate. Unlike Elasticsearch, Loki only indexes metadata (labels), not the full log text. Think of it as "Prometheus, but for logs."

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           LOKI WORKFLOW                                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Promtail pushes logs                                               β”‚
β”‚  ─────────────────────────────                                              β”‚
β”‚                                                                             β”‚
β”‚      Promtail                                                               β”‚
β”‚          β”‚                                                                  β”‚
β”‚          β”‚ HTTP POST to /loki/api/v1/push                                   β”‚
β”‚          β”‚                                                                  β”‚
β”‚          β”‚ {                                                                β”‚
β”‚          β”‚   "streams": [{                                                  β”‚
β”‚          β”‚     "stream": {"app": "django", "level": "INFO"},                β”‚
β”‚          β”‚     "values": [["1773536578000000000", "GET /todo/ 200"]]        β”‚
β”‚          β”‚   }]                                                             β”‚
β”‚          β”‚ }                                                                β”‚
β”‚          β–Ό                                                                  β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Loki receives and stores                                           β”‚
β”‚  ──────────────────────────────                                             β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Loki                                            β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ 1. Parse request                                β”‚                   β”‚
β”‚      β”‚ 2. Extract labels (app, level)                  β”‚                   β”‚
β”‚      β”‚ 3. Store log line                               β”‚                   β”‚
β”‚      β”‚ 4. Update index                                 β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Loki stores data                                                   β”‚
β”‚  ────────────────────────                                                   β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Storage Structure                              β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ INDEX (Labels)                                  β”‚                   β”‚
β”‚      β”‚ ─────────────────────────────────────────────  β”‚                   β”‚
β”‚      β”‚ {app="django"} β†’ chunk_001                      β”‚                   β”‚
β”‚      β”‚ {app="django", level="ERROR"} β†’ chunk_002      β”‚                   β”‚
β”‚      β”‚ {app="django", log_type="access"} β†’ chunk_003  β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ CHUNKS (Actual log data)                        β”‚                   β”‚
β”‚      β”‚ ─────────────────────────────────────────────  β”‚                   β”‚
β”‚      β”‚ chunk_001:                                      β”‚                   β”‚
β”‚      β”‚   [1773536578000000000] "GET /todo/ 200"       β”‚                   β”‚
β”‚      β”‚   [1773536578000000001] "POST /todo/ 302"      β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ chunk_002:                                      β”‚                   β”‚
β”‚      β”‚   [1773536578000000002] "Error: DB timeout"     β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 4: Loki serves queries                                                β”‚
β”‚  ──────────────────────────                                                 β”‚
β”‚                                                                             β”‚
β”‚      Grafana β†’ GET /loki/api/v1/query?query={app="django"}                 β”‚
β”‚                                                                             β”‚
β”‚      Response:                                                              β”‚
β”‚      {                                                                      β”‚
β”‚        "status": "success",                                                 β”‚
β”‚        "data": {                                                            β”‚
β”‚          "result": [{                                                       β”‚
β”‚            "stream": {"app": "django", "level": "INFO"},                    β”‚
β”‚            "values": [["1773536578000000000", "GET /todo/ 200"]]            β”‚
β”‚          }]                                                                 β”‚
β”‚        }                                                                    β”‚
β”‚      }                                                                      β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Configuration Explained

File: loki/loki-config.yml

# Don't require authentication (development mode)
auth_enabled: false

# Server settings
server:
  http_listen_port: 3100  # Port for HTTP API

# Common configuration
common:
  instance_addr: 127.0.0.1  # Instance address
  path_prefix: /loki          # Root directory for Loki data
  storage:
    filesystem:
      chunks_directory: /loki/chunks   # Where log data is stored
      rules_directory: /loki/rules     # Where recording rules are stored
  replication_factor: 1                  # Number of replicas (1 for single node)
  ring:
    kvstore:
      store: inmemory  # Store ring info in memory

# Schema configuration (how data is organized)
schema_config:
  configs:
    - from: 2024-01-01          # Start date for this schema
      store: tsdb                # Storage backend
      object_store: filesystem   # Where to store objects
      schema: v12                # Schema version
      index:
        prefix: index_           # Index file prefix
        period: 24h              # Rotate index every 24 hours

# Rate limits
limits_config:
  retention_period: 30d         # Keep logs for 30 days
  ingestion_rate_mb: 16         # Max ingestion rate (MB/s)
  ingestion_burst_size_mb: 32   # Max burst size (MB)

Key Concepts

Concept Meaning
Stream A set of logs with same labels
Labels Metadata tags (app=django, level=ERROR)
Chunks Compressed log data
Index Label-based search index
LogQL Loki query language
Push API HTTP POST to ingest logs
Query API HTTP GET to search logs

How to Test

# Check if Loki is ready
curl http://localhost:3100/ready

# Push a test log
curl -X POST 'http://localhost:3100/loki/api/v1/push' \
  -H 'Content-Type: application/json' \
  -d '{"streams":[{"stream":{"app":"test"},"values":[["1773536578000000000","test"]]}]}'

# Query logs
curl 'http://localhost:3100/loki/api/v1/query?query={app="test"}'

# List labels
curl http://localhost:3100/loki/api/v1/label | jq

Official Documentation


Promtail

What Is Promtail?

Promtail is an agent that reads log files and sends them to Loki. Think of it as a "log mailman" - it picks up logs and delivers them.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        PROMTAIL WORKFLOW                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Watch log files                                                    β”‚
β”‚  ─────────────────────────                                                  β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ /app/logs/django.log                            β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ {"asctime":"2026-03-15",                        β”‚                   β”‚
β”‚      β”‚  "levelname":"INFO",                            β”‚                   β”‚
β”‚      β”‚  "name":"django",                               β”‚                   β”‚
β”‚      β”‚  "message":"GET /todo/ 200"}                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ ← Promtail watches this file (tail -f)         β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Parse JSON                                                         β”‚
β”‚  ──────────────────                                                         β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Pipeline Stages                                 β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Input JSON:                                     β”‚                   β”‚
β”‚      β”‚ {"levelname":"INFO","name":"django",            β”‚                   β”‚
β”‚      β”‚  "message":"GET /todo/ 200"}                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ After json parsing:                             β”‚                   β”‚
β”‚      β”‚ level = "INFO"                                  β”‚                   β”‚
β”‚      β”‚ logger = "django"                               β”‚                   β”‚
β”‚      β”‚ message = "GET /todo/ 200"                      β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ After labels extraction:                        β”‚                   β”‚
β”‚      β”‚ Labels: {app="django", env="dev", level="INFO"} β”‚                   β”‚
β”‚      β”‚ Log line: "GET /todo/ 200"                      β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Push to Loki                                                       β”‚
β”‚  ────────────────────                                                       β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ HTTP POST                                       β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ URL: http://obs-loki:3100/loki/api/v1/push     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Body:                                           β”‚                   β”‚
β”‚      β”‚ {                                               β”‚                   β”‚
β”‚      β”‚   "streams": [{                                 β”‚                   β”‚
β”‚      β”‚     "stream": {                                 β”‚                   β”‚
β”‚      β”‚       "app": "django",                          β”‚                   β”‚
β”‚      β”‚       "env": "dev",                             β”‚                   β”‚
β”‚      β”‚       "level": "INFO"                           β”‚                   β”‚
β”‚      β”‚     },                                          β”‚                   β”‚
β”‚      β”‚     "values": [                                 β”‚                   β”‚
β”‚      β”‚       ["1773536578000000000",                   β”‚                   β”‚
β”‚      β”‚        "GET /todo/ 200"]                        β”‚                   β”‚
β”‚      β”‚     ]                                           β”‚                   β”‚
β”‚      β”‚   }]                                            β”‚                   β”‚
β”‚      β”‚ }                                               β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 4: Track position                                                     β”‚
β”‚  ──────────────────────                                                     β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ positions.yaml                                  β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ /var/log/django/django.log:                     β”‚                   β”‚
β”‚      β”‚   value: 12345   ← Last byte position read     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ /var/log/django/access.log:                     β”‚                   β”‚
β”‚      β”‚   value: 67890   ← Last byte position read     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ This ensures Promtail doesn't re-send logs     β”‚                   β”‚
β”‚      β”‚ after restart                                   β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Configuration Explained

File: promtail/promtail-config.yml

# Server settings
server:
  http_listen_port: 9080    # Port for metrics endpoint
  grpc_listen_port: 0       # Disable gRPC (not needed)

# Position tracking (remembers where it left off)
positions:
  filename: /tmp/positions.yaml  # File to store read positions

# Where to send logs
clients:
  - url: http://obs-loki:3100/loki/api/v1/push  # Loki push endpoint

# What files to watch
scrape_configs:

  # Job 1: Django application logs (JSON format)
  - job_name: django                # Job name
    static_configs:
      - targets:
          - localhost               # (required but ignored)
        labels:
          app: django               # Label: app=django
          env: dev                  # Label: env=dev
          __path__: /var/log/django/django.log  # File to watch
    pipeline_stages:
      - json:                       # Parse as JSON
          expressions:
            level: levelname        # Extract "levelname" field β†’ "level" label
            logger: name            # Extract "name" field β†’ "logger" label
            message: message        # Extract "message" field
      - labels:                     # Convert to Loki labels
          level:                    # Make "level" a searchable label
          logger:                   # Make "logger" a searchable label

  # Job 2: Gunicorn access logs
  - job_name: gunicorn_access
    static_configs:
      - targets:
          - localhost
        labels:
          app: django
          log_type: access          # Label: log_type=access
          env: dev
          __path__: /var/log/django/access.log
    pipeline_stages:
      - labels:
          log_type:

  # Job 3: Gunicorn error logs
  - job_name: gunicorn_error
    static_configs:
      - targets:
          - localhost
        labels:
          app: django
          log_type: error           # Label: log_type=error
          level: ERROR              # Label: level=ERROR
          env: dev
          __path__: /var/log/django/error.log
    pipeline_stages:
      - labels:
          level:
          log_type:

Key Concepts

Concept Meaning
__path__ File path to watch
targets Required but ignored for file watching
labels Metadata tags added to logs
pipeline_stages How to process log lines
json Parse JSON format
positions.yaml Tracks last read position

How to Test

# Check Promtail logs
docker logs obs-promtail --tail 20

# Check targets
curl http://localhost:9080/targets | jq

# Verify tailing
docker logs obs-promtail | grep "tail routine"

Official Documentation


Alertmanager

What Is Alertmanager?

Alertmanager receives alerts from Prometheus and routes them to different notification channels (Slack, email, etc.). Think of it as a "dispatcher" that decides where to send messages.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      ALERTMANAGER WORKFLOW                                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Receive alert from Prometheus                                      β”‚
β”‚  ─────────────────────────────────────                                      β”‚
β”‚                                                                             β”‚
β”‚      Prometheus                                                             β”‚
β”‚          β”‚                                                                  β”‚
β”‚          β”‚ HTTP POST to /api/v1/alerts                                      β”‚
β”‚          β”‚                                                                  β”‚
β”‚          β”‚ [                                                                β”‚
β”‚          β”‚   {                                                              β”‚
β”‚          β”‚     "labels": {                                                  β”‚
β”‚          β”‚       "alertname": "DjangoDown",                                 β”‚
β”‚          β”‚       "severity": "critical",                                    β”‚
β”‚          β”‚       "category": "http"                                         β”‚
β”‚          β”‚     },                                                           β”‚
β”‚          β”‚     "annotations": {                                             β”‚
β”‚          β”‚       "summary": "Django is DOWN"                                β”‚
β”‚          β”‚     }                                                            β”‚
β”‚          β”‚   }                                                              β”‚
β”‚          β”‚ ]                                                                β”‚
β”‚          β–Ό                                                                  β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Group alerts                                                       β”‚
β”‚  ────────────────────                                                       β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Alertmanager                                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ group_by: ["alertname", "category"]             β”‚                   β”‚
β”‚      β”‚ group_wait: 30s                                 β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ If multiple alerts come within 30s:             β”‚                   β”‚
β”‚      β”‚ - Group by alertname and category               β”‚                   β”‚
β”‚      β”‚ - Send one notification per group               β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Route to receiver                                                  β”‚
β”‚  ───────────────────────                                                    β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Routing Rules                                   β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ If category=db     β†’ send to #alerts-db        β”‚                   β”‚
β”‚      β”‚ If category=http   β†’ send to #alerts-http      β”‚                   β”‚
│      │ If category=latency→ send to #alerts-latency   │                   │
β”‚      β”‚ If category=infra  β†’ send to #alerts-infra     β”‚                   β”‚
β”‚      β”‚ Otherwise          β†’ send to #alerts           β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 4: Send to Slack                                                      β”‚
β”‚  ────────────────────                                                       β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Slack Message                                   β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ πŸ”₯ [FIRING] DjangoDown                          β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ 🚨 CRITICAL β€” Django is DOWN                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ πŸ“‹ Reason: App unreachable for 10s             β”‚                   β”‚
β”‚      β”‚ 🏷️ Alert: `DjangoDown`                         β”‚                   β”‚
β”‚      β”‚ πŸ”– Category: `http`                            β”‚                   β”‚
β”‚      β”‚ πŸ• Started: 2026-03-15 10:30:00 UTC           β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Configuration Explained

File: alertmanager/alertmanager.yml

# Global settings
global:
  resolve_timeout: 5m           # Wait 5 minutes before marking as resolved
  slack_api_url: "PLACEHOLDER"  # Replaced at runtime with SLACK_WEBHOOK_URL

# Routing configuration
route:
  group_by: ["alertname", "category"]  # Group alerts by name and category
  group_wait: 30s                       # Wait 30s to collect alerts
  group_interval: 5m                    # Wait 5m between groups
  repeat_interval: 12h                  # Repeat alert every 12 hours
  receiver: "slack-default"             # Default receiver
  routes:                               # Specific routing rules
    - match:
        category: db                    # If category=db
      receiver: "slack-db"              # Send to #alerts-db

    - match:
        alertname: DjangoDown           # If DjangoDown
        category: http
      receiver: "slack-http"
      repeat_interval: 10m              # Repeat every 10 minutes

    - match:
        category: http                  # Other HTTP alerts
      receiver: "slack-http"

    - match:
        category: latency
      receiver: "slack-latency"

    - match:
        category: infrastructure
      receiver: "slack-infra"

# Receivers (notification targets)
receivers:
  - name: "slack-default"
    slack_configs:
      - channel: "#alerts"
        username: "Observability Bot"
        icon_emoji: ":bell:"
        color: '{{ if eq .Status "firing" }}#FF0000{{ else }}#36A64F{{ end }}'
        title: '[{{ .Status | toUpper }}] {{ .GroupLabels.alertname }}'
        text: |
          {{ range .Alerts }}
          {{ .Annotations.summary }}
          {{ end }}
        send_resolved: true

# Inhibit rules (suppress lower severity if higher is active)
inhibit_rules:
  - source_match:
      severity: "critical"
    target_match:
      severity: "warning"
    equal: ["alertname"]

Key Concepts

Concept Meaning
Receiver Where to send (Slack channel)
Route How to match alerts to receivers
Group Combine similar alerts
group_wait Wait before sending
repeat_interval How often to repeat
inhibit_rule Suppress lower severity alerts

How to Test

# Check Alertmanager status
curl http://localhost:9093/api/v1/status | jq

# View active alerts
curl http://localhost:9093/api/v1/alerts | jq

# Send test alert
curl -X POST http://localhost:9093/api/v1/alerts \
  -H "Content-Type: application/json" \
  -d '[{"labels":{"alertname":"Test","severity":"warning","category":"http"},"annotations":{"summary":"Test alert"}}]'

Official Documentation


Grafana

What Is Grafana?

Grafana is a visualization platform that creates dashboards from your data. Think of it as a "TV" that shows channels from Prometheus and Loki.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         GRAFANA WORKFLOW                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Connect to datasources                                             β”‚
β”‚  ────────────────────────────────                                           β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Grafana                                         β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Datasources:                                    β”‚                   β”‚
β”‚      β”‚ - Prometheus: http://obs-prometheus:9090        β”‚                   β”‚
β”‚      β”‚ - Loki: http://obs-loki:3100                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Configured in:                                  β”‚                   β”‚
β”‚      β”‚ grafana/provisioning/datasources/datasources.ymlβ”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Query datasources                                                  β”‚
β”‚  ───────────────────────                                                    β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ PromQL (Prometheus)                             β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ rate(django_http_requests_total[5m])            β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ This calculates requests per second over 5 min  β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ LogQL (Loki)                                    β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ {app="django", level="ERROR"}                   β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ This returns all error logs from Django         β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Display dashboards                                                 β”‚
β”‚  ────────────────────────                                                   β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Dashboard Panels                               β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚                   β”‚
β”‚      β”‚ β”‚ Request Rate β”‚  β”‚ Error Rate   β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β”Œβ”€β”€β”€β”     β”‚  β”‚    β”Œβ”€β”€β”€β”     β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β”‚   β”‚     β”‚  β”‚    β”‚   β”‚     β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β”‚   β”‚     β”‚  β”‚    β”‚   β”‚     β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β””β”€β”€β”€β”˜     β”‚  β”‚    β””β”€β”€β”€β”˜     β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚  0.5 req/s   β”‚  β”‚  0.01%       β”‚             β”‚                   β”‚
β”‚      β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚                   β”‚
β”‚      β”‚ β”‚ P95 Latency  β”‚  β”‚ Recent Logs  β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β”Œβ”€β”€β”€β”     β”‚  β”‚ INFO: GET    β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β”‚   β”‚     β”‚  β”‚ ERROR: ...   β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚    β””β”€β”€β”€β”˜     β”‚  β”‚              β”‚             β”‚                   β”‚
β”‚      β”‚ β”‚   45.2ms     β”‚  β”‚              β”‚             β”‚                   β”‚
β”‚      β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Configuration Explained

File: grafana/provisioning/datasources/datasources.yml

# Datasource provisioning format
apiVersion: 1

# Delete old datasources on startup (clean slate)
deleteDatasources:
  - name: Prometheus
    orgId: 1
  - name: Loki
    orgId: 1

# Define datasources
datasources:

  # Prometheus datasource
  - name: Prometheus                    # Display name
    type: prometheus                    # Datasource type
    uid: prometheus                     # Unique identifier
    orgId: 1                            # Organization ID
    access: proxy                       # Access mode (proxy = through Grafana)
    url: http://obs-prometheus:9090    # Prometheus URL
    isDefault: true                     # Default datasource
    editable: false                     # Can't edit in UI
    version: 1                          # Version number

  # Loki datasource
  - name: Loki
    type: loki
    uid: loki
    orgId: 1
    access: proxy
    url: http://obs-loki:3100
    isDefault: false
    editable: false
    version: 1
    jsonData:
      maxLines: 1000                    # Max lines to display

Key Concepts

Concept Meaning
Datasource Data source connection
Panel Dashboard widget
Dashboard Collection of panels
PromQL Prometheus query language
LogQL Loki query language
Provisioning Auto-configure on startup

How to Test

# Check Grafana health
curl http://localhost:3000/api/health

# List datasources
curl -u admin:admin http://localhost:3000/api/datasources | jq

# Query Prometheus via Grafana
curl -u admin:admin 'http://localhost:3000/api/datasources/uid/prometheus/api/v1/query?query=up'

Official Documentation


Node Exporter

What Is Node Exporter?

Node Exporter collects host-level metrics (CPU, memory, disk, network). Think of it as a "stethoscope" for your server.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      NODE EXPORTER WORKFLOW                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  Step 1: Read host metrics                                                  β”‚
β”‚  ─────────────────────────                                                  β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ Node Exporter                                   β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ Reads from:                                     β”‚                   β”‚
β”‚      β”‚ - /proc (CPU, memory, network)                  β”‚                   β”‚
β”‚      β”‚ - /sys (system information)                     β”‚                   β”‚
β”‚      β”‚ - / (disk usage)                                β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 2: Expose metrics                                                     β”‚
β”‚  ──────────────────────                                                     β”‚
β”‚                                                                             β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚      β”‚ GET :9100/metrics                               β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP node_cpu_seconds_total                   β”‚                   β”‚
β”‚      β”‚ node_cpu_seconds_total{cpu="0",mode="idle"}     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP node_memory_MemTotal_bytes               β”‚                   β”‚
β”‚      β”‚ node_memory_MemTotal_bytes 8589934592           β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP node_filesystem_avail_bytes              β”‚                   β”‚
β”‚      β”‚ node_filesystem_avail_bytes{mountpoint="/"}     β”‚                   β”‚
β”‚      β”‚                                                 β”‚                   β”‚
β”‚      β”‚ # HELP node_network_receive_bytes_total         β”‚                   β”‚
β”‚      β”‚ node_network_receive_bytes_total{device="eth0"} β”‚                   β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                             β”‚
β”‚  Step 3: Prometheus scrapes                                                 β”‚
β”‚  ────────────────────────                                                   β”‚
β”‚                                                                             β”‚
β”‚      Prometheus β†’ GET :9100/metrics β†’ Store in TSDB                        β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Docker Configuration Explained

obs-node-exporter:
  image: prom/node-exporter:latest
  pid: host                      # Share host's PID namespace
  volumes:
    - /proc:/host/proc:ro        # Mount host /proc (CPU, memory, processes)
    - /sys:/host/sys:ro          # Mount host /sys (system info)
    - /:/rootfs:ro               # Mount host filesystem (disk usage)
  command:
    - "--path.procfs=/host/proc"                                        # Where to read proc
    - "--path.sysfs=/host/sys"                                          # Where to read sys
    - "--path.rootfs=/rootfs"                                           # Where to read rootfs
    - "--collector.filesystem.mount-points-exclude=^/(sys|proc|..."     # Exclude system mounts

Key Metrics

Metric Description
node_cpu_seconds_total CPU usage by mode
node_memory_MemAvailable_bytes Available memory
node_memory_MemTotal_bytes Total memory
node_filesystem_avail_bytes Available disk space
node_network_receive_bytes_total Network traffic received

How to Test

# View all metrics
curl http://localhost:9100/metrics

# View CPU metrics
curl http://localhost:9100/metrics | grep node_cpu

# View memory metrics
curl http://localhost:9100/metrics | grep node_memory

Official Documentation


Data Flow Summary

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        COMPLETE DATA FLOW                                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  METRICS (Pull Model)                                                       β”‚
β”‚  ─────────────────────                                                      β”‚
β”‚                                                                             β”‚
β”‚  Django β†’ GET /metrics β†’ Prometheus β†’ Alertmanager β†’ Slack                  β”‚
β”‚                 ↓                                                           β”‚
β”‚            Grafana                                                           β”‚
β”‚                                                                             β”‚
β”‚  LOGS (Push Model)                                                          β”‚
β”‚  ─────────────────                                                          β”‚
β”‚                                                                             β”‚
β”‚  Django β†’ Log File β†’ Promtail β†’ POST /loki/api/v1/push β†’ Loki             β”‚
β”‚                                                      ↓                      β”‚
β”‚                                                  Grafana                    β”‚
β”‚                                                                             β”‚
β”‚  HOST METRICS (Pull Model)                                                  β”‚
β”‚  ─────────────────────────                                                  β”‚
β”‚                                                                             β”‚
β”‚  Host System β†’ Node Exporter β†’ GET /metrics β†’ Prometheus                   β”‚
β”‚                                                     ↓                       β”‚
β”‚                                                 Grafana                     β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quick Reference

Tool Port URL Purpose
Prometheus 9090 http://localhost:9090 Metrics collection
Loki 3100 http://localhost:3100 Log storage
Alertmanager 9093 http://localhost:9093 Alert routing
Grafana 3000 http://localhost:3000 Dashboards
Node Exporter 9100 http://localhost:9100 Host metrics
pgAdmin 5050 http://localhost:5050 DB admin
Django 9000 http://localhost:9000 Application