diff --git a/.fossa.yml b/.fossa.yml index 5b1f524cc4f9..ee2afeee42f6 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -751,6 +751,9 @@ targets: - type: gradle path: ./ target: ':instrumentation:openai:openai-java-1.1:library' + - type: gradle + path: ./ + target: ':instrumentation:opensearch:opensearch-java-3.0:javaagent' - type: gradle path: ./ target: ':instrumentation:opensearch:opensearch-rest-1.0:javaagent' diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c95e8c459aa7 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.opensearch.client") + module.set("opensearch-java") + versions.set("[3.0,)") + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) +} + +dependencies { + library("org.opensearch.client:opensearch-java:3.0.0") + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation("org.opensearch.client:opensearch-rest-client:3.0.0") + testImplementation(project(":instrumentation:opensearch:opensearch-rest-common:testing")) + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent")) + + // For testing AwsSdk2Transport + testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent")) + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testImplementation("software.amazon.awssdk:auth:2.22.0") + testImplementation("software.amazon.awssdk:identity-spi:2.22.0") + testImplementation("software.amazon.awssdk:apache-client:2.22.0") + testImplementation("software.amazon.awssdk:netty-nio-client:2.22.0") + testImplementation("software.amazon.awssdk:regions:2.22.0") +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } + + val testStableSemconv by registering(Test::class) { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + jvmArgs("-Dotel.semconv-stability.opt-in=database") + } + + check { + dependsOn(testStableSemconv) + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAttributesGetter.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAttributesGetter.java new file mode 100644 index 000000000000..7c2c9a0d1f51 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAttributesGetter.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import javax.annotation.Nullable; + +final class OpenSearchAttributesGetter + implements DbClientAttributesGetter { + + @Override + public String getDbSystem(OpenSearchRequest request) { + return DbIncubatingAttributes.DbSystemNameIncubatingValues.OPENSEARCH; + } + + @Override + @Nullable + public String getDbNamespace(OpenSearchRequest request) { + return null; + } + + @Override + @Nullable + public String getDbQueryText(OpenSearchRequest request) { + return request.getMethod() + " " + request.getOperation(); + } + + @Override + @Nullable + public String getDbOperationName(OpenSearchRequest request) { + return request.getMethod(); + } + + @Nullable + @Override + public String getResponseStatus(@Nullable Void response, @Nullable Throwable error) { + return null; // Response status is handled by HTTP instrumentation + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchInstrumentationModule.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchInstrumentationModule.java new file mode 100644 index 000000000000..3397ae57ade4 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchInstrumentationModule.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +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 OpenSearchInstrumentationModule extends InstrumentationModule { + public OpenSearchInstrumentationModule() { + super("opensearch-java", "opensearch-java-3.0", "opensearch"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new OpenSearchTransportInstrumentation()); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRequest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRequest.java new file mode 100644 index 000000000000..a7db9ceb21be --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRequest.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class OpenSearchRequest { + + public static OpenSearchRequest create(String method, String endpoint) { + return new AutoValue_OpenSearchRequest(method, endpoint); + } + + public abstract String getMethod(); + + public abstract String getOperation(); +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchResponseHandler.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchResponseHandler.java new file mode 100644 index 000000000000..870fb318f29d --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchResponseHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import static io.opentelemetry.javaagent.instrumentation.opensearch.v3_0.OpenSearchSingletons.instrumenter; + +import io.opentelemetry.context.Context; +import java.util.function.BiConsumer; + +public final class OpenSearchResponseHandler implements BiConsumer { + private final Context context; + private final OpenSearchRequest otelRequest; + + public OpenSearchResponseHandler(Context context, OpenSearchRequest otelRequest) { + this.context = context; + this.otelRequest = otelRequest; + } + + @Override + public void accept(Object response, Throwable error) { + // OpenSearch responses don't provide response information, so the span is closed with null. + instrumenter().end(context, otelRequest, null, error); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchSingletons.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchSingletons.java new file mode 100644 index 000000000000..ef07442b65e8 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchSingletons.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; + +public final class OpenSearchSingletons { + private static final Instrumenter INSTRUMENTER = createInstrumenter(); + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private static Instrumenter createInstrumenter() { + OpenSearchAttributesGetter dbClientAttributesGetter = new OpenSearchAttributesGetter(); + + return Instrumenter.builder( + GlobalOpenTelemetry.get(), + "io.opentelemetry.opensearch-java-3.0", + DbClientSpanNameExtractor.create(dbClientAttributesGetter)) + .addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter)) + .addOperationMetrics(DbClientMetrics.get()) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + private OpenSearchSingletons() {} +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchTransportInstrumentation.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchTransportInstrumentation.java new file mode 100644 index 000000000000..9a475aa76d92 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchTransportInstrumentation.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.opensearch.v3_0.OpenSearchSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.concurrent.CompletableFuture; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.opensearch.client.transport.Endpoint; + +public class OpenSearchTransportInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.opensearch.client.transport.OpenSearchTransport")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("performRequest")) + .and(takesArgument(0, Object.class)) + .and(takesArgument(1, named("org.opensearch.client.transport.Endpoint"))), + this.getClass().getName() + "$PerformRequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(isPublic()) + .and(named("performRequestAsync")) + .and(takesArgument(0, Object.class)) + .and(takesArgument(1, named("org.opensearch.client.transport.Endpoint"))), + this.getClass().getName() + "$PerformRequestAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class PerformRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Object request, + @Advice.Argument(1) Endpoint endpoint, + @Advice.Local("otelRequest") OpenSearchRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + otelRequest = + OpenSearchRequest.create(endpoint.method(request), endpoint.requestUrl(request)); + if (!instrumenter().shouldStart(parentContext, otelRequest)) { + return; + } + + context = instrumenter().start(parentContext, otelRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") OpenSearchRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope == null) { + return; + } + scope.close(); + + instrumenter().end(context, otelRequest, null, throwable); + } + } + + @SuppressWarnings("unused") + public static class PerformRequestAsyncAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(0) Object request, + @Advice.Argument(value = 1, readOnly = false) Endpoint endpoint, + @Advice.Local("otelRequest") OpenSearchRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + Context parentContext = currentContext(); + otelRequest = + OpenSearchRequest.create(endpoint.method(request), endpoint.requestUrl(request)); + if (!instrumenter().shouldStart(parentContext, otelRequest)) { + return; + } + + context = instrumenter().start(parentContext, otelRequest); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Return(readOnly = false) CompletableFuture future, + @Advice.Local("otelRequest") OpenSearchRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + + if (scope == null) { + return; + } + scope.close(); + + if (throwable != null) { + instrumenter().end(context, otelRequest, null, throwable); + } + + future.whenComplete(new OpenSearchResponseHandler(context, otelRequest)); + } + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/AbstractOpenSearchTest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/AbstractOpenSearchTest.java new file mode 100644 index 000000000000..a59e27399801 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/AbstractOpenSearchTest.java @@ -0,0 +1,154 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.opensearch.OpenSearchAsyncClient; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch.cluster.HealthResponse; +import org.opensearch.testcontainers.OpensearchContainer; +import org.testcontainers.utility.DockerImageName; + +@SuppressWarnings("deprecation") // using deprecated semconv +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class AbstractOpenSearchTest { + + protected OpenSearchClient openSearchClient; + protected OpenSearchAsyncClient openSearchAsyncClient; + protected OpensearchContainer opensearch; + protected URI httpHost; + + protected abstract OpenSearchClient buildOpenSearchClient() throws Exception; + + protected abstract OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception; + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + protected InstrumentationExtension getTesting() { + return testing; + } + + @BeforeAll + void setUp() throws Exception { + opensearch = + new OpensearchContainer(DockerImageName.parse("opensearchproject/opensearch:1.3.6")) + .withSecurityEnabled(); + opensearch.withEnv("OPENSEARCH_JAVA_OPTS", "-Xmx256m -Xms256m"); + opensearch.start(); + httpHost = URI.create(opensearch.getHttpHostAddress()); + openSearchClient = buildOpenSearchClient(); + openSearchAsyncClient = buildOpenSearchAsyncClient(); + } + + @AfterAll + void tearDown() { + opensearch.stop(); + } + + @Test + void shouldGetStatusWithTraces() throws IOException { + HealthResponse healthResponse = openSearchClient.cluster().health(); + assertThat(healthResponse).isNotNull(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "GET"), + equalTo(maybeStable(DB_STATEMENT), "GET /_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(SERVER_ADDRESS, httpHost.getHost()), + equalTo(SERVER_PORT, httpHost.getPort()), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(URL_FULL, httpHost + "/_cluster/health"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L)))); + } + + @Test + void shouldGetStatusAsyncWithTraces() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + + CompletableFuture responseCompletableFuture = + getTesting() + .runWithSpan( + "client", + () -> + openSearchAsyncClient + .cluster() + .health() + .whenComplete( + (response, throwable) -> + getTesting().runWithSpan("callback", countDownLatch::countDown))); + + countDownLatch.await(); + HealthResponse healthResponse = responseCompletableFuture.get(); + assertThat(healthResponse).isNotNull(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("client").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "GET"), + equalTo(maybeStable(DB_STATEMENT), "GET /_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(SERVER_ADDRESS, httpHost.getHost()), + equalTo(SERVER_PORT, httpHost.getPort()), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(URL_FULL, httpHost + "/_cluster/health"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchApacheHttpClient5TransportTest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchApacheHttpClient5TransportTest.java new file mode 100644 index 000000000000..bfaac5aa4a77 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchApacheHttpClient5TransportTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import javax.net.ssl.SSLContext; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.ssl.TrustStrategy; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.opensearch.OpenSearchAsyncClient; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; + +class OpenSearchApacheHttpClient5TransportTest extends AbstractOpenSearchTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected OpenSearchClient buildOpenSearchClient() throws Exception { + HttpHost host = new HttpHost("https", httpHost.getHost(), httpHost.getPort()); + + TrustStrategy acceptingTrustStrategy = (certificate, authType) -> true; + SSLContext sslContext = + SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + TlsStrategy tlsStrategy = + ClientTlsStrategyBuilder.create() + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSslContext(sslContext) + .build(); + PoolingAsyncClientConnectionManager connectionManager = + PoolingAsyncClientConnectionManagerBuilder.create().setTlsStrategy(tlsStrategy).build(); + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(null, -1), + new UsernamePasswordCredentials( + opensearch.getUsername(), opensearch.getPassword().toCharArray())); + + OpenSearchTransport apacheHttpClient5Transport = + ApacheHttpClient5TransportBuilder.builder(host) + .setHttpClientConfigCallback( + httpClientBuilder -> + httpClientBuilder + .setDefaultCredentialsProvider(credentialsProvider) + .setConnectionManager(connectionManager) + .setDefaultCredentialsProvider(credentialsProvider)) + .build(); + return new OpenSearchClient(apacheHttpClient5Transport); + } + + @Override + protected OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception { + HttpHost host = new HttpHost("https", httpHost.getHost(), httpHost.getPort()); + + TrustStrategy acceptingTrustStrategy = (certificate, authType) -> true; + SSLContext sslContext = + SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + TlsStrategy tlsStrategy = + ClientTlsStrategyBuilder.create() + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSslContext(sslContext) + .build(); + PoolingAsyncClientConnectionManager connectionManager = + PoolingAsyncClientConnectionManagerBuilder.create().setTlsStrategy(tlsStrategy).build(); + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(null, -1), + new UsernamePasswordCredentials( + opensearch.getUsername(), opensearch.getPassword().toCharArray())); + + OpenSearchTransport apacheHttpClient5Transport = + ApacheHttpClient5TransportBuilder.builder(host) + .setHttpClientConfigCallback( + httpClientBuilder -> + httpClientBuilder + .setDefaultCredentialsProvider(credentialsProvider) + .setConnectionManager(connectionManager) + .setDefaultCredentialsProvider(credentialsProvider)) + .build(); + return new OpenSearchAsyncClient(apacheHttpClient5Transport); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAwsSdk2TransportTest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAwsSdk2TransportTest.java new file mode 100644 index 000000000000..b97b6f97aaa8 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAwsSdk2TransportTest.java @@ -0,0 +1,209 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import io.opentelemetry.testing.internal.armeria.common.MediaType; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.opensearch.OpenSearchAsyncClient; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch.cluster.HealthResponse; +import org.opensearch.client.transport.aws.AwsSdk2Transport; +import org.opensearch.client.transport.aws.AwsSdk2TransportOptions; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.AttributeMap; + +@SuppressWarnings("deprecation") // using deprecated semconv +class OpenSearchAwsSdk2TransportTest extends AbstractOpenSearchTest { + + protected static final MockWebServerExtension server = new MockWebServerExtension(); + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeAll + @Override + void setUp() throws Exception { + server.start(); + openSearchClient = buildOpenSearchClient(); + openSearchAsyncClient = buildOpenSearchAsyncClient(); + httpHost = server.httpsUri(); + } + + @AfterAll + @Override + void tearDown() { + server.stop(); + } + + @BeforeEach + void prepTest() { + server.beforeTestExecution(null); + + // Mock OpenSearch cluster health response + String healthResponse = + "{\n" + + " \"cluster_name\": \"test-cluster\",\n" + + " \"status\": \"green\",\n" + + " \"timed_out\": false,\n" + + " \"number_of_nodes\": 1,\n" + + " \"number_of_data_nodes\": 1,\n" + + " \"active_primary_shards\": 0,\n" + + " \"active_shards\": 0,\n" + + " \"relocating_shards\": 0,\n" + + " \"initializing_shards\": 0,\n" + + " \"unassigned_shards\": 0,\n" + + " \"delayed_unassigned_shards\": 0,\n" + + " \"number_of_pending_tasks\": 0,\n" + + " \"number_of_in_flight_fetch\": 0,\n" + + " \"task_max_waiting_in_queue_millis\": 0,\n" + + " \"active_shards_percent_as_number\": 100.0\n" + + "}"; + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, healthResponse)); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, healthResponse)); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected OpenSearchClient buildOpenSearchClient() throws Exception { + SdkHttpClient httpClient = + ApacheHttpClient.builder() + .buildWithDefaults( + AttributeMap.builder() + .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) + .build()); + + AwsSdk2Transport transport = + new AwsSdk2Transport( + httpClient, + server.httpsUri().toString().replace("https://", ""), + Region.AP_NORTHEAST_1, + AwsSdk2TransportOptions.builder().setCredentials(CREDENTIALS_PROVIDER).build()); + + return new OpenSearchClient(transport); + } + + @Override + protected OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception { + SdkAsyncHttpClient httpClient = + NettyNioAsyncHttpClient.builder() + .buildWithDefaults( + AttributeMap.builder() + .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true) + .build()); + + AwsSdk2Transport transport = + new AwsSdk2Transport( + httpClient, + server.httpsUri().toString().replace("https://", ""), + Region.AP_NORTHEAST_1, + AwsSdk2TransportOptions.builder().setCredentials(CREDENTIALS_PROVIDER).build()); + + return new OpenSearchAsyncClient(transport); + } + + @Test + @Override + void shouldGetStatusAsyncWithTraces() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + + CompletableFuture responseCompletableFuture = + getTesting() + .runWithSpan( + "client", + () -> + openSearchAsyncClient + .cluster() + .health() + .whenComplete( + (response, throwable) -> + getTesting().runWithSpan("callback", countDownLatch::countDown))); + + countDownLatch.await(); + HealthResponse healthResponse = responseCompletableFuture.get(); + assertThat(healthResponse).isNotNull(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("client").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "GET"), + equalTo(maybeStable(DB_STATEMENT), "GET /_cluster/health")), + span -> + span.hasName("GET") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(SERVER_ADDRESS, httpHost.getHost()), + equalTo(SERVER_PORT, httpHost.getPort()), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(URL_FULL, httpHost + "/_cluster/health"), + equalTo( + NETWORK_PEER_ADDRESS, + httpHost.getHost()), // Netty 4.1 Instrumentation collects + // NETWORK_PEER_ADDRESS + equalTo( + NETWORK_PEER_PORT, + httpHost.getPort()), // Netty 4.1 Instrumentation collects + // NETWORK_PEER_PORT + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(1)))); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRestClientTransportTest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRestClientTransportTest.java new file mode 100644 index 000000000000..409b81e1fff7 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRestClientTransportTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import javax.net.ssl.SSLContext; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.ssl.TrustStrategy; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.opensearch.client.RestClient; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.opensearch.OpenSearchAsyncClient; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.rest_client.RestClientTransport; + +@SuppressWarnings( + "deprecation") // RestClientTransport is deprecated but still the correct way for OpenSearch +// Java 3.0 +class OpenSearchRestClientTransportTest extends AbstractOpenSearchTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected OpenSearchClient buildOpenSearchClient() throws Exception { + TrustStrategy acceptingTrustStrategy = (certificate, authType) -> true; + SSLContext sslContext = + SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + TlsStrategy tlsStrategy = + ClientTlsStrategyBuilder.create() + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSslContext(sslContext) + .build(); + PoolingAsyncClientConnectionManager connectionManager = + PoolingAsyncClientConnectionManagerBuilder.create().setTlsStrategy(tlsStrategy).build(); + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(null, -1), + new UsernamePasswordCredentials( + opensearch.getUsername(), opensearch.getPassword().toCharArray())); + + HttpHost httpHost = HttpHost.create(opensearch.getHttpHostAddress()); + RestClient restClient = + RestClient.builder(httpHost) + .setHttpClientConfigCallback( + httpClientBuilder -> + httpClientBuilder + .setConnectionManager(connectionManager) + .setDefaultCredentialsProvider(credentialsProvider)) + .build(); + + OpenSearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); + return new OpenSearchClient(transport); + } + + @Override + protected OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception { + TrustStrategy acceptingTrustStrategy = (certificate, authType) -> true; + SSLContext sslContext = + SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); + TlsStrategy tlsStrategy = + ClientTlsStrategyBuilder.create() + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSslContext(sslContext) + .build(); + PoolingAsyncClientConnectionManager connectionManager = + PoolingAsyncClientConnectionManagerBuilder.create().setTlsStrategy(tlsStrategy).build(); + + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + new AuthScope(null, -1), + new UsernamePasswordCredentials( + opensearch.getUsername(), opensearch.getPassword().toCharArray())); + + HttpHost httpHost = HttpHost.create(opensearch.getHttpHostAddress()); + RestClient restClient = + RestClient.builder(httpHost) + .setHttpClientConfigCallback( + httpClientBuilder -> + httpClientBuilder + .setConnectionManager(connectionManager) + .setDefaultCredentialsProvider(credentialsProvider)) + .build(); + + OpenSearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); + return new OpenSearchAsyncClient(transport); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b6bf858fb56..6e2125926918 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -435,6 +435,7 @@ include(":instrumentation:openai:openai-java-1.1:library") include(":instrumentation:openai:openai-java-1.1:testing") include(":instrumentation:openai:openai-java-1.1:openai3-testing") include(":instrumentation:opencensus-shim:testing") +include(":instrumentation:opensearch:opensearch-java-3.0:javaagent") include(":instrumentation:opensearch:opensearch-rest-1.0:javaagent") include(":instrumentation:opensearch:opensearch-rest-3.0:javaagent") include(":instrumentation:opensearch:opensearch-rest-common:javaagent")