AI EngineeringZero to ProductionHome·About·Contact
Kubernetes Orchestration · Part 8

Observability & SLOs

You cannot operate what you cannot see. This lesson turns a cluster from a black box into an instrumented system: the four signals (metrics, logs, traces, events), Prometheus for metrics + PromQL, Grafana for dashboards, Loki for logs, OpenTelemetry for distributed traces, and kube-state-metrics for object state. Then the part that matters to your users: defining SLIs/SLOs, spending an error budget, and wiring Alertmanager so you page on symptoms, not noise.

⏱️ ~110 min📈 Observability🎯 Advanced→Production
🌱 Honesty up front: this needs a cluster + moving partsEvery kubectl/Helm/PromQL/YAML sample here needs a cluster (a local minikube/kind for the Prometheus stack, or EKS) — terminal output is illustrative. The Prometheus Operator, kube-prometheus-stack, Loki, and OpenTelemetry Collector move fast: verify CRD names, field paths and chart values against their current docs. The SLO math (error budget, burn rate) is real arithmetic and is runnable offline in plain Python — that part does not need a cluster.

Learning objectives

  • Name the four signals (metrics, logs, traces, events) and what each answers.
  • Deploy a Prometheus + Grafana stack (conceptually) and scrape a service with a ServiceMonitor.
  • Write PromQL for a rate, an error ratio, and a p95 latency from a histogram.
  • Explain kube-state-metrics vs node/cAdvisor metrics — object state vs resource usage.
  • Aggregate logs with Loki and correlate a trace across services with OpenTelemetry.
  • Define an SLI/SLO, compute an error budget and burn rate, and alert on symptoms via Alertmanager.

1 · The four signals — what each one answers

Observability is not one thing. Four kinds of telemetry answer four different questions, and a mature setup collects all four and correlates them. Metrics tell you that something is wrong; traces and logs tell you where and why.

Metrics is it healthy? Logs what happened? Traces where's the time? Events what did k8s do?
SignalAnswersCardinality / costTool here
MetricsIs it healthy? How much? How fast?Cheap, aggregated, low-cardinalityPrometheus
LogsWhat exactly happened on this request?Expensive, high-volume textLoki
TracesWhere did the latency go across services?Sampled spans, medium costOpenTelemetry
EventsWhat did Kubernetes itself do (schedule, evict, OOM)?Low volume, short TTLkubectl events / kube-state-metrics
Metrics for alerting, traces/logs for debuggingA common trap is to alert on logs (expensive, noisy) or to try to debug from metrics alone (no per-request detail). The healthy split: alert on metrics (cheap, low-cardinality symptoms), then drill into traces and logs for the specific failing request. High-cardinality labels (user IDs, request IDs) belong in logs/traces, never in Prometheus metric labels — they explode series count and can OOM Prometheus.

2 · Prometheus: scrape, store, query

Prometheus is a pull-based time-series database: it periodically scrapes an HTTP /metrics endpoint on each target, stores samples, and answers PromQL queries. In Kubernetes the Prometheus Operator (shipped in the kube-prometheus-stack Helm chart) lets you declare scrape targets as CRDs — a ServiceMonitor — instead of editing raw config.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
bash · install the kube-prometheus-stack (needs a cluster: minikube/kind local, or EKS)
install.sh# Verify chart name/version and values against the current kube-prometheus-stack docs.
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kps prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace
# This bundles Prometheus, Alertmanager, Grafana, node-exporter and kube-state-metrics.

Now point Prometheus at your app. Your app must expose /metrics (most languages have a Prometheus client). A ServiceMonitor selects the Service by label and tells Prometheus which port/path to scrape:

yaml · a ServiceMonitor scraping your app's /metrics (needs a cluster; verify CRD fields vs docs)
servicemonitor.yamlapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: llm-api
  namespace: monitoring
  labels:
    release: kps          # must match the Prometheus serviceMonitorSelector
spec:
  selector:
    matchLabels:
      app: llm-api        # selects the Service to scrape
  namespaceSelector:
    matchNames: [default]
  endpoints:
    - port: http          # the named Service port
      path: /metrics
      interval: 15s
The label link is the #1 gotchaA ServiceMonitor is only picked up if its labels match the Prometheus serviceMonitorSelector (often release: <helm-release>), and its selector must match your Service's labels, and the port must be the Service's named port. If a target isn't scraped, check Status → Targets in the Prometheus UI first. Verify these field names against the current Operator docs.

PromQL basics. Counters only go up, so you almost always wrap them in rate() over a window. Three queries you will write constantly:

