Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions instrumentation/opensearch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@
| System property | Type | Default | Description |
| -------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- |
| `otel.instrumentation.opensearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |

## Settings for the [OpenSearch Java Client](https://docs.opensearch.org/latest/clients/java/) instrumentation

| System property | Type | Default | Description |
| -------------------------------------------------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `otel.instrumentation.opensearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |
| `otel.instrumentation.opensearch.capture-search-query` | Boolean | `false` | Enable the capture of sanitized search query bodies. **Note**: Enabling this feature adds overhead for JSON serialization and parsing on search requests. |
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@ 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")
compileOnly("com.fasterxml.jackson.core:jackson-core")

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
// AwsSdk2Transport supports awssdk version 2.26.0
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")
testImplementation("software.amazon.awssdk:auth:2.26.0")
testImplementation("software.amazon.awssdk:identity-spi:2.26.0")
testImplementation("software.amazon.awssdk:apache-client:2.26.0")
testImplementation("software.amazon.awssdk:netty-nio-client:2.26.0")
testImplementation("software.amazon.awssdk:regions:2.26.0")
}

tasks {
Expand All @@ -39,14 +40,47 @@ tasks {
systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false")
}

test {
filter {
excludeTestsMatching("OpenSearchCaptureSearchQueryTest")
}
}

val testCaptureSearchQuery by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath

filter {
includeTestsMatching("OpenSearchCaptureSearchQueryTest")
}
jvmArgs("-Dotel.instrumentation.opensearch.capture-search-query=true")
}

val testStableSemconv by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath

filter {
excludeTestsMatching("OpenSearchCaptureSearchQueryTest")
}
jvmArgs("-Dotel.semconv-stability.opt-in=database")
systemProperty("metadataConfig", "otel.semconv-stability.opt-in=database")
}

val testCaptureSearchQueryStableSemconv by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath

filter {
includeTestsMatching("OpenSearchCaptureSearchQueryTest")
}
jvmArgs("-Dotel.instrumentation.opensearch.capture-search-query=true")
jvmArgs("-Dotel.semconv-stability.opt-in=database")
}

