Metrics & the Prometheus Data Model

Observability

Chapter 2 · Metrics & the Prometheus Data Model

obs1-1 named metrics as the pillar that tells you something is wrong, cheaply, in aggregate. This chapter goes underneath that claim: exactly how Prometheus — the tool this course builds around for the metrics pillar — actually represents a metric. Everything in Ch.3 (scraping) and Ch.4 (PromQL) builds directly on the data model established here.

A Metric Is a Time Series

A Prometheus metric isn't one number — it's a named stream of (timestamp, value) pairs collected over time, identified by a metric name plus a set of labels, key-value pairs attached to that specific series.

http_requests_total{method="GET", status="200", handler="/checkout"} 1027

http_requests_total is the metric name; the three labels together identify exactly which time series this particular value belongs to. This isn't one running total for the whole application — it's one time series among potentially many under the same metric name.

Labels — The Dimension That Makes Aggregation Possible

Every unique combination of label values creates a genuinely separate time series:

http_requests_total{method="GET", status="200"} 8934 http_requests_total{method="POST", status="500"} 12 http_requests_total{method="GET", status="404"} 201

Three distinct time series, one metric name. This is exactly what makes Chapter 4's PromQL genuinely useful — labels are the dimension you filter and aggregate along, letting a single instrumented metric answer "what's my GET rate," "what's my 500 rate," and "what's my total request rate across every method and status combined" from the same underlying data.

The Four Metric Types

Prometheus defines exactly four kinds of metric, each with a different shape of value and a different intended use:

# HELP http_requests_total Total HTTP requests served # TYPE http_requests_total counter http_requests_total{method="GET"} 8934 # HELP memory_usage_bytes Current process memory usage # TYPE memory_usage_bytes gauge memory_usage_bytes 52428800 # HELP http_request_duration_seconds Request duration # TYPE http_request_duration_seconds histogram http_request_duration_seconds_bucket{le="0.1"} 8010 http_request_duration_seconds_bucket{le="0.5"} 8900 http_request_duration_seconds_bucket{le="1.0"} 8930 http_request_duration_seconds_bucket{le="+Inf"} 8934 http_request_duration_seconds_sum 452.3 http_request_duration_seconds_count 8934
  • Counter — only ever increases (or resets to zero on restart). Right for "total requests served," "total errors," anything that's a running cumulative count. A raw counter value is rarely useful by itself; Chapter 4's rate() turns it into a meaningful per-second figure.
  • Gauge — a value that can go up or down freely. Right for "current memory usage," "active connections," "queue depth right now."
  • Histogram — sorts observations (like request durations) into configurable buckets, alongside a running sum and count. Quantiles are computed later, at query time, from the bucket counts.
  • Summary — similar intent to a histogram, but quantiles are calculated client-side, inside the instrumented application itself, before the metric is ever exposed.

Histogram vs. Summary — A Real Operational Tradeoff

These two look similar but behave very differently once there's more than one instance of a service running. A histogram's raw bucket counts from many different pods can simply be summed together — Chapter 4's histogram_quantile() then computes one fleet-wide p99 from the combined buckets. A summary's quantile, by contrast, is already computed inside one specific instance before it's ever exposed — there is no meaningful way to average two different instances' own p99 values together and get a real fleet-wide p99. For anything that might ever need aggregating across replicas — which, in practice, is nearly everything in a real production system — histograms are the safer default.

TypeBehaviorAggregatable across instances?
CounterOnly increases, resets on restartYes — sum, or rate() over time
GaugeFreely goes up or downYes — sum, avg, min, max
HistogramBucketed observations + sum + countYes — buckets sum cleanly across instances
SummaryClient-side quantiles + sum + countNo — per-instance quantiles can't be meaningfully combined
A real naming convention worth following
Prometheus doesn't enforce metric names, but the community convention is strong and worth adopting: a unit suffix (_seconds, _bytes) and a _total suffix specifically for counters. http_request_duration_seconds and http_requests_total both follow this pattern — it makes a metric's own type and unit legible from its name alone, without needing to check the # TYPE line.
Label cardinality — a real, common production incident
Never put an unbounded value — a raw user ID, a full URL with query parameters, a timestamp — into a label. Every distinct label-value combination creates a genuinely separate time series; a label that can take millions of distinct values multiplies Prometheus's own storage and memory usage by that same factor, and has caused real outages in real production Prometheus deployments. Labels should have a small, bounded set of possible values — method, status, handler — not anything that grows without limit.

Hands-On Exercises

Exercise 1

Write the raw exposition-format lines for a counter metric tracking total failed login attempts, with labels for reason ("bad_password" or "account_locked"), and explain why a counter — not a gauge — is the right choice here.

📄 View solution
Exercise 2

A service exposes a gauge called active_websocket_connections. Explain why a gauge is the correct type here rather than a counter, referencing what would go wrong if it were implemented as a counter instead.

📄 View solution
Exercise 3

A colleague proposes adding a raw user_id label to a request-duration histogram, planning to aggregate p99 latency across all instances afterward. Explain the two separate problems with this plan — one about cardinality, one about histogram vs. summary aggregation.

📄 View solution

Chapter 2 Quick Reference

  • A metric is a named time series, identified by its name plus a set of label key-value pairs
  • Every unique label combination is a genuinely separate time series under the same metric name
  • Counter — only increases; Gauge — goes up or down freely
  • Histogram — bucketed observations, quantiles computed at query time, aggregatable across instances
  • Summary — client-side quantiles, NOT meaningfully aggregatable across instances
  • Naming convention: unit suffix (_seconds, _bytes), _total suffix for counters
  • Never label with unbounded values — a real, common cause of production Prometheus incidents (cardinality explosion)