promql · request rate, error ratio, and p95 latency (illustrative — needs live metrics)
queries.promql# 1) Requests per second, per route, averaged over 5m:
sum by (route) (rate(http_requests_total[5m]))

# 2) Error ratio (5xx as a fraction of all requests) — an SLI:
sum(rate(http_requests_total{status=~"5.."}[5m]))
  /
sum(rate(http_requests_total[5m]))

# 3) p95 latency from a histogram (buckets recorded by the client lib):
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
route         value
/chat         42.7
/embed        11.3
# error ratio -> 0.004  (0.4%)
# p95 -> 0.82  (820 ms)

3 · kube-state-metrics, node metrics & Grafana

Two different metric sources are easy to confuse. node-exporter / cAdvisor report resource usage — CPU, memory, disk of nodes and containers. kube-state-metrics reports object state from the API server — how many replicas a Deployment wants vs has ready, whether a Pod is Pending, how many restarts a container has. You need both: usage tells you the machine is hot; state tells you the desired vs actual gap.

QuestionWhich sourceExample metric
Is a node out of memory?node-exporternode_memory_MemAvailable_bytes
Is a container using its whole limit?cAdvisorcontainer_memory_working_set_bytes
Are all Deployment replicas ready?kube-state-metricskube_deployment_status_replicas_ready
Is a Pod stuck Pending / CrashLooping?kube-state-metricskube_pod_status_phase, kube_pod_container_status_restarts_total

Grafana turns PromQL into dashboards. The stack ships useful defaults; the skill is building a focused service dashboard around your SLIs (rate, errors, latency — the 'RED' method) rather than 200 panels no one reads. Provision dashboards as code (a ConfigMap the Grafana sidecar picks up) so they live in Git.

RED for services, USE for resourcesTwo mnemonics keep dashboards honest. RED (Rate, Errors, Duration) for request-driven services — the three panels that map directly to user pain. USE (Utilization, Saturation, Errors) for resources like nodes and queues. Start every service dashboard with RED at the top; everything else is drill-down.

4 · Logs with Loki, traces with OpenTelemetry

Loki is 'Prometheus for logs': it indexes only labels (app, namespace, pod) — not the full log text — which makes it cheap to run. You query with LogQL, which looks like PromQL. An agent (Promtail / Grafana Alloy) ships pod stdout to Loki, and because it uses the same labels as your metrics, you can jump from a spiking error graph straight to the matching logs in Grafana.

logql · find errors for one app in the last window (illustrative — needs Loki)
logs.logql# Label filter (indexed, fast) then a line filter (scanned):
{app="llm-api", namespace="default"} |= "error" | json | status >= 500

# Rate of error lines per pod (LogQL metric query):
sum by (pod) (rate({app="llm-api"} |= "error" [5m]))

Distributed tracing answers the question metrics can't: for this slow request, which service or call ate the time? A request is assigned a trace ID; each service records spans (start/end + attributes) that nest into a waterfall. OpenTelemetry (OTel) is the vendor-neutral standard — instrument once with the OTel SDK, export via the OTel Collector to any backend (Tempo, Jaeger). For a RAG/agent app this is gold: you see retrieve → rerank → LLM-call as separate spans and learn the LLM call is 90% of latency.

App + OTel SDK emits spans OTel Collector batch/route Tempo / Jaeger store traces Grafana waterfall view
Correlate by shared IDs, sample by costThe payoff of the three signals is correlation: a metric alert links to the trace ID, the trace links to the exact logs. Make that possible by propagating trace_id into log lines and using consistent labels. Because storing every span is expensive, use tail-based sampling (keep all traces that erred or were slow, sample the rest) — configure it in the Collector. Verify OTel Collector processor names against current docs; they change.

5 · SLIs, SLOs, error budgets & burn rate

Now the part that decides what you page on. An SLI (indicator) is a measured ratio of good events to total — e.g. the fraction of requests served under 500 ms without a 5xx. An SLO (objective) is a target for that SLI over a window — e.g. 99.9% over 28 days. The gap between 100% and the SLO is your error budget: at 99.9%, you may 'spend' 0.1% of requests failing before you've broken your promise. This reframes reliability from 'never fail' to 'fail within budget' — and gives you a data-driven brake on shipping.

TermDefinitionExample
SLIA measured quality ratio (good / total)% requests < 500 ms and non-5xx
SLOTarget for the SLI over a window99.9% over 28 days
Error budgetAllowed failure = 1 − SLO0.1% of requests may fail
Burn rateHow fast you're spending the budget vs 'even' pace14.4× = budget gone in 2 days

