Trace Requests with Zipkin
Trace requests from Apache APISIX to a controlled local upstream, verify the resulting spans through the Zipkin API, and clean up.
Applies to:Apache APISIX 3.18.0
Deployments:DockerRoles:Platform engineerTime:15 minutesImpact:Changes live traffic
Goal
Enable the zipkin plugin on one isolated Route, send spans to a pinned local Zipkin server, and prove through the Zipkin query API that a request produced spans for the intended service. The example does not enable tracing globally or depend on a public upstream.
Tracing can record request metadata and adds processing and storage overhead. This local test samples every request for deterministic validation; select a lower production sampling ratio and apply access control, encryption, and retention to the collector.
Before you begin
- Complete Get APISIX with the Docker quickstart.
- Install Docker, cURL, and jq.
- Use a shell that supports Bash or Zsh heredocs.
- Ensure TCP port
9411on loopback is available for the temporary Zipkin UI and query API.
Set the endpoints, current directory, and Admin API key:
export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
export ZIPKIN_URL="http://127.0.0.1:9411"
export GUIDE_DIR="$(pwd)"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echoProtect the fixed Route, containers, and local file used by this guide:
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/zipkin-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route zipkin-guide-route already exists'; exit 1; }
for container in zipkin-guide-server zipkin-guide-upstream; do
test -z "$(docker ps -aq --filter "name=^/${container}$")" || { echo "Stop: container ${container} already exists"; exit 1; }
done
test ! -e zipkin-guide-nginx.conf || { echo 'Stop: zipkin-guide-nginx.conf already exists'; exit 1; }Steps
Step 1: Start a pinned Zipkin server
Start Zipkin on the APISIX quickstart network and publish its UI and query API only on loopback:
docker run -d --name zipkin-guide-server \
--network apisix-quickstart-net \
-p 127.0.0.1:9411:9411 \
openzipkin/zipkin:3.6.1
for attempt in {1..60}; do
curl --fail --silent --show-error --connect-timeout 2 --max-time 5 \
"${ZIPKIN_URL}/health" >/dev/null && break
sleep 1
done
curl --fail --silent --show-error --connect-timeout 2 --max-time 5 \
"${ZIPKIN_URL}/health" >/dev/nullWhat changed: a fresh in-memory Zipkin 3.6.1 server is reachable by APISIX and from local loopback, but not from other host interfaces.
Step 2: Start a controlled upstream
Create a deterministic NGINX response and start it without publishing an upstream port:
cat > zipkin-guide-nginx.conf <<'NGINX'
events {}
http {
access_log off;
server {
listen 80;
location = /zipkin-guide { return 200 '{"source":"zipkin-guide-upstream"}'; }
}
}
NGINX
docker run -d --name zipkin-guide-upstream \
--network apisix-quickstart-net \
-v "${GUIDE_DIR}/zipkin-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/zipkin-guide-upstream/80' >/dev/null 2>&1 && break
sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/zipkin-guide-upstream/80'What changed: APISIX has one controlled local HTTP endpoint for the traced request.
Step 3: Create the traced Route
Create one authenticated fixed-ID Route. sample_ratio: 1 makes this isolated validation deterministic, while span_version: 2 emits the current two-span representation:
curl -sS --connect-timeout 3 --max-time 15 \
"${APISIX_ADMIN_URL}/apisix/admin/routes/zipkin-guide-route" -X PUT \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H 'Content-Type: application/json' \
-d '{
"name":"zipkin-guide-route",
"uri":"/zipkin-guide",
"methods":["GET"],
"plugins":{
"zipkin":{
"endpoint":"http://zipkin-guide-server:9411/api/v2/spans",
"sample_ratio":1,
"service_name":"apisix-zipkin-guide",
"span_version":2
}
},
"upstream":{"type":"roundrobin","nodes":{"zipkin-guide-upstream:80":1}}
}' | \
jq -e '.value.id == "zipkin-guide-route" and .value.plugins.zipkin.endpoint == "http://zipkin-guide-server:9411/api/v2/spans" and .value.plugins.zipkin.service_name == "apisix-zipkin-guide"'What changed: only GET /zipkin-guide is sampled and reported to the isolated Zipkin server.
Validate
Require the controlled upstream response:
curl --fail-with-body -sS --connect-timeout 3 --max-time 10 \
"${GATEWAY_URL}/zipkin-guide" | \
jq -e '. == {source:"zipkin-guide-upstream"}'Poll the Zipkin v2 query API until it returns a trace containing the configured local service and both APISIX span names:
for attempt in {1..30}; do
curl --fail --silent --show-error --connect-timeout 3 --max-time 10 \
"${ZIPKIN_URL}/api/v2/traces?serviceName=apisix-zipkin-guide&limit=10" | \
jq -e 'length > 0 and any(.[][]; .localEndpoint.serviceName == "apisix-zipkin-guide") and (([.[][] | .name] | index("proxy")) != null) and (([.[][] | .name] | index("response")) != null)' >/dev/null && break
sleep 1
done
curl --fail --silent --show-error --connect-timeout 3 --max-time 10 \
"${ZIPKIN_URL}/api/v2/traces?serviceName=apisix-zipkin-guide&limit=10" | \
jq -e 'length > 0 and any(.[][]; .localEndpoint.serviceName == "apisix-zipkin-guide") and (([.[][] | .name] | index("proxy")) != null) and (([.[][] | .name] | index("response")) != null)'Both assertions must pass. The first proves traffic reached the intended upstream; the second proves Zipkin stored the current APISIX span representation for that request. The optional UI is available at ${ZIPKIN_URL}/zipkin/ while the container is running.
Roll back / Clean up
Delete the Route before removing its collector and upstream, then delete the local configuration file:
curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -X DELETE \
"${APISIX_ADMIN_URL}/apisix/admin/routes/zipkin-guide-route" \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f zipkin-guide-upstream zipkin-guide-server
rm -f zipkin-guide-nginx.conf
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL ZIPKIN_URL GUIDE_DIRExpected: the fixed Route, two containers, local file, loopback listener, and in-memory traces are absent.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| Zipkin does not become healthy. | Port 9411 is occupied, the image could not start, or the host lacks resources. | Inspect docker logs zipkin-guide-server and the port binding; keep the listener on loopback. |
| The gateway response succeeds but no trace appears. | APISIX cannot resolve or reach zipkin-guide-server, or reporting has not completed. | Verify both containers share apisix-quickstart-net, inspect APISIX errors, and keep the bounded poll rather than assuming immediate delivery. |
| The trace exists without both expected span names. | The Route uses a different span_version or the stored trace came from another service. | Read the fixed Route and query only apisix-zipkin-guide; do not loosen the span assertion. |
| Tracing overhead is too high in a load test. | Sampling every request creates work and storage for every request. | Reduce sample_ratio for the tested production workload and measure the resulting coverage and overhead. |
Related
- Zipkin plugin (opens in Plugin Hub docs) — endpoint, sampling, service-name, and span-version fields.
- Zipkin API — v2 span ingestion and trace-query endpoints.
- OpenZipkin releases — pinned server version provenance.
- OpenTelemetry plugin (opens in Plugin Hub docs) — export APISIX traces with OpenTelemetry instead.