diff --git a/README.md b/README.md index 1fc6c348..5bfb055e 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,12 @@ All in all, you should do the following: 4. Adjust your logging configuration accordingly. Let's say you want to make use of the *servlet filter* feature, then you need to add the following dependency to your -POM with property `cf-logging-version` referring to the latest nexus version (currently `4.0.0`): +POM with property `cf-logging-version` referring to the latest nexus version (currently `4.0.1`): ```xml - 4.0.0 + 4.0.1 ``` diff --git a/cf-java-logging-support-core/pom.xml b/cf-java-logging-support-core/pom.xml index 4c2c6a24..ab24b0ed 100644 --- a/cf-java-logging-support-core/pom.xml +++ b/cf-java-logging-support-core/pom.xml @@ -25,7 +25,7 @@ com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 ../pom.xml diff --git a/cf-java-logging-support-log4j2/pom.xml b/cf-java-logging-support-log4j2/pom.xml index 3cc47476..a1f3d894 100644 --- a/cf-java-logging-support-log4j2/pom.xml +++ b/cf-java-logging-support-log4j2/pom.xml @@ -11,7 +11,7 @@ ../pom.xml com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 diff --git a/cf-java-logging-support-logback/pom.xml b/cf-java-logging-support-logback/pom.xml index ffe0a3aa..20b85824 100644 --- a/cf-java-logging-support-logback/pom.xml +++ b/cf-java-logging-support-logback/pom.xml @@ -10,7 +10,7 @@ ../pom.xml com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 diff --git a/cf-java-logging-support-opentelemetry-agent-extension/pom.xml b/cf-java-logging-support-opentelemetry-agent-extension/pom.xml index b54f5108..e3098351 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/pom.xml +++ b/cf-java-logging-support-opentelemetry-agent-extension/pom.xml @@ -8,13 +8,57 @@ jar cf-java-logging-support-opentelemetry-agent-extension - + OpenTelemetry Java Agent Extension for auto-configuration of SAP BTP services. cf-java-logging-support-parent com.sap.hcp.cf.logging - 4.0.0 + 4.0.1 + ../pom.xml + https://github.com/SAP/cf-java-logging-support + + SAP SE + https://www.sap.com + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + KarstenSchnitter + Karsten Schnitter + k.schnitter@sap.com + SAP SE + https://github.com/SAP + + + WolfgangTheilmann + Wolfgang Theilmann + wolfgang.theilmann@sap.com + SAP SE + https://github.com/SAP + + + HariG + Hariharan Gandhi + hariharan.gandhi@sap.com + SAP SE + https://github.com/SAP + + + + + scm:git:git@github.com:SAP/cf-java-logging-support.git + scm:git:git@github.com:SAP/cf-java-logging-support.git + git@github.com:SAP/cf-java-logging-support.git + + 1.55.0 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..037770da 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; @@ -9,14 +10,13 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigurationCustomizerProvider { private static final Logger LOG = Logger.getLogger(CloudLoggingConfigurationCustomizerProvider.class.getName()); - private static final String VERSION = "4.0.0"; + private static final String VERSION = "4.0.1"; @Override 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..0690833e --- /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 filteredAttributes) { + super(delegate); + this.filteredAttributes = filteredAttributes; + } + + @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(); + } +} diff --git a/cf-java-logging-support-servlet-dynlog-jwt/pom.xml b/cf-java-logging-support-servlet-dynlog-jwt/pom.xml index 5f1c34d3..d55d89b7 100644 --- a/cf-java-logging-support-servlet-dynlog-jwt/pom.xml +++ b/cf-java-logging-support-servlet-dynlog-jwt/pom.xml @@ -11,7 +11,7 @@ com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 ../pom.xml diff --git a/cf-java-logging-support-servlet/pom.xml b/cf-java-logging-support-servlet/pom.xml index 29487ac3..5ff57931 100644 --- a/cf-java-logging-support-servlet/pom.xml +++ b/cf-java-logging-support-servlet/pom.xml @@ -9,7 +9,7 @@ com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 ../pom.xml diff --git a/pom.xml b/pom.xml index fb168675..c7e3b063 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 pom Cloud Foundry Java logging support components diff --git a/sample-spring-boot/manifest-otel-javaagent.yml b/sample-spring-boot/manifest-otel-javaagent.yml index a8c41027..8d9c03c6 100644 --- a/sample-spring-boot/manifest-otel-javaagent.yml +++ b/sample-spring-boot/manifest-otel-javaagent.yml @@ -5,7 +5,7 @@ applications: # - name: otel-sample-app instances: 1 - path: target/sample-app-spring-boot-4.0.0.jar + path: target/sample-app-spring-boot-4.0.1.jar buildpack: sap_java_buildpack_jakarta memory: 256M random-route: true @@ -16,7 +16,7 @@ applications: LOG_REFERER: false JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" JBP_CONFIG_SAP_MACHINE_JRE: '{ use_offline_repository: false, version: 17.+ }' - JBP_CONFIG_JAVA_OPTS: '[from_environment: false, java_opts: ''-javaagent:BOOT-INF/lib/opentelemetry-javaagent-2.21.0.jar -Dotel.javaagent.extensions=BOOT-INF/lib/cf-java-logging-support-opentelemetry-agent-extension-4.0.0.jar -Dotel.logs.exporter=cloud-logging -Dotel.metrics.exporter=cloud-logging,dynatrace -Dotel.traces.exporter=cloud-logging -Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* -Dotel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes=true -Dotel.instrumentation.logback-appender.experimental.capture-code-attributes=true -Dotel.instrumentation.logback-appender.experimental-log-attributes=true -Dotel.experimental.resource.disabled-keys=process.command_line,process.command_args,process.executable.path -Dotel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name=ingest_token'']' + JBP_CONFIG_JAVA_OPTS: '[from_environment: false, java_opts: ''-javaagent:BOOT-INF/lib/opentelemetry-javaagent-2.21.0.jar -Dotel.javaagent.extensions=BOOT-INF/lib/cf-java-logging-support-opentelemetry-agent-extension-4.0.1.jar -Dotel.logs.exporter=cloud-logging -Dotel.metrics.exporter=cloud-logging,dynatrace -Dotel.traces.exporter=cloud-logging -Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* -Dotel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes=true -Dotel.instrumentation.logback-appender.experimental.capture-code-attributes=true -Dotel.instrumentation.logback-appender.experimental-log-attributes=true -Dotel.experimental.resource.disabled-keys=process.command_line,process.command_args,process.executable.path -Dotel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name=ingest_token'']' services: - cloud-logging - dynatrace-service diff --git a/sample-spring-boot/manifest.yml b/sample-spring-boot/manifest.yml index 429bbda3..cacdc08d 100644 --- a/sample-spring-boot/manifest.yml +++ b/sample-spring-boot/manifest.yml @@ -5,7 +5,7 @@ applications: # - name: logging-sample-app instances: 1 - path: target/sample-app-spring-boot-4.0.0.jar + path: target/sample-app-spring-boot-4.0.1.jar buildpack: sap_java_buildpack env: # Set LOG_*: true to activate logging of respective field diff --git a/sample-spring-boot/pom.xml b/sample-spring-boot/pom.xml index ef2f249e..62433ee9 100644 --- a/sample-spring-boot/pom.xml +++ b/sample-spring-boot/pom.xml @@ -9,7 +9,7 @@ com.sap.hcp.cf.logging cf-java-logging-support-parent - 4.0.0 + 4.0.1 ../pom.xml