Skip to content

feat(storage): Forward configured headers to Elasticsearch/OpenSearch backend#8544

Merged
yurishkuro merged 15 commits into
jaegertracing:mainfrom
ChaitanyaD48:feat/header-forwarding-es
May 10, 2026
Merged

feat(storage): Forward configured headers to Elasticsearch/OpenSearch backend#8544
yurishkuro merged 15 commits into
jaegertracing:mainfrom
ChaitanyaD48:feat/header-forwarding-es

Conversation

@ChaitanyaD48
Copy link
Copy Markdown
Contributor

@ChaitanyaD48 ChaitanyaD48 commented May 10, 2026

Which problem is this PR solving?

Resolves #8540

PR #8539 added a header_forwarding config to jaeger_query and wired it to the v2 gRPC storage backend.
This PR adds the outbound HTTP forwarding hop so the same configuration works end-to-end for ElasticSearch/OpenSearch

This PR builds on top of #8539, which introduced the internal/headerforwarding package along with the HTTP middleware and gRPC unary/stream server + client interceptors. It adds the missing HTTP-client analog and wires it into the Elasticsearch storage path.

Description of the changes

  • internal/headerforwarding/http_client.go

    Added a new NewHTTPClientRoundTripper(base) implementation. It reads CapturedHeaders from the request context and injects them into outbound *http.Requests using ForwardedHeader.HTTPName.

    This acts as the HTTP equivalent of the existing NewUnaryClientInterceptor / NewStreamClientInterceptor.

  • internal/storage/elasticsearch/config/config.go

    Wrapped the outbound HTTP transport with the new RoundTripper at both Elasticsearch client installation sites:

    • newElasticsearchV8 (v8 client)
    • getConfigOptions (v7 / olivere client)

    The wrapper is applied outside the existing TLS / bearer-token / body-fix transport chain so captured headers are appended last on outbound requests.

  • Header capture behavior remains unchanged.

    Capture still occurs on the query-server side through HTTPServerMiddleware introduced in feat(query): Add configurable header forwarding to gRPC storage backend #8539. This PR only adds the outbound HTTP forwarding hop for Elasticsearch / OpenSearch.

How was this change tested?

  • Added unit tests in internal/headerforwarding/http_client_test.go covering:

    • Pass-through behavior
    • Header injection
    • Skip-empty / invalid entries
    • Error propagation
    • Request clone contract validation
  • Added integration tests in internal/storage/elasticsearch/config/config_test.go using an httptest.Server to verify:

    • Captured headers are forwarded to outbound Elasticsearch requests
    • Requests without captured headers remain unchanged
  • Verified end-to-end locally against Elasticsearch 8 (docker-compose/elasticsearch/v8) using mitmweb as a reverse proxy.

Configuration used:

jaeger_query:
  header_forwarding:
    - http_name: X-Forwarded-User
      header_role: username
    - http_name: X-Forwarded-Email
      header_role: email

Test Request - curl -H 'X-Forwarded-User: alice' \ -H 'X-Forwarded-Email: alice@example.com' \ http://localhost:16686/api/services
image

Negative control — same curl without the headers; outbound ES requests do not carry X-Forwarded-*.
image

Checklist

AI Usage in this PR (choose one)

See AI Usage Policy.

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

yurishkuro and others added 9 commits May 10, 2026 02:30
Add a new internal/headerforwarding package that extracts specified
headers from inbound HTTP or gRPC requests and forwards them as gRPC
metadata on outbound calls to the remote storage backend.

Each forwarded header is configured with:
- http_name: HTTP request header to extract
- grpc_name: inbound gRPC metadata key (falls back to http_name)
- header_role: semantic role (username|email), informational only
- grpc_outbound_name: outbound metadata key (falls back to grpc_name/http_name)

Wire into jaeger-query (HTTP middleware + gRPC server interceptors)
and the gRPC storage factory (client interceptors).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
…e values

Matches http.Header.Get() semantics which also returns the first value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
…andler chain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Yuri Shkuro <github@ysh.us>
…to Elasticsearch requests

Signed-off-by: ChaitanyaD48 <chaitanya.d48@gmail.com>
…sing headerforwarding RoundTripper

Signed-off-by: ChaitanyaD48 <chaitanya.d48@gmail.com>
@ChaitanyaD48 ChaitanyaD48 requested a review from a team as a code owner May 10, 2026 13:44
Copilot AI review requested due to automatic review settings May 10, 2026 13:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds end-to-end header forwarding support so identity headers captured by jaeger-query can be propagated to HTTP-based storage backends (Elasticsearch/OpenSearch), complementing the existing gRPC storage forwarding support.

Changes:

  • Introduces an HTTP RoundTripper in internal/headerforwarding that injects captured headers from context.Context into outbound HTTP requests.
  • Wraps Elasticsearch/OpenSearch HTTP transports (v7 olivere + v8 client) with the new forwarding RoundTripper, ensuring forwarded headers are applied on outbound requests.
  • Adds/extends header-forwarding capture and propagation wiring in jaeger-query (HTTP middleware + gRPC interceptors) and updates unit/integration tests.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/storage/v2/grpc/factory.go Installs header-forwarding gRPC client interceptors when configured.
