Prometheus is an open-source monitoring system built around one idea: it pulls numeric measurements (metrics) from your systems at fixed intervals and stores them as a time series. Instead of watching dashboards waiting for something to look wrong, you write rules that fire alerts when a measurement crosses a threshold.

The pull model

Application / Exporter  <-- Prometheus scrapes every 15s
        |                        via HTTP GET /metrics
        v
   time series DB  -->  Alertmanager  -->  Email / Chat / Pager
                    -->  Grafana       -->  Dashboards
                    -->  long-term storage (Thanos, VictoriaMetrics)

Key point: Prometheus PULLS. Your service does not need to know
where the monitoring lives - it just has to expose /metrics.

The four metric types

  • Counter — only increases (requests served, errors, bytes sent). Read it with rate(), never use the raw value.
  • Gauge — can go up and down (temperature, memory used, active connections).
  • Histogram — buckets of observed values, used for request durations. Gives you averages and percentiles.
  • Summary — pre-calculated quantiles on the client side. Cheaper to query but quantiles cannot be aggregated across instances.

The query language: PromQL

# HTTP requests per second, averaged over 5 minutes
rate(http_requests_total[5m])

# Only errors
sum(rate(http_requests_total{status=~"5.."}[5m]))

# Error ratio as a percentage
100 * sum(rate(http_requests_total{status=~"5.."}[5m]))
    / sum(rate(http_requests_total[5m]))

# 95th percentile response time
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

# Memory usage below 5% free
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 5

# Predict disk exhaustion within 4 hours
predict_linear(node_filesystem_avail_bytes[1h], 4 * 3600) < 0

# Absent = the target stopped reporting (a metric that should exist)
absent(up{job="api"} == 1)

# A target that is down
up == 0

Alert rules

groups:
  - name: availability
    rules:
      - alert: InstanceDown
        expr: up == 0
        for: 5m                    # must be true for 5 minutes
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.instance }} is down"
          description: "Target has been unreachable for 5 minutes."

      - alert: HighErrorRate
        expr: |
          100 * sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
              / sum(rate(http_requests_total[5m])) by (job) > 5
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.job }} error rate above 5%"

# THE most important rule, often forgotten:
      - alert: NoData
        expr: absent(up{job="api"})
        for: 10m
        labels: {severity: critical}
        annotations:
          summary: "No metrics arriving from api"

Alert on symptoms, not causes. "The API returns 5 % errors" is actionable. "CPU is above 80 %" is often noise. If CPU is high and users are happy, nobody should be paged.

Instrumenting your own service

# Python - the four golden signals, plus a business metric
from prometheus_client import Counter, Histogram, Gauge, start_http_server

anfragen = Counter("app_requests_total", "Requests", ["method", "path", "status"])
dauer    = Histogram("app_request_duration_seconds", "Duration", ["path"],
                     buckets=(.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5))
laufend  = Gauge("app_requests_in_progress", "In progress")
auftraege = Counter("app_orders_total", "Completed orders", ["status"])

@dauer.labels(path="/api/orders").time()      # decorator measures duration
def bestellungen_holen():
    laufend.inc()
    try:
        ...
        anfragen.labels("GET", "/api/orders", "200").inc()
    except Exception:
        anfragen.labels("GET", "/api/orders", "500").inc()
        raise
    finally:
        laufend.dec()

if __name__ == "__main__":
    start_http_server(9100)                   # /metrics on :9100
    ...

# Cardinaility warning: labels are multiplied together.
#   path="/api/orders/12345"  -> one series PER ORDER ID -> thousands
#   of useless series. Use route templates: path="/api/orders/:id".

Exporters for things that do not speak Prometheus

  • node_exporter — CPU, memory, disk, network of a Linux host.
  • cadvisor — per-container metrics.
  • postgres_exporter / mysqld_exporter — database internals.
  • blackbox_exporter — probes HTTP/TCP/ICMP endpoints from outside.
  • snmp_exporter — network devices.
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - /etc/prometheus/rules/*.yml

alerting:
  alertmanagers:
    - static_configs: [{targets: ["localhost:9093"]}]

scrape_configs:
  - job_name: node
    static_configs:
      - targets: ["web01:9100", "web02:9100"]
  - job_name: api
    metrics_path: /metrics
    static_configs:
      - targets: ["api:8080"]

# Validate before reloading - catches YAML and PromQL errors
promtool check config /etc/prometheus/prometheus.yml
promtool check rules /etc/prometheus/rules/*.yml
curl -X POST http://localhost:9090/-/reload

Limitations worth knowing

  1. Not for long-term storage. Local storage is sized in weeks. For years of history add Thanos, Mimir or VictoriaMetrics.
  2. Not for logs or traces. That is Loki, Elasticsearch or Tempo. Prometheus handles numbers only.
  3. No exact per-request data. Metrics are aggregated; you cannot ask "show me the 5 slowest requests".
  4. Cardinality is the killer. Every unique label combination is a separate series. User IDs, request IDs and raw URLs in labels will bring Prometheus down. This is the number one operational mistake.
  5. Alert fatigue is a configuration problem, not a tool problem. Fewer, better alerts with meaningful thresholds and for: durations beat hundreds of noisy ones.

Alert on what users feel: error rate, latency, saturation, availability. Everything else supports the diagnosis, it does not deserve a page.

Related: Monitoring, Cache, systemd, Monitoring mit Prometheus (DE).