bhuvan-somisetty opened a new issue, #13945:
URL: https://github.com/apache/apisix/issues/13945

   ### Description
   
   The `opentelemetry` plugin emits exactly one span per request, with `kind = 
span_kind.server`, and never emits a `CLIENT` span for the call APISIX makes to 
the upstream. Verified directly against current `master` (2026-09-15):
   
   ```
   $ grep -n "span_kind\." apisix/plugins/opentelemetry.lua apisix/tracer.lua
   apisix/plugins/opentelemetry.lua:429:        kind = span_kind.server,
   ```
   
   `span_kind.client` does not appear anywhere in `opentelemetry.lua`. All 
internal phase spans created via `apisix/tracer.lua` (`apisix.phase.access`, 
`apisix.phase.header_filter`, `apisix.phase.body_filter`, `http_router_match`, 
`resolve_dns`, ...) are `server` or `internal` kind and describe APISIX's own 
phases, not the outbound call to the upstream.
   
   Originally raised and investigated in #13828 (credit: @alice101-dev, who 
supplied the reproduction below). I independently re-verified the root cause 
against current master and traced a concrete fix path through the existing 
tracer internals (details below), and I'm filing it as a tracked issue since it 
has a clear, actionable solution and real impact but no PR yet.
   
   ### Reproduction / evidence
   
   Same application, same OpenTelemetry Collector, same APM backend 
(HyperDX/ClickStack) — only the gateway in front differs.
   
   **Fronted by APISIX**: the service map shows two disconnected clusters. 
`api-gateway` is joined to the callers in front of it, but not to the services 
behind it (e.g. `orders-service`).
   
   **Fronted by Kong** (which emits a `kong.balancer` CLIENT span for the same 
hop): one continuous chain from browser through to backends.
   
   Every cross-service parent/child span pair in the trace data, with span 
kinds:
   
   | parent | parent kind | child | child kind | service-map edge drawn |
   |---|---|---|---|---|
   | orders | CLIENT | inventory | SERVER | yes |
   | orders | CLIENT | payments | SERVER | yes |
   | storefront-web | CLIENT | api-gateway | SERVER | yes |
   | **api-gateway** | **SERVER** | orders | SERVER | **no** |
   | **api-gateway** | **SERVER** | auth | SERVER | **no** |
   
   Context propagation itself is not broken — child spans exist and are 
correctly parented to the gateway's span. There is simply no `CLIENT` span for 
the proxy hop for APM tools to anchor a `CLIENT -> SERVER` edge to, which is 
the pairing that OpenTelemetry-based service-map generation (Jaeger, Grafana 
Tempo, HyperDX/ClickStack, Datadog APM, etc.) relies on by convention.
   
   Environment: APISIX 3.17.0 and master, official `apache/apisix` image, 
`apisix-ingress-controller` 2.1.0 with Gateway API, OTel Collector -> 
ClickHouse.
   
   ### Root cause
   
   `apisix/plugins/opentelemetry.lua:428-431` starts a single request span with 
a hardcoded `kind = span_kind.server` and never opens a corresponding `CLIENT` 
span around the proxy-to-upstream leg:
   
   ```lua
   local ctx = tracer:start(upstream_context, span_name, {
       kind = span_kind.server,
       attributes = attributes,
   })
   ```
   
   This was not addressed by the two most recent tracing PRs on this plugin 
(#12686, merged 2026-02-07, added more *phase* spans; #13008, merged 
2026-02-13, fixed SSL span nesting) — neither introduces a `CLIENT`-kind span 
for the upstream call, which #13828's author confirmed by inspecting master 
after both had landed, and which I re-confirmed today with the grep above.
   
   Note this isn't a missing capability in the tracing layer: 
`apisix/secret.lua:158` already creates a `tracer.kind.client` span for 
outbound secret-manager fetches, so the `client` kind is already wired through 
`apisix/tracer.lua` and simply never applied to the (far more common) upstream 
proxy hop.
   
   ### Impact
   
   Any APISIX deployment using the `opentelemetry` plugin with an APM backend 
that builds a service topology from span-kind pairs (which is most of them) 
shows APISIX as a disconnected node rather than the gateway hop it actually is. 
This is a correctness gap in the plugin's tracing semantics, not a 
configuration issue on the user's side — there is no config option in the 
plugin's schema (`sampler`, `additional_attributes`, 
`additional_header_prefix_attributes`) that can work around it. For anyone 
using APISIX as their edge/gateway with standard OTel-based observability 
tooling, this silently degrades one of the main reasons to adopt distributed 
tracing.
   
   ### Affected components
   
   - `apisix/plugins/opentelemetry.lua` (primary; this issue's scope)
   - `apisix/plugins/zipkin.lua` has the analogous gap (`["span.kind"] = 
"server"` only, confirmed via inspection) but is **not** in scope for this 
issue — worth a follow-up once the pattern here is settled, to avoid conflating 
two plugins' review cycles.
   
   ### Acceptance criteria
   
   - When the `opentelemetry` plugin is enabled and a request is proxied to an 
upstream, a `CLIENT`-kind span is emitted as a child of the existing `SERVER` 
span, bounding the upstream leg (started before the upstream call, finished 
once the upstream response is known).
   - The `CLIENT` span carries standard OTel HTTP client attributes at minimum: 
`server.address`, `server.port` (available via `ctx.balancer_ip` / 
`ctx.balancer_port`, set in `apisix/balancer.lua`), and 
`http.response.status_code`.
   - Span timing reflects the real upstream latency rather than the full 
request latency (`ngx.var.upstream_connect_time` / 
`ngx.var.upstream_response_time` are already read elsewhere, e.g. 
`apisix/init.lua:1213-1214`, confirming they're available at 
`log`/`header_filter` time).
   - No regression in existing `t/plugin/opentelemetry*.t` tests; new test 
coverage added in the same suite for: span present, correct parent/child 
relationship, correct `kind`, and correct attributes.
   - Behavior is off by default in no way that requires a new schema field 
unless the implementation direction settled on in review needs one (to be 
confirmed during design discussion, not assumed here).
   
   ### Solution direction (starting point, not a mandate)
   
   `apisix/plugins/opentelemetry.lua` already has the mechanism needed via 
`create_child_span()` (line ~454), which accepts an arbitrary `span.kind` plus 
explicit `span.start_time` / `span.end_time` to backdate a span rather than 
requiring a live start/finish hook at the exact call site — this is the same 
technique that would let a `CLIENT` span be constructed after the fact from 
`ngx.var.upstream_connect_time` / `upstream_response_time`, without needing to 
hook into `balancer_by_lua` directly (nginx's actual `proxy_pass` to the 
upstream is not itself Lua-driven, so there's no single call to wrap 
synchronously).
   
   The concrete open question, flagged by the original reporter and still open: 
whether the `CLIENT` span's create/finish should live in `header_filter` 
(attributes and timing are known but the request log's `upstream_response_time` 
isn't finalized as a var yet in some codepaths) or `log` (all upstream vars are 
final, but this is after the response has already started streaming to the 
client, which is fine for tracing since APM ingestion is always async). This is 
the main design decision a contributor would need maintainer input on before 
implementing.
   
   Given the scope (touches the tracing plugin's span-emission logic, requires 
understanding of nginx phase boundaries and upstream timing vars, and needs 
solid regression tests against the existing Test::Nginx-based suite) without 
being an open-ended research problem (the data needed is already available in 
`ctx`, and the span-construction pattern already exists in the same file), this 
seems like a good fit for a mentored contribution.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to