diff --git a/dd-java-agent/instrumentation/aws-java-s3-2.0/build.gradle b/dd-java-agent/instrumentation/aws-java-s3-2.0/build.gradle new file mode 100644 index 00000000000..21fa4b5818c --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-s3-2.0/build.gradle @@ -0,0 +1,29 @@ +muzzle { + pass { + group = "software.amazon.awssdk" + module = "s3" + versions = "[2,3)" + assertInverse = true + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir('latestDepTest', 'test') +addTestSuiteExtendingForDir('latestDepForkedTest', 'latestDepTest', 'test') + +dependencies { + compileOnly group: 'software.amazon.awssdk', name: 's3', version: '2.29.26' + + // Include httpclient instrumentation for testing because it is a dependency for aws-sdk. + testRuntimeOnly project(':dd-java-agent:instrumentation:apache-httpclient-4') + testRuntimeOnly project(':dd-java-agent:instrumentation:aws-java-sdk-2.2') + testImplementation 'software.amazon.awssdk:s3:2.29.26' + testImplementation 'org.testcontainers:localstack:1.20.1' + + latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: '+' +} + +tasks.withType(Test).configureEach { + usesService(testcontainersLimit) +} diff --git a/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3ClientInstrumentation.java b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3ClientInstrumentation.java new file mode 100644 index 00000000000..6f28c9aef98 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3ClientInstrumentation.java @@ -0,0 +1,48 @@ +package datadog.trace.instrumentation.aws.v2.s3; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.List; +import net.bytebuddy.asm.Advice; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; + +@AutoService(InstrumenterModule.class) +public final class S3ClientInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + public S3ClientInstrumentation() { + super("s3", "aws-s3"); + } + + @Override + public String instrumentedType() { + return "software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("resolveExecutionInterceptors")), + S3ClientInstrumentation.class.getName() + "$AwsS3BuilderAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".S3Interceptor", packageName + ".TextMapInjectAdapter"}; + } + + public static class AwsS3BuilderAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addHandler(@Advice.Return final List interceptors) { + for (ExecutionInterceptor interceptor : interceptors) { + if (interceptor instanceof S3Interceptor) { + return; // list already has our interceptor, return to builder + } + } + interceptors.add(new S3Interceptor()); + } + } +} diff --git a/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java new file mode 100644 index 00000000000..bdaf1ba6559 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java @@ -0,0 +1,59 @@ +package datadog.trace.instrumentation.aws.v2.s3; + +import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.S3_ETAG; + +import datadog.trace.api.Config; +import datadog.trace.bootstrap.InstanceStore; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CopyObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +public class S3Interceptor implements ExecutionInterceptor { + private static final Logger log = LoggerFactory.getLogger(S3Interceptor.class); + + public static final ExecutionAttribute SPAN_ATTRIBUTE = + InstanceStore.of(ExecutionAttribute.class) + .putIfAbsent("DatadogSpan", () -> new ExecutionAttribute<>("DatadogSpan")); + + private static final boolean CAN_ADD_SPAN_POINTERS = Config.get().isAddSpanPointers("aws"); + + @Override + public void afterExecution( + Context.AfterExecution context, ExecutionAttributes executionAttributes) { + if (!CAN_ADD_SPAN_POINTERS) { + return; + } + + AgentSpan span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); + if (span == null) { + log.debug("Unable to find S3 request span. Not creating span pointer."); + return; + } + String eTag; + Object response = context.response(); + + // Get eTag for hash calculation. + // https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/S3Client.html + if (response instanceof PutObjectResponse) { + eTag = ((PutObjectResponse) response).eTag(); + } else if (response instanceof CopyObjectResponse) { + eTag = ((CopyObjectResponse) response).copyObjectResult().eTag(); + } else if (response instanceof CompleteMultipartUploadResponse) { + eTag = ((CompleteMultipartUploadResponse) response).eTag(); + } else { + return; + } + + // Store eTag as tag, then calculate hash + add span pointers in SpanPointersProcessor. + // Bucket and key are already stored as tags in AwsSdkClientDecorator, so need to make redundant + // tags. + span.setTag(S3_ETAG, eTag); + } +} diff --git a/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/TextMapInjectAdapter.java b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/TextMapInjectAdapter.java new file mode 100644 index 00000000000..e3ccf485543 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/TextMapInjectAdapter.java @@ -0,0 +1,13 @@ +package datadog.trace.instrumentation.aws.v2.s3; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; + +public class TextMapInjectAdapter implements AgentPropagation.Setter { + + public static final TextMapInjectAdapter SETTER = new TextMapInjectAdapter(); + + @Override + public void set(final StringBuilder builder, final String key, final String value) { + builder.append('"').append(key).append("\":\"").append(value).append("\","); + } +} diff --git a/dd-java-agent/instrumentation/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy new file mode 100644 index 00000000000..fe9decb6f11 --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy @@ -0,0 +1,385 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes +import datadog.trace.core.tagprocessor.SpanPointersProcessor +import groovy.json.JsonSlurper +import org.testcontainers.containers.GenericContainer +import org.testcontainers.utility.DockerImageName +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.* +import software.amazon.awssdk.services.s3.S3Configuration +import spock.lang.Shared + +import java.time.Duration + +class S3ClientTest extends AgentTestRunner { + static final LOCALSTACK = new GenericContainer(DockerImageName.parse("localstack/localstack")) + .withExposedPorts(4566) + .withEnv("SERVICES", "s3") + .withReuse(true) + .withStartupTimeout(Duration.ofSeconds(120)) + + @Shared + S3Client s3Client + + @Shared + String bucketName + + def setupSpec() { + LOCALSTACK.start() + def endPoint = "http://" + LOCALSTACK.getHost() + ":" + LOCALSTACK.getMappedPort(4566) + + s3Client = S3Client.builder() + .endpointOverride(URI.create(endPoint)) + .region(Region.of("us-east-1")) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("test", "test"))) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(true) + .checksumValidationEnabled(false) + .build()) + .build() + + // Create a test bucket + bucketName = "s3-bucket-name-test" + s3Client.createBucket { it.bucket(bucketName) } + } + + def cleanupSpec() { + LOCALSTACK.stop() + } + + def "should add span pointer for putObject operation"() { + when: + TEST_WRITER.clear() + String key = "test-key" + String content = "test body" + + s3Client.putObject( + PutObjectRequest.builder().bucket(bucketName).key(key).build(), + RequestBody.fromString(content) + ) + + then: + assertTraces(1) { + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.PutObject" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "PutObject" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", key + tag "bucketname", bucketName + tag "http.method", "PUT" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$key") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "_dd.span_links", { it != null } + + // Assert the span links + def spanLinks = tags["_dd.span_links"] + assert spanLinks != null + def links = new JsonSlurper().parseText(spanLinks) + assert links.size() == 1 + def link = links[0] + assert link["attributes"] != null + assert link["attributes"]["ptr.kind"] == SpanPointersProcessor.S3_PTR_KIND + assert link["attributes"]["ptr.dir"] == SpanPointersProcessor.DOWN_DIRECTION + assert link["attributes"]["ptr.hash"] == "6d1a2fe194c6579187408f827f942be3" + assert link["attributes"]["link.kind"] == SpanPointersProcessor.LINK_KIND + } + } + } + } + } + + def "should add span pointer for copyObject operation"() { + when: + TEST_WRITER.clear() + String sourceKey = "test-key" + String destKey = "new-key" + String content = "test body" + + s3Client.putObject( + PutObjectRequest.builder().bucket(bucketName).key(sourceKey).build(), + RequestBody.fromString(content) + ) + s3Client.copyObject( + CopyObjectRequest.builder() + .sourceBucket(bucketName) + .destinationBucket(bucketName) + .sourceKey(sourceKey) + .destinationKey(destKey) + .build() + ) + + then: + assertTraces(2) { + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.PutObject" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "PutObject" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", sourceKey + tag "bucketname", bucketName + tag "http.method", "PUT" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$sourceKey") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "_dd.span_links", { it != null } + } + } + } + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.CopyObject" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "CopyObject" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", destKey + tag "bucketname", bucketName + tag "http.method", "PUT" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$destKey") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "_dd.span_links", { it != null } + + // Assert the span links + def spanLinks = tags["_dd.span_links"] + assert spanLinks != null + def links = new JsonSlurper().parseText(spanLinks) + assert links.size() == 1 + def link = links[0] + assert link["attributes"] != null + assert link["attributes"]["ptr.kind"] == SpanPointersProcessor.S3_PTR_KIND + assert link["attributes"]["ptr.dir"] == SpanPointersProcessor.DOWN_DIRECTION + assert link["attributes"]["ptr.hash"] == "1542053ce6d393c424b1374bac1fc0c5" + assert link["attributes"]["link.kind"] == SpanPointersProcessor.LINK_KIND + } + } + } + } + } + + def "should add span pointer for completeMultipartUpload operation"() { + when: + TEST_WRITER.clear() + String key = "multipart-test" + + // Initiate multipart upload + def createMultipartUploadResponse = s3Client.createMultipartUpload( + CreateMultipartUploadRequest.builder().bucket(bucketName).key(key).build() + ) + String uploadId = createMultipartUploadResponse.uploadId() + + // Create parts (5MB each) + int partSize = 5 * 1024 * 1024 + byte[] part1Data = new byte[partSize] + Arrays.fill(part1Data, (byte) 'a') + byte[] part2Data = new byte[partSize] + Arrays.fill(part2Data, (byte) 'b') + + // Upload parts + List completedParts = [] + s3Client.uploadPart( + UploadPartRequest.builder() + .bucket(bucketName) + .key(key) + .uploadId(uploadId) + .partNumber(1) + .build(), + RequestBody.fromBytes(part1Data) + ).with { response -> + completedParts.add(CompletedPart.builder() + .partNumber(1) + .eTag(response.eTag()) + .build()) + } + s3Client.uploadPart( + UploadPartRequest.builder() + .bucket(bucketName) + .key(key) + .uploadId(uploadId) + .partNumber(2) + .build(), + RequestBody.fromBytes(part2Data) + ).with { response -> + completedParts.add(CompletedPart.builder() + .partNumber(2) + .eTag(response.eTag()) + .build()) + } + + // Complete multipart upload + s3Client.completeMultipartUpload( + CompleteMultipartUploadRequest.builder() + .bucket(bucketName) + .key(key) + .uploadId(uploadId) + .multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build()) + .build() + ) + + then: + // Check that spans were created and that the CompleteMultipartUpload span has the expected span pointer + assertTraces(4) { + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.CreateMultipartUpload" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "CreateMultipartUpload" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", key + tag "bucketname", bucketName + tag "http.method", "POST" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$key") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "http.query.string", "uploads" + } + } + } + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.UploadPart" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "UploadPart" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", key + tag "bucketname", bucketName + tag "http.method", "PUT" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$key") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "http.query.string", { it != null } + } + } + } + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.UploadPart" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "UploadPart" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", key + tag "bucketname", bucketName + tag "http.method", "PUT" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$key") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "http.query.string", { it != null } + } + } + } + trace(1) { + span { + serviceName "java-aws-sdk" + operationName "aws.http" + resourceName "S3.CompleteMultipartUpload" + spanType DDSpanTypes.HTTP_CLIENT + tags { + defaultTags() + tag "component", "java-aws-sdk" + tag "aws.operation", "CompleteMultipartUpload" + tag "aws.service", "S3" + tag "aws_service", "S3" + tag "aws.agent", "java-aws-sdk" + tag "aws.bucket.name", bucketName + tag "aws.object.key", key + tag "bucketname", bucketName + tag "http.method", "POST" + tag "http.status_code", 200 + tag "http.url", { it.startsWith("http://localhost") && it.contains("/$key") } + tag "peer.hostname", "localhost" + tag "peer.port", { it instanceof Integer } + tag "span.kind", "client" + tag "aws.requestId", { it != null } + tag "http.query.string", { it != null } + tag "_dd.span_links", { it != null } + + // Assert the span links + def spanLinks = tags["_dd.span_links"] + assert spanLinks != null + def links = new JsonSlurper().parseText(spanLinks) + assert links.size() == 1 + def link = links[0] + assert link["attributes"] != null + assert link["attributes"]["ptr.kind"] == SpanPointersProcessor.S3_PTR_KIND + assert link["attributes"]["ptr.dir"] == SpanPointersProcessor.DOWN_DIRECTION + assert link["attributes"]["ptr.hash"] == "422412aa6b472a7194f3e24f4b12b4a6" + assert link["attributes"]["link.kind"] == SpanPointersProcessor.LINK_KIND + } + } + } + } + } +} 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 8bbdc38f272..4eb414f3394 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 @@ -248,6 +248,7 @@ public final class ConfigDefaults { static final boolean DEFAULT_ELASTICSEARCH_BODY_ENABLED = false; static final boolean DEFAULT_ELASTICSEARCH_PARAMS_ENABLED = true; static final boolean DEFAULT_ELASTICSEARCH_BODY_AND_PARAMS_ENABLED = false; + static final boolean DEFAULT_ADD_SPAN_POINTERS = true; static final boolean DEFAULT_SPARK_TASK_HISTOGRAM_ENABLED = true; static final boolean DEFAULT_SPARK_APP_NAME_AS_SERVICE = false; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java index 6535e1c464a..b124e03131f 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java @@ -167,6 +167,7 @@ public final class TraceInstrumentationConfig { public static final String AXIS_PROMOTE_RESOURCE_NAME = "trace.axis.promote.resource-name"; public static final String SQS_BODY_PROPAGATION_ENABLED = "trace.sqs.body.propagation.enabled"; + public static final String ADD_SPAN_POINTERS = "add.span.pointers"; private TraceInstrumentationConfig() {} } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index d8092874bb9..fcbf046122a 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -848,7 +848,8 @@ public void processTagsAndBaggage( final MetadataConsumer consumer, int longRunningVersion, List links) { synchronized (unsafeTags) { // Tags - Map tags = TagsPostProcessorFactory.instance().processTags(unsafeTags, this); + Map tags = + TagsPostProcessorFactory.instance().processTags(unsafeTags, this, links); String linksTag = DDSpanLink.toTag(links); if (linksTag != null) { tags.put(SPAN_LINKS, linksTag); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java index 9184213f369..c855262f048 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/BaseServiceAdder.java @@ -1,8 +1,10 @@ package datadog.trace.core.tagprocessor; import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.DDSpanContext; +import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -15,7 +17,7 @@ public BaseServiceAdder(@Nullable final String ddService) { @Override public Map processTags( - Map unsafeTags, DDSpanContext spanContext) { + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { if (ddService != null && spanContext != null && !ddService.toString().equalsIgnoreCase(spanContext.getServiceName())) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java index 22a0752a413..d50c8510295 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PayloadTagsProcessor.java @@ -3,6 +3,7 @@ import datadog.trace.api.Config; import datadog.trace.api.ConfigDefaults; import datadog.trace.api.telemetry.LogCollector; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; import datadog.trace.payloadtags.PayloadTagsData; import datadog.trace.payloadtags.json.JsonPath; @@ -68,7 +69,8 @@ public static PayloadTagsProcessor create(Config config) { } @Override - public Map processTags(Map spanTags, DDSpanContext spanContext) { + public Map processTags( + Map spanTags, DDSpanContext spanContext, List spanLinks) { int spanMaxTags = maxTags + spanTags.size(); for (Map.Entry tagPrefixRedactionRules : redactionRulesByTagPrefix.entrySet()) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java index 9167bde7e10..625fae0f839 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PeerServiceCalculator.java @@ -4,8 +4,10 @@ import datadog.trace.api.DDTags; import datadog.trace.api.naming.NamingSchema; import datadog.trace.api.naming.SpanNaming; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.core.DDSpanContext; +import java.util.List; import java.util.Map; import javax.annotation.Nonnull; @@ -31,7 +33,7 @@ public PeerServiceCalculator() { @Override public Map processTags( - Map unsafeTags, DDSpanContext spanContext) { + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { Object peerService = unsafeTags.get(Tags.PEER_SERVICE); // the user set it if (peerService != null) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java index 32156b50356..fbf5b511e24 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/PostProcessorChain.java @@ -1,6 +1,8 @@ package datadog.trace.core.tagprocessor; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; +import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; @@ -14,10 +16,10 @@ public PostProcessorChain(@Nonnull final TagsPostProcessor... processors) { @Override public Map processTags( - Map unsafeTags, DDSpanContext spanContext) { + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { Map currentTags = unsafeTags; for (final TagsPostProcessor tagsPostProcessor : chain) { - currentTags = tagsPostProcessor.processTags(currentTags, spanContext); + currentTags = tagsPostProcessor.processTags(currentTags, spanContext, spanLinks); } return currentTags; } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java index 1af11529398..934aa5df1f0 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/QueryObfuscator.java @@ -4,9 +4,11 @@ import com.google.re2j.Pattern; import com.google.re2j.PatternSyntaxException; import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.core.DDSpanContext; import datadog.trace.util.Strings; +import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +59,7 @@ private String obfuscate(String query) { @Override public Map processTags( - Map unsafeTags, DDSpanContext spanContext) { + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { Object query = unsafeTags.get(DDTags.HTTP_QUERY); if (query instanceof CharSequence) { query = obfuscate(query.toString()); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java index 0cf80cd18c0..b872b824e38 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/RemoteHostnameAdder.java @@ -1,7 +1,9 @@ package datadog.trace.core.tagprocessor; import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -14,7 +16,7 @@ public RemoteHostnameAdder(Supplier hostnameSupplier) { @Override public Map processTags( - Map unsafeTags, DDSpanContext spanContext) { + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { if (spanContext.getSpanId() == spanContext.getRootSpanId()) { unsafeTags.put(DDTags.TRACER_HOST, hostnameSupplier.get()); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java new file mode 100644 index 00000000000..685ad55fa89 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java @@ -0,0 +1,101 @@ +package datadog.trace.core.tagprocessor; + +import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.AWS_BUCKET_NAME; +import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.AWS_OBJECT_KEY; +import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.S3_ETAG; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; +import datadog.trace.bootstrap.instrumentation.api.SpanLink; +import datadog.trace.core.DDSpanContext; +import datadog.trace.util.Strings; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SpanPointersProcessor implements TagsPostProcessor { + private static final Logger LOG = LoggerFactory.getLogger(SpanPointersProcessor.class); + + // The pointer direction will always be down. The serverless agent handles cases where the + // direction is up. + static final String DOWN_DIRECTION = "d"; + static final String S3_PTR_KIND = "aws.s3.object"; + static final String LINK_KIND = "span-pointer"; + + @Override + public Map processTags( + Map unsafeTags, DDSpanContext spanContext, List spanLinks) { + String eTag = asString(unsafeTags.remove(S3_ETAG)); + if (eTag == null) { + return unsafeTags; + } + String bucket = asString(unsafeTags.get(AWS_BUCKET_NAME)); + String key = asString(unsafeTags.get(AWS_OBJECT_KEY)); + if (bucket == null || key == null) { + LOG.debug("Unable to calculate span pointer hash because could not find bucket or key tags."); + return unsafeTags; + } + + // Hash calculation rules: + // https://github.com/DataDog/dd-span-pointer-rules/blob/main/AWS/S3/Object/README.md + if (!eTag.isEmpty() && eTag.charAt(0) == '"' && eTag.charAt(eTag.length() - 1) == '"') { + eTag = eTag.substring(1, eTag.length() - 1); + } + String[] components = new String[] {bucket, key, eTag}; + try { + SpanAttributes attributes = + SpanAttributes.builder() + .put("ptr.kind", S3_PTR_KIND) + .put("ptr.dir", DOWN_DIRECTION) + .put("ptr.hash", generatePointerHash(components)) + .put("link.kind", LINK_KIND) + .build(); + + AgentTracer.NoopContext zeroContext = AgentTracer.NoopContext.INSTANCE; + AgentSpanLink link = SpanLink.from(zeroContext, AgentSpanLink.DEFAULT_FLAGS, "", attributes); + spanLinks.add(link); + } catch (Exception e) { + LOG.debug("Failed to add span pointer: {}", e.getMessage()); + } + + return unsafeTags; + } + + private static String asString(Object o) { + return o == null ? null : o.toString(); + } + + /** + * Generates a unique hash from an array of strings by joining them with | before hashing. Used to + * uniquely identify AWS requests for span pointers. + * + * @param components Array of strings to hash + * @return A 32-character hash uniquely identifying the components + * @throws NoSuchAlgorithmException this should never happen; but should be handled just in case. + */ + private static String generatePointerHash(String[] components) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + + // Update the digest incrementally for each component. + boolean first = true; + for (String component : components) { + if (!first) { + messageDigest.update((byte) '|'); + } else { + first = false; + } + messageDigest.update(component.getBytes(StandardCharsets.UTF_8)); + } + + byte[] fullHash = messageDigest.digest(); + // Only take first 16 bytes of the hash and convert to hex + byte[] truncatedHash = Arrays.copyOf(fullHash, 16); + return Strings.toHexString(truncatedHash); + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java index 423d3654074..f188e10d090 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessor.java @@ -1,8 +1,11 @@ package datadog.trace.core.tagprocessor; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpanContext; +import java.util.List; import java.util.Map; public interface TagsPostProcessor { - Map processTags(Map unsafeTags, DDSpanContext spanContext); + Map processTags( + Map unsafeTags, DDSpanContext spanContext, List spanLinks); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java index 33fe68dfe4b..154d7f54153 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java @@ -25,6 +25,13 @@ private static TagsPostProcessor create() { processors.add(ptp); } } + // today we only have aws as config key however we could have span pointers for different + // integrations. + // At that moment we should run the postprocessor for all the spans (and filter by component + // to skip non-interesting ones) + if (Config.get().isAddSpanPointers("aws")) { + processors.add(new SpanPointersProcessor()); + } return new PostProcessorChain( processors.toArray(processors.toArray(new TagsPostProcessor[0]))); } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/BaseServiceAdderTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/BaseServiceAdderTest.groovy index 22646e0fe98..fbab796b7c8 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/BaseServiceAdderTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/BaseServiceAdderTest.groovy @@ -12,7 +12,7 @@ class BaseServiceAdderTest extends DDSpecification { def spanContext = Mock(DDSpanContext) when: - def enrichedTags = calculator.processTags([:], spanContext) + def enrichedTags = calculator.processTags([:], spanContext, []) then: 1 * spanContext.getServiceName() >> serviceName diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy index 3e144b38f5d..40ef22a3f4e 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PayloadTagsProcessorTest.groovy @@ -82,7 +82,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ] expect: - ptp.processTags(spanTags, null) == ["foo": "bar", "tag1": 1] + ptp.processTags(spanTags, null, []) == ["foo": "bar", "tag1": 1] } static PathAndValue pv(PathCursor path, Object value) { @@ -107,7 +107,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ] expect: - ptp.processTags(spanTags, null) == ["foo": "bar", "tag1": 1, "payload.tag1": 0] + ptp.processTags(spanTags, null, []) == ["foo": "bar", "tag1": 1, "payload.tag1": 0] } def "expand preserving tag types"() { @@ -124,7 +124,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "payload.tag1": 11, "payload.tag2.Value": 2342l, "payload.tag3.0": 3.14d, @@ -142,7 +142,7 @@ class PayloadTagsProcessorTest extends DDSpecification { def st = spanTags("payload", [pv(pc().push("tag7"), unknownValue),]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "payload.tag7": unknownValue.toString(), ] } @@ -159,7 +159,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.j3.0": "1", "p.j3.1": 2, "p.j3.2": 3.14d, @@ -204,7 +204,7 @@ class PayloadTagsProcessorTest extends DDSpecification { def st = spanTags("dd", [pv(pc(), json),]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ 'dd.a' : 33, 'dd.Message.id' : 45, 'dd.Message.user.a' : 1.15d, @@ -220,7 +220,7 @@ class PayloadTagsProcessorTest extends DDSpecification { def st = spanTags("p", [pv(pc().push("key"), invalidJson),]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.key": invalidJson, ] @@ -246,7 +246,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.j1.foo": "bar", "p.j2.0": 1, "p.j2.1": true, @@ -261,7 +261,7 @@ class PayloadTagsProcessorTest extends DDSpecification { def st = spanTags("p", [pv(pc().push("v"), new ByteArrayInputStream("""{ "inner": $innerJson}""".bytes))]) then: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.v.inner.a": 1.15d, "p.v.inner.password": "my-secret-password", ] @@ -283,7 +283,7 @@ class PayloadTagsProcessorTest extends DDSpecification { def st = spanTags("p", [pv(pc().push("key"), new ByteArrayInputStream(invalidJson.bytes)),]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.key": "", ] @@ -303,7 +303,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.j3.0": "redacted", "p.j3.1": 2, "p.j3.2": 3.14d, @@ -326,7 +326,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.j3.0": "redacted", "p.j3.1": 2, "p.j3.2": 3.14d, @@ -345,7 +345,7 @@ class PayloadTagsProcessorTest extends DDSpecification { ]) expect: - ptp.processTags(st, null) == [ + ptp.processTags(st, null, []) == [ "p.j3.0": "redacted", "p.j3.1": 2, "p.j3.2": 3.14d, diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy index 4c104a93070..bc8281835bb 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PeerServiceCalculatorTest.groovy @@ -13,7 +13,7 @@ class PeerServiceCalculatorTest extends DDSpecification { setup: def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()) when: - def enrichedTags = calculator.processTags(tags, null) + def enrichedTags = calculator.processTags(tags, null, []) then: // tags are not modified assert enrichedTags == tags @@ -33,7 +33,7 @@ class PeerServiceCalculatorTest extends DDSpecification { def calculator = new PeerServiceCalculator(new NamingSchemaV1().peerService(), Collections.emptyMap()) when: tags.put(Tags.SPAN_KIND, Tags.SPAN_KIND_CLIENT) - def calculated = calculator.processTags(tags, null) + def calculated = calculator.processTags(tags, null, []) then: calculated.get(DDTags.PEER_SERVICE_SOURCE) == provenance @@ -56,7 +56,7 @@ class PeerServiceCalculatorTest extends DDSpecification { injectSysConfig(TracerConfig.TRACE_PEER_SERVICE_DEFAULTS_ENABLED, "true") def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Collections.emptyMap()) when: - def calculated = calculator.processTags(["span.kind": "client", "peer.hostname": "test"], null) + def calculated = calculator.processTags(["span.kind": "client", "peer.hostname": "test"], null, []) then: assert calculated.get(Tags.PEER_SERVICE) == "test" } @@ -70,7 +70,7 @@ class PeerServiceCalculatorTest extends DDSpecification { def tags = ["span.kind": kind, "peer.hostname": "test"] then: - assert calculator.processTags(tags, null).containsKey(Tags.PEER_SERVICE) == calculate + assert calculator.processTags(tags, null, []).containsKey(Tags.PEER_SERVICE) == calculate where: kind | calculate @@ -87,7 +87,7 @@ class PeerServiceCalculatorTest extends DDSpecification { def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Config.get().getPeerServiceMapping()) when: - def calculated = calculator.processTags(tags, null) + def calculated = calculator.processTags(tags, null, []) then: assert calculated.get(Tags.PEER_SERVICE) == expected @@ -108,7 +108,7 @@ class PeerServiceCalculatorTest extends DDSpecification { def calculator = new PeerServiceCalculator(new NamingSchemaV0().peerService(), Config.get().getPeerServiceComponentOverrides()) when: - def calculated = calculator.processTags(tags, null) + def calculated = calculator.processTags(tags, null, []) then: assert calculated.get(Tags.PEER_SERVICE) == expected diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy index 772268ec201..2a1dc2583f3 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/PostProcessorChainTest.groovy @@ -1,5 +1,6 @@ package datadog.trace.core.tagprocessor +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink import datadog.trace.core.DDSpanContext import datadog.trace.test.util.DDSpecification @@ -8,7 +9,7 @@ class PostProcessorChainTest extends DDSpecification { setup: def processor1 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext) { + Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { unsafeTags.put("key1", "processor1") unsafeTags.put("key2", "processor1") return unsafeTags @@ -16,7 +17,7 @@ class PostProcessorChainTest extends DDSpecification { } def processor2 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext) { + Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { unsafeTags.put("key1", "processor2") return unsafeTags } @@ -26,7 +27,7 @@ class PostProcessorChainTest extends DDSpecification { def tags = ["key1": "root", "key3": "root"] when: - def out = chain.processTags(tags, null) + def out = chain.processTags(tags, null, []) then: assert out == ["key1": "processor2", "key2": "processor1", "key3": "root"] @@ -36,13 +37,13 @@ class PostProcessorChainTest extends DDSpecification { setup: def processor1 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext) { + Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { return ["my": "tag"] } } def processor2 = new TagsPostProcessor() { @Override - Map processTags(Map unsafeTags, DDSpanContext spanContext) { + Map processTags(Map unsafeTags, DDSpanContext spanContext, List spanLinks) { if (unsafeTags.containsKey("test")) { unsafeTags.put("found", "true") } @@ -54,7 +55,7 @@ class PostProcessorChainTest extends DDSpecification { def tags = ["test": "test"] when: - def out = chain.processTags(tags, null) + def out = chain.processTags(tags, null, []) then: assert out == ["my": "tag"] diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy index 11dbcbb7c76..655a1a720fb 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/QueryObfuscatorTest.groovy @@ -14,7 +14,7 @@ class QueryObfuscatorTest extends DDSpecification { ] when: - def result = obfuscator.processTags(tags, null) + def result = obfuscator.processTags(tags, null, []) then: assert result.get(DDTags.HTTP_QUERY) == expectedQuery @@ -36,7 +36,7 @@ class QueryObfuscatorTest extends DDSpecification { ] when: - def result = obfuscator.processTags(tags, null) + def result = obfuscator.processTags(tags, null, []) then: assert result.get(DDTags.HTTP_QUERY) == expectedQuery diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy new file mode 100644 index 00000000000..09bae762eff --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy @@ -0,0 +1,100 @@ +package datadog.trace.core.tagprocessor + +import datadog.trace.api.DDSpanId +import datadog.trace.api.DDTraceId +import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags +import datadog.trace.bootstrap.instrumentation.api.SpanLink +import datadog.trace.core.DDSpanContext +import datadog.trace.test.util.DDSpecification + +class SpanPointersProcessorTest extends DDSpecification{ + def "SpanPointersProcessor adds correct link with basic values"() { + given: + def processor = new SpanPointersProcessor() + def unsafeTags = [ + (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", + (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.data", + "s3.eTag" : "ab12ef34" + ] + def spanContext = Mock(DDSpanContext) + def spanLinks = [] + def expectedHash = "e721375466d4116ab551213fdea08413" + + when: + // Process the tags; the processor should remove 's3.eTag' and add one link + def returnedTags = processor.processTags(unsafeTags, spanContext, spanLinks) + + then: + // 1. s3.eTag was removed + !returnedTags.containsKey("s3.eTag") + // 2. Exactly one link was added + spanLinks.size() == 1 + // 3. Check link + def link = spanLinks[0] + link instanceof SpanLink + link.traceId() == DDTraceId.ZERO + link.spanId() == DDSpanId.ZERO + link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND + link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION + link.attributes.asMap().get("ptr.hash") == expectedHash + link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND + } + + def "SpanPointersProcessor adds correct link with non-ascii key"() { + given: + def processor = new SpanPointersProcessor() + def unsafeTags = [ + (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", + (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.你好", + "s3.eTag" : "ab12ef34" + ] + def spanContext = Mock(DDSpanContext) + def spanLinks = [] + + // From the original test, expected hash for these components + def expectedHash = "d1333a04b9928ab462b5c6cadfa401f4" + + when: + def returnedTags = processor.processTags(unsafeTags, spanContext, spanLinks) + + then: + !returnedTags.containsKey("s3.eTag") + spanLinks.size() == 1 + def link = spanLinks[0] + link.traceId() == DDTraceId.ZERO + link.spanId() == DDSpanId.ZERO + link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND + link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION + link.attributes.asMap().get("ptr.hash") == expectedHash + link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND + } + + def "SpanPointersProcessor adds correct link with multipart-upload ETag"() { + given: + def processor = new SpanPointersProcessor() + def unsafeTags = [ + (InstrumentationTags.AWS_BUCKET_NAME): "some-bucket", + (InstrumentationTags.AWS_OBJECT_KEY) : "some-key.data", + "s3.eTag" : "ab12ef34-5" + ] + def spanContext = Mock(DDSpanContext) + def spanLinks = [] + + // From the original test, expected hash for these components + def expectedHash = "2b90dffc37ebc7bc610152c3dc72af9f" + + when: + def returnedTags = processor.processTags(unsafeTags, spanContext, spanLinks) + + then: + !returnedTags.containsKey("s3.eTag") + spanLinks.size() == 1 + def link = spanLinks[0] + link.traceId() == DDTraceId.ZERO + link.spanId() == DDSpanId.ZERO + link.attributes.asMap().get("ptr.kind") == SpanPointersProcessor.S3_PTR_KIND + link.attributes.asMap().get("ptr.dir") == SpanPointersProcessor.DOWN_DIRECTION + link.attributes.asMap().get("ptr.hash") == expectedHash + link.attributes.asMap().get("link.kind") == SpanPointersProcessor.LINK_KIND + } +} 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 c3d4b9e0071..a5f376ce812 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -3786,6 +3786,14 @@ public boolean isTimeInQueueEnabled( Arrays.asList(integrationNames), "", ".time-in-queue.enabled", defaultEnabled); } + public boolean isAddSpanPointers(final String integrationName) { + return configProvider.isEnabled( + Collections.singletonList(ADD_SPAN_POINTERS), + integrationName, + "", + DEFAULT_ADD_SPAN_POINTERS); + } + public boolean isEnabled( final boolean defaultEnabled, final String settingName, String settingSuffix) { return configProvider.isEnabled( diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index ff94ab022d7..5c8e0405e7c 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -36,6 +36,7 @@ public class InstrumentationTags { public static final String TABLE_NAME = "tablename"; public static final String AWS_REQUEST_ID = "aws.requestId"; public static final String AWS_STORAGE_CLASS = "aws.storage.class"; + public static final String S3_ETAG = "s3.eTag"; public static final String BUCKET = "bucket"; public static final String CASSANDRA_CONTACT_POINTS = "db.cassandra.contact.points"; diff --git a/settings.gradle b/settings.gradle index 53343a3ce6c..ba530788e46 100644 --- a/settings.gradle +++ b/settings.gradle @@ -200,6 +200,7 @@ include ':dd-java-agent:instrumentation:aws-java-sns-1.0' include ':dd-java-agent:instrumentation:aws-java-sns-2.0' include ':dd-java-agent:instrumentation:aws-java-sqs-1.0' include ':dd-java-agent:instrumentation:aws-java-sqs-2.0' +include ':dd-java-agent:instrumentation:aws-java-s3-2.0' include ':dd-java-agent:instrumentation:aws-lambda-handler' include ':dd-java-agent:instrumentation:axis-2' include ':dd-java-agent:instrumentation:axway-api'