Skip to content

Conversation

@nacx
Copy link
Contributor

@nacx nacx commented Oct 12, 2025

Description

The LLM metrics and trace providers allow for the configuration of custom headers to be included as metric attributes and span attributes. The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take this configuration into account.

  • The tracer method to create a span now receives the request headers.
  • To avoid having to propagate request headers all the way down inside the MCP Proxy so that they can be taken into account when recording metrics, I've opted to instantiate an MCPProxy on each request, and have it set up with the request attributes from the beginning. This allows reusing the metrics implementation in a clean way.

Related Issues/PRs (if applicable)

Fixes #1335
Fixes #1348

Special notes for reviewers (if applicable)

N/A

@nacx nacx changed the title mpc: add configured header attribtues to metrics and spans mcp: add configured header attribtues to metrics and spans Oct 12, 2025
@codecov-commenter
Copy link

codecov-commenter commented Oct 12, 2025

Codecov Report

❌ Patch coverage is 97.70115% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.13%. Comparing base (3341eb8) to head (296fa40).

Files with missing lines Patch % Lines
internal/mcpproxy/mcpproxy.go 77.77% 4 Missing ⚠️

❌ Your project status has failed because the head coverage (77.13%) is below the target coverage (86.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1342      +/-   ##
==========================================
+ Coverage   77.03%   77.13%   +0.09%     
==========================================
  Files         125      126       +1     
  Lines       16136    16204      +68     
==========================================
+ Hits        12431    12499      +68     
  Misses       3080     3080              
  Partials      625      625              

☔ 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.

@nacx nacx marked this pull request as ready for review October 12, 2025 15:59
@nacx nacx requested a review from a team as a code owner October 12, 2025 15:59
Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

this is "ok" but I think we are misunderstanding the session concept here. So attributes like session.id will need to be propagated as request meta, just like trace IDs are. This change will not be able to be used for all kinds of session because it relies on header which are typically not set in the smaller scope of JSON rpc messages.

initializationDuration metric.Float64Histogram
capabilitiesNegotiated metric.Float64Counter
progressNotifications metric.Float64Counter
requestHeaderLabelMapping map[string]string // maps HTTP headers to metric label names.
Copy link
Contributor

Choose a reason for hiding this comment

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

please use the word attribute as this is otel and not just prom. label is a prom thing and applied at export

Copy link
Contributor Author

@nacx nacx Oct 13, 2025

Choose a reason for hiding this comment

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

Done. I've also added a commit to rename it in the chat completion and embeddings metrics implementations

}

span := m.tracer.StartSpanAndInjectMeta(ctx, req, p)
// TODO: headers with multiple values
Copy link
Contributor

Choose a reason for hiding this comment

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

curious why not pass http.Header type instead of converting it to a map like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mostly to keep a similar interface to the chat completions one: https://github.com/envoyproxy/ai-gateway/blob/main/internal/tracing/api/api.go#L81
But I can change to http.Header if we think that'd be better.

Copy link
Contributor

Choose a reason for hiding this comment

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

it isn't too important just I must have started a problem with copying in maps? if we don't need to copy we shouldn't especailly as headers are case insentive in http

@codefromthecrypt
Copy link
Contributor

another way out would be to do the HTTP tier in case some attribute maps apply, just don't close out MCP mapping until there's another attribute for request meta -> span attributes. Doing things at the HTTP layer will apply attributes to multiple traces (to to the JSON-RPC messaging layer), which is different than normal HTTP where one request is a single span.

modelcontextprotocol/modelcontextprotocol#414 is the details about why trace propagation is at meta level, and so likely should be other things like session.id.

@nacx
Copy link
Contributor Author

nacx commented Oct 13, 2025

I thought about the _meta field as well, but I don't know if it would be useful in practice to achieve the custom attribute thing.
Enriching LLM metrics with custom headers today, is mostly about initializing their agents with the headers they want. Something like:

