diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/build.gradle b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/build.gradle index b0c49f3eb4f..a6bde8bce05 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/build.gradle +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/build.gradle @@ -1,5 +1,6 @@ plugins { id 'dd-trace-java.module.instrumentation' + id 'dd-trace-java.jmh-conventions' } muzzle { @@ -15,4 +16,6 @@ addTestSuiteForDir('latestDepTest', 'test') dependencies { compileOnly group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1' testImplementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1' + testImplementation project(':utils:test-utils') + testImplementation libs.jackson.databind } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/gradle.lockfile b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/gradle.lockfile index 4e36b31be18..9b11bd80363 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/gradle.lockfile +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/gradle.lockfile @@ -2,69 +2,75 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. # To regenerate this file, run: ./gradlew :dd-java-agent:instrumentation:aws-java:aws-java-lambda-handler-1.2:dependencies --write-locks -cafe.cryptography:curve25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -cafe.cryptography:ed25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -ch.qos.logback:logback-classic:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -ch.qos.logback:logback-core:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.amazonaws:aws-lambda-java-core:1.2.1=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okhttp3:okhttp:3.12.15=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okio:okio:1.17.6=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-instrument-java:0.0.6=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:java-dogstatsd-client:4.4.5=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.datadoghq:sketches-java:0.8.3=latestDepTestRuntimeClasspath,testRuntimeClasspath +cafe.cryptography:curve25519-elisabeth:0.1.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +cafe.cryptography:ed25519-elisabeth:0.1.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +ch.qos.logback:logback-classic:1.2.13=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +ch.qos.logback:logback-core:1.2.13=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.amazonaws:aws-lambda-java-core:1.2.1=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okhttp3:okhttp:3.12.15=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq.okio:okio:1.17.6=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-instrument-java:0.0.6=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.datadoghq:java-dogstatsd-client:4.4.5=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.datadoghq:sketches-java:0.8.3=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.20=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.20.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.20.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.20.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.javaparser:javaparser-core:3.25.6=codenarc -com.github.jnr:jffi:1.3.15=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-a64asm:1.0.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-constants:0.10.4=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-enxio:0.32.20=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-ffi:2.2.19=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-posix:3.1.22=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-unixsocket:0.38.25=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-x86asm:1.0.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.spotbugs:spotbugs-annotations:4.10.4=compileClasspath,spotbugs +com.github.jnr:jffi:1.3.15=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-a64asm:1.0.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-constants:0.10.4=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-enxio:0.32.20=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-ffi:2.2.19=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-posix:3.1.22=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-unixsocket:0.38.25=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.jnr:jnr-x86asm:1.0.2=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.github.spotbugs:spotbugs-annotations:4.10.4=compileClasspath,jmhCompileClasspath,spotbugs com.github.spotbugs:spotbugs:4.10.4=spotbugs com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs -com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath +com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,jmhCompileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath com.google.auto.service:auto-service:1.1.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor com.google.auto:auto-common:1.2.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.code.gson:gson:2.14.0=spotbugs com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.47.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.47.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.48.0=spotbugs com.google.guava:failureaccess:1.0.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:failureaccess:1.0.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.3=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:guava:33.6.0-jre=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:33.6.0-jre=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,jmhRuntimeClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.j2objc:j2objc-annotations:3.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.re2j:re2j:1.8=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.squareup.moshi:moshi:1.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okio:okio:1.17.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:3.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.re2j:re2j:1.8=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +com.squareup.moshi:moshi:1.11.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:logging-interceptor:3.12.12=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:3.12.12=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:1.17.5=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.thoughtworks.qdox:qdox:1.12.1=codenarc -commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-fileupload:commons-fileupload:1.5=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-io:commons-io:2.11.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.21.0=spotbugs -de.thetaphi:forbiddenapis:3.11=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -io.leangen.geantyref:geantyref:1.3.16=latestDepTestRuntimeClasspath,testRuntimeClasspath -io.sqreen:libsqreen:17.5.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -javax.servlet:javax.servlet-api:3.1.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +de.thetaphi:forbiddenapis:3.11=compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +io.leangen.geantyref:geantyref:1.3.16=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +io.sqreen:libsqreen:17.5.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +javax.servlet:javax.servlet-api:3.1.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath jaxen:jaxen:2.0.6=spotbugs -junit:junit:4.13.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.18.12=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.12=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath +junit:junit:4.13.2=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.12=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.12=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna-platform:5.8.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.8.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +net.sf.jopt-simple:jopt-simple:5.0.4=jmh,jmhCompileClasspath,jmhRuntimeClasspath net.sf.saxon:Saxon-HE:12.10=spotbugs org.apache.ant:ant-antlr:1.10.14=codenarc org.apache.ant:ant-junit:1.10.14=codenarc org.apache.bcel:bcel:6.12.0=spotbugs org.apache.commons:commons-lang3:3.20.0=spotbugs +org.apache.commons:commons-math3:3.2=jmh,jmhCompileClasspath,jmhRuntimeClasspath org.apache.commons:commons-text:1.15.0=spotbugs org.apache.logging.log4j:log4j-api:2.26.1=spotbugs org.apache.logging.log4j:log4j-core:2.26.1=spotbugs @@ -74,52 +80,57 @@ org.codehaus.groovy:groovy-ant:3.0.23=codenarc org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc org.codehaus.groovy:groovy-json:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy-json:3.0.25=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.codehaus.groovy:groovy-templates:3.0.23=codenarc org.codehaus.groovy:groovy-xml:3.0.23=codenarc org.codehaus.groovy:groovy:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.codehaus.groovy:groovy:3.0.25=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.codenarc:CodeNarc:3.7.0=codenarc org.dom4j:dom4j:2.2.0=spotbugs org.gmetrics:GMetrics:2.1.0=codenarc -org.hamcrest:hamcrest-core:1.3=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jctools:jctools-core-jdk11:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.jctools:jctools-core:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest:3.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jctools:jctools-core-jdk11:4.0.6=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jctools:jctools-core:4.0.6=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.jspecify:jspecify:1.0.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.14.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.14.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-runner:1.14.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-api:1.14.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-suite-commons:1.14.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.junit:junit-bom:5.14.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:4.4.0=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.objenesis:objenesis:3.3=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.openjdk.jmh:jmh-core:1.36=jmh,jmhCompileClasspath,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-asm:1.36=jmh,jmhCompileClasspath,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-bytecode:1.36=jmh,jmhCompileClasspath,jmhRuntimeClasspath +org.openjdk.jmh:jmh-generator-reflection:1.36=jmh,jmhCompileClasspath,jmhRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm-analysis:9.10.1=spotbugs -org.ow2.asm:asm-analysis:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-commons:9.10.1=latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath -org.ow2.asm:asm-tree:9.10.1=latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath +org.ow2.asm:asm-analysis:9.7.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm-commons:9.10.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath +org.ow2.asm:asm-tree:9.10.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,spotbugs,testRuntimeClasspath org.ow2.asm:asm-util:9.10.1=spotbugs -org.ow2.asm:asm-util:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm:9.10.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath -org.slf4j:slf4j-api:1.7.32=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.ow2.asm:asm-util:9.7.1=jmhRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath +org.ow2.asm:asm:9.0=jmh +org.ow2.asm:asm:9.10.1=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath +org.slf4j:jcl-over-slf4j:1.7.30=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:jul-to-slf4j:1.7.30=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:log4j-over-slf4j:1.7.30=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,jmhCompileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath +org.slf4j:slf4j-api:1.7.32=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.17=spotbugsSlf4j org.slf4j:slf4j-api:2.0.18=spotbugs org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j -org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -org.spockframework:spock-bom:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.spockframework:spock-core:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-junit:1.2.2=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-parser:1.2.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,jmhRuntimeClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath +org.spockframework:spock-bom:2.4-groovy-3.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.spockframework:spock-core:2.4-groovy-3.0=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-junit:1.2.2=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +org.tabletest:tabletest-parser:1.2.1=jmhRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.xmlresolver:xmlresolver:5.3.3=spotbugs -empty=spotbugsPlugins +empty=jmhAnnotationProcessor,spotbugsPlugins diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/jmh/java/StripInjectedContextBenchmark.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/jmh/java/StripInjectedContextBenchmark.java new file mode 100644 index 00000000000..110501322cf --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/jmh/java/StripInjectedContextBenchmark.java @@ -0,0 +1,82 @@ +package datadog.trace.instrumentation.aws.v1.lambda; + +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.nio.charset.StandardCharsets; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 5, timeUnit = SECONDS) +@Measurement(iterations = 3, time = 5, timeUnit = SECONDS) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(MICROSECONDS) +@Fork(value = 1) +public class StripInjectedContextBenchmark { + + @Param({"true", "false"}) + boolean containsDatadogCarrier; + + // Event shapes: "eventbridge", "sqs", "sns" + @Param({"eventbridge", "sqs", "sns"}) + String payloadShape; + + byte[] payload; + + @Setup(Level.Trial) + public void setUp() { + String carrier = "{\"x-datadog-trace-id\":\"123\",\"x-datadog-parent-id\":\"456\"}"; + + switch (payloadShape) { + case "sqs": + { + // SQS-style: _datadog appears as a direct top-level key. + String body = + containsDatadogCarrier + ? "{\"orderId\":42,\"customer\":\"acme\",\"_datadog\":" + carrier + "}" + : "{\"orderId\":42,\"customer\":\"acme\"}"; + payload = body.getBytes(StandardCharsets.UTF_8); + break; + } + case "sns": + { + // SNS-style: _datadog is embedded inside a string-encoded "Message" field. + String inner = + containsDatadogCarrier + ? "{\\\"orderId\\\":42,\\\"_datadog\\\":" + carrier.replace("\"", "\\\"") + "}" + : "{\\\"orderId\\\":42}"; + String envelope = "{\"Type\":\"Notification\",\"Message\":\"" + inner + "\"}"; + payload = envelope.getBytes(StandardCharsets.UTF_8); + break; + } + default: + { + // EventBridge-style: _datadog is inside the "detail" object. + String detail = + containsDatadogCarrier + ? "{\"orderId\":42,\"customer\":\"acme\",\"_datadog\":" + carrier + "}" + : "{\"orderId\":42,\"customer\":\"acme\"}"; + String envelope = "{\"detail-type\":\"order.created\",\"detail\":" + detail + "}"; + payload = envelope.getBytes(StandardCharsets.UTF_8); + break; + } + } + } + + @Benchmark + public void stripInternal(Blackhole blackhole) { + blackhole.consume(StripInjectedContext.stripInternal(payload)); + } +} diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/LambdaHandlerInstrumentation.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/LambdaHandlerInstrumentation.java index 7f62f4337d9..33f86d2935c 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/LambdaHandlerInstrumentation.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/LambdaHandlerInstrumentation.java @@ -12,7 +12,9 @@ import static net.bytebuddy.asm.Advice.Origin; import static net.bytebuddy.asm.Advice.This; import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -20,6 +22,7 @@ import datadog.context.ContextScope; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; @@ -27,8 +30,10 @@ import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes; import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities; import datadog.trace.config.inversion.ConfigHelper; +import java.io.InputStream; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumenterModule.class) @@ -68,6 +73,10 @@ public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( isMethod() .and(named("handleRequest")) + .and(takesArguments(3)) + // Constrain arg0 to InputStream so an unrelated 3-arg "handleRequest" overload + // with a non-InputStream first parameter isn't matched by mistake. + .and(takesArgument(0, isSubTypeOf(InputStream.class))) .and(takesArgument(2, named("com.amazonaws.services.lambda.runtime.Context"))), getClass().getName() + "$ExtensionCommunicationAdvice"); } @@ -76,16 +85,32 @@ public static class ExtensionCommunicationAdvice { @OnMethodEnter static ContextScope enter( @This final Object that, - @Advice.Argument(0) final Object in, + // Bound as Object (not InputStream) so Byte Buddy can always bind this argument for + // writing back, even if the matcher's arg0 constraint is ever loosened or bypassed. + @Advice.Argument(value = 0, readOnly = false, typing = Assigner.Typing.DYNAMIC) Object in, @Advice.Argument(1) final Object out, @Advice.Argument(2) final Context awsContext, @Origin("#m") final String methodName) { - if (CallDepthThreadLocalMap.incrementCallDepth(RequestHandler.class) > 0) { return null; } + if (!(in instanceof InputStream)) { + // Defensive guard: should never happen given the matcher's arg0 constraint. + return null; + } + InputStream inputStream = (InputStream) in; String lambdaRequestId = awsContext.getAwsRequestId(); - AgentSpanContext lambdaContext = AgentTracer.get().notifyLambdaStart(in, lambdaRequestId); + AgentSpanContext lambdaContext = + AgentTracer.get().notifyLambdaStart(inputStream, lambdaRequestId); + // Skip the strip pass entirely (extra read/parse/re-serialize) unless explicitly enabled. + if (Config.get().isLambdaStripInjectedContextEnabled()) { + InputStream stripped = StripInjectedContext.replaceInputStream(inputStream); + if (stripped != null) { + inputStream = stripped; + } + } + in = inputStream; + final AgentSpan span; if (null == lambdaContext) { span = startSpan("java-aws-sdk", INVOCATION_SPAN_NAME); diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/StripInjectedContext.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/StripInjectedContext.java new file mode 100644 index 00000000000..0042a30c673 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/main/java/datadog/trace/instrumentation/aws/v1/lambda/StripInjectedContext.java @@ -0,0 +1,267 @@ +package datadog.trace.instrumentation.aws.v1.lambda; + +import datadog.json.JsonMapper; +import datadog.json.JsonReader; +import datadog.trace.api.Config; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Removes {@code "_datadog":{...}} trace-context carriers injected by Datadog into Lambda events + * (EventBridge, SNS, SQS, ...) before the customer handler reads them. + * + *

Carriers are cut out of the JSON text instead of parsing and re-serializing the payload, so + * numbers, key order and formatting of customer data are kept. Carriers inside string-encoded JSON + * values (an SNS {@code Message}, an SQS {@code body}) are handled by decoding that string, + * stripping it recursively up to {@link #MAX_DEPTH} levels, and re-encoding it. Object keys are + * never rewritten, and a {@code _datadog} key whose value is not an object is customer data and is + * kept. Fail-open: on any error the original payload is used. + */ +public final class StripInjectedContext { + + private static final Logger log = LoggerFactory.getLogger(StripInjectedContext.class); + + /** SNS-to-SQS fan-out needs 2 levels of string-encoded JSON; one more is a safety margin. */ + static final int MAX_DEPTH = 3; + + private static final String CARRIER_KEY = "_datadog"; + private static final String QUOTED_CARRIER_KEY = "\"" + CARRIER_KEY + "\""; + private static final byte[] CARRIER_KEY_BYTES = CARRIER_KEY.getBytes(StandardCharsets.UTF_8); + + /** Returned by {@link #carrierRange} when the carrier object never closes (malformed JSON). */ + private static final int[] UNCLOSED = new int[0]; + + private StripInjectedContext() {} + + /** + * Returns a stream over the stripped payload, or {@code null} to keep {@code in}. Like the other + * Lambda event consumers, only {@link ByteArrayInputStream} is supported, because it can be read + * with mark/reset without consuming the customer's input. + */ + public static ByteArrayInputStream replaceInputStream(InputStream in) { + if (!(in instanceof ByteArrayInputStream)) { + return null; + } + byte[] original = readAllBytes((ByteArrayInputStream) in); + try { + return new ByteArrayInputStream(strip(original)); + } catch (RuntimeException e) { + log.debug("Unable to strip _datadog from Lambda payload", e); + return new ByteArrayInputStream(original); + } + } + + /** Strips "_datadog" from {@code payload} when the config is enabled. */ + public static byte[] strip(byte[] payload) { + if (!Config.get().isLambdaStripInjectedContextEnabled()) { + return payload; + } + return stripInternal(payload); + } + + /** Unconditional strip logic, package-private so tests can call it directly. */ + static byte[] stripInternal(byte[] payload) { + if (payload == null || !containsCarrierKey(payload)) { + return payload; + } + String json = new String(payload, StandardCharsets.UTF_8); + // Invalid UTF-8 decodes to U+FFFD and would be re-encoded as such, so keep the payload as is. + if (json.indexOf('\uFFFD') >= 0) { + return payload; + } + String stripped = stripJson(json, 0); + return stripped.equals(json) ? payload : stripped.getBytes(StandardCharsets.UTF_8); + } + + /** + * Walks the string tokens of {@code json} once. A {@code "_datadog"} key with an object value is + * cut out; any other string value containing {@code _datadog} is treated as encoded JSON and + * stripped recursively. Returns {@code json} itself when nothing changed. + */ + private static String stripJson(String json, int depth) { + StringBuilder out = null; + int copyFrom = 0; + int nextKey = -1; + int start = json.indexOf('"'); + while (start >= 0) { + if (nextKey < start) { + nextKey = json.indexOf(CARRIER_KEY, start); + if (nextKey < 0) { + break; + } + } + int end = stringEnd(json, start); + if (end < 0) { + break; + } + int[] carrier = carrierRange(json, start, end, copyFrom); + if (carrier == UNCLOSED) { + break; + } + if (carrier != null) { + if (out == null) { + out = new StringBuilder(json.length()); + } + out.append(json, copyFrom, carrier[0]); + copyFrom = carrier[1]; + end = carrier[1]; + } else if (nextKey < end && depth < MAX_DEPTH && !isKey(json, end)) { + String replacement = stripEncodedJson(json.substring(start, end), depth); + if (replacement != null) { + if (out == null) { + out = new StringBuilder(json.length()); + } + out.append(json, copyFrom, start).append(replacement); + copyFrom = end; + } + } + start = json.indexOf('"', end); + } + if (out == null) { + return json; + } + return out.append(json, copyFrom, json.length()).toString(); + } + + /** + * If the string token {@code [keyStart, keyEnd)} is a {@code "_datadog"} key with an object + * value, returns the {@code [start, end)} range to cut, including one adjacent comma so the JSON + * stays valid. A preceding comma before {@code copyFrom} was already cut and is not reused. + * Returns {@link #UNCLOSED} if the value object never closes. + */ + private static int[] carrierRange(String json, int keyStart, int keyEnd, int copyFrom) { + if (keyEnd - keyStart != QUOTED_CARRIER_KEY.length() + || !json.startsWith(QUOTED_CARRIER_KEY, keyStart)) { + return null; + } + int colon = skipWhitespace(json, keyEnd); + if (colon >= json.length() || json.charAt(colon) != ':') { + return null; + } + int valueStart = skipWhitespace(json, colon + 1); + if (valueStart >= json.length() || json.charAt(valueStart) != '{') { + return null; + } + int valueEnd = objectEnd(json, valueStart); + if (valueEnd < 0) { + return UNCLOSED; + } + int before = skipWhitespaceBackward(json, keyStart - 1); + if (before >= copyFrom && json.charAt(before) == ',') { + return new int[] {before, valueEnd}; + } + int after = skipWhitespace(json, valueEnd); + if (after < json.length() && json.charAt(after) == ',') { + return new int[] {keyStart, after + 1}; + } + return new int[] {keyStart, valueEnd}; + } + + /** + * Decodes the JSON string literal {@code literal}, strips it if it looks like a JSON object + * (starts with '{' and ends with '}'), and returns the re-encoded literal, or {@code null} if + * nothing changed. + */ + private static String stripEncodedJson(String literal, int depth) { + String decoded; + try { + decoded = new JsonReader(literal).nextString(); + } catch (IOException e) { + return null; + } + int first = skipWhitespace(decoded, 0); + if (first >= decoded.length() || decoded.charAt(first) != '{') { + return null; + } + int last = skipWhitespaceBackward(decoded, decoded.length() - 1); + if (decoded.charAt(last) != '}') { + return null; + } + String stripped = stripJson(decoded, depth + 1); + return stripped.equals(decoded) ? null : JsonMapper.toJson(stripped); + } + + /** Returns the index just past the closing quote of the string opening at {@code start}. */ + private static int stringEnd(String json, int start) { + for (int i = start + 1; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '\\') { + i++; + } else if (c == '"') { + return i + 1; + } + } + return -1; + } + + /** Returns the index just past the closing brace of the object opening at {@code start}. */ + private static int objectEnd(String json, int start) { + int depth = 0; + for (int i = start; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '"') { + int end = stringEnd(json, i); + if (end < 0) { + return -1; + } + i = end - 1; + } else if (c == '{') { + depth++; + } else if (c == '}' && --depth == 0) { + return i + 1; + } + } + return -1; + } + + /** Object keys are customer property names and are never rewritten. */ + private static boolean isKey(String json, int stringEnd) { + int next = skipWhitespace(json, stringEnd); + return next < json.length() && json.charAt(next) == ':'; + } + + private static int skipWhitespace(String json, int i) { + while (i < json.length() && isWhitespace(json.charAt(i))) { + i++; + } + return i; + } + + private static int skipWhitespaceBackward(String json, int i) { + while (i >= 0 && isWhitespace(json.charAt(i))) { + i--; + } + return i; + } + + private static boolean isWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + private static boolean containsCarrierKey(byte[] payload) { + outer: + for (int i = 0; i <= payload.length - CARRIER_KEY_BYTES.length; i++) { + for (int j = 0; j < CARRIER_KEY_BYTES.length; j++) { + if (payload[i + j] != CARRIER_KEY_BYTES[j]) { + continue outer; + } + } + return true; + } + return false; + } + + /** Reads all bytes from {@code in} and restores its position, so the stream is not consumed. */ + private static byte[] readAllBytes(ByteArrayInputStream in) { + in.mark(Integer.MAX_VALUE); + byte[] bytes = new byte[in.available()]; + int read = in.read(bytes, 0, bytes.length); + in.reset(); + return read == bytes.length ? bytes : Arrays.copyOf(bytes, Math.max(read, 0)); + } +} diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java index 0aff2114e51..a5a1563d2d5 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/LambdaHandlerInstrumentationTest.java @@ -15,9 +15,11 @@ import com.amazonaws.services.lambda.runtime.CognitoIdentity; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import datadog.trace.agent.test.AbstractInstrumentationTest; import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; +import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.function.TriConsumer; import datadog.trace.api.function.TriFunction; import datadog.trace.api.gateway.Flow; @@ -34,6 +36,8 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -634,6 +638,46 @@ void responseCallbacksReceiveNoDataWhenHandlerThrows() { error(Error.class, "Some error")))); } + @WithConfig(key = GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT, value = "true") + @Test + void stripsInjectedDatadogContextFromEventBridgeDetail() throws IOException { + String eventJson = + "{" + + "\"detail-type\": \"order.created\"," + + "\"detail\": {" + + " \"orderId\":42," + + " \"_datadog\":{\"x-datadog-trace-id\": \"123\"}" + + "}" + + "}"; + + ByteArrayInputStream input = + new ByteArrayInputStream(eventJson.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + class CapturingHandler implements RequestStreamHandler { + String capturedBody; + + @Override + public void handleRequest(InputStream in, OutputStream out, Context context) + throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = in.read(chunk)) != -1) { + buffer.write(chunk, 0, n); + } + capturedBody = new String(buffer.toByteArray(), StandardCharsets.UTF_8); + out.write("{}".getBytes(StandardCharsets.UTF_8)); + } + } + + CapturingHandler handler = new CapturingHandler(); + handler.handleRequest(input, output, newContext()); + + assertFalse(handler.capturedBody.contains("_datadog")); + assertTrue(handler.capturedBody.contains("\"orderId\":42")); + } + private static final class TestContext implements Context { private final String requestId; diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextFuzzTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextFuzzTest.java new file mode 100644 index 00000000000..3b665d14d00 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextFuzzTest.java @@ -0,0 +1,380 @@ +package datadog.trace.instrumentation.aws.v1.lambda; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Random; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Randomized invariant testing for {@link StripInjectedContext}, mirroring the repo's existing + * fuzz-style test pattern (see {@code TagMapFuzzTest}). Generates many random payloads in + * EventBridge, SQS, and SNS shapes instead of enumerating fixed cases by hand, and asserts core + * invariants hold for every one of them. + */ +public final class StripInjectedContextFuzzTest { + + private static final long SEED = envLong("FUZZ_SEED", System.nanoTime()); + private static final int ITERATIONS = (int) envLong("FUZZ_ITERATIONS", 5_000); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String[] SEEDS = { + "{\"detail\":{\"_datadog\":{\"x-datadog-trace-id\":\"1\"},\"foo\":\"bar\"}}", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\",\\\"_datadog\\\":{\\\"k\\\":\\\"v\\\"}}\"}", + "{\"foo\":\"a,}b\",\"_datadog\":{\"x\":1}}", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\",\\\"_datadog\\\":{\\\"k\\\":\\\"v}z\\\"}}\"}", + "{\"_datadog\":{},\"_datadog\":{}}", + "not even json", + "{}", + "" + }; + + /** Inputs that once failed. Paste the input from a failure message here so it always runs. */ + private static final String[] REGRESSIONS = {}; + + @BeforeAll + static void printSeed() { + System.out.println( + "StripInjectedContextFuzzTest FUZZ_SEED=" + SEED + " FUZZ_ITERATIONS=" + ITERATIONS); + } + + static Stream corpus() { + return Stream.concat(Arrays.stream(SEEDS), Arrays.stream(REGRESSIONS)); + } + + @ParameterizedTest + @MethodSource("corpus") + void seedCorpusHoldsInvariants(String input) { + checkInvariants(input, ""); + } + + private static final String[] FRAGMENTS = { + "{", + "}", + "[", + "]", + "\"", + ",", + ":", + "\\", + " ", + "\\\"", + "_datadog", + "\"_datadog\":", + "\"_datadog\":{}", + "\\u00e9", + "1" + }; + + private static final String[] STRING_PARTS = { + "a", "}", "{", ",", "\\\"", "\\\\", ":", " ", "_datadog", "\\u00e9" + }; + + // A "_datadog" key inside the "other" value would itself be stripped, so keys there avoid it. + private static final String[] OTHER_KEY_PARTS = { + "a", "}", "{", ",", "\\\"", "\\\\", ":", " ", "x_datadog", "\\u00e9" + }; + + private static void checkInvariants(String input, String where) { + byte[] in = input.getBytes(StandardCharsets.UTF_8); + byte[] out; + try { + out = StripInjectedContext.stripInternal(in); + } catch (Throwable t) { + fail(where + "stripInternal threw for in=" + input, t); + return; + } + // Malformed input is only guaranteed not to throw: cutting a carrier out of it can splice + // the surrounding bytes into a new carrier, so it is not necessarily idempotent. + if (isValidJson(in)) { + assertTrue( + isValidJson(out), where + "valid JSON became invalid: in=" + input + " out=" + utf8(out)); + assertArrayEquals( + out, StripInjectedContext.stripInternal(out), where + "not idempotent: in=" + input); + } + } + + private static String where(int iteration) { + return "[FUZZ_SEED=" + SEED + " iteration=" + iteration + "] "; + } + + private static long envLong(String name, long defaultValue) { + String value = System.getenv(name); + return value == null || value.isEmpty() ? defaultValue : Long.parseLong(value); + } + + @Test + void neverThrowsAndAlwaysRemovesDatadogWhenPresent() { + Random random = new Random(SEED); + + for (int i = 0; i < ITERATIONS; i++) { + byte[] payload = randomPayload(random); + String input = utf8(payload); + + byte[] result; + try { + result = StripInjectedContext.stripInternal(payload); + } catch (Throwable t) { + fail(where(i) + "stripInternal threw for in=" + input, t); + return; + } + + String resultJson = utf8(result); + assertFalse( + resultJson.contains("_datadog"), + where(i) + "output contains _datadog: in=" + input + " out=" + resultJson); + assertArrayEquals( + result, + StripInjectedContext.stripInternal(result), + where(i) + "not idempotent: in=" + input); + } + } + + @Test + void mutatedSeedsKeepValidJsonValidAndAreIdempotent() { + Random random = new Random(SEED); + + for (int i = 0; i < ITERATIONS; i++) { + checkInvariants(mutate(SEEDS[random.nextInt(SEEDS.length)], random), where(i)); + } + } + + @Test + void arbitraryCarrierIsAlwaysRemovedAndOtherFieldSurvives() throws IOException { + Random random = new Random(SEED); + + for (int i = 0; i < ITERATIONS; i++) { + String carrier = randomObject(random, 0, STRING_PARTS); + String other = randomValue(random, 0, OTHER_KEY_PARTS); + String payload = "{\"_datadog\":" + carrier + ",\"other\":" + other + "}"; + + byte[] out = StripInjectedContext.stripInternal(payload.getBytes(StandardCharsets.UTF_8)); + + JsonNode result; + try { + result = MAPPER.readTree(out); + } catch (IOException e) { + fail(where(i) + "output is not valid JSON: in=" + payload + " out=" + utf8(out), e); + return; + } + assertFalse( + result.has("_datadog"), + where(i) + "carrier was not removed: in=" + payload + " out=" + utf8(out)); + assertEquals( + MAPPER.readTree(other), result.get("other"), where(i) + "other changed: in=" + payload); + } + } + + private static byte[] randomPayload(Random random) { + switch (random.nextInt(3)) { + case 0: + return randomEventBridgePayload(random); + case 1: + return randomTopLevelCarrierPayload(random); + default: + return randomStringEncodedFieldPayload(random); + } + } + + // EventBridge-shaped: {"detail-type": ..., "detail": {...}} or {"detail": "..."} + private static byte[] randomEventBridgePayload(Random random) { + StringBuilder sb = new StringBuilder(); + sb.append("{\"detail-type\":\"order.created\",\"detail\":"); + boolean asString = random.nextBoolean(); + boolean includeDatadog = random.nextBoolean(); + + String detailJson = randomDetailObject(random, includeDatadog); + if (asString) { + sb.append('"').append(detailJson.replace("\"", "\\\"")).append('"'); + } else { + sb.append(detailJson); + } + sb.append('}'); + return sb.toString().getBytes(StandardCharsets.UTF_8); + } + + // SQS/generic-shaped: _datadog appears directly as a top-level key. + // Randomly places _datadog as the sole field, the first field, a middle field, + // or the last field so that all four comma-placement cases in carrierRange + // are exercised across the fuzz iterations. + private static byte[] randomTopLevelCarrierPayload(Random random) { + boolean includeDatadog = random.nextBoolean(); + // Allow 0 regular fields so the sole-field case ({"_datadog":{...}}) is also generated. + int fieldCount = random.nextInt(4); + // Choose a random insertion position for the _datadog key among the regular fields. + // Position 0 = first, fieldCount = last, values in between = middle. + int datadogPos = includeDatadog ? random.nextInt(fieldCount + 1) : fieldCount + 1; + + StringBuilder sb = new StringBuilder("{"); + boolean needsComma = false; + int regularIdx = 0; + + for (int slot = 0; slot <= fieldCount; slot++) { + if (slot == datadogPos) { + // Insert _datadog at the chosen position. + if (needsComma) sb.append(','); + sb.append("\"_datadog\":{\"x-datadog-trace-id\":\"") + .append(random.nextLong()) + .append("\"}"); + needsComma = true; + } + if (regularIdx < fieldCount) { + // Insert one regular field at this slot. + if (needsComma) sb.append(','); + sb.append("\"field").append(regularIdx).append("\":"); + appendRandomValue(sb, random); + needsComma = true; + regularIdx++; + } + } + + sb.append('}'); + return sb.toString().getBytes(StandardCharsets.UTF_8); + } + + private static String randomDetailObject(Random random, boolean includeDatadog) { + StringBuilder sb = new StringBuilder("{"); + int fieldCount = 1 + random.nextInt(5); + for (int i = 0; i < fieldCount; i++) { + if (i > 0) { + sb.append(','); + } + sb.append('"').append("field").append(i).append("\":"); + appendRandomValue(sb, random); + } + if (includeDatadog) { + if (fieldCount > 0) { + sb.append(','); + } + sb.append("\"_datadog\":{\"x-datadog-trace-id\":\"").append(random.nextLong()).append("\"}"); + } + sb.append('}'); + return sb.toString(); + } + + // SNS-shaped: a top-level string field contains a string-encoded JSON object with _datadog. + private static byte[] randomStringEncodedFieldPayload(Random random) { + boolean includeDatadog = random.nextBoolean(); + String innerJson = randomDetailObject(random, includeDatadog); + String escaped = innerJson.replace("\"", "\\\""); + String payload = + "{\"Type\":\"Notification\",\"Message\":\"" + escaped + "\",\"Subject\":\"test\"}"; + return payload.getBytes(StandardCharsets.UTF_8); + } + + private static void appendRandomValue(StringBuilder sb, Random random) { + switch (random.nextInt(4)) { + case 0: + sb.append(random.nextInt()); + break; + case 1: + sb.append(random.nextDouble()); + break; + case 2: + sb.append('"').append("value").append(random.nextInt(1000)).append('"'); + break; + default: + sb.append(random.nextBoolean()); + } + } + + /** Applies up to three random insert, delete, or replace edits using JSON-significant text. */ + private static String mutate(String seed, Random random) { + StringBuilder sb = new StringBuilder(seed); + int edits = random.nextInt(4); + for (int e = 0; e < edits; e++) { + int pos = random.nextInt(sb.length() + 1); + switch (random.nextInt(3)) { + case 0: + sb.insert(pos, FRAGMENTS[random.nextInt(FRAGMENTS.length)]); + break; + case 1: + if (pos < sb.length()) { + sb.deleteCharAt(pos); + } + break; + default: + if (pos < sb.length()) { + sb.setCharAt(pos, FRAGMENTS[random.nextInt(FRAGMENTS.length)].charAt(0)); + } + } + } + return sb.toString(); + } + + private static String randomValue(Random random, int depth, String[] keyParts) { + switch (random.nextInt(depth < 3 ? 7 : 5)) { + case 0: + return Long.toString(random.nextLong()); + case 1: + return random.nextBoolean() ? "true" : "null"; + case 2: + return randomString(random, STRING_PARTS); + case 3: + return "1.50"; + case 4: + return "9007199254740993"; + case 5: + return randomObject(random, depth + 1, keyParts); + default: + StringBuilder sb = new StringBuilder("["); + for (int k = 0, n = random.nextInt(3); k < n; k++) { + sb.append(k > 0 ? "," : "").append(randomValue(random, depth + 1, keyParts)); + } + return sb.append(']').toString(); + } + } + + private static String randomObject(Random random, int depth, String[] keyParts) { + StringBuilder sb = new StringBuilder("{"); + for (int k = 0, n = random.nextInt(4); k < n; k++) { + sb.append(k > 0 ? "," : "") + .append(randomString(random, keyParts)) + .append(':') + .append(randomValue(random, depth + 1, keyParts)); + } + return sb.append('}').toString(); + } + + private static String randomString(Random random, String[] parts) { + StringBuilder sb = new StringBuilder("\""); + for (int k = 0, n = random.nextInt(5); k < n; k++) { + sb.append(parts[random.nextInt(parts.length)]); + } + return sb.append('"').toString(); + } + + /** Strict check: exactly one JSON value with nothing but whitespace after it. */ + private static boolean isValidJson(byte[] json) { + if (json.length == 0) { + return false; + } + try (JsonParser parser = MAPPER.getFactory().createParser(json)) { + if (parser.nextToken() == null) { + return false; + } + parser.skipChildren(); + return parser.nextToken() == null; + } catch (IOException e) { + return false; + } + } + + private static String utf8(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextTest.java new file mode 100644 index 00000000000..837af133305 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-lambda-handler-1.2/src/test/java/StripInjectedContextTest.java @@ -0,0 +1,696 @@ +package datadog.trace.instrumentation.aws.v1.lambda; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.config.GeneralConfig; +import datadog.trace.test.junit.utils.config.WithConfig; +import datadog.trace.test.util.DDJavaSpecification; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** Unit tests for {@link StripInjectedContext}. */ +class StripInjectedContextTest extends DDJavaSpecification { + + private static byte[] bytes(String json) { + return json.getBytes(StandardCharsets.UTF_8); + } + + private static String toUtf8(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + + private static byte[] readAll(ByteArrayInputStream in) { + in.mark(Integer.MAX_VALUE); + byte[] bytes = new byte[in.available()]; + int read = in.read(bytes, 0, bytes.length); + in.reset(); + return read == bytes.length ? bytes : java.util.Arrays.copyOf(bytes, Math.max(read, 0)); + } + + // ============================================================================ + // stripInternal: detail as an object + // ============================================================================ + + @Test + void removesDatadogKeyFromObjectDetail() { + byte[] input = + bytes( + "{\"detail-type\":\"order.created\",\"detail\":{\"orderId\":42," + + "\"_datadog\":{\"x-datadog-trace-id\":\"123\"}}}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(json.contains("\"orderId\":42"), "unrelated fields must survive untouched"); + assertTrue(!json.contains("_datadog"), "the _datadog carrier must be removed"); + } + + // ============================================================================ + // stripInternal: detail as a string-encoded JSON object + // ============================================================================ + + @Test + void removesDatadogKeyFromStringEncodedDetail() { + // "detail" itself is a JSON string containing an escaped JSON object + byte[] input = + bytes( + "{\"detail\":\"{\\\"orderId\\\":42,\\\"_datadog\\\":{\\\"x-datadog-trace-id\\\":\\\"123\\\"}}\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog")); + assertTrue(json.contains("orderId")); + } + + // ============================================================================ + // Fail-open cases: original payload must be returned unchanged + // ============================================================================ + + @Test + void returnsOriginalWhenDatadogKeyIsAbsent() { + byte[] input = bytes("{\"detail\":{\"orderId\":42}}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + @Test + void removesTopLevelDatadogKey() { + // _datadog appearing directly as a top-level key is removed regardless of event shape. + byte[] input = bytes("{\"detail-type\":\"order.created\",\"_datadog\":{\"trace\":\"1\"}}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog at the top level must be stripped"); + assertTrue(json.contains("detail-type"), "unrelated top-level fields must survive"); + } + + @Test + void returnsOriginalOnMalformedJson() { + byte[] input = bytes("{not valid json, but contains _datadog anyway"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + @Test + void returnsOriginalForNullOrEmptyPayload() { + assertNull(StripInjectedContext.stripInternal(null)); + assertArrayEquals(new byte[0], StripInjectedContext.stripInternal(new byte[0])); + } + + @Test + void returnsOriginalWhenDetailIsNeitherObjectNorString() { + // e.g. detail is a number — nothing to strip from, so the fast path returns unchanged. + byte[] input = bytes("{\"detail\":42}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + // ============================================================================ + // Number type preservation (the whole reason for the custom Moshi adapter) + // ============================================================================ + + @Test + void preservesWholeNumbersAsIntegersAfterStripping() { + byte[] input = + bytes("{\"detail\":{\"orderId\":42,\"quantity\":7," + "\"_datadog\":{\"trace\":\"1\"}}}"); + + String json = toUtf8(StripInjectedContext.stripInternal(input)); + + // Without the custom adapter, Moshi's default would turn these into "42.0"/"7.0" + assertTrue(json.contains("\"orderId\":42")); + assertTrue(json.contains("\"quantity\":7")); + assertTrue(!json.contains(".0")); + } + + @Test + void preservesFractionalNumbersAsDoublesAfterStripping() { + byte[] input = bytes("{\"detail\":{\"price\":19.99,\"_datadog\":{\"trace\":\"1\"}}}"); + + String json = toUtf8(StripInjectedContext.stripInternal(input)); + + assertTrue(json.contains("\"price\":19.99")); + } + + @Test + void preservesLargeIntegersBeyondIntRangeAsLong() { + byte[] input = + bytes("{\"detail\":{\"bigId\":9007199254740993,\"_datadog\":{\"trace\":\"1\"}}}"); + + String json = toUtf8(StripInjectedContext.stripInternal(input)); + + // A double could not represent this value exactly; Long must be used instead. + assertTrue(json.contains("\"bigId\":9007199254740993")); + } + + // ============================================================================ + // strip(): the public entry point that is gated by config + // ============================================================================ + + @Test + void stripReturnsOriginalPayloadWhenConfigIsDisabledByDefault() { + byte[] input = bytes("{\"detail\":{\"_datadog\":{\"trace\":\"1\"}}}"); + + byte[] result = StripInjectedContext.strip(input); + + assertArrayEquals(input, result); + } + + @Test + @WithConfig(key = GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT, value = "true") + void stripRemovesDatadogKeyWhenConfigIsEnabled() { + byte[] input = bytes("{\"detail\":{\"orderId\":1,\"_datadog\":{\"trace\":\"1\"}}}"); + + byte[] result = StripInjectedContext.strip(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog")); + } + + // ============================================================================ + // Non-EventBridge payloads: direct top-level _datadog key (SQS, Kinesis, etc.) + // ============================================================================ + + @Test + void removesTopLevelDatadogKeyFromSqsStylePayload() { + // SQS-style: _datadog appears as a direct top-level property alongside business data. + byte[] input = + bytes( + "{\"orderId\":\"abc-123\",\"amount\":99," + + "\"_datadog\":{\"x-datadog-trace-id\":\"456\",\"x-datadog-parent-id\":\"789\"}}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog carrier must be removed"); + assertTrue(json.contains("\"orderId\":\"abc-123\""), "orderId must survive untouched"); + assertTrue(json.contains("\"amount\":99"), "amount must survive untouched"); + } + + @Test + void returnsOriginalWhenTopLevelDatadogKeyIsAbsent() { + // No _datadog anywhere: fast path should return the original byte array unchanged. + byte[] input = bytes("{\"orderId\":\"abc-123\",\"amount\":99}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + // ============================================================================ + // Non-EventBridge payloads: string-encoded JSON fields (SNS, etc.) + // ============================================================================ + + @Test + void removesDatadogKeyFromStringEncodedNonDetailField() { + // SNS-style: the "Message" field is a string-encoded JSON object containing _datadog. + byte[] input = + bytes( + "{\"Type\":\"Notification\"," + + "\"Message\":\"{\\\"hello\\\":\\\"world\\\"," + + "\\\"_datadog\\\":{\\\"x-datadog-trace-id\\\":\\\"123\\\"}}\"," + + "\"Subject\":\"test-event\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be stripped from string-encoded Message"); + assertTrue(json.contains("hello"), "business data inside Message must survive"); + assertTrue(json.contains("Subject"), "other top-level fields must survive"); + } + + @Test + void returnsOriginalWhenStringEncodedFieldContainsNoDatadogKey() { + // A string-encoded JSON field that doesn't carry _datadog must be left completely unchanged. + byte[] input = + bytes("{\"Type\":\"Notification\",\"Message\":\"{\\\"hello\\\":\\\"world\\\"}\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + @Test + void handlesMultipleTopLevelFieldsWithDatadogCarrier() { + // Both a direct top-level _datadog key and a string-encoded field containing _datadog + // are present at the same time; both must be stripped. + byte[] input = + bytes( + "{\"_datadog\":{\"trace\":\"1\"}," + + "\"Message\":\"{\\\"key\\\":\\\"val\\\"," + + "\\\"_datadog\\\":{\\\"trace\\\":\\\"2\\\"}}\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "all _datadog carriers must be stripped"); + assertTrue(json.contains("key"), "business data inside Message must survive"); + } + + // ============================================================================ + // Byte-level splice edge cases: comma placement around the removed carrier + // ============================================================================ + + @Test + void removesDatadogWhenItIsTheSoleField() { + // carrierRange: no preceding comma, no following comma -> remove key+value only. + // Result must be a valid empty object, not a dangling comma. + byte[] input = bytes("{\"_datadog\":{\"trace\":\"1\"}}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be removed"); + assertTrue(json.equals("{}"), "result must be a valid empty object, got: " + json); + } + + @Test + void removesDatadogWhenItIsTheFirstField() { + // carrierRange: no preceding comma but a following comma exists -> remove trailing + // comma. + // If the trailing comma is left in, the result is invalid JSON: {"other":"val"} vs + // {,"other":"val"}. + byte[] input = bytes("{\"_datadog\":{\"trace\":\"1\"},\"other\":\"val\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be removed"); + assertTrue(json.contains("\"other\":\"val\""), "following field must survive"); + assertTrue(!json.startsWith("{,"), "result must not start with a dangling comma"); + } + + @Test + void removesDatadogWhenItIsTheMiddleField() { + // carrierRange: both a preceding comma and a following comma exist. + // The preceding comma is consumed (preferred), so the remaining fields stay adjacent. + byte[] input = bytes("{\"first\":\"a\",\"_datadog\":{\"trace\":\"1\"},\"last\":\"b\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be removed"); + assertTrue(json.contains("\"first\":\"a\""), "first field must survive"); + assertTrue(json.contains("\"last\":\"b\""), "last field must survive"); + assertTrue(!json.contains(",,"), "result must not contain a double comma"); + } + + @Test + void doesNotRemoveDatadogKeyWhenValueIsNotAnObject() { + // The byte-level splice only removes "_datadog":{...} (object value form). + // A string, null, or number value is not a carrier and must be left untouched. + byte[] input = bytes("{\"_datadog\":\"not-a-carrier\",\"other\":1}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + @Test + void doesNotMatchDatadogTextEmbeddedInsideAStringValue() { + // A string value that happens to contain the text "_datadog:" must not trigger removal. + // String tokens are skipped as a whole, so text inside a value is never treated as a key. + byte[] input = bytes("{\"note\":\"the _datadog: key is for tracing\",\"other\":1}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result); + } + + @Test + void handlesWhitespacePaddedJson() { + // skipWhitespace and skipWhitespaceBackward must tolerate spaces, tabs, and newlines + // around the key, colon, and value so that pretty-printed payloads are handled correctly. + byte[] input = bytes("{ \"_datadog\": { \"trace\" : \"1\" } , \"other\" : 1 }"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be removed from whitespace-padded JSON"); + assertTrue(json.contains("\"other\""), "unrelated field must survive"); + } + + // ============================================================================ + // replaceInputStream(): the InputStream-based helper used by the advice + // ============================================================================ + + @Test + void replaceInputStreamReturnsNullForNullInput() { + assertNull(StripInjectedContext.replaceInputStream(null)); + } + + @Test + @WithConfig(key = GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT, value = "true") + void replaceInputStreamStripsAndDoesNotConsumeTheOriginalStream() { + String eventJson = "{\"detail\":{\"orderId\":1,\"_datadog\":{\"trace\":\"1\"}}}"; + ByteArrayInputStream original = + new ByteArrayInputStream(eventJson.getBytes(StandardCharsets.UTF_8)); + + ByteArrayInputStream stripped = StripInjectedContext.replaceInputStream(original); + + assertNotNull(stripped); + String strippedJson = new String(readAll(stripped), StandardCharsets.UTF_8); + assertTrue(!strippedJson.contains("_datadog")); + + // readAllBytes() inside replaceInputStream() must mark/reset, not drain, the original stream + String originalJson = new String(readAll(original), StandardCharsets.UTF_8); + assertTrue(originalJson.contains("_datadog"), "original stream must still be fully readable"); + } + + @Test + @WithConfig(key = GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT, value = "true") + void replaceInputStreamDoesNotReadUnsupportedStreams() { + byte[] payload = bytes("{\"a\":1,\"_datadog\":{\"t\":\"1\"}}"); + AtomicInteger reads = new AtomicInteger(); + InputStream failsAfterPrefix = + new InputStream() { + private int pos; + + @Override + public int read() throws IOException { + reads.incrementAndGet(); + if (pos >= 5) { + throw new IOException("stream failed after a prefix"); + } + return payload[pos++]; + } + }; + + assertNull(StripInjectedContext.replaceInputStream(failsAfterPrefix)); + assertEquals(0, reads.get(), "an unsupported stream must be passed through unread"); + } + + @Test + void doesNotRewriteObjectKeysThatLookLikeEncodedJson() { + // {"{\"_datadog\":{}}":1,"{}":2} must not become {"{}":1,"{}":2} + byte[] input = bytes("{\"{\\\"_datadog\\\":{}}\":1,\"{}\":2}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result, "object keys must never be rewritten: " + toUtf8(result)); + } + + @Test + void keepsPayloadWithInvalidUtf8Untouched() { + // byte 6 is the 'X' value; 0xFF is never valid in UTF-8 + byte[] input = bytes("{\"a\":\"X\",\"_datadog\":{}}"); + input[6] = (byte) 0xFF; + + assertArrayEquals(input, StripInjectedContext.stripInternal(input)); + } + + @Test + void keepsPayloadWithLiteralReplacementCharacterUntouched() { + // A raw U+FFFD can't be told apart from a decoded invalid byte, so the payload is kept as is. + byte[] input = bytes("{\"a\":\"\uFFFD\",\"_datadog\":{}}"); + + assertArrayEquals(input, StripInjectedContext.stripInternal(input)); + } + + @Test + void stillStripsPayloadWithEscapedReplacementCharacter() { + String result = + toUtf8(StripInjectedContext.stripInternal(bytes("{\"a\":\"\\uFFFD\",\"_datadog\":{}}"))); + + assertEquals("{\"a\":\"\\uFFFD\"}", result); + } + + @Test + void stillStripsPayloadWithValidMultiByteUtf8() { + String result = + toUtf8( + StripInjectedContext.stripInternal(bytes("{\"a\":\"\u00e9\u4e2d\",\"_datadog\":{}}"))); + + assertEquals("{\"a\":\"\u00e9\u4e2d\"}", result); + } + + @Test + void doesNotTreatFreeTextStartingWithBraceAsEncodedJson() { + byte[] input = bytes("{\"note\":\"{ pasted \\\"_datadog\\\":{x} here\"}"); + + assertArrayEquals(input, StripInjectedContext.stripInternal(input)); + } + + @Test + void stillStripsEncodedJsonWithSurroundingWhitespace() { + String result = + toUtf8( + StripInjectedContext.stripInternal( + bytes("{\"m\":\" {\\\"a\\\":1,\\\"_datadog\\\":{}} \"}"))); + + assertEquals("{\"m\":\" {\\\"a\\\":1} \"}", result); + } + + @Test + void stripsEncodedValueButKeepsEncodedLookingKeyInSameObject() { + String key = "\"{\\\"_datadog\\\":{}}\""; + byte[] input = bytes("{" + key + ":\"{\\\"a\\\":1,\\\"_datadog\\\":{\\\"t\\\":\\\"1\\\"}}\"}"); + + String result = toUtf8(StripInjectedContext.stripInternal(input)); + + assertEquals("{" + key + ":\"{\\\"a\\\":1}\"}", result); + } + + @Test + void doesNotRemoveNonObjectDatadogValueInsideStringEncodedField() { + // Regression test for review feedback: a string-encoded business payload (e.g. SQS body) + // whose "_datadog" field holds a plain string, not an object, must survive untouched. + // Only object-shaped propagation carriers ("_datadog":{...}) are carriers; anything else + // is customer data that happens to reuse the same key name. + byte[] input = + bytes( + "{\"Records\":[{\"body\":\"{\\\"_datadog\\\":\\\"customer-value\\\"," + + "\\\"order\\\":\\\"foo\\\"}\"}]}"); + + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals( + input, result, "non-object _datadog value inside a string-encoded field must be preserved"); + } + + @Test + void removesDatadogFromNestedSqsRecordsBody() { + // SQS Records[*].body is itself a string-encoded JSON object. + byte[] input = + bytes( + "{\"Records\":[{\"body\":\"{\\\"orderId\\\":1," + + "\\\"_datadog\\\":{\\\"trace\\\":\\\"1\\\"}}\"}]}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog inside Records[].body must be stripped"); + assertTrue(json.contains("orderId")); + } + + @Test + void removesDatadogFromDoubleEncodedSnsInsideSqsBody() { + // SQS body containing an SNS envelope whose Message is itself string-encoded JSON. + String snsMessage = + "{\\\\\\\"orderId\\\\\\\":1,\\\\\\\"_datadog\\\\\\\":" + + "{\\\\\\\"trace\\\\\\\":\\\\\\\"1\\\\\\\"}}"; + byte[] input = + bytes("{\"Records\":[{\"body\":\"{\\\"Message\\\":\\\"" + snsMessage + "\\\"}\"}]}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "double-encoded _datadog must be stripped"); + assertTrue(json.contains("orderId")); + } + + @Test + void removesObjectCarrierWithWhitespaceBeforeColon() { + byte[] input = bytes("{\"_datadog\" : {\"trace\":\"1\"},\"other\":1}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "whitespace before colon must not block removal"); + assertTrue(json.contains("\"other\":1")); + } + + @Test + void doesNotRecurseBeyondMaxDepth() { + // The carrier is wrapped one level deeper than MAX_DEPTH allows. The final layer is never + // decoded, + // so no plain "_datadog" object key is ever seen. + int wraps = StripInjectedContext.MAX_DEPTH + 1; + String payload = "{\"orderId\":1,\"_datadog\":{\"trace\":\"1\"}}"; + for (int i = 0; i < wraps; i++) { + payload = "{\"detail\":" + jsonQuote(payload) + "}"; + } + + byte[] input = bytes(payload); + byte[] result = StripInjectedContext.stripInternal(input); + + assertArrayEquals(input, result, "carrier nested beyond MAX_DEPTH must be returned unchanged"); + } + + /** Encodes {@code raw} as a JSON string literal (adds surrounding quotes and escapes). */ + private static String jsonQuote(String raw) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < raw.length(); i++) { + char c = raw.charAt(i); + if (c == '"' || c == '\\') { + sb.append('\\'); + } + sb.append(c); + } + return sb.append('"').toString(); + } + + @Test + void stripsCarrierNestedExactlyAtMaxDepth() { + int wraps = StripInjectedContext.MAX_DEPTH; + String payload = "{\"orderId\":1,\"_datadog\":{\"trace\":\"1\"}}"; + for (int i = 0; i < wraps; i++) { + payload = "{\"detail\":" + jsonQuote(payload) + "}"; + } + + byte[] result = StripInjectedContext.stripInternal(bytes(payload)); + String json = toUtf8(result); + + assertTrue( + !json.contains("_datadog"), "carrier nested exactly at MAX_DEPTH must still be stripped"); + assertTrue(json.contains("orderId")); + } + + @Test + void preservesSurrogatePairEmojiInStringEncodedBusinessData() { + // Business text containing an astral-plane character encoded as a surrogate pair of unicode + // escapes (e.g. an emoji) must survive when it sits in the same string-encoded field as a + // genuine _datadog carrier that gets stripped. + byte[] input = + bytes( + "{\"Message\":\"{\\\"note\\\":\\\"Thanks \\uD83D\\uDE00\\\"," + + "\\\"_datadog\\\":{\\\"trace\\\":\\\"1\\\"}}\"}"); + + byte[] result = StripInjectedContext.stripInternal(input); + String json = toUtf8(result); + + assertTrue(!json.contains("_datadog"), "_datadog must be stripped"); + assertTrue( + json.contains("Thanks \\uD83D\\uDE00"), + "surrogate-pair emoji in business data must survive intact"); + } + + @Test + void doesNotThrowOnSignedHexInUnicodeEscape() { + byte[] input = bytes("{\"Message\":\"{\\\"n\\\":\\\"\\u-001\\\",\\\"_datadog\\\":{}}\"}"); + + String json = toUtf8(StripInjectedContext.stripInternal(input)); + + assertTrue(!json.contains("_datadog"), "_datadog must be stripped"); + } + + @Test + void preservesLoneSurrogateWhileStrippingCarrier() { + byte[] input = bytes("{\"Message\":\"{\\\"n\\\":\\\"x\\uD83D y\\\",\\\"_datadog\\\":{}}\"}"); + + String json = toUtf8(StripInjectedContext.stripInternal(input)); + + assertTrue(!json.contains("_datadog"), "_datadog must be stripped"); + assertTrue(json.contains("x\\uD83D y"), "a lone surrogate must not be turned into '?'"); + } + + @Test + void unclosedCarriersDoNotCauseQuadraticScan() { + StringBuilder body = new StringBuilder(); + for (int i = 0; i < 20_000; i++) { + body.append("{\\\"_datadog\\\":{"); + } + byte[] input = bytes("{\"Records\":[{\"body\":\"" + body + "\"}]}"); + + assertTimeoutPreemptively( + Duration.ofSeconds(1), + () -> assertArrayEquals(input, StripInjectedContext.stripInternal(input))); + } + + /** + * Keeps behavior aligned with the dd-trace-go implementation. The only intended difference is + * that whitespace before the colon does not prevent removal. + */ + static Stream sharedCasesWithGo() { + return Stream.of( + Arguments.of( + "no datadog key", "{\"detail\":{\"foo\":\"bar\"}}", "{\"detail\":{\"foo\":\"bar\"}}"), + Arguments.of("empty object", "{}", "{}"), + Arguments.of("empty input", "", ""), + Arguments.of("invalid JSON", "not even json", "not even json"), + Arguments.of( + "SQS event", + "{\"Records\":[{\"body\":\"hello\"}]}", + "{\"Records\":[{\"body\":\"hello\"}]}"), + Arguments.of( + "object detail alone", "{\"detail\":{\"_datadog\":{\"x\":\"y\"}}}", "{\"detail\":{}}"), + Arguments.of( + "object detail at beginning", + "{\"detail\":{\"_datadog\":{\"x\":\"y\"},\"foo\":\"bar\"}}", + "{\"detail\":{\"foo\":\"bar\"}}"), + Arguments.of( + "object detail at end", + "{\"detail\":{\"foo\":\"bar\",\"_datadog\":{\"x\":\"y\"}}}", + "{\"detail\":{\"foo\":\"bar\"}}"), + Arguments.of( + "object detail in middle", + "{\"detail\":{\"foo\":\"bar\",\"_datadog\":{\"x\":\"y\"},\"baz\":\"qux\"}}", + "{\"detail\":{\"foo\":\"bar\",\"baz\":\"qux\"}}"), + Arguments.of("adjacent carriers", "{\"_datadog\":{},\"_datadog\":{}}", "{}"), + Arguments.of( + "value with comma and brace", + "{\"foo\":\"a,}b\",\"_datadog\":{\"x\":1}}", + "{\"foo\":\"a,}b\"}"), + Arguments.of("_datadog value null", "{\"_datadog\":null}", "{\"_datadog\":null}"), + Arguments.of("_datadog value bool", "{\"_datadog\":true}", "{\"_datadog\":true}"), + Arguments.of("_datadog value number", "{\"_datadog\":42}", "{\"_datadog\":42}"), + Arguments.of("_datadog value string", "{\"_datadog\":\"str\"}", "{\"_datadog\":\"str\"}"), + Arguments.of("_datadog value array", "{\"_datadog\":[1,2,3]}", "{\"_datadog\":[1,2,3]}"), + Arguments.of( + "similar key prefix", "{\"x_datadog\":{\"a\":\"b\"}}", "{\"x_datadog\":{\"a\":\"b\"}}"), + Arguments.of( + "similar key suffix", "{\"_datadogx\":{\"a\":\"b\"}}", "{\"_datadogx\":{\"a\":\"b\"}}"), + Arguments.of("whitespace before colon", "{\"_datadog\" :{\"x\":\"y\"}}", "{}"), + Arguments.of( + "even backslashes in value", + "{\"foo\":\"bar\\\\\\\\\",\"_datadog\":{\"x\":\"y\"}}", + "{\"foo\":\"bar\\\\\\\\\"}"), + Arguments.of( + "odd backslashes in carrier value", "{\"_datadog\":{\"x\":\"val\\\\\\\"\"}}", "{}"), + Arguments.of( + "string detail", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\",\\\"_datadog\\\":{\\\"k\\\":\\\"v\\\"}}\"}", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\"}\"}"), + Arguments.of( + "string detail with brace in carrier", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\",\\\"_datadog\\\":{\\\"k\\\":\\\"v}z\\\"}}\"}", + "{\"detail\":\"{\\\"foo\\\":\\\"bar\\\"}\"}")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("sharedCasesWithGo") + void matchesExpectedOutput(String name, String input, String expected) { + assertEquals(expected, toUtf8(StripInjectedContext.stripInternal(bytes(input)))); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index bf5bab30d05..b1971cbd1db 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -342,6 +342,8 @@ public final class ConfigDefaults { static final boolean DEFAULT_WEBSOCKET_MESSAGES_SEPARATE_TRACES = true; static final boolean DEFAULT_WEBSOCKET_TAG_SESSION_ID = false; + static final boolean DEFAULT_LAMBDA_STRIP_INJECTED_CONTEXT = false; + static final Set DEFAULT_TRACE_CLOUD_PAYLOAD_TAGGING_SERVICES = new HashSet<>( Arrays.asList( diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index a32f997cebe..b3f05eba9be 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -140,5 +140,9 @@ public final class GeneralConfig { public static final String INSTRUMENTATION_SOURCE = "instrumentation.source"; public static final String APP_LOGS_COLLECTION_ENABLED = "app.logs.collection.enabled"; + public static final String LAMBDA_STRIP_INJECTED_CONTEXT = "lambda.strip.injected.context"; + public static final String TRACE_LAMBDA_STRIP_INJECTED_CONTEXT = + "trace.lambda.strip.injected.context"; + private GeneralConfig() {} } diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 3471750c347..377b5c12359 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -118,6 +118,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_JMX_FETCH_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_JMX_FETCH_MULTIPLE_RUNTIME_SERVICES_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_JMX_FETCH_MULTIPLE_RUNTIME_SERVICES_LIMIT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_LAMBDA_STRIP_INJECTED_CONTEXT; import static datadog.trace.api.ConfigDefaults.DEFAULT_LLM_OBS_AGENTLESS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_LLM_OBS_SAMPLE_RATE; import static datadog.trace.api.ConfigDefaults.DEFAULT_LOGS_INJECTION_ENABLED; @@ -406,6 +407,7 @@ import static datadog.trace.api.config.GeneralConfig.HEALTH_METRICS_STATSD_PORT; import static datadog.trace.api.config.GeneralConfig.INSTRUMENTATION_SOURCE; import static datadog.trace.api.config.GeneralConfig.JDK_SOCKET_ENABLED; +import static datadog.trace.api.config.GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT; import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL; import static datadog.trace.api.config.GeneralConfig.PERF_METRICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.PRIMARY_TAG; @@ -438,6 +440,7 @@ import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_MAX_AGGREGATES; import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_MAX_PENDING; import static datadog.trace.api.config.GeneralConfig.TRACE_DEBUG; +import static datadog.trace.api.config.GeneralConfig.TRACE_LAMBDA_STRIP_INJECTED_CONTEXT; import static datadog.trace.api.config.GeneralConfig.TRACE_LOG_LEVEL; import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.TRACE_STATS_ADDITIONAL_TAGS; @@ -1415,6 +1418,7 @@ public static String getHostName() { private final String dogStatsDPath; private final List dogStatsDArgs; private final int dogStatsDPort; + private final boolean lambdaStripInjectedContextEnabled; private String env; private String version; @@ -1777,6 +1781,12 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins sqsInjectDatadogAttributeEnabled = isInjectDatadogAttributeEnabled(DEFAULT_INJECT_DATADOG_ATTRIBUTE, "sqs"); + lambdaStripInjectedContextEnabled = + configProvider.getBoolean( + LAMBDA_STRIP_INJECTED_CONTEXT, + DEFAULT_LAMBDA_STRIP_INJECTED_CONTEXT, + TRACE_LAMBDA_STRIP_INJECTED_CONTEXT); + spanAttributeSchemaVersion = schemaVersionFromConfig(); peerHostNameEnabled = configProvider.getBoolean(TRACE_PEER_HOSTNAME_ENABLED, true); @@ -5462,6 +5472,10 @@ public boolean isSfnInjectDatadogAttributeEnabled() { return sfnInjectDatadogAttributeEnabled; } + public boolean isLambdaStripInjectedContextEnabled() { + return lambdaStripInjectedContextEnabled; + } + /** * @return A map of tags to be applied only to the local application root span. */ @@ -7209,6 +7223,8 @@ public String toString() { + sqsInjectDatadogAttributeEnabled + ", snsInjectDatadogAttributeEnabled=" + snsInjectDatadogAttributeEnabled + + ", lambdaStripInjectedContextEnabled=" + + lambdaStripInjectedContextEnabled + '}'; } } diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java index bbea22a73fa..0e8d255ca45 100644 --- a/internal-api/src/test/java/datadog/trace/api/ConfigTest.java +++ b/internal-api/src/test/java/datadog/trace/api/ConfigTest.java @@ -54,6 +54,7 @@ import static datadog.trace.api.config.GeneralConfig.HEALTH_METRICS_STATSD_PORT; import static datadog.trace.api.config.GeneralConfig.INSTRUMENTATION_SOURCE; import static datadog.trace.api.config.GeneralConfig.JDK_SOCKET_ENABLED; +import static datadog.trace.api.config.GeneralConfig.LAMBDA_STRIP_INJECTED_CONTEXT; import static datadog.trace.api.config.GeneralConfig.PERF_METRICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; import static datadog.trace.api.config.GeneralConfig.SITE; @@ -61,6 +62,7 @@ import static datadog.trace.api.config.GeneralConfig.SSI_INJECTION_FORCE; import static datadog.trace.api.config.GeneralConfig.TAGS; import static datadog.trace.api.config.GeneralConfig.TRACER_METRICS_IGNORED_RESOURCES; +import static datadog.trace.api.config.GeneralConfig.TRACE_LAMBDA_STRIP_INJECTED_CONTEXT; import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.VERSION; import static datadog.trace.api.config.JmxFetchConfig.JMX_FETCH_CHECK_PERIOD; @@ -3010,6 +3012,33 @@ void configInstantiationShouldNotFailIfLlmObsIsEnabledViaEnvVarAndMlAppIsEmpty() assertEquals("test-service", config.getLlmObsMlApp()); } + @Test + void lambdaStripInjectedContextIsDisabledByDefault() throws Exception { + Config config = newConfig(); + + assertFalse(config.isLambdaStripInjectedContextEnabled()); + } + + @Test + void lambdaStripInjectedContextCanBeEnabled() throws Exception { + Properties properties = new Properties(); + properties.setProperty(LAMBDA_STRIP_INJECTED_CONTEXT, "true"); + + Config config = newConfig(withPropertiesOverride(properties)); + + assertTrue(config.isLambdaStripInjectedContextEnabled()); + } + + @Test + void lambdaStripInjectedContextCanBeEnabledWithTraceAlias() throws Exception { + Properties properties = new Properties(); + properties.setProperty(TRACE_LAMBDA_STRIP_INJECTED_CONTEXT, "true"); + + Config config = newConfig(withPropertiesOverride(properties)); + + assertTrue(config.isLambdaStripInjectedContextEnabled()); + } + @Test void configInstantiationShouldNotFailIfLlmObsIsEnabledAgentlessDisabledViaSysPropAndMlAppIsSet() throws Exception { diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 45e12e44c85..766f951d374 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2356,6 +2356,14 @@ "aliases": [] } ], + "DD_LAMBDA_STRIP_INJECTED_CONTEXT": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_TRACE_LAMBDA_STRIP_INJECTED_CONTEXT"] + } + ], "DD_LLMOBS_AGENTLESS_ENABLED": [ { "version": "B",