The error budget and burn rate are just arithmetic — here it is as offline-runnable Python (no cluster needed), the same math Prometheus alert rules encode:

python · error budget + multi-window burn-rate alerting (offline-runnable)
error_budget.pySLO = 0.999                      # 99.9% success objective
WINDOW_DAYS = 28
budget = 1 - SLO                 # 0.001 -> 0.1% of requests may fail

def burn_rate(bad, total):
    """How many times faster than the 'even' pace we are spending budget."""
    if total == 0:
        return 0.0
    error_ratio = bad / total
    return error_ratio / budget  # 1.0 = exactly on pace to exhaust budget over the window

def budget_remaining(bad_28d, total_28d):
    spent = (bad_28d / total_28d) / budget if total_28d else 0.0
    return max(0.0, 1 - spent)   # fraction of the error budget still available

# A fast-burn page: 2% errors over the last 5m is 20x the budget pace.
print(round(burn_rate(bad=20, total=1000), 1))          # -> 20.0  (page now)
print(round(budget_remaining(bad_28d=1200, total_28d=2_000_000) * 100, 1))  # -> 40.0% left
20.0
40.0
Multi-window, multi-burn-rate alertingAlerting on 'error ratio > 0' is noise; alerting only on the 28-day SLO is too slow. The Google SRE pattern is multi-window multi-burn-rate: page on a fast burn (e.g. 14.4× over 1h and 5m) which would exhaust the budget in ~2 days, and open a ticket on a slow burn (e.g. 3× over 6h). This pages you for real, fast damage and files calmer work for slow leaks. Verify exact multipliers against the SRE workbook.

6 · Alerting that pages on symptoms, not causes

Prometheus records alert conditions as rules; Alertmanager handles them — grouping related alerts into one notification, routing by severity/team, silencing during maintenance, and inhibiting (suppress the 100 downstream alerts when the one root-cause alert fires). The philosophy that keeps on-call sane: alert on symptoms your users feel (SLO burn, high error ratio, high latency) — not on every cause (one node at 80% CPU is not a page).

yaml · a burn-rate PrometheusRule that pages on symptoms (needs a cluster; verify CRD vs docs)
prometheusrule.yamlapiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: llm-api-slo
  labels: { release: kps }
spec:
  groups:
    - name: slo-burn
      rules:
        - alert: FastErrorBudgetBurn
          # 5xx ratio > 14.4x the 0.1% budget, confirmed over 1h and 5m windows.
          expr: |
            (sum(rate(http_requests_total{status=~"5.."}[1h])) / sum(rate(http_requests_total[1h])) > 14.4*0.001)
            and
            (sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 14.4*0.001)
          for: 2m
          labels: { severity: page }
          annotations:
            summary: "Fast error-budget burn on llm-api"
yaml · Alertmanager routing: page on severity=page, ticket otherwise (verify vs docs)
alertmanager.yamlroute:
  receiver: default-ticket
  group_by: [alertname, namespace]
  routes:
    - matchers: [ 'severity = page' ]
      receiver: pagerduty-oncall
inhibit_rules:
  - source_matchers: [ 'severity = page' ]
    target_matchers: [ 'severity = ticket' ]
    equal: [namespace]        # if we're already paging, don't also file tickets for the same ns
receivers:
  - name: default-ticket
  - name: pagerduty-oncall
    # pagerduty_configs / webhook_configs go here — verify fields vs current docs
Every page must be actionableThe fastest way to burn out an on-call rotation is alerts that fire but need no action ('CPU high', 'a pod restarted'). If a page can't be tied to a human action, it should be a dashboard or a ticket, not a page. Tie pages to SLO burn so you're woken only when users are actually being hurt fast — and every page links to a runbook (K12). Verify Alertmanager routing/inhibit syntax against current docs.

✓ Checkpoint — you can move on when you can…

  • Name the four signals and which one you alert on vs debug with.
  • Write a ServiceMonitor and explain the label link to Prometheus and to the Service.
  • Write PromQL for a rate, an error ratio, and a p95 from a histogram.
  • Distinguish kube-state-metrics (object state) from node/cAdvisor (resource usage).
  • Define an SLI/SLO, compute an error budget and burn rate, and say why you page on symptoms.
  • Explain multi-window multi-burn-rate alerting and Alertmanager grouping/inhibition.
✓ Knowledge check

A teammate proposes an alert: node_cpu utilisation > 80% for 5m → page on-call. Why is this a poor page, and what should page instead?

