Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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<SpanExporter, ConfigProperties, SpanExporter> {

private static final String PROPERTY_ENABLED_KEY = "sap.cf.integration.otel.extension.sanitizer.enabled";
private static final AttributeKey<String> DB_QUERY_TEXT = stringKey("db.query.text");
//@Deprecated
private static final AttributeKey<String> 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<SpanData> 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 filteredAttributes) {
super(delegate);
this.filteredAttributes = filteredAttributes;
}

@Override
public Attributes getAttributes() {
return filteredAttributes;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<List<SpanData>> spanDataCaptor;

private SpanExporter sanitizeExporter;

@BeforeEach
void setUp() {
when(spanData.getName()).thenReturn("test-span");
this.sanitizeExporter = new SanitizeSpanExporterCustomizer().apply(delegateExporter, null);
}

@Test
void forwardsSpanWithoutAttributes() {
List<SpanData> spans = List.of(spanData);
sanitizeExporter.export(spans);

verify(delegateExporter).export(spans);
}

@Test
void forwardsSpanWithEmptyAttributes() {
List<SpanData> 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<SpanData> 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<SpanData> 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<SpanData> 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<SpanData> 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<SpanData> 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<String, String> 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();
}
}