Repository navigation
Add opt-in active discovery of cluster name to OpenTelemetry For Elasticsearch instrumentation. #1304
Description
Description
Cross-posted from open-telemetry/opentelemetry-java-instrumentation#19376.
Problem
When monitoring a self-managed Elasticsearch cluster with OpenTelemetry, the spans this client produces and the metrics the Collector's elasticsearchreceiver produces cannot be correlated, because they identify the cluster differently:
elasticsearchreceivertags metrics withelasticsearch.cluster.name, from the cluster's monitoring API.- This client's spans (via
OpenTelemetryForElasticsearch) never carry a cluster name at all.
I looked through OpenTelemetryForElasticsearch.java to confirm what's actually captured today. Per request, it sets:
db.system.name="elasticsearch"(static)db.operation.name(the endpoint id)http.request.method,http.response.status_codeurl.full,server.address,server.port(fromhttpResponse.node().uri())db.elasticsearch.path_parts.*(per path parameter, viaAttributeKeyTemplate)
None of these identify the cluster. server.address/server.port reflect whichever node (or load balancer / proxy) the low-level transport happened to hit — in most production topologies this is not the cluster's own identity, and Elasticsearch's data-plane responses (_search, _bulk, etc.) never return cluster_name for the client to pick up passively.
solution
An opt-in, off-by-default active discovery step in OpenTelemetryForElasticsearch, following the same pattern already used for otel.instrumentation.elasticsearch.capture-search-query:
- New config option, e.g.
otel.instrumentation.elasticsearch.capture-cluster-name(system property /OTEL_INSTRUMENTATION_ELASTICSEARCH_CAPTURE_CLUSTER_NAMEenv var), defaultfalse. - When enabled, on first use of a given transport/node pool, issue a single
GET /and cache thecluster_namefield from the response, scoped to that client instance. - Stamp an attribute carrying that value onto every subsequent span from that client — I've suggested
db.elasticsearch.cluster.namebelow (following the existingdb.elasticsearch.path_partsnaming convention), but I'm flexible on the exact attribute name/namespace. I know the DB semantic conventions have been under active revision recently (this file itself already tracks that, e.g.DB_SYSTEM_NAMErather than the olderDB_SYSTEM), so happy to align to whatever's current/settled rather than debate naming here. - On failure (403, timeout, older ES version quirks) fail silently and fall back to today's behavior — never block or fail the actual request.
Because the plumbing for Elasticsearch-specific attributes already exists (AttributeKeyTemplate, PATH_PART_PREFIX), this would be a small, idiomatic addition rather than a new mechanism.
alternatives considered
- Passively picking up cluster name from a normal response. Doesn't work — data-plane endpoints (
_search,_bulk) never returncluster_name. - Correlating on
server.addressin the Collector. Breaks under any load balancer, proxy, or container network — which is most production self-managed deployments. This is exactly whatserver.addressreflects today, and it's not a reliable join key. - Manual tagging on both APM and Collector config. Requires two separate teams to coordinate and hand-maintain matching strings — defeats the point of automatic correlation.
Happy to help test/validate against a self-managed cluster if useful.
Activity
l-trotta commentedon Aug 5, 2026
Hey @vagrawal-newrelic! Instead of an additional opt in metric, how about an extension mechanism that allows custom metrics to be added? This is what the .net client does already, I'm figuring it would work better long term. What do you think?
vagrawal-newrelic commentedon Aug 6, 2026
Thanks @l-trotta , agreed on going the extension route rather than per-feature flags. A provider interface means new attributes can come from new providers instead of a new flag each time, which is the reusable direction I'd rather build.
The way I'm picturing it: the instrumentation exposes a small provider interface for contributing attributes to a span, and Elastic ships one default provider that fills in the cluster info. Anyone who needs a custom attribute writes their own provider and registers it the same way, without new flags or core changes. Any options (like a refresh interval) would just be settings on the provider when you register it, rather than global config.
Before I go further, a few design points I'd like us to agree on for that default cluster-info provider:
Where the cluster name comes from. The .NET client reads it from the X-Found-Handling-Cluster header, which is added by the Elastic Cloud proxy. Self-managed clusters don't send it today, and normal responses (_search, _bulk, …) don't carry cluster_name. So I'd have the provider check the header first and only fall back to an active GET / when it's absent. That keeps it future-proof: if the header ever becomes available for self-managed too, the provider picks it up automatically with no change on our side, and the active fetch just stops being needed.
How the active fetch is made. That GET / should go through the client's own transport rather than a separate HTTP client, so it inherits the configured auth, TLS/mTLS, proxy and node selection and doesn't break on secured clusters. Two things I'd want your take on: is it OK for a provider to access the transport for this, and is there a preferred way to keep that internal request from being instrumented / looping back on itself?
Caching and refresh. The provider would fetch once and cache per client, then refresh on an interval (I'd suggest a 30-minute default, configurable) since the name can change when a node restarts. Any failure (403, timeout, older versions) would fall back silently and never affect the real request. Do those defaults sound reasonable?
What it captures. db.elasticsearch.cluster.name matches what .NET already defines, so that's the natural primary attribute. Since the GET / returns cluster_uuid in the same response, the provider could expose that too at no extra cost, giving a stable unique identifier for anyone who wants one later. I'd suggest the default captures the name, with uuid as an easy opt-in.
If this lines up with what you had in mind, I'll draft it and open a PR for review.
l-trotta commentedon Aug 6, 2026
@vagrawal-newrelic I took a better look at the current situation for the java client, and realized that an extension mechanism is already in place, and you can add cluster name (or any custom metric) by adding a custom SpanProcessor:
you can use this workaround while I decide if this is actually worth implementing and how :) I'll keep this open in the meantime!
vagrawal-newrelic commentedon Aug 7, 2026
Thanks @l-trotta , that's a helpful pointer — the custom
SpanProcessoris a clean way to attach the attribute without touching the client.The limitation is that it needs the cluster name supplied up front (hardcoded), with each user wiring up their own processor and discovery. For anything meant to work generically / out of the box, that per-setup custom code isn't really viable — which is where having the client resolve the name itself would make the difference: it would work for any user without hand-rolling anything.
The core gap is dynamically discovering the name, particularly for self-managed, where it isn't available passively: data-plane responses (
_search,_bulk) don't returncluster_name, and theX-Found-Handling-Clusterheader is Elastic Cloud only.A native default could:
X-Found-Handling-Clusterheader first (so if it's ever added for self-managed too, it keeps working with no change).GET /through the client's own transport (reusing the configured auth/TLS) to resolve the name.db.elasticsearch.cluster.nameautomatically (andcluster_uuidfrom the same response as a bonus).Happy to open a PR along these lines if you think it's worth adding. No rush, and thanks for keeping it open while you evaluate!
Capture the Elasticsearch cluster name on spans via a pluggable provider
vagrawal-newrelic commentedon Aug 17, 2026
Hi @l-trotta — to make the goal and approach concrete, I've opened a draft PR (#1319) as a starting point for discussion. I'd really value your feedback on the direction before going further.
Happy to adjust the design, attribute names, or approach based on your guidance — thanks for taking a look!
l-trotta commentedon Aug 18, 2026
hello @vagrawal-newrelic, thanks for your proposal! we've been discussing with both my team and the APM team, we're trying to figure out a more generic approach that would work for all clients. when we finalize the details we can reiterate on your PR if you'd like to keep working on this.
l-trotta commentedon Sep 3, 2026
Hey @vagrawal-newrelic! I have good news: we decided to take a more general approach, so it will be the server itself that will provide the on-prem cluster name as a header, just like it already does with Cloud. The server side PR has already been merged, so now it's just a matter for the java client to handle both headers. Would you be willing to modify your PR to fit the new requirements?
Here's a short summary of the work that should be done:
Update the client's OpenTelemetry instrumentation to read the
X-Found-Handling-Clusterand the newElastic-Cluster-Nameresponse headers and record the cluster name (db.elasticsearch.cluster.name). Precedence when both are present: preferX-Found-Handling-Cluster, since its presence indicates an Elastic Cloud (proxied) deployment and it carries the canonical, globally-unique cluster resource id. Fall back toElastic-Cluster-Namewhen only it is present (the self-managed / on-prem case).vagrawal-newrelic commentedon Sep 4, 2026
Thanks a lot @l-trotta for the quick turnaround and for laying out the approach and naming so clearly — really appreciate it! 🙏
That all makes sense to me. I'll go ahead and make the changes: read both headers, prefer X-Found-Handling-Cluster and fall back to Elastic-Cluster-Name, record it as db.elasticsearch.cluster.name, and stay quiet when neither is present. I'll get the PR up and validate it against a self-managed cluster.
vagrawal-newrelic commentedon Sep 9, 2026
@l-trotta the client-side change is up and ready for review: #1319
It follows the approach you outlined — it reads the cluster name from the response headers and stamps db.elasticsearch.cluster.name, preferring X-Found-Handling-Cluster (Elastic Cloud) and falling back to Elastic-Cluster-Name (self-managed, 9.6+). The value is read per response, so a cluster rename is reflected without restarting the client, and it makes no extra request. It includes unit tests covering all the header combinations and a short note in the OpenTelemetry docs; ./gradlew check passes locally.
Since I'm an external contributor, the CI workflows are waiting on maintainer approval to run — would you mind kicking those off and taking a look when you get a chance? Happy to adjust anything. Thanks!