Skip to content

fix(policy-engine): extract trace context from request headers - #3061

Open
renuka-fernando wants to merge 3 commits into
wso2:mainfrom
renuka-fernando:fix-policy-engine-trace-context-extraction
Open

fix(policy-engine): extract trace context from request headers#3061
renuka-fernando wants to merge 3 commits into
wso2:mainfrom
renuka-fernando:fix-policy-engine-trace-context-extraction

Conversation

@renuka-fernando

Copy link
Copy Markdown
Contributor

Purpose

The policy engine logged No valid trace context extracted on every request and never continued the router's distributed trace. The extraction read traceparent/tracestate from the ext_proc gRPC stream metadata, but the downstream request's W3C trace context is delivered by Envoy inside the RequestHeaders ProcessingRequest body — not as stream metadata. The long-lived bidirectional ext_proc stream also sets its gRPC metadata only once at establishment, so it can never carry a per-request traceparent. As a result the propagator always produced an empty (zero-ID) span context whose IsValid() is false, causing the noisy log and an unparented root trace for every request.

Goals

Extract the trace context from the correct source (the HTTP request headers) so the policy engine's spans continue the incoming trace, and stop logging a benign condition at Info on the per-request path.

Approach

  • Build the W3C trace-context carrier from the HTTP request headers in the RequestHeaders phase (new traceContextCarrier helper), copying only traceparent/tracestate, lowercasing keys and preferring RawValue with a Value fallback.
  • Change tracing.ExtractTraceContext to accept a propagation.TextMapCarrier instead of reading gRPC stream metadata, keeping the tracing package free of Envoy proto types.
  • Create the root ext_proc span lazily on the first message, parented on the extracted context, so child spans correctly continue the router's trace.
  • A stream that fails before the first message still starts a root span so stream-level errors remain observable.
  • Demote the not-found log from Info to Debug — it is an ordinary condition (e.g. Envoy tracing disabled or no inbound traceparent).

User stories

N/A

Documentation

N/A — internal observability fix with no user-facing API or configuration change.

Automation tests

  • Unit tests

    Reworked ExtractTraceContext tests to drive propagation.MapCarrier instead of gRPC metadata. Added TestTraceContextCarrier covering header extraction, the deprecated Value fallback, non-header messages, and the no-headers case. Existing Process span-status tests (stream recv error, context canceled) still pass.

  • Integration tests

    No new integration tests; the full policy-engine module test suite passes (go test ./...) and go vet ./... is clean.

Security checks

Samples

N/A

Related PRs

N/A

Test environment

Go (policy-engine module), macOS (darwin/arm64). Verified via go build ./..., go vet ./..., and go test ./....

Related Issues

N/A

Checklist

  • Tests added or updated (unit, integration, etc.)
  • Samples updated (if applicable)

The request's W3C traceparent is delivered by Envoy inside the
RequestHeaders ProcessingRequest body, not as ext_proc gRPC stream
metadata. Reading it from stream metadata always yielded an empty
span context, logging "No valid trace context extracted" per request
and starting an unparented root trace every time.

- Build the trace-context carrier from the HTTP request headers and
  create the root span lazily on the first message, so child spans
  correctly continue the router's trace
- Demote the not-found log from Info to Debug (an ordinary condition,
  e.g. Envoy tracing disabled)
- A stream failing before the first message still starts a root span
  so stream-level errors remain observable

Signed-off-by: Renuka Fernando <renukapiyumal@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@renuka-fernando, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 635eb1a4-ce26-49d3-ab0f-d39a87d5b43f

📥 Commits

Reviewing files that changed from the base of the PR and between 8e94333 and 2f2056b.

📒 Files selected for processing (1)
  • gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go
📝 Walkthrough

Walkthrough

The policy engine extracts W3C trace context from Envoy HTTP headers. Root spans are created lazily from the first request’s context. Carrier-based tracing tests replace gRPC metadata-based tests.

Changes

Trace context propagation

Layer / File(s) Summary
HTTP trace carrier
gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go, gateway/gateway-runtime/policy-engine/internal/kernel/extproc_test.go
The policy engine extracts traceparent and tracestate from Envoy headers, normalizes header names, and falls back to deprecated header values. Tests cover unrelated, missing, and non-header messages.
Carrier-based trace extraction
gateway/gateway-runtime/policy-engine/internal/tracing/tracer.go, gateway/gateway-runtime/policy-engine/internal/tracing/tracer_test.go
ExtractTraceContext now accepts a propagation.TextMapCarrier. Tests cover valid, invalid, empty, nil, sampled, and additional HTTP headers.
Deferred span initialization
gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go
Root-span creation moves to the first received request. Later messages reuse the span, while early receive failures create an unparented root span.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • wso2/api-platform#2989: Both PRs modify internal/kernel/extproc.go tracing behavior, but this PR changes trace-context propagation while that PR records terminal HTTP outcomes and error spans.

Suggested reviewers: krishanx92, anugayan, arshardh

Sequence Diagram(s)

sequenceDiagram
  participant Envoy
  participant ExtProcStream
  participant traceContextCarrier
  participant ExtractTraceContext
  participant RootSpan
  Envoy->>ExtProcStream: Send first HTTP request
  ExtProcStream->>traceContextCarrier: Read traceparent and tracestate
  traceContextCarrier-->>ExtProcStream: Return propagation carrier
  ExtProcStream->>ExtractTraceContext: Extract W3C trace context
  ExtractTraceContext-->>ExtProcStream: Return extracted context
  ExtProcStream->>RootSpan: Create root span
  ExtProcStream->>RootSpan: Reuse span for later messages
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the purpose, goals, approach, tests, security checks, documentation impact, and test environment; some non-applicable sections are marked N/A.
Title check ✅ Passed The title clearly and concisely describes the main change: extracting policy-engine trace context from request headers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go`:
- Around line 113-124: The traceContextCarrier header loop currently overwrites
repeated tracestate values. Update the carrier assignment to append subsequent
tracestate values with a comma while preserving field order, without changing
traceparent handling; add a regression test covering two tracestate headers and
their combined value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 18881ea1-47f0-4935-8e64-a1f34ffbb0d7

📥 Commits

Reviewing files that changed from the base of the PR and between 29f710c and 68ae532.

📒 Files selected for processing (4)
  • gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/extproc_test.go
  • gateway/gateway-runtime/policy-engine/internal/tracing/tracer.go
  • gateway/gateway-runtime/policy-engine/internal/tracing/tracer_test.go

Comment thread gateway/gateway-runtime/policy-engine/internal/kernel/extproc.go
W3C Trace Context permits tracestate to be split across multiple header
fields; a receiver must combine them into a single comma-separated value
in field order rather than overwrite. traceContextCarrier now appends
repeated tracestate values (traceparent stays last-wins, as multiple
traceparent is malformed). Adds a regression test with two tracestate
headers.

Addresses PR review feedback.

Signed-off-by: Renuka Fernando <renukapiyumal@gmail.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 2, 2026
The tracer is a no-op when tracing is disabled, so building the carrier
and running the propagator per request was wasted work on the ext_proc
hot path. Gate it behind the (previously unused) tracingConfig.Enabled
flag.

- Store tracingEnabled on the server from tracingConfig.Enabled
- Add startRootSpan helper that skips carrier build + extraction when
  disabled and de-duplicates the two root-span creation sites

Signed-off-by: Renuka Fernando <renukapiyumal@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant