Configure Distributed Tracing
Distributed tracing shows how requests move through API7 Gateway and backend services, helping you locate latency and failures. API7 Gateway uses the opentelemetry plugin to create spans and export them to an OTLP/HTTP endpoint, such as an OpenTelemetry Collector or a compatible observability backend.
This guide configures a local Jaeger instance for evaluation, enables tracing globally, and correlates the exported traces with access logs.
How It Works
The opentelemetry plugin uses two configuration layers:
- Plugin metadata configures the collector endpoint, request timeout, resource attributes, custom headers, batch span processor, and trace ID source for a gateway group.
- Plugin configuration selects a sampling strategy and adds span attributes. You can apply the plugin to a route, service, or global rule.
Plugin metadata must be configured before the plugin can create and export spans.
Prerequisites
- A running API7 Gateway deployment with at least one online data plane.
- Docker to run Jaeger for this local evaluation.
- A Dashboard token assigned to the
API_KEYenvironment variable. - If you use ADC, install and configure ADC, including the
ADC_BACKEND,ADC_SERVER,ADC_TOKEN, andADC_GATEWAY_GROUPenvironment variables.
Start Jaeger
This local example assumes that the data plane runs in Docker. For another deployment type, use an OTLP/HTTP endpoint reachable from the data plane, skip the Docker commands in this section, and substitute its address when configuring plugin metadata.
Create a Docker network and connect the data plane container to it. Replace <data-plane-container> with the container name:
docker network create gateway-tracing-net
docker network connect gateway-tracing-net <data-plane-container>
Start a pinned Jaeger instance on the same network. The image accepts OTLP traces on port 4318 and provides the Jaeger UI on port 16686:
docker run -d --name jaeger \
--network gateway-tracing-net \
-p 127.0.0.1:16686:16686 \
cr.jaegertracing.io/jaegertracing/jaeger:2.20.0
Wait for the Jaeger UI to become available:
until curl -fsS "http://127.0.0.1:16686/" > /dev/null; do
sleep 1
done
This Jaeger instance uses transient in-memory storage without authentication or TLS. For production, use a secured collector or observability backend with durable storage. If a suitable OTLP/HTTP endpoint is already available, skip this section and substitute its address below.
Configure the Collector Endpoint
Configure plugin metadata to send spans to Jaeger and expose the trace and span IDs as NGINX variables. The variables are used later to correlate traces with access logs.
- Admin API
- ADC
curl -k "https://localhost:7443/apisix/admin/plugin_metadata/opentelemetry?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"resource": {
"service.name": "api7-gateway",
"deployment.environment": "development"
},
"collector": {
"address": "jaeger:4318",
"request_timeout": 3
},
"set_ngx_var": true
}'
Add the plugin metadata to your existing ADC configuration so other plugin metadata remains in the desired state:
plugin_metadata:
opentelemetry:
resource:
service.name: api7-gateway
deployment.environment: development
collector:
address: "jaeger:4318"
request_timeout: 3
set_ngx_var: true
Preview the plugin-metadata reconciliation and confirm that it contains no unintended updates or deletions:
adc diff -f adc.yaml --include-resource-type plugin_metadata
Synchronize the reviewed configuration:
adc sync -f adc.yaml --include-resource-type plugin_metadata
If the collector requires authentication, configure its headers under collector.request_headers. For all metadata fields and defaults, see the plugin metadata reference.
Enable Tracing Globally
Create a sample service and route, then apply opentelemetry in a global rule. The rule traces requests to every route in the gateway group. To trace selected traffic instead, configure the plugin on individual routes or services.
The examples use the fixed IDs tracing-demo and opentelemetry. Confirm that these IDs are unused, or replace them consistently throughout the configuration before applying it.
- Admin API
- ADC
Create a service with an upstream:
curl -k "https://localhost:7443/apisix/admin/services/tracing-demo?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "tracing-demo",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"nodes": [
{ "host": "httpbin.org", "port": 80, "weight": 1 }
]
}
}'
Create a route under the service:
curl -k "https://localhost:7443/apisix/admin/routes/tracing-demo?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "tracing-demo",
"service_id": "tracing-demo",
"paths": ["/anything"]
}'
Enable tracing with a global rule:
curl -k "https://localhost:7443/apisix/admin/global_rules/opentelemetry?gateway_group_id={gateway_group_id}" -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"plugins": {
"opentelemetry": {
"sampler": {
"name": "trace_id_ratio",
"options": { "fraction": 1.0 }
},
"additional_attributes": ["request_uri"]
}
}
}'
Add the service, route, and global rule to your existing ADC configuration so other services and global rules remain in the desired state:
services:
- name: tracing-demo
routes:
- name: tracing-demo
uris:
- /anything
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
global_rules:
opentelemetry:
sampler:
name: trace_id_ratio
options:
fraction: 1.0
additional_attributes:
- request_uri
Preview the reconciliation and confirm that it contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type global_rule
Synchronize the reviewed configuration. The resource-type filters also keep the plugin metadata unchanged:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type global_rule
Values in additional_attributes are APISIX variables, such as request_uri or route_name. The plugin automatically adds common attributes, including the matched route, route name, status code, and response source.
Choose a Sampling Strategy
| Strategy | When to use |
|---|---|
always_on | Development, staging, or low-volume traffic where every request should be traced. |
trace_id_ratio | Production traffic where a sampling fraction such as 0.01 keeps collector load and storage bounded. The decision is deterministic for services that share the same trace ID. |
parent_base | Traffic where an upstream service or service mesh makes the sampling decision. The gateway respects a parent decision and uses a root sampler when no parent span exists. |
always_off | The default. Use it to keep the plugin attached without emitting spans. |
A common production strategy is parent_base with a low-ratio root sampler. Existing distributed traces remain intact while new root traces are sampled at a controlled rate:
{
"sampler": {
"name": "parent_base",
"options": {
"root": {
"name": "trace_id_ratio",
"options": { "fraction": 0.01 }
}
}
}
}
For all plugin parameters, see the opentelemetry plugin reference.
Verify Trace Export
Send a request through the gateway:
curl -i "http://127.0.0.1:9080/anything"
Wait for the asynchronously exported trace to become available:
trace_ready=false
for attempt in $(seq 1 20); do
if curl -fsS \
"http://127.0.0.1:16686/api/traces?service=api7-gateway&limit=10&lookback=1h" | \
grep -q '"traceID"'; then
trace_ready=true
break
fi
sleep 1
done
[ "$trace_ready" = true ]
Navigate to the Jaeger UI at http://127.0.0.1:16686 and search for the api7-gateway service. The trace should include a span named GET /anything with attributes such as the status code, route name, response source, and request URI.
Correlate Traces with Access Logs
Add the trace and span ID variables to the access log format. This makes it possible to find the trace associated with a log entry.
- Host or Docker
- Kubernetes
Add the following configuration to the data plane's conf/config.yaml:
nginx_config:
http:
enable_access_log: true
access_log_format: '{"time":"$time_iso8601","trace_id":"$opentelemetry_trace_id","span_id":"$opentelemetry_span_id","status":$status,"request":"$request"}'
access_log_format_escape: json
Reload or restart the data plane for the change to take effect.
Add the following values to the data plane Helm configuration:
logs:
enableAccessLog: true
accessLogFormat: '{"time":"$time_iso8601","trace_id":"$opentelemetry_trace_id","span_id":"$opentelemetry_span_id","status":$status,"request":"$request"}'
accessLogFormatEscape: json
Apply the values. Replace the placeholders with the Helm release name and namespace:
helm upgrade <release-name> api7/gateway \
--version "~3.10.0" \
--namespace <namespace> \
-f values.yaml
After the data plane is ready, send another request:
curl -i "http://127.0.0.1:9080/anything"
The access log should contain nonempty trace and span IDs similar to the following:
{
"time": "2026-09-16T10:30:00+00:00",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"status": 200,
"request": "GET /anything HTTP/1.1"
}
The same variables can be used in the log_format field of logger plugins such as http-logger and kafka-logger.
Next Steps
- Monitor Metrics to correlate traces with Prometheus metrics and latency dashboards.
- Configure Centralized Logging to send trace-correlated access logs to a log backend.
- Review the
opentelemetryplugin reference for all plugin and metadata fields.