From dea68d6986f2321eeac7fd89f368816a570e3519 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Sat, 13 Dec 2025 17:57:31 +0900 Subject: [PATCH 01/13] sanitize opensearch transport query body --- .../javaagent/build.gradle.kts | 1 + .../v3_0/OpenSearchBodyExtractor.java | 64 +++++++ .../v3_0/OpenSearchBodySanitizer.java | 110 +++++++++++ .../opensearch/v3_0/OpenSearchRequest.java | 5 +- .../opensearch/v3_0/OpenSearchSingletons.java | 5 + .../OpenSearchTransportInstrumentation.java | 24 ++- .../opensearch/v3_0/QuerySplitter.java | 60 ++++++ .../v3_0/AbstractOpenSearchTest.java | 165 +++++++++++++++++ .../v3_0/OpenSearchAwsSdk2TransportTest.java | 174 ++++++++++++++++-- .../opensearch/v3_0/TestDocument.java | 30 +++ 10 files changed, 617 insertions(+), 21 deletions(-) create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/TestDocument.java diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index da36e2438828..c76ec7d852cc 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -18,6 +18,7 @@ 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") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") testImplementation("org.opensearch.client:opensearch-rest-client:3.0.0") testImplementation(project(":instrumentation:opensearch:opensearch-rest-common:testing")) diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java new file mode 100644 index 000000000000..df6b22095933 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import jakarta.json.stream.JsonGenerator; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import javax.annotation.Nullable; +import org.apache.hc.core5.http.ContentType; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.NdJsonpSerializable; +import org.opensearch.client.transport.GenericSerializable; + +public class OpenSearchBodyExtractor { + + @Nullable + public static String extract(JsonpMapper mapper, Object request) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if (request instanceof NdJsonpSerializable) { + writeNdJson(mapper, (NdJsonpSerializable) request, baos); + } else if (request instanceof GenericSerializable) { + ContentType.parse(((GenericSerializable) request).serialize(baos)); + } else { + JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); + mapper.serialize(request, generator); + generator.close(); + } + + String body = baos.toString(StandardCharsets.UTF_8); + return body.isEmpty() ? null : body; + } catch (RuntimeException e) { + return null; + } + } + + private static void writeNdJson( + JsonpMapper mapper, NdJsonpSerializable value, ByteArrayOutputStream baos) { + try { + Iterator values = value._serializables(); + while (values.hasNext()) { + Object item = values.next(); + if (item instanceof NdJsonpSerializable && item != value) { + // do not recurse on the item itself + writeNdJson(mapper, (NdJsonpSerializable) item, baos); + } else { + JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); + mapper.serialize(item, generator); + generator.close(); + baos.write('\n'); + } + } + } catch (RuntimeException e) { + // Ignore + } + } + + private OpenSearchBodyExtractor() {} +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java new file mode 100644 index 000000000000..dbf791dc2a39 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java @@ -0,0 +1,110 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class OpenSearchBodySanitizer { + + private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); + private static final String MASKED_VALUE = "?"; + private static final OpenSearchBodySanitizer DEFAULT_INSTANCE = + new OpenSearchBodySanitizer(DEFAULT_OBJECT_MAPPER); + + private final ObjectMapper objectMapper; + + private OpenSearchBodySanitizer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public static OpenSearchBodySanitizer create() { + return new OpenSearchBodySanitizer(DEFAULT_OBJECT_MAPPER); + } + + public static OpenSearchBodySanitizer create(ObjectMapper objectMapper) { + return new OpenSearchBodySanitizer(objectMapper); + } + + public static OpenSearchBodySanitizer getDefault() { + return DEFAULT_INSTANCE; + } + + public static String sanitize(String jsonString) { + return DEFAULT_INSTANCE.sanitizeInstance(jsonString); + } + + public String sanitizeInstance(String jsonString) { + if (jsonString == null) { + return null; + } + + List queries = QuerySplitter.splitQueries(jsonString); + if (queries.isEmpty()) { + return null; + } + + List sanitizedQueries = new ArrayList<>(); + for (String query : queries) { + String sanitized = sanitizeSingleQuery(query); + sanitizedQueries.add(sanitized); + } + + return QuerySplitter.joinQueries(sanitizedQueries); + } + + private String sanitizeSingleQuery(String query) { + try { + JsonNode rootNode = objectMapper.readTree(query); + JsonNode sanitizedNode = sanitizeNode(rootNode); + return objectMapper.writeValueAsString(sanitizedNode); + } catch (Exception e) { + return query; + } + } + + private JsonNode sanitizeNode(JsonNode node) { + if (node == null || node.isNull()) { + return node; + } + + if (node.isTextual()) { + return new TextNode(MASKED_VALUE); + } + + if (node.isNumber() || node.isBoolean()) { + return new TextNode(MASKED_VALUE); + } + + if (node.isArray()) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (JsonNode element : node) { + arrayNode.add(sanitizeNode(element)); + } + return arrayNode; + } + + if (node.isObject()) { + ObjectNode objectNode = objectMapper.createObjectNode(); + + for (Map.Entry field : node.properties()) { + String key = field.getKey(); + JsonNode value = field.getValue(); + + objectNode.set(key, sanitizeNode(value)); + } + return objectNode; + } + + return node; + } +} 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 index a7db9ceb21be..a43df2395d4a 100644 --- 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 @@ -6,12 +6,13 @@ 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(); 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 index ef07442b65e8..a34a8e548da0 100644 --- 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 @@ -11,10 +11,15 @@ import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class OpenSearchSingletons { private static final Instrumenter INSTRUMENTER = createInstrumenter(); + public static final boolean CAPTURE_SEARCH_QUERY = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.opensearch.capture-search-query", false); + public static Instrumenter instrumenter() { return INSTRUMENTER; } 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 index 6cf1cfe84993..7a359a389703 100644 --- 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 @@ -21,7 +21,9 @@ 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.transport.Endpoint; +import org.opensearch.client.transport.OpenSearchTransport; public class OpenSearchTransportInstrumentation implements TypeInstrumentation { @Override @@ -60,10 +62,21 @@ private AdviceScope(OpenSearchRequest otelRequest, Context context, Scope scope) } @Nullable - public static AdviceScope start(Object request, Endpoint endpoint) { + public static AdviceScope start( + Object request, Endpoint endpoint, JsonpMapper jsonpMapper) { Context parentContext = Context.current(); + + String queryBody = null; + + if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY) { + String rawBody = OpenSearchBodyExtractor.extract(jsonpMapper, request); + queryBody = OpenSearchBodyExtractor.extract(jsonpMapper, rawBody); + } + 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; } @@ -94,9 +107,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 endpoint) { - return AdviceScope.start(request, endpoint); + return AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper()); } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) @@ -114,9 +128,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 endpoint) { - AdviceScope adviceScope = AdviceScope.start(request, endpoint); + AdviceScope adviceScope = + AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper()); return new Object[] {adviceScope}; } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java new file mode 100644 index 000000000000..5ef95cff21d1 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java @@ -0,0 +1,60 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Splits multiple queries separated by semicolons. Single Responsibility: Only responsible for + * query separation logic. + */ +class QuerySplitter { + + private static final String QUERY_SEPARATOR = "\n"; + private static final String QUERY_COMBINATOR = ";"; + + private QuerySplitter() {} + + /** + * Splits a string containing multiple queries separated by semicolons. + * + * @param queriesString input string containing queries + * @return list of individual query strings, empty if input is null or empty + */ + static List splitQueries(String queriesString) { + if (queriesString == null || queriesString.trim().isEmpty()) { + return Collections.emptyList(); + } + + String[] queries = queriesString.split(QUERY_SEPARATOR, -1); + List result = new ArrayList<>(); + + for (String query : queries) { + String trimmed = query.trim(); + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + + return result; + } + + /** + * Joins multiple sanitized queries back into a single string. + * + * @param sanitizedQueries list of sanitized query strings + * @return joined string with semicolon separator, or null if list is empty + */ + static String joinQueries(List sanitizedQueries) { + if (sanitizedQueries == null || sanitizedQueries.isEmpty()) { + return null; + } + + return String.join(QUERY_COMBINATOR, sanitizedQueries); + } +} 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 index 63d522fe8c36..578fd6ba7ada 100644 --- 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 @@ -8,6 +8,7 @@ import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric; import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME; import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME; import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; @@ -36,7 +37,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.opensearch.client.opensearch.OpenSearchAsyncClient; import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch._types.query_dsl.Query; import org.opensearch.client.opensearch.cluster.HealthResponse; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.core.MsearchRequest; +import org.opensearch.client.opensearch.core.MsearchResponse; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; import org.opensearch.testcontainers.OpensearchContainer; import org.testcontainers.utility.DockerImageName; @@ -44,6 +53,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) abstract class AbstractOpenSearchTest { + protected static final String INDEX_NAME = "test-search-index"; protected OpenSearchClient openSearchClient; protected OpenSearchAsyncClient openSearchAsyncClient; protected OpensearchContainer opensearch; @@ -73,6 +83,32 @@ void setUp() throws Exception { httpHost = URI.create(opensearch.getHttpHostAddress()); openSearchClient = buildOpenSearchClient(); openSearchAsyncClient = buildOpenSearchAsyncClient(); + + String documentId = "test-doc-1"; + + // Create index + CreateIndexRequest createIndexRequest = + CreateIndexRequest.of( + c -> + c.index(INDEX_NAME) + .mappings( + TypeMapping.of( + t -> + t.properties( + "message", + p -> + p.text(txt -> txt.fielddata(true).analyzer("standard")))))); + + openSearchClient.indices().create(createIndexRequest); + + TestDocument testDocument = TestDocument.create(documentId, "test message for search"); + IndexRequest indexRequest = + new IndexRequest.Builder().index(INDEX_NAME).document(testDocument).build(); + + openSearchClient.index(indexRequest); + + // Wait for indexing to complete + openSearchClient.indices().refresh(r -> r.index(INDEX_NAME)); } @AfterAll @@ -171,4 +207,133 @@ void shouldRecordMetrics() throws IOException { assertDurationMetric( getTesting(), "io.opentelemetry.opensearch-java-3.0", DB_OPERATION_NAME, DB_SYSTEM_NAME); } + + @Test + void shouldCaptureSearchQueryBody() throws IOException { + // Execute search query with body + SearchRequest searchRequest = + SearchRequest.of( + s -> + s.index(INDEX_NAME) + .query( + Query.of( + q -> + q.match( + m -> m.field("message").query(v -> v.stringValue("test")))))); + + SearchResponse searchResponse = + openSearchClient.search(searchRequest, TestDocument.class); + assertThat(searchResponse.hits().total().value()).isGreaterThan(0); + + // Verify trace includes query body in DB_STATEMENT with exact match + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "POST"), + // DB_STATEMENT should exactly match the extracted JSON query body + equalTo( + maybeStable(DB_STATEMENT), + "{\"query\":{\"match\":{\"message\":{\"query\":\"?\"}}}}")), + span -> + span.hasName("POST") + .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, "POST"), + satisfies( + URL_FULL, + url -> + url.asString() + .startsWith(httpHost + "/" + INDEX_NAME + "/_search")), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(PEER_SERVICE, "test-peer-service")))); + } + + @Test + void shouldCaptureMsearchQueryBody() throws IOException { + // Execute search query with body + + MsearchRequest msearchRequest = + new MsearchRequest.Builder() + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message") + .value(v -> v.stringValue("message")))))) + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message2") + .value(v -> v.longValue(100L)))))) + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message3") + .value(v -> v.booleanValue(true)))))) + .build(); + + MsearchResponse msearchResponse = + openSearchClient.msearch(msearchRequest, TestDocument.class); + assertThat(msearchResponse.responses().size()).isGreaterThan(0); + + // Verify trace includes query body in DB_STATEMENT with exact match + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "POST"), + // DB_STATEMENT should exactly match the extracted JSON query body + equalTo( + maybeStable(DB_STATEMENT), + "{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message2\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message3\":{\"value\":\"?\"}}}}")), + span -> + span.hasName("POST") + .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, "POST"), + satisfies( + URL_FULL, + url -> + url.asString() + .startsWith( + httpHost + "/" + "_msearch?typed_keys=true")), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(PEER_SERVICE, "test-peer-service")))); + } } 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 index 13999ba6ad43..e579ec25bcad 100644 --- 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 @@ -28,11 +28,11 @@ 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.io.IOException; 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; @@ -64,7 +64,7 @@ class OpenSearchAwsSdk2TransportTest extends AbstractOpenSearchTest { @BeforeAll @Override - void setUp() throws Exception { + void setUp() { server.start(); openSearchClient = buildOpenSearchClient(); openSearchAsyncClient = buildOpenSearchAsyncClient(); @@ -77,8 +77,7 @@ void tearDown() { server.stop(); } - @BeforeEach - void prepTest() { + void setupForHealthResponse() { server.beforeTestExecution(null); // Mock OpenSearch cluster health response @@ -106,13 +105,133 @@ void prepTest() { server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, healthResponse)); } + void setupForSearchResponse() { + server.beforeTestExecution(null); // Added this line + + // Mock OpenSearch Search response, matching the TestDocument class structure + String searchResponseJson = + "{\n" + + " \"took\": 5,\n" + + " \"timed_out\": false,\n" + + " \"_shards\": {\n" + + " \"total\": 1,\n" + + " \"successful\": 1,\n" + + " \"skipped\": 0,\n" + + " \"failed\": 0\n" + + " },\n" + + " \"hits\": {\n" + + " \"total\": {\n" + + " \"value\": 1,\n" + + " \"relation\": \"eq\"\n" + + " },\n" + + " \"max_score\": 1.0,\n" + + " \"hits\": [\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"1\",\n" + + " \"_score\": 1.0,\n" + + " \"_source\": {\n" + + " \"id\": \"doc-1\",\n" // Corrected field + + " \"message\": \"This is a test document.\"\n" // Corrected field + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, searchResponseJson)); + } + + void setupForMsearchResponse() { + server.beforeTestExecution(null); + + String msearchResponseJson = + "{\n" + + " \"took\": 17,\n" + + " \"responses\": [\n" + + " {\n" + + " \"took\": 4,\n" + + " \"timed_out\": false,\n" + + " \"_shards\": {\"total\": 5, \"successful\": 5, \"skipped\": 0, \"failed\": 0},\n" + + " \"hits\": {\n" + + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" + + " \"max_score\": 1.0,\n" + + " \"hits\": [\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-1-1\",\n" + + " \"_score\": 1.0,\n" + + " \"_source\": {\"id\": \"doc-1-1\", \"message\": \"message for search 1 hit 1\"}\n" + + " },\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-1-2\",\n" + + " \"_score\": 0.95,\n" + + " \"_source\": {\"id\": \"doc-1-2\", \"message\": \"message for search 1 hit 2\"}\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"status\": 200\n" + + " },\n" + + " {\n" + + " \"took\": 7,\n" + + " \"timed_out\": false,\n" + + " \"_shards\": {\"total\": 3, \"successful\": 3, \"skipped\": 0, \"failed\": 0},\n" + + " \"hits\": {\n" + + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" + + " \"max_score\": 1.0,\n" + + " \"hits\": [\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-2-1\",\n" + + " \"_score\": 1.0,\n" + + " \"_source\": {\"id\": \"doc-2-1\", \"message\": \"message for search 2 hit 1\"}\n" + + " },\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-2-2\",\n" + + " \"_score\": 0.9,\n" + + " \"_source\": {\"id\": \"doc-2-2\", \"message\": \"message for search 2 hit 2\"}\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"status\": 200\n" + + " },\n" + + " {\n" + + " \"took\": 6,\n" + + " \"timed_out\": false,\n" + + " \"_shards\": {\"total\": 4, \"successful\": 4, \"skipped\": 0, \"failed\": 0},\n" + + " \"hits\": {\n" + + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" + + " \"max_score\": 1.0,\n" + + " \"hits\": [\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-3-1\",\n" + + " \"_score\": 1.0,\n" + + " \"_source\": {\"id\": \"doc-3-1\", \"message\": \"message for search 3 hit 1\"}\n" + + " },\n" + + " {\n" + + " \"_index\": \"my_index\",\n" + + " \"_id\": \"doc-3-2\",\n" + + " \"_score\": 0.8,\n" + + " \"_source\": {\"id\": \"doc-3-2\", \"message\": \"message for search 3 hit 2\"}\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"status\": 200\n" + + " }\n" + + " ]\n" + + "}"; + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, msearchResponseJson)); + } + @Override protected InstrumentationExtension getTesting() { return testing; } @Override - protected OpenSearchClient buildOpenSearchClient() throws Exception { + protected OpenSearchClient buildOpenSearchClient() { SdkHttpClient httpClient = ApacheHttpClient.builder() .buildWithDefaults( @@ -131,7 +250,7 @@ protected OpenSearchClient buildOpenSearchClient() throws Exception { } @Override - protected OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception { + protected OpenSearchAsyncClient buildOpenSearchAsyncClient() { SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() .buildWithDefaults( @@ -149,9 +268,17 @@ protected OpenSearchAsyncClient buildOpenSearchAsyncClient() throws Exception { return new OpenSearchAsyncClient(transport); } + @Test + @Override + void shouldGetStatusWithTraces() throws IOException { + setupForHealthResponse(); + super.shouldGetStatusWithTraces(); + } + @Test @Override void shouldGetStatusAsyncWithTraces() throws Exception { + setupForHealthResponse(); CountDownLatch countDownLatch = new CountDownLatch(1); CompletableFuture responseCompletableFuture = @@ -193,19 +320,36 @@ void shouldGetStatusAsyncWithTraces() throws Exception { 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), - equalTo(PEER_SERVICE, "test-peer-service")), + equalTo(PEER_SERVICE, "test-peer-service"), + equalTo(NETWORK_PEER_ADDRESS, server.httpsEndpoint().host()), + equalTo(NETWORK_PEER_PORT, server.httpsPort())), span -> span.hasName("callback") .hasKind(SpanKind.INTERNAL) .hasParent(trace.getSpan(1)))); } + + @Test + @Override + void shouldRecordMetrics() throws IOException { + setupForHealthResponse(); + super.shouldRecordMetrics(); + } + + @Test + @Override + void shouldCaptureSearchQueryBody() throws IOException { + // Execute search query with body + setupForSearchResponse(); + super.shouldCaptureSearchQueryBody(); + } + + @Test + @Override + void shouldCaptureMsearchQueryBody() throws IOException { + // Execute search query with body + setupForMsearchResponse(); + super.shouldCaptureMsearchQueryBody(); + } } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/TestDocument.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/TestDocument.java new file mode 100644 index 000000000000..85fe78fb3d7b --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/TestDocument.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +public class TestDocument { + private String id; + private String message; + + public TestDocument() {} + + public TestDocument(String id, String message) { + this.id = id; + this.message = message; + } + + public static TestDocument create(String id, String message) { + return new TestDocument(id, message); + } + + public String getId() { + return id; + } + + public String getMessage() { + return message; + } +} From ff7a85c5cf104e0b1dc1e3d9f5b57bbf22a800d0 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Sat, 13 Dec 2025 19:08:22 +0900 Subject: [PATCH 02/13] do not extract query body in case of non-search request --- instrumentation/opensearch/README.md | 7 + .../javaagent/build.gradle.kts | 46 ++- .../v3_0/OpenSearchAttributesGetter.java | 7 +- .../opensearch/v3_0/OpenSearchRequest.java | 3 + .../OpenSearchTransportInstrumentation.java | 7 +- .../v3_0/AbstractOpenSearchTest.java | 94 +----- .../v3_0/OpenSearchAwsSdk2TransportTest.java | 13 +- .../OpenSearchCaptureSearchQueryTest.java | 311 ++++++++++++++++++ 8 files changed, 382 insertions(+), 106 deletions(-) create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchCaptureSearchQueryTest.java diff --git a/instrumentation/opensearch/README.md b/instrumentation/opensearch/README.md index 8bc5f198736d..ca9db818e0a3 100644 --- a/instrumentation/opensearch/README.md +++ b/instrumentation/opensearch/README.md @@ -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. | diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index c76ec7d852cc..febe5688a84d 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -24,14 +24,15 @@ dependencies { 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:netty-nio-client:2.26.0") + testImplementation("software.amazon.awssdk:regions:2.26.0") } tasks { @@ -40,14 +41,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) } } 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 index 7c2c9a0d1f51..3c590d817deb 100644 --- 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 @@ -26,7 +26,12 @@ 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(); + } else { + return request.getBody(); + } } @Override 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 index a43df2395d4a..0b3705166cd0 100644 --- 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 @@ -18,4 +18,7 @@ public static OpenSearchRequest create(String method, String endpoint, @Nullable public abstract String getMethod(); public abstract String getOperation(); + + @Nullable + public abstract String getBody(); } 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 index 7a359a389703..f403e75d2414 100644 --- 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 @@ -22,6 +22,8 @@ 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; @@ -68,9 +70,10 @@ public static AdviceScope start( String queryBody = null; - if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY) { + if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY + && (request instanceof SearchRequest || request instanceof MsearchRequest)) { String rawBody = OpenSearchBodyExtractor.extract(jsonpMapper, request); - queryBody = OpenSearchBodyExtractor.extract(jsonpMapper, rawBody); + queryBody = OpenSearchBodySanitizer.sanitize(rawBody); } OpenSearchRequest 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 index 578fd6ba7ada..ed914b2cf09e 100644 --- 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 @@ -41,8 +41,6 @@ import org.opensearch.client.opensearch._types.query_dsl.Query; import org.opensearch.client.opensearch.cluster.HealthResponse; import org.opensearch.client.opensearch.core.IndexRequest; -import org.opensearch.client.opensearch.core.MsearchRequest; -import org.opensearch.client.opensearch.core.MsearchResponse; import org.opensearch.client.opensearch.core.SearchRequest; import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.indices.CreateIndexRequest; @@ -209,7 +207,7 @@ void shouldRecordMetrics() throws IOException { } @Test - void shouldCaptureSearchQueryBody() throws IOException { + void shouldNotCaptureSearchQueryBodyWhenDisabled() throws IOException { // Execute search query with body SearchRequest searchRequest = SearchRequest.of( @@ -225,7 +223,7 @@ void shouldCaptureSearchQueryBody() throws IOException { openSearchClient.search(searchRequest, TestDocument.class); assertThat(searchResponse.hits().total().value()).isGreaterThan(0); - // Verify trace includes query body in DB_STATEMENT with exact match + // Verify trace does NOT include query body, only method + operation getTesting() .waitAndAssertTraces( trace -> @@ -236,88 +234,13 @@ void shouldCaptureSearchQueryBody() throws IOException { .hasAttributesSatisfyingExactly( equalTo(maybeStable(DB_SYSTEM), "opensearch"), equalTo(maybeStable(DB_OPERATION), "POST"), - // DB_STATEMENT should exactly match the extracted JSON query body - equalTo( - maybeStable(DB_STATEMENT), - "{\"query\":{\"match\":{\"message\":{\"query\":\"?\"}}}}")), - span -> - span.hasName("POST") - .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, "POST"), + // DB_STATEMENT should be method + operation, not JSON body satisfies( - URL_FULL, - url -> - url.asString() - .startsWith(httpHost + "/" + INDEX_NAME + "/_search")), - equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), - equalTo(PEER_SERVICE, "test-peer-service")))); - } - - @Test - void shouldCaptureMsearchQueryBody() throws IOException { - // Execute search query with body - - MsearchRequest msearchRequest = - new MsearchRequest.Builder() - .searches( - s -> - s.header(h -> h.index(INDEX_NAME)) - .body( - b -> - b.query( - q -> - q.term( - t -> - t.field("message") - .value(v -> v.stringValue("message")))))) - .searches( - s -> - s.header(h -> h.index(INDEX_NAME)) - .body( - b -> - b.query( - q -> - q.term( - t -> - t.field("message2") - .value(v -> v.longValue(100L)))))) - .searches( - s -> - s.header(h -> h.index(INDEX_NAME)) - .body( - b -> - b.query( - q -> - q.term( - t -> - t.field("message3") - .value(v -> v.booleanValue(true)))))) - .build(); - - MsearchResponse msearchResponse = - openSearchClient.msearch(msearchRequest, TestDocument.class); - assertThat(msearchResponse.responses().size()).isGreaterThan(0); - - // Verify trace includes query body in DB_STATEMENT with exact match - getTesting() - .waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - span.hasName("POST") - .hasKind(SpanKind.CLIENT) - .hasAttributesSatisfyingExactly( - equalTo(maybeStable(DB_SYSTEM), "opensearch"), - equalTo(maybeStable(DB_OPERATION), "POST"), - // DB_STATEMENT should exactly match the extracted JSON query body - equalTo( maybeStable(DB_STATEMENT), - "{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message2\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message3\":{\"value\":\"?\"}}}}")), + statement -> + statement + .asString() + .startsWith("POST /" + INDEX_NAME + "/_search"))), span -> span.hasName("POST") .hasKind(SpanKind.CLIENT) @@ -331,8 +254,7 @@ void shouldCaptureMsearchQueryBody() throws IOException { URL_FULL, url -> url.asString() - .startsWith( - httpHost + "/" + "_msearch?typed_keys=true")), + .startsWith(httpHost + "/" + INDEX_NAME + "/_search")), equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), equalTo(PEER_SERVICE, "test-peer-service")))); } 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 index e579ec25bcad..6106cb5c9bf0 100644 --- 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 @@ -339,17 +339,8 @@ void shouldRecordMetrics() throws IOException { @Test @Override - void shouldCaptureSearchQueryBody() throws IOException { - // Execute search query with body + void shouldNotCaptureSearchQueryBodyWhenDisabled() throws IOException { setupForSearchResponse(); - super.shouldCaptureSearchQueryBody(); - } - - @Test - @Override - void shouldCaptureMsearchQueryBody() throws IOException { - // Execute search query with body - setupForMsearchResponse(); - super.shouldCaptureMsearchQueryBody(); + super.shouldNotCaptureSearchQueryBodyWhenDisabled(); } } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchCaptureSearchQueryTest.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchCaptureSearchQueryTest.java new file mode 100644 index 000000000000..01af90df888b --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchCaptureSearchQueryTest.java @@ -0,0 +1,311 @@ +/* + * 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.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +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 io.opentelemetry.semconv.incubating.PeerIncubatingAttributes.PEER_SERVICE; +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 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.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.OpenSearchClient; +import org.opensearch.client.opensearch._types.mapping.TypeMapping; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch.core.IndexRequest; +import org.opensearch.client.opensearch.core.MsearchRequest; +import org.opensearch.client.opensearch.core.MsearchResponse; +import org.opensearch.client.opensearch.core.SearchRequest; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; +import org.opensearch.testcontainers.OpensearchContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Tests for capture-search-query=true configuration. This test class runs with + * -Dotel.instrumentation.opensearch.capture-search-query=true and verifies that query bodies are + * captured in DB_STATEMENT. + */ +@SuppressWarnings("deprecation") // using deprecated semconv +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class OpenSearchCaptureSearchQueryTest { + + private static final String INDEX_NAME = "test-search-index"; + private OpenSearchClient openSearchClient; + private OpensearchContainer opensearch; + private URI httpHost; + + @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 -Dlog4j2.disableJmx=true -Dlog4j2.disable.jmx=true -XX:-UseContainerSupport"); + opensearch.start(); + httpHost = URI.create(opensearch.getHttpHostAddress()); + openSearchClient = buildOpenSearchClient(); + + String documentId = "test-doc-1"; + + CreateIndexRequest createIndexRequest = + CreateIndexRequest.of( + c -> + c.index(INDEX_NAME) + .mappings( + TypeMapping.of( + t -> + t.properties( + "message", + p -> + p.text(txt -> txt.fielddata(true).analyzer("standard")))))); + + openSearchClient.indices().create(createIndexRequest); + + TestDocument testDocument = TestDocument.create(documentId, "test message for search"); + IndexRequest indexRequest = + new IndexRequest.Builder().index(INDEX_NAME).document(testDocument).build(); + + openSearchClient.index(indexRequest); + openSearchClient.indices().refresh(r -> r.index(INDEX_NAME)); + } + + private 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); + } + + @AfterAll + void tearDown() { + opensearch.stop(); + } + + @Test + void shouldCaptureSearchQueryBody() throws IOException { + SearchRequest searchRequest = + SearchRequest.of( + s -> + s.index(INDEX_NAME) + .query( + Query.of( + q -> + q.match( + m -> m.field("message").query(v -> v.stringValue("test")))))); + + SearchResponse searchResponse = + openSearchClient.search(searchRequest, TestDocument.class); + assertThat(searchResponse.hits().total().value()).isGreaterThan(0); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "POST"), + equalTo( + maybeStable(DB_STATEMENT), + "{\"query\":{\"match\":{\"message\":{\"query\":\"?\"}}}}")), + span -> + span.hasName("POST") + .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, "POST"), + satisfies( + URL_FULL, + url -> + url.asString() + .startsWith(httpHost + "/" + INDEX_NAME + "/_search")), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(PEER_SERVICE, "test-peer-service")))); + } + + @Test + void shouldCaptureMsearchQueryBody() throws IOException { + MsearchRequest msearchRequest = + new MsearchRequest.Builder() + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message") + .value(v -> v.stringValue("message")))))) + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message2") + .value(v -> v.longValue(100L)))))) + .searches( + s -> + s.header(h -> h.index(INDEX_NAME)) + .body( + b -> + b.query( + q -> + q.term( + t -> + t.field("message3") + .value(v -> v.booleanValue(true)))))) + .build(); + + MsearchResponse msearchResponse = + openSearchClient.msearch(msearchRequest, TestDocument.class); + assertThat(msearchResponse.responses().size()).isGreaterThan(0); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "POST"), + equalTo( + maybeStable(DB_STATEMENT), + "{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message2\":{\"value\":\"?\"}}}};{\"index\":[\"?\"]};{\"query\":{\"term\":{\"message3\":{\"value\":\"?\"}}}}")), + span -> + span.hasName("POST") + .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, "POST"), + satisfies( + URL_FULL, + url -> + url.asString() + .startsWith( + httpHost + "/" + "_msearch?typed_keys=true")), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200L), + equalTo(PEER_SERVICE, "test-peer-service")))); + } + + @Test + void shouldNotCaptureIndexQueryBody() throws IOException { + TestDocument testDocument = TestDocument.create("test-doc-2", "index body test message"); + IndexRequest indexRequest = + new IndexRequest.Builder().index(INDEX_NAME).document(testDocument).build(); + + openSearchClient.index(indexRequest); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("POST") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), "opensearch"), + equalTo(maybeStable(DB_OPERATION), "POST"), + equalTo(maybeStable(DB_STATEMENT), "POST /test-search-index/_doc")), + span -> + span.hasName("POST") + .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, "POST"), + satisfies( + URL_FULL, + url -> + url.asString() + .startsWith(httpHost + "/" + INDEX_NAME + "/_doc")), + equalTo(HTTP_RESPONSE_STATUS_CODE, 201L), + equalTo(PEER_SERVICE, "test-peer-service")))); + } +} From 5c487335bcea9dcbf22004a79ef4618968fc38c7 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Sat, 13 Dec 2025 19:18:45 +0900 Subject: [PATCH 03/13] logging extraction and sanitization failures --- .../opensearch/v3_0/OpenSearchBodyExtractor.java | 8 +++++++- .../opensearch/v3_0/OpenSearchBodySanitizer.java | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java index df6b22095933..4f318df9b61e 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java @@ -5,10 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; +import static java.util.logging.Level.FINE; + 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.apache.hc.core5.http.ContentType; import org.opensearch.client.json.JsonpMapper; @@ -17,6 +20,8 @@ public class OpenSearchBodyExtractor { + private static final Logger logger = Logger.getLogger(OpenSearchBodyExtractor.class.getName()); + @Nullable public static String extract(JsonpMapper mapper, Object request) { try { @@ -35,6 +40,7 @@ public static String extract(JsonpMapper mapper, Object request) { String body = baos.toString(StandardCharsets.UTF_8); return body.isEmpty() ? null : body; } catch (RuntimeException e) { + logger.log(FINE, "Failure extracting body", e); return null; } } @@ -56,7 +62,7 @@ private static void writeNdJson( } } } catch (RuntimeException e) { - // Ignore + logger.log(FINE, "Failure serializing NdJson", e); } } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java index dbf791dc2a39..0a7c25f1bc3c 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java @@ -5,6 +5,8 @@ package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; +import static java.util.logging.Level.FINE; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -13,9 +15,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.logging.Logger; public class OpenSearchBodySanitizer { + private static final Logger logger = Logger.getLogger(OpenSearchBodySanitizer.class.getName()); + private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); private static final String MASKED_VALUE = "?"; private static final OpenSearchBodySanitizer DEFAULT_INSTANCE = @@ -68,6 +73,7 @@ private String sanitizeSingleQuery(String query) { JsonNode sanitizedNode = sanitizeNode(rootNode); return objectMapper.writeValueAsString(sanitizedNode); } catch (Exception e) { + logger.log(FINE, "Failure sanitizing single query", e); return query; } } From fa40f55ed4cea1da111181d668eaede16a291345 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Sun, 14 Dec 2025 17:39:11 +0900 Subject: [PATCH 04/13] fix markdown formatting and remove specific jackson-databind version --- instrumentation/opensearch/README.md | 8 ++++---- .../opensearch-java-3.0/javaagent/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/instrumentation/opensearch/README.md b/instrumentation/opensearch/README.md index ca9db818e0a3..85eeb46e0504 100644 --- a/instrumentation/opensearch/README.md +++ b/instrumentation/opensearch/README.md @@ -6,7 +6,7 @@ ## 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. | +| 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. | diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index febe5688a84d..6dc647ffc66e 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -18,7 +18,7 @@ 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") - implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") + implementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("org.opensearch.client:opensearch-rest-client:3.0.0") testImplementation(project(":instrumentation:opensearch:opensearch-rest-common:testing")) From 29a2c2ddd4133f6ba5e49f829ab5422286b1baa4 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 17 Dec 2025 12:59:09 +0900 Subject: [PATCH 05/13] fix: remove duplicated and unused code - remove duplicated code - fix wrong comment --- .../KafkaProducerAttributesExtractor.java | 1 + .../javaagent/build.gradle.kts | 1 - .../v3_0/OpenSearchAttributesGetter.java | 3 +- .../v3_0/OpenSearchBodyExtractor.java | 5 +- .../v3_0/OpenSearchBodySanitizer.java | 14 +--- .../opensearch/v3_0/QuerySplitter.java | 6 +- .../v3_0/OpenSearchAwsSdk2TransportTest.java | 84 ------------------- 7 files changed, 8 insertions(+), 106 deletions(-) diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java index ba483967270d..88acbbbbf603 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java @@ -53,6 +53,7 @@ public void onEnd( @Nullable Throwable error) { if (recordMetadata != null) { + recordMetadata.serializedValueSize(); attributes.put( MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(recordMetadata.partition())); attributes.put(MESSAGING_KAFKA_MESSAGE_OFFSET, recordMetadata.offset()); diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index 6dc647ffc66e..f900e4b2f1a6 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -31,7 +31,6 @@ dependencies { 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:netty-nio-client:2.26.0") testImplementation("software.amazon.awssdk:regions:2.26.0") } 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 index 3c590d817deb..5be00473c8f9 100644 --- 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 @@ -29,9 +29,8 @@ public String getDbQueryText(OpenSearchRequest request) { // keep the previous logic in case of failure to extract the query body if (request.getBody() == null) { return request.getMethod() + " " + request.getOperation(); - } else { - return request.getBody(); } + return request.getBody(); } @Override diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java index 4f318df9b61e..10c88d5ab353 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java @@ -13,12 +13,11 @@ import java.util.Iterator; import java.util.logging.Logger; import javax.annotation.Nullable; -import org.apache.hc.core5.http.ContentType; import org.opensearch.client.json.JsonpMapper; import org.opensearch.client.json.NdJsonpSerializable; import org.opensearch.client.transport.GenericSerializable; -public class OpenSearchBodyExtractor { +public final class OpenSearchBodyExtractor { private static final Logger logger = Logger.getLogger(OpenSearchBodyExtractor.class.getName()); @@ -30,7 +29,7 @@ public static String extract(JsonpMapper mapper, Object request) { if (request instanceof NdJsonpSerializable) { writeNdJson(mapper, (NdJsonpSerializable) request, baos); } else if (request instanceof GenericSerializable) { - ContentType.parse(((GenericSerializable) request).serialize(baos)); + ((GenericSerializable) request).serialize(baos); } else { JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); mapper.serialize(request, generator); diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java index 0a7c25f1bc3c..d44c73ed9b9b 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.logging.Logger; -public class OpenSearchBodySanitizer { +public final class OpenSearchBodySanitizer { private static final Logger logger = Logger.getLogger(OpenSearchBodySanitizer.class.getName()); @@ -32,18 +32,6 @@ private OpenSearchBodySanitizer(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - public static OpenSearchBodySanitizer create() { - return new OpenSearchBodySanitizer(DEFAULT_OBJECT_MAPPER); - } - - public static OpenSearchBodySanitizer create(ObjectMapper objectMapper) { - return new OpenSearchBodySanitizer(objectMapper); - } - - public static OpenSearchBodySanitizer getDefault() { - return DEFAULT_INSTANCE; - } - public static String sanitize(String jsonString) { return DEFAULT_INSTANCE.sanitizeInstance(jsonString); } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java index 5ef95cff21d1..77d87f7e4a40 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java @@ -10,8 +10,8 @@ import java.util.List; /** - * Splits multiple queries separated by semicolons. Single Responsibility: Only responsible for - * query separation logic. + * Splits and joins queries for newline-delimited JSON (nd-json) format. Splits input by newlines + * and joins output with semicolons for display. */ class QuerySplitter { @@ -21,7 +21,7 @@ class QuerySplitter { private QuerySplitter() {} /** - * Splits a string containing multiple queries separated by semicolons. + * Splits a string containing multiple queries separated by newlines. * * @param queriesString input string containing queries * @return list of individual query strings, empty if input is null or empty 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 index 6106cb5c9bf0..953aa4d141e0 100644 --- 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 @@ -141,90 +141,6 @@ void setupForSearchResponse() { server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, searchResponseJson)); } - void setupForMsearchResponse() { - server.beforeTestExecution(null); - - String msearchResponseJson = - "{\n" - + " \"took\": 17,\n" - + " \"responses\": [\n" - + " {\n" - + " \"took\": 4,\n" - + " \"timed_out\": false,\n" - + " \"_shards\": {\"total\": 5, \"successful\": 5, \"skipped\": 0, \"failed\": 0},\n" - + " \"hits\": {\n" - + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" - + " \"max_score\": 1.0,\n" - + " \"hits\": [\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-1-1\",\n" - + " \"_score\": 1.0,\n" - + " \"_source\": {\"id\": \"doc-1-1\", \"message\": \"message for search 1 hit 1\"}\n" - + " },\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-1-2\",\n" - + " \"_score\": 0.95,\n" - + " \"_source\": {\"id\": \"doc-1-2\", \"message\": \"message for search 1 hit 2\"}\n" - + " }\n" - + " ]\n" - + " },\n" - + " \"status\": 200\n" - + " },\n" - + " {\n" - + " \"took\": 7,\n" - + " \"timed_out\": false,\n" - + " \"_shards\": {\"total\": 3, \"successful\": 3, \"skipped\": 0, \"failed\": 0},\n" - + " \"hits\": {\n" - + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" - + " \"max_score\": 1.0,\n" - + " \"hits\": [\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-2-1\",\n" - + " \"_score\": 1.0,\n" - + " \"_source\": {\"id\": \"doc-2-1\", \"message\": \"message for search 2 hit 1\"}\n" - + " },\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-2-2\",\n" - + " \"_score\": 0.9,\n" - + " \"_source\": {\"id\": \"doc-2-2\", \"message\": \"message for search 2 hit 2\"}\n" - + " }\n" - + " ]\n" - + " },\n" - + " \"status\": 200\n" - + " },\n" - + " {\n" - + " \"took\": 6,\n" - + " \"timed_out\": false,\n" - + " \"_shards\": {\"total\": 4, \"successful\": 4, \"skipped\": 0, \"failed\": 0},\n" - + " \"hits\": {\n" - + " \"total\": { \"value\": 2, \"relation\": \"eq\" },\n" - + " \"max_score\": 1.0,\n" - + " \"hits\": [\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-3-1\",\n" - + " \"_score\": 1.0,\n" - + " \"_source\": {\"id\": \"doc-3-1\", \"message\": \"message for search 3 hit 1\"}\n" - + " },\n" - + " {\n" - + " \"_index\": \"my_index\",\n" - + " \"_id\": \"doc-3-2\",\n" - + " \"_score\": 0.8,\n" - + " \"_source\": {\"id\": \"doc-3-2\", \"message\": \"message for search 3 hit 2\"}\n" - + " }\n" - + " ]\n" - + " },\n" - + " \"status\": 200\n" - + " }\n" - + " ]\n" - + "}"; - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, msearchResponseJson)); - } - @Override protected InstrumentationExtension getTesting() { return testing; From 864c25fc9a070454d236e20de6cc3f12baf23c1e Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 17 Dec 2025 13:00:26 +0900 Subject: [PATCH 06/13] add overhead description --- instrumentation/opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opensearch/README.md b/instrumentation/opensearch/README.md index 85eeb46e0504..5289906d9e34 100644 --- a/instrumentation/opensearch/README.md +++ b/instrumentation/opensearch/README.md @@ -9,4 +9,4 @@ | 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. | +| `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. | From ccc184642a23b3ee718e728bff5491d3eab29202 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Fri, 19 Dec 2025 09:30:31 +0900 Subject: [PATCH 07/13] remove unrelated code --- .../common/v0_11/internal/KafkaProducerAttributesExtractor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java index 88acbbbbf603..ba483967270d 100644 --- a/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java +++ b/instrumentation/kafka/kafka-clients/kafka-clients-common-0.11/library/src/main/java/io/opentelemetry/instrumentation/kafkaclients/common/v0_11/internal/KafkaProducerAttributesExtractor.java @@ -53,7 +53,6 @@ public void onEnd( @Nullable Throwable error) { if (recordMetadata != null) { - recordMetadata.serializedValueSize(); attributes.put( MESSAGING_DESTINATION_PARTITION_ID, String.valueOf(recordMetadata.partition())); attributes.put(MESSAGING_KAFKA_MESSAGE_OFFSET, recordMetadata.offset()); From a2c772e48acf3cedc9d28be6aac35486d54260ee Mon Sep 17 00:00:00 2001 From: Minje Park Date: Sun, 21 Dec 2025 17:55:04 +0900 Subject: [PATCH 08/13] change implementation to compileOnly --- .../opensearch/opensearch-java-3.0/javaagent/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index f900e4b2f1a6..a5df2e7624e4 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -18,7 +18,7 @@ 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") - implementation("com.fasterxml.jackson.core:jackson-databind") + compileOnly("com.fasterxml.jackson.core:jackson-databind") testImplementation("org.opensearch.client:opensearch-rest-client:3.0.0") testImplementation(project(":instrumentation:opensearch:opensearch-rest-common:testing")) From 0d2452b2033ed7929618eb19ef7b730bca2b6d76 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 24 Dec 2025 10:51:24 +0900 Subject: [PATCH 09/13] sanitize query with JsonGenerator --- .../javaagent/build.gradle.kts | 2 +- .../v3_0/OpenSearchBodyExtractor.java | 82 +++++-- .../v3_0/OpenSearchBodySanitizer.java | 104 --------- .../OpenSearchTransportInstrumentation.java | 3 +- .../opensearch/v3_0/QuerySplitter.java | 60 ----- .../v3_0/SanitizingJacksonJsonGenerator.java | 99 +++++++++ .../v3_0/SanitizingJsonGenerator.java | 206 ++++++++++++++++++ 7 files changed, 371 insertions(+), 185 deletions(-) delete mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java delete mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJacksonJsonGenerator.java create mode 100644 instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJsonGenerator.java diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts index a5df2e7624e4..fe29fd5879de 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts @@ -18,7 +18,7 @@ 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-databind") + 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")) diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java index 10c88d5ab353..daafffe12ed5 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java @@ -7,6 +7,7 @@ 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; @@ -15,53 +16,98 @@ 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 extract(JsonpMapper mapper, Object request) { + public static String extractSanitized(JsonpMapper mapper, Object request) { try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (request instanceof NdJsonpSerializable) { - writeNdJson(mapper, (NdJsonpSerializable) request, baos); + return serializeNdJsonSanitized(mapper, (NdJsonpSerializable) request); } else 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; } else { - JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); - mapper.serialize(request, generator); - generator.close(); + return serializeSanitized(mapper, request); } - - String body = baos.toString(StandardCharsets.UTF_8); - return body.isEmpty() ? null : body; } catch (RuntimeException e) { logger.log(FINE, "Failure extracting body", e); return null; } } - private static void writeNdJson( - JsonpMapper mapper, NdJsonpSerializable value, ByteArrayOutputStream baos) { + @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) { - // do not recurse on the item itself - writeNdJson(mapper, (NdJsonpSerializable) item, baos); + // Recursively handle nested NdJsonpSerializable + itemStr = serializeNdJsonSanitized(mapper, (NdJsonpSerializable) item); } else { - JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); - mapper.serialize(item, generator); - generator.close(); - baos.write('\n'); + 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; } } diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java deleted file mode 100644 index d44c73ed9b9b..000000000000 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodySanitizer.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -public final class OpenSearchBodySanitizer { - - private static final Logger logger = Logger.getLogger(OpenSearchBodySanitizer.class.getName()); - - private static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper(); - private static final String MASKED_VALUE = "?"; - private static final OpenSearchBodySanitizer DEFAULT_INSTANCE = - new OpenSearchBodySanitizer(DEFAULT_OBJECT_MAPPER); - - private final ObjectMapper objectMapper; - - private OpenSearchBodySanitizer(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - public static String sanitize(String jsonString) { - return DEFAULT_INSTANCE.sanitizeInstance(jsonString); - } - - public String sanitizeInstance(String jsonString) { - if (jsonString == null) { - return null; - } - - List queries = QuerySplitter.splitQueries(jsonString); - if (queries.isEmpty()) { - return null; - } - - List sanitizedQueries = new ArrayList<>(); - for (String query : queries) { - String sanitized = sanitizeSingleQuery(query); - sanitizedQueries.add(sanitized); - } - - return QuerySplitter.joinQueries(sanitizedQueries); - } - - private String sanitizeSingleQuery(String query) { - try { - JsonNode rootNode = objectMapper.readTree(query); - JsonNode sanitizedNode = sanitizeNode(rootNode); - return objectMapper.writeValueAsString(sanitizedNode); - } catch (Exception e) { - logger.log(FINE, "Failure sanitizing single query", e); - return query; - } - } - - private JsonNode sanitizeNode(JsonNode node) { - if (node == null || node.isNull()) { - return node; - } - - if (node.isTextual()) { - return new TextNode(MASKED_VALUE); - } - - if (node.isNumber() || node.isBoolean()) { - return new TextNode(MASKED_VALUE); - } - - if (node.isArray()) { - ArrayNode arrayNode = objectMapper.createArrayNode(); - for (JsonNode element : node) { - arrayNode.add(sanitizeNode(element)); - } - return arrayNode; - } - - if (node.isObject()) { - ObjectNode objectNode = objectMapper.createObjectNode(); - - for (Map.Entry field : node.properties()) { - String key = field.getKey(); - JsonNode value = field.getValue(); - - objectNode.set(key, sanitizeNode(value)); - } - return objectNode; - } - - return node; - } -} 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 index f403e75d2414..0edd39bcd7af 100644 --- 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 @@ -72,8 +72,7 @@ public static AdviceScope start( if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY && (request instanceof SearchRequest || request instanceof MsearchRequest)) { - String rawBody = OpenSearchBodyExtractor.extract(jsonpMapper, request); - queryBody = OpenSearchBodySanitizer.sanitize(rawBody); + queryBody = OpenSearchBodyExtractor.extractSanitized(jsonpMapper, request); } OpenSearchRequest otelRequest = diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java deleted file mode 100644 index 77d87f7e4a40..000000000000 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/QuerySplitter.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Splits and joins queries for newline-delimited JSON (nd-json) format. Splits input by newlines - * and joins output with semicolons for display. - */ -class QuerySplitter { - - private static final String QUERY_SEPARATOR = "\n"; - private static final String QUERY_COMBINATOR = ";"; - - private QuerySplitter() {} - - /** - * Splits a string containing multiple queries separated by newlines. - * - * @param queriesString input string containing queries - * @return list of individual query strings, empty if input is null or empty - */ - static List splitQueries(String queriesString) { - if (queriesString == null || queriesString.trim().isEmpty()) { - return Collections.emptyList(); - } - - String[] queries = queriesString.split(QUERY_SEPARATOR, -1); - List result = new ArrayList<>(); - - for (String query : queries) { - String trimmed = query.trim(); - if (!trimmed.isEmpty()) { - result.add(trimmed); - } - } - - return result; - } - - /** - * Joins multiple sanitized queries back into a single string. - * - * @param sanitizedQueries list of sanitized query strings - * @return joined string with semicolon separator, or null if list is empty - */ - static String joinQueries(List sanitizedQueries) { - if (sanitizedQueries == null || sanitizedQueries.isEmpty()) { - return null; - } - - return String.join(QUERY_COMBINATOR, sanitizedQueries); - } -} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJacksonJsonGenerator.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJacksonJsonGenerator.java new file mode 100644 index 000000000000..a919dd54b49a --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJacksonJsonGenerator.java @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.JsonGeneratorDelegate; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * A Jackson JsonGenerator wrapper that sanitizes all literal values by replacing them with "?". + * This is used to sanitize OpenSearch query bodies before they are captured as span attributes, + * ensuring that sensitive data is not exposed in telemetry. + */ +final class SanitizingJacksonJsonGenerator extends JsonGeneratorDelegate { + + private static final String MASKED_VALUE = "?"; + + SanitizingJacksonJsonGenerator(JsonGenerator delegate) { + super(delegate); + } + + // String value methods - sanitize + + @Override + public void writeString(String value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeString(char[] buffer, int offset, int length) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeRawUTF8String(byte[] buffer, int offset, int length) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeUTF8String(byte[] buffer, int offset, int length) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + // Number value methods - sanitize + + @Override + public void writeNumber(int value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(long value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(float value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(double value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(BigInteger value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(BigDecimal value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + @Override + public void writeNumber(String encodedValue) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + // Boolean value methods - sanitize + + @Override + public void writeBoolean(boolean value) throws IOException { + delegate.writeString(MASKED_VALUE); + } + + // Null value methods - sanitize + + @Override + public void writeNull() throws IOException { + delegate.writeString(MASKED_VALUE); + } +} diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJsonGenerator.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJsonGenerator.java new file mode 100644 index 000000000000..8970a7cf6796 --- /dev/null +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/SanitizingJsonGenerator.java @@ -0,0 +1,206 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0; + +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerator; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** A JsonGenerator wrapper that sanitizes literal values by replacing them with "?". */ +final class SanitizingJsonGenerator implements JsonGenerator { + + private static final String MASKED_VALUE = "?"; + + private final JsonGenerator delegate; + + SanitizingJsonGenerator(JsonGenerator delegate) { + this.delegate = delegate; + } + + // Structure methods - delegate and return this for chaining + + @Override + public JsonGenerator writeStartObject() { + delegate.writeStartObject(); + return this; + } + + @Override + public JsonGenerator writeStartObject(String name) { + delegate.writeStartObject(name); + return this; + } + + @Override + public JsonGenerator writeStartArray() { + delegate.writeStartArray(); + return this; + } + + @Override + public JsonGenerator writeStartArray(String name) { + delegate.writeStartArray(name); + return this; + } + + @Override + public JsonGenerator writeEnd() { + delegate.writeEnd(); + return this; + } + + @Override + public JsonGenerator writeKey(String name) { + delegate.writeKey(name); + return this; + } + + // All write() overloads grouped together - sanitize values and return this + + @Override + public JsonGenerator write(String value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(int value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(long value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(double value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(BigInteger value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(BigDecimal value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(boolean value) { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(JsonValue value) { + writeJsonValue(value); + return this; + } + + @Override + public JsonGenerator write(String name, String value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, int value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, long value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, double value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, BigInteger value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, BigDecimal value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, boolean value) { + delegate.write(name, MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator write(String name, JsonValue value) { + delegate.writeKey(name); + writeJsonValue(value); + return this; + } + + // All writeNull() overloads grouped together + + @Override + public JsonGenerator writeNull() { + delegate.write(MASKED_VALUE); + return this; + } + + @Override + public JsonGenerator writeNull(String name) { + delegate.write(name, MASKED_VALUE); + return this; + } + + private void writeJsonValue(JsonValue value) { + switch (value.getValueType()) { + case OBJECT: + delegate.writeStartObject(); + value.asJsonObject().forEach((k, v) -> write(k, v)); + delegate.writeEnd(); + break; + case ARRAY: + delegate.writeStartArray(); + value.asJsonArray().forEach(this::write); + delegate.writeEnd(); + break; + case STRING: + case NUMBER: + case TRUE: + case FALSE: + case NULL: + delegate.write(MASKED_VALUE); + break; + } + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public void flush() { + delegate.flush(); + } +} From 5283ac7c848976efcc957392e10b478715269aa9 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 24 Dec 2025 10:56:44 +0900 Subject: [PATCH 10/13] fix Readme.md format --- instrumentation/opensearch/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instrumentation/opensearch/README.md b/instrumentation/opensearch/README.md index 5289906d9e34..f3821355214a 100644 --- a/instrumentation/opensearch/README.md +++ b/instrumentation/opensearch/README.md @@ -6,7 +6,7 @@ ## 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. | +| 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. | From c4bae7b9a61fecf3574c46c4ffbac3a2cf83293b Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 7 Jan 2026 09:56:21 +0900 Subject: [PATCH 11/13] refactor: replace else-if chain with early return pattern in OpenSearchBodyExtractor --- .../opensearch/v3_0/OpenSearchBodyExtractor.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java index daafffe12ed5..45f219d2ab70 100644 --- a/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java +++ b/instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchBodyExtractor.java @@ -31,16 +31,18 @@ public static String extractSanitized(JsonpMapper mapper, Object request) { try { if (request instanceof NdJsonpSerializable) { return serializeNdJsonSanitized(mapper, (NdJsonpSerializable) request); - } else if (request instanceof GenericSerializable) { + } + + 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; - } else { - return serializeSanitized(mapper, request); } + + return serializeSanitized(mapper, request); } catch (RuntimeException e) { logger.log(FINE, "Failure extracting body", e); return null; From 01ef0ea5971072994e193cacdcfab3ac6b86caae Mon Sep 17 00:00:00 2001 From: Minje Park Date: Wed, 7 Jan 2026 13:04:09 +0900 Subject: [PATCH 12/13] fix: replace AgentInstrumentationConfig with ConfigPropertiesUtil --- .../opensearch/v3_0/OpenSearchSingletons.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index a34a8e548da0..c5ec7c28a727 100644 --- 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 @@ -11,14 +11,14 @@ import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; public final class OpenSearchSingletons { private static final Instrumenter INSTRUMENTER = createInstrumenter(); public static final boolean CAPTURE_SEARCH_QUERY = - AgentInstrumentationConfig.get() - .getBoolean("otel.instrumentation.opensearch.capture-search-query", false); + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.opensearch.capture-search-query", false); public static Instrumenter instrumenter() { return INSTRUMENTER; From 0bad4116f91bf644d322fbb1eff4c4a7d2d44b52 Mon Sep 17 00:00:00 2001 From: Minje Park Date: Thu, 8 Jan 2026 21:38:12 +0900 Subject: [PATCH 13/13] refactor: migrate OpenSearch config to DeclarativeConfigUtil --- .../opensearch/v3_0/OpenSearchSingletons.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index c5ec7c28a727..9f0a7eaa233d 100644 --- 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 @@ -6,19 +6,19 @@ 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; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; public final class OpenSearchSingletons { private static final Instrumenter INSTRUMENTER = createInstrumenter(); public static final boolean CAPTURE_SEARCH_QUERY = - ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.opensearch.capture-search-query", false); + DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "opensearch") + .getBoolean("capture_search_query", false); public static Instrumenter instrumenter() { return INSTRUMENTER;