client = AsyncOpenAI(base_url="...", api_key="...", default_headers=CUSTOM_HEADERS)
provider = OpenAIProvider(openai_client=client)

So, for people already using custom headers for LLM, initializing the MCP client using the same configuration looks like something reasonable that will just work with current setups, as the clients are already accepting custom headers.

mcp = MCPServerStreamableHttp(name="Envoy AI Gateway MCP",
        params={"url": mcp_url, "timeout": 300, "headers": CUSTOM_HEADERS},
        cache_tools_list=True,
)

Having the tracing implementation extract these custom attributes from the _meta field would require agent/mcp SDKs to populate that information there, and AFAIK this is done specifically for the tracing information, but not ina. more general fashion today.

Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

@nacx it is ok to go forward with this, my main point was to highlight that it is solving a different scope than why that feature was added (session.id). That said, the issue you are mentioning is at a higher scope "user.region" which this would solve.

To do session.id properly we need to extract attributes from the json-rpc messages, and that can happen later. I don't think you can predict this will never happen because already in order to try new MCP spec additions people put things in meta.

Anyway, since this change is about "X-Tracing-Enrichment-User-Region"-> "user.region" I recommend using exactly those values in your unit tests. Much better than arbitrary and explains the motivation!

@codefromthecrypt
Copy link
Contributor

opened #1348 so we don't have to go around on this one. It is ok to skip if you don't feel it is straightforward to implement and wait for someone to complain. Just it takes several months sometimes for an aigw release. Meanwhile any late request can use the docker images built from main branch anyway!

@nacx
Copy link
Contributor Author

nacx commented Oct 13, 2025

@codefromthecrypt in 1420dbb I'm reading custom attributes from the metadata as well, those taking precedence over the HTTP header ones. WDYT?

@nacx
Copy link
Contributor Author

nacx commented Oct 13, 2025

/retest

