Metrics 7 min read

Prometheus metrics, explained

The four metric types, the handful worth watching, and how to collect and chart them.

By JeremyFunk

01 · Definition

What a Prometheus metric is

A Prometheus metric is a named number that your application keeps up to date, such as how many requests it has served. Prometheus reads it every few seconds and stores each reading, so you can chart how the number changes over time.

Applications publish their metrics as plain text on an HTTP endpoint, usually /metrics. Every line is one number:

Every combination of label values is its own time series. A POST that returned 500 and a GET that returned 200 are counted separately, as two series of the same metric. That is what lets a query pick out only the errors, or only one route.

Two comment lines above the samples give each metric a description and a type:

# HELP http_requests_total Total HTTP requests handled.
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 10432
http_requests_total{method="POST",status="500"} 17

Prometheus pulls: the scraper asks and your app answers. Every answer becomes one more reading in storage, which is what queries, dashboards and alerts read.

02 · Types

The four Prometheus metric types

The type decides how a number behaves over time, and that decides which query makes sense for it.

Counter: a number that only goes up

Requests served, errors, bytes sent. The raw value is a running total since the process started, which is rarely what you want to see. Ask how fast it grows instead:

rate(http_requests_total[5m])       # requests per second
increase(http_requests_total[1h])   # requests in the last hour

When the process restarts, the counter drops to zero. rate() and increase() notice the drop and carry on as if it hadn't happened.

Gauge: a number that goes up and down

Memory in use, items in a queue, open connections. A gauge is a snapshot, so its raw value is the answer:

process_resident_memory_bytes                        # memory now
max_over_time(process_resident_memory_bytes[1h])     # peak in the last hour

Histogram: how values are spread

Use a histogram for durations and sizes, where the average hides the slow requests you care about. Instead of storing every request's duration, it counts how many requests finished within each limit. The buckets are cumulative: the 250 ms bucket also contains everything under 100 ms.

95% of 10,449 requests is 9,927. That count falls between the 100 ms and 250 ms buckets, so the p95 is somewhere in that range: histogram_quantile() estimates about 177 ms.
# p95 latency across every instance
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

Because bucket counts add up, you can combine histograms from every instance and get one percentile for the whole service. Pick bucket limits around your targets: if the goal is 300 ms, put a bucket at 300 ms.

Summary: percentiles computed in the app

A summary works out percentiles such as p50 and p99 inside your application and publishes them ready-made:

rpc_duration_seconds{quantile="0.99"} 0.231

That is convenient for one process and a dead end for a service. Percentiles from five instances cannot be averaged into the service's p99, and you can't ask for a p95 later if the app only computed p50 and p99. Reach for a histogram unless you have a reason not to.

TypeUse it forQuery it with
Counter Requests served, errors, bytes sent rate(), increase()
Gauge Memory in use, queue depth, temperature Read it directly, max_over_time()
Histogram Request duration, response size histogram_quantile()
Summary Latency of one process Read the quantile label

03 · What to watch

Prometheus metrics worth watching

An exporter can publish hundreds of metrics. A useful first dashboard needs seven. The first three come from your own service: a request counter and a duration histogram. The rest come from node_exporter on each host, plus up, which Prometheus records for every target it scrapes.

Traffic Counter
sum by (route) (rate(http_requests_total[5m]))
Errors Counter
sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m]))
Latency Histogram
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
CPU Counter
1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))
Memory Gauge
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
Disk Gauge
node_filesystem_avail_bytes / node_filesystem_size_bytes
Scrape health Gauge
up == 0

Alert on up == 0 first

When a target stops answering, its other metrics don't turn red. They stop arriving, and a dashboard shows a gap that is easy to miss. up is 0 for every failed scrape, so it is the one alert that catches an exporter or a whole host going away.

04 · Collect

Collect and chart Prometheus metrics

1. Expose /metrics from your code

Every major language has a Prometheus client library. Each example creates the counter and histogram from this guide and serves them on port 8000:

Installpip install prometheus-client

from prometheus_client import Counter, Histogram, start_http_server

REQUESTS = Counter("http_requests", "HTTP requests handled.", ["method", "status"])
LATENCY = Histogram("http_request_duration_seconds", "Request latency.")

start_http_server(8000)  # serves /metrics on port 8000

# Call this from your request handler
def record(method, status, handle):
    with LATENCY.time():
        handle()
    REQUESTS.labels(method, str(status)).inc()

Installnpm install prom-client

import { createServer } from "node:http";
import { Counter, Histogram, register } from "prom-client";

const requests = new Counter({ name: "http_requests_total", help: "HTTP requests handled.", labelNames: ["method", "status"] });
const latency = new Histogram({ name: "http_request_duration_seconds", help: "Request latency." });

