diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbInterceptor.java b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbInterceptor.java index 3468915c7f1..8028c14f4e7 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbInterceptor.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbInterceptor.java @@ -41,12 +41,17 @@ public void afterExecution(AfterExecution context, ExecutionAttributes execution } SdkRequest request = context.request(); + String tableName = request.getValueForField("TableName", String.class).orElse(null); + Map keys = null; + if (request instanceof UpdateItemRequest) { - Map keys = ((UpdateItemRequest) request).key(); - DynamoDbUtil.exportTagsWithKnownKeys(span, keys); + keys = ((UpdateItemRequest) request).key(); } else if (request instanceof DeleteItemRequest) { - Map keys = ((DeleteItemRequest) request).key(); - DynamoDbUtil.exportTagsWithKnownKeys(span, keys); + keys = ((DeleteItemRequest) request).key(); + } + + if (keys != null) { + DynamoDbUtil.addSpanPointer(span, tableName, keys); } } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbUtil.java b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbUtil.java index 021965bec22..937a63977fc 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbUtil.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/dynamodb/DynamoDbUtil.java @@ -1,11 +1,7 @@ package datadog.trace.instrumentation.aws.v2.dynamodb; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_1; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_2; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_2_VALUE; - import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPointerUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -37,36 +33,42 @@ private static String extractValueAsString(AttributeValue value) { } /** - * Gets primary key/values and exports them as temporary tags on the span so that - * SpanPointersProcessor.java can complete the span pointer creation. + * Creates a DynamoDB span pointer link from the given primary keys and adds it to the span. * - * @param span The span to set the temporary tags on + * @param span The span to add the pointer link to + * @param tableName The DynamoDB table name * @param keys The primary key/values to extract from */ - static void exportTagsWithKnownKeys(AgentSpan span, Map keys) { - if (keys == null || keys.isEmpty()) { + static void addSpanPointer(AgentSpan span, String tableName, Map keys) { + if (keys == null || keys.isEmpty() || tableName == null) { return; } + String primaryKey1Name; + String primaryKey1Value; + String primaryKey2Name = null; + String primaryKey2Value = null; + if (keys.size() == 1) { // Single primary key case Map.Entry entry = keys.entrySet().iterator().next(); - span.setTag(DYNAMO_PRIMARY_KEY_1, entry.getKey()); - span.setTag(DYNAMO_PRIMARY_KEY_1_VALUE, extractValueAsString(entry.getValue())); + primaryKey1Name = entry.getKey(); + primaryKey1Value = extractValueAsString(entry.getValue()); } else { // Sort keys alphabetically List keyNames = new ArrayList<>(keys.keySet()); Collections.sort(keyNames); // First key (alphabetically) - String primaryKey1Name = keyNames.get(0); - span.setTag(DYNAMO_PRIMARY_KEY_1, primaryKey1Name); - span.setTag(DYNAMO_PRIMARY_KEY_1_VALUE, extractValueAsString(keys.get(primaryKey1Name))); + primaryKey1Name = keyNames.get(0); + primaryKey1Value = extractValueAsString(keys.get(primaryKey1Name)); // Second key - String primaryKey2Name = keyNames.get(1); - span.setTag(DYNAMO_PRIMARY_KEY_2, primaryKey2Name); - span.setTag(DYNAMO_PRIMARY_KEY_2_VALUE, extractValueAsString(keys.get(primaryKey2Name))); + primaryKey2Name = keyNames.get(1); + primaryKey2Value = extractValueAsString(keys.get(primaryKey2Name)); } + + SpanPointerUtils.addDynamoDbSpanPointer( + span, tableName, primaryKey1Name, primaryKey1Value, primaryKey2Name, primaryKey2Value); } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbClientTest.groovy index 02acd67be37..bc6f58ab116 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbClientTest.groovy @@ -3,7 +3,7 @@ import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTraceId import datadog.trace.bootstrap.instrumentation.api.SpanAttributes import datadog.trace.bootstrap.instrumentation.api.SpanLink -import datadog.trace.core.tagprocessor.SpanPointersProcessor +import datadog.trace.bootstrap.instrumentation.api.SpanPointerUtils import org.testcontainers.containers.GenericContainer import org.testcontainers.utility.DockerImageName import software.amazon.awssdk.auth.credentials.AwsBasicCredentials @@ -193,11 +193,11 @@ class DynamoDbClientTest extends InstrumentationSpecification { spanType DDSpanTypes.HTTP_CLIENT links { link(DDTraceId.ZERO, 0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.DYNAMODB_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.DYNAMODB_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) // First 32 chars of SHA256("dynamodb-one-key-table|id|test-id-1||") .put("ptr.hash", "ca8daaa857b00545ed5186a915cf1ab5") - .put("link.kind", SpanPointersProcessor.LINK_KIND) + .put("link.kind", SpanPointerUtils.LINK_KIND) .build()) } tags { @@ -295,11 +295,11 @@ class DynamoDbClientTest extends InstrumentationSpecification { spanType DDSpanTypes.HTTP_CLIENT links { link(DDTraceId.ZERO, 0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.DYNAMODB_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.DYNAMODB_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) // First 32 chars of SHA256("dynamodb-two-key-table|primaryKey|customer-123|sortKey|order-456") .put("ptr.hash", "90922c7899a82ea34406fdcdfb95161e") - .put("link.kind", SpanPointersProcessor.LINK_KIND) + .put("link.kind", SpanPointerUtils.LINK_KIND) .build()) } tags { @@ -383,11 +383,11 @@ class DynamoDbClientTest extends InstrumentationSpecification { spanType DDSpanTypes.HTTP_CLIENT links { link(DDTraceId.ZERO, 0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.DYNAMODB_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.DYNAMODB_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) // First 32 chars of SHA256("dynamodb-one-key-table|id|delete-test-id||") .put("ptr.hash", "65031164be5e929fddd274a02cba3f9f") - .put("link.kind", SpanPointersProcessor.LINK_KIND) + .put("link.kind", SpanPointerUtils.LINK_KIND) .build()) } tags { @@ -482,11 +482,11 @@ class DynamoDbClientTest extends InstrumentationSpecification { spanType DDSpanTypes.HTTP_CLIENT links { link(DDTraceId.ZERO, 0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.DYNAMODB_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.DYNAMODB_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) // First 32 chars of SHA256("dynamodb-two-key-table|primaryKey|user-789|sortKey|profile") .put("ptr.hash", "e5ce1148208c6f88041c73ceb9bbbf3a") - .put("link.kind", SpanPointersProcessor.LINK_KIND) + .put("link.kind", SpanPointerUtils.LINK_KIND) .build()) } tags { diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbUtilTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbUtilTest.groovy index efd1318ebe7..47ead0fa57d 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbUtilTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-dynamodb-2.0/src/test/groovy/DynamoDbUtilTest.groovy @@ -1,5 +1,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan -import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags +import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink +import datadog.trace.bootstrap.instrumentation.api.SpanPointerUtils import datadog.trace.instrumentation.aws.v2.dynamodb.DynamoDbUtil import org.junit.jupiter.api.Test import software.amazon.awssdk.core.SdkBytes @@ -7,124 +8,157 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue class DynamoDbUtilTest { static createMockSpan() { - def tags = [:] + def links = [] def mockSpan = [ - setTag: { String key, String value -> - tags[key] = value - return null + addLink: { AgentSpanLink link -> + links.add(link) } ] as AgentSpan - return [span: mockSpan, tags: tags] + return [span: mockSpan, links: links] } @Test - void testExportTagsWithNullKeys() { + void testAddSpanPointerWithNullKeys() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, null) + DynamoDbUtil.addSpanPointer(mockSpan, "table", null) - assert tags.isEmpty() + assert links.isEmpty() } @Test - void testExportTagsWithEmptyKeys() { + void testAddSpanPointerWithEmptyKeys() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, [:]) + DynamoDbUtil.addSpanPointer(mockSpan, "table", [:]) - assert tags.isEmpty() + assert links.isEmpty() } @Test - void testExportTagsWithSingleStringKey() { + void testAddSpanPointerWithNullTableName() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links def keys = [ "id": AttributeValue.builder().s("12345").build() ] - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, keys) + DynamoDbUtil.addSpanPointer(mockSpan, null, keys) - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1] == "id" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE] == "12345" + assert links.isEmpty() } @Test - void testExportTagsWithSingleNumberKey() { + void testAddSpanPointerWithSingleStringKey() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links + + def keys = [ + "id": AttributeValue.builder().s("12345").build() + ] + + DynamoDbUtil.addSpanPointer(mockSpan, "my-table", keys) + + assert links.size() == 1 + def link = links[0] + assert link.attributes().asMap().get("ptr.kind") == SpanPointerUtils.DYNAMODB_PTR_KIND + assert link.attributes().asMap().get("ptr.dir") == SpanPointerUtils.DOWN_DIRECTION + assert link.attributes().asMap().get("link.kind") == SpanPointerUtils.LINK_KIND + assert link.attributes().asMap().get("ptr.hash") != null + } + + @Test + void testAddSpanPointerWithSingleNumberKey() { + def mockData = createMockSpan() + def mockSpan = mockData.span + def links = mockData.links def keys = [ "count": AttributeValue.builder().n("42").build() ] - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, keys) + DynamoDbUtil.addSpanPointer(mockSpan, "my-table", keys) - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1] == "count" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE] == "42" + assert links.size() == 1 + def link = links[0] + assert link.attributes().asMap().get("ptr.kind") == SpanPointerUtils.DYNAMODB_PTR_KIND } @Test - void testExportTagsWithSingleBinaryKey() { + void testAddSpanPointerWithSingleBinaryKey() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links def binaryData = "binary-data".getBytes() def keys = [ "data": AttributeValue.builder().b(SdkBytes.fromByteArray(binaryData)).build() ] - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, keys) + DynamoDbUtil.addSpanPointer(mockSpan, "my-table", keys) - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1] == "data" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE] == "binary-data" + assert links.size() == 1 + def link = links[0] + assert link.attributes().asMap().get("ptr.kind") == SpanPointerUtils.DYNAMODB_PTR_KIND } @Test - void testExportTagsWithTwoKeys() { + void testAddSpanPointerWithTwoKeys() { def mockData = createMockSpan() def mockSpan = mockData.span - def tags = mockData.tags + def links = mockData.links def keys = [ "id": AttributeValue.builder().s("12345").build(), "name": AttributeValue.builder().s("item-name").build() ] - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, keys) + DynamoDbUtil.addSpanPointer(mockSpan, "my-table", keys) - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1] == "id" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE] == "12345" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_2] == "name" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_2_VALUE] == "item-name" + assert links.size() == 1 + def link = links[0] + assert link.attributes().asMap().get("ptr.kind") == SpanPointerUtils.DYNAMODB_PTR_KIND + assert link.attributes().asMap().get("ptr.hash") != null } @Test - void testExportTagsWithTwoKeysSortsAlphabetically() { + void testAddSpanPointerWithTwoKeysSortsAlphabetically() { def mockData = createMockSpan() - def mockSpan = mockData.span - def tags = mockData.tags + def mockSpan1 = mockData.span + def links1 = mockData.links - def keys = [ + // Keys in order bKey, aKey + def keys1 = [ "bKey": AttributeValue.builder().s("abc").build(), "aKey": AttributeValue.builder().s("zxy").build() ] - DynamoDbUtil.exportTagsWithKnownKeys(mockSpan, keys) + DynamoDbUtil.addSpanPointer(mockSpan1, "my-table", keys1) + + // Reverse order: aKey, bKey — should produce the same hash + def mockData2 = createMockSpan() + def mockSpan2 = mockData2.span + def links2 = mockData2.links + + def keys2 = [ + "aKey": AttributeValue.builder().s("zxy").build(), + "bKey": AttributeValue.builder().s("abc").build() + ] + + DynamoDbUtil.addSpanPointer(mockSpan2, "my-table", keys2) - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1] == "aKey" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE] == "zxy" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_2] == "bKey" - assert tags[InstrumentationTags.DYNAMO_PRIMARY_KEY_2_VALUE] == "abc" + assert links1.size() == 1 + assert links2.size() == 1 + // Both should produce the same hash regardless of input order + assert links1[0].attributes().asMap().get("ptr.hash") == links2[0].attributes().asMap().get("ptr.hash") } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java b/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java index e03b6bb2427..e944393b4c4 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/main/java/datadog/trace/instrumentation/aws/v2/s3/S3Interceptor.java @@ -1,11 +1,10 @@ package datadog.trace.instrumentation.aws.v2.s3; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.S3_ETAG; - import datadog.context.Context; import datadog.trace.api.Config; import datadog.trace.bootstrap.InstanceStore; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.SpanPointerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.interceptor.Context.AfterExecution; @@ -52,9 +51,9 @@ public void afterExecution(AfterExecution context, ExecutionAttributes execution 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); + // Get bucket and key from the request to create the span pointer directly + String bucket = context.request().getValueForField("Bucket", String.class).orElse(null); + String key = context.request().getValueForField("Key", String.class).orElse(null); + SpanPointerUtils.addS3SpanPointer(span, bucket, key, eTag); } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy index c2071fb7858..5c3bd36885f 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-s3-2.0/src/test/groovy/S3ClientTest.groovy @@ -3,7 +3,7 @@ import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTraceId import datadog.trace.bootstrap.instrumentation.api.SpanAttributes import datadog.trace.bootstrap.instrumentation.api.SpanLink -import datadog.trace.core.tagprocessor.SpanPointersProcessor +import datadog.trace.bootstrap.instrumentation.api.SpanPointerUtils import org.testcontainers.containers.GenericContainer import org.testcontainers.utility.DockerImageName import software.amazon.awssdk.auth.credentials.AwsBasicCredentials @@ -75,10 +75,10 @@ class S3ClientTest extends InstrumentationSpecification { links { link(DDTraceId.ZERO, (long)0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.S3_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.S3_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) .put("ptr.hash","6d1a2fe194c6579187408f827f942be3") - .put("link.kind",SpanPointersProcessor.LINK_KIND).build()) + .put("link.kind",SpanPointerUtils.LINK_KIND).build()) } tags { defaultTags() @@ -134,10 +134,10 @@ class S3ClientTest extends InstrumentationSpecification { links { link(DDTraceId.ZERO, (long)0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.S3_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.S3_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) .put("ptr.hash","6d1a2fe194c6579187408f827f942be3") - .put("link.kind",SpanPointersProcessor.LINK_KIND).build()) + .put("link.kind",SpanPointerUtils.LINK_KIND).build()) } tags { defaultTags() @@ -168,10 +168,10 @@ class S3ClientTest extends InstrumentationSpecification { links { link(DDTraceId.ZERO, (long)0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.S3_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.S3_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) .put("ptr.hash","1542053ce6d393c424b1374bac1fc0c5") - .put("link.kind",SpanPointersProcessor.LINK_KIND).build()) + .put("link.kind",SpanPointerUtils.LINK_KIND).build()) } tags { defaultTags() @@ -348,10 +348,10 @@ class S3ClientTest extends InstrumentationSpecification { links { link(DDTraceId.ZERO, (long)0, SpanLink.DEFAULT_FLAGS, SpanAttributes.builder() - .put("ptr.kind", SpanPointersProcessor.S3_PTR_KIND) - .put("ptr.dir", SpanPointersProcessor.DOWN_DIRECTION) + .put("ptr.kind", SpanPointerUtils.S3_PTR_KIND) + .put("ptr.dir", SpanPointerUtils.DOWN_DIRECTION) .put("ptr.hash","422412aa6b472a7194f3e24f4b12b4a6") - .put("link.kind",SpanPointersProcessor.LINK_KIND).build()) + .put("link.kind",SpanPointerUtils.LINK_KIND).build()) } tags { defaultTags() 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 deleted file mode 100644 index 8282583cbf2..00000000000 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/SpanPointersProcessor.java +++ /dev/null @@ -1,164 +0,0 @@ -package datadog.trace.core.tagprocessor; - -import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.DEFAULT_FLAGS; -import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpanContext; -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.AWS_TABLE_NAME; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_1; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_1_VALUE; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_2; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DYNAMO_PRIMARY_KEY_2_VALUE; -import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.S3_ETAG; - -import datadog.trace.api.TagMap; -import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; -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 extends 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. - public static final String DOWN_DIRECTION = "d"; - public static final String DYNAMODB_PTR_KIND = "aws.dynamodb.item"; - public static final String S3_PTR_KIND = "aws.s3.object"; - public static final String LINK_KIND = "span-pointer"; - - @Override - public void processTags( - TagMap unsafeTags, DDSpanContext spanContext, List spanLinks) { - // DQH - TODO - There's a lot room to optimize this using TagMap's capabilities - AgentSpanLink s3Link = handleS3SpanPointer(unsafeTags); - if (s3Link != null) { - spanLinks.add(s3Link); - } - - AgentSpanLink dynamoDbLink = handleDynamoDbSpanPointer(unsafeTags); - if (dynamoDbLink != null) { - spanLinks.add(dynamoDbLink); - } - } - - private static AgentSpanLink handleS3SpanPointer(Map unsafeTags) { - String eTag = asString(unsafeTags.remove(S3_ETAG)); - if (eTag == null) { - return null; - } - String bucket = asString(unsafeTags.get(AWS_BUCKET_NAME)); - String key = asString(unsafeTags.get(AWS_OBJECT_KEY)); - if (bucket == null || key == null) { - // This might be from an S3 operation not supported by span pointers, so we skip without - // logging anything. - return null; - } - - // 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 { - String hash = generatePointerHash(components); - return buildSpanPointer(hash, S3_PTR_KIND); - } catch (Exception e) { - LOG.debug("Failed to add span pointer: {}", e.getMessage()); - return null; - } - } - - private static AgentSpanLink handleDynamoDbSpanPointer(Map unsafeTags) { - // Hash calculation rules: - // https://github.com/DataDog/dd-span-pointer-rules/blob/main/AWS/DynamoDB/Item/README.md - String tableName = asString(unsafeTags.get(AWS_TABLE_NAME)); - if (tableName == null) { - return null; - } - String primaryKey1Name = asString(unsafeTags.remove(DYNAMO_PRIMARY_KEY_1)); - String primaryKey1Value = asString(unsafeTags.remove(DYNAMO_PRIMARY_KEY_1_VALUE)); - if (primaryKey1Name == null || primaryKey1Value == null) { - // This might be from a DynamoDB operation not supported by span pointers, so we skip without - // logging anything. - return null; - } - - // If these don't exist, the user has a table with only partition key but no sort key. - // Then, we set them to empty strings when calculating the hash. - String primaryKey2Name = asString(unsafeTags.remove(DYNAMO_PRIMARY_KEY_2)); - String primaryKey2Value = asString(unsafeTags.remove(DYNAMO_PRIMARY_KEY_2_VALUE)); - if (primaryKey2Name == null) { - primaryKey2Name = ""; - } - if (primaryKey2Value == null) { - primaryKey2Value = ""; - } - - String[] components = - new String[] { - tableName, primaryKey1Name, primaryKey1Value, primaryKey2Name, primaryKey2Value - }; - try { - String hash = generatePointerHash(components); - return buildSpanPointer(hash, DYNAMODB_PTR_KIND); - } catch (Exception e) { - LOG.debug("Failed to add span pointer: {}", e.getMessage()); - return null; - } - } - - 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); - } - - private static AgentSpanLink buildSpanPointer(String hash, String ptrKind) { - SpanAttributes attributes = - SpanAttributes.builder() - .put("ptr.kind", ptrKind) - .put("ptr.dir", DOWN_DIRECTION) - .put("ptr.hash", hash) - .put("link.kind", LINK_KIND) - .build(); - - return SpanLink.from(noopSpanContext(), DEFAULT_FLAGS, "", attributes); - } -} 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 08c52a2658a..42ed82b1b9c 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 @@ -40,13 +40,6 @@ private static TagsPostProcessor createLazyChain() { 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()); - } processors.add(new IntegrationAdder()); processors.add(new ServiceNameSourceAdder()); return new PostProcessorChain( 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 deleted file mode 100644 index 09bae762eff..00000000000 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/tagprocessor/SpanPointersProcessorTest.groovy +++ /dev/null @@ -1,100 +0,0 @@ -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/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index c7397a18114..501ae26cd26 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 @@ -37,13 +37,6 @@ public class InstrumentationTags { public static final String AWS_REQUEST_ID = "aws.requestId"; public static final String AWS_STORAGE_CLASS = "aws.storage.class"; - // These are temporary keys used for span pointer hash calculation - public static final String S3_ETAG = "s3.eTag"; - public static final String DYNAMO_PRIMARY_KEY_1 = "dynamodb.primary_key_1"; - public static final String DYNAMO_PRIMARY_KEY_1_VALUE = "dynamodb.primary_key_1_value"; - public static final String DYNAMO_PRIMARY_KEY_2 = "dynamodb.primary_key_2"; - public static final String DYNAMO_PRIMARY_KEY_2_VALUE = "dynamodb.primary_key_2_value"; - public static final String BUCKET = "bucket"; public static final String CASSANDRA_CONTACT_POINTS = "db.cassandra.contact.points"; public static final String COUCHBASE_OPERATION_ID = "couchbase.operation_id"; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtils.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtils.java new file mode 100644 index 00000000000..d46f3b87a49 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtils.java @@ -0,0 +1,133 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.DEFAULT_FLAGS; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.noopSpanContext; + +import datadog.trace.util.Strings; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utility class for creating span pointer links for AWS resources (S3, DynamoDB, etc.). */ +public class SpanPointerUtils { + private static final Logger LOG = LoggerFactory.getLogger(SpanPointerUtils.class); + + // The pointer direction will always be down. The serverless agent handles cases where the + // direction is up. + public static final String DOWN_DIRECTION = "d"; + public static final String DYNAMODB_PTR_KIND = "aws.dynamodb.item"; + public static final String S3_PTR_KIND = "aws.s3.object"; + public static final String LINK_KIND = "span-pointer"; + + /** + * 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 hex hash uniquely identifying the components + * @throws NoSuchAlgorithmException this should never happen; but should be handled just in case. + */ + public 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); + } + + /** + * Builds a span pointer link with the given hash and pointer kind. + * + * @param hash The 32-character hex hash identifying the resource + * @param ptrKind The kind of pointer (e.g. "aws.s3.object", "aws.dynamodb.item") + * @return An AgentSpanLink representing the span pointer + */ + public static AgentSpanLink buildSpanPointer(String hash, String ptrKind) { + SpanAttributes attributes = + SpanAttributes.builder() + .put("ptr.kind", ptrKind) + .put("ptr.dir", DOWN_DIRECTION) + .put("ptr.hash", hash) + .put("link.kind", LINK_KIND) + .build(); + + return SpanLink.from(noopSpanContext(), DEFAULT_FLAGS, "", attributes); + } + + /** + * Creates and adds an S3 span pointer link to the given span. + * + * @param span The span to add the pointer link to + * @param bucket The S3 bucket name + * @param key The S3 object key + * @param eTag The S3 object ETag (quotes will be stripped if present) + */ + public static void addS3SpanPointer(AgentSpan span, String bucket, String key, String eTag) { + if (bucket == null || key == null || eTag == null) { + return; + } + // Strip surrounding quotes from eTag + // 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); + } + try { + String hash = generatePointerHash(bucket, key, eTag); + span.addLink(buildSpanPointer(hash, S3_PTR_KIND)); + } catch (Exception e) { + LOG.debug("Failed to add S3 span pointer: {}", e.getMessage()); + } + } + + /** + * Creates and adds a DynamoDB span pointer link to the given span. + * + * @param span The span to add the pointer link to + * @param tableName The DynamoDB table name + * @param primaryKey1Name The partition key name + * @param primaryKey1Value The partition key value + * @param primaryKey2Name The sort key name (empty string if no sort key) + * @param primaryKey2Value The sort key value (empty string if no sort key) + */ + public static void addDynamoDbSpanPointer( + AgentSpan span, + String tableName, + String primaryKey1Name, + String primaryKey1Value, + String primaryKey2Name, + String primaryKey2Value) { + if (tableName == null || primaryKey1Name == null || primaryKey1Value == null) { + return; + } + if (primaryKey2Name == null) { + primaryKey2Name = ""; + } + if (primaryKey2Value == null) { + primaryKey2Value = ""; + } + // https://github.com/DataDog/dd-span-pointer-rules/blob/main/AWS/DynamoDB/Item/README.md + try { + String hash = + generatePointerHash( + tableName, primaryKey1Name, primaryKey1Value, primaryKey2Name, primaryKey2Value); + span.addLink(buildSpanPointer(hash, DYNAMODB_PTR_KIND)); + } catch (Exception e) { + LOG.debug("Failed to add DynamoDB span pointer: {}", e.getMessage()); + } + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtilsTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtilsTest.groovy new file mode 100644 index 00000000000..29a73a4ee7f --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/instrumentation/api/SpanPointerUtilsTest.groovy @@ -0,0 +1,132 @@ +package datadog.trace.bootstrap.instrumentation.api + +import datadog.trace.api.DDSpanId +import datadog.trace.api.DDTraceId +import datadog.trace.test.util.DDSpecification + +class SpanPointerUtilsTest extends DDSpecification { + def "generatePointerHash produces correct hash for S3 basic values"() { + when: + def hash = SpanPointerUtils.generatePointerHash("some-bucket", "some-key.data", "ab12ef34") + + then: + hash == "e721375466d4116ab551213fdea08413" + } + + def "generatePointerHash produces correct hash for S3 non-ascii key"() { + when: + def hash = SpanPointerUtils.generatePointerHash("some-bucket", "some-key.\u4f60\u597d", "ab12ef34") + + then: + hash == "d1333a04b9928ab462b5c6cadfa401f4" + } + + def "generatePointerHash produces correct hash for S3 multipart-upload ETag"() { + when: + def hash = SpanPointerUtils.generatePointerHash("some-bucket", "some-key.data", "ab12ef34-5") + + then: + hash == "2b90dffc37ebc7bc610152c3dc72af9f" + } + + def "buildSpanPointer creates link with correct attributes"() { + when: + def link = SpanPointerUtils.buildSpanPointer("testhash", SpanPointerUtils.S3_PTR_KIND) + + then: + link instanceof SpanLink + link.traceId() == DDTraceId.ZERO + link.spanId() == DDSpanId.ZERO + link.attributes().asMap().get("ptr.kind") == SpanPointerUtils.S3_PTR_KIND + link.attributes().asMap().get("ptr.dir") == SpanPointerUtils.DOWN_DIRECTION + link.attributes().asMap().get("ptr.hash") == "testhash" + link.attributes().asMap().get("link.kind") == SpanPointerUtils.LINK_KIND + } + + def "addS3SpanPointer strips surrounding quotes from eTag"() { + given: + def links = [] + def span = Mock(AgentSpan) + span.addLink(_) >> { AgentSpanLink link -> links.add(link) } + + when: + SpanPointerUtils.addS3SpanPointer(span, "some-bucket", "some-key.data", '"ab12ef34"') + + then: + links.size() == 1 + // Quoted eTag should produce the same hash as unquoted + links[0].attributes().asMap().get("ptr.hash") == "e721375466d4116ab551213fdea08413" + } + + def "addS3SpanPointer skips when bucket is null"() { + given: + def span = Mock(AgentSpan) + + when: + SpanPointerUtils.addS3SpanPointer(span, null, "key", "etag") + + then: + 0 * span.addLink(_) + } + + def "addS3SpanPointer skips when key is null"() { + given: + def span = Mock(AgentSpan) + + when: + SpanPointerUtils.addS3SpanPointer(span, "bucket", null, "etag") + + then: + 0 * span.addLink(_) + } + + def "addS3SpanPointer skips when eTag is null"() { + given: + def span = Mock(AgentSpan) + + when: + SpanPointerUtils.addS3SpanPointer(span, "bucket", "key", null) + + then: + 0 * span.addLink(_) + } + + def "addDynamoDbSpanPointer creates link with correct attributes"() { + given: + def links = [] + def span = Mock(AgentSpan) + span.addLink(_) >> { AgentSpanLink link -> links.add(link) } + + when: + SpanPointerUtils.addDynamoDbSpanPointer(span, "my-table", "id", "12345", null, null) + + then: + links.size() == 1 + links[0].attributes().asMap().get("ptr.kind") == SpanPointerUtils.DYNAMODB_PTR_KIND + links[0].attributes().asMap().get("ptr.dir") == SpanPointerUtils.DOWN_DIRECTION + links[0].attributes().asMap().get("link.kind") == SpanPointerUtils.LINK_KIND + links[0].attributes().asMap().get("ptr.hash") != null + } + + def "addDynamoDbSpanPointer skips when tableName is null"() { + given: + def span = Mock(AgentSpan) + + when: + SpanPointerUtils.addDynamoDbSpanPointer(span, null, "id", "12345", null, null) + + then: + 0 * span.addLink(_) + } + + def "addDynamoDbSpanPointer skips when primaryKey1Name is null"() { + given: + def span = Mock(AgentSpan) + + when: + SpanPointerUtils.addDynamoDbSpanPointer(span, "my-table", null, "12345", null, null) + + then: + 0 * span.addLink(_) + } +}