Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/reference/setup/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ ElasticsearchClient esClient = ElasticsearchClient.of(b -> b
esClient.close();
```

## Capturing the {{es}} cluster name [opentelemetry-cluster-name]

The built-in instrumentation automatically captures the `db.elasticsearch.cluster.name` span attribute whenever {{es}} includes the cluster name in its response headers. No client-side configuration is required.

The source of the cluster name depends on your deployment:

* **Elastic Cloud**: Automatically populates the cluster's canonical, globally unique ID (extracted from the `X-Found-Handling-Cluster` proxy header).
* **Self-managed {{es}} (v9.6+)**: Populates the configured `cluster.name` (extracted from the `Elastic-Cluster-Name` header). To enable this header, set `http.headers.cluster_name.enabled: true` in your cluster settings.


Comment on lines +63 to +72

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.

nit: we should document the difference between the on-prem and cloud value

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.

@l-trotta Thanks for the feedback! updated the docs to spell out the value in each case: the canonical globally-unique cluster id on Elastic Cloud (X-Found-Handling-Cluster) vs the configured cluster.name on self-managed (Elastic-Cluster-Name). Thanks!

## Configuring the OpenTelemetry instrumentation [_configuring_the_opentelemetry_instrumentation]

You can configure the OpenTelemetry instrumentation either through Java System properties or Environment Variables. The following configuration options are available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ public class OpenTelemetryForElasticsearch implements Instrumentation {

private static final Log logger = LogFactory.getLog(OpenTelemetryForElasticsearch.class);

/**
* Cluster-name header added by the Elastic Cloud proxy; carries the cluster's canonical,
* globally-unique id.
*/
private static final String CLOUD_CLUSTER_HEADER = "X-Found-Handling-Cluster";

/**
* Cluster-name header emitted by self-managed Elasticsearch (9.6+) when
* {@code http.headers.cluster_name.enabled} is set; carries the configured {@code cluster.name}.
*/
private static final String ONPREM_CLUSTER_HEADER = "Elastic-Cluster-Name";

private static final AttributeKey<String> DB_ES_CLUSTER_NAME =
AttributeKey.stringKey("db.elasticsearch.cluster.name");

private final Tracer tracer;
private final boolean captureSearchBody;

Expand Down Expand Up @@ -241,6 +256,19 @@ public void afterReceivingHttpResponse(TransportHttpClient.Response httpResponse
span.setAttribute(SERVER_PORT, uri.getPort());
span.setAttribute(SERVER_ADDRESS, uri.getHost());
span.setAttribute(HTTP_RESPONSE_STATUS_CODE, httpResponse.statusCode());

// Record the cluster identity as db.elasticsearch.cluster.name from the response headers —
// address-independent, so it survives load balancers, proxies and node lists. A deployment
// normally sends only one of these headers (Elastic Cloud sends X-Found-Handling-Cluster;
// self-managed sends Elastic-Cluster-Name when enabled). If both are present, the Cloud
// header takes precedence because it carries the canonical, globally-unique cluster id.
String clusterName = httpResponse.header(CLOUD_CLUSTER_HEADER);
if (clusterName == null || clusterName.isEmpty()) {
clusterName = httpResponse.header(ONPREM_CLUSTER_HEADER);
}
if (clusterName != null && !clusterName.isEmpty()) {
span.setAttribute(DB_ES_CLUSTER_NAME, clusterName);
}
}
} catch (RuntimeException e) {
logger.debug("Failed capturing response information for the OpenTelemetry span.", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
public class OpenTelemetryForElasticsearchTest {
private static final String INDEX = "test-index";
private static final String DOC_ID = "1234567";
private static final AttributeKey<String> DB_ES_CLUSTER_NAME =
AttributeKey.stringKey("db.elasticsearch.cluster.name");
private static final String CLOUD_CLUSTER_HEADER = "X-Found-Handling-Cluster";
private static final String ONPREM_CLUSTER_HEADER = "Elastic-Cluster-Name";
private static final String DOC_RESPONSE = "{\n" +
" \"_index\": \"" + INDEX + "\",\n" +
" \"_id\": \"" + DOC_ID + "\",\n" +
Expand Down Expand Up @@ -155,9 +159,36 @@ private static void setupHttpServer() throws IOException {
exchange.close();
});

// handlers for the cluster-name capture scenarios: each index returns a specific
// combination of the cloud and on-prem cluster-name headers.
addDocHandler("cluster-cloud", "cloud-cluster-01", null);
addDocHandler("cluster-onprem", null, "onprem-cluster-01");
addDocHandler("cluster-both", "cloud-cluster-01", "onprem-cluster-01");
addDocHandler("cluster-none", null, null);
addDocHandler("cluster-empty-cloud", "", "onprem-cluster-01");
addDocHandler("cluster-empty-both", "", "");

httpServer.start();
}

// Registers a GET-document handler for the given index that stamps the given cluster-name headers on the
// response. A null value omits the header; an empty string sends the header with an empty value.
private static void addDocHandler(String index, String cloudHeaderValue, String onPremHeaderValue) {
httpServer.createContext("/" + index + "/_doc/" + DOC_ID, exchange -> {
exchange.getResponseHeaders().set("X-Elastic-Product", "Elasticsearch");
exchange.getResponseHeaders().set("Content-Type", "application/json");
if (cloudHeaderValue != null) {
exchange.getResponseHeaders().set(CLOUD_CLUSTER_HEADER, cloudHeaderValue);
}
if (onPremHeaderValue != null) {
exchange.getResponseHeaders().set(ONPREM_CLUSTER_HEADER, onPremHeaderValue);
}
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().write(DOC_RESPONSE.getBytes());
exchange.close();
});
}

private static void setupOTel() {
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(SERVICE_NAME, "es-api-test")));
Expand Down Expand Up @@ -230,6 +261,54 @@ public void testAsyncSearchRequest() throws IOException, InterruptedException, T
Assertions.assertNull(span.getAttributes().get(DbAttributes.DB_QUERY_TEXT));
}

@Test
public void testClusterNameFromCloudHeader() throws IOException {
// Elastic Cloud proxy header present -> captured directly.
client.get(r -> r.index("cluster-cloud").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertEquals("cloud-cluster-01", span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

@Test
public void testClusterNameFromOnPremHeader() throws IOException {
// Self-managed header present (no cloud header) -> captured as fallback.
client.get(r -> r.index("cluster-onprem").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertEquals("onprem-cluster-01", span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

@Test
public void testClusterNamePrefersCloudHeaderWhenBothPresent() throws IOException {
// Both headers present -> the cloud header wins (canonical, globally-unique id).
client.get(r -> r.index("cluster-both").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertEquals("cloud-cluster-01", span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

@Test
public void testClusterNameAbsentWhenNoHeader() throws IOException {
// Neither header present -> the attribute is not stamped.
client.get(r -> r.index("cluster-none").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertNull(span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

@Test
public void testClusterNameFallsBackToOnPremWhenCloudHeaderEmpty() throws IOException {
// Empty cloud header is treated as absent -> fall back to the on-prem header.
client.get(r -> r.index("cluster-empty-cloud").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertEquals("onprem-cluster-01", span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

@Test
public void testClusterNameAbsentWhenHeadersEmpty() throws IOException {
// Both headers present but empty -> the attribute is not stamped.
client.get(r -> r.index("cluster-empty-both").id(DOC_ID), Object.class);
SpanData span = spanExporter.getSpans().get(0);
Assertions.assertNull(span.getAttributes().get(DB_ES_CLUSTER_NAME));
}

private static class MockSpanExporter implements SpanExporter {

private final List<SpanData> spans = new ArrayList();
Expand Down
Loading