internal/storage/v2/grpc/factory_test.go Adds factory test coverage for enabling header forwarding.
internal/storage/v2/grpc/config.go Extends gRPC storage config with header_forwarding.
internal/storage/elasticsearch/config/config.go Wraps ES transports with HTTP header-forwarding RoundTripper (v7 + v8).
internal/storage/elasticsearch/config/config_test.go Adds httptest.Server coverage asserting forwarded headers reach outbound ES requests.
internal/headerforwarding/package_test.go Adds goleak verification TestMain for the package tests.
internal/headerforwarding/http.go Adds HTTP server middleware to capture configured headers into request context.
internal/headerforwarding/http_test.go Adds tests for HTTP header capture middleware.
internal/headerforwarding/http_client.go Adds HTTP client RoundTripper for propagating captured headers to outbound HTTP requests.
internal/headerforwarding/http_client_test.go Adds unit tests for the forwarding RoundTripper.
internal/headerforwarding/grpc.go Adds gRPC server/client interceptors for capture + propagation via metadata.
internal/headerforwarding/grpc_test.go Adds unit tests for gRPC interceptors and fallback behaviors.
internal/headerforwarding/context.go Adds context storage helpers for captured header pairs.
internal/headerforwarding/context_test.go Adds tests for context round-tripping captured headers.
internal/headerforwarding/config.go Adds ForwardedHeader config schema and name fallback helpers.
cmd/jaeger/internal/extension/jaegerquery/internal/server.go Wires header capture into jaeger-query HTTP and gRPC server stacks.
cmd/jaeger/internal/extension/jaegerquery/internal/server_test.go Adds tests validating capture into context for both HTTP and gRPC requests.
cmd/jaeger/internal/extension/jaegerquery/internal/flags.go Adds header_forwarding config field to query options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to +29
// ForwardedHeader describes one header to be forwarded from inbound requests to outbound gRPC storage calls.
type ForwardedHeader struct {
// HTTPName is the name of the HTTP request header to extract on inbound HTTP requests.
HTTPName string `mapstructure:"http_name"`
// GRPCName is the name of the gRPC metadata key to extract on inbound gRPC requests.
// When empty, HTTPName is used as the fallback.
GRPCName string `mapstructure:"grpc_name"`
// Role describes the semantic meaning of the header value (e.g. username, email).
// Jaeger does not act on this today; it is informational for downstream consumers.
Role HeaderRole `mapstructure:"header_role"`
// GRPCOutboundName is the metadata key used when forwarding the value to the gRPC storage backend.
// When empty, GRPCName/HTTPName is used as the fallback (in that order).
GRPCOutboundName string `mapstructure:"grpc_outbound_name"`
}
Comment on lines +34 to +39
for _, c := range captured {
if c.Value == "" || c.Header == nil || c.Header.HTTPName == "" {
continue
}
cloned.Header.Set(c.Header.HTTPName, c.Value)
}
Comment on lines +32 to +39
// RoundTripper contract: do not mutate the inbound *http.Request.
cloned := req.Clone(req.Context())
for _, c := range captured {
if c.Value == "" || c.Header == nil || c.Header.HTTPName == "" {
continue
}
cloned.Header.Set(c.Header.HTTPName, c.Value)
}
Comment on lines +23 to +31
func TestNewHTTPClientRoundTripper_NilBaseUsesDefault(t *testing.T) {
rt := headerforwarding.NewHTTPClientRoundTripper(nil)
require.NotNil(t, rt)
// We cannot call DefaultTransport here without making a real network call,
// but we can confirm the RoundTripper short-circuits when there are no
// captured headers and forwards to its base. Using a non-nil base verifies
// the wrapping behavior; the nil branch is exercised by construction above.
}