if headerValue, ok := headers[headerName]; ok {
attrs = append(attrs, attribute.String(attrName, headerValue))
for srcName, targetName := range m.attributeMappings {
// Check if the attribute is present in the metadata first, as this is the common place to add custom attributes
Copy link
Contributor

Choose a reason for hiding this comment

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

great idea!

@codefromthecrypt
Copy link
Contributor

fyi I am working on goose to do propagation at the moment and will probably try to get it to propagate its session id when done

@mathetake
Copy link
Member

is this ready or on hold?

@nacx
Copy link
Contributor Author

nacx commented Oct 14, 2025

This is ready! :)
With the latest changes, it also tackles #1348

Comment on lines 1223 to 1227
headerMap := make(map[string]string, len(headers))
for header := range headers {
headerMap[header] = headers.Get(header)
}

Copy link
Member

Choose a reason for hiding this comment

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

non blocking nit comment (as i think the impact is negligible): maybe skip this header copying when the header-to-attribute thingy is not configured at the startup time

// Check if the attribute is present in the metadata first, as this is the common place to add custom attributes
// in MCP requests. Fall back to headers if not found in metadata.
// If the attribute is not found there, check if there is any custom header to map.
if metaValue, ok := param.GetMeta()[srcName]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to make sure that we do downcase based comparison as unlike http these are case sensitive, but we should clobber anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

or any other normal way to do case insensitive map (probably need to scan and decide to pick first or last even though maps aren't ordered in go..)

Copy link
Member

Choose a reason for hiding this comment

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

@nacx could you address this adrian's comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@mathetake mathetake enabled auto-merge (squash) October 14, 2025 16:16
@codefromthecrypt
Copy link
Contributor

fyi for testing block/goose#5165

@mathetake mathetake disabled auto-merge October 14, 2025 17:35
nacx and others added 6 commits October 14, 2025 23:09
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
@nacx
Copy link
Contributor Author

nacx commented Oct 14, 2025

/retest

@mathetake mathetake merged commit 220b90c into envoyproxy:main Oct 14, 2025
49 of 51 checks passed
nutanix-Hrushikesh pushed a commit to nutanix-Hrushikesh/ai-gateway that referenced this pull request Oct 16, 2025
…y#1342)

**Description**

The LLM metrics and trace providers allow for the configuration of
custom headers to be included as metric attributes and span attributes.
The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take
this configuration into account.
* The tracer method to create a span now receives the request headers.
* To avoid having to propagate request headers all the way down inside
the MCP Proxy so that they can be taken into account when recording
metrics, I've opted to instantiate an MCPProxy on each request, and have
it set up with the request attributes from the beginning. This allows
reusing the metrics implementation in a clean way.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1335
Fixes envoyproxy#1348

**Special notes for reviewers (if applicable)**

N/A

---------

Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
nutanix-Hrushikesh pushed a commit to nutanix-Hrushikesh/ai-gateway that referenced this pull request Oct 16, 2025
…y#1342)

**Description**

The LLM metrics and trace providers allow for the configuration of
custom headers to be included as metric attributes and span attributes.
The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take
this configuration into account.
* The tracer method to create a span now receives the request headers.
* To avoid having to propagate request headers all the way down inside
the MCP Proxy so that they can be taken into account when recording
metrics, I've opted to instantiate an MCPProxy on each request, and have
it set up with the request attributes from the beginning. This allows
reusing the metrics implementation in a clean way.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1335
Fixes envoyproxy#1348

**Special notes for reviewers (if applicable)**

N/A

---------

Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Hrushikesh Patil <hrushikesh.patil@nutanix.com>
nutanix-Hrushikesh pushed a commit to nutanix-Hrushikesh/ai-gateway that referenced this pull request Oct 16, 2025
…y#1342)

**Description**

The LLM metrics and trace providers allow for the configuration of
custom headers to be included as metric attributes and span attributes.
The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take
this configuration into account.
* The tracer method to create a span now receives the request headers.
* To avoid having to propagate request headers all the way down inside
the MCP Proxy so that they can be taken into account when recording
metrics, I've opted to instantiate an MCPProxy on each request, and have
it set up with the request attributes from the beginning. This allows
reusing the metrics implementation in a clean way.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1335
Fixes envoyproxy#1348

**Special notes for reviewers (if applicable)**

N/A

---------

Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Hrushikesh Patil <hrushikesh.patil@nutanix.com>
nutanix-Hrushikesh pushed a commit to nutanix-Hrushikesh/ai-gateway that referenced this pull request Oct 16, 2025
…y#1342)

**Description**

The LLM metrics and trace providers allow for the configuration of
custom headers to be included as metric attributes and span attributes.
The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take
this configuration into account.
* The tracer method to create a span now receives the request headers.
* To avoid having to propagate request headers all the way down inside
the MCP Proxy so that they can be taken into account when recording
metrics, I've opted to instantiate an MCPProxy on each request, and have
it set up with the request attributes from the beginning. This allows
reusing the metrics implementation in a clean way.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1335
Fixes envoyproxy#1348

**Special notes for reviewers (if applicable)**

N/A

---------

Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Hrushikesh Patil <hrushikesh.patil@nutanix.com>
AyushSawant18588 pushed a commit to AyushSawant18588/ai-gateway that referenced this pull request Oct 24, 2025
…y#1342)

**Description**

The LLM metrics and trace providers allow for the configuration of
custom headers to be included as metric attributes and span attributes.
The MCP implementation, however, did not honor those values.

This PR updates the MCP metrics and tracer configurations to also take
this configuration into account.
* The tracer method to create a span now receives the request headers.
* To avoid having to propagate request headers all the way down inside
the MCP Proxy so that they can be taken into account when recording
metrics, I've opted to instantiate an MCPProxy on each request, and have
it set up with the request attributes from the beginning. This allows
reusing the metrics implementation in a clean way.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1335
Fixes envoyproxy#1348

**Special notes for reviewers (if applicable)**

N/A

---------

Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
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.

MCP request.params._meta support for tracing: Headers to tracing (span) attributes

4 participants