From 7d5945560a66d608dc2ecc6c79caec9cc494de6b Mon Sep 17 00:00:00 2001 From: Alexander Wert Date: Wed, 26 Jul 2023 12:37:51 +0200 Subject: [PATCH] Added ES client 8.x instrumentation (#3157) --------- Signed-off-by: Alexander Wert Co-authored-by: Jonas Kunz --- CHANGELOG.asciidoc | 1 + apm-agent-builds/pom.xml | 5 + .../apm/esjavaclient/ElasticsearchJavaIT.java | 76 -- ...sticsearchRestClientInstrumentationIT.java | 61 +- ...asticsearchClientAsyncInstrumentation.java | 2 +- ...lasticsearchClientSyncInstrumentation.java | 2 +- ...bstractEs6_4ClientInstrumentationTest.java | 94 +- ...sticsearchRestClientInstrumentationIT.java | 13 +- ...sticsearchRestClientInstrumentationIT.java | 22 +- .../pom.xml | 8 +- .../RestClientTransportInstrumentation.java | 71 ++ .../agent/esrestclient/v8_x/package-info.java | 22 + ...ic.apm.agent.sdk.ElasticApmInstrumentation | 1 + .../v8_x/Elasticsearch8JavaIT.java} | 231 +++-- .../test/resources/commons-logging.properties | 0 .../src/test/resources/log4j.properties | 0 .../ElasticsearchEndpointDefinition.java | 193 ++++ .../ElasticsearchEndpointMap.java | 899 ++++++++++++++++++ ...searchRestClientInstrumentationHelper.java | 50 +- .../AbstractEsClientInstrumentationTest.java | 245 +++-- .../ElasticsearchEndpointMapTest.java | 150 +++ .../apm-es-restclient-plugin/pom.xml | 2 +- 22 files changed, 1822 insertions(+), 326 deletions(-) delete mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/ElasticsearchJavaIT.java rename apm-agent-plugins/apm-es-restclient-plugin/{apm-es-api-client-test => apm-es-restclient-plugin-8_x}/pom.xml (90%) create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/RestClientTransportInstrumentation.java create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/package-info.java create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation rename apm-agent-plugins/apm-es-restclient-plugin/{apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/AbstractElasticsearchJavaTest.java => apm-es-restclient-plugin-8_x/src/test/java/co/elastic/apm/agent/esrestclient/v8_x/Elasticsearch8JavaIT.java} (75%) rename apm-agent-plugins/apm-es-restclient-plugin/{apm-es-api-client-test => apm-es-restclient-plugin-8_x}/src/test/resources/commons-logging.properties (100%) rename apm-agent-plugins/apm-es-restclient-plugin/{apm-es-api-client-test => apm-es-restclient-plugin-8_x}/src/test/resources/log4j.properties (100%) create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointDefinition.java create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMap.java create mode 100644 apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMapTest.java diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dcc1d67e27..562f19b7e8 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -35,6 +35,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes: ===== Features * Added W3C baggage propagation - {pull}3236[#3236], {pull}3248[#3248] * Added support for baggage in OpenTelemetry bridge - {pull}3249[#3249] +* Improved span naming and attribute collection for 7.16+ elasticsearch clients - {pull}3157[#3157] [[release-notes-1.x]] === Java Agent version 1.x diff --git a/apm-agent-builds/pom.xml b/apm-agent-builds/pom.xml index 77e9422808..faeddeb27f 100644 --- a/apm-agent-builds/pom.xml +++ b/apm-agent-builds/pom.xml @@ -97,6 +97,11 @@ apm-es-restclient-plugin-7_x ${project.version} + + ${project.groupId} + apm-es-restclient-plugin-8_x + ${project.version} + ${project.groupId} apm-grails-plugin diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/ElasticsearchJavaIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/ElasticsearchJavaIT.java deleted file mode 100644 index d64116b0a5..0000000000 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/ElasticsearchJavaIT.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package co.elastic.apm.esjavaclient; - -import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; -import co.elastic.clients.elasticsearch.ElasticsearchClient; -import co.elastic.clients.elasticsearch.indices.CreateIndexRequest; -import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; -import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.rest_client.RestClientTransport; -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.elasticsearch.client.RestClient; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.io.IOException; - -@RunWith(Parameterized.class) -public class ElasticsearchJavaIT extends AbstractElasticsearchJavaTest { - - private static final String ELASTICSEARCH_CONTAINER_VERSION = "docker.elastic.co/elasticsearch/elasticsearch:7.17.2"; - - public ElasticsearchJavaIT(boolean async) { - this.async = async; - } - - @BeforeClass - public static void startElasticsearchContainerAndClient() throws IOException { - startContainer(ELASTICSEARCH_CONTAINER_VERSION); - - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USER_NAME, PASSWORD)); - - RestClient restClient = RestClient.builder(HttpHost.create(container.getHttpHostAddress())) - .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) - .build(); - - ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); - client = new ElasticsearchClient(transport); - asyncClient = new ElasticsearchAsyncClient(transport); - - client.indices().create(new CreateIndexRequest.Builder().index(INDEX).build()); - reporter.reset(); - } - - @AfterClass - public static void stopElasticsearchContainerAndClient() throws IOException { - if (client != null) { - // prevent misleading NPE when failed to start container - client.indices().delete(new DeleteIndexRequest.Builder().index(INDEX).build()); - } - } -} diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/esrestclient/v5_6/ElasticsearchRestClientInstrumentationIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/esrestclient/v5_6/ElasticsearchRestClientInstrumentationIT.java index ae7ccfccad..76916bf9c6 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/esrestclient/v5_6/ElasticsearchRestClientInstrumentationIT.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/esrestclient/v5_6/ElasticsearchRestClientInstrumentationIT.java @@ -19,8 +19,8 @@ package co.elastic.apm.agent.esrestclient.v5_6; import co.elastic.apm.agent.esrestclient.AbstractEsClientInstrumentationTest; -import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.tracer.Outcome; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -65,14 +65,13 @@ @RunWith(Parameterized.class) public class ElasticsearchRestClientInstrumentationIT extends AbstractEsClientInstrumentationTest { - private static final String ELASTICSEARCH_CONTAINER_VERSION = "docker.elastic.co/elasticsearch/elasticsearch:5.6.0"; protected static final String USER_NAME = "elastic"; protected static final String PASSWORD = "changeme"; - protected static final String DOC_TYPE = "doc"; - private static RestHighLevelClient client; + private static final String ELASTICSEARCH_CONTAINER_VERSION = "docker.elastic.co/elasticsearch/elasticsearch:5.6.0"; @SuppressWarnings("NullableProblems") protected static RestClient lowLevelClient; + private static RestHighLevelClient client; public ElasticsearchRestClientInstrumentationIT(boolean async) { this.async = async; @@ -86,7 +85,7 @@ public static void startElasticsearchContainerAndClient() throws IOException { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USER_NAME, PASSWORD)); - RestClientBuilder builder = RestClient.builder(HttpHost.create(container.getHttpHostAddress())) + RestClientBuilder builder = RestClient.builder(HttpHost.create(container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); lowLevelClient = builder.build(); client = new RestHighLevelClient(lowLevelClient); @@ -124,14 +123,18 @@ public void testCreateAndDeleteIndex() throws IOException, ExecutionException { // Create an Index doPerformRequest("PUT", "/" + SECOND_INDEX); - validateSpanContentAfterIndexCreateRequest(); + validateSpan() + .method("PUT").pathName("/%s", SECOND_INDEX) + .check(); // Delete the index reporter.reset(); doPerformRequest("DELETE", "/" + SECOND_INDEX); - validateSpanContentAfterIndexDeleteRequest(); + validateSpan() + .method("DELETE").pathName("/%s", SECOND_INDEX) + .check(); assertThat(reporter.getFirstSpan().getOutcome()).isEqualTo(Outcome.SUCCESS); } @@ -147,10 +150,10 @@ public void testDocumentScenario() throws IOException, ExecutionException, Inter ).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)); assertThat(ir.status().getStatus()).isEqualTo(201); - - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - validateSpanContent(spans.get(0), String.format("Elasticsearch: PUT /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 201, "PUT"); + validateSpan() + .method("PUT").pathName("/%s/%s/%s", INDEX, DOC_TYPE, DOC_ID) + .statusCode(201) + .check(); // Search the index reporter.reset(); @@ -165,12 +168,10 @@ public void testDocumentScenario() throws IOException, ExecutionException, Inter assertThat(sr.getHits().totalHits).isEqualTo(1L); assertThat(sr.getHits().getAt(0).getSourceAsMap().get(FOO)).isEqualTo(BAR); - - spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span searchSpan = spans.get(0); - validateSpanContent(searchSpan, String.format("Elasticsearch: GET /%s/_search", INDEX), 200, "GET"); - validateDbContextContent(searchSpan, "{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}"); + validateSpan() + .method("GET").pathName("/%s/_search", INDEX) + .expectStatement("{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}") + .check(); // Now update and re-search reporter.reset(); @@ -184,11 +185,11 @@ public void testDocumentScenario() throws IOException, ExecutionException, Inter assertThat(sr.getHits().getAt(0).getSourceAsMap().get(FOO)).isEqualTo(BAZ); - spans = reporter.getSpans(); + List spans = reporter.getSpans(); assertThat(spans).hasSize(2); boolean updateSpanFound = false; - for(Span span: spans) { - if(span.getNameAsString().contains("_update")) { + for (Span span : spans) { + if (span.getNameAsString().contains("_update")) { updateSpanFound = true; break; } @@ -198,7 +199,10 @@ public void testDocumentScenario() throws IOException, ExecutionException, Inter // Finally - delete the document reporter.reset(); DeleteResponse dr = doDelete(new DeleteRequest(INDEX, DOC_TYPE, DOC_ID)); - validateSpanContent(spans.get(0), String.format("Elasticsearch: DELETE /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 200, "DELETE"); + + validateSpan() + .method("DELETE").pathName("/%s/%s/%s", INDEX, DOC_TYPE, DOC_ID) + .check(); } @Test @@ -212,11 +216,10 @@ public void testScenarioAsBulkRequest() throws IOException, ExecutionException, )) .add(new DeleteRequest(INDEX, DOC_TYPE, "2"))); - validateSpanContentAfterBulkRequest(); - } - - private interface ClientMethod { - void invoke(Req request, ActionListener listener); + validateSpan() + .method("POST") + .pathName("/_bulk") + .check(); } private Res invokeAsync(Req request, ClientMethod method) throws InterruptedException, ExecutionException { @@ -280,7 +283,6 @@ private BulkResponse doBulk(BulkRequest bulkRequest) throws IOException, Executi return client.bulk(bulkRequest); } - private Response doPerformRequest(String method, String path) throws IOException, ExecutionException { if (async) { final CompletableFuture resultFuture = new CompletableFuture<>(); @@ -304,4 +306,9 @@ public void onFailure(Exception exception) { return lowLevelClient.performRequest(method, path); } + + private interface ClientMethod { + void invoke(Req request, ActionListener listener); + } + } diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientAsyncInstrumentation.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientAsyncInstrumentation.java index e50edf0dc9..9cacaed07b 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientAsyncInstrumentation.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientAsyncInstrumentation.java @@ -66,7 +66,7 @@ public static class ElasticsearchRestClientAsyncAdvice { @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) public static Object[] onBeforeExecute(@Advice.Argument(0) Request request, @Advice.Argument(1) ResponseListener responseListener) { - Span span = helper.createClientSpan(request.getMethod(), request.getEndpoint(), request.getEntity()); + Span span = helper.createClientSpan(request, request.getMethod(), request.getEndpoint(), request.getEntity()); if (span != null) { Object[] ret = new Object[2]; ret[0] = span; diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientSyncInstrumentation.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientSyncInstrumentation.java index 1186200f26..e29e2d9a98 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientSyncInstrumentation.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/main/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchClientSyncInstrumentation.java @@ -60,7 +60,7 @@ public static class ElasticsearchRestClientSyncAdvice { @Nullable @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) public static Object onBeforeExecute(@Advice.Argument(0) Request request) { - return helper.createClientSpan(request.getMethod(), request.getEndpoint(), request.getEntity()); + return helper.createClientSpan(request, request.getMethod(), request.getEndpoint(), request.getEntity()); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/AbstractEs6_4ClientInstrumentationTest.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/AbstractEs6_4ClientInstrumentationTest.java index 28e09e1d00..e3e0aa498d 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/AbstractEs6_4ClientInstrumentationTest.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/AbstractEs6_4ClientInstrumentationTest.java @@ -76,13 +76,17 @@ public void testCreateAndDeleteIndex() throws IOException, ExecutionException, I // Create an Index doCreateIndex(new CreateIndexRequest(SECOND_INDEX)); - validateSpanContentAfterIndexCreateRequest(); + validateSpan() + .method("PUT").pathName("/%s", SECOND_INDEX) + .check(); // Delete the index reporter.reset(); doDeleteIndex(new DeleteIndexRequest(SECOND_INDEX)); - validateSpanContentAfterIndexDeleteRequest(); + validateSpan() + .method("DELETE").pathName("/%s", SECOND_INDEX) + .check(); } @Test @@ -108,9 +112,10 @@ public void testDocumentScenario() throws Exception { // Index a document createDocument(); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - validateSpanContent(spans.get(0), String.format("Elasticsearch: PUT /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 201, "PUT"); + validateSpan() + .method("PUT").pathName("/%s/%s/%s", INDEX, DOC_TYPE, DOC_ID) + .statusCode(201) + .check(); reporter.reset(); // do search request @@ -119,11 +124,11 @@ public void testDocumentScenario() throws Exception { SearchResponse response = doSearch(searchRequest); verifyTotalHits(response.getHits()); - spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span searchSpan = spans.get(0); - validateSpanContent(searchSpan, String.format("Elasticsearch: POST /%s/_search", INDEX), 200, "POST"); - validateDbContextContent(searchSpan, "{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}"); + + validateSpan() + .method("POST").pathName("/%s/_search", INDEX) + .expectStatement("{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}") + .check(); reporter.reset(); Map jsonMap = new HashMap<>(); @@ -134,7 +139,7 @@ public void testDocumentScenario() throws Exception { SearchResponse sr = doSearch(new SearchRequest(INDEX)); assertThat(sr.getHits().getAt(0).getSourceAsMap().get(FOO)).isEqualTo(BAZ); - spans = reporter.getSpans(); + List spans = reporter.getSpans(); assertThat(spans).hasSize(2); boolean updateSpanFound = false; for (Span span : spans) { @@ -149,7 +154,10 @@ public void testDocumentScenario() throws Exception { reporter.reset(); DeleteResponse dr = deleteDocument(); assertThat(dr.status().getStatus()).isEqualTo(200); - validateSpanContent(spans.get(0), String.format("Elasticsearch: DELETE /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 200, "DELETE"); + + validateSpan() + .method("DELETE").pathName("/%s/%s/%s", INDEX, DOC_TYPE, DOC_ID) + .check(); } @Test @@ -165,11 +173,11 @@ public void testCountRequest_validateSpanContentAndDbContext() throws Exception CountResponse responses = doCount(countRequest); assertThat(responses.getCount()).isEqualTo(1); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /%s/_count", INDEX), 200, "POST"); - validateDbContextContent(span, "{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}"); + + validateSpan() + .method("POST").pathName("/%s/_count", INDEX) + .expectStatement("{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}") + .check(); deleteDocument(); } @@ -188,11 +196,10 @@ public void testMultiSearchRequest_validateSpanContentAndDbContext() throws Inte MultiSearchResponse response = doMultiSearch(multiSearchRequest); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, "Elasticsearch: POST /_msearch", 200, "POST"); - verifyMultiSearchSpanContent(span); + validateSpan() + .method("POST").pathName("/_msearch") + .expectStatement(getExpectedMultisearchStatement()) + .check(); deleteDocument(); } @@ -212,11 +219,11 @@ public void testRollupSearch_validateSpanContentAndDbContext() throws Interrupte SearchResponse response = doRollupSearch(rollupSearchRequest); verifyTotalHits(response.getHits()); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /%s/_rollup_search", INDEX), 200, "POST"); - validateDbContextContent(span, "{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}"); + + validateSpan() + .method("POST").pathName("/%s/_rollup_search", INDEX) + .expectStatement("{\"from\":0,\"size\":5,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\",\"boost\":1.0}}}}") + .check(); deleteDocument(); } @@ -231,12 +238,12 @@ public void testSearchTemplateRequest_validateSpanContentAndDbContext() throws I SearchTemplateResponse response = doSearchTemplate(searchTemplateRequest); verifyTotalHits(response.getResponse().getHits()); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); String httpMethod = getSearchTemplateHttpMethod(); - validateSpanContent(span, String.format("Elasticsearch: %s /%s/_search/template", httpMethod, INDEX), 200, httpMethod); - validateDbContextContent(span, "{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}"); + + validateSpan() + .method(httpMethod).pathName("/%s/_search/template", INDEX) + .expectStatement("{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}") + .check(); deleteDocument(); } @@ -259,11 +266,11 @@ public void testMultisearchTemplateRequest_validateSpanContentAndDbContext() thr MultiSearchTemplateResponse.Item[] items = response.getResponses(); assertThat(items.length).isEqualTo(1); verifyTotalHits(items[0].getResponse().getResponse().getHits()); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /_msearch/template", INDEX), 200, "POST"); - verifyMultiSearchTemplateSpanContent(span); + + validateSpan() + .method("POST").pathName("/_msearch/template") + .expectStatement(getMultiSearchTemplateStatement()) + .check(); deleteDocument(); } @@ -304,13 +311,9 @@ private SearchTemplateRequest prepareSearchTemplateRequest() { return searchTemplateRequest; } - protected void verifyMultiSearchTemplateSpanContent(Span span) { + protected abstract String getMultiSearchTemplateStatement(); - } - - protected void verifyMultiSearchSpanContent(Span span) { - - } + protected abstract String getExpectedMultisearchStatement(); protected void verifyTotalHits(SearchHits searchHits) { @@ -322,7 +325,10 @@ public void testScenarioAsBulkRequest() throws IOException, ExecutionException, .add(createIndexRequest("2")) .add(new DeleteRequest(INDEX, DOC_TYPE, "2"))); - validateSpanContentAfterBulkRequest(); + validateSpan() + .method("POST") + .pathName("/_bulk") + .check(); } diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchRestClientInstrumentationIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchRestClientInstrumentationIT.java index 23d72eca1b..b16ef2298f 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchRestClientInstrumentationIT.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/esrestclient/v6_4/ElasticsearchRestClientInstrumentationIT.java @@ -18,7 +18,6 @@ */ package co.elastic.apm.agent.esrestclient.v6_4; -import co.elastic.apm.agent.impl.transaction.Span; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -74,15 +73,15 @@ public static void stopElasticsearchContainerAndClient() throws IOException { } @Override - protected void verifyMultiSearchTemplateSpanContent(Span span) { - validateDbContextContent(span, "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\"}\n" + - "{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}\n"); + protected String getMultiSearchTemplateStatement() { + return "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\"}\n" + + "{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}\n"; } @Override - protected void verifyMultiSearchSpanContent(Span span) { - validateDbContextContent(span, "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\"}\n" + - "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\",\"operator\":\"OR\",\"prefix_length\":0,\"max_expansions\":50,\"fuzzy_transpositions\":true,\"lenient\":false,\"zero_terms_query\":\"NONE\",\"auto_generate_synonyms_phrase_query\":true,\"boost\":1.0}}}}\n"); + protected String getExpectedMultisearchStatement() { + return "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\"}\n" + + "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\",\"operator\":\"OR\",\"prefix_length\":0,\"max_expansions\":50,\"fuzzy_transpositions\":true,\"lenient\":false,\"zero_terms_query\":\"NONE\",\"auto_generate_synonyms_phrase_query\":true,\"boost\":1.0}}}}\n"; } @Override diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-7_x/src/test/java/co/elastic/apm/agent/esrestclient/v7_x/ElasticsearchRestClientInstrumentationIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-7_x/src/test/java/co/elastic/apm/agent/esrestclient/v7_x/ElasticsearchRestClientInstrumentationIT.java index 6cfde2f329..cceba3d000 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-7_x/src/test/java/co/elastic/apm/agent/esrestclient/v7_x/ElasticsearchRestClientInstrumentationIT.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-7_x/src/test/java/co/elastic/apm/agent/esrestclient/v7_x/ElasticsearchRestClientInstrumentationIT.java @@ -20,9 +20,9 @@ import co.elastic.apm.agent.esrestclient.v6_4.AbstractEs6_4ClientInstrumentationTest; import co.elastic.apm.agent.impl.transaction.AbstractSpan; -import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.tracer.Outcome; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -117,7 +117,6 @@ public void testCancelScenario() throws InterruptedException, ExecutionException // When spans are cancelled, we can't know the actual address, because there is no response, and we set the outcome as UNKNOWN reporter.disableCheckDestinationAddress(); reporter.disableCheckUnknownOutcome(); - disableHttpUrlCheck(); createDocument(); reporter.reset(); @@ -139,7 +138,12 @@ public void onFailure(Exception e) { cancellable.cancel(); Span searchSpan = reporter.getFirstSpan(500); - validateSpanContent(searchSpan, String.format("Elasticsearch: POST /%s/_search", INDEX), -1, "POST"); + validateSpan(searchSpan) + .method("POST").pathName("/%s/_search", INDEX) + .statusCode(-1) + .disableHttpUrlCheck() + .expectAnyStatement() + .check(); assertThat(searchSpan.getOutcome()) .describedAs("span outcome should be unknown when cancelled") @@ -188,15 +192,15 @@ public void onFailure(Exception exception) { } @Override - protected void verifyMultiSearchTemplateSpanContent(Span span) { - validateDbContextContent(span, "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\",\"ccs_minimize_roundtrips\":true}\n" + - "{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}\n"); + protected String getMultiSearchTemplateStatement() { + return "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\",\"ccs_minimize_roundtrips\":true}\n" + + "{\"source\":\"{ \\\"query\\\": { \\\"term\\\" : { \\\"{{field}}\\\" : \\\"{{value}}\\\" } }, \\\"size\\\" : \\\"{{size}}\\\"}\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"},\"explain\":false,\"profile\":false}\n"; } @Override - protected void verifyMultiSearchSpanContent(Span span) { - validateDbContextContent(span, "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\",\"ccs_minimize_roundtrips\":true}\n" + - "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\",\"operator\":\"OR\",\"prefix_length\":0,\"max_expansions\":50,\"fuzzy_transpositions\":true,\"lenient\":false,\"zero_terms_query\":\"NONE\",\"auto_generate_synonyms_phrase_query\":true,\"boost\":1.0}}}}\n"); + protected String getExpectedMultisearchStatement() { + return "{\"index\":[\"my-index\"],\"types\":[],\"search_type\":\"query_then_fetch\",\"ccs_minimize_roundtrips\":true}\n" + + "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\",\"operator\":\"OR\",\"prefix_length\":0,\"max_expansions\":50,\"fuzzy_transpositions\":true,\"lenient\":false,\"zero_terms_query\":\"NONE\",\"auto_generate_synonyms_phrase_query\":true,\"boost\":1.0}}}}\n"; } @Override diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/pom.xml b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/pom.xml similarity index 90% rename from apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/pom.xml rename to apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/pom.xml index 8c04574e8e..1fedc5f681 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/pom.xml +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/pom.xml @@ -7,7 +7,7 @@ 4.0.0 - apm-es-api-client-test + apm-es-restclient-plugin-8_x ${project.groupId}:${project.artifactId} @@ -25,11 +25,7 @@ co.elastic.clients elasticsearch-java ${version.elasticsearch-java} - - - com.fasterxml.jackson.core - jackson-databind - 2.14.2 + provided org.testcontainers diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/RestClientTransportInstrumentation.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/RestClientTransportInstrumentation.java new file mode 100644 index 0000000000..69241d1b8d --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/RestClientTransportInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.esrestclient.v8_x; + +import co.elastic.apm.agent.esrestclient.ElasticsearchRestClientInstrumentation; +import co.elastic.apm.agent.esrestclient.ElasticsearchRestClientInstrumentationHelper; +import co.elastic.clients.transport.Endpoint; +import co.elastic.clients.transport.TransportOptions; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.client.Request; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * Instruments {@link co.elastic.clients.transport.rest_client.RestClientTransport#prepareLowLevelRequest(Object, Endpoint, TransportOptions)}. + */ +@SuppressWarnings("JavadocReference") +public class RestClientTransportInstrumentation extends ElasticsearchRestClientInstrumentation { + + @Override + public ElementMatcher getTypeMatcher() { + return named("co.elastic.clients.transport.rest_client.RestClientTransport"); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("prepareLowLevelRequest") + .and(takesArguments(3)) + .and(takesArgument(1, named("co.elastic.clients.transport.Endpoint"))) + .and(returns(named("org.elasticsearch.client.Request"))); + } + + @Override + public String getAdviceClassName() { + return getClass().getName() + "$RestClientTransportAdvice"; + } + + public static class RestClientTransportAdvice { + + private static final ElasticsearchRestClientInstrumentationHelper helper = ElasticsearchRestClientInstrumentationHelper.get(); + + + @Advice.OnMethodExit(suppress = Throwable.class, inline = false) + public static void onPrepareLowLevelRequest(@Advice.Argument(1) Endpoint endpoint, @Advice.Return Request request) { + helper.registerEndpointId(request, endpoint.id()); + } + + } +} diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/package-info.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/package-info.java new file mode 100644 index 0000000000..04dd867f33 --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/java/co/elastic/apm/agent/esrestclient/v8_x/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +@NonnullApi +package co.elastic.apm.agent.esrestclient.v8_x; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..9f45d5ea0e --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1 @@ +co.elastic.apm.agent.esrestclient.v8_x.RestClientTransportInstrumentation diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/AbstractElasticsearchJavaTest.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/java/co/elastic/apm/agent/esrestclient/v8_x/Elasticsearch8JavaIT.java similarity index 75% rename from apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/AbstractElasticsearchJavaTest.java rename to apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/java/co/elastic/apm/agent/esrestclient/v8_x/Elasticsearch8JavaIT.java index 4c3042b79a..fbc1a7e392 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/java/co/elastic/apm/esjavaclient/AbstractElasticsearchJavaTest.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/java/co/elastic/apm/agent/esrestclient/v8_x/Elasticsearch8JavaIT.java @@ -16,14 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.esjavaclient; +package co.elastic.apm.agent.esrestclient.v8_x; import co.elastic.apm.agent.esrestclient.AbstractEsClientInstrumentationTest; -import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.tracer.Outcome; import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch._types.FieldValue; import co.elastic.clients.elasticsearch._types.Refresh; import co.elastic.clients.elasticsearch._types.SearchType; import co.elastic.clients.elasticsearch._types.StoredScript; @@ -68,25 +69,39 @@ import co.elastic.clients.elasticsearch.rollup.RollupSearchRequest; import co.elastic.clients.elasticsearch.rollup.RollupSearchResponse; import co.elastic.clients.json.JsonData; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; import jakarta.json.Json; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.elasticsearch.client.RestClient; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; -public abstract class AbstractElasticsearchJavaTest extends AbstractEsClientInstrumentationTest { +@RunWith(Parameterized.class) +public class Elasticsearch8JavaIT extends AbstractEsClientInstrumentationTest { + + private static final String ELASTICSEARCH_CONTAINER_VERSION = "docker.elastic.co/elasticsearch/elasticsearch:7.17.2"; - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractElasticsearchJavaTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Elasticsearch8JavaIT.class); protected static final String USER_NAME = "elastic-user"; protected static final String PASSWORD = "elastic-pass"; @@ -94,17 +109,57 @@ public abstract class AbstractElasticsearchJavaTest extends AbstractEsClientInst protected static ElasticsearchClient client; protected static ElasticsearchAsyncClient asyncClient; + public Elasticsearch8JavaIT(boolean async) { + this.async = async; + } + + @BeforeClass + public static void startElasticsearchContainerAndClient() throws IOException { + startContainer(ELASTICSEARCH_CONTAINER_VERSION); + + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(USER_NAME, PASSWORD)); + + RestClient restClient = RestClient.builder(HttpHost.create(container.getHttpHostAddress())) + .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) + .build(); + + ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); + client = new ElasticsearchClient(transport); + asyncClient = new ElasticsearchAsyncClient(transport); + + client.indices().create(new CreateIndexRequest.Builder().index(INDEX).build()); + reporter.reset(); + } + + @AfterClass + public static void stopElasticsearchContainerAndClient() throws IOException { + if (client != null) { + // prevent misleading NPE when failed to start container + client.indices().delete(new DeleteIndexRequest.Builder().index(INDEX).build()); + } + } + + @Test public void testCreateAndDeleteIndex() throws IOException, ExecutionException, InterruptedException { // Create an Index doCreateIndex(new CreateIndexRequest.Builder().index(SECOND_INDEX).build()); - validateSpanContentAfterIndexCreateRequest(); + validateSpan() + .method("PUT") + .endpointName("indices.create") + .expectPathPart("index", SECOND_INDEX) + .check(); reporter.reset(); // Delete the index doDeleteIndex(new DeleteIndexRequest.Builder().index(SECOND_INDEX).build()); - validateSpanContentAfterIndexDeleteRequest(); + validateSpan() + .method("DELETE") + .endpointName("indices.delete") + .expectPathPart("index", SECOND_INDEX) + .check(); } @Test @@ -134,7 +189,12 @@ public void testTryToDeleteNonExistingIndex() { Span span = reporter.getFirstSpan(); assertThat(span.getOutcome()).isEqualTo(Outcome.FAILURE); - validateSpanContent(span, String.format("Elasticsearch: DELETE /%s", SECOND_INDEX), 404, "DELETE"); + validateSpan(span) + .method("DELETE") + .endpointName("indices.delete") + .expectPathPart("index", SECOND_INDEX) + .statusCode(404) + .check(); } @Test @@ -144,7 +204,13 @@ public void testDocumentScenario() throws Exception { List spans = reporter.getSpans(); try { assertThat(spans).hasSize(1); - validateSpanContent(spans.get(0), String.format("Elasticsearch: PUT /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 201, "PUT"); + validateSpan(spans.get(0)) + .method("PUT") + .endpointName("index") + .expectPathPart("index", INDEX) + .expectPathPart("id", DOC_ID) + .statusCode(201) + .check(); // *** RESET *** reporter.reset(); @@ -158,8 +224,13 @@ public void testDocumentScenario() throws Exception { spans = reporter.getSpans(); assertThat(spans).hasSize(1); Span searchSpan = spans.get(0); - validateSpanContent(searchSpan, String.format("Elasticsearch: POST /%s/_search", INDEX), 200, "POST"); - validateDbContextContent(searchSpan, "{\"from\":0,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}},\"size\":5}"); + + validateSpan(searchSpan) + .method("POST") + .endpointName("search") + .expectPathPart("index", INDEX) + .expectStatement("{\"from\":0,\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}},\"size\":5}") + .check(); // *** RESET *** reporter.reset(); @@ -178,10 +249,20 @@ public void testDocumentScenario() throws Exception { spans = reporter.getSpans(); assertThat(spans).hasSize(2); Span updateSpan = spans.get(0); - validateSpanContent(updateSpan, String.format("Elasticsearch: POST /%s/_update/%s", INDEX, DOC_ID), 200, "POST"); + validateSpan(updateSpan) + .method("POST") + .endpointName("update") + .expectPathPart("index", INDEX) + .expectPathPart("id", DOC_ID) + .check(); + searchSpan = spans.get(1); - validateSpanContent(searchSpan, String.format("Elasticsearch: POST /%s/_search", INDEX), 200, "POST"); - validateDbContextContent(searchSpan, "{}"); + validateSpan(searchSpan) + .method("POST") + .endpointName("search") + .expectPathPart("index", INDEX) + .expectStatement("{}") + .check(); // *** RESET *** reporter.reset(); @@ -190,7 +271,12 @@ public void testDocumentScenario() throws Exception { // 4. Delete document and validate span content. co.elastic.clients.elasticsearch.core.DeleteResponse dr = deleteDocument(); assertThat(dr.result().jsonValue()).isEqualTo("deleted"); - validateSpanContent(spans.get(0), String.format("Elasticsearch: DELETE /%s/%s/%s", INDEX, DOC_TYPE, DOC_ID), 200, "DELETE"); + validateSpan(spans.get(0)) + .method("DELETE") + .endpointName("delete") + .expectPathPart("index", INDEX) + .expectPathPart("id", DOC_ID) + .check(); } } @@ -200,17 +286,18 @@ public void testCountRequest_validateSpanContentAndDbContext() throws Exception reporter.reset(); CountRequest countRequest = new CountRequest.Builder().index(INDEX).query(new Query.Builder() - .term(new TermQuery.Builder().field(FOO).value(BAR).build()) + .term(new TermQuery.Builder().field(FOO).value(FieldValue.of(BAR)).build()) .build()).build(); try { CountResponse responses = doCount(countRequest); assertThat(responses.count()).isEqualTo(1L); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /%s/_count", INDEX), 200, "POST"); - validateDbContextContent(span, "{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}}}"); + validateSpan() + .method("POST") + .endpointName("count") + .expectPathPart("index", INDEX) + .expectStatement("{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}}}") + .check(); } finally { deleteDocument(); } @@ -225,9 +312,26 @@ public void testMultiSearchRequest_validateSpanContentAndDbContext() throws Inte MsearchRequest multiSearchRequestWithIndex = getMultiSearchRequestBuilder().index(INDEX).build(); try { - doMultiSearchAndSpanValidate(multiSearchRequest, "Elasticsearch: POST /_msearch"); + doMultiSearch(multiSearchRequest, Map.class); + + String statement = "{\"index\":[\"my-index\"],\"search_type\":\"query_then_fetch\"}\n" + + "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\"}}}}\n"; + + validateSpan() + .method("POST") + .endpointName("msearch") + .expectNoPathParts() + .expectStatement(statement) + .check(); reporter.reset(); - doMultiSearchAndSpanValidate(multiSearchRequestWithIndex, String.format("Elasticsearch: POST /%s/_msearch", INDEX)); + doMultiSearch(multiSearchRequestWithIndex, Map.class); + + validateSpan() + .method("POST") + .endpointName("msearch") + .expectPathPart("index", INDEX) + .expectStatement(statement) + .check(); } finally { deleteDocument(); } @@ -244,23 +348,13 @@ private MsearchRequest.Builder getMultiSearchRequestBuilder() { .query(new Query.Builder() .match(new MatchQuery.Builder() .field(FOO) - .query(BAR) + .query(FieldValue.of(BAR)) .build()) .build()) .build()) .build()); } - private void doMultiSearchAndSpanValidate(MsearchRequest multiSearchRequest, String expectedSpanName) throws IOException, ExecutionException, InterruptedException { - doMultiSearch(multiSearchRequest, Map.class); - - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, expectedSpanName, 200, "POST"); - verifyMultiSearchSpanContent(span); - } - @Test public void testRollupSearch_validateSpanContentAndDbContext() throws InterruptedException, ExecutionException, IOException { prepareDefaultDocumentAndIndex(); @@ -269,7 +363,7 @@ public void testRollupSearch_validateSpanContentAndDbContext() throws Interrupte RollupSearchRequest searchRequest = new RollupSearchRequest.Builder() .index(INDEX) .query(new Query.Builder() - .term(new TermQuery.Builder().field(FOO).value(BAR).build()) + .term(new TermQuery.Builder().field(FOO).value(FieldValue.of(BAR)).build()) .build()) .size(5) .build(); @@ -277,11 +371,13 @@ public void testRollupSearch_validateSpanContentAndDbContext() throws Interrupte RollupSearchResponse response = doRollupSearch(searchRequest, Map.class); verifyTotalHits(response.hits()); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /%s/_rollup_search", INDEX), 200, "POST"); - validateDbContextContent(span, "{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}},\"size\":5}"); + + validateSpan() + .method("POST") + .endpointName("rollup.rollup_search") + .expectPathPart("index", INDEX) + .expectStatement("{\"query\":{\"term\":{\"foo\":{\"value\":\"bar\"}}},\"size\":5}") + .check(); } finally { deleteDocument(); } @@ -298,11 +394,13 @@ public void testSearchTemplateRequest_validateSpanContentAndDbContext() throws I SearchTemplateResponse response = doSearchTemplate(searchTemplateRequest, Map.class); verifyTotalHits(response.hits()); - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /%s/_search/template", INDEX), 200, "POST"); - validateDbContextContent(span, "{\"id\":\"elastic-search-template\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"}}"); + + validateSpan() + .method("POST") + .endpointName("search_template") + .expectPathPart("index", INDEX) + .expectStatement("{\"id\":\"elastic-search-template\",\"params\":{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"}}") + .check(); } finally { deleteMustacheScript(); deleteDocument(); @@ -340,8 +438,14 @@ public void testMultisearchTemplateRequest_validateSpanContentAndDbContext() thr List spans = reporter.getSpans(); assertThat(spans).hasSize(1); Span span = spans.get(0); - validateSpanContent(span, String.format("Elasticsearch: POST /_msearch/template", INDEX), 200, "POST"); - verifyMultiSearchTemplateSpanContent(span); + + validateSpan(span) + .method("POST") + .endpointName("msearch_template") + .expectNoPathParts() + .expectStatement("{\"index\":[\"my-index\"],\"search_type\":\"query_then_fetch\"}\n" + + "{\"id\":\"elastic-search-template\",\"params\":{\"value\":\"bar\",\"field\":\"foo\",\"size\":5}}") + .check(); } finally { deleteMustacheScript(); deleteDocument(); @@ -369,31 +473,13 @@ public void testScenarioAsBulkRequest() throws IOException, ExecutionException, doBulk(bulkRequest); - validateSpanContentAfterBulkRequest(); + validateSpan() + .method("POST") + .endpointName("bulk") + .expectNoPathParts() + .check(); } - private void verifyMultiSearchTemplateSpanContent(Span span) { - String immutablePart = "{\"index\":[\"my-index\"],\"search_type\":\"query_then_fetch\"}\n" + - "{\"id\":\"elastic-search-template\",\"params\":"; - List params = Arrays.asList( - "{\"size\":5,\"field\":\"foo\",\"value\":\"bar\"}", - "{\"size\":5,\"value\":\"bar\",\"field\":\"foo\"}", - "{\"field\":\"foo\",\"size\":5,\"value\":\"bar\"}", - "{\"value\":\"bar\",\"size\":5,\"field\":\"foo\"}", - "{\"field\":\"foo\",\"value\":\"bar\",\"size\":5}", - "{\"value\":\"bar\",\"field\":\"foo\",\"size\":5}"); - String end = "}\n"; - - List possibleSpanContent = params.stream() - .map(k -> immutablePart + k + end).collect(Collectors.toList()); - - validateDbContextContent(span, possibleSpanContent); - } - - private void verifyMultiSearchSpanContent(Span span) { - validateDbContextContent(span, "{\"index\":[\"my-index\"],\"search_type\":\"query_then_fetch\"}\n" + - "{\"query\":{\"match\":{\"foo\":{\"query\":\"bar\"}}}}\n"); - } private void verifyTotalHits(HitsMetadata hitsMetadata) { assertThat(hitsMetadata.total().value()).isEqualTo(1L); @@ -484,7 +570,7 @@ private DeleteResponse deleteDocument() throws IOException { private SearchRequest prepareSearchRequestWithTermQuery() { return new SearchRequest.Builder().index(INDEX) .query(new Query.Builder() - .term(new TermQuery.Builder().field(FOO).value(BAR).build()) + .term(new TermQuery.Builder().field(FOO).value(FieldValue.of(BAR)).build()) .build()) .from(0) .size(5) @@ -534,5 +620,4 @@ private SearchTemplateRequest prepareSearchTemplateRequest(String templateId) { .build(); return searchTemplateRequest; } - } diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/resources/commons-logging.properties b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/resources/commons-logging.properties similarity index 100% rename from apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/resources/commons-logging.properties rename to apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/resources/commons-logging.properties diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/resources/log4j.properties b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/resources/log4j.properties similarity index 100% rename from apm-agent-plugins/apm-es-restclient-plugin/apm-es-api-client-test/src/test/resources/log4j.properties rename to apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-8_x/src/test/resources/log4j.properties diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointDefinition.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointDefinition.java new file mode 100644 index 0000000000..9da1d712d6 --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointDefinition.java @@ -0,0 +1,193 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.esrestclient; + +import co.elastic.apm.agent.tracer.Span; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class ElasticsearchEndpointDefinition { + + private static final String OTEL_PATH_PARTS_ATTRIBUTE_PREFIX = "db.elasticsearch.path_parts."; + + private static final String UNDERSCORE_REPLACEMENT = "0"; + + private final String endpointName; + private final List routes; + + private final boolean isSearchEndpoint; + + public ElasticsearchEndpointDefinition( + String endpointName, String[] routes, boolean isSearchEndpoint) { + this.endpointName = endpointName; + this.routes = new ArrayList<>(); + for (String route : routes) { + this.routes.add(new Route(route)); + } + this.isSearchEndpoint = isSearchEndpoint; + } + + + public String getEndpointName() { + return endpointName; + } + + public boolean isSearchEndpoint() { + return isSearchEndpoint; + } + + public void addPathPartAttributes(String urlPath, Span spanToEnrich) { + for (Route route : routes) { + if (route.hasParameters()) { + EndpointPattern pattern = route.getEndpointPattern(); + Matcher matcher = pattern.createMatcher(urlPath); + if (matcher.find()) { + for (String groupName : pattern.getPatternGroupNames()) { + String value = matcher.group(groupName); + String attributeKey = pattern.getOtelPathPartAttributeName(groupName); + spanToEnrich.withOtelAttribute(attributeKey, value); + } + return; + } + } + } + } + + List getRoutes() { + return routes; + } + + static final class Route { + private final String name; + private final boolean hasParameters; + + private volatile EndpointPattern epPattern; + + public Route(String name) { + this.name = name; + this.hasParameters = name.contains("{") && name.contains("}"); + } + + String getName() { + return name; + } + + boolean hasParameters() { + return hasParameters; + } + + private EndpointPattern getEndpointPattern() { + // Intentionally NOT synchronizing here to avoid synchronization overhead. + // Main purpose here is to cache the pattern without the need for strict thread-safety. + if (epPattern == null) { + epPattern = new EndpointPattern(this); + } + + return epPattern; + } + } + + static final class EndpointPattern { + private static final Pattern PATH_PART_NAMES_PATTERN = Pattern.compile("\\{([^}]+)}"); + private final Pattern pattern; + private final Map pathPartNamesToOtelAttributes; + + /** + * Creates, compiles and caches a regular expression pattern and retrieves a set of + * pathPartNames (names of the URL path parameters) for this route. + * + *

The regex pattern is later being used to match against a URL path to retrieve the URL path + * parameters for that route pattern using named regex capture groups. + */ + private EndpointPattern(Route route) { + pattern = buildRegexPattern(route.getName()); + + if (route.hasParameters()) { + pathPartNamesToOtelAttributes = new HashMap<>(); + Matcher matcher = PATH_PART_NAMES_PATTERN.matcher(route.getName()); + while (matcher.find()) { + String groupName = matcher.group(1); + + if (groupName != null) { + String actualPatternGroupName = groupName.replace("_", UNDERSCORE_REPLACEMENT); + pathPartNamesToOtelAttributes.put(actualPatternGroupName, OTEL_PATH_PARTS_ATTRIBUTE_PREFIX + groupName); + } + } + } else { + pathPartNamesToOtelAttributes = Collections.emptyMap(); + } + } + + /** + * Builds a regex pattern from the parameterized route pattern. + */ + static Pattern buildRegexPattern(String routeStr) { + StringBuilder regexStr = new StringBuilder(); + regexStr.append('^'); + int startIdx = routeStr.indexOf("{"); + while (startIdx >= 0) { + regexStr.append(routeStr.substring(0, startIdx)); + + int endIndex = routeStr.indexOf("}"); + if (endIndex <= startIdx + 1) { + break; + } + + // Append named capture group. + // If group name contains an underscore `_` it is being replaced with `0`, + // because `_` is not allowed in capture group names. + regexStr.append("(?<"); + regexStr.append( + routeStr.substring(startIdx + 1, endIndex).replace("_", UNDERSCORE_REPLACEMENT)); + regexStr.append(">[^/]+)"); + + routeStr = routeStr.substring(endIndex + 1); + startIdx = routeStr.indexOf("{"); + } + + regexStr.append(routeStr); + regexStr.append('$'); + + return Pattern.compile(regexStr.toString()); + } + + Matcher createMatcher(String urlPath) { + return pattern.matcher(urlPath); + } + + String getOtelPathPartAttributeName(String patternGroupName) { + String attributeName = pathPartNamesToOtelAttributes.get(patternGroupName); + if (attributeName == null) { + throw new IllegalArgumentException(patternGroupName + " is not a group of this pattern!"); + } + return attributeName; + } + + Collection getPatternGroupNames() { + return pathPartNamesToOtelAttributes.keySet(); + } + } +} diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMap.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMap.java new file mode 100644 index 0000000000..d32aecd0b7 --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMap.java @@ -0,0 +1,899 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.esrestclient; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class ElasticsearchEndpointMap { + + private static final Map routesMap; + + static { + Map routes = new HashMap<>(415); + initEndpoint(routes, "async_search.status", false, "/_async_search/status/{id}"); + initEndpoint(routes, "indices.analyze", false, "/_analyze", "/{index}/_analyze"); + initEndpoint(routes, "sql.clear_cursor", false, "/_sql/close"); + initEndpoint(routes, "ml.delete_datafeed", false, "/_ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "explain", false, "/{index}/_explain/{id}"); + initEndpoint( + routes, + "cat.thread_pool", + false, + "/_cat/thread_pool", + "/_cat/thread_pool/{thread_pool_patterns}"); + initEndpoint(routes, "ml.delete_calendar", false, "/_ml/calendars/{calendar_id}"); + initEndpoint(routes, "indices.create_data_stream", false, "/_data_stream/{name}"); + initEndpoint(routes, "cat.fielddata", false, "/_cat/fielddata", "/_cat/fielddata/{fields}"); + initEndpoint(routes, "security.enroll_node", false, "/_security/enroll/node"); + initEndpoint(routes, "slm.get_status", false, "/_slm/status"); + initEndpoint(routes, "ml.put_calendar", false, "/_ml/calendars/{calendar_id}"); + initEndpoint(routes, "create", false, "/{index}/_create/{id}"); + initEndpoint( + routes, + "ml.preview_datafeed", + false, + "/_ml/datafeeds/{datafeed_id}/_preview", + "/_ml/datafeeds/_preview"); + initEndpoint(routes, "indices.put_template", false, "/_template/{name}"); + initEndpoint( + routes, + "nodes.reload_secure_settings", + false, + "/_nodes/reload_secure_settings", + "/_nodes/{node_id}/reload_secure_settings"); + initEndpoint(routes, "indices.delete_data_stream", false, "/_data_stream/{name}"); + initEndpoint( + routes, + "transform.schedule_now_transform", + false, + "/_transform/{transform_id}/_schedule_now"); + initEndpoint(routes, "slm.stop", false, "/_slm/stop"); + initEndpoint(routes, "rollup.delete_job", false, "/_rollup/job/{id}"); + initEndpoint(routes, "cluster.put_component_template", false, "/_component_template/{name}"); + initEndpoint(routes, "delete_script", false, "/_scripts/{id}"); + initEndpoint(routes, "ml.delete_trained_model", false, "/_ml/trained_models/{model_id}"); + initEndpoint( + routes, + "indices.simulate_template", + false, + "/_index_template/_simulate", + "/_index_template/_simulate/{name}"); + initEndpoint(routes, "slm.get_lifecycle", false, "/_slm/policy/{policy_id}", "/_slm/policy"); + initEndpoint(routes, "security.enroll_kibana", false, "/_security/enroll/kibana"); + initEndpoint(routes, "fleet.search", false, "/{index}/_fleet/_fleet_search"); + initEndpoint(routes, "reindex_rethrottle", false, "/_reindex/{task_id}/_rethrottle"); + initEndpoint(routes, "ml.update_filter", false, "/_ml/filters/{filter_id}/_update"); + initEndpoint(routes, "rollup.get_rollup_caps", false, "/_rollup/data/{id}", "/_rollup/data"); + initEndpoint( + routes, "ccr.resume_auto_follow_pattern", false, "/_ccr/auto_follow/{name}/resume"); + initEndpoint(routes, "features.get_features", false, "/_features"); + initEndpoint(routes, "slm.get_stats", false, "/_slm/stats"); + initEndpoint(routes, "indices.clear_cache", false, "/_cache/clear", "/{index}/_cache/clear"); + initEndpoint( + routes, + "cluster.post_voting_config_exclusions", + false, + "/_cluster/voting_config_exclusions"); + initEndpoint(routes, "index", false, "/{index}/_doc/{id}", "/{index}/_doc"); + initEndpoint(routes, "cat.pending_tasks", false, "/_cat/pending_tasks"); + initEndpoint(routes, "indices.promote_data_stream", false, "/_data_stream/_promote/{name}"); + initEndpoint(routes, "ml.delete_filter", false, "/_ml/filters/{filter_id}"); + initEndpoint(routes, "sql.query", false, "/_sql"); + initEndpoint(routes, "ccr.follow_stats", false, "/{index}/_ccr/stats"); + initEndpoint(routes, "transform.stop_transform", false, "/_transform/{transform_id}/_stop"); + initEndpoint( + routes, + "security.has_privileges_user_profile", + false, + "/_security/profile/_has_privileges"); + initEndpoint( + routes, "autoscaling.delete_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "scripts_painless_execute", false, "/_scripts/painless/_execute"); + initEndpoint(routes, "indices.delete", false, "/{index}"); + initEndpoint( + routes, "security.clear_cached_roles", false, "/_security/role/{name}/_clear_cache"); + initEndpoint(routes, "eql.delete", false, "/_eql/search/{id}"); + initEndpoint(routes, "update", false, "/{index}/_update/{id}"); + initEndpoint( + routes, + "snapshot.clone", + false, + "/_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}"); + initEndpoint(routes, "license.get_basic_status", false, "/_license/basic_status"); + initEndpoint(routes, "indices.close", false, "/{index}/_close"); + initEndpoint(routes, "security.saml_authenticate", false, "/_security/saml/authenticate"); + initEndpoint( + routes, "search_application.put", false, "/_application/search_application/{name}"); + initEndpoint(routes, "count", false, "/_count", "/{index}/_count"); + initEndpoint( + routes, + "migration.deprecations", + false, + "/_migration/deprecations", + "/{index}/_migration/deprecations"); + initEndpoint(routes, "indices.segments", false, "/_segments", "/{index}/_segments"); + initEndpoint(routes, "security.suggest_user_profiles", false, "/_security/profile/_suggest"); + initEndpoint(routes, "security.get_user_privileges", false, "/_security/user/_privileges"); + initEndpoint( + routes, + "indices.delete_alias", + false, + "/{index}/_alias/{name}", + "/{index}/_aliases/{name}"); + initEndpoint(routes, "indices.get_mapping", false, "/_mapping", "/{index}/_mapping"); + initEndpoint(routes, "indices.put_index_template", false, "/_index_template/{name}"); + initEndpoint( + routes, + "searchable_snapshots.stats", + false, + "/_searchable_snapshots/stats", + "/{index}/_searchable_snapshots/stats"); + initEndpoint(routes, "security.disable_user", false, "/_security/user/{username}/_disable"); + initEndpoint( + routes, + "ml.upgrade_job_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_upgrade"); + initEndpoint(routes, "delete", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "async_search.delete", false, "/_async_search/{id}"); + initEndpoint( + routes, "cat.transforms", false, "/_cat/transforms", "/_cat/transforms/{transform_id}"); + initEndpoint(routes, "ping", false, "/"); + initEndpoint(routes, "ccr.pause_auto_follow_pattern", false, "/_ccr/auto_follow/{name}/pause"); + initEndpoint(routes, "indices.shard_stores", false, "/_shard_stores", "/{index}/_shard_stores"); + initEndpoint( + routes, "ml.update_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_update"); + initEndpoint(routes, "logstash.delete_pipeline", false, "/_logstash/pipeline/{id}"); + initEndpoint(routes, "sql.translate", false, "/_sql/translate"); + initEndpoint(routes, "exists", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "snapshot.get_repository", false, "/_snapshot", "/_snapshot/{repository}"); + initEndpoint(routes, "snapshot.verify_repository", false, "/_snapshot/{repository}/_verify"); + initEndpoint(routes, "indices.put_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint(routes, "ml.open_job", false, "/_ml/anomaly_detectors/{job_id}/_open"); + initEndpoint( + routes, "security.update_user_profile_data", false, "/_security/profile/{uid}/_data"); + initEndpoint(routes, "enrich.put_policy", false, "/_enrich/policy/{name}"); + initEndpoint( + routes, + "ml.get_datafeed_stats", + false, + "/_ml/datafeeds/{datafeed_id}/_stats", + "/_ml/datafeeds/_stats"); + initEndpoint(routes, "open_point_in_time", false, "/{index}/_pit"); + initEndpoint(routes, "get_source", false, "/{index}/_source/{id}"); + initEndpoint(routes, "delete_by_query", false, "/{index}/_delete_by_query"); + initEndpoint(routes, "security.create_api_key", false, "/_security/api_key"); + initEndpoint(routes, "cat.tasks", false, "/_cat/tasks"); + initEndpoint(routes, "watcher.delete_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "ingest.processor_grok", false, "/_ingest/processor/grok"); + initEndpoint(routes, "ingest.put_pipeline", false, "/_ingest/pipeline/{id}"); + initEndpoint( + routes, + "ml.get_data_frame_analytics_stats", + false, + "/_ml/data_frame/analytics/_stats", + "/_ml/data_frame/analytics/{id}/_stats"); + initEndpoint( + routes, + "indices.data_streams_stats", + false, + "/_data_stream/_stats", + "/_data_stream/{name}/_stats"); + initEndpoint( + routes, "security.clear_cached_realms", false, "/_security/realm/{realms}/_clear_cache"); + initEndpoint(routes, "field_caps", false, "/_field_caps", "/{index}/_field_caps"); + initEndpoint(routes, "ml.evaluate_data_frame", false, "/_ml/data_frame/_evaluate"); + initEndpoint( + routes, + "ml.delete_forecast", + false, + "/_ml/anomaly_detectors/{job_id}/_forecast", + "/_ml/anomaly_detectors/{job_id}/_forecast/{forecast_id}"); + initEndpoint(routes, "enrich.get_policy", false, "/_enrich/policy/{name}", "/_enrich/policy"); + initEndpoint(routes, "rollup.start_job", false, "/_rollup/job/{id}/_start"); + initEndpoint(routes, "tasks.cancel", false, "/_tasks/_cancel", "/_tasks/{task_id}/_cancel"); + initEndpoint(routes, "security.saml_logout", false, "/_security/saml/logout"); + initEndpoint( + routes, "render_search_template", true, "/_render/template", "/_render/template/{id}"); + initEndpoint(routes, "ml.get_calendar_events", false, "/_ml/calendars/{calendar_id}/events"); + initEndpoint(routes, "security.enable_user_profile", false, "/_security/profile/{uid}/_enable"); + initEndpoint( + routes, "logstash.get_pipeline", false, "/_logstash/pipeline", "/_logstash/pipeline/{id}"); + initEndpoint(routes, "cat.snapshots", false, "/_cat/snapshots", "/_cat/snapshots/{repository}"); + initEndpoint(routes, "indices.add_block", false, "/{index}/_block/{block}"); + initEndpoint(routes, "terms_enum", true, "/{index}/_terms_enum"); + initEndpoint(routes, "ml.forecast", false, "/_ml/anomaly_detectors/{job_id}/_forecast"); + initEndpoint( + routes, "cluster.stats", false, "/_cluster/stats", "/_cluster/stats/nodes/{node_id}"); + initEndpoint(routes, "search_application.list", false, "/_application/search_application"); + initEndpoint(routes, "cat.count", false, "/_cat/count", "/_cat/count/{index}"); + initEndpoint(routes, "cat.segments", false, "/_cat/segments", "/_cat/segments/{index}"); + initEndpoint(routes, "ccr.resume_follow", false, "/{index}/_ccr/resume_follow"); + initEndpoint( + routes, "search_application.get", false, "/_application/search_application/{name}"); + initEndpoint( + routes, + "security.saml_service_provider_metadata", + false, + "/_security/saml/metadata/{realm_name}"); + initEndpoint(routes, "update_by_query", false, "/{index}/_update_by_query"); + initEndpoint(routes, "ml.stop_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_stop"); + initEndpoint(routes, "ilm.explain_lifecycle", false, "/{index}/_ilm/explain"); + initEndpoint( + routes, + "ml.put_trained_model_vocabulary", + false, + "/_ml/trained_models/{model_id}/vocabulary"); + initEndpoint(routes, "indices.exists", false, "/{index}"); + initEndpoint(routes, "ml.set_upgrade_mode", false, "/_ml/set_upgrade_mode"); + initEndpoint(routes, "security.saml_invalidate", false, "/_security/saml/invalidate"); + initEndpoint( + routes, + "ml.get_job_stats", + false, + "/_ml/anomaly_detectors/_stats", + "/_ml/anomaly_detectors/{job_id}/_stats"); + initEndpoint(routes, "cluster.allocation_explain", false, "/_cluster/allocation/explain"); + initEndpoint(routes, "watcher.activate_watch", false, "/_watcher/watch/{watch_id}/_activate"); + initEndpoint( + routes, + "searchable_snapshots.clear_cache", + false, + "/_searchable_snapshots/cache/clear", + "/{index}/_searchable_snapshots/cache/clear"); + initEndpoint( + routes, "msearch_template", true, "/_msearch/template", "/{index}/_msearch/template"); + initEndpoint(routes, "bulk", false, "/_bulk", "/{index}/_bulk"); + initEndpoint(routes, "cat.nodeattrs", false, "/_cat/nodeattrs"); + initEndpoint( + routes, "indices.get_index_template", false, "/_index_template", "/_index_template/{name}"); + initEndpoint(routes, "license.get", false, "/_license"); + initEndpoint(routes, "ccr.forget_follower", false, "/{index}/_ccr/forget_follower"); + initEndpoint(routes, "security.delete_role", false, "/_security/role/{name}"); + initEndpoint( + routes, "indices.validate_query", false, "/_validate/query", "/{index}/_validate/query"); + initEndpoint(routes, "tasks.get", false, "/_tasks/{task_id}"); + initEndpoint( + routes, "ml.start_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_start"); + initEndpoint(routes, "indices.create", false, "/{index}"); + initEndpoint( + routes, + "cluster.delete_voting_config_exclusions", + false, + "/_cluster/voting_config_exclusions"); + initEndpoint(routes, "info", false, "/"); + initEndpoint(routes, "watcher.stop", false, "/_watcher/_stop"); + initEndpoint(routes, "enrich.delete_policy", false, "/_enrich/policy/{name}"); + initEndpoint( + routes, + "cat.ml_data_frame_analytics", + false, + "/_cat/ml/data_frame/analytics", + "/_cat/ml/data_frame/analytics/{id}"); + initEndpoint( + routes, + "security.change_password", + false, + "/_security/user/{username}/_password", + "/_security/user/_password"); + initEndpoint(routes, "put_script", false, "/_scripts/{id}", "/_scripts/{id}/{context}"); + initEndpoint(routes, "ml.put_datafeed", false, "/_ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "cat.master", false, "/_cat/master"); + initEndpoint(routes, "features.reset_features", false, "/_features/_reset"); + initEndpoint(routes, "indices.get_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint( + routes, + "ml.get_data_frame_analytics", + false, + "/_ml/data_frame/analytics/{id}", + "/_ml/data_frame/analytics"); + initEndpoint( + routes, + "security.delete_service_token", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}"); + initEndpoint(routes, "indices.recovery", false, "/_recovery", "/{index}/_recovery"); + initEndpoint(routes, "cat.recovery", false, "/_cat/recovery", "/_cat/recovery/{index}"); + initEndpoint(routes, "indices.downsample", false, "/{index}/_downsample/{target_index}"); + initEndpoint(routes, "ingest.delete_pipeline", false, "/_ingest/pipeline/{id}"); + initEndpoint(routes, "async_search.get", false, "/_async_search/{id}"); + initEndpoint(routes, "eql.get", false, "/_eql/search/{id}"); + initEndpoint(routes, "cat.aliases", false, "/_cat/aliases", "/_cat/aliases/{name}"); + initEndpoint( + routes, + "security.get_service_credentials", + false, + "/_security/service/{namespace}/{service}/credential"); + initEndpoint(routes, "cat.allocation", false, "/_cat/allocation", "/_cat/allocation/{node_id}"); + initEndpoint( + routes, "ml.stop_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}/_stop"); + initEndpoint(routes, "indices.open", false, "/{index}/_open"); + initEndpoint(routes, "ilm.get_lifecycle", false, "/_ilm/policy/{policy}", "/_ilm/policy"); + initEndpoint(routes, "ilm.remove_policy", false, "/{index}/_ilm/remove"); + initEndpoint( + routes, + "security.get_role_mapping", + false, + "/_security/role_mapping/{name}", + "/_security/role_mapping"); + initEndpoint(routes, "snapshot.create", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "watcher.get_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "license.post_start_trial", false, "/_license/start_trial"); + initEndpoint(routes, "snapshot.restore", false, "/_snapshot/{repository}/{snapshot}/_restore"); + initEndpoint(routes, "indices.put_mapping", false, "/{index}/_mapping"); + initEndpoint( + routes, "ml.delete_calendar_job", false, "/_ml/calendars/{calendar_id}/jobs/{job_id}"); + initEndpoint( + routes, "security.clear_api_key_cache", false, "/_security/api_key/{ids}/_clear_cache"); + initEndpoint(routes, "slm.start", false, "/_slm/start"); + initEndpoint( + routes, + "cat.component_templates", + false, + "/_cat/component_templates", + "/_cat/component_templates/{name}"); + initEndpoint(routes, "security.enable_user", false, "/_security/user/{username}/_enable"); + initEndpoint(routes, "cluster.delete_component_template", false, "/_component_template/{name}"); + initEndpoint(routes, "security.get_role", false, "/_security/role/{name}", "/_security/role"); + initEndpoint( + routes, "ingest.get_pipeline", false, "/_ingest/pipeline", "/_ingest/pipeline/{id}"); + initEndpoint( + routes, + "ml.delete_expired_data", + false, + "/_ml/_delete_expired_data/{job_id}", + "/_ml/_delete_expired_data"); + initEndpoint( + routes, + "indices.get_settings", + false, + "/_settings", + "/{index}/_settings", + "/{index}/_settings/{name}", + "/_settings/{name}"); + initEndpoint(routes, "ccr.follow", false, "/{index}/_ccr/follow"); + initEndpoint( + routes, "termvectors", false, "/{index}/_termvectors/{id}", "/{index}/_termvectors"); + initEndpoint(routes, "ml.post_data", false, "/_ml/anomaly_detectors/{job_id}/_data"); + initEndpoint(routes, "eql.search", true, "/{index}/_eql/search"); + initEndpoint( + routes, + "ml.get_trained_models", + false, + "/_ml/trained_models/{model_id}", + "/_ml/trained_models"); + initEndpoint( + routes, "security.disable_user_profile", false, "/_security/profile/{uid}/_disable"); + initEndpoint(routes, "security.put_privileges", false, "/_security/privilege"); + initEndpoint(routes, "cat.nodes", false, "/_cat/nodes"); + initEndpoint( + routes, "nodes.info", false, "/_nodes", "/_nodes/{node_id}", "/_nodes/{node_id}/{metric}"); + initEndpoint(routes, "graph.explore", false, "/{index}/_graph/explore"); + initEndpoint( + routes, "autoscaling.put_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "cat.templates", false, "/_cat/templates", "/_cat/templates/{name}"); + initEndpoint(routes, "cluster.remote_info", false, "/_remote/info"); + initEndpoint(routes, "rank_eval", false, "/_rank_eval", "/{index}/_rank_eval"); + initEndpoint( + routes, "security.delete_privileges", false, "/_security/privilege/{application}/{name}"); + initEndpoint( + routes, + "security.get_privileges", + false, + "/_security/privilege", + "/_security/privilege/{application}", + "/_security/privilege/{application}/{name}"); + initEndpoint(routes, "scroll", false, "/_search/scroll"); + initEndpoint(routes, "license.delete", false, "/_license"); + initEndpoint(routes, "indices.disk_usage", false, "/{index}/_disk_usage"); + initEndpoint(routes, "msearch", true, "/_msearch", "/{index}/_msearch"); + initEndpoint(routes, "indices.field_usage_stats", false, "/{index}/_field_usage_stats"); + initEndpoint( + routes, "indices.rollover", false, "/{alias}/_rollover", "/{alias}/_rollover/{new_index}"); + initEndpoint( + routes, + "cat.ml_trained_models", + false, + "/_cat/ml/trained_models", + "/_cat/ml/trained_models/{model_id}"); + initEndpoint( + routes, + "ml.delete_trained_model_alias", + false, + "/_ml/trained_models/{model_id}/model_aliases/{model_alias}"); + initEndpoint(routes, "indices.get", false, "/{index}"); + initEndpoint(routes, "sql.get_async_status", false, "/_sql/async/status/{id}"); + initEndpoint(routes, "ilm.stop", false, "/_ilm/stop"); + initEndpoint(routes, "security.put_user", false, "/_security/user/{username}"); + initEndpoint( + routes, + "cluster.state", + false, + "/_cluster/state", + "/_cluster/state/{metric}", + "/_cluster/state/{metric}/{index}"); + initEndpoint(routes, "indices.put_settings", false, "/_settings", "/{index}/_settings"); + initEndpoint(routes, "knn_search", false, "/{index}/_knn_search"); + initEndpoint(routes, "get", false, "/{index}/_doc/{id}"); + initEndpoint(routes, "eql.get_status", false, "/_eql/search/status/{id}"); + initEndpoint(routes, "ssl.certificates", false, "/_ssl/certificates"); + initEndpoint( + routes, + "ml.get_model_snapshots", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}", + "/_ml/anomaly_detectors/{job_id}/model_snapshots"); + initEndpoint( + routes, + "nodes.clear_repositories_metering_archive", + false, + "/_nodes/{node_id}/_repositories_metering/{max_archive_version}"); + initEndpoint(routes, "security.put_role", false, "/_security/role/{name}"); + initEndpoint( + routes, "ml.get_influencers", false, "/_ml/anomaly_detectors/{job_id}/results/influencers"); + initEndpoint(routes, "transform.upgrade_transforms", false, "/_transform/_upgrade"); + initEndpoint( + routes, + "ml.delete_calendar_event", + false, + "/_ml/calendars/{calendar_id}/events/{event_id}"); + initEndpoint( + routes, + "indices.get_field_mapping", + false, + "/_mapping/field/{fields}", + "/{index}/_mapping/field/{fields}"); + initEndpoint( + routes, + "transform.preview_transform", + false, + "/_transform/{transform_id}/_preview", + "/_transform/_preview"); + initEndpoint(routes, "tasks.list", false, "/_tasks"); + initEndpoint( + routes, + "ml.clear_trained_model_deployment_cache", + false, + "/_ml/trained_models/{model_id}/deployment/cache/_clear"); + initEndpoint(routes, "cluster.reroute", false, "/_cluster/reroute"); + initEndpoint(routes, "security.saml_complete_logout", false, "/_security/saml/complete_logout"); + initEndpoint( + routes, + "indices.simulate_index_template", + false, + "/_index_template/_simulate_index/{name}"); + initEndpoint(routes, "snapshot.get", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "ccr.put_auto_follow_pattern", false, "/_ccr/auto_follow/{name}"); + initEndpoint( + routes, "nodes.hot_threads", false, "/_nodes/hot_threads", "/_nodes/{node_id}/hot_threads"); + initEndpoint( + routes, + "ml.preview_data_frame_analytics", + false, + "/_ml/data_frame/analytics/_preview", + "/_ml/data_frame/analytics/{id}/_preview"); + initEndpoint(routes, "indices.flush", false, "/_flush", "/{index}/_flush"); + initEndpoint(routes, "cluster.exists_component_template", false, "/_component_template/{name}"); + initEndpoint( + routes, + "snapshot.status", + false, + "/_snapshot/_status", + "/_snapshot/{repository}/_status", + "/_snapshot/{repository}/{snapshot}/_status"); + initEndpoint(routes, "ml.update_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_update"); + initEndpoint(routes, "indices.update_aliases", false, "/_aliases"); + initEndpoint(routes, "autoscaling.get_autoscaling_capacity", false, "/_autoscaling/capacity"); + initEndpoint(routes, "migration.post_feature_upgrade", false, "/_migration/system_features"); + initEndpoint( + routes, "ml.get_records", false, "/_ml/anomaly_detectors/{job_id}/results/records"); + initEndpoint( + routes, + "indices.get_alias", + false, + "/_alias", + "/_alias/{name}", + "/{index}/_alias/{name}", + "/{index}/_alias"); + initEndpoint(routes, "logstash.put_pipeline", false, "/_logstash/pipeline/{id}"); + initEndpoint(routes, "snapshot.delete_repository", false, "/_snapshot/{repository}"); + initEndpoint( + routes, + "security.has_privileges", + false, + "/_security/user/_has_privileges", + "/_security/user/{user}/_has_privileges"); + initEndpoint(routes, "cat.indices", false, "/_cat/indices", "/_cat/indices/{index}"); + initEndpoint( + routes, + "ccr.get_auto_follow_pattern", + false, + "/_ccr/auto_follow", + "/_ccr/auto_follow/{name}"); + initEndpoint(routes, "ml.start_datafeed", false, "/_ml/datafeeds/{datafeed_id}/_start"); + initEndpoint(routes, "indices.clone", false, "/{index}/_clone/{target}"); + initEndpoint( + routes, "search_application.delete", false, "/_application/search_application/{name}"); + initEndpoint(routes, "security.query_api_keys", false, "/_security/_query/api_key"); + initEndpoint(routes, "ml.flush_job", false, "/_ml/anomaly_detectors/{job_id}/_flush"); + initEndpoint( + routes, + "security.clear_cached_privileges", + false, + "/_security/privilege/{application}/_clear_cache"); + initEndpoint(routes, "indices.exists_index_template", false, "/_index_template/{name}"); + initEndpoint(routes, "indices.explain_data_lifecycle", false, "/{index}/_lifecycle/explain"); + initEndpoint( + routes, "indices.put_alias", false, "/{index}/_alias/{name}", "/{index}/_aliases/{name}"); + initEndpoint( + routes, + "ml.get_buckets", + false, + "/_ml/anomaly_detectors/{job_id}/results/buckets/{timestamp}", + "/_ml/anomaly_detectors/{job_id}/results/buckets"); + initEndpoint( + routes, + "ml.put_trained_model_definition_part", + false, + "/_ml/trained_models/{model_id}/definition/{part}"); + initEndpoint(routes, "get_script", false, "/_scripts/{id}"); + initEndpoint( + routes, + "ingest.simulate", + false, + "/_ingest/pipeline/_simulate", + "/_ingest/pipeline/{id}/_simulate"); + initEndpoint(routes, "indices.migrate_to_data_stream", false, "/_data_stream/_migrate/{name}"); + initEndpoint(routes, "enrich.execute_policy", false, "/_enrich/policy/{name}/_execute"); + initEndpoint(routes, "indices.split", false, "/{index}/_split/{target}"); + initEndpoint( + routes, + "ml.delete_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}"); + initEndpoint( + routes, + "nodes.usage", + false, + "/_nodes/usage", + "/_nodes/{node_id}/usage", + "/_nodes/usage/{metric}", + "/_nodes/{node_id}/usage/{metric}"); + initEndpoint(routes, "cat.help", false, "/_cat"); + initEndpoint( + routes, "ml.estimate_model_memory", false, "/_ml/anomaly_detectors/_estimate_model_memory"); + initEndpoint(routes, "exists_source", false, "/{index}/_source/{id}"); + initEndpoint(routes, "ml.put_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}"); + initEndpoint(routes, "security.put_role_mapping", false, "/_security/role_mapping/{name}"); + initEndpoint(routes, "rollup.get_rollup_index_caps", false, "/{index}/_rollup/data"); + initEndpoint(routes, "transform.reset_transform", false, "/_transform/{transform_id}/_reset"); + initEndpoint( + routes, + "ml.infer_trained_model", + false, + "/_ml/trained_models/{model_id}/_infer", + "/_ml/trained_models/{model_id}/deployment/_infer"); + initEndpoint(routes, "reindex", false, "/_reindex"); + initEndpoint(routes, "ml.put_trained_model", false, "/_ml/trained_models/{model_id}"); + initEndpoint( + routes, + "cat.ml_jobs", + false, + "/_cat/ml/anomaly_detectors", + "/_cat/ml/anomaly_detectors/{job_id}"); + initEndpoint( + routes, + "search_application.search", + false, + "/_application/search_application/{name}/_search"); + initEndpoint(routes, "ilm.put_lifecycle", false, "/_ilm/policy/{policy}"); + initEndpoint(routes, "security.get_token", false, "/_security/oauth2/token"); + initEndpoint(routes, "ilm.move_to_step", false, "/_ilm/move/{index}"); + initEndpoint(routes, "search_template", true, "/_search/template", "/{index}/_search/template"); + initEndpoint(routes, "indices.delete_data_lifecycle", false, "/_data_stream/{name}/_lifecycle"); + initEndpoint(routes, "indices.get_data_stream", false, "/_data_stream", "/_data_stream/{name}"); + initEndpoint(routes, "ml.get_filters", false, "/_ml/filters", "/_ml/filters/{filter_id}"); + initEndpoint( + routes, + "cat.ml_datafeeds", + false, + "/_cat/ml/datafeeds", + "/_cat/ml/datafeeds/{datafeed_id}"); + initEndpoint(routes, "rollup.rollup_search", false, "/{index}/_rollup_search"); + initEndpoint(routes, "ml.put_job", false, "/_ml/anomaly_detectors/{job_id}"); + initEndpoint( + routes, "update_by_query_rethrottle", false, "/_update_by_query/{task_id}/_rethrottle"); + initEndpoint(routes, "indices.delete_index_template", false, "/_index_template/{name}"); + initEndpoint( + routes, "indices.reload_search_analyzers", false, "/{index}/_reload_search_analyzers"); + initEndpoint(routes, "cluster.get_settings", false, "/_cluster/settings"); + initEndpoint(routes, "cluster.put_settings", false, "/_cluster/settings"); + initEndpoint(routes, "transform.put_transform", false, "/_transform/{transform_id}"); + initEndpoint(routes, "watcher.stats", false, "/_watcher/stats", "/_watcher/stats/{metric}"); + initEndpoint(routes, "ccr.delete_auto_follow_pattern", false, "/_ccr/auto_follow/{name}"); + initEndpoint(routes, "mtermvectors", false, "/_mtermvectors", "/{index}/_mtermvectors"); + initEndpoint(routes, "license.post", false, "/_license"); + initEndpoint(routes, "xpack.info", false, "/_xpack"); + initEndpoint( + routes, "dangling_indices.import_dangling_index", false, "/_dangling/{index_uuid}"); + initEndpoint( + routes, + "nodes.get_repositories_metering_info", + false, + "/_nodes/{node_id}/_repositories_metering"); + initEndpoint( + routes, "transform.get_transform_stats", false, "/_transform/{transform_id}/_stats"); + initEndpoint(routes, "mget", false, "/_mget", "/{index}/_mget"); + initEndpoint(routes, "security.get_builtin_privileges", false, "/_security/privilege/_builtin"); + initEndpoint( + routes, + "ml.update_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_update"); + initEndpoint(routes, "ml.info", false, "/_ml/info"); + initEndpoint(routes, "indices.exists_template", false, "/_template/{name}"); + initEndpoint( + routes, + "watcher.ack_watch", + false, + "/_watcher/watch/{watch_id}/_ack", + "/_watcher/watch/{watch_id}/_ack/{action_id}"); + initEndpoint( + routes, "security.get_user", false, "/_security/user/{username}", "/_security/user"); + initEndpoint( + routes, "shutdown.get_node", false, "/_nodes/shutdown", "/_nodes/{node_id}/shutdown"); + initEndpoint(routes, "watcher.start", false, "/_watcher/_start"); + initEndpoint(routes, "indices.shrink", false, "/{index}/_shrink/{target}"); + initEndpoint(routes, "license.post_start_basic", false, "/_license/start_basic"); + initEndpoint(routes, "xpack.usage", false, "/_xpack/usage"); + initEndpoint(routes, "ilm.delete_lifecycle", false, "/_ilm/policy/{policy}"); + initEndpoint(routes, "ccr.follow_info", false, "/{index}/_ccr/info"); + initEndpoint( + routes, "ml.put_calendar_job", false, "/_ml/calendars/{calendar_id}/jobs/{job_id}"); + initEndpoint(routes, "rollup.put_job", false, "/_rollup/job/{id}"); + initEndpoint(routes, "clear_scroll", false, "/_search/scroll"); + initEndpoint(routes, "ml.delete_data_frame_analytics", false, "/_ml/data_frame/analytics/{id}"); + initEndpoint(routes, "security.get_api_key", false, "/_security/api_key"); + initEndpoint(routes, "cat.health", false, "/_cat/health"); + initEndpoint(routes, "security.invalidate_token", false, "/_security/oauth2/token"); + initEndpoint(routes, "slm.delete_lifecycle", false, "/_slm/policy/{policy_id}"); + initEndpoint( + routes, + "ml.stop_trained_model_deployment", + false, + "/_ml/trained_models/{model_id}/deployment/_stop"); + initEndpoint(routes, "monitoring.bulk", false, "/_monitoring/bulk", "/_monitoring/{type}/bulk"); + initEndpoint( + routes, + "indices.stats", + false, + "/_stats", + "/_stats/{metric}", + "/{index}/_stats", + "/{index}/_stats/{metric}"); + initEndpoint( + routes, + "searchable_snapshots.cache_stats", + false, + "/_searchable_snapshots/cache/stats", + "/_searchable_snapshots/{node_id}/cache/stats"); + initEndpoint(routes, "async_search.submit", true, "/_async_search", "/{index}/_async_search"); + initEndpoint(routes, "rollup.get_jobs", false, "/_rollup/job/{id}", "/_rollup/job"); + initEndpoint( + routes, + "ml.revert_model_snapshot", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_revert"); + initEndpoint(routes, "transform.delete_transform", false, "/_transform/{transform_id}"); + initEndpoint(routes, "cluster.pending_tasks", false, "/_cluster/pending_tasks"); + initEndpoint( + routes, + "ml.get_model_snapshot_upgrade_stats", + false, + "/_ml/anomaly_detectors/{job_id}/model_snapshots/{snapshot_id}/_upgrade/_stats"); + initEndpoint( + routes, + "ml.get_categories", + false, + "/_ml/anomaly_detectors/{job_id}/results/categories/{category_id}", + "/_ml/anomaly_detectors/{job_id}/results/categories"); + initEndpoint(routes, "ccr.pause_follow", false, "/{index}/_ccr/pause_follow"); + initEndpoint(routes, "security.authenticate", false, "/_security/_authenticate"); + initEndpoint(routes, "enrich.stats", false, "/_enrich/_stats"); + initEndpoint( + routes, + "ml.put_trained_model_alias", + false, + "/_ml/trained_models/{model_id}/model_aliases/{model_alias}"); + initEndpoint( + routes, + "ml.get_overall_buckets", + false, + "/_ml/anomaly_detectors/{job_id}/results/overall_buckets"); + initEndpoint(routes, "indices.get_template", false, "/_template", "/_template/{name}"); + initEndpoint(routes, "security.delete_role_mapping", false, "/_security/role_mapping/{name}"); + initEndpoint( + routes, "ml.get_datafeeds", false, "/_ml/datafeeds/{datafeed_id}", "/_ml/datafeeds"); + initEndpoint(routes, "slm.execute_lifecycle", false, "/_slm/policy/{policy_id}/_execute"); + initEndpoint(routes, "close_point_in_time", false, "/_pit"); + initEndpoint(routes, "snapshot.cleanup_repository", false, "/_snapshot/{repository}/_cleanup"); + initEndpoint( + routes, "autoscaling.get_autoscaling_policy", false, "/_autoscaling/policy/{name}"); + initEndpoint(routes, "slm.put_lifecycle", false, "/_slm/policy/{policy_id}"); + initEndpoint( + routes, "ml.get_jobs", false, "/_ml/anomaly_detectors/{job_id}", "/_ml/anomaly_detectors"); + initEndpoint( + routes, + "ml.get_trained_models_stats", + false, + "/_ml/trained_models/{model_id}/_stats", + "/_ml/trained_models/_stats"); + initEndpoint( + routes, "ml.validate_detector", false, "/_ml/anomaly_detectors/_validate/detector"); + initEndpoint(routes, "watcher.put_watch", false, "/_watcher/watch/{id}"); + initEndpoint(routes, "transform.update_transform", false, "/_transform/{transform_id}/_update"); + initEndpoint(routes, "ml.post_calendar_events", false, "/_ml/calendars/{calendar_id}/events"); + initEndpoint( + routes, "migration.get_feature_upgrade_status", false, "/_migration/system_features"); + initEndpoint(routes, "get_script_context", false, "/_script_context"); + initEndpoint(routes, "ml.put_filter", false, "/_ml/filters/{filter_id}"); + initEndpoint(routes, "ml.update_job", false, "/_ml/anomaly_detectors/{job_id}/_update"); + initEndpoint(routes, "ingest.geo_ip_stats", false, "/_ingest/geoip/stats"); + initEndpoint(routes, "security.delete_user", false, "/_security/user/{username}"); + initEndpoint(routes, "indices.unfreeze", false, "/{index}/_unfreeze"); + initEndpoint(routes, "snapshot.create_repository", false, "/_snapshot/{repository}"); + initEndpoint( + routes, + "cluster.get_component_template", + false, + "/_component_template", + "/_component_template/{name}"); + initEndpoint(routes, "ilm.migrate_to_data_tiers", false, "/_ilm/migrate_to_data_tiers"); + initEndpoint(routes, "indices.refresh", false, "/_refresh", "/{index}/_refresh"); + initEndpoint( + routes, "ml.get_calendars", false, "/_ml/calendars", "/_ml/calendars/{calendar_id}"); + initEndpoint( + routes, "watcher.deactivate_watch", false, "/_watcher/watch/{watch_id}/_deactivate"); + initEndpoint(routes, "cluster.health", false, "/_cluster/health", "/_cluster/health/{index}"); + initEndpoint( + routes, "dangling_indices.delete_dangling_index", false, "/_dangling/{index_uuid}"); + initEndpoint(routes, "health_report", false, "/_health_report", "/_health_report/{feature}"); + initEndpoint(routes, "watcher.query_watches", false, "/_watcher/_query/watches"); + initEndpoint(routes, "ccr.unfollow", false, "/{index}/_ccr/unfollow"); + initEndpoint(routes, "ml.validate", false, "/_ml/anomaly_detectors/_validate"); + initEndpoint(routes, "cat.plugins", false, "/_cat/plugins"); + initEndpoint( + routes, + "watcher.execute_watch", + false, + "/_watcher/watch/{id}/_execute", + "/_watcher/watch/_execute"); + initEndpoint(routes, "search_shards", false, "/_search_shards", "/{index}/_search_shards"); + initEndpoint(routes, "cat.shards", false, "/_cat/shards", "/_cat/shards/{index}"); + initEndpoint(routes, "ml.delete_job", false, "/_ml/anomaly_detectors/{job_id}"); + initEndpoint(routes, "ilm.start", false, "/_ilm/start"); + initEndpoint(routes, "security.get_user_profile", false, "/_security/profile/{uid}"); + initEndpoint(routes, "indices.modify_data_stream", false, "/_data_stream/_modify"); + initEndpoint(routes, "indices.exists_alias", false, "/_alias/{name}", "/{index}/_alias/{name}"); + initEndpoint(routes, "rollup.stop_job", false, "/_rollup/job/{id}/_stop"); + initEndpoint(routes, "dangling_indices.list_dangling_indices", false, "/_dangling"); + initEndpoint(routes, "snapshot.delete", false, "/_snapshot/{repository}/{snapshot}"); + initEndpoint(routes, "security.activate_user_profile", false, "/_security/profile/_activate"); + initEndpoint( + routes, + "ml.start_trained_model_deployment", + false, + "/_ml/trained_models/{model_id}/deployment/_start"); + initEndpoint(routes, "transform.start_transform", false, "/_transform/{transform_id}/_start"); + initEndpoint(routes, "cat.repositories", false, "/_cat/repositories"); + initEndpoint(routes, "ilm.get_status", false, "/_ilm/status"); + initEndpoint(routes, "shutdown.delete_node", false, "/_nodes/{node_id}/shutdown"); + initEndpoint( + routes, + "nodes.stats", + false, + "/_nodes/stats", + "/_nodes/{node_id}/stats", + "/_nodes/stats/{metric}", + "/_nodes/{node_id}/stats/{metric}", + "/_nodes/stats/{metric}/{index_metric}", + "/_nodes/{node_id}/stats/{metric}/{index_metric}"); + initEndpoint(routes, "get_script_languages", false, "/_script_language"); + initEndpoint(routes, "slm.execute_retention", false, "/_slm/_execute_retention"); + initEndpoint( + routes, + "security.get_service_accounts", + false, + "/_security/service/{namespace}/{service}", + "/_security/service/{namespace}", + "/_security/service"); + initEndpoint(routes, "shutdown.put_node", false, "/_nodes/{node_id}/shutdown"); + initEndpoint(routes, "indices.resolve_index", false, "/_resolve/index/{name}"); + initEndpoint(routes, "search", true, "/_search", "/{index}/_search"); + initEndpoint(routes, "sql.get_async", false, "/_sql/async/{id}"); + initEndpoint( + routes, "delete_by_query_rethrottle", false, "/_delete_by_query/{task_id}/_rethrottle"); + initEndpoint( + routes, "transform.get_transform", false, "/_transform/{transform_id}", "/_transform"); + initEndpoint(routes, "security.invalidate_api_key", false, "/_security/api_key"); + initEndpoint(routes, "security.saml_prepare_authentication", false, "/_security/saml/prepare"); + initEndpoint( + routes, "ml.get_memory_stats", false, "/_ml/memory/_stats", "/_ml/memory/{node_id}/_stats"); + initEndpoint(routes, "ccr.stats", false, "/_ccr/stats"); + initEndpoint(routes, "indices.forcemerge", false, "/_forcemerge", "/{index}/_forcemerge"); + initEndpoint(routes, "indices.delete_template", false, "/_template/{name}"); + initEndpoint(routes, "sql.delete_async", false, "/_sql/async/delete/{id}"); + initEndpoint(routes, "security.update_api_key", false, "/_security/api_key/{id}"); + initEndpoint( + routes, + "security.create_service_token", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}", + "/_security/service/{namespace}/{service}/credential/token"); + initEndpoint(routes, "license.get_trial_status", false, "/_license/trial_status"); + initEndpoint( + routes, "searchable_snapshots.mount", false, "/_snapshot/{repository}/{snapshot}/_mount"); + initEndpoint(routes, "security.grant_api_key", false, "/_security/api_key/grant"); + initEndpoint(routes, "ilm.retry", false, "/{index}/_ilm/retry"); + initEndpoint(routes, "ml.reset_job", false, "/_ml/anomaly_detectors/{job_id}/_reset"); + initEndpoint(routes, "ml.close_job", false, "/_ml/anomaly_detectors/{job_id}/_close"); + initEndpoint( + routes, + "ml.explain_data_frame_analytics", + false, + "/_ml/data_frame/analytics/_explain", + "/_ml/data_frame/analytics/{id}/_explain"); + initEndpoint( + routes, + "security.clear_cached_service_tokens", + false, + "/_security/service/{namespace}/{service}/credential/token/{name}/_clear_cache"); + initEndpoint(routes, "search_mvt", false, "/{index}/_mvt/{field}/{zoom}/{x}/{y}"); + routesMap = Collections.unmodifiableMap(routes); + } + + private ElasticsearchEndpointMap() { + } + + private static void initEndpoint( + Map map, + String endpointId, + boolean isSearchEndpoint, + String... routes) { + ElasticsearchEndpointDefinition endpointDef = + new ElasticsearchEndpointDefinition(endpointId, routes, isSearchEndpoint); + map.put(endpointId, endpointDef); + } + + @Nullable + public static ElasticsearchEndpointDefinition get(@Nullable String endpointId) { + if (endpointId == null) { + return null; + } + return routesMap.get(endpointId); + } + + public static Collection getAllEndpoints() { + return routesMap.values(); + } +} diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchRestClientInstrumentationHelper.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchRestClientInstrumentationHelper.java index 0ce18d6e32..58ed814796 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchRestClientInstrumentationHelper.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/main/java/co/elastic/apm/agent/esrestclient/ElasticsearchRestClientInstrumentationHelper.java @@ -19,12 +19,14 @@ package co.elastic.apm.agent.esrestclient; import co.elastic.apm.agent.common.util.WildcardMatcher; -import co.elastic.apm.agent.tracer.GlobalTracer; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent; +import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap; import co.elastic.apm.agent.tracer.AbstractSpan; +import co.elastic.apm.agent.tracer.GlobalTracer; import co.elastic.apm.agent.tracer.Outcome; import co.elastic.apm.agent.tracer.Span; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; import co.elastic.apm.agent.tracer.Tracer; import co.elastic.apm.agent.tracer.pooling.ObjectPool; import co.elastic.apm.agent.sdk.internal.util.IOUtils; @@ -41,6 +43,7 @@ public class ElasticsearchRestClientInstrumentationHelper { + private static final WeakMap requestEndpointMap = WeakConcurrent.buildMap(); private static final Logger logger = LoggerFactory.getLogger(ElasticsearchRestClientInstrumentationHelper.class); private static final Logger unsupportedOperationOnceLogger = LoggerUtils.logOnce(logger); @@ -73,9 +76,29 @@ public ResponseListenerWrapper createInstance() { } } + public void registerEndpointId(Object requestObj, String endpointId) { + if (endpointId.startsWith("es/") && endpointId.length() > 3) { + endpointId = endpointId.substring(3); + } + ElasticsearchEndpointDefinition endpoint = ElasticsearchEndpointMap.get(endpointId); + if (endpoint != null) { + requestEndpointMap.put(requestObj, endpoint); + } + } + @Nullable - public Span createClientSpan(String method, String endpoint, @Nullable HttpEntity httpEntity) { + public Span createClientSpan(Object requestObj, String method, String httpPath, @Nullable HttpEntity httpEntity) { + ElasticsearchEndpointDefinition endpoint = requestEndpointMap.remove(requestObj); + return createClientSpan(method, httpPath, httpEntity, endpoint); + } + @Nullable + public Span createClientSpan(String method, String httpPath, @Nullable HttpEntity httpEntity) { + return createClientSpan(method, httpPath, httpEntity, null); + } + + @Nullable + private Span createClientSpan(String method, String httpPath, @Nullable HttpEntity httpEntity, @Nullable ElasticsearchEndpointDefinition endpoint) { Span span = tracer.currentContext().createExitSpan(); // Don't record nested spans. In 5.x clients the instrumented sync method is calling the instrumented async method @@ -85,14 +108,27 @@ public Span createClientSpan(String method, String endpoint, @Nullable HttpEn span.withType(SPAN_TYPE) .withSubtype(ELASTICSEARCH) - .withAction(SPAN_ACTION) - .appendToName("Elasticsearch: ").appendToName(method).appendToName(" ").appendToName(endpoint); + .withAction(SPAN_ACTION); + + StringBuilder name = span.getAndOverrideName(AbstractSpan.PRIORITY_HIGH_LEVEL_FRAMEWORK); + if (endpoint != null) { + if (name != null) { + name.append("Elasticsearch: ").append(endpoint.getEndpointName()); + } + span.withOtelAttribute("db.operation", endpoint.getEndpointName()); + endpoint.addPathPartAttributes(httpPath, span); + } else { + if (name != null) { + name.append("Elasticsearch: ").append(method).append(" ").append(httpPath); + } + } + span.getContext().getDb().withType(ELASTICSEARCH); span.getContext().getServiceTarget().withType(ELASTICSEARCH); span.activate(); if (span.isSampled()) { span.getContext().getHttp().withMethod(method); - if (WildcardMatcher.isAnyMatch(config.getCaptureBodyUrls(), endpoint)) { + if (WildcardMatcher.isAnyMatch(config.getCaptureBodyUrls(), httpPath)) { if (httpEntity != null && httpEntity.isRepeatable()) { try { IOUtils.readUtf8Stream(httpEntity.getContent(), span.getContext().getDb().withStatementBuffer()); diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/AbstractEsClientInstrumentationTest.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/AbstractEsClientInstrumentationTest.java index 9f08f029a7..ae641992d6 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/AbstractEsClientInstrumentationTest.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/AbstractEsClientInstrumentationTest.java @@ -26,6 +26,9 @@ import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.testutils.TestContainersUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -34,8 +37,10 @@ import javax.annotation.Nullable; import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static co.elastic.apm.agent.esrestclient.ElasticsearchRestClientInstrumentationHelper.ELASTICSEARCH; import static co.elastic.apm.agent.esrestclient.ElasticsearchRestClientInstrumentationHelper.SPAN_ACTION; @@ -54,21 +59,9 @@ public abstract class AbstractEsClientInstrumentationTest extends AbstractInstru protected static final String FOO = "foo"; protected static final String BAR = "bar"; protected static final String BAZ = "baz"; - protected static final String SEARCH_QUERY_PATH_SUFFIX = "_search"; - protected static final String MSEARCH_QUERY_PATH_SUFFIX = "_msearch"; - protected static final String COUNT_QUERY_PATH_SUFFIX = "_count"; protected boolean async; - private boolean checkHttpUrl = true; - - /** - * Disables HTTP URL check for the current test method - */ - public void disableHttpUrlCheck() { - checkHttpUrl = false; - } - @Parameterized.Parameters(name = "Async={0}") public static Iterable data() { return Arrays.asList(new Object[][]{{Boolean.FALSE}, {Boolean.TRUE}}); @@ -89,12 +82,6 @@ public static void stopContainer() { @Before public void startTransaction() { - // While JUnit does not recycle test class instances between method invocations by default - // this test should not be required, but it allows to ensure proper correctness even if that changes - assertThat(checkHttpUrl) - .describedAs("checking HTTP URLs should be enabled by default") - .isTrue(); - startTestRootTransaction("ES Transaction"); } @@ -113,75 +100,185 @@ public void assertThatErrorsExistWhenDeleteNonExistingIndex() { assertThat(errorCapture.getException()).isNotNull(); } - protected void validateSpanContentWithoutContext(Span span, String expectedName) { - assertThat(span) - .hasType(SPAN_TYPE) - .hasSubType(ELASTICSEARCH) - .hasAction(SPAN_ACTION) - .hasName(expectedName); - - assertThat(span.getContext().getDb().getType()).isEqualTo(ELASTICSEARCH); - if (!expectedName.contains(SEARCH_QUERY_PATH_SUFFIX) && !expectedName.contains(MSEARCH_QUERY_PATH_SUFFIX) && !expectedName.contains(COUNT_QUERY_PATH_SUFFIX)) { - assertThat((CharSequence) (span.getContext().getDb().getStatementBuffer())).isNull(); - } + protected EsSpanValidationBuilder validateSpan(Span spanToValidate) { + return new EsSpanValidationBuilder(spanToValidate); } - protected void validateDbContextContent(Span span, String statement) { - validateDbContextContent(span, Collections.singletonList(statement)); + protected EsSpanValidationBuilder validateSpan() { + List spans = reporter.getSpans(); + assertThat(spans).hasSize(1); + return validateSpan(spans.get(0)); } - protected void validateDbContextContent(Span span, List possibleContents) { - Db db = span.getContext().getDb(); - assertThat(db.getType()).isEqualTo(ELASTICSEARCH); - assertThat((CharSequence) db.getStatementBuffer()).isNotNull(); + protected static class EsSpanValidationBuilder { - assertThat(db.getStatementBuffer().toString()).isIn(possibleContents); - } + private static final ObjectMapper jackson = new ObjectMapper(); - protected void validateSpanContent(Span span, String expectedName, int statusCode, String method) { - validateSpanContentWithoutContext(span, expectedName); - validateHttpContextContent(span.getContext().getHttp(), statusCode, method); - validateDestinationContextContent(span.getContext().getDestination()); + private final Span span; - assertThat(span.getContext().getServiceTarget()) - .hasType(ELASTICSEARCH) - .hasNoName() // we can't validate cluster name here as there is no simple way to inject that without reverse-proxy - .hasDestinationResource(ELASTICSEARCH); - } + private boolean statementExpectedNonNull = false; + + @Nullable + private JsonNode expectedStatement; + + @Nullable + private Map expectedPathParts; + + private int expectedStatusCode = 200; + + @Nullable + private String expectedHttpMethod; - private void validateDestinationContextContent(Destination destination) { - assertThat(destination).isNotNull(); - if (reporter.checkDestinationAddress()) { - assertThat(destination.getAddress().toString()).isEqualTo(container.getContainerIpAddress()); - assertThat(destination.getPort()).isEqualTo(container.getMappedPort(9200)); + @Nullable + private String expectedNameEndpoint; + + @Nullable + private String expectedNamePath; + + @Nullable + private String expectedHttpUrl = "http://" + container.getHttpHostAddress(); + + public EsSpanValidationBuilder(Span spanToValidate) { + this.span = spanToValidate; } - } - private void validateHttpContextContent(Http http, int statusCode, String method) { - assertThat(http).isNotNull(); - assertThat(http.getMethod()).isEqualTo(method); - assertThat(http.getStatusCode()).isEqualTo(statusCode); - if (checkHttpUrl) { - assertThat(http.getUrl().toString()).isEqualTo("http://" + container.getHttpHostAddress()); + public EsSpanValidationBuilder expectNoStatement() { + statementExpectedNonNull = false; + expectedStatement = null; + return this; } - } - protected void validateSpanContentAfterIndexCreateRequest() { - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - validateSpanContent(spans.get(0), String.format("Elasticsearch: PUT /%s", SECOND_INDEX), 200, "PUT"); - } + public EsSpanValidationBuilder expectAnyStatement() { + statementExpectedNonNull = true; + expectedStatement = null; + return this; + } - protected void validateSpanContentAfterIndexDeleteRequest() { - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - validateSpanContent(spans.get(0), String.format("Elasticsearch: DELETE /%s", SECOND_INDEX), 200, "DELETE"); - } + public EsSpanValidationBuilder expectStatement(String statement) { + try { + this.expectedStatement = jackson.readTree(statement); + statementExpectedNonNull = true; + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + return this; + } + + public EsSpanValidationBuilder expectPathPart(String key, String value) { + if (expectedPathParts == null) { + expectedPathParts = new HashMap<>(); + } + expectedPathParts.put(key, value); + return this; + } + + public EsSpanValidationBuilder expectNoPathParts() { + expectedPathParts = new HashMap<>(); + return this; + } + + public EsSpanValidationBuilder statusCode(int expectedStatusCode) { + this.expectedStatusCode = expectedStatusCode; + return this; + } + + public EsSpanValidationBuilder method(String httpMethod) { + this.expectedHttpMethod = httpMethod; + return this; + } + + public EsSpanValidationBuilder disableHttpUrlCheck() { + expectedHttpUrl = null; + return this; + } + + public EsSpanValidationBuilder endpointName(String endpoint) { + this.expectedNameEndpoint = endpoint; + this.expectedNamePath = null; + return this; + } + + public EsSpanValidationBuilder pathName(String pathFormat, Object... args) { + this.expectedNameEndpoint = null; + this.expectedNamePath = String.format(pathFormat, args); + return this; + } + + public void check() { + assertThat(span) + .hasType(SPAN_TYPE) + .hasSubType(ELASTICSEARCH) + .hasAction(SPAN_ACTION); + + if (expectedNameEndpoint != null) { + assertThat(span.getOtelAttributes()).containsEntry("db.operation", expectedNameEndpoint); + assertThat(span).hasName("Elasticsearch: " + expectedNameEndpoint); + } else if (expectedNamePath != null) { + assertThat(span).hasName("Elasticsearch: " + expectedHttpMethod + " " + expectedNamePath); + } + + checkHttpContext(); + checkDbContext(); + checkPathPartAttributes(); + checkDestinationContext(); + } + + + private void checkHttpContext() { + Http http = span.getContext().getHttp(); + assertThat(http).isNotNull(); + if (expectedHttpMethod != null) { + assertThat(http.getMethod()).isEqualTo(expectedHttpMethod); + } + assertThat(http.getStatusCode()).isEqualTo(expectedStatusCode); + if (expectedHttpUrl != null) { + assertThat(http.getUrl().toString()).isEqualTo(expectedHttpUrl); + } + } + + private void checkDbContext() { + Db db = span.getContext().getDb(); + assertThat(db.getType()).isEqualTo(ELASTICSEARCH); + CharSequence statement = db.getStatementBuffer(); + if (statementExpectedNonNull) { + assertThat(statement).isNotNull(); + if (expectedStatement != null) { + //Comparing JsonNodes ensures that the child-order within JSON objects does not matter + JsonNode parsedStatement; + try { + parsedStatement = jackson.readTree(statement.toString()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + assertThat(parsedStatement).isEqualTo(expectedStatement); + } + } else { + assertThat(statement).isNull(); + } + } + + private void checkPathPartAttributes() { + if (expectedPathParts != null) { + expectedPathParts.forEach((partName, value) -> { + assertThat(span.getOtelAttributes()).containsEntry("db.elasticsearch.path_parts." + partName, value); + }); + List spanPartAttributes = span.getOtelAttributes().keySet().stream() + .filter(name -> name.startsWith("db.elasticsearch.path_parts.")) + .map(name -> name.substring("db.elasticsearch.path_parts.".length())) + .collect(Collectors.toList()); + assertThat(spanPartAttributes).containsExactlyElementsOf(expectedPathParts.keySet()); + } + } + + private void checkDestinationContext() { + Destination destination = span.getContext().getDestination(); + assertThat(destination).isNotNull(); + if (reporter.checkDestinationAddress()) { + assertThat(destination.getAddress().toString()).isEqualTo(container.getContainerIpAddress()); + assertThat(destination.getPort()).isEqualTo(container.getMappedPort(9200)); + } + } - protected void validateSpanContentAfterBulkRequest() { - List spans = reporter.getSpans(); - assertThat(spans).hasSize(1); - assertThat(spans.get(0).getNameAsString()).isEqualTo("Elasticsearch: POST /_bulk"); } } diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMapTest.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMapTest.java new file mode 100644 index 0000000000..d99cb82f22 --- /dev/null +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-common/src/test/java/co/elastic/apm/agent/esrestclient/ElasticsearchEndpointMapTest.java @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.esrestclient; + +import co.elastic.apm.agent.tracer.Span; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + +public class ElasticsearchEndpointMapTest { + + private static final Set SEARCH_ENDPOINTS = + new HashSet<>( + Arrays.asList( + "search", + "async_search.submit", + "msearch", + "eql.search", + "terms_enum", + "search_template", + "msearch_template", + "render_search_template")); + + private static List getPathParts(String route) { + List pathParts = new ArrayList<>(); + String routeFragment = route; + int paramStartIndex = routeFragment.indexOf('{'); + while (paramStartIndex >= 0) { + int paramEndIndex = routeFragment.indexOf('}'); + if (paramEndIndex < 0 || paramEndIndex <= paramStartIndex + 1) { + throw new IllegalStateException("Invalid route syntax!"); + } + pathParts.add(routeFragment.substring(paramStartIndex + 1, paramEndIndex)); + + int nextIdx = paramEndIndex + 1; + if (nextIdx >= routeFragment.length()) { + break; + } + + routeFragment = routeFragment.substring(nextIdx); + paramStartIndex = routeFragment.indexOf('{'); + } + return pathParts; + } + + @Test + public void testIsSearchEndpoint() { + for (ElasticsearchEndpointDefinition esEndpointDefinition : + ElasticsearchEndpointMap.getAllEndpoints()) { + String endpointId = esEndpointDefinition.getEndpointName(); + assertEquals(SEARCH_ENDPOINTS.contains(endpointId), esEndpointDefinition.isSearchEndpoint()); + } + } + + @Test + public void testProcessPathParts() { + for (ElasticsearchEndpointDefinition esEndpointDefinition : + ElasticsearchEndpointMap.getAllEndpoints()) { + for (String route : + esEndpointDefinition.getRoutes().stream() + .map(ElasticsearchEndpointDefinition.Route::getName) + .collect(Collectors.toList())) { + List pathParts = getPathParts(route); + String resolvedRoute = route.replace("{", "").replace("}", ""); + Map observedParams = new HashMap<>(); + + Span dummy = Mockito.mock(Span.class); + doAnswer((invoc) -> observedParams.put(invoc.getArgument(0), invoc.getArgument(1))) + .when(dummy).withOtelAttribute(any(), any()); + esEndpointDefinition.addPathPartAttributes(resolvedRoute, dummy); + + Map expectedMap = new HashMap<>(); + pathParts.forEach(part -> expectedMap.put("db.elasticsearch.path_parts." + part, part)); + + assertEquals(expectedMap, observedParams); + } + } + } + + @Test + public void testSearchEndpoint() { + ElasticsearchEndpointDefinition esEndpoint = ElasticsearchEndpointMap.get("search"); + + Map observedParams = new HashMap<>(); + Span dummy = Mockito.mock(Span.class); + doAnswer((invoc) -> observedParams.put(invoc.getArgument(0), invoc.getArgument(1))) + .when(dummy).withOtelAttribute(any(), any()); + + esEndpoint.addPathPartAttributes("/test-index-1,test-index-2/_search", dummy); + + assertEquals("test-index-1,test-index-2", observedParams.get("db.elasticsearch.path_parts.index")); + } + + @Test + public void testBuildRegexPattern() { + Pattern pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_nodes/{node_id}/shutdown"); + assertEquals("^/_nodes/(?[^/]+)/shutdown$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_snapshot/{repository}/{snapshot}/_mount"); + assertEquals("^/_snapshot/(?[^/]+)/(?[^/]+)/_mount$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_security/profile/_suggest"); + assertEquals("^/_security/profile/_suggest$", pattern.pattern()); + + pattern = + ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern( + "/_application/search_application/{name}"); + assertEquals("^/_application/search_application/(?[^/]+)$", pattern.pattern()); + + pattern = ElasticsearchEndpointDefinition.EndpointPattern.buildRegexPattern("/"); + assertEquals("^/$", pattern.pattern()); + } + + +} diff --git a/apm-agent-plugins/apm-es-restclient-plugin/pom.xml b/apm-agent-plugins/apm-es-restclient-plugin/pom.xml index 2efcb84740..abfd0e7b0a 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/pom.xml +++ b/apm-agent-plugins/apm-es-restclient-plugin/pom.xml @@ -22,7 +22,7 @@ apm-es-restclient-plugin-5_6 apm-es-restclient-plugin-6_4 apm-es-restclient-plugin-7_x - apm-es-api-client-test + apm-es-restclient-plugin-8_x