Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.v3_0;

import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter;
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
import javax.annotation.Nullable;

final class OpenSearchJavaAttributesGetter
implements DbClientAttributesGetter<OpenSearchJavaRequest, Void> {

@SuppressWarnings("deprecation") // using deprecated DbSystemIncubatingValues
@Override
public String getDbSystem(OpenSearchJavaRequest request) {
return DbIncubatingAttributes.DbSystemIncubatingValues.OPENSEARCH;
}

@Deprecated
@Override
@Nullable
public String getUser(OpenSearchJavaRequest request) {
return null;
}

@Override
@Nullable
public String getDbNamespace(OpenSearchJavaRequest request) {
return null;
}

@Deprecated
@Override
@Nullable
public String getConnectionString(OpenSearchJavaRequest request) {
return null;
}

@Override
@Nullable
public String getDbQueryText(OpenSearchJavaRequest request) {
return request.getMethod() + " " + request.getOperation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably isn't correct, ideally this should return the full query that is sanitized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurit when i first wrote the implementation, extracting the full query text seemed impossible because the request only provided limited information. on second thought, it might actually be possible, so i’ll take another look.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the full query isn't always desirable either because it needs to be sanitized. If it can't be sanitized then it can't be enabled by default. Unfortunately sanitizing might not be easy. I think that since the existing opensearch-rest instrumentation uses the same query text we can use it here too for now. In the future we could use the same approach to fill the query summary (https://opentelemetry.io/docs/specs/semconv/database/database-spans/#generating-a-summary-of-the-query) instead and set the query text to null. Before we can do that we have to implement support for the db.query.summary attribute. Also since db.query.summary is only in the stable semconv would need to figure out what to do with the old semconv (continue filling the query text as is or set it to null). cc @trask

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurit I was finally able to extract the full query text, but composing the sanitization logic will take more time. The existing sanitization logic can’t be reused since this isn’t just sql or sql-like but it’s a json query. I’ve roughly figured out how to approach opensearch query sanitization, but I’ll need more time to validate that it’s the right solution.

So I agree with your review. Let’s leave it as is for now, and I’ll come back once I’ve implemented the sanitization logic and can extract the full query text with it.

}

@Override
@Nullable
public String getDbOperationName(OpenSearchJavaRequest request) {
return request.getMethod();
}

@Nullable
@Override
public String getResponseStatus(@Nullable Void response, @Nullable Throwable error) {
return null; // Response status is handled by HTTP instrumentation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.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 OpenSearchJavaInstrumentationModule extends InstrumentationModule {
public OpenSearchJavaInstrumentationModule() {
super("opensearch-java", "opensearch-java-3.0", "opensearch");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new OpenSearchTransportInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.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 OpenSearchJavaInstrumenterFactory {

public static Instrumenter<OpenSearchJavaRequest, Void> create(String instrumentationName) {
OpenSearchJavaAttributesGetter dbClientAttributesGetter = new OpenSearchJavaAttributesGetter();

return Instrumenter.<OpenSearchJavaRequest, Void>builder(
GlobalOpenTelemetry.get(),
instrumentationName,
DbClientSpanNameExtractor.create(dbClientAttributesGetter))
.addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter))
.addOperationMetrics(DbClientMetrics.get())
.buildInstrumenter(SpanKindExtractor.alwaysClient());
}

private OpenSearchJavaInstrumenterFactory() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.v3_0;

import com.google.auto.value.AutoValue;

@AutoValue
public abstract class OpenSearchJavaRequest {

public static OpenSearchJavaRequest create(String method, String endpoint) {
return new AutoValue_OpenSearchJavaRequest(method, endpoint);
}

public abstract String getMethod();

public abstract String getOperation();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.v3_0;

import io.opentelemetry.context.Context;
import java.util.function.BiConsumer;

public final class OpenSearchJavaResponseHandler implements BiConsumer<Object, Throwable> {
private final Context context;
private final OpenSearchJavaRequest otelRequest;

public OpenSearchJavaResponseHandler(Context context, OpenSearchJavaRequest 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.
OpenSearchJavaSingletons.instrumenter().end(context, otelRequest, null, error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.v3_0;

import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;

public final class OpenSearchJavaSingletons {
private static final Instrumenter<OpenSearchJavaRequest, Void> INSTRUMENTER =
OpenSearchJavaInstrumenterFactory.create("io.opentelemetry.opensearch-java-3.0");

public static Instrumenter<OpenSearchJavaRequest, Void> instrumenter() {
return INSTRUMENTER;
}

private OpenSearchJavaSingletons() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.opensearch.java.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.java.v3_0.OpenSearchJavaSingletons.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<TypeDescription> 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<Object, Object, Object> endpoint,
@Advice.Local("otelRequest") OpenSearchJavaRequest otelRequest,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {

Context parentContext = currentContext();
otelRequest =
OpenSearchJavaRequest.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") OpenSearchJavaRequest 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<Object, Object, Object> endpoint,
@Advice.Local("otelRequest") OpenSearchJavaRequest otelRequest,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {

Context parentContext = currentContext();
otelRequest =
OpenSearchJavaRequest.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<Object> future,
@Advice.Local("otelRequest") OpenSearchJavaRequest 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 OpenSearchJavaResponseHandler(context, otelRequest));
}
}
}
Loading
Loading