Show answer
High node CPU is a cause, not a symptom — a node can sit at 85% CPU while every user request is fast and successful, so this pages you when nothing is wrong (alert fatigue) and stays silent when a downstream dependency is failing at low CPU. Page on what users feel: an SLO error-budget burn (e.g. 5xx ratio or p95 latency exceeding the burn-rate threshold). High CPU belongs on a dashboard or at most a low-severity ticket for capacity planning. Rule of thumb: page on symptoms (RED/SLO), investigate causes (USE/CPU/memory).
✓ Knowledge check

Your Prometheus keeps OOMing after a deploy. Someone added a request_id label to http_requests_total. What's the connection, and what's the fix?

Show answer
Cardinality explosion. Every unique label combination is a separate time series; a request_id is unbounded (a new value per request), so the series count grows without limit and Prometheus's memory blows up. Prometheus labels must be low-cardinality (route, method, status — bounded sets). The fix: remove request_id from the metric; if you need per-request detail, put it in logs (Loki) or traces (OTel), which are built for high cardinality, and correlate by trace_id.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Expose and scrape a serviceBeginner

Context: Nothing is observable until something scrapes it.

Your task: Describe how to get an app's custom metrics into Prometheus in a kube-prometheus-stack cluster.

Requirements:

  • App exposes a Prometheus /metrics endpoint
  • A ServiceMonitor selects the Service and names the port/path
  • Note the label that links the ServiceMonitor to Prometheus
  • Label it as needing a cluster

💡 Hint: The ServiceMonitor's own labels must match Prometheus's serviceMonitorSelector.

Show solution

The app imports a Prometheus client and serves /metrics on a named Service port (e.g. http). Then a ServiceMonitor wires it in:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: llm-api
  labels: { release: kps }   # matches Prometheus serviceMonitorSelector
spec:
  selector: { matchLabels: { app: llm-api } }   # matches the Service
  endpoints: [ { port: http, path: /metrics, interval: 15s } ]

Three links must all line up: ServiceMonitor label → Prometheus selector; ServiceMonitor selector → Service labels; port → the Service's named port. If it isn't scraped, check Prometheus → Status → Targets. Needs a cluster; verify CRD fields vs current docs.

Exercise 2 · Write the three RED queriesIntermediate

Context: The RED method (Rate, Errors, Duration) is the backbone of any service dashboard.

Your task: Write PromQL for request rate per route, the 5xx error ratio, and p95 latency, and say which is your SLI.

Requirements:

  • A rate() over a counter for throughput
  • An error ratio as a fraction of total
  • A p95 from a histogram via histogram_quantile
  • Identify which one(s) you'd base an SLO on

💡 Hint: Counters always need rate(); percentiles come from *_bucket series.

Show solution
# Rate per route
sum by (route) (rate(http_requests_total[5m]))

# Error ratio (an SLI)
sum(rate(http_requests_total{status=~"5.."}[5m]))
 /
sum(rate(http_requests_total[5m]))

# p95 latency (an SLI)
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

SLIs: the error ratio and p95 latency are the user-facing quality signals you'd set SLOs on (e.g. availability 99.9%, latency p95 < 500 ms). Raw rate is throughput context, not a quality target. Illustrative — needs live metrics.

Exercise 3 · Correlate a slow request across metrics, traces, logsAdvanced

Context: A metric tells you latency spiked; only traces+logs tell you why.

Your task: Design how you'd go from a p95-latency alert to the exact failing code path using all three signals.

Requirements:

  • Start from a metric/SLO alert
  • Use a trace to find where time went across services
  • Jump to the matching logs by a shared ID
  • Say what makes this correlation possible

💡 Hint: Propagate trace_id into log lines and keep labels consistent.

Show solution

Flow: (1) A p95-latency SLO alert fires (metric). (2) Open the service's traces and filter to slow traces in that window — the waterfall shows one span (say the LLM call, or a DB query) dominating. (3) That span carries a trace_id; in Grafana, pivot to Loki filtered by that trace_id to read the exact log lines for that request.

What makes it work: you must (a) instrument with OpenTelemetry so spans exist, (b) propagate trace_id and log it into every log line, and (c) use consistent labels (app, namespace) across metrics, traces and logs. Without shared IDs the three signals are three silos. Use tail-based sampling so slow/errored traces are always kept. Verify OTel Collector config vs current docs.

Exercise 4 · Define an SLO and its multi-burn-rate alertsExpert

Context: Choosing what to page on is an engineering decision, not a default.

Your task: Define an availability SLO for an LLM API and design the burn-rate alerts, computing the fast-burn threshold.

Requirements:

  • State the SLI, SLO target and window
  • Compute the error budget
  • Design fast-burn (page) and slow-burn (ticket) alerts with windows
  • Explain why single-threshold alerting is wrong

