Skip to content

Commit 6e1f950

Browse files
mznetlaurit
andauthored
Opensearch transport query sanitization (#15634)
Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
1 parent 1748645 commit 6e1f950

15 files changed

Lines changed: 1039 additions & 26 deletions

instrumentation/opensearch/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@
33
| System property | Type | Default | Description |
44
| -------------------------------------------------------------- | ------- | ------- | --------------------------------------------------- |
55
| `otel.instrumentation.opensearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |
6+
7+
## Settings for the [OpenSearch Java Client](https://docs.opensearch.org/latest/clients/java/) instrumentation
8+
9+
| System property | Type | Default | Description |
10+
|----------------------------------------------------------------|---------|---------|------------------------------------------------------|
11+
| `otel.instrumentation.opensearch.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |
12+
| `otel.instrumentation.opensearch.capture-search-query` | Boolean | `true` | Enable the capture of sanitized search query bodies. |

instrumentation/opensearch/opensearch-java-3.0/javaagent/build.gradle.kts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@ dependencies {
1818
library("org.opensearch.client:opensearch-java:3.0.0")
1919
compileOnly("com.google.auto.value:auto-value-annotations")
2020
annotationProcessor("com.google.auto.value:auto-value")
21+
compileOnly("com.fasterxml.jackson.core:jackson-core")
2122

2223
testImplementation("org.opensearch.client:opensearch-rest-client:3.0.0")
2324
testImplementation(project(":instrumentation:opensearch:opensearch-rest-common:testing"))
2425
testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent"))
2526

26-
// For testing AwsSdk2Transport
27+
// AwsSdk2Transport supports awssdk version 2.26.0
2728
testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent"))
2829
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
29-
testImplementation("software.amazon.awssdk:auth:2.22.0")
30-
testImplementation("software.amazon.awssdk:identity-spi:2.22.0")
31-
testImplementation("software.amazon.awssdk:apache-client:2.22.0")
32-
testImplementation("software.amazon.awssdk:netty-nio-client:2.22.0")
33-
testImplementation("software.amazon.awssdk:regions:2.22.0")
30+
testImplementation("software.amazon.awssdk:auth:2.26.0")
31+
testImplementation("software.amazon.awssdk:identity-spi:2.26.0")
32+
testImplementation("software.amazon.awssdk:apache-client:2.26.0")
33+
testImplementation("software.amazon.awssdk:netty-nio-client:2.26.0")
34+
testImplementation("software.amazon.awssdk:regions:2.26.0")
3435
}
3536

3637
tasks {
@@ -39,14 +40,34 @@ tasks {
3940
systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false")
4041
}
4142

43+
test {
44+
filter {
45+
excludeTestsMatching("OpenSearchDisabledCaptureSearchQueryTest")
46+
}
47+
}
48+
49+
val testDisabledCaptureSearchQuery by registering(Test::class) {
50+
testClassesDirs = sourceSets.test.get().output.classesDirs
51+
classpath = sourceSets.test.get().runtimeClasspath
52+
53+
filter {
54+
includeTestsMatching("OpenSearchDisabledCaptureSearchQueryTest")
55+
}
56+
jvmArgs("-Dotel.instrumentation.opensearch.capture-search-query=false")
57+
}
58+
4259
val testStableSemconv by registering(Test::class) {
4360
testClassesDirs = sourceSets.test.get().output.classesDirs
4461
classpath = sourceSets.test.get().runtimeClasspath
62+
63+
filter {
64+
excludeTestsMatching("OpenSearchDisabledCaptureSearchQueryTest")
65+
}
4566
jvmArgs("-Dotel.semconv-stability.opt-in=database")
4667
systemProperty("metadataConfig", "otel.semconv-stability.opt-in=database")
4768
}
4869

4970
check {
50-
dependsOn(testStableSemconv)
71+
dependsOn(testStableSemconv, testDisabledCaptureSearchQuery)
5172
}
5273
}

instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchAttributesGetter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ public String getDbNamespace(OpenSearchRequest request) {
2626
@Override
2727
@Nullable
2828
public String getDbQueryText(OpenSearchRequest request) {
29-
return request.getMethod() + " " + request.getEndpoint();
29+
if (request.getBody() == null) {
30+
// fall back to method and endpoint if capturing the query body is disabled or if the body is
31+
// not available
32+
return request.getMethod() + " " + request.getEndpoint();
33+
}
34+
return request.getBody();
3035
}
3136

3237
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;
7+
8+
import static java.nio.charset.StandardCharsets.UTF_8;
9+
import static java.util.logging.Level.FINE;
10+
11+
import com.fasterxml.jackson.core.JsonFactory;
12+
import jakarta.json.stream.JsonGenerator;
13+
import java.io.ByteArrayOutputStream;
14+
import java.io.IOException;
15+
import java.util.Iterator;
16+
import java.util.logging.Logger;
17+
import javax.annotation.Nullable;
18+
import org.opensearch.client.json.JsonpMapper;
19+
import org.opensearch.client.json.NdJsonpSerializable;
20+
import org.opensearch.client.json.jackson.JacksonJsonpGenerator;
21+
import org.opensearch.client.json.jackson.JacksonJsonpMapper;
22+
23+
public final class OpenSearchBodyExtractor {
24+
25+
private static final Logger logger = Logger.getLogger(OpenSearchBodyExtractor.class.getName());
26+
private static final String QUERY_SEPARATOR = ";";
27+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
28+
29+
@Nullable
30+
public static String extractSanitized(JsonpMapper mapper, Object request) {
31+
try {
32+
if (request instanceof NdJsonpSerializable) {
33+
return serializeNdJsonSanitized(mapper, (NdJsonpSerializable) request);
34+
}
35+
36+
return serializeSanitized(mapper, request);
37+
} catch (Exception exception) {
38+
logger.log(FINE, "Failure extracting body", exception);
39+
return null;
40+
}
41+
}
42+
43+
@Nullable
44+
private static String serializeSanitized(JsonpMapper mapper, Object item) throws IOException {
45+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
46+
47+
if (mapper instanceof JacksonJsonpMapper) {
48+
// Use Jackson-based sanitizing generator for JacksonJsonpMapper
49+
com.fasterxml.jackson.core.JsonGenerator jacksonGenerator =
50+
JSON_FACTORY.createGenerator(baos);
51+
com.fasterxml.jackson.core.JsonGenerator sanitizingGenerator =
52+
new SanitizingJacksonJsonGenerator(jacksonGenerator);
53+
try (JsonGenerator generator = new JacksonJsonpGenerator(sanitizingGenerator)) {
54+
mapper.serialize(item, generator);
55+
}
56+
} else {
57+
// Fallback for other mappers (may not work for all implementations)
58+
JsonGenerator rawGenerator = mapper.jsonProvider().createGenerator(baos);
59+
try (JsonGenerator generator = new SanitizingJsonGenerator(rawGenerator)) {
60+
mapper.serialize(item, generator);
61+
}
62+
}
63+
64+
String result = baos.toString(UTF_8).trim();
65+
return result.isEmpty() ? null : result;
66+
}
67+
68+
@Nullable
69+
private static String serializeNdJsonSanitized(JsonpMapper mapper, NdJsonpSerializable value)
70+
throws IOException {
71+
StringBuilder result = new StringBuilder();
72+
Iterator<?> values = value._serializables();
73+
boolean first = true;
74+
75+
while (values.hasNext()) {
76+
Object item = values.next();
77+
String itemStr;
78+
79+
if (item instanceof NdJsonpSerializable && item != value) {
80+
// Recursively handle nested NdJsonpSerializable
81+
itemStr = serializeNdJsonSanitized(mapper, (NdJsonpSerializable) item);
82+
} else {
83+
itemStr = serializeSanitized(mapper, item);
84+
}
85+
86+
if (itemStr != null && !itemStr.isEmpty()) {
87+
if (!first) {
88+
result.append(QUERY_SEPARATOR);
89+
}
90+
result.append(itemStr);
91+
first = false;
92+
}
93+
}
94+
95+
return result.length() == 0 ? null : result.toString();
96+
}
97+
98+
private OpenSearchBodyExtractor() {}
99+
}

instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchRequest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;
77

88
import com.google.auto.value.AutoValue;
9+
import javax.annotation.Nullable;
910

1011
@AutoValue
1112
public abstract class OpenSearchRequest {
1213

13-
public static OpenSearchRequest create(String method, String endpoint) {
14-
return new AutoValue_OpenSearchRequest(method, endpoint);
14+
public static OpenSearchRequest create(String method, String endpoint, @Nullable String body) {
15+
return new AutoValue_OpenSearchRequest(method, endpoint, body);
1516
}
1617

1718
public abstract String getMethod();
1819

1920
public abstract String getEndpoint();
21+
22+
@Nullable
23+
public abstract String getBody();
2024
}

instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchSingletons.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;
77

88
import io.opentelemetry.api.GlobalOpenTelemetry;
9+
import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil;
910
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor;
1011
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1112
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
@@ -15,6 +16,10 @@
1516
public final class OpenSearchSingletons {
1617
private static final Instrumenter<OpenSearchRequest, Void> INSTRUMENTER = createInstrumenter();
1718

19+
public static final boolean CAPTURE_SEARCH_QUERY =
20+
DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "opensearch")
21+
.getBoolean("capture_search_query", true);
22+
1823
public static Instrumenter<OpenSearchRequest, Void> instrumenter() {
1924
return INSTRUMENTER;
2025
}

instrumentation/opensearch/opensearch-java-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/opensearch/v3_0/OpenSearchTransportInstrumentation.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
import net.bytebuddy.asm.Advice;
2222
import net.bytebuddy.description.type.TypeDescription;
2323
import net.bytebuddy.matcher.ElementMatcher;
24+
import org.opensearch.client.json.JsonpMapper;
25+
import org.opensearch.client.opensearch.core.MsearchRequest;
26+
import org.opensearch.client.opensearch.core.SearchRequest;
2427
import org.opensearch.client.transport.Endpoint;
28+
import org.opensearch.client.transport.OpenSearchTransport;
2529

2630
public class OpenSearchTransportInstrumentation implements TypeInstrumentation {
2731
@Override
@@ -60,10 +64,21 @@ private AdviceScope(OpenSearchRequest otelRequest, Context context, Scope scope)
6064
}
6165

6266
@Nullable
63-
public static AdviceScope start(Object request, Endpoint<Object, Object, Object> endpoint) {
67+
public static AdviceScope start(
68+
Object request, Endpoint<Object, Object, Object> endpoint, JsonpMapper jsonpMapper) {
6469
Context parentContext = Context.current();
70+
71+
String queryBody = null;
72+
73+
if (OpenSearchSingletons.CAPTURE_SEARCH_QUERY
74+
&& (request instanceof SearchRequest || request instanceof MsearchRequest)) {
75+
queryBody = OpenSearchBodyExtractor.extractSanitized(jsonpMapper, request);
76+
}
77+
6578
OpenSearchRequest otelRequest =
66-
OpenSearchRequest.create(endpoint.method(request), endpoint.requestUrl(request));
79+
OpenSearchRequest.create(
80+
endpoint.method(request), endpoint.requestUrl(request), queryBody);
81+
6782
if (!instrumenter().shouldStart(parentContext, otelRequest)) {
6883
return null;
6984
}
@@ -94,9 +109,10 @@ public static class PerformRequestAdvice {
94109

95110
@Advice.OnMethodEnter(suppress = Throwable.class)
96111
public static AdviceScope onEnter(
112+
@Advice.This OpenSearchTransport openSearchTransport,
97113
@Advice.Argument(0) Object request,
98114
@Advice.Argument(1) Endpoint<Object, Object, Object> endpoint) {
99-
return AdviceScope.start(request, endpoint);
115+
return AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper());
100116
}
101117

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

115131
@Advice.OnMethodEnter(suppress = Throwable.class)
116132
public static Object[] onEnter(
133+
@Advice.This OpenSearchTransport openSearchTransport,
117134
@Advice.Argument(0) Object request,
118135
@Advice.Argument(1) Endpoint<Object, Object, Object> endpoint) {
119-
AdviceScope adviceScope = AdviceScope.start(request, endpoint);
136+
AdviceScope adviceScope =
137+
AdviceScope.start(request, endpoint, openSearchTransport.jsonpMapper());
120138
return new Object[] {adviceScope};
121139
}
122140

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.opensearch.v3_0;
7+
8+
import com.fasterxml.jackson.core.JsonGenerator;
9+
import com.fasterxml.jackson.core.util.JsonGeneratorDelegate;
10+
import java.io.IOException;
11+
import java.math.BigDecimal;
12+
import java.math.BigInteger;
13+
14+
/**
15+
* A Jackson JsonGenerator wrapper that sanitizes all literal values by replacing them with "?".
16+
* This is used to sanitize OpenSearch query bodies before they are captured as span attributes,
17+
* ensuring that sensitive data is not exposed in telemetry.
18+
*/
19+
final class SanitizingJacksonJsonGenerator extends JsonGeneratorDelegate {
20+
21+
private static final String MASKED_VALUE = "?";
22+
23+
SanitizingJacksonJsonGenerator(JsonGenerator delegate) {
24+
super(delegate);
25+
}
26+
27+
// String value methods - sanitize
28+
29+
@Override
30+
public void writeString(String value) throws IOException {
31+
delegate.writeString(MASKED_VALUE);
32+
}
33+
34+
@Override
35+
public void writeString(char[] buffer, int offset, int length) throws IOException {
36+
delegate.writeString(MASKED_VALUE);
37+
}
38+
39+
@Override
40+
public void writeRawUTF8String(byte[] buffer, int offset, int length) throws IOException {
41+
delegate.writeString(MASKED_VALUE);
42+
}
43+
44+
@Override
45+
public void writeUTF8String(byte[] buffer, int offset, int length) throws IOException {
46+
delegate.writeString(MASKED_VALUE);
47+
}
48+
49+
// Number value methods - sanitize
50+
51+
@Override
52+
public void writeNumber(int value) throws IOException {
53+
delegate.writeString(MASKED_VALUE);
54+
}
55+
56+
@Override
57+
public void writeNumber(long value) throws IOException {
58+
delegate.writeString(MASKED_VALUE);
59+
}
60+
61+
@Override
62+
public void writeNumber(float value) throws IOException {
63+
delegate.writeString(MASKED_VALUE);
64+
}
65+
66+
@Override
67+
public void writeNumber(double value) throws IOException {
68+
delegate.writeString(MASKED_VALUE);
69+
}
70+
71+
@Override
72+
public void writeNumber(BigInteger value) throws IOException {
73+
delegate.writeString(MASKED_VALUE);
74+
}
75+
76+
@Override
77+
public void writeNumber(BigDecimal value) throws IOException {
78+
delegate.writeString(MASKED_VALUE);
79+
}
80+
81+
@Override
82+
public void writeNumber(String encodedValue) throws IOException {
83+
delegate.writeString(MASKED_VALUE);
84+
}
85+
86+
// Boolean value methods - sanitize
87+
88+
@Override
89+
public void writeBoolean(boolean value) throws IOException {
90+
delegate.writeString(MASKED_VALUE);
91+
}
92+
93+
// Null value methods - sanitize
94+
95+
@Override
96+
public void writeNull() throws IOException {
97+
delegate.writeString(MASKED_VALUE);
98+
}
99+
}

0 commit comments

Comments
 (0)