diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 8a1bd07ba5f9..f5b06e5ecdcc 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -53,8 +53,9 @@ These are the supported libraries and frameworks: | [Eclipse Jetty HTTP Client](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/client/HttpClient.html) | 9.2+ (not including 10+ yet) | [opentelemetry-jetty-httpclient-9.2](../instrumentation/jetty-httpclient/jetty-httpclient-9.2/library) | [HTTP Client Spans], [HTTP Client Metrics] | | [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | | [Eclipse Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Elasticsearch API](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] | +| [Elasticsearch API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) | 7.16+ and 8.0+ | N/A | [Elasticsearch Client Spans] | | [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans] | +| [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans] | | [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] | | [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans] | | [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | @@ -141,6 +142,7 @@ These are the supported libraries and frameworks: **[3]** Controller Spans are `INTERNAL` spans capturing the controller and/or view execution. See [Suppressing controller and/or view spans](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#suppressing-controller-andor-view-spans). +[Elasticsearch Client Spans]: https://github.com/open-telemetry/semantic-conventions/blob/main/specification/database/elasticsearch.md [HTTP Server Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-server-semantic-conventions [HTTP Client Spans]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client [HTTP Server Metrics]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server diff --git a/instrumentation/elasticsearch/README.md b/instrumentation/elasticsearch/README.md index 0b78df613e25..18135dee857d 100644 --- a/instrumentation/elasticsearch/README.md +++ b/instrumentation/elasticsearch/README.md @@ -1,5 +1,12 @@ # Settings for the elasticsearch instrumentation +## Settings for the [Elasticsearch Java API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) instrumentation +| System property | Type | Default | Description | +|---|---|---|----------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.elasticsearch.capture-search-query` | `Boolean | `false` | Enable the capture of search query bodies. Attention: Elasticsearch queries may contain personal or sensitive information. | + + +## Settings for the [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) instrumentation | System property | Type | Default | Description | |---|---|---|---| | `otel.instrumentation.elasticsearch.experimental-span-attributes` | `Boolean | `false` | Enable the capture of experimental span attributes. | diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts new file mode 100644 index 000000000000..2eb2dd8c5a71 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + testImplementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent")) + testImplementation(project(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent")) +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java new file mode 100644 index 000000000000..1e639ac22638 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent-unit-tests/src/test/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointMapTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient.ElasticsearchEndpointMap; +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 org.junit.jupiter.api.Test; + +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<>(); + esEndpointDefinition.processPathParts(resolvedRoute, (k, v) -> observedParams.put(k, v)); + + Map expectedMap = new HashMap<>(); + pathParts.forEach(part -> expectedMap.put(part, part)); + + assertEquals(expectedMap, observedParams); + } + } + } + + @Test + public void testSearchEndpoint() { + ElasticsearchEndpointDefinition esEndpoint = ElasticsearchEndpointMap.get("search"); + Map observedParams = new HashMap<>(); + esEndpoint.processPathParts( + "/test-index-1,test-index-2/_search", (k, v) -> observedParams.put(k, v)); + + assertEquals("test-index-1,test-index-2", observedParams.get("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/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts new file mode 100644 index 000000000000..01e2416df29d --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("co.elastic.clients") + module.set("elasticsearch-java") + versions.set("[7.16,)") + assertInverse.set(true) + } +} + +dependencies { + library("co.elastic.clients:elasticsearch-java:7.16.0") + + implementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent")) + + testInstrumentation(project(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent")) + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent")) + testInstrumentation(project(":instrumentation:apache-httpasyncclient-4.1:javaagent")) + + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + testImplementation("org.testcontainers:elasticsearch") +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ApiClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ApiClientInstrumentation.java new file mode 100644 index 000000000000..9388a92d0387 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ApiClientInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import co.elastic.clients.transport.Endpoint; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchEndpointDefinition; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.elasticsearch.client.Request; + +public class ApiClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("co.elastic.clients.transport.rest_client.RestClientTransport"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(named("prepareLowLevelRequest")) + .and(takesArgument(1, named("co.elastic.clients.transport.Endpoint"))) + .and(returns(named("org.elasticsearch.client.Request"))), + this.getClass().getName() + "$RestClientTransportAdvice"); + } + + @SuppressWarnings("unused") + public static class RestClientTransportAdvice { + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onPrepareLowLevelRequest( + @Advice.Argument(1) Endpoint endpoint, @Advice.Return Request request) { + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + String endpointId = endpoint.id(); + if (endpointId.startsWith("es/") && endpointId.length() > 3) { + endpointId = endpointId.substring(3); + } + virtualField.set(request, ElasticsearchEndpointMap.get(endpointId)); + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java new file mode 100644 index 000000000000..b8e2cb866ba0 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchApiClientInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class ElasticsearchApiClientInstrumentationModule extends InstrumentationModule { + public ElasticsearchApiClientInstrumentationModule() { + super("elasticsearch-api-client-7.16", "elasticsearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new ApiClientInstrumentation()); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java new file mode 100644 index 000000000000..49096b7a5f97 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/apiclient/ElasticsearchEndpointMap.java @@ -0,0 +1,883 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient; + +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchEndpointDefinition; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; + +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(String endpointId) { + return routesMap.get(endpointId); + } + + public static Collection getAllEndpoints() { + return routesMap.values(); + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/ElasticsearchClientTest.java b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/ElasticsearchClientTest.java new file mode 100644 index 000000000000..19ce31686b77 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-api-client-7.16/javaagent/src/test/java/ElasticsearchClientTest.java @@ -0,0 +1,252 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.InfoResponse; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.Version; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpHost; +import org.assertj.core.api.AbstractLongAssert; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.elasticsearch.ElasticsearchContainer; + +class ElasticsearchClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static ElasticsearchContainer elasticsearch; + + static HttpHost httpHost; + + static ElasticsearchClient client; + static ElasticsearchAsyncClient asyncClient; + + @BeforeAll + static void setUp() { + elasticsearch = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.2"); + // limit memory usage + elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m"); + elasticsearch.start(); + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()); + + RestClient restClient = + RestClient.builder(httpHost) + .setRequestConfigCallback( + builder -> + builder + .setConnectTimeout(Integer.MAX_VALUE) + .setSocketTimeout(Integer.MAX_VALUE)) + .build(); + + ElasticsearchTransport transport = + new RestClientTransport(restClient, new JacksonJsonpMapper()); + client = new ElasticsearchClient(transport); + asyncClient = new ElasticsearchAsyncClient(transport); + } + + @AfterAll + static void cleanUp() { + elasticsearch.stop(); + } + + private static String userAgent() { + return "elastic-java/" + Version.VERSION + " (Java/" + System.getProperty("java.version") + ")"; + } + + @Test + public void elasticsearchStatus() throws IOException { + InfoResponse response = client.info(); + Assertions.assertEquals(response.version().number(), "7.17.2"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("info") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(SemanticAttributes.DB_OPERATION, "info"), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort())), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), + equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), + equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/"), + equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), + equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, userAgent()), + satisfies( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + AbstractLongAssert::isPositive)))); + } + + @Test + public void elasticsearchIndex() throws IOException { + client.index( + r -> + r.id("test-id") + .index("test-index") + .document(new Person("person-name")) + .timeout(t -> t.time("10s"))); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("index") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(SemanticAttributes.DB_OPERATION, "index"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "PUT"), + equalTo( + SemanticAttributes.HTTP_URL, + httpHost.toURI() + "/test-index/_doc/test-id?timeout=10s"), + equalTo( + AttributeKey.stringKey("db.elasticsearch.path_parts.index"), + "test-index"), + equalTo( + AttributeKey.stringKey("db.elasticsearch.path_parts.id"), + "test-id")), + span -> + span.hasName("PUT") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "PUT"), + equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), + equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), + equalTo( + SemanticAttributes.HTTP_URL, + httpHost.toURI() + "/test-index/_doc/test-id?timeout=10s"), + equalTo(SemanticAttributes.HTTP_STATUS_CODE, 201L), + equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, userAgent()), + satisfies( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + AbstractLongAssert::isPositive)))); + } + + @Test + public void elasticsearchStatusAsync() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + AsyncRequest request = new AsyncRequest(); + + runWithSpan( + "parent", + () -> + asyncClient + .info() + .thenAccept( + infoResponse -> + runWithSpan( + "callback", + () -> { + request.setResponse(infoResponse); + countDownLatch.countDown(); + }))); + //noinspection ResultOfMethodCallIgnored + countDownLatch.await(10, TimeUnit.SECONDS); + + Assertions.assertEquals(request.getResponse().version().number(), "7.17.2"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("info") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(SemanticAttributes.DB_OPERATION, "info"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), + equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), + equalTo(SemanticAttributes.HTTP_URL, httpHost.toURI() + "/"), + equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), + equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, userAgent()), + satisfies( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + AbstractLongAssert::isPositive)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class AsyncRequest { + volatile InfoResponse response = null; + + public InfoResponse getResponse() { + return response; + } + + public void setResponse(InfoResponse response) { + this.response = response; + } + } + + private static class Person { + public final String name; + + Person(String name) { + this.name = name; + } + + @SuppressWarnings("unused") + public String getName() { + return name; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java index 138ed7260b94..72d9e51368d8 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-5.0/javaagent/src/test/java/ElasticsearchRest5Test.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.testcontainers.elasticsearch.ElasticsearchContainer; -public class ElasticsearchRest5Test { +class ElasticsearchRest5Test { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @@ -92,8 +92,11 @@ void elasticsearchStatus() throws IOException { .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") @@ -170,8 +173,11 @@ public void onFailure(Exception e) { .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") diff --git a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java index 178a2351235f..e1120bedaa2c 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-6.4/javaagent/src/test/java/ElasticsearchRest6Test.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.testcontainers.elasticsearch.ElasticsearchContainer; -public class ElasticsearchRest6Test { +class ElasticsearchRest6Test { @RegisterExtension static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); @@ -83,8 +83,11 @@ public void elasticsearchStatus() throws IOException { .hasNoParent() .hasAttributesSatisfyingExactly( equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") @@ -160,8 +163,11 @@ public void onFailure(Exception e) { .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), - equalTo(SemanticAttributes.DB_OPERATION, "GET"), - equalTo(SemanticAttributes.DB_STATEMENT, "GET _cluster/health")); + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health")); }, span -> { span.hasName("GET") diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts index 6e04bbed3905..e6d7dfc9c71b 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { testImplementation("org.apache.logging.log4j:log4j-core:2.11.0") testImplementation("org.apache.logging.log4j:log4j-api:2.11.0") + testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("org.testcontainers:elasticsearch") } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java index bc51e5757636..5143c0151b8e 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/v7_0/RestClientInstrumentation.java @@ -14,8 +14,10 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchEndpointDefinition; import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest; import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener; import net.bytebuddy.asm.Advice; @@ -59,7 +61,14 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - otelRequest = ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + otelRequest = + ElasticsearchRestRequest.create( + request.getMethod(), + request.getEndpoint(), + virtualField.get(request), + request.getEntity()); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } @@ -97,7 +106,15 @@ public static void onEnter( @Advice.Local("otelScope") Scope scope) { Context parentContext = currentContext(); - otelRequest = ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint()); + VirtualField virtualField = + VirtualField.find(Request.class, ElasticsearchEndpointDefinition.class); + + otelRequest = + ElasticsearchRestRequest.create( + request.getMethod(), + request.getEndpoint(), + virtualField.get(request), + request.getEntity()); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return; } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy deleted file mode 100644 index 299795ddf1cb..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/groovy/ElasticsearchRest7Test.groovy +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import groovy.json.JsonSlurper -import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes -import org.apache.http.HttpHost -import org.apache.http.client.config.RequestConfig -import org.apache.http.util.EntityUtils -import org.elasticsearch.client.Request -import org.elasticsearch.client.Response -import org.elasticsearch.client.ResponseListener -import org.elasticsearch.client.RestClient -import org.elasticsearch.client.RestClientBuilder -import org.testcontainers.elasticsearch.ElasticsearchContainer -import spock.lang.Shared - -import java.util.concurrent.CountDownLatch - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.INTERNAL - -class ElasticsearchRest7Test extends AgentInstrumentationSpecification { - @Shared - ElasticsearchContainer elasticsearch - - @Shared - HttpHost httpHost - - @Shared - RestClient client - - def setupSpec() { - elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2") - // limit memory usage - elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m") - elasticsearch.start() - - httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()) - client = RestClient.builder(httpHost) - .setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { - @Override - RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder builder) { - return builder.setConnectTimeout(Integer.MAX_VALUE).setSocketTimeout(Integer.MAX_VALUE) - } - }) - .build() - } - - def cleanupSpec() { - elasticsearch.stop() - } - - def "test elasticsearch status"() { - setup: - Response response = client.performRequest(new Request("GET", "_cluster/health")) - - Map result = new JsonSlurper().parseText(EntityUtils.toString(response.entity)) - - expect: - result.status == "green" - - assertTraces(1) { - trace(0, 2) { - span(0) { - name "GET" - kind CLIENT - hasNoParent() - attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GET" - "$SemanticAttributes.DB_STATEMENT" "GET _cluster/health" - } - } - span(1) { - name "GET" - kind CLIENT - childOf span(0) - attributes { - "$SemanticAttributes.NET_PEER_NAME" httpHost.hostName - "$SemanticAttributes.NET_PEER_PORT" httpHost.port - "$SemanticAttributes.HTTP_METHOD" "GET" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_URL" "${httpHost.toURI()}/_cluster/health" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - } - } - } - - def "test elasticsearch status async"() { - setup: - Response requestResponse = null - Exception exception = null - CountDownLatch countDownLatch = new CountDownLatch(1) - ResponseListener responseListener = new ResponseListener() { - @Override - void onSuccess(Response response) { - runWithSpan("callback") { - requestResponse = response - countDownLatch.countDown() - } - } - - @Override - void onFailure(Exception e) { - runWithSpan("callback") { - exception = e - countDownLatch.countDown() - } - } - } - runWithSpan("parent") { - client.performRequestAsync(new Request("GET", "_cluster/health"), responseListener) - } - countDownLatch.await() - - if (exception != null) { - throw exception - } - Map result = new JsonSlurper().parseText(EntityUtils.toString(requestResponse.entity)) - - expect: - result.status == "green" - - assertTraces(1) { - trace(0, 4) { - span(0) { - name "parent" - kind INTERNAL - hasNoParent() - } - span(1) { - name "GET" - kind CLIENT - childOf(span(0)) - attributes { - "$SemanticAttributes.DB_SYSTEM" "elasticsearch" - "$SemanticAttributes.DB_OPERATION" "GET" - "$SemanticAttributes.DB_STATEMENT" "GET _cluster/health" - } - } - span(2) { - name "GET" - kind CLIENT - childOf span(1) - attributes { - "$SemanticAttributes.NET_PEER_NAME" httpHost.hostName - "$SemanticAttributes.NET_PEER_PORT" httpHost.port - "$SemanticAttributes.HTTP_METHOD" "GET" - "net.protocol.name" "http" - "net.protocol.version" "1.1" - "$SemanticAttributes.HTTP_URL" "${httpHost.toURI()}/_cluster/health" - "$SemanticAttributes.HTTP_STATUS_CODE" 200 - "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long - } - } - span(3) { - name "callback" - kind INTERNAL - childOf(span(0)) - } - } - } - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/ElasticsearchRest7Test.java b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/ElasticsearchRest7Test.java new file mode 100644 index 000000000000..535f32b84022 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-7.0/javaagent/src/test/java/ElasticsearchRest7Test.java @@ -0,0 +1,211 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.http.HttpHost; +import org.assertj.core.api.AbstractLongAssert; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +class ElasticsearchRest7Test { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static ElasticsearchContainer elasticsearch; + + static HttpHost httpHost; + + static RestClient client; + + static ObjectMapper objectMapper; + + @BeforeAll + static void setUp() { + elasticsearch = + new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2"); + // limit memory usage + elasticsearch.withEnv("ES_JAVA_OPTS", "-Xmx256m -Xms256m"); + elasticsearch.start(); + + httpHost = HttpHost.create(elasticsearch.getHttpHostAddress()); + + client = + RestClient.builder(httpHost) + .setRequestConfigCallback( + builder -> + builder + .setConnectTimeout(Integer.MAX_VALUE) + .setSocketTimeout(Integer.MAX_VALUE)) + .build(); + + objectMapper = new ObjectMapper(); + } + + @AfterAll + static void cleanUp() { + elasticsearch.stop(); + } + + @Test + public void elasticsearchStatus() throws Exception { + Response response = client.performRequest(new Request("GET", "_cluster/health")); + Map result = objectMapper.readValue(response.getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, + httpHost.toURI() + "/_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), + equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), + equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), + satisfies( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + AbstractLongAssert::isPositive)))); + } + + @Test + public void elasticsearchStatusAsync() throws Exception { + AsyncRequest asyncRequest = new AsyncRequest(); + CountDownLatch countDownLatch = new CountDownLatch(1); + ResponseListener responseListener = + new ResponseListener() { + @Override + public void onSuccess(Response response) { + + runWithSpan( + "callback", + () -> { + asyncRequest.setRequestResponse(response); + countDownLatch.countDown(); + }); + } + + @Override + public void onFailure(Exception e) { + runWithSpan( + "callback", + () -> { + asyncRequest.setException(e); + countDownLatch.countDown(); + }); + } + }; + + runWithSpan( + "parent", + () -> client.performRequestAsync(new Request("GET", "_cluster/health"), responseListener)); + //noinspection ResultOfMethodCallIgnored + countDownLatch.await(10, TimeUnit.SECONDS); + + if (asyncRequest.getException() != null) { + throw asyncRequest.getException(); + } + + Map result = + objectMapper.readValue( + asyncRequest.getRequestResponse().getEntity().getContent(), Map.class); + Assertions.assertEquals(result.get("status"), "green"); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.DB_SYSTEM, "elasticsearch"), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo( + SemanticAttributes.HTTP_URL, + httpHost.toURI() + "/_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.NET_PEER_NAME, httpHost.getHostName()), + equalTo(SemanticAttributes.NET_PEER_PORT, httpHost.getPort()), + equalTo(SemanticAttributes.HTTP_METHOD, "GET"), + equalTo(AttributeKey.stringKey("net.protocol.name"), "http"), + equalTo(AttributeKey.stringKey("net.protocol.version"), "1.1"), + equalTo( + SemanticAttributes.HTTP_URL, httpHost.toURI() + "/_cluster/health"), + equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200L), + satisfies( + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, + AbstractLongAssert::isPositive)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + private static class AsyncRequest { + volatile Response requestResponse = null; + volatile Exception exception = null; + + public Response getRequestResponse() { + return requestResponse; + } + + public void setRequestResponse(Response requestResponse) { + this.requestResponse = requestResponse; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchClientAttributeExtractor.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchClientAttributeExtractor.java new file mode 100644 index 000000000000..87d4724d9141 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchClientAttributeExtractor.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.network.internal.NetworkAttributes; +import io.opentelemetry.instrumentation.api.instrumenter.url.internal.UrlAttributes; +import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import io.opentelemetry.instrumentation.api.internal.cache.Cache; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import javax.annotation.Nullable; +import org.apache.http.HttpHost; +import org.elasticsearch.client.Response; + +public class ElasticsearchClientAttributeExtractor + implements AttributesExtractor { + + private static final String PATH_PARTS_ATTRIBUTE_PREFIX = "db.elasticsearch.path_parts."; + + private static final Cache> pathPartKeysCache = Cache.bounded(64); + + private static void setServerAttributes(AttributesBuilder attributes, Response response) { + HttpHost host = response.getHost(); + if (host != null) { + if (SemconvStability.emitStableHttpSemconv()) { + internalSet(attributes, NetworkAttributes.SERVER_ADDRESS, host.getHostName()); + internalSet(attributes, NetworkAttributes.SERVER_PORT, (long) host.getPort()); + } + if (SemconvStability.emitOldHttpSemconv()) { + internalSet(attributes, SemanticAttributes.NET_PEER_NAME, host.getHostName()); + internalSet(attributes, SemanticAttributes.NET_PEER_PORT, (long) host.getPort()); + } + } + } + + private static void setUrlAttribute(AttributesBuilder attributes, Response response) { + String uri = response.getRequestLine().getUri(); + uri = uri.startsWith("/") ? uri : "/" + uri; + String fullUrl = response.getHost().toURI() + uri; + + if (SemconvStability.emitStableHttpSemconv()) { + internalSet(attributes, UrlAttributes.URL_FULL, fullUrl); + } + + if (SemconvStability.emitOldHttpSemconv()) { + internalSet(attributes, SemanticAttributes.HTTP_URL, fullUrl); + } + } + + private static void setPathPartsAttributes( + AttributesBuilder attributes, ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition endpointDef = request.getEndpointDefinition(); + if (endpointDef == null) { + return; + } + + endpointDef.processPathParts( + request.getEndpoint(), + (key, value) -> { + AttributeKey attributeKey = + pathPartKeysCache.computeIfAbsent( + key, k -> AttributeKey.stringKey(PATH_PARTS_ATTRIBUTE_PREFIX + k)); + internalSet(attributes, attributeKey, value); + }); + } + + @Override + public void onStart( + AttributesBuilder attributes, Context parentContext, ElasticsearchRestRequest request) { + internalSet(attributes, SemanticAttributes.HTTP_METHOD, request.getMethod()); + setPathPartsAttributes(attributes, request); + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + ElasticsearchRestRequest request, + @Nullable Response response, + @Nullable Throwable error) { + if (response != null) { + + setUrlAttribute(attributes, response); + setServerAttributes(attributes, response); + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchDbAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchDbAttributesGetter.java new file mode 100644 index 000000000000..39e324b82548 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchDbAttributesGetter.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static java.util.logging.Level.FINE; + +import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; +import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.http.HttpEntity; + +final class ElasticsearchDbAttributesGetter + implements DbClientAttributesGetter { + + private static final boolean CAPTURE_SEARCH_QUERY = + InstrumentationConfig.get() + .getBoolean("otel.instrumentation.elasticsearch.capture-search-query", false); + + private static final Logger logger = + Logger.getLogger(ElasticsearchDbAttributesGetter.class.getName()); + + @Override + public String getSystem(ElasticsearchRestRequest request) { + return SemanticAttributes.DbSystemValues.ELASTICSEARCH; + } + + @Override + @Nullable + public String getUser(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getName(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getConnectionString(ElasticsearchRestRequest request) { + return null; + } + + @Override + @Nullable + public String getStatement(ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition epDefinition = request.getEndpointDefinition(); + HttpEntity httpEntity = request.getHttpEntity(); + if (CAPTURE_SEARCH_QUERY + && epDefinition != null + && epDefinition.isSearchEndpoint() + && httpEntity != null + && httpEntity.isRepeatable()) { + // Retrieve HTTP body for search-type Elasticsearch requests when CAPTURE_SEARCH_QUERY is + // enabled. + try { + return new BufferedReader( + new InputStreamReader(httpEntity.getContent(), StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining()); + } catch (IOException e) { + logger.log(FINE, "Failed reading HTTP body content.", e); + } + } + return null; + } + + @Override + @Nullable + public String getOperation(ElasticsearchRestRequest request) { + ElasticsearchEndpointDefinition endpointDefinition = request.getEndpointDefinition(); + return endpointDefinition != null ? endpointDefinition.getEndpointName() : null; + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointDefinition.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointDefinition.java new file mode 100644 index 000000000000..3f26d19fc065 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchEndpointDefinition.java @@ -0,0 +1,177 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import static java.util.Collections.unmodifiableList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +public final class ElasticsearchEndpointDefinition { + + 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 = + unmodifiableList(Arrays.stream(routes).map(Route::new).collect(Collectors.toList())); + this.isSearchEndpoint = isSearchEndpoint; + } + + @Nullable + public String getEndpointName() { + return endpointName; + } + + public boolean isSearchEndpoint() { + return isSearchEndpoint; + } + + public void processPathParts(String urlPath, BiConsumer consumer) { + for (Route route : routes) { + if (route.hasParameters()) { + Matcher matcher = route.createMatcher(urlPath); + if (matcher.find()) { + for (String key : route.getPathPartNames()) { + String value = matcher.group(key); + if (key.contains(UNDERSCORE_REPLACEMENT)) { + // replace underscore back + key = key.replace(UNDERSCORE_REPLACEMENT, "_"); + } + consumer.accept(key, 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; + } + + List getPathPartNames() { + return getEndpointPattern().getPathPartNames(); + } + + Matcher createMatcher(String urlPath) { + return getEndpointPattern().getPattern().matcher(urlPath); + } + + 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 List pathPartNames; + + /** + * 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()) { + pathPartNames = new ArrayList<>(); + Matcher matcher = PATH_PART_NAMES_PATTERN.matcher(route.getName()); + while (matcher.find()) { + String groupName = matcher.group(1); + + if (groupName != null) { + groupName = groupName.replace("_", UNDERSCORE_REPLACEMENT); + pathPartNames.add(groupName); + } + } + } else { + pathPartNames = Collections.emptyList(); + } + } + + /** 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()); + } + + Pattern getPattern() { + return pattern; + } + + List getPathPartNames() { + return pathPartNames; + } + } +} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java deleted file mode 100644 index e6ae243108c8..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestAttributesGetter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesGetter; -import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; -import javax.annotation.Nullable; - -final class ElasticsearchRestAttributesGetter - implements DbClientAttributesGetter { - - @Override - public String getSystem(ElasticsearchRestRequest request) { - return SemanticAttributes.DbSystemValues.ELASTICSEARCH; - } - - @Override - @Nullable - public String getUser(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getName(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getConnectionString(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public String getStatement(ElasticsearchRestRequest request) { - return request.getMethod() + " " + request.getOperation(); - } - - @Override - @Nullable - public String getOperation(ElasticsearchRestRequest request) { - return request.getMethod(); - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java index e624a3a2d388..4a06e099f07b 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestInstrumenterFactory.java @@ -9,32 +9,25 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; -import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; import org.elasticsearch.client.Response; public final class ElasticsearchRestInstrumenterFactory { + private ElasticsearchRestInstrumenterFactory() {} + public static Instrumenter create( String instrumentationName) { - ElasticsearchRestAttributesGetter dbClientAttributesGetter = - new ElasticsearchRestAttributesGetter(); - ElasticsearchRestNetResponseAttributesGetter netAttributesGetter = - new ElasticsearchRestNetResponseAttributesGetter(); + ElasticsearchDbAttributesGetter dbClientAttributesGetter = + new ElasticsearchDbAttributesGetter(); + ElasticsearchClientAttributeExtractor esClientAtrributesExtractor = + new ElasticsearchClientAttributeExtractor(); + ElasticsearchSpanNameExtractor nameExtractor = + new ElasticsearchSpanNameExtractor(dbClientAttributesGetter); return Instrumenter.builder( - GlobalOpenTelemetry.get(), - instrumentationName, - DbClientSpanNameExtractor.create(dbClientAttributesGetter)) + GlobalOpenTelemetry.get(), instrumentationName, nameExtractor) .addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter)) - .addAttributesExtractor(NetClientAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, CommonConfig.get().getPeerServiceMapping())) + .addAttributesExtractor(esClientAtrributesExtractor) .buildInstrumenter(SpanKindExtractor.alwaysClient()); } - - private ElasticsearchRestInstrumenterFactory() {} } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java deleted file mode 100644 index 7716a2b8562c..000000000000 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestNetResponseAttributesGetter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; - -import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; -import java.net.Inet6Address; -import javax.annotation.Nullable; -import org.elasticsearch.client.Response; - -final class ElasticsearchRestNetResponseAttributesGetter - implements NetClientAttributesGetter { - - @Override - @Nullable - public String getServerAddress(ElasticsearchRestRequest request) { - return null; - } - - @Override - @Nullable - public Integer getServerPort(ElasticsearchRestRequest request) { - return null; - } - - @Nullable - @Override - public String getSockFamily( - ElasticsearchRestRequest elasticsearchRestRequest, @Nullable Response response) { - if (response != null && response.getHost().getAddress() instanceof Inet6Address) { - return "inet6"; - } - return null; - } - - @Override - @Nullable - public String getServerSocketAddress( - ElasticsearchRestRequest request, @Nullable Response response) { - if (response != null && response.getHost().getAddress() != null) { - return response.getHost().getAddress().getHostAddress(); - } - return null; - } -} diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java index 3298c1a40dc7..1debc037d775 100644 --- a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchRestRequest.java @@ -6,15 +6,31 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; +import org.apache.http.HttpEntity; @AutoValue public abstract class ElasticsearchRestRequest { public static ElasticsearchRestRequest create(String method, String endpoint) { - return new AutoValue_ElasticsearchRestRequest(method, endpoint); + return create(method, endpoint, null, null); + } + + public static ElasticsearchRestRequest create( + String method, + String endpoint, + @Nullable ElasticsearchEndpointDefinition endpointDefinition, + @Nullable HttpEntity httpEntity) { + return new AutoValue_ElasticsearchRestRequest(method, endpoint, endpointDefinition, httpEntity); } public abstract String getMethod(); - public abstract String getOperation(); + public abstract String getEndpoint(); + + @Nullable + public abstract ElasticsearchEndpointDefinition getEndpointDefinition(); + + @Nullable + public abstract HttpEntity getHttpEntity(); } diff --git a/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchSpanNameExtractor.java b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchSpanNameExtractor.java new file mode 100644 index 000000000000..152e3e21b428 --- /dev/null +++ b/instrumentation/elasticsearch/elasticsearch-rest-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/rest/ElasticsearchSpanNameExtractor.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest; + +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +public class ElasticsearchSpanNameExtractor implements SpanNameExtractor { + + private final ElasticsearchDbAttributesGetter dbAttributesGetter; + + public ElasticsearchSpanNameExtractor(ElasticsearchDbAttributesGetter dbAttributesGetter) { + this.dbAttributesGetter = dbAttributesGetter; + } + + @Override + public String extract(ElasticsearchRestRequest elasticsearchRestRequest) { + String name = dbAttributesGetter.getOperation(elasticsearchRestRequest); + return name != null ? name : elasticsearchRestRequest.getMethod(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e376d02ed66e..69b88da49833 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -223,6 +223,8 @@ hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-commo hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-5.0:javaagent") hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-5.3:javaagent") hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-6.0:javaagent") +hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent") +hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent-unit-tests") hideFromDependabot(":instrumentation:executors:bootstrap") hideFromDependabot(":instrumentation:executors:javaagent") hideFromDependabot(":instrumentation:executors:testing")