Skip to content

Commit a629def

Browse files
Add sanitization for span attributes (#314)
* Add sanitization for span attributes Registers a span exporter customizer to redact attribute values. It can be disabled with -Dsap.cf.integration.otel.extension.sanitizer.enabled=false. Signed-off-by: Karsten Schnitter <[email protected]> Co-authored-by: bennygoerzig <[email protected]>
1 parent 50714f1 commit a629def

File tree

3 files changed

+238
-2
lines changed

3 files changed

+238
-2
lines changed

cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.sap.hcf.cf.logging.opentelemetry.agent.ext;
22

33
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingBindingPropertiesSupplier;
4+
import com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.SanitizeSpanExporterCustomizer;
45
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
56
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
67

@@ -15,8 +16,7 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur
1516
public void customize(AutoConfigurationCustomizer autoConfiguration) {
1617
LOG.info("Initializing SAP BTP Observability extension " + VERSION);
1718
autoConfiguration.addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier());
18-
19-
// ConfigurableLogRecordExporterProvider
19+
autoConfiguration.addSpanExporterCustomizer(new SanitizeSpanExporterCustomizer());
2020
}
2121

2222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter;
2+
3+
import io.opentelemetry.api.common.AttributeKey;
4+
import io.opentelemetry.api.common.Attributes;
5+
import io.opentelemetry.api.common.AttributesBuilder;
6+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
7+
import io.opentelemetry.sdk.common.CompletableResultCode;
8+
import io.opentelemetry.sdk.trace.data.DelegatingSpanData;
9+
import io.opentelemetry.sdk.trace.data.SpanData;
10+
import io.opentelemetry.sdk.trace.export.SpanExporter;
11+
12+
import java.util.Collection;
13+
import java.util.function.BiFunction;
14+
import java.util.stream.Collectors;
15+
16+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
17+
18+
public class SanitizeSpanExporterCustomizer implements BiFunction<SpanExporter, ConfigProperties, SpanExporter> {
19+
20+
private static final String PROPERTY_ENABLED_KEY = "sap.cf.integration.otel.extension.sanitizer.enabled";
21+
private static final AttributeKey<String> DB_QUERY_TEXT = stringKey("db.query.text");
22+
//@Deprecated
23+
private static final AttributeKey<String> DB_STATEMENT = stringKey("db.statement");
24+
25+
@Override
26+
public SpanExporter apply(SpanExporter delegate, ConfigProperties config) {
27+
if (config != null && !config.getBoolean(PROPERTY_ENABLED_KEY, true)) {
28+
return delegate;
29+
}
30+
return new SpanExporter() {
31+
@Override
32+
public CompletableResultCode export(Collection<SpanData> spans) {
33+
return delegate.export(spans.stream().map(this::sanitizeSpanData).collect(Collectors.toList()));
34+
}
35+
36+
private SpanData sanitizeSpanData(SpanData spanData) {
37+
Attributes attributes = spanData.getAttributes();
38+
if (attributes == null) {
39+
return spanData;
40+
}
41+
String dbQueryText = attributes.get(DB_QUERY_TEXT);
42+
String dbStatement = attributes.get(DB_STATEMENT);
43+
if (isClean(dbQueryText) && isClean(dbStatement)) {
44+
return spanData;
45+
}
46+
AttributesBuilder sanitized = attributes.toBuilder();
47+
if (!isClean(dbQueryText)) {
48+
sanitized.put(DB_QUERY_TEXT, dbQueryText.substring(0, 7) + " [REDACTED]");
49+
}
50+
if (!isClean(dbStatement)) {
51+
sanitized.put(DB_STATEMENT, dbStatement.substring(0, 7) + " [REDACTED]");
52+
}
53+
return new SanitizedSpanData(spanData, sanitized.build());
54+
}
55+
56+
private boolean isClean(String query) {
57+
return query == null || !query.toLowerCase().startsWith("connect");
58+
}
59+
60+
@Override
61+
public CompletableResultCode flush() {
62+
return delegate.flush();
63+
}
64+
65+
@Override
66+
public CompletableResultCode shutdown() {
67+
return delegate.shutdown();
68+
}
69+
};
70+
}
71+
72+
private static class SanitizedSpanData extends DelegatingSpanData {
73+
74+
private final Attributes filteredAttributes;
75+
76+
protected SanitizedSpanData(SpanData delegate, Attributes filteredAttributes) {
77+
super(delegate);
78+
this.filteredAttributes = filteredAttributes;
79+
}
80+
81+
@Override
82+
public Attributes getAttributes() {
83+
return filteredAttributes;
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter;
2+
3+
import io.opentelemetry.api.common.AttributeKey;
4+
import io.opentelemetry.api.common.Attributes;
5+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
6+
import io.opentelemetry.sdk.trace.data.SpanData;
7+
import io.opentelemetry.sdk.trace.export.SpanExporter;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.mockito.ArgumentCaptor;
12+
import org.mockito.Captor;
13+
import org.mockito.Mock;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
16+
import java.util.HashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
@ExtendWith(MockitoExtension.class)
25+
class SanitizeSpanExporterCustomizerTest {
26+
27+
@Mock(strictness = Mock.Strictness.LENIENT)
28+
private SpanData spanData;
29+
30+
@Mock
31+
private SpanExporter delegateExporter;
32+
33+
@Captor
34+
private ArgumentCaptor<List<SpanData>> spanDataCaptor;
35+
36+
private SpanExporter sanitizeExporter;
37+
38+
@BeforeEach
39+
void setUp() {
40+
when(spanData.getName()).thenReturn("test-span");
41+
this.sanitizeExporter = new SanitizeSpanExporterCustomizer().apply(delegateExporter, null);
42+
}
43+
44+
@Test
45+
void forwardsSpanWithoutAttributes() {
46+
List<SpanData> spans = List.of(spanData);
47+
sanitizeExporter.export(spans);
48+
49+
verify(delegateExporter).export(spans);
50+
}
51+
52+
@Test
53+
void forwardsSpanWithEmptyAttributes() {
54+
List<SpanData> spans = List.of(spanData);
55+
when(spanData.getAttributes()).thenReturn(Attributes.empty());
56+
sanitizeExporter.export(spans);
57+
58+
verify(delegateExporter).export(spans);
59+
}
60+
61+
@Test
62+
void forwardsSpanWithoutSensitiveAttributeKey() {
63+
Attributes attributes = Attributes.builder().put("some.key", "some value").build();
64+
when(spanData.getAttributes()).thenReturn(attributes);
65+
List<SpanData> spans = List.of(spanData);
66+
sanitizeExporter.export(spans);
67+
68+
verify(delegateExporter).export(spans);
69+
}
70+
71+
@Test
72+
void forwardsSpanWithSensitiveAttributeKeyButWithoutSensitiveValue() {
73+
Attributes attributes = Attributes.builder().put("db.query.text", "some safe value").build();
74+
when(spanData.getAttributes()).thenReturn(attributes);
75+
List<SpanData> spans = List.of(spanData);
76+
sanitizeExporter.export(spans);
77+
78+
verify(delegateExporter).export(spans);
79+
}
80+
81+
@Test
82+
void redactsSensitiveDbQueryTextValue() {
83+
Attributes attributes = Attributes.builder().put("db.query.text", "Connect somewhere").build();
84+
when(spanData.getAttributes()).thenReturn(attributes);
85+
List<SpanData> spans = List.of(spanData);
86+
sanitizeExporter.export(spans);
87+
88+
verify(delegateExporter).export(spanDataCaptor.capture());
89+
SpanData sanitizedSpan = spanDataCaptor.getValue().get(0);
90+
assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span");
91+
assertThat(sanitizedSpan).extracting(SpanData::getAttributes)
92+
.extracting(attrs -> attrs.get(AttributeKey.stringKey("db.query.text")))
93+
.isEqualTo("Connect [REDACTED]");
94+
}
95+
96+
@Test
97+
void redactsSensitiveDbStatementValue() {
98+
Attributes attributes = Attributes.builder().put("db.statement", "CONNECT somewhere").build();
99+
when(spanData.getAttributes()).thenReturn(attributes);
100+
List<SpanData> spans = List.of(spanData);
101+
sanitizeExporter.export(spans);
102+
103+
verify(delegateExporter).export(spanDataCaptor.capture());
104+
SpanData sanitizedSpan = spanDataCaptor.getValue().get(0);
105+
assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span");
106+
assertThat(sanitizedSpan).extracting(SpanData::getAttributes)
107+
.extracting(attrs -> attrs.get(AttributeKey.stringKey("db.statement")))
108+
.isEqualTo("CONNECT [REDACTED]");
109+
}
110+
111+
@Test
112+
void keepsOtherAttributesOnRedaction() {
113+
Attributes attributes =
114+
Attributes.builder().put("db.query.text", "connect somewhere").put("some.key", "some.value").build();
115+
when(spanData.getAttributes()).thenReturn(attributes);
116+
List<SpanData> spans = List.of(spanData);
117+
sanitizeExporter.export(spans);
118+
119+
verify(delegateExporter).export(spanDataCaptor.capture());
120+
SpanData sanitizedSpan = spanDataCaptor.getValue().get(0);
121+
assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span");
122+
assertThat(sanitizedSpan).extracting(SpanData::getAttributes)
123+
.extracting(attrs -> attrs.get(AttributeKey.stringKey("db.query.text")))
124+
.isEqualTo("connect [REDACTED]");
125+
assertThat(sanitizedSpan).extracting(SpanData::getAttributes)
126+
.extracting(attrs -> attrs.get(AttributeKey.stringKey("some.key")))
127+
.isEqualTo("some.value");
128+
}
129+
130+
@Test
131+
void canBeDisabledViaConfig() {
132+
Map<String, String> configEntries = new HashMap<>();
133+
configEntries.put("sap.cf.integration.otel.extension.sanitizer.enabled", "false");
134+
DefaultConfigProperties configProperties = DefaultConfigProperties.createFromMap(configEntries);
135+
SpanExporter spanExporter = new SanitizeSpanExporterCustomizer().apply(delegateExporter, configProperties);
136+
assertThat(spanExporter).isSameAs(delegateExporter);
137+
}
138+
139+
@Test
140+
void delegatesFlush() {
141+
sanitizeExporter.flush();
142+
verify(delegateExporter).flush();
143+
}
144+
145+
@Test
146+
void delegatesShutdown() {
147+
sanitizeExporter.shutdown();
148+
verify(delegateExporter).shutdown();
149+
}
150+
}

0 commit comments

Comments
 (0)