Query gateway metrics
Read current totals for a Gateway, chart requests over time broken down by route, and save your own metric recipe.
This guide reads traffic metrics for a Gateway three ways: a snapshot of current totals, a time series broken down by route, and a metric recipe you define yourself.
Prerequisites
- An Apoxy project with a Gateway serving traffic.
- An API key from the Apoxy console (Account Settings > API Keys).
- Your project ID, shown in the Apoxy console.
Set both in your shell so the examples below stay short:
export APOXY_API_KEY="<api-key>"
export APOXY_API="https://<project-id>.api.apoxy.dev"See what you can query
The catalog lists every metric recipe available in your project:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/metrics.apoxy.dev/v1alpha1/metrics"You should see the built-in recipes, including http.requests, http.latency, http.bytes, and http.errors. Each entry carries its measures under status.measures and the dimensions it can be grouped by under status.keys.
Read a Gateway snapshot
A snapshot returns every applicable recipe for one object over one window in a single call. Ask for the last hour of a Gateway named prod, with a per-route breakdown:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/gateway.apoxy.dev/v1/gateways/prod/metrics?window=1h&include=routes&top=10"You should see totals for the Gateway, then a listeners array, each listener carrying its own totals and the top routes under it:
{
"kind": "GatewayMetrics",
"metadata": { "name": "prod" },
"window": "1h0m0s",
"since": "2026-08-20T20:00:00Z",
"until": "2026-08-20T21:00:00Z",
"dataUpTo": "2026-08-20T21:00:00Z",
"metrics": {
"http.requests": { "total": 76821, "status_2xx": 74604, "status_4xx": 2061, "status_5xx": 156 },
"http.latency": { "p50": 38, "p95": 190, "p99": 412 }
},
"units": { "p50": "ms", "p95": "ms", "p99": "ms" },
"listeners": [
{
"name": "https",
"metrics": { "http.requests": { "total": 76705, "status_5xx": 156 } },
"truncated": true,
"totalCount": 37,
"routes": [
{ "kind": "HTTPRoute", "name": "api", "rule": 0,
"metrics": { "http.requests": { "total": 62958, "status_5xx": 149 } } }
]
}
]
}truncated: true with totalCount: 37 means the listener has 37 routes with traffic and you asked for the top 10. Raise top up to 50, or use the series query below for complete coverage.
Omit include=routes and you get the Gateway and listener totals only, which is the cheaper read for a status tile.
Chart requests over time
The series endpoint returns one line per group. Ask for six hours of requests on the same Gateway, in five-minute buckets, split by route and ranked by server errors:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/metrics.apoxy.dev/v1alpha1/metrics/http.requests/series?scopeKind=Gateway&scopeName=prod&groupBy=route_name&since=-6h&step=5m&top=10&orderBy=status_5xx"You should see a series array, one entry per route, each with a points array:
{
"kind": "MetricSeriesSet",
"metric": "http.requests",
"scopeKind": "Gateway",
"scopeName": "prod",
"since": "2026-08-20T15:00:00Z",
"until": "2026-08-20T21:00:00Z",
"step": "5m0s",
"dataUpTo": "2026-08-20T21:00:00Z",
"truncated": false,
"totalCount": 7,
"series": [
{
"labels": { "route_name": "api" },
"points": [
{ "timestamp": "2026-08-20T15:00:00Z", "values": { "total": 5210, "status_4xx": 140, "status_5xx": 12 } },
{ "timestamp": "2026-08-20T15:05:00Z", "values": { "total": 5188, "status_4xx": 131, "status_5xx": 9 } }
]
}
]
}Buckets with no traffic are omitted rather than returned as zero. Anything after dataUpTo is still filling, so plot up to that timestamp to avoid a trailing dip that corrects itself a minute later.
Swap groupBy=route_name for status_class, method, listener, or backend_name to slice the same recipe a different way. Swap http.requests for http.latency to chart p50, p95, and p99 instead.
Get one row per Gateway
Set step equal to the window and you get a single bucket, which turns the series endpoint into a fleet listing. Group by gateway at project scope for one row per Gateway:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/metrics.apoxy.dev/v1alpha1/metrics/http.requests/series?scopeKind=Project&groupBy=gateway&since=-1h&step=1h"Each series comes back with exactly one point. This is the read behind a list page, and it replaces polling every object's snapshot separately.
Save your own metric
The built-in recipes cover request counts, errors, latency, and bytes. For anything else, save a recipe and query it through the same endpoint. This one counts server errors on a single path prefix:
apiVersion: metrics.apoxy.dev/v1alpha1
kind: Metric
metadata:
name: v2.errors
spec:
source: otel_logs
type: counter
description: Server errors on the /v2 path prefix.
prql: |
filter (url.path | text.starts_with "/v2/")
filter http.response.status_code >= 500
aggregate { n = count this }Apply it:
apoxy apply -f metric.yamlThe fragment is aggregate-only: filter and derive steps followed by one aggregate. Leave out from, group, and time_bucket entirely. The source comes from spec.source, the time bucket from the step parameter, and the grouping from groupBy, so the recipe stays reusable at any resolution.
The server compiles the fragment as it stores it. A fragment that does not compile is rejected on the spot with the compiler's message, so you find out at apply time rather than at read time.
Query it exactly like a built-in:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/metrics.apoxy.dev/v1alpha1/metrics/v2.errors/series?scopeKind=Gateway&scopeName=prod&since=-1h&step=1m&groupBy=envoy.response_flags"The prefixes http., log., upstream., and tls. belong to the built-in recipes, and the name series is taken by the subresource. Writing to any of them returns 403. Pick a prefix of your own, as v2.errors does above.
Verify
Confirm the recipe was stored and compiled:
curl -s -H "X-Apoxy-API-Key: $APOXY_API_KEY" \
"$APOXY_API/apis/metrics.apoxy.dev/v1alpha1/metrics/v2.errors"You should see a Compiled condition with status True, along with a status.measures entry for n and the list of keys you can group it by.
Common errors
400with a list of valid keys - thegroupByfield is not one of the recipe'sstatus.keys. Read the recipe to see what it supports.400naming buckets, series, and measures - the request asks for more than 20000 points. Widenstep, lowertop, or restrict recipes withmetric=.403on apply - the recipe name uses a reserved prefix.404- the Gateway, route, or other owner object in the path does not exist.429- too many concurrent queries. Retry after the interval in theRetry-Afterheader.
Where to next
- Full parameter, response, and limit reference in the Metrics API.
- Free-form questions over raw log records with PRQL and the MCP server.