From 846d23faec7ed2b23975b45161730174a2663d98 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Sat, 13 Jun 2026 18:57:04 -0700 Subject: [PATCH 1/3] Fix Elasticsearch REST stable DB operation names --- .../rest/v5_0/ElasticsearchRest5Test.java | 14 ++- .../rest/v6_4/ElasticsearchRest6Test.java | 14 ++- .../rest/v7_0/ElasticsearchRest7Test.java | 14 ++- .../rest/v7_0/ElasticsearchRest7Test.java | 12 ++- .../ElasticsearchDbAttributesGetter.java | 102 ++++++++++++++++++ .../ElasticsearchSpanNameExtractor.java | 8 +- .../ElasticsearchDbAttributesGetterTest.java | 61 +++++++++++ 7 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java diff --git a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java index 92db87193d90..e83218990400 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v5_0/ElasticsearchRest5Test.java @@ -5,17 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric; import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; import static io.opentelemetry.instrumentation.testing.junit.service.SemconvServiceStabilityUtil.maybeStablePeerService; import static io.opentelemetry.instrumentation.testing.util.TestLatestDeps.testLatestDeps; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME; import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM_NAME; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ELASTICSEARCH; @@ -99,11 +102,14 @@ void elasticsearchStatus() throws IOException { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), @@ -125,6 +131,7 @@ void elasticsearchStatus() throws IOException { testing, "io.opentelemetry.elasticsearch-rest-5.0", DB_SYSTEM_NAME, + DB_OPERATION_NAME, SERVER_ADDRESS, SERVER_PORT); } @@ -174,11 +181,14 @@ public void onFailure(Exception e) { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java index b9e6298c8404..7d121d913ed5 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v6_4/ElasticsearchRest6Test.java @@ -5,16 +5,19 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric; import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; import static io.opentelemetry.instrumentation.testing.junit.service.SemconvServiceStabilityUtil.maybeStablePeerService; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME; import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM_NAME; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ELASTICSEARCH; @@ -93,11 +96,14 @@ void elasticsearchStatus() throws IOException { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), @@ -119,6 +125,7 @@ void elasticsearchStatus() throws IOException { testing, "io.opentelemetry.elasticsearch-rest-6.4", DB_SYSTEM_NAME, + DB_OPERATION_NAME, SERVER_ADDRESS, SERVER_PORT); } @@ -168,11 +175,14 @@ public void onFailure(Exception e) { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java index 145ea48257e9..dcb6da7e5306 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java @@ -5,17 +5,20 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric; import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; import static io.opentelemetry.instrumentation.testing.junit.service.SemconvServiceStabilityUtil.maybeStablePeerService; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME; import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM_NAME; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ELASTICSEARCH; @@ -90,11 +93,14 @@ void elasticsearchStatus() throws IOException { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), @@ -116,6 +122,7 @@ void elasticsearchStatus() throws IOException { testing, "io.opentelemetry.elasticsearch-rest-7.0", DB_SYSTEM_NAME, + DB_OPERATION_NAME, SERVER_ADDRESS, SERVER_PORT); } @@ -166,11 +173,14 @@ public void onFailure(Exception e) { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java index 1676c3191e44..b301c2d9b871 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/v7_0/ElasticsearchRest7Test.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -12,6 +13,7 @@ import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ELASTICSEARCH; import static java.util.concurrent.TimeUnit.SECONDS; @@ -87,11 +89,14 @@ void elasticsearchStatus() throws IOException { trace -> trace.hasSpansSatisfyingExactly( span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), @@ -143,11 +148,14 @@ public void onFailure(Exception e) { trace.hasSpansSatisfyingExactly( span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), span -> - span.hasName("GET") + span.hasName(emitStableDatabaseSemconv() ? "cluster.health" : "GET") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), ELASTICSEARCH), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? "cluster.health" : null), equalTo(HTTP_REQUEST_METHOD, "GET"), equalTo(SERVER_ADDRESS, httpHost.getHostName()), equalTo(SERVER_PORT, httpHost.getPort()), diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java index 3db28a748748..5c3a2ad76284 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java @@ -73,10 +73,112 @@ public String getDbQueryText(ElasticsearchRestRequest request) { @Override @Nullable public String getDbOperationName(ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition endpointDefinition = request.getEndpointDefinition(); + if (endpointDefinition != null) { + return endpointDefinition.getEndpointName(); + } + return inferOperationName(request.getMethod(), request.getEndpoint()); + } + + @Deprecated // to be removed in 3.0 + @Override + @Nullable + public String getDbOperation(ElasticsearchRestRequest request) { ElasticsearchEndpointDefinition endpointDefinition = request.getEndpointDefinition(); return endpointDefinition != null ? endpointDefinition.getEndpointName() : null; } + @Nullable + static String inferOperationName(String method, String endpoint) { + int queryStart = endpoint.indexOf('?'); + if (queryStart >= 0) { + endpoint = endpoint.substring(0, queryStart); + } + + while (endpoint.startsWith("/")) { + endpoint = endpoint.substring(1); + } + if (endpoint.isEmpty()) { + return null; + } + + String[] segments = endpoint.split("/"); + if (segments[0].startsWith("_")) { + return inferOperationNameFromApiSegments(method, segments, 0); + } + if (segments.length > 1 && segments[1].startsWith("_")) { + return inferOperationNameFromApiSegments(method, segments, 1); + } + return null; + } + + @Nullable + private static String inferOperationNameFromApiSegments( + String method, String[] segments, int apiSegmentIndex) { + String apiSegment = stripLeadingUnderscores(segments[apiSegmentIndex]); + if (apiSegment.isEmpty()) { + return null; + } + String documentOperation = inferDocumentOperationName(method, apiSegment); + if (documentOperation != null) { + return documentOperation; + } + if (isGroupedApi(apiSegment) + && segments.length > apiSegmentIndex + 1 + && !segments[apiSegmentIndex + 1].startsWith("_")) { + return apiSegment + "." + segments[apiSegmentIndex + 1]; + } + return apiSegment; + } + + @Nullable + private static String inferDocumentOperationName(String method, String apiSegment) { + switch (apiSegment) { + case "create": + case "update": + return apiSegment; + case "doc": + return inferDocOperationName(method); + default: + return null; + } + } + + @Nullable + private static String inferDocOperationName(String method) { + switch (method) { + case "DELETE": + return "delete"; + case "GET": + return "get"; + case "POST": + case "PUT": + return "index"; + default: + return null; + } + } + + private static boolean isGroupedApi(String apiSegment) { + switch (apiSegment) { + case "cat": + case "cluster": + case "nodes": + case "snapshot": + case "tasks": + return true; + default: + return false; + } + } + + private static String stripLeadingUnderscores(String value) { + while (value.startsWith("_")) { + value = value.substring(1); + } + return value; + } + @Override @Nullable public String getErrorType( diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchSpanNameExtractor.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchSpanNameExtractor.java index 8184a3ea400d..59274986053c 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchSpanNameExtractor.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchSpanNameExtractor.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.elasticsearch.rest.common.v5_0.internal; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; + import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; /** @@ -19,9 +21,13 @@ final class ElasticsearchSpanNameExtractor implements SpanNameExtractor Date: Mon, 15 Jun 2026 09:57:51 -0700 Subject: [PATCH 2/3] Avoid array allocation when inferring operation name; add tests --- .../ElasticsearchDbAttributesGetter.java | 60 +++++++++++++------ .../ElasticsearchDbAttributesGetterTest.java | 35 +++++++++++ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java index 5c3a2ad76284..5cb241e75879 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java @@ -90,32 +90,58 @@ public String getDbOperation(ElasticsearchRestRequest request) { @Nullable static String inferOperationName(String method, String endpoint) { - int queryStart = endpoint.indexOf('?'); - if (queryStart >= 0) { - endpoint = endpoint.substring(0, queryStart); + int end = endpoint.indexOf('?'); + if (end < 0) { + end = endpoint.length(); } - while (endpoint.startsWith("/")) { - endpoint = endpoint.substring(1); + int start = 0; + if (start < end && endpoint.charAt(start) == '/') { + // low-level REST client endpoints conventionally start with a single leading slash + start++; } - if (endpoint.isEmpty()) { - return null; + + // Only the first three path segments are needed to infer the operation name, so extract them + // in a single pass instead of allocating an array for the whole path on this hot path. + String segment0 = null; + String segment1 = null; + String segment2 = null; + int segmentStart = start; + int found = 0; + for (int i = start; i <= end && found < 3; i++) { + if (i == end || endpoint.charAt(i) == '/') { + String segment = endpoint.substring(segmentStart, i); + switch (found++) { + case 0: + segment0 = segment; + break; + case 1: + segment1 = segment; + break; + default: + segment2 = segment; + break; + } + segmentStart = i + 1; + } } - String[] segments = endpoint.split("/"); - if (segments[0].startsWith("_")) { - return inferOperationNameFromApiSegments(method, segments, 0); + if (segment0 == null || segment0.isEmpty()) { + return null; + } + if (segment0.startsWith("_")) { + return inferOperationNameFromApiSegments(method, segment0, segment1); } - if (segments.length > 1 && segments[1].startsWith("_")) { - return inferOperationNameFromApiSegments(method, segments, 1); + if (segment1 != null && segment1.startsWith("_")) { + return inferOperationNameFromApiSegments(method, segment1, segment2); } return null; } @Nullable private static String inferOperationNameFromApiSegments( - String method, String[] segments, int apiSegmentIndex) { - String apiSegment = stripLeadingUnderscores(segments[apiSegmentIndex]); + String method, String apiSegmentRaw, @Nullable String nextSegment) { + String apiSegment = stripLeadingUnderscores(apiSegmentRaw); if (apiSegment.isEmpty()) { return null; } @@ -123,10 +149,8 @@ private static String inferOperationNameFromApiSegments( if (documentOperation != null) { return documentOperation; } - if (isGroupedApi(apiSegment) - && segments.length > apiSegmentIndex + 1 - && !segments[apiSegmentIndex + 1].startsWith("_")) { - return apiSegment + "." + segments[apiSegmentIndex + 1]; + if (isGroupedApi(apiSegment) && nextSegment != null && !nextSegment.startsWith("_")) { + return apiSegment + "." + nextSegment; } return apiSegment; } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java index e69a64646078..c0f9d4a7de23 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java @@ -58,4 +58,39 @@ void shouldKeepOldLowLevelOperationUnavailable() { assertThat(underTest.getDbOperation(ElasticsearchRestRequest.create("GET", "_cluster/health"))) .isNull(); } + + @Test + void shouldStripLeadingSlashAndQueryString() { + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "/_cat/indices")) + .isEqualTo("cat.indices"); + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "_search?size=10")) + .isEqualTo("search"); + assertThat( + ElasticsearchDbAttributesGetter.inferOperationName( + "GET", "/test-index/_search?from=0&size=10")) + .isEqualTo("search"); + } + + @Test + void shouldInferGroupedApiNameWithAndWithoutSubcommand() { + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "_nodes/stats")) + .isEqualTo("nodes.stats"); + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "_nodes")) + .isEqualTo("nodes"); + // a following underscore segment is not treated as a subcommand + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("POST", "_tasks/_cancel")) + .isEqualTo("tasks"); + // a non-grouped api keeps just the api segment + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("POST", "_search/scroll")) + .isEqualTo("search"); + } + + @Test + void shouldReturnNullWhenNoApiSegmentPresent() { + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "")).isNull(); + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "/")).isNull(); + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "test-index")).isNull(); + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "test-index/doc-id")) + .isNull(); + } } From 26692efad355be094c45b8dd8083c51ee0f59653 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 15 Jun 2026 10:08:39 -0700 Subject: [PATCH 3/3] Avoid trailing dot for grouped API with trailing slash --- .../v5_0/internal/ElasticsearchDbAttributesGetter.java | 5 ++++- .../v5_0/internal/ElasticsearchDbAttributesGetterTest.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java index 5cb241e75879..e36a83daa2d0 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetter.java @@ -149,7 +149,10 @@ private static String inferOperationNameFromApiSegments( if (documentOperation != null) { return documentOperation; } - if (isGroupedApi(apiSegment) && nextSegment != null && !nextSegment.startsWith("_")) { + if (isGroupedApi(apiSegment) + && nextSegment != null + && !nextSegment.isEmpty() + && !nextSegment.startsWith("_")) { return apiSegment + "." + nextSegment; } return apiSegment; diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java index c0f9d4a7de23..9c030d37ee31 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common-5.0/library/src/test/java/io/opentelemetry/instrumentation/elasticsearch/rest/common/v5_0/internal/ElasticsearchDbAttributesGetterTest.java @@ -83,6 +83,9 @@ void shouldInferGroupedApiNameWithAndWithoutSubcommand() { // a non-grouped api keeps just the api segment assertThat(ElasticsearchDbAttributesGetter.inferOperationName("POST", "_search/scroll")) .isEqualTo("search"); + // a trailing slash does not produce a trailing dot + assertThat(ElasticsearchDbAttributesGetter.inferOperationName("GET", "_nodes/")) + .isEqualTo("nodes"); } @Test