Your Kubernetes app is running. But is it healthy? Is it slow? Are any Pods crashing? How much memory is it using?

Without monitoring, you find out about problems when users complain. With monitoring, you know before they do.

The industry standard for Kubernetes monitoring is Prometheus + Grafana. Prometheus collects and stores metrics. Grafana visualizes them in dashboards.

The Observability Stack

Observability has three pillars:

  • Metrics — numbers over time (CPU usage, request count, error rate)
  • Logs — what happened and when (application output, events)
  • Traces — how a request flows through multiple services

This tutorial focuses on metrics with Prometheus and Grafana. Logging (Fluent Bit) and tracing (OpenTelemetry + Jaeger) are separate topics.

How Prometheus Works

Prometheus uses a pull model. Instead of your app sending metrics to Prometheus, Prometheus scrapes metrics from your app on a schedule.

Your app → exposes /metrics endpoint
Prometheus → scrapes /metrics every 15 seconds
Grafana → queries Prometheus to build dashboards

Your app exposes metrics in Prometheus format:

# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1234
http_requests_total{method="POST",status="500"} 5

Kubernetes components (kubelet, API server, nodes) already expose metrics in this format.

Installing kube-prometheus-stack

The easiest way to set up Prometheus, Grafana, and all required components is with the kube-prometheus-stack Helm chart. It installs:

  • Prometheus (metrics storage and alerting)
  • Grafana (dashboards)
  • Alertmanager (alert routing)
  • node-exporter (node-level metrics)
  • kube-state-metrics (Kubernetes object metrics)
  • Pre-built dashboards for Kubernetes
# Add the Prometheus community Helm repository
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Create a namespace for monitoring
kubectl create namespace monitoring

# Install kube-prometheus-stack
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set grafana.adminPassword=securepassword123

Wait for all Pods to be ready:

kubectl get pods -n monitoring -w
NAME                                                  READY   STATUS    RESTARTS   AGE
prometheus-grafana-7d6b4b5c8-4xkzp                    3/3     Running   0          2m
prometheus-kube-state-metrics-7d4f96c-wqmnr            1/1     Running   0          2m
prometheus-prometheus-node-exporter-abcde              1/1     Running   0          2m
prometheus-prometheus-operator-5f9c6b-abc12            1/1     Running   0          2m
prometheus-prometheus-kube-prometheus-prometheus-0     2/2     Running   0          90s
prometheus-alertmanager-0                              2/2     Running   0          90s

Accessing Grafana

Port-forward the Grafana service to your local machine:

kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80

Open http://localhost:3000 in your browser.

  • Username: admin
  • Password: securepassword123 (what you set above)

Pre-built Kubernetes Dashboards

Grafana has a library of pre-built dashboards. The most useful for Kubernetes:

Dashboard IDNameWhat it shows
315Kubernetes cluster monitoringCPU/memory per node and pod
3662Prometheus statsPrometheus itself
6417Kubernetes clusterHigh-level cluster view
13332Kubernetes Resource OverviewAll namespace resources

To import a dashboard:

  1. Click the “+” menu in Grafana → Import
  2. Enter the dashboard ID (e.g., 315)
  3. Click Load → Select your Prometheus datasource → Import

You immediately get graphs for CPU usage, memory, pod counts, and node health.

Prometheus Metric Types

Understanding metric types helps you query and interpret them correctly.

Counter — only increases. Use for total requests, total errors.

http_requests_total

Gauge — can go up or down. Use for current memory, replica count.

container_memory_usage_bytes

Histogram — measures distribution (e.g., request duration). Use for latency percentiles.

http_request_duration_seconds_bucket

Summary — like histogram but calculated on the client. Less common.

PromQL Basics

Prometheus Query Language (PromQL) is how you query metrics. Grafana uses PromQL to build graphs.

# Current CPU usage per pod (as a rate over 5 minutes)
rate(container_cpu_usage_seconds_total{namespace="default"}[5m])

# Total memory used per pod
container_memory_usage_bytes{namespace="default"}

# HTTP request rate per second
rate(http_requests_total[5m])

