Prometheus Metrics skill for Codex
Metrics with units, bounded labels and honest PromQL. Codex designs metrics from the questions they must answer, picks the right type, names them with base units and bounded labels, and places histogram buckets around the SLO threshold. Its PromQL aggregates in the right order (rate before sum, buckets by le before histogram_quantile), and recording rules are linted and unit tested with promtool.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- prometheus-metrics Installs to ~/.agents/skills/prometheus-metrics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install prometheus-metrics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project prometheus-metrics Installs to .agents/skills/prometheus-metrics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/prometheus-metrics.zipunzip -o prometheus-metrics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) prometheus-metrics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $prometheus-metrics, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs Prometheus metrics and PromQL that answer real questions - choosing counter, gauge or histogram, naming with base units, bounded labels, bucket layout, client library instrumentation, recording rules and correct rate, increase and histogram_quantile queries checked with promtool. Use when adding metrics to a service, reviewing a /metrics endpoint, writing or debugging PromQL, building recording rules, or when a query returns odd numbers after restarts or aggregation.
Use it when
- A service needs new Prometheus metrics or a review of its /metrics output
- A PromQL query returns numbers that do not match reality
- Dashboards or alerts re-evaluate the same expensive query
- You need to choose histogram buckets for a latency target
Use something else when
- Series counts or metric bills are exploding: use metrics-cardinality
- You are writing alert rules and routing: use alerting-rules
- You need SLO burn-rate rules: use slo-error-budgets
How to check the result
promtool check metrics and check rules pass, promtool test rules covers each recording rule, and live query magnitudes match an independent source such as load balancer counts.
The mistake it prevents
Running histogram_quantile on unaggregated buckets or averaging per-pod percentiles, which produces a number with no meaning.
Done well means: each metric answers a question someone will ask during an incident, names and units are unambiguous, labels are bounded, and every query aggregates in the right order so the number on the graph is mathematically correct.
Inspect first
- Read
AGENTS.md; find the client library (prom-client,prometheus_client,client_golang, Micrometer) and the existing registry and/metricshandler. - Fetch current exposition locally:
curl -s localhost:8080/metrics | head -100. - Check scrape config or ServiceMonitor for
jobandinstancelabels; do not duplicate them as metric labels. - If Prometheus is reachable, check
promtool --versionand the server version; some features (native histograms, UTF-8 names) depend on it.
Pick the type
| Question | Type | Example |
|---|---|---|
| How many times did X happen? | Counter | http_requests_total |
| What is the value right now? | Gauge | queue_depth, inflight_requests |
| How long / how big, with percentiles across instances? | Histogram | http_request_duration_seconds |
| Percentiles for a single instance only | Summary (rarely) | avoid; cannot be aggregated |
| Static build metadata | Gauge set to 1 with labels | build_info{version="1.4.2",commit="ab12"} 1 |
Never use a gauge for something that only goes up; rate() on it is wrong. Never compute
averages in the app; expose _sum and _count (a histogram gives both).
Naming
<namespace>_<what>_<unit>[_total]:checkout_payment_attempts_total,db_query_duration_seconds,cache_size_bytes.- Base units only: seconds, bytes, ratios 0-1. Never milliseconds, never percent.
- Counters end in
_total. Do not put the type or label names in the metric name. - One metric with a label beats many metrics:
http_requests_total{code="500"}, nothttp_500_total.
Labels
- Every label value set is a new time series. Allowed: route template, method, status
class or code, outcome (
success|failure), small enums. Forbidden: user ID, email, raw path, query string, trace ID, error message, timestamp. - Use the route template (
/orders/:id), never the raw URL. - Initialise known label combinations to zero at startup so
rate()and alerts see a series before the first event. - For cardinality budgets and cleanup, see
$metrics-cardinality.
Instrumentation examples
Python (prometheus_client):
from prometheus_client import Counter, Histogram
REQUESTS = Counter("http_requests_total", "HTTP requests", ["method", "route", "code"])
LATENCY = Histogram(
"http_request_duration_seconds", "Request latency", ["method", "route"],
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10),
)
def handle(req):
with LATENCY.labels(req.method, req.route_template).time():
resp = app(req)
REQUESTS.labels(req.method, req.route_template, str(resp.status)).inc()
return resp
Go (client_golang):
var latency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Request latency.",
Buckets: prometheus.ExponentialBucketsRange(0.005, 10, 12),
}, []string{"method", "route"})
Node (prom-client): new client.Histogram({ name, help, labelNames, buckets });
call client.collectDefaultMetrics() once.
Bucket rule: put boundaries around your SLO threshold (if the target is 300 ms, have
buckets at 0.25 and 0.5 at minimum, ideally 0.3 exactly). histogram_quantile can only
interpolate between bucket edges. If the Prometheus server and client support native
histograms, they remove most bucket tuning; confirm both sides before switching.
PromQL that is correct
# request rate per route
sum by (route) (rate(http_requests_total[5m]))
# error ratio: aggregate numerator and denominator separately, then divide
sum(rate(http_requests_total{code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
# p99 latency per route: sum buckets by le BEFORE histogram_quantile
histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
# average latency
sum(rate(http_request_duration_seconds_sum[5m])) / sum(rate(http_request_duration_seconds_count[5m]))
# fraction of requests faster than 300 ms (needs a 0.3 bucket)
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))
# events in the last day
sum(increase(payment_attempts_total[1d]))
# targets down
up == 0
Rules:
rate()thensum(), neversum()thenrate(): summing raw counters hides resets.- The range must cover at least four scrape intervals; in Grafana use
$__rate_interval. - Averaging percentiles across instances is meaningless; aggregate buckets instead.
increase()extrapolates, so it returns non-integers for integer counters; that is expected.irate()is for spiky, zoomed-in graphs, not for alerts or recording rules.- Matching two vectors with different labels needs
on()/ignoring()and, for many-to-one,group_left. Prefer aggregating both sides to the same labels.
Recording rules
Precompute anything a dashboard or alert evaluates repeatedly. Name with
level:metric:operations:
groups:
- name: http-recording
interval: 1m
rules:
- record: route:http_requests:rate5m
expr: sum by (route) (rate(http_requests_total[5m]))
- record: route:http_request_duration_seconds:p99_5m
expr: histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
Verification
curl -s localhost:8080/metrics | promtool check metrics # naming and help lint
promtool check rules rules/*.yml
promtool test rules tests/*.yml # unit tests with input_series
Query the live server and sanity-check magnitudes: the request rate should match load balancer or access-log counts within a few percent; error ratio should be between 0 and
- If a graph drops to zero on deploy and spikes after, look for a counter reset combined with the wrong aggregation order.
Output
When adding metrics, deliver a table of metric name, type, labels with allowed values, unit, and the question it answers, plus the queries or recording rules that use them.
Gotchas
- Creating a metric inside a request handler re-registers it and throws (or leaks). Define metrics at module scope.
- Multi-process Python servers (gunicorn) need
prometheus_clientmultiprocess mode withPROMETHEUS_MULTIPROC_DIR; otherwise each scrape hits one worker. - Label values with unescaped quotes or newlines break the exposition format.
- Absent series are not zero:
rate(x[5m])with no samples returns nothing, so alerts silently never fire. Useabsent()or initialise series. - Exposing a summary’s
quantilelabel and then averaging it across pods is a common, wrong dashboard.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Prometheus Metrics"
short_description: "Metrics with units, bounded labels and honest PromQL"
default_prompt: "$prometheus-metrics Add Prometheus request rate, error and latency metrics to this FastAPI service" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.