Monitor APISIX Metrics with Prometheus
Scrape a route-scoped Apache APISIX metric with a pinned local Prometheus server, assert the exact series, and clean up.
Applies to:Apache APISIX 3.18.0
Deployments:DockerRoles:Platform engineerTime:20 minutesImpact:Changes live traffic
Goal
Enable prometheus on one isolated Route, scrape the APISIX metrics export server with a pinned local Prometheus container, and assert both target health and the Route's HTTP status counter. Remove every guide-owned resource afterward.
Metrics labels create time series. Keep Route scope for this example and evaluate label cardinality, retention, authentication, network policy, storage, and alert ownership before production rollout.
Before you begin
- Complete Get APISIX with the Docker quickstart.
- Install Docker, cURL, and jq.
- Use a shell that supports Bash or Zsh heredocs.
- Confirm that the quickstart metrics export server is reachable as
apisix-quickstart:9091fromapisix-quickstart-net. If a custom deployment binds it only to loopback, configure a protected scrape path using the Prometheus static configuration (opens in Plugin Hub docs) before continuing.
Set the endpoints and protect all fixed resources and files used by this guide:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
export PROMETHEUS_URL="http://127.0.0.1:9092"
export GUIDE_DIR="$(pwd)"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/prometheus-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route prometheus-guide-route already exists'; exit 1; }
for container in prometheus-guide-server prometheus-guide-upstream; do
test -z "$(docker ps -aq --filter "name=^/${container}$")" || { echo "Stop: container ${container} already exists"; exit 1; }
done
for file in prometheus-guide.yml prometheus-guide-nginx.conf; do
test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }
doneVerify network-local access to the APISIX metrics endpoint without publishing another listener:
docker run --rm --network apisix-quickstart-net curlimages/curl:8.16.0 \
--fail --silent --show-error --connect-timeout 3 --max-time 10 \
"http://apisix-quickstart:9091/apisix/prometheus/metrics" | \
grep -q '^apisix_etcd_reachable 'Stop here if this assertion fails. Do not expose an unauthenticated metrics endpoint broadly just to make the tutorial continue.
Steps
Step 1: Start a controlled upstream
Create a deterministic NGINX response and start it without publishing an upstream port:
cat > prometheus-guide-nginx.conf <<'NGINX'
events {}
http {
access_log off;
server {
listen 80;
location = /prometheus-guide { return 200 '{"source":"prometheus-guide-upstream"}'; }
}
}
NGINX
docker run -d --name prometheus-guide-upstream \
--network apisix-quickstart-net \
-v "${GUIDE_DIR}/prometheus-guide-nginx.conf:/etc/nginx/nginx.conf:ro" \
nginx:1.29.1-alpine
for attempt in {1..15}; do
docker exec apisix-quickstart bash -c '</dev/tcp/prometheus-guide-upstream/80' >/dev/null 2>&1 && break
sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/prometheus-guide-upstream/80'What changed: APISIX has one controlled local endpoint for generating metrics.
Step 2: Create one monitored Route
Enable prometheus only on the authenticated fixed-ID Route. prefer_name: false makes the route label use the stable Route ID:
jq -n '{
name:"prometheus-guide-route",
uri:"/prometheus-guide",
methods:["GET"],
plugins:{prometheus:{prefer_name:false}},
upstream:{type:"roundrobin",nodes:{"prometheus-guide-upstream:80":1}}
}' | \
curl -sS --connect-timeout 3 --max-time 15 \
"${APISIX_ADMIN_URL}/apisix/admin/routes/prometheus-guide-route" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H 'Content-Type: application/json' \
--data-binary @- | \
jq -e '.value.id == "prometheus-guide-route" and .value.plugins.prometheus.prefer_name == false'What changed: only the guide Route contributes route-scoped HTTP metrics.
Step 3: Start a pinned Prometheus server
Create a one-target configuration with a five-second scrape interval:
cat > prometheus-guide.yml <<'YAML'
global:
scrape_interval: 5s
evaluation_interval: 5s
scrape_configs:
- job_name: apisix
metrics_path: /apisix/prometheus/metrics
static_configs:
- targets:
- apisix-quickstart:9091
YAML
docker run -d --name prometheus-guide-server \
--network apisix-quickstart-net \
-p 127.0.0.1:9092:9090 \
-v "${GUIDE_DIR}/prometheus-guide.yml:/etc/prometheus/prometheus.yml:ro" \
prom/prometheus:v3.7.3 \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus \
--storage.tsdb.retention.time=1h
for attempt in {1..30}; do
curl --fail --silent --show-error --connect-timeout 2 --max-time 5 \
"${PROMETHEUS_URL}/-/ready" >/dev/null 2>&1 && break
sleep 1
done
curl --fail --silent --show-error --connect-timeout 3 --max-time 10 \
"${PROMETHEUS_URL}/-/ready" | grep -q 'Prometheus Server is Ready'The Prometheus UI is available only on host loopback. No persistent volume is used, and the one-hour retention limit bounds this disposable example.
What changed: a pinned local Prometheus server scrapes only the APISIX quickstart endpoint.
Validate
Poll the Prometheus API until the APISIX target is up, send one controlled request, then require a non-zero 200 counter for the exact Route ID:
for attempt in {1..12}; do
curl --fail --silent --show-error --get "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=up{job="apisix"}' | \
jq -e '.status == "success" and any(.data.result[]?; .value[1] == "1")' && break
sleep 5
done
curl --fail --silent --show-error --get "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=up{job="apisix"}' | \
jq -e '.status == "success" and any(.data.result[]?; .value[1] == "1")'
curl --fail-with-body -sS --connect-timeout 3 --max-time 10 \
"${GATEWAY_URL}/prometheus-guide" | \
jq -e '. == {source:"prometheus-guide-upstream"}'
for attempt in {1..12}; do
PROMETHEUS_RESPONSE="$(curl --fail --silent --show-error --get "${PROMETHEUS_URL}/api/v1/query" \
--data-urlencode 'query=apisix_http_status{route="prometheus-guide-route",code="200"}')"
jq -e '.status == "success" and any(.data.result[]?; (.["value"][1] | tonumber) > 0)' <<<"${PROMETHEUS_RESPONSE}" && break
sleep 5
done
jq -e '.status == "success" and any(.data.result[]?; (.["value"][1] | tonumber) > 0)' <<<"${PROMETHEUS_RESPONSE}"All assertions must pass. They prove the scrape target is healthy, the intended upstream responded, and Prometheus stored the HTTP status series for this exact Route.
Roll back / Clean up
Delete the Route, then remove the collector, upstream, files, and shell variables:
curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -X DELETE \
"${APISIX_ADMIN_URL}/apisix/admin/routes/prometheus-guide-route" \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f prometheus-guide-server prometheus-guide-upstream
rm -f prometheus-guide.yml prometheus-guide-nginx.conf
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL PROMETHEUS_URL GUIDE_DIR PROMETHEUS_RESPONSEExpected: the fixed Route, both containers, in-container metrics data, local files, and shell variables are absent.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| The preflight metrics assertion fails. | The export server is bound to container loopback, disabled, or uses another address. | Configure a protected scrape endpoint from the static configuration reference; verify it from the collector network before proceeding. |
| Prometheus stays unready. | The YAML is invalid, port 9092 is occupied, or local resources are insufficient. | Run docker logs prometheus-guide-server and docker inspect prometheus-guide-server; correct the issue without changing the fixed Route. |
The up query returns 0. | Prometheus cannot resolve or connect to apisix-quickstart:9091. | Confirm both containers use apisix-quickstart-net and repeat the preflight from the same network. |
| The upstream response passes but the Route series is absent. | The Route configuration has not propagated or the next scrape has not completed. | Read the fixed Route through the authenticated Admin API, inspect APISIX errors, and retain the bounded polling interval. |
| Prometheus uses excessive memory or storage. | Retention, scrape frequency, or label cardinality is too high. | Measure active series, disable only optional labels documented for APISIX, and size production storage from retention and ingestion targets. |
Related
- Prometheus plugin (opens in Plugin Hub docs) — metric names, label semantics, cardinality controls, and alternative export paths.
- Prometheus configuration (opens in Plugin Hub docs) — static server, Route parameter, and plugin-metadata fields.
- Grafana support for Prometheus — add visualization only after the scrape and query assertions pass.