# 95th percentile request duration
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# Sum memory by namespace
sum by (namespace) (container_memory_usage_bytes)

# Number of pod restarts in the last hour
increase(kube_pod_container_status_restarts_total[1h])

Key Kubernetes Metrics to Monitor

# Pods NOT in Running state
kube_pod_status_phase{phase!="Running"}

# CPU throttling (bad if high)
rate(container_cpu_throttled_seconds_total[5m])

# Pods in CrashLoopBackOff
kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"}

# Nodes not ready
kube_node_status_condition{condition="Ready",status="true"} == 0

# Persistent Volume almost full
(kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.85

Adding Application Metrics

Your app can expose custom metrics using a Prometheus client library.

Python example (with prometheus-client):

from prometheus_client import Counter, Histogram, start_http_server
import time

# Define metrics
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests',
                        ['method', 'endpoint', 'status'])
REQUEST_LATENCY = Histogram('http_request_duration_seconds', 'Request latency',
                            ['endpoint'])

# In your request handler:
start = time.time()
# ... handle request ...
REQUEST_COUNT.labels(method='GET', endpoint='/api', status='200').inc()
REQUEST_LATENCY.labels(endpoint='/api').observe(time.time() - start)

# Expose /metrics endpoint
start_http_server(8080)

Go example (with prometheus/client_golang):

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
)

var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests",
    },
    []string{"method", "status"},
)

func init() {
    prometheus.MustRegister(httpRequests)
}

// Expose /metrics
http.Handle("/metrics", promhttp.Handler())

Tell Prometheus to scrape your app by adding a ServiceMonitor:

# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp-monitor
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: myapp
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
  namespaceSelector:
    matchNames:
      - default
kubectl apply -f servicemonitor.yaml

Prometheus will now scrape your app’s /metrics endpoint every 30 seconds.

Setting Up Alerts

Alertmanager routes alerts to Slack, PagerDuty, email, or other destinations.

Define alert rules with PrometheusRule:

# alertrules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: myapp-alerts
  namespace: monitoring
spec:
  groups:
    - name: myapp
      rules:
        - alert: HighErrorRate
          expr: rate(http_requests_total{status="500"}[5m]) > 0.1
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "High error rate on {{ $labels.instance }}"
            description: "Error rate is {{ $value }} req/s"

        - alert: PodCrashLooping
          expr: increase(kube_pod_container_status_restarts_total[15m]) > 5
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Pod {{ $labels.pod }} is crash looping"
kubectl apply -f alertrules.yaml

Accessing Prometheus Directly

Port-forward to the Prometheus UI for debugging queries:

kubectl port-forward -n monitoring svc/prometheus-kube-prometheus-prometheus 9090:9090

Open http://localhost:9090. Use the expression browser to run PromQL queries directly.

Common Mistakes

No retention limit on Prometheus

The kube-prometheus-stack Helm chart defaults to 10 days retention. Vanilla Prometheus defaults to 15 days. On busy clusters, this can fill your disk quickly. Set an explicit retention limit:

helm upgrade prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.prometheusSpec.retention=7d \
  --set prometheus.prometheusSpec.retentionSize=10GB

High-cardinality labels

Avoid using user IDs, request IDs, or other unbounded values as Prometheus labels. Each unique label combination creates a new time series. 1 million unique user IDs = 1 million time series = Prometheus crashes.

# BAD — user_id has unbounded cardinality
REQUEST_COUNT = Counter('requests', 'Requests', ['user_id'])

# GOOD — status has bounded cardinality (200, 404, 500, ...)
REQUEST_COUNT = Counter('requests', 'Requests', ['status'])

No resource limits on Prometheus

Prometheus can be memory-hungry on large clusters. Always set resource requests and limits:

helm upgrade prometheus prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --set prometheus.prometheusSpec.resources.requests.memory=1Gi \
  --set prometheus.prometheusSpec.resources.limits.memory=4Gi

What’s Next?

Your cluster is observable. The next step is automating deployments — setting up a CI/CD pipeline with GitHub Actions.

Next: Kubernetes Tutorial #9: CI/CD with Kubernetes and GitHub Actions