createServer(async (req, res) => {
  if (req.url === "/metrics") {
    res.setHeader("Content-Type", register.contentType);
    res.end(await register.metrics());
    return;
  }
  const stopTimer = latency.startTimer();
  res.end("ok");
  stopTimer();
  requests.inc({ method: req.method, status: String(res.statusCode) });
}).listen(8000);

Installgo get github.com/prometheus/client_golang

package main

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	requests = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "http_requests_total", Help: "HTTP requests handled.",
	}, []string{"method", "status"})
	latency = promauto.NewHistogram(prometheus.HistogramOpts{
		Name: "http_request_duration_seconds", Help: "Request latency.",
	})
)

func handle(w http.ResponseWriter, r *http.Request) {
	timer := prometheus.NewTimer(latency)
	defer timer.ObserveDuration()
	w.Write([]byte("ok"))
	requests.WithLabelValues(r.Method, "200").Inc()
}

func main() {
	http.Handle("/metrics", promhttp.Handler())
	http.HandleFunc("/", handle)
	http.ListenAndServe(":8000", nil)
}

Installcargo add prometheus

use prometheus::{register_histogram, register_int_counter_vec, Encoder, TextEncoder};
use std::io::{Read, Write};
use std::net::TcpListener;

fn main() {
    let requests = register_int_counter_vec!(
        "http_requests_total", "HTTP requests handled.", &["method", "status"]
    ).unwrap();
    let latency = register_histogram!(
        "http_request_duration_seconds", "Request latency."
    ).unwrap();

    // In a real service, do this in your request handler.
    let timer = latency.start_timer();
    requests.with_label_values(&["GET", "200"]).inc();
    timer.observe_duration();

    // Serve /metrics. With axum or actix, return this body from a route instead.
    for stream in TcpListener::bind("0.0.0.0:8000").unwrap().incoming() {
        let mut stream = stream.unwrap();
        stream.read(&mut [0; 1024]).unwrap();
        let mut body = Vec::new();
        TextEncoder::new().encode(&prometheus::gather(), &mut body).unwrap();
        write!(stream, "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).unwrap();
        stream.write_all(&body).unwrap();
    }
}

Installio.prometheus:prometheus-metrics-core + prometheus-metrics-exporter-httpserver

import io.prometheus.metrics.core.datapoints.Timer;
import io.prometheus.metrics.core.metrics.Counter;
import io.prometheus.metrics.core.metrics.Histogram;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;

public class App {
    static final Counter requests = Counter.builder()
            .name("http_requests_total").help("HTTP requests handled.")
            .labelNames("method", "status").register();
    static final Histogram latency = Histogram.builder()
            .name("http_request_duration_seconds").help("Request latency.")
            .classicOnly().register();

    public static void main(String[] args) throws Exception {
        HTTPServer.builder().port(8000).buildAndStart(); // serves /metrics

        // In a real service, do this in your request handler.
        try (Timer timer = latency.startTimer()) {
            requests.labelValues("GET", "200").inc();
        }
        Thread.currentThread().join();
    }
}

Installdotnet add package prometheus-net

using Prometheus;

var requests = Metrics.CreateCounter("http_requests_total", "HTTP requests handled.", "method", "status");
var latency = Metrics.CreateHistogram("http_request_duration_seconds", "Request latency.");

using var server = new MetricServer(port: 8000); // serves /metrics
server.Start();

// In a real service, do this in your request handler.
using (latency.NewTimer())
{
    requests.WithLabels("GET", "200").Inc();
}

await Task.Delay(Timeout.Infinite);

Open http://localhost:8000/metrics and you'll see the counter, and the histogram as _bucket, _sum and _count series. The Python client adds _total to the counter name itself. For software you don't own, such as databases, queues or the host itself, install an exporter that serves the same format.

2. Run Prometheus and Grafana

Your app now has a /metrics page, but nothing is reading it yet. That is the job of the Prometheus server, a separate program you run: it reads a config file listing the endpoints to scrape, requests each one every 15 seconds, and stores the results. Grafana is another program that draws dashboards from what Prometheus stored.

The quickest way to run both is Docker Compose. Put these three files in one folder:

compose.yaml

services:
  prometheus:
    image: prom/prometheus
    ports: ["9090:9090"]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    extra_hosts: ["host.docker.internal:host-gateway"]
  grafana:
    image: grafana/grafana
    ports: ["3000:3000"]
    volumes:
      - ./grafana-datasource.yml:/etc/grafana/provisioning/datasources/prometheus.yml

prometheus.yml

scrape_configs:
  - job_name: api
    scrape_interval: 15s
    static_configs:
      - targets: ["host.docker.internal:8000"]

grafana-datasource.yml

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true

Run docker compose up with your app running on port 8000. host.docker.internal is how the containers reach your machine.

  1. 01

    Check the scrape in Prometheus

    Open http://localhost:9090, type up and run it. A 1 means Prometheus reached your app. Any query from the list above works here too.

  2. 02

    Chart it in Grafana

    Open http://localhost:3000 and sign in with admin / admin. The Prometheus data source is already set up by grafana-datasource.yml. Go to Explore, enter a query, and add it to a dashboard.

Use $__rate_interval in Grafana

rate() needs at least two samples inside its window. Zoom a Grafana panel in far enough and a fixed window like [1m] can hold fewer than that, so the line goes blank. Write rate(http_requests_total[$__rate_interval]) instead: Grafana then sets the window to at least four scrape intervals, whatever the zoom.

3. Or scrape straight into Maple

Maple can do the scraping, so there's no Prometheus server to run. It converts each sample to an OpenTelemetry metric, next to your traces and logs.

  1. 01

    Add a target

    Open Integrations → Prometheus, click Add target and enter the endpoint's URL, a scrape interval (5 to 300 seconds) and, if needed, a bearer token or basic auth. The service name is what the metrics appear under.

    The Add scrape target dialog filled in: name Node exporter, service name node-exporter, URL https://node.demo.prometheus.io/metrics, scrape interval 15 seconds, authentication None.
  2. 02

    Check that it's up

    Click Test for an immediate scrape. It reports success, or the exact HTTP, TLS or timeout error. Once the scheduled scrapes start, the target shows Up, with each run's duration and sample count in its check history.

    The Node exporter target marked Up: a 15 second interval, a 529 ms scrape with 1.4K samples, and a check history of successful scrapes every 15 seconds.
  3. 03

    Chart, then alert

    Find the metric in the Metrics explorer. Counters default to rate. Filter and group by any label, then use Add to dashboard or Create alert.

    The metrics explorer charting rate(node_cpu_seconds_total) grouped by attr.mode with the filter attr.mode != idle, showing user, system and softirq CPU time over the last 15 minutes.

    The same CPU query as in the list above, in Maple: rate of node_cpu_seconds_total, grouped by mode, idle filtered out.

Prometheus typeStored in Maple as
CounterCumulative monotonic sum
GaugeGauge (untyped metrics too)
HistogramHistogram with the original bucket limits
Summary_sum and _count as sums, the quantiles as a gauge

Maple only scrapes public endpoints. For one inside a private network, run an OpenTelemetry Collector there with a prometheus receiver and send the results to Maple over OTLP.

Set it up in Maple

Scrape your first endpoint

Every field, the scrape history and troubleshooting are in the docs.

05 · Mistakes

Common Prometheus metric mistakes

01

Charting a raw counter

A counter only tells you something through its rate. Plotted raw it is a line that climbs, then drops to zero whenever the process restarts.

02

Summing before rate

Write sum(rate(x[5m])), not rate(sum(x)[5m]). rate() detects restarts per series, and summing first hides them.

03

A label for every user

Every distinct label value is a new time series. User IDs, emails or full URLs in labels can grow one metric into millions of series.

04

Averaging summary quantiles

A p99 from five processes cannot be averaged into a service p99. Use a histogram and compute the percentile across all of them.

05

Units missing from names

Base units go in the name: _seconds, _bytes, and _total for counters. http_request_duration_seconds needs no comment.

06

A rate window that is too short

rate() needs at least two samples in its window. With a 15 s scrape interval, [1m] is the shortest safe window.

Sources and further reading

FAQ

よくある質問

What are the 4 types of metrics in Prometheus?
Counter, gauge, histogram and summary. A counter only goes up, such as requests served. A gauge goes up and down, such as memory in use. A histogram counts values into buckets so you can compute percentiles when you query. A summary computes percentiles inside the application and exposes them ready-made.
What is the difference between a counter and a gauge?
A counter only increases and resets to zero when the process restarts, so you query its rate of change with rate() or increase(). A gauge is a current value that can rise or fall, so you read it directly. If a value can go down for any reason other than a restart, it is a gauge.
Should I use a histogram or a summary?
A histogram, in almost every case. Histogram buckets from many instances add up, so you can compute a percentile for the whole service and pick the percentile when you query. Summary percentiles are computed per process and cannot be combined across instances.
Which Prometheus metrics should I monitor?
For a service: request rate, error ratio and latency percentiles, from a request counter and a duration histogram. For a host: CPU, memory and disk from node_exporter. Add an alert on up == 0 so you know when a target stops being scraped.
How do I see Prometheus metrics in Grafana?
Add Prometheus as a data source in Grafana with the address of your Prometheus server, for example http://prometheus:9090. Then create a dashboard panel, choose that data source and enter a PromQL query such as sum(rate(http_requests_total[5m])).

今日、最初のトレースを。

SDKを追加し、OTLPエクスポーターをMapleに向ければトレースが届きます。

maple.dev:OpenTelemetryの上のオブザーバビリティ