Add library instrumentation for elasticsearch rest client 7 (#8911)

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
Lauri Tulmin 2023-08-01 17:17:05 +03:00 committed by GitHub
parent 6892c64928
commit fbae980fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 642 additions and 50 deletions

View File

@ -7,6 +7,7 @@ package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient.ElasticsearchEndpointMap;
import java.util.ArrayList;
import java.util.Arrays;

View File

@ -5,7 +5,7 @@
package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchEndpointDefinition;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;

View File

@ -14,9 +14,9 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition;
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;

View File

@ -12,9 +12,9 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import co.elastic.clients.transport.Endpoint;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition;
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;

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.apiclient;
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;

View File

@ -6,14 +6,15 @@
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory;
import org.elasticsearch.client.Response;
public final class ElasticsearchRest5Singletons {
private static final Instrumenter<ElasticsearchRestRequest, Response> INSTRUMENTER =
ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-5.0");
ElasticsearchRestJavaagentInstrumenterFactory.create(
"io.opentelemetry.elasticsearch-rest-5.0");
public static Instrumenter<ElasticsearchRestRequest, Response> instrumenter() {
return INSTRUMENTER;

View File

@ -15,10 +15,10 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v5_0;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -6,14 +6,15 @@
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory;
import org.elasticsearch.client.Response;
public final class ElasticsearchRest6Singletons {
private static final Instrumenter<ElasticsearchRestRequest, Response> INSTRUMENTER =
ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-6.4");
ElasticsearchRestJavaagentInstrumenterFactory.create(
"io.opentelemetry.elasticsearch-rest-6.4");
public static Instrumenter<ElasticsearchRestRequest, Response> instrumenter() {
return INSTRUMENTER;

View File

@ -14,10 +14,10 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.RestResponseListener;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v6_4;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -28,10 +28,7 @@ dependencies {
// Netty is used, but it adds complexity to the tests since we're using embedded ES.
// testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
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")
}

View File

@ -6,14 +6,15 @@
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_0;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestInstrumenterFactory;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.ElasticsearchRestJavaagentInstrumenterFactory;
import org.elasticsearch.client.Response;
public final class ElasticsearchRest7Singletons {
private static final Instrumenter<ElasticsearchRestRequest, Response> INSTRUMENTER =
ElasticsearchRestInstrumenterFactory.create("io.opentelemetry.elasticsearch-rest-7.0");
ElasticsearchRestJavaagentInstrumenterFactory.create(
"io.opentelemetry.elasticsearch-rest-7.0");
public static Instrumenter<ElasticsearchRestRequest, Response> instrumenter() {
return INSTRUMENTER;

View File

@ -15,11 +15,11 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.util.VirtualField;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchEndpointDefinition;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener;
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;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@ -67,6 +67,7 @@ public class RestClientInstrumentation implements TypeInstrumentation {
ElasticsearchRestRequest.create(
request.getMethod(),
request.getEndpoint(),
// set by elasticsearch-api-client instrumentation
virtualField.get(request),
request.getEntity());
if (!instrumenter().shouldStart(parentContext, otelRequest)) {
@ -113,6 +114,7 @@ public class RestClientInstrumentation implements TypeInstrumentation {
ElasticsearchRestRequest.create(
request.getMethod(),
request.getEndpoint(),
// set by elasticsearch-api-client instrumentation
virtualField.get(request),
request.getEntity());
if (!instrumenter().shouldStart(parentContext, otelRequest)) {

View File

@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest.v7_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;

View File

@ -0,0 +1,18 @@
plugins {
id("otel.library-instrumentation")
}
dependencies {
library("org.elasticsearch.client:elasticsearch-rest-client:7.0.0")
implementation("net.bytebuddy:byte-buddy")
implementation(project(":instrumentation:elasticsearch:elasticsearch-rest-common:library"))
testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("org.testcontainers:elasticsearch")
}
tasks {
test {
usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;
/** Entrypoint for instrumenting Apache Elasticsearch Rest clients. */
public final class ElasticsearchRest7Telemetry {
/**
* Returns a new {@link ElasticsearchRest7Telemetry} configured with the given {@link
* OpenTelemetry}.
*/
public static ElasticsearchRest7Telemetry create(OpenTelemetry openTelemetry) {
return builder(openTelemetry).build();
}
/**
* Returns a new {@link ElasticsearchRest7TelemetryBuilder} configured with the given {@link
* OpenTelemetry}.
*/
public static ElasticsearchRest7TelemetryBuilder builder(OpenTelemetry openTelemetry) {
return new ElasticsearchRest7TelemetryBuilder(openTelemetry);
}
private final Instrumenter<ElasticsearchRestRequest, Response> instrumenter;
ElasticsearchRest7Telemetry(Instrumenter<ElasticsearchRestRequest, Response> instrumenter) {
this.instrumenter = instrumenter;
}
/**
* Construct a new tracing-enable {@link RestClient} using the provided {@link RestClient}
* instance.
*/
public RestClient wrap(RestClient restClient) {
return RestClientWrapper.wrap(restClient, instrumenter);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestInstrumenterFactory;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import java.util.ArrayList;
import java.util.List;
import org.elasticsearch.client.Response;
public final class ElasticsearchRest7TelemetryBuilder {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.elasticsearch-rest-7.0";
private final OpenTelemetry openTelemetry;
private final List<AttributesExtractor<ElasticsearchRestRequest, Response>> attributesExtractors =
new ArrayList<>();
ElasticsearchRest7TelemetryBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/**
* Adds an additional {@link AttributesExtractor} to invoke to set attributes to instrumented
* items.
*/
@CanIgnoreReturnValue
public ElasticsearchRest7TelemetryBuilder addAttributesExtractor(
AttributesExtractor<ElasticsearchRestRequest, Response> attributesExtractor) {
attributesExtractors.add(attributesExtractor);
return this;
}
/**
* Returns a new {@link ElasticsearchRest7Telemetry} with the settings of this {@link
* ElasticsearchRest7TelemetryBuilder}.
*/
public ElasticsearchRest7Telemetry build() {
Instrumenter<ElasticsearchRestRequest, Response> instrumenter =
ElasticsearchRestInstrumenterFactory.create(
openTelemetry, INSTRUMENTATION_NAME, attributesExtractors, false);
return new ElasticsearchRest7Telemetry(instrumenter);
}
}

View File

@ -0,0 +1,198 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.RestResponseListener;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.http.Header;
import org.elasticsearch.client.Node;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseListener;
import org.elasticsearch.client.RestClient;
class RestClientWrapper {
private static final Class<?> proxyClass = createProxyClass();
private static final Field targetField = getTargetField(proxyClass);
private static final Field instrumenterSupplierField = getInstrumenterSupplierField(proxyClass);
private static final Function<RestClient, RestClient> proxyFactory = getProxyFactory(proxyClass);
private static Class<?> createProxyClass() {
return new ByteBuddy()
.subclass(RestClient.class)
.defineField("target", RestClient.class, Visibility.PUBLIC)
// using Supplier instead of Instrumenter in case RestClientWrapper and opentelemetry apis
// are in a child class loader of RestClient's class loader and Instrumenter is not visible
// for RestClient
.defineField("instrumenterSupplier", Supplier.class, Visibility.PUBLIC)
.method(ElementMatchers.any())
.intercept(
InvocationHandlerAdapter.of(
(proxy, method, args) -> {
RestClient target = (RestClient) targetField.get(proxy);
Instrumenter<ElasticsearchRestRequest, Response> instrumenter =
getInstrumenter(proxy);
// target is null when running proxy constructor
if (target == null || instrumenter == null) {
return null;
}
// instrument performRequest and performRequestAsync methods
if ("performRequest".equals(method.getName())
&& args.length == 1
&& args[0] instanceof Request
&& Response.class == method.getReturnType()) {
Request request = (Request) args[0];
Context parentContext = Context.current();
ElasticsearchRestRequest otelRequest =
ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint());
if (!instrumenter.shouldStart(parentContext, otelRequest)) {
return method.invoke(target, args);
}
Response response = null;
Throwable throwable = null;
Context context = instrumenter.start(parentContext, otelRequest);
try (Scope scope = context.makeCurrent()) {
response = (Response) method.invoke(target, args);
} catch (Throwable exception) {
throwable = exception;
} finally {
instrumenter.end(context, otelRequest, response, throwable);
}
return response;
} else if ("performRequestAsync".equals(method.getName())
&& args.length == 2
&& args[0] instanceof Request
&& args[1] instanceof ResponseListener) {
Request request = (Request) args[0];
ResponseListener responseListener = (ResponseListener) args[1];
Context parentContext = Context.current();
ElasticsearchRestRequest otelRequest =
ElasticsearchRestRequest.create(request.getMethod(), request.getEndpoint());
if (!instrumenter.shouldStart(parentContext, otelRequest)) {
return method.invoke(target, args);
}
Throwable throwable = null;
Context context = instrumenter.start(parentContext, otelRequest);
args[1] =
new RestResponseListener(
responseListener, parentContext, instrumenter, context, otelRequest);
try (Scope scope = context.makeCurrent()) {
return method.invoke(target, args);
} catch (Throwable exception) {
throwable = exception;
} finally {
if (throwable != null) {
instrumenter.end(context, otelRequest, null, throwable);
}
// span ended in RestResponseListener
}
}
// delegate to wrapped RestClient
return method.invoke(target, args);
}))
.make()
.load(RestClient.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded();
}
private static Field getTargetField(Class<?> clazz) {
return getProxyField(clazz, "target");
}
private static Field getInstrumenterSupplierField(Class<?> clazz) {
return getProxyField(clazz, "instrumenterSupplier");
}
private static Field getProxyField(Class<?> clazz, String fieldName) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException exception) {
throw new IllegalStateException("Could not find proxy field", exception);
}
}
@SuppressWarnings("unchecked")
private static Instrumenter<ElasticsearchRestRequest, Response> getInstrumenter(Object proxy)
throws IllegalAccessException {
Supplier<Instrumenter<ElasticsearchRestRequest, Response>> supplier =
(Supplier<Instrumenter<ElasticsearchRestRequest, Response>>)
instrumenterSupplierField.get(proxy);
return supplier != null ? supplier.get() : null;
}
private static Function<RestClient, RestClient> getProxyFactory(Class<?> clazz) {
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length >= 3
&& !parameterTypes[0].isPrimitive()
&& parameterTypes[1] == Header[].class
&& parameterTypes[2] == List.class) {
return restClient -> {
List<Node> nodes = restClient.getNodes();
// all the proxy methods will delegate to the wrapped RestClient, we need to fill only the
// arguments that are required by the constructor
Object[] arguments = new Object[parameterTypes.length];
arguments[1] = new Header[0];
arguments[2] = nodes;
for (int i = 3; i < parameterTypes.length; i++) {
if (parameterTypes[i].isPrimitive()) {
arguments[i] = getDefaultValue(parameterTypes[i]);
}
}
try {
return (RestClient) constructor.newInstance(arguments);
} catch (Exception exception) {
throw new IllegalStateException("Failed to construct proxy instance", exception);
}
};
}
}
throw new IllegalStateException("Failed to find suitable constructor");
}
// create a single element array of given type, this method is used to get the default value of
// a primitive type
@SuppressWarnings("unchecked")
private static <T> T getDefaultValue(Class<T> clazz) {
return (T) Array.get(Array.newInstance(clazz, 1), 0);
}
static RestClient wrap(
RestClient restClient, Instrumenter<ElasticsearchRestRequest, Response> instrumenter) {
RestClient wrapped = proxyFactory.apply(restClient);
try {
// set wrapped RestClient instance and the instrumenter on the proxy
targetField.set(wrapped, restClient);
instrumenterSupplierField.set(
wrapped, (Supplier<Instrumenter<ElasticsearchRestRequest, Response>>) () -> instrumenter);
return wrapped;
} catch (Exception exception) {
throw new IllegalStateException("Failed to construct proxy instance", exception);
}
}
private RestClientWrapper() {}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.elasticsearch.rest.v7_0;
import static io.opentelemetry.instrumentation.testing.GlobalTraceUtil.runWithSpan;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
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.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 = LibraryInstrumentationExtension.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();
client = ElasticsearchRest7Telemetry.create(testing.getOpenTelemetry()).wrap(client);
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"))));
}
@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("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;
}
}
}

View File

@ -4,7 +4,6 @@ plugins {
dependencies {
compileOnly("org.elasticsearch.client:rest:5.0.0")
compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")
api(project(":instrumentation:elasticsearch:elasticsearch-rest-common:library"))
}

View File

@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestInstrumenterFactory;
import io.opentelemetry.instrumentation.elasticsearch.rest.internal.ElasticsearchRestRequest;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import java.util.Collections;
import org.elasticsearch.client.Response;
public final class ElasticsearchRestJavaagentInstrumenterFactory {
private static final boolean CAPTURE_SEARCH_QUERY =
InstrumentationConfig.get()
.getBoolean("otel.instrumentation.elasticsearch.capture-search-query", false);
private ElasticsearchRestJavaagentInstrumenterFactory() {}
public static Instrumenter<ElasticsearchRestRequest, Response> create(
String instrumentationName) {
return ElasticsearchRestInstrumenterFactory.create(
GlobalOpenTelemetry.get(),
instrumentationName,
Collections.emptyList(),
CAPTURE_SEARCH_QUERY);
}
}

View File

@ -0,0 +1,10 @@
plugins {
id("otel.library-instrumentation")
}
dependencies {
compileOnly("org.elasticsearch.client:rest:5.0.0")
compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")
}

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
@ -20,6 +20,10 @@ import javax.annotation.Nullable;
import org.apache.http.HttpHost;
import org.elasticsearch.client.Response;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public class ElasticsearchClientAttributeExtractor
implements AttributesExtractor<ElasticsearchRestRequest, Response> {

View File

@ -3,12 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
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;
@ -19,16 +18,22 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.http.HttpEntity;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
final class ElasticsearchDbAttributesGetter
implements DbClientAttributesGetter<ElasticsearchRestRequest> {
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());
private final boolean captureSearchQuery;
ElasticsearchDbAttributesGetter(boolean captureSearchQuery) {
this.captureSearchQuery = captureSearchQuery;
}
@Override
public String getSystem(ElasticsearchRestRequest request) {
return SemanticAttributes.DbSystemValues.ELASTICSEARCH;
@ -57,7 +62,7 @@ final class ElasticsearchDbAttributesGetter
public String getStatement(ElasticsearchRestRequest request) {
ElasticsearchEndpointDefinition epDefinition = request.getEndpointDefinition();
HttpEntity httpEntity = request.getHttpEntity();
if (CAPTURE_SEARCH_QUERY
if (captureSearchQuery
&& epDefinition != null
&& epDefinition.isSearchEndpoint()
&& httpEntity != null

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import static java.util.Collections.unmodifiableList;
@ -17,6 +17,10 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class ElasticsearchEndpointDefinition {
private static final String UNDERSCORE_REPLACEMENT = "0";
@ -62,11 +66,16 @@ public final class ElasticsearchEndpointDefinition {
}
}
List<Route> getRoutes() {
public List<Route> getRoutes() {
return routes;
}
static final class Route {
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
// Visible for testing
public static final class Route {
private final String name;
private final boolean hasParameters;
@ -77,7 +86,7 @@ public final class ElasticsearchEndpointDefinition {
this.hasParameters = name.contains("{") && name.contains("}");
}
String getName() {
public String getName() {
return name;
}
@ -104,7 +113,12 @@ public final class ElasticsearchEndpointDefinition {
}
}
static final class EndpointPattern {
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
// Visible for testing
public static final class EndpointPattern {
private static final Pattern PATH_PART_NAMES_PATTERN = Pattern.compile("\\{([^}]+)}");
private final Pattern pattern;
private final List<String> pathPartNames;
@ -136,7 +150,7 @@ public final class ElasticsearchEndpointDefinition {
}
/** Builds a regex pattern from the parameterized route pattern. */
static Pattern buildRegexPattern(String routeStr) {
public static Pattern buildRegexPattern(String routeStr) {
StringBuilder regexStr = new StringBuilder();
regexStr.append('^');
int startIdx = routeStr.indexOf("{");

View File

@ -3,31 +3,41 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientAttributesExtractor;
import java.util.List;
import org.elasticsearch.client.Response;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class ElasticsearchRestInstrumenterFactory {
private ElasticsearchRestInstrumenterFactory() {}
public static Instrumenter<ElasticsearchRestRequest, Response> create(
String instrumentationName) {
OpenTelemetry opentelemetry,
String instrumentationName,
List<AttributesExtractor<ElasticsearchRestRequest, Response>> attributesExtractors,
boolean captureSearchQuery) {
ElasticsearchDbAttributesGetter dbClientAttributesGetter =
new ElasticsearchDbAttributesGetter();
new ElasticsearchDbAttributesGetter(captureSearchQuery);
ElasticsearchClientAttributeExtractor esClientAtrributesExtractor =
new ElasticsearchClientAttributeExtractor();
ElasticsearchSpanNameExtractor nameExtractor =
new ElasticsearchSpanNameExtractor(dbClientAttributesGetter);
return Instrumenter.<ElasticsearchRestRequest, Response>builder(
GlobalOpenTelemetry.get(), instrumentationName, nameExtractor)
opentelemetry, instrumentationName, nameExtractor)
.addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter))
.addAttributesExtractor(esClientAtrributesExtractor)
.addAttributesExtractors(attributesExtractors)
.buildInstrumenter(SpanKindExtractor.alwaysClient());
}
}

View File

@ -3,12 +3,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import com.google.auto.value.AutoValue;
import javax.annotation.Nullable;
import org.apache.http.HttpEntity;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
@AutoValue
public abstract class ElasticsearchRestRequest {

View File

@ -3,10 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public class ElasticsearchSpanNameExtractor implements SpanNameExtractor<ElasticsearchRestRequest> {
private final ElasticsearchDbAttributesGetter dbAttributesGetter;

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.elasticsearch.rest;
package io.opentelemetry.instrumentation.elasticsearch.rest.internal;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
@ -11,6 +11,10 @@ import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseListener;
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.
*/
public final class RestResponseListener implements ResponseListener {
private final ResponseListener listener;

View File

@ -214,19 +214,19 @@ hideFromDependabot(":instrumentation:couchbase:couchbase-common:testing")
hideFromDependabot(":instrumentation:dropwizard:dropwizard-metrics-4.0:javaagent")
hideFromDependabot(":instrumentation:dropwizard:dropwizard-views-0.7:javaagent")
hideFromDependabot(":instrumentation:dropwizard:dropwizard-testing")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-api-client-7.16:javaagent-unit-tests")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-common:javaagent")
hideFromDependabot(":instrumentation:opensearch:opensearch-rest-common:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-common:library")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-5.0:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-6.4:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-7.0:javaagent")
hideFromDependabot(":instrumentation:opensearch:opensearch-rest-1.0:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-rest-7.0:library")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-common:javaagent")
hideFromDependabot(":instrumentation:elasticsearch:elasticsearch-transport-common:testing")
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")
@ -383,6 +383,8 @@ hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:javaagent")
hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:library")
hideFromDependabot(":instrumentation:okhttp:okhttp-3.0:testing")
hideFromDependabot(":instrumentation:opencensus-shim:testing")
hideFromDependabot(":instrumentation:opensearch:opensearch-rest-common:javaagent")
hideFromDependabot(":instrumentation:opensearch:opensearch-rest-1.0:javaagent")
hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent")
hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")
hideFromDependabot(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")