func appendOutgoing(ctx context.Context) context.Context {
for _, c := range CapturedFromContext(ctx) {
ctx = metadata.AppendToOutgoingContext(ctx, c.Header.outboundGRPCName(), c.Value)
@ChaitanyaD48 ChaitanyaD48 changed the title feat(storage): Forward configured headers to Elasticsearch/OpenSearch backend feat(storage): Configurable header forwarding from query to Elasticsearch/OpenSearch storage backends May 10, 2026
@ChaitanyaD48 ChaitanyaD48 changed the title feat(storage): Configurable header forwarding from query to Elasticsearch/OpenSearch storage backends feat(storage): Forward configured headers to Elasticsearch/OpenSearch backend May 10, 2026
if c.Value == "" || c.Header == nil || c.Header.HTTPName == "" {
continue
}
cloned.Header.Set(c.Header.HTTPName, c.Value)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there must be a way to rewrite the header name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review @yurishkuro! Fixed in the latest push. Mirrored the existing GRPCOutboundName pattern.

Copilot AI review requested due to automatic review settings May 10, 2026 17:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +423 to +426
// Outermost wrapper: forward headers captured on the inbound request context
// (populated by the jaeger_query header_forwarding middleware/interceptors)
// onto every outbound request to Elasticsearch.
options.Transport = headerforwarding.NewHTTPClientRoundTripper(transport)
Comment on lines +24 to +29
rt := headerforwarding.NewHTTPClientRoundTripper(nil)
require.NotNil(t, rt)
// We cannot call DefaultTransport here without making a real network call,
// but we can confirm the RoundTripper short-circuits when there are no
// captured headers and forwards to its base. Using a non-nil base verifies
// the wrapping behavior; the nil branch is exercised by construction above.
…client requests

Signed-off-by: ChaitanyaD48 <chaitanya.d48@gmail.com>
…ack to HTTPName

Signed-off-by: ChaitanyaD48 <chaitanya.d48@gmail.com>
…ack to HTTPName

Signed-off-by: ChaitanyaD48 <chaitanya.d48@gmail.com>
Copilot AI review requested due to automatic review settings May 10, 2026 18:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment on lines +40 to +44
if name == "" {
continue
}
cloned.Header.Set(name, c.Value)
}
Comment on lines +29 to +31
// HTTPOutboundName is the header name used when forwarding the value to an HTTP storage backend.
// When empty, HTTPName is used as the fallback.
HTTPOutboundName string `mapstructure:"http_outbound_name"`
Comment on lines +23 to +30
func TestNewHTTPClientRoundTripper_NilBaseUsesDefault(t *testing.T) {
rt := headerforwarding.NewHTTPClientRoundTripper(nil)
require.NotNil(t, rt)
// We cannot call DefaultTransport here without making a real network call,
// but we can confirm the RoundTripper short-circuits when there are no
// captured headers and forwards to its base. Using a non-nil base verifies
// the wrapping behavior; the nil branch is exercised by construction above.
}
@ChaitanyaD48 ChaitanyaD48 requested a review from yurishkuro May 10, 2026 18:33
@yurishkuro yurishkuro added the changelog:new-feature Change that should be called out as new feature in CHANGELOG label May 10, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.51%. Comparing base (8f60037) to head (909c1e4).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8544   +/-   ##
=======================================
  Coverage   96.51%   96.51%           
=======================================
  Files         329      330    +1     
  Lines       17342    17364   +22     
=======================================
+ Hits        16737    16759   +22     
  Misses        455      455           
  Partials      150      150           
Flag Coverage Δ
badger_direct 8.95% <0.00%> (-0.03%) ⬇️
badger_e2e 1.04% <0.00%> (-0.01%) ⬇️
cassandra-4.x-direct-manual 14.56% <0.00%> (-0.04%) ⬇️
cassandra-4.x-e2e-auto 1.03% <0.00%> (-0.01%) ⬇️
cassandra-4.x-e2e-manual 1.03% <0.00%> (-0.01%) ⬇️
cassandra-5.x-direct-manual 14.56% <0.00%> (-0.04%) ⬇️
cassandra-5.x-e2e-auto 1.03% <0.00%> (-0.01%) ⬇️
cassandra-5.x-e2e-manual 1.03% <0.00%> (-0.01%) ⬇️
clickhouse-direct 8.98% <0.00%> (-0.03%) ⬇️
clickhouse-e2e 1.16% <0.00%> (-0.01%) ⬇️
elasticsearch-6.x-direct 16.89% <30.43%> (+0.05%) ⬆️
elasticsearch-7.x-direct 16.92% <30.43%> (+0.05%) ⬆️
elasticsearch-8.x-direct 17.07% <34.78%> (+0.05%) ⬆️
elasticsearch-8.x-e2e 1.04% <0.00%> (-0.01%) ⬇️
elasticsearch-9.x-e2e 1.04% <0.00%> (-0.01%) ⬇️
grpc_direct 7.89% <0.00%> (-0.02%) ⬇️
grpc_e2e 1.04% <0.00%> (-0.01%) ⬇️
kafka-3.x-v2 1.04% <0.00%> (-0.01%) ⬇️
memory_v2 1.04% <0.00%> (-0.01%) ⬇️
opensearch-1.x-direct 16.96% <30.43%> (+0.05%) ⬆️
opensearch-2.x-direct 16.96% <30.43%> (+0.05%) ⬆️
opensearch-2.x-e2e 1.04% <0.00%> (-0.01%) ⬇️
opensearch-3.x-e2e 1.04% <0.00%> (-0.01%) ⬇️
query 1.04% <0.00%> (-0.01%) ⬇️
tailsampling-processor 0.53% <0.00%> (-0.01%) ⬇️
unittests 94.79% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yurishkuro yurishkuro merged commit b818cfd into jaegertracing:main May 10, 2026
71 of 72 checks passed
@ChaitanyaD48 ChaitanyaD48 deleted the feat/header-forwarding-es branch May 11, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/storage changelog:new-feature Change that should be called out as new feature in CHANGELOG enhancement storage/elasticsearch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(storage): Configurable header forwarding from query to storage backends

3 participants