check {
dependsOn(testCaptureSearchQuery)
dependsOn(testStableSemconv)
dependsOn(testCaptureSearchQueryStableSemconv)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public String getDbNamespace(OpenSearchRequest request) {
@Override
@Nullable
public String getDbQueryText(OpenSearchRequest request) {
return request.getMethod() + " " + request.getOperation();
// keep the previous logic in case of failure to extract the query body
if (request.getBody() == null) {
return request.getMethod() + " " + request.getOperation();
}
return request.getBody();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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

import static java.util.logging.Level.FINE;

import com.fasterxml.jackson.core.JsonFactory;
import jakarta.json.stream.JsonGenerator;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.json.NdJsonpSerializable;
import org.opensearch.client.json.jackson.JacksonJsonpGenerator;
import org.opensearch.client.json.jackson.JacksonJsonpMapper;
import org.opensearch.client.transport.GenericSerializable;

public final class OpenSearchBodyExtractor {

private static final Logger logger = Logger.getLogger(OpenSearchBodyExtractor.class.getName());
private static final String QUERY_SEPARATOR = ";";
private static final JsonFactory JSON_FACTORY = new JsonFactory();

@Nullable
public static String extractSanitized(JsonpMapper mapper, Object request) {
try {
if (request instanceof NdJsonpSerializable) {
return serializeNdJsonSanitized(mapper, (NdJsonpSerializable) request);
}

if (request instanceof GenericSerializable) {
// GenericSerializable writes directly to output stream, cannot sanitize
// This path is typically not used for search queries
ByteArrayOutputStream baos = new ByteArrayOutputStream();
((GenericSerializable) request).serialize(baos);
String body = baos.toString(StandardCharsets.UTF_8);
return body.isEmpty() ? null : body;
}

return serializeSanitized(mapper, request);
} catch (RuntimeException e) {
logger.log(FINE, "Failure extracting body", e);
return null;
}
}

@Nullable
private static String serializeSanitized(JsonpMapper mapper, Object item) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

if (mapper instanceof JacksonJsonpMapper) {
// Use Jackson-based sanitizing generator for JacksonJsonpMapper
com.fasterxml.jackson.core.JsonGenerator jacksonGenerator =
JSON_FACTORY.createGenerator(baos);
com.fasterxml.jackson.core.JsonGenerator sanitizingGenerator =
new SanitizingJacksonJsonGenerator(jacksonGenerator);
JsonGenerator generator = new JacksonJsonpGenerator(sanitizingGenerator);
mapper.serialize(item, generator);
generator.close();
} else {
// Fallback for other mappers (may not work for all implementations)
JsonGenerator rawGenerator = mapper.jsonProvider().createGenerator(baos);
JsonGenerator generator = new SanitizingJsonGenerator(rawGenerator);
mapper.serialize(item, generator);
generator.close();
}

String result = baos.toString(StandardCharsets.UTF_8).trim();
return result.isEmpty() ? null : result;
} catch (Exception e) {
logger.log(FINE, "Failure serializing item", e);
return null;
}
}

@Nullable
private static String serializeNdJsonSanitized(JsonpMapper mapper, NdJsonpSerializable value) {
try {
StringBuilder result = new StringBuilder();
Iterator<?> values = value._serializables();
boolean first = true;

while (values.hasNext()) {
Object item = values.next();
String itemStr;

if (item instanceof NdJsonpSerializable && item != value) {
// Recursively handle nested NdJsonpSerializable
itemStr = serializeNdJsonSanitized(mapper, (NdJsonpSerializable) item);
} else {
itemStr = serializeSanitized(mapper, item);
}

if (itemStr != null && !itemStr.isEmpty()) {
if (!first) {
result.append(QUERY_SEPARATOR);
}
result.append(itemStr);
first = false;
}
}

return result.length() == 0 ? null : result.toString();
} catch (RuntimeException e) {
logger.log(FINE, "Failure serializing NdJson", e);
return null;
}
}

private OpenSearchBodyExtractor() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;

import com.google.auto.value.AutoValue;
import javax.annotation.Nullable;

@AutoValue
public abstract class OpenSearchRequest {

public static OpenSearchRequest create(String method, String endpoint) {
return new AutoValue_OpenSearchRequest(method, endpoint);
public static OpenSearchRequest create(String method, String endpoint, @Nullable String body) {
return new AutoValue_OpenSearchRequest(method, endpoint, body);
}

public abstract String getMethod();

public abstract String getOperation();

@Nullable
public abstract String getBody();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil;
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;
Expand All @@ -15,6 +16,10 @@
public final class OpenSearchSingletons {
private static final Instrumenter<OpenSearchRequest, Void> INSTRUMENTER = createInstrumenter();

public static final boolean CAPTURE_SEARCH_QUERY =
DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "opensearch")
.getBoolean("capture_search_query", false);

public static Instrumenter<OpenSearchRequest, Void> instrumenter() {
return INSTRUMENTER;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.opensearch.core.MsearchRequest;
import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.transport.Endpoint;
import org.opensearch.client.transport.OpenSearchTransport;

public class OpenSearchTransportInstrumentation implements TypeInstrumentation {
@Override
Expand Down Expand Up @@ -60,10 +64,21 @@ private AdviceScope(OpenSearchRequest otelRequest, Context context, Scope scope)
}

@Nullable
public static AdviceScope start(Object request, Endpoint<Object, Object, Object> endpoint) {
public static AdviceScope start(
Object request, Endpoint<Object, Object, Object> endpoint, JsonpMapper jsonpMapper) {
Context parentContext = Context.current();

String queryBody = null;

if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY
&& (request instanceof SearchRequest || request instanceof MsearchRequest)) {
queryBody = OpenSearchBodyExtractor.extractSanitized(jsonpMapper, request);
}

OpenSearchRequest otelRequest =
OpenSearchRequest.create(endpoint.method(request), endpoint.requestUrl(request));
OpenSearchRequest.create(
endpoint.method(request), endpoint.requestUrl(request), queryBody);

if (!instrumenter().shouldStart(parentContext, otelRequest)) {
return null;
}
Expand Down Expand Up @@ -94,9 +109,10 @@ public static class PerformRequestAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static AdviceScope onEnter(
@Advice.This OpenSearchTransport openSearchTransport,
@Advice.Argument(0) Object request,
@Advice.Argument(1) Endpoint<Object, Object, Object> endpoint) {
return AdviceScope.start(request, endpoint);
return AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper());
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
Expand All @@ -114,9 +130,11 @@ public static class PerformRequestAsyncAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static Object[] onEnter(
@Advice.This OpenSearchTransport openSearchTransport,
@Advice.Argument(0) Object request,
@Advice.Argument(1) Endpoint<Object, Object, Object> endpoint) {
AdviceScope adviceScope = AdviceScope.start(request, endpoint);
AdviceScope adviceScope =
AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper());
return new Object[] {adviceScope};
}

Expand Down
Loading
Loading