💡 Hint: Fast burn ≈ 14.4× budget over 1h exhausts a 28-day budget in ~2 days.

Show solution

SLI: fraction of requests non-5xx. SLO: 99.9% over 28 days. Error budget = 1 − 0.999 = 0.1% of requests may fail.

Multi-window multi-burn-rate:

# Fast burn -> PAGE: 14.4x budget, confirmed on 1h AND 5m windows (~2 days to exhaust)
err_ratio_1h > 14.4*0.001 and err_ratio_5m > 14.4*0.001

# Slow burn -> TICKET: ~3x budget on 6h AND 30m windows (a slow leak)
err_ratio_6h > 3*0.001 and err_ratio_30m > 3*0.001

Why single-threshold is wrong: a fixed 'errors > X%' either flaps on brief blips (too sensitive) or misses a slow leak that quietly drains the budget over days (too slow). Two windows confirm the burn is real (not a 5-min blip) and two burn rates separate 'wake a human now' from 'file work for tomorrow'. Verify multipliers vs the Google SRE workbook.

Exercise 5 · Cut an alert storm down to actionable pagesProfessional

Context: A team is drowning: one bad deploy fires 60 alerts and pages on-call 12 times.

Your task: Redesign the alerting so a single incident produces one actionable page, using Alertmanager features.

Requirements:

  • Move paging onto SLO symptoms, not causes
  • Use grouping to collapse related alerts
  • Use inhibition so root-cause suppresses downstream
  • Keep non-actionable signals off the pager

💡 Hint: group_by + inhibit_rules + severity routing do most of the work.

Show solution

1 · Page on symptoms. Replace cause-based pages (CPU, single-pod restarts) with a small set of SLO burn-rate pages per service. A node at 80% CPU with healthy SLIs pages no one.

2 · Group. group_by: [alertname, namespace] so 30 pods failing the same way arrive as one notification, not 30.

3 · Inhibit. When the service-level severity=page alert fires, an inhibit rule suppresses the downstream severity=ticket alerts for the same namespace — you get the root cause, not the 60 symptoms.

route: { group_by: [alertname, namespace], routes:
  [ { matchers: ['severity=page'], receiver: pagerduty } ] }
inhibit_rules:
  - source_matchers: ['severity=page']
    target_matchers: ['severity=ticket']
    equal: [namespace]

4 · Everything non-actionable becomes a dashboard panel or a low-severity ticket. Net effect: one incident → one page → one runbook. Verify Alertmanager syntax vs current docs.

Exercise 6 · Design the observability + SLO stack for an LLM platformIndustry scenario

Context: Representative scenario: you own an LLM/RAG platform on EKS serving several product teams, and leadership wants 'real reliability, not vanity dashboards'.

Your task: Produce an end-to-end observability and SLO design: signals collected, stack, SLOs, alerting, and the failure modes each choice defends against.

Requirements:

  • All four signals with the right tool and cost stance
  • SLIs/SLOs per user-facing surface with error budgets
  • Symptom-based, multi-burn-rate alerting wired to on-call
  • Trace/log correlation for fast RCA
  • Name the top failure modes each choice defends against

💡 Hint: Metrics for alerting; traces+logs for RCA; SLOs decide what's worth a human.

Show solution

Signals & stack: kube-prometheus-stack for metrics (Prometheus + Grafana + kube-state-metrics + node-exporter), Loki for logs (label-indexed, cheap), OpenTelemetry → Tempo/Jaeger for traces with tail-based sampling (keep slow/errored). Keep high-cardinality IDs out of metrics; they live in logs/traces.

SLOs: per surface (chat, embeddings, RAG query) define availability + latency SLIs with explicit targets and 28-day windows; publish error budgets and use them as a release brake — if the budget is spent, freeze risky changes and spend the sprint on reliability.

Alerting: multi-window multi-burn-rate pages on SLO burn only; Alertmanager groups and inhibits so one incident = one page; everything else is dashboard/ticket. Every page links a K12 runbook.

RCA: shared trace_id across metrics→traces→logs lets on-call go from 'p95 alert' to the exact failing span and its logs in minutes — critical for RAG, where the LLM call often dominates latency.

Failure modes defended: cardinality explosion (IDs kept out of metrics) → Prometheus stays up; alert fatigue (symptom + grouping + inhibition) → on-call trusts pages; blind debugging (correlation) → fast RCA; shipping-into-the-ground (error budgets) → reliability has teeth. Verify every CRD/chart/Collector detail against current docs — this stack drifts.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in