From 977e458e3e515e4a8bd5a1b19168f1b4a4474b0c Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Fri, 5 Dec 2025 07:08:03 +0100 Subject: [PATCH 1/2] 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 --- ...oggingConfigurationCustomizerProvider.java | 4 +- .../SanitizeSpanExporterCustomizer.java | 86 ++++++++++ .../SanitizeSpanExporterCustomizerTest.java | 150 ++++++++++++++++++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizerTest.java diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java index e3507d32..99e33288 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java @@ -1,6 +1,7 @@ package com.sap.hcf.cf.logging.opentelemetry.agent.ext; import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingBindingPropertiesSupplier; +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.SanitizeSpanExporterCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; @@ -15,8 +16,7 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur public void customize(AutoConfigurationCustomizer autoConfiguration) { LOG.info("Initializing SAP BTP Observability extension " + VERSION); autoConfiguration.addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier()); - - // ConfigurableLogRecordExporterProvider + autoConfiguration.addSpanExporterCustomizer(new SanitizeSpanExporterCustomizer()); } } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java new file mode 100644 index 00000000..f6fb46bc --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java @@ -0,0 +1,86 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.DelegatingSpanData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +import java.util.Collection; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +public class SanitizeSpanExporterCustomizer implements BiFunction { + + private static final String PROPERTY_ENABLED_KEY = "sap.cf.integration.otel.extension.sanitizer.enabled"; + private static final AttributeKey DB_QUERY_TEXT = stringKey("db.query.text"); + //@Deprecated + private static final AttributeKey DB_STATEMENT = stringKey("db.statement"); + + @Override + public SpanExporter apply(SpanExporter delegate, ConfigProperties config) { + if (config != null && !config.getBoolean(PROPERTY_ENABLED_KEY, true)) { + return delegate; + } + return new SpanExporter() { + @Override + public CompletableResultCode export(Collection spans) { + return delegate.export(spans.stream().map(this::sanitizeSpanData).collect(Collectors.toList())); + } + + private SpanData sanitizeSpanData(SpanData spanData) { + Attributes attributes = spanData.getAttributes(); + if (attributes == null) { + return spanData; + } + String dbQueryText = attributes.get(DB_QUERY_TEXT); + String dbStatement = attributes.get(DB_STATEMENT); + if (isClean(dbQueryText) && isClean(dbStatement)) { + return spanData; + } + AttributesBuilder sanitized = attributes.toBuilder(); + if (!isClean(dbQueryText)) { + sanitized.put(DB_QUERY_TEXT, dbQueryText.substring(0, 7) + " [REDACTED]"); + } + if (!isClean(dbStatement)) { + sanitized.put(DB_STATEMENT, dbStatement.substring(0, 7) + " [REDACTED]"); + } + return new SanitizedSpanData(spanData, sanitized.build()); + } + + private boolean isClean(String query) { + return query == null || !query.toLowerCase().startsWith("connect"); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + }; + } + + private static class SanitizedSpanData extends DelegatingSpanData { + + private final Attributes filteredAttributes; + + protected SanitizedSpanData(SpanData delegate, Attributes filteredAttrinutes) { + super(delegate); + this.filteredAttributes = filteredAttrinutes; + } + + @Override + public Attributes getAttributes() { + return filteredAttributes; + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizerTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizerTest.java new file mode 100644 index 00000000..802a9dd7 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizerTest.java @@ -0,0 +1,150 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SanitizeSpanExporterCustomizerTest { + + @Mock(strictness = Mock.Strictness.LENIENT) + private SpanData spanData; + + @Mock + private SpanExporter delegateExporter; + + @Captor + private ArgumentCaptor> spanDataCaptor; + + private SpanExporter sanitizeExporter; + + @BeforeEach + void setUp() { + when(spanData.getName()).thenReturn("test-span"); + this.sanitizeExporter = new SanitizeSpanExporterCustomizer().apply(delegateExporter, null); + } + + @Test + void forwardsSpanWithoutAttributes() { + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spans); + } + + @Test + void forwardsSpanWithEmptyAttributes() { + List spans = List.of(spanData); + when(spanData.getAttributes()).thenReturn(Attributes.empty()); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spans); + } + + @Test + void forwardsSpanWithoutSensitiveAttributeKey() { + Attributes attributes = Attributes.builder().put("some.key", "some value").build(); + when(spanData.getAttributes()).thenReturn(attributes); + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spans); + } + + @Test + void forwardsSpanWithSensitiveAttributeKeyButWithoutSensitiveValue() { + Attributes attributes = Attributes.builder().put("db.query.text", "some safe value").build(); + when(spanData.getAttributes()).thenReturn(attributes); + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spans); + } + + @Test + void redactsSensitiveDbQueryTextValue() { + Attributes attributes = Attributes.builder().put("db.query.text", "Connect somewhere").build(); + when(spanData.getAttributes()).thenReturn(attributes); + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spanDataCaptor.capture()); + SpanData sanitizedSpan = spanDataCaptor.getValue().get(0); + assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span"); + assertThat(sanitizedSpan).extracting(SpanData::getAttributes) + .extracting(attrs -> attrs.get(AttributeKey.stringKey("db.query.text"))) + .isEqualTo("Connect [REDACTED]"); + } + + @Test + void redactsSensitiveDbStatementValue() { + Attributes attributes = Attributes.builder().put("db.statement", "CONNECT somewhere").build(); + when(spanData.getAttributes()).thenReturn(attributes); + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spanDataCaptor.capture()); + SpanData sanitizedSpan = spanDataCaptor.getValue().get(0); + assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span"); + assertThat(sanitizedSpan).extracting(SpanData::getAttributes) + .extracting(attrs -> attrs.get(AttributeKey.stringKey("db.statement"))) + .isEqualTo("CONNECT [REDACTED]"); + } + + @Test + void keepsOtherAttributesOnRedaction() { + Attributes attributes = + Attributes.builder().put("db.query.text", "connect somewhere").put("some.key", "some.value").build(); + when(spanData.getAttributes()).thenReturn(attributes); + List spans = List.of(spanData); + sanitizeExporter.export(spans); + + verify(delegateExporter).export(spanDataCaptor.capture()); + SpanData sanitizedSpan = spanDataCaptor.getValue().get(0); + assertThat(sanitizedSpan).extracting(SpanData::getName).isEqualTo("test-span"); + assertThat(sanitizedSpan).extracting(SpanData::getAttributes) + .extracting(attrs -> attrs.get(AttributeKey.stringKey("db.query.text"))) + .isEqualTo("connect [REDACTED]"); + assertThat(sanitizedSpan).extracting(SpanData::getAttributes) + .extracting(attrs -> attrs.get(AttributeKey.stringKey("some.key"))) + .isEqualTo("some.value"); + } + + @Test + void canBeDisabledViaConfig() { + Map configEntries = new HashMap<>(); + configEntries.put("sap.cf.integration.otel.extension.sanitizer.enabled", "false"); + DefaultConfigProperties configProperties = DefaultConfigProperties.createFromMap(configEntries); + SpanExporter spanExporter = new SanitizeSpanExporterCustomizer().apply(delegateExporter, configProperties); + assertThat(spanExporter).isSameAs(delegateExporter); + } + + @Test + void delegatesFlush() { + sanitizeExporter.flush(); + verify(delegateExporter).flush(); + } + + @Test + void delegatesShutdown() { + sanitizeExporter.shutdown(); + verify(delegateExporter).shutdown(); + } +} From 7e9c528dc12825d135a90c493df26686049cbadb Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Fri, 5 Dec 2025 09:38:34 +0100 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: bennygoerzig <166001170+bennygoerzig@users.noreply.github.com> --- .../agent/ext/exporter/SanitizeSpanExporterCustomizer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java index f6fb46bc..0690833e 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/SanitizeSpanExporterCustomizer.java @@ -73,9 +73,9 @@ private static class SanitizedSpanData extends DelegatingSpanData { private final Attributes filteredAttributes; - protected SanitizedSpanData(SpanData delegate, Attributes filteredAttrinutes) { + protected SanitizedSpanData(SpanData delegate, Attributes filteredAttributes) { super(delegate); - this.filteredAttributes = filteredAttrinutes; + this.filteredAttributes = filteredAttributes; } @Override