Skip to content

Commit 6461f04

Browse files
Instrumentation for Elasticsearch 8+ (#8799)
1 parent 7144f5a commit 6461f04

File tree

24 files changed

+2050
-299
lines changed

24 files changed

+2050
-299
lines changed

docs/supported-libraries.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ These are the supported libraries and frameworks:
5353
| [Eclipse Jetty HTTP Client](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/client/HttpClient.html) | 9.2+ (not including 10+ yet) | [opentelemetry-jetty-httpclient-9.2](../instrumentation/jetty-httpclient/jetty-httpclient-9.2/library) | [HTTP Client Spans], [HTTP Client Metrics] |
5454
| [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
5555
| [Eclipse Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
56-
| [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] |
56+
| [Elasticsearch API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) | 7.16+ and 8.0+ | N/A | [Elasticsearch Client Spans] |
5757
| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans] |
58+
| [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] |
5859
| [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] |
5960
| [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans] |
6061
| [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] |
@@ -141,6 +142,7 @@ These are the supported libraries and frameworks:
141142

142143
**[3]** Controller Spans are `INTERNAL` spans capturing the controller and/or view execution. See [Suppressing controller and/or view spans](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#suppressing-controller-andor-view-spans).
143144

145+
[Elasticsearch Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/specification/database/elasticsearch.md
144146
[HTTP Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions
145147
[HTTP Client Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client
146148
[HTTP Server Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Settings for the elasticsearch instrumentation
22

3+
## Settings for the [Elasticsearch Java API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) instrumentation
4+
| System property | Type | Default | Description |
5+
|---|---|---|----------------------------------------------------------------------------------------------------------------------------|
6+
| `otel.instrumentation.elasticsearch.capture-search-query` | `Boolean | `false` | Enable the capture of search query bodies. Attention: Elasticsearch queries may contain personal or sensitive information. |
7+
8+
9+
## Settings for the [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) instrumentation
310
| System property | Type | Default | Description |
411
|---|---|---|---|
512
| `otel.instrumentation.elasticsearch.experimental-span-attributes` | `Boolean | `false` | Enable the capture of experimental span attributes. |
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
}
4+
5+
dependencies {
6+
testImplementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent"))
7+
testImplementation(project(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent"))
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
import io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient.ElasticsearchEndpointMap;
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
13+
import java.util.HashMap;
14+
import java.util.HashSet;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.Set;
18+
import java.util.regex.Pattern;
19+
import java.util.stream.Collectors;
20+
import org.junit.jupiter.api.Test;
21+
22+
public class ElasticsearchEndpointMapTest {
23+
24+
private static final Set<String> SEARCH_ENDPOINTS =
25+
new HashSet<>(
26+
Arrays.asList(
27+
"search",
28+
"async_search.submit",
29+
"msearch",
30+
"eql.search",
31+
"terms_enum",
32+
"search_template",
33+
"msearch_template",
34+
"render_search_template"));
35+
36+
private static List<String> getPathParts(String route) {
37+
List<String> pathParts = new ArrayList<>();
38+
String routeFragment = route;
39+
int paramStartIndex = routeFragment.indexOf('{');
40+
while (paramStartIndex >= 0) {
41+
int paramEndIndex = routeFragment.indexOf('}');
42+
if (paramEndIndex < 0 || paramEndIndex <= paramStartIndex + 1) {
43+
throw new IllegalStateException("Invalid route syntax!");
44+
}
45+
pathParts.add(routeFragment.substring(paramStartIndex + 1, paramEndIndex));
46+
47+
int nextIdx = paramEndIndex + 1;
48+
if (nextIdx >= routeFragment.length()) {
49+
break;
50+
}
51+
52+
routeFragment = routeFragment.substring(nextIdx);
53+
paramStartIndex = routeFragment.indexOf('{');
54+
}
55+
return pathParts;
56+
}
57+
58+
@Test
59+
public void testIsSearchEndpoint() {
60+
for (ElasticsearchEndpointDefinition esEndpointDefinition :
61+
ElasticsearchEndpointMap.getAllEndpoints()) {
62+
String endpointId = esEndpointDefinition.getEndpointName();
63+
assertEquals(SEARCH_ENDPOINTS.contains(endpointId), esEndpointDefinition.isSearchEndpoint());
64+
}
65+
}
66+
67+
@Test
68+
public void testProcessPathParts() {
69+
for (ElasticsearchEndpointDefinition esEndpointDefinition :
70+
ElasticsearchEndpointMap.getAllEndpoints()) {
71+
for (String route :
72+
esEndpointDefinition.getRoutes().stream()
73+
.map(ElasticsearchEndpointDefinition.Route::getName)
74+
.collect(Collectors.toList())) {
75+
List<String> pathParts = getPathParts(route);
76+
String resolvedRoute = route.replace("{", "").replace("}", "");
77+
Map<String, String> observedParams = new HashMap<>();
78+
esEndpointDefinition.processPathParts(resolvedRoute, (k, v) -> observedParams.put(k, v));
79+
80+
Map<String, String> expectedMap = new HashMap<>();
81+
pathParts.forEach(part -> expectedMap.put(part, part));
82+
83+
assertEquals(expectedMap, observedParams);
84+
}
85+
}
86+
}
87+
88+
@Test
89+
public void testSearchEndpoint() {
90+
ElasticsearchEndpointDefinition esEndpoint = ElasticsearchEndpointMap.get("search");
91+
Map<String, String> observedParams = new HashMap<>();
92+
esEndpoint.processPathParts(
93+
"/test-index-1,test-index-2/_search", (k, v) -> observedParams.put(k, v));
94+
95+
assertEquals("test-index-1,test-index-2", observedParams.get("index"));
96+
}
97+
98+
@Test
99+
public void testBuildRegexPattern() {
100+
Pattern pattern =
101+
ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern(
102+
"/_nodes/{node_id}/shutdown");
103+
assertEquals("^/_nodes/(?<node0id>[^/]+)/shutdown$", pattern.pattern());
104+
105+
pattern =
106+
ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern(
107+
"/_snapshot/{repository}/{snapshot}/_mount");
108+
assertEquals("^/_snapshot/(?<repository>[^/]+)/(?<snapshot>[^/]+)/_mount$", pattern.pattern());
109+
110+
pattern =
111+
ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern(
112+
"/_security/profile/_suggest");
113+
assertEquals("^/_security/profile/_suggest$", pattern.pattern());
114+
115+
pattern =
116+
ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern(
117+
"/_application/search_application/{name}");
118+
assertEquals("^/_application/search_application/(?<name>[^/]+)$", pattern.pattern());
119+
120+
pattern = ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern("/");
121+
assertEquals("^/$", pattern.pattern());
122+
}
123+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
}
4+
5+
muzzle {
6+
pass {
7+
group.set("co.elastic.clients")
8+
module.set("elasticsearch-java")
9+
versions.set("[7.16,)")
10+
assertInverse.set(true)
11+
}
12+
}
13+
14+
dependencies {
15+
library("co.elastic.clients:elasticsearch-java:7.16.0")
16+
17+
implementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent"))
18+
19+
testInstrumentation(project(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent"))
20+
testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent"))
21+
testInstrumentation(project(":instrumentation:apache-httpasyncclient-4.1:javaagent"))
22+
23+
testImplementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
24+
testImplementation("org.testcontainers:elasticsearch")
25+
}
26+
27+
tasks {
28+
test {
29+
usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
9+
import static net.bytebuddy.matcher.ElementMatchers.named;
10+
import static net.bytebuddy.matcher.ElementMatchers.returns;
11+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
12+
13+
import co.elastic.clients.transport.Endpoint;
14+
import io.opentelemetry.instrumentation.api.util.VirtualField;
15+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
16+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
17+
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchEndpointDefinition;
18+
import net.bytebuddy.asm.Advice;
19+
import net.bytebuddy.description.type.TypeDescription;
20+
import net.bytebuddy.matcher.ElementMatcher;
21+
import org.elasticsearch.client.Request;
22+
23+
public class ApiClientInstrumentation implements TypeInstrumentation {
24+
25+
@Override
26+
public ElementMatcher<TypeDescription> typeMatcher() {
27+
return named("co.elastic.clients.transport.rest_client.RestClientTransport");
28+
}
29+
30+
@Override
31+
public void transform(TypeTransformer transformer) {
32+
transformer.applyAdviceToMethod(
33+
isMethod()
34+
.and(named("prepareLowLevelRequest"))
35+
.and(takesArgument(1, named("co.elastic.clients.transport.Endpoint")))
36+
.and(returns(named("org.elasticsearch.client.Request"))),
37+
this.getClass().getName() + "$RestClientTransportAdvice");
38+
}
39+
40+
@SuppressWarnings("unused")
41+
public static class RestClientTransportAdvice {
42+
43+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
44+
public static void onPrepareLowLevelRequest(
45+
@Advice.Argument(1) Endpoint<?, ?, ?> endpoint, @Advice.Return Request request) {
46+
VirtualField<Request, ElasticsearchEndpointDefinition> virtualField =
47+
VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class);
48+
String endpointId = endpoint.id();
49+
if (endpointId.startsWith("es/") && endpointId.length() > 3) {
50+
endpointId = endpointId.substring(3);
51+
}
52+
virtualField.set(request, ElasticsearchEndpointMap.get(endpointId));
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient;
7+
8+
import static java.util.Collections.singletonList;
9+
10+
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
12+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
13+
import java.util.List;
14+
15+
@AutoService(InstrumentationModule.class)
16+
public class ElasticsearchApiClientInstrumentationModule extends InstrumentationModule {
17+
public ElasticsearchApiClientInstrumentationModule() {
18+
super("elasticsearch-api-client-7.16", "elasticsearch");
19+
}
20+
21+
@Override
22+
public List<TypeInstrumentation> typeInstrumentations() {
23+
return singletonList(new ApiClientInstrumentation());
24+
}
25+
}

0 commit comments

Comments
 (0)