From 8aa4fb8a2ece9442611b737d1678e1b53e01a4b7 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 18 Jun 2026 08:23:38 -0700 Subject: [PATCH 1/5] Fix DynamoDB batch cardinality --- .../internal/DynamoDbAttributesExtractor.java | 99 ++- .../awssdk/v1_11/internal/RequestAccess.java | 68 ++ .../v1_11/AbstractDynamoDbClientTest.java | 345 ++++++---- .../internal/DynamoDbAttributesExtractor.java | 106 ++- .../v2_2/AbstractAws2ClientCoreTest.java | 611 ++++++++++++------ 5 files changed, 845 insertions(+), 384 deletions(-) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java index 3a0694ad74a6..b902244a6467 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java @@ -38,6 +38,12 @@ class DynamoDbAttributesExtractor implements AttributesExtractor, Res // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues private static final String AWS_DYNAMODB = "aws.dynamodb"; + // write operation type classification + private static final int WRITE_OP_NONE = 0; + private static final int WRITE_OP_PUT = 1; + private static final int WRITE_OP_DELETE = 2; + private static final int WRITE_OP_MIXED = 3; + @Override public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { if (emitStableDatabaseSemconv()) { @@ -49,9 +55,13 @@ public void onStart(AttributesBuilder attributes, Context parentContext, Request String operation = getOperationName(request.getOriginalRequest()); Long batchSize = extractBatchSize(operation, request.getOriginalRequest()); + int writeOpType = + "BatchWriteItem".equals(operation) + ? extractWriteOperationType(request.getOriginalRequest()) + : WRITE_OP_NONE; if (emitStableDatabaseSemconv()) { - attributes.put(DB_OPERATION_NAME, getStableOperationName(operation, batchSize)); - if (isBatch(batchSize)) { + attributes.put(DB_OPERATION_NAME, getStableOperationName(operation, batchSize, writeOpType)); + if (shouldEmitBatchSize(batchSize)) { attributes.put(DB_OPERATION_BATCH_SIZE, batchSize); } } @@ -96,30 +106,31 @@ private static String getSingleCollectionName(Map requestItems) { @Nullable private static String getStableOperationName( - @Nullable String operation, @Nullable Long batchSize) { - if ("BatchGetItem".equals(operation)) { - return getStableBatchOperationName(batchSize, "GetItem", operation); - } + @Nullable String operation, @Nullable Long batchSize, int writeOpType) { if ("BatchWriteItem".equals(operation)) { - return getStableBatchOperationName(batchSize, "WriteItem", operation); + return getStableWriteOperationName(batchSize, writeOpType); } return operation; } - private static String getStableBatchOperationName( - @Nullable Long batchSize, String itemOperation, String batchOperation) { + private static String getStableWriteOperationName(@Nullable Long batchSize, int writeOpType) { if (batchSize == null || batchSize == 0) { - return batchOperation; + return "BatchWriteItem"; } + String itemOp = writeOpType == WRITE_OP_PUT ? "PutItem" : "DeleteItem"; if (batchSize == 1) { - return itemOperation; + return itemOp; + } + // mixed operations collapse to bare BATCH (consistent with SQL/Cassandra) + if (writeOpType == WRITE_OP_MIXED) { + return "BATCH"; } - return "BATCH " + itemOperation; + return "BATCH " + itemOp; } @Nullable private static Long extractBatchSize(@Nullable String operation, Object request) { - if (!"BatchGetItem".equals(operation) && !"BatchWriteItem".equals(operation)) { + if (!"BatchWriteItem".equals(operation)) { return null; } @@ -128,36 +139,66 @@ private static Long extractBatchSize(@Nullable String operation, Object request) return null; } - long batchSize = - "BatchGetItem".equals(operation) - ? countBatchGetItems(requestItems) - : countBatchWriteItems(requestItems); - return batchSize == 0 ? null : batchSize; + long batchSize = countBatchWriteItems(requestItems); + // return the size for every batch request, including an empty batch with size 0 + return batchSize; } - private static long countBatchGetItems(Map requestItems) { + private static long countBatchWriteItems(Map requestItems) { long count = 0; - for (Object keysAndAttributes : requestItems.values()) { - List keys = RequestAccess.getKeys(keysAndAttributes); - if (keys != null) { - count += keys.size(); + for (Object writeRequests : requestItems.values()) { + if (writeRequests instanceof Collection) { + count += ((Collection) writeRequests).size(); } } return count; } - private static long countBatchWriteItems(Map requestItems) { - long count = 0; + /** + * Extracts the write operation type from a BatchWriteItem request. Returns WRITE_OP_PUT if all + * requests are PutRequests, WRITE_OP_DELETE if all are DeleteRequests, WRITE_OP_MIXED if both + * types are present, or WRITE_OP_NONE if the request is empty or cannot be inspected. + */ + private static int extractWriteOperationType(Object request) { + Map requestItems = RequestAccess.getRequestItems(request); + if (requestItems == null) { + return WRITE_OP_NONE; + } + + int result = WRITE_OP_NONE; for (Object writeRequests : requestItems.values()) { if (writeRequests instanceof Collection) { - count += ((Collection) writeRequests).size(); + for (Object writeRequest : (Collection) writeRequests) { + int opType = classifyWriteRequest(writeRequest); + if (opType == WRITE_OP_NONE) { + continue; + } + if (result == WRITE_OP_NONE) { + result = opType; + } else if (result != opType) { + return WRITE_OP_MIXED; + } + } } } - return count; + return result; + } + + private static int classifyWriteRequest(Object writeRequest) { + // WriteRequest has getPutRequest() and getDeleteRequest() methods; exactly one returns non-null + if (RequestAccess.hasPutRequest(writeRequest)) { + return WRITE_OP_PUT; + } + if (RequestAccess.hasDeleteRequest(writeRequest)) { + return WRITE_OP_DELETE; + } + return WRITE_OP_NONE; } - private static boolean isBatch(@Nullable Long batchSize) { - return batchSize != null && batchSize > 1; + // db.operation.batch.size is captured for every batch request (including an empty batch with + // size 0); it is only omitted for a single-item batch, which is reported as a non-batch operation + private static boolean shouldEmitBatchSize(@Nullable Long batchSize) { + return batchSize != null && batchSize != 1; } @Nullable diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/RequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/RequestAccess.java index f4fe2066f243..54d5405e3b37 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/RequestAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/RequestAccess.java @@ -8,6 +8,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -111,6 +112,24 @@ static List getKeys(Object request) { return invokeOrNull(access.getKeys, request, List.class); } + /** + * Returns true if the given WriteRequest contains a PutRequest, false otherwise. Uses reflection + * to call getPutRequest() on the WriteRequest object. + */ + static boolean hasPutRequest(Object writeRequest) { + WriteRequestAccess access = WriteRequestAccess.ACCESSORS.get(writeRequest.getClass()); + return access.invokeGetPutRequest(writeRequest) != null; + } + + /** + * Returns true if the given WriteRequest contains a DeleteRequest, false otherwise. Uses + * reflection to call getDeleteRequest() on the WriteRequest object. + */ + static boolean hasDeleteRequest(Object writeRequest) { + WriteRequestAccess access = WriteRequestAccess.ACCESSORS.get(writeRequest.getClass()); + return access.invokeGetDeleteRequest(writeRequest) != null; + } + @Nullable static String getSnsTopicArn(Object request) { RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); @@ -220,4 +239,53 @@ private static MethodHandle findGetLambdaArnMethod() { } } } + + private static class WriteRequestAccess { + private static final ClassValue ACCESSORS = + new ClassValue() { + @Override + protected WriteRequestAccess computeValue(Class type) { + return new WriteRequestAccess(type); + } + }; + + @Nullable private final Method getPutRequest; + @Nullable private final Method getDeleteRequest; + + private WriteRequestAccess(Class clz) { + getPutRequest = findMethodOrNull(clz, "getPutRequest"); + getDeleteRequest = findMethodOrNull(clz, "getDeleteRequest"); + } + + @Nullable + Object invokeGetPutRequest(Object obj) { + return invokeMethod(getPutRequest, obj); + } + + @Nullable + Object invokeGetDeleteRequest(Object obj) { + return invokeMethod(getDeleteRequest, obj); + } + + @Nullable + private static Object invokeMethod(@Nullable Method method, Object obj) { + if (method == null) { + return null; + } + try { + return method.invoke(obj); + } catch (Throwable ignored) { + return null; + } + } + + @Nullable + private static Method findMethodOrNull(Class clz, String methodName) { + try { + return clz.getMethod(methodName); + } catch (Throwable ignored) { + return null; + } + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java index be8bbaf96692..79ef381c4a10 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java @@ -21,6 +21,7 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.DYNAMODB; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.AWS_DYNAMODB; import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; @@ -30,6 +31,7 @@ import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest; import com.amazonaws.services.dynamodbv2.model.BatchWriteItemRequest; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteRequest; import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes; import com.amazonaws.services.dynamodbv2.model.PutRequest; import com.amazonaws.services.dynamodbv2.model.WriteRequest; @@ -39,7 +41,12 @@ import io.opentelemetry.testing.internal.armeria.common.MediaType; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; public abstract class AbstractDynamoDbClientTest extends AbstractBaseAwsClientTest { @@ -82,10 +89,15 @@ void sendRequestWithMockedResponse() throws ReflectiveOperationException { SERVER_PORT); } + // describes the batch cases for the two DynamoDB batch operations (BatchGetItem and + // BatchWriteItem): the request to send and the expected client span. batch attributes + // (db.operation.batch.size, BATCH operation name, db.collection.name) are only emitted under + // stable database semconv for BatchWriteItem, whose request entries represent explicit write + // operations. BatchGetItem request entries are keys, so they do not produce batch telemetry. @SuppressWarnings("deprecation") // using deprecated semconv - @Test - void batchGetItemWithMultipleItemsUsesStableBatchAttributes() - throws ReflectiveOperationException { + @ParameterizedTest + @MethodSource("batchScenarios") + void batchOperation(BatchScenario scenario) throws ReflectiveOperationException { AmazonDynamoDB client = createClient(); server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "{}")); @@ -97,153 +109,220 @@ void batchGetItemWithMultipleItemsUsesStableBatchAttributes() maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB), equalTo( maybeStable(DB_OPERATION), - emitStableDatabaseSemconv() ? "BATCH GetItem" : "BatchGetItem"), + emitStableDatabaseSemconv() ? scenario.stableOperation : scenario.awsOperation), equalTo( - DB_OPERATION_BATCH_SIZE, emitStableDatabaseSemconv() ? Long.valueOf(2) : null), - equalTo(DB_COLLECTION_NAME, emitStableDatabaseSemconv() ? "sometable" : null))); - - Object response = - client.batchGetItem( - new BatchGetItemRequest() - .withRequestItems( - singletonMap( - "sometable", - new KeysAndAttributes() - .withKeys( - asList( - singletonMap("key", new AttributeValue().withS("value")), - singletonMap( - "key", new AttributeValue().withS("anotherValue"))))))); - assertRequestWithMockedResponse( - response, client, "DynamoDBv2", "BatchGetItem", "POST", additionalAttributes); + DB_OPERATION_BATCH_SIZE, + emitStableDatabaseSemconv() ? scenario.batchSize : null), + equalTo( + DB_COLLECTION_NAME, + emitStableDatabaseSemconv() && scenario.hasCollection ? "sometable" : null))); - assertDurationMetric( - testing(), - "io.opentelemetry.aws-sdk-1.11", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME, - SERVER_ADDRESS, - SERVER_PORT); + Object response = scenario.execute.apply(client); + assertRequestWithMockedResponse( + response, client, "DynamoDBv2", scenario.awsOperation, "POST", additionalAttributes); } - @SuppressWarnings("deprecation") // using deprecated semconv - @Test - void batchGetItemWithSingleItemUsesStableItemOperation() throws ReflectiveOperationException { - AmazonDynamoDB client = createClient(); - - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "{}")); - - List additionalAttributes = - new ArrayList<>( - asList( - equalTo( - maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB), - equalTo( - maybeStable(DB_OPERATION), - emitStableDatabaseSemconv() ? "GetItem" : "BatchGetItem"), - equalTo(DB_COLLECTION_NAME, emitStableDatabaseSemconv() ? "sometable" : null))); - - Object response = - client.batchGetItem( - new BatchGetItemRequest() - .withRequestItems( - singletonMap( - "sometable", - new KeysAndAttributes() - .withKeys( - singletonList( - singletonMap("key", new AttributeValue().withS("value"))))))); - assertRequestWithMockedResponse( - response, client, "DynamoDBv2", "BatchGetItem", "POST", additionalAttributes); + private static Stream batchScenarios() { + return Stream.of( + // BatchGetItem entries are keys, not explicit operations, so the stable operation name + // remains the raw batch operation and db.operation.batch.size is not emitted. + BatchScenario.builder("getItemEmpty") + .awsOperation("BatchGetItem") + .execute(client -> client.batchGetItem(getItemRequest(0))) + .stableOperation("BatchGetItem") + .build(), + BatchScenario.builder("getItemSingle") + .awsOperation("BatchGetItem") + .execute(client -> client.batchGetItem(getItemRequest(1))) + .stableOperation("BatchGetItem") + .hasCollection() + .build(), + BatchScenario.builder("getItemTwo") + .awsOperation("BatchGetItem") + .execute(client -> client.batchGetItem(getItemRequest(2))) + .stableOperation("BatchGetItem") + .hasCollection() + .build(), + BatchScenario.builder("writeItemEmpty") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(writeItemRequest(0))) + .stableOperation("BatchWriteItem") + .batchSize(0) + .build(), + // a single put request is reported as PutItem + BatchScenario.builder("writeItemSinglePut") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(putItemsRequest(1))) + .stableOperation("PutItem") + .hasCollection() + .build(), + // a single delete request is reported as DeleteItem + BatchScenario.builder("writeItemSingleDelete") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(deleteItemsRequest(1))) + .stableOperation("DeleteItem") + .hasCollection() + .build(), + // two put requests are reported as BATCH PutItem + BatchScenario.builder("writeItemTwoPuts") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(putItemsRequest(2))) + .stableOperation("BATCH PutItem") + .batchSize(2) + .hasCollection() + .build(), + // two delete requests are reported as BATCH DeleteItem + BatchScenario.builder("writeItemTwoDeletes") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(deleteItemsRequest(2))) + .stableOperation("BATCH DeleteItem") + .batchSize(2) + .hasCollection() + .build(), + // a batch mixing a put and a delete collapses to bare "BATCH" + // (consistent with SQL/Cassandra mixed-operation batches) + BatchScenario.builder("writeItemMixed") + .awsOperation("BatchWriteItem") + .execute(client -> client.batchWriteItem(mixedWriteItemRequest())) + .stableOperation("BATCH") + .batchSize(2) + .hasCollection() + .build()); + } - assertDurationMetric( - testing(), - "io.opentelemetry.aws-sdk-1.11", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME, - SERVER_ADDRESS, - SERVER_PORT); + private static BatchGetItemRequest getItemRequest(int count) { + if (count == 0) { + return new BatchGetItemRequest().withRequestItems(emptyMap()); + } + List> keys = new ArrayList<>(); + for (int i = 0; i < count; i++) { + keys.add(singletonMap("key", new AttributeValue().withS("value" + i))); + } + return new BatchGetItemRequest() + .withRequestItems(singletonMap("sometable", new KeysAndAttributes().withKeys(keys))); } - @SuppressWarnings("deprecation") // using deprecated semconv - @Test - void batchWriteItemWithMultipleItemsUsesStableBatchAttributes() - throws ReflectiveOperationException { - AmazonDynamoDB client = createClient(); + private static BatchWriteItemRequest writeItemRequest(int count) { + if (count == 0) { + return new BatchWriteItemRequest().withRequestItems(emptyMap()); + } + List writes = new ArrayList<>(); + for (int i = 0; i < count; i++) { + writes.add(putRequest("value" + i)); + } + return new BatchWriteItemRequest().withRequestItems(singletonMap("sometable", writes)); + } - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "{}")); + private static BatchWriteItemRequest putItemsRequest(int count) { + List writes = new ArrayList<>(); + for (int i = 0; i < count; i++) { + writes.add(putRequest("value" + i)); + } + return new BatchWriteItemRequest().withRequestItems(singletonMap("sometable", writes)); + } - List additionalAttributes = - new ArrayList<>( - asList( - equalTo( - maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB), - equalTo( - maybeStable(DB_OPERATION), - emitStableDatabaseSemconv() ? "BATCH WriteItem" : "BatchWriteItem"), - equalTo( - DB_OPERATION_BATCH_SIZE, emitStableDatabaseSemconv() ? Long.valueOf(2) : null), - equalTo(DB_COLLECTION_NAME, emitStableDatabaseSemconv() ? "sometable" : null))); - - Object response = - client.batchWriteItem( - new BatchWriteItemRequest() - .withRequestItems( - singletonMap( - "sometable", asList(writeRequest("value"), writeRequest("anotherValue"))))); - assertRequestWithMockedResponse( - response, client, "DynamoDBv2", "BatchWriteItem", "POST", additionalAttributes); + private static BatchWriteItemRequest deleteItemsRequest(int count) { + List writes = new ArrayList<>(); + for (int i = 0; i < count; i++) { + writes.add(deleteRequest("value" + i)); + } + return new BatchWriteItemRequest().withRequestItems(singletonMap("sometable", writes)); + } - assertDurationMetric( - testing(), - "io.opentelemetry.aws-sdk-1.11", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME, - SERVER_ADDRESS, - SERVER_PORT); + private static WriteRequest putRequest(String value) { + return new WriteRequest() + .withPutRequest( + new PutRequest().withItem(singletonMap("key", new AttributeValue().withS(value)))); } - @SuppressWarnings("deprecation") // using deprecated semconv - @Test - void batchWriteItemWithSingleItemUsesStableItemOperation() throws ReflectiveOperationException { - AmazonDynamoDB client = createClient(); + private static WriteRequest deleteRequest(String value) { + return new WriteRequest() + .withDeleteRequest( + new DeleteRequest().withKey(singletonMap("key", new AttributeValue().withS(value)))); + } - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "{}")); + private static BatchWriteItemRequest mixedWriteItemRequest() { + List writes = + asList( + new WriteRequest() + .withPutRequest( + new PutRequest() + .withItem(singletonMap("key", new AttributeValue().withS("value")))), + new WriteRequest() + .withDeleteRequest( + new DeleteRequest() + .withKey(singletonMap("key", new AttributeValue().withS("anotherValue"))))); + return new BatchWriteItemRequest().withRequestItems(singletonMap("sometable", writes)); + } - List additionalAttributes = - new ArrayList<>( - asList( - equalTo( - maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB), - equalTo( - maybeStable(DB_OPERATION), - emitStableDatabaseSemconv() ? "WriteItem" : "BatchWriteItem"), - equalTo(DB_COLLECTION_NAME, emitStableDatabaseSemconv() ? "sometable" : null))); + private static final class BatchScenario { + final String name; + final String awsOperation; + final Function execute; + final String stableOperation; + final Long batchSize; + final boolean hasCollection; + + BatchScenario(Builder builder) { + this.name = builder.name; + this.awsOperation = builder.awsOperation; + this.execute = builder.execute; + this.stableOperation = builder.stableOperation; + this.batchSize = builder.batchSize; + this.hasCollection = builder.hasCollection; + } - Object response = - client.batchWriteItem( - new BatchWriteItemRequest() - .withRequestItems(singletonMap("sometable", singletonList(writeRequest("value"))))); - assertRequestWithMockedResponse( - response, client, "DynamoDBv2", "BatchWriteItem", "POST", additionalAttributes); + @Override + public String toString() { + // used as the parameterized test display name + return name; + } - assertDurationMetric( - testing(), - "io.opentelemetry.aws-sdk-1.11", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME, - SERVER_ADDRESS, - SERVER_PORT); - } + static Builder builder(String name) { + return new Builder(name); + } - private static WriteRequest writeRequest(String value) { - return new WriteRequest() - .withPutRequest( - new PutRequest().withItem(singletonMap("key", new AttributeValue().withS(value)))); + static final class Builder { + private final String name; + private String awsOperation; + private Function execute; + private String stableOperation; + private Long batchSize; + private boolean hasCollection; + + Builder(String name) { + this.name = name; + } + + Builder awsOperation(String awsOperation) { + this.awsOperation = awsOperation; + return this; + } + + Builder execute(Function execute) { + this.execute = execute; + return this; + } + + Builder stableOperation(String stableOperation) { + this.stableOperation = stableOperation; + return this; + } + + Builder batchSize(long batchSize) { + this.batchSize = batchSize; + return this; + } + + Builder hasCollection() { + this.hasCollection = true; + return this; + } + + BatchScenario build() { + return new BatchScenario(this); + } + } } private AmazonDynamoDB createClient() { diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java index 5e80b7b96062..574906cc0497 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java @@ -35,6 +35,12 @@ class DynamoDbAttributesExtractor implements AttributesExtractor requestItemsMap = (Map) requestItems.get(); - return "BatchGetItem".equals(operation) - ? countBatchGetItems(requestItemsMap) - : countBatchWriteItems(requestItemsMap); + return countBatchWriteItems(requestItemsMap); } - private long countBatchGetItems(Map requestItems) { + private static long countBatchWriteItems(Map requestItems) { long count = 0; - for (Object keysAndAttributes : requestItems.values()) { - Object keys = next(keysAndAttributes, "Keys"); - if (keys instanceof Collection) { - count += ((Collection) keys).size(); + for (Object writeRequests : requestItems.values()) { + if (writeRequests instanceof Collection) { + count += ((Collection) writeRequests).size(); } } return count; } - private static long countBatchWriteItems(Map requestItems) { - long count = 0; - for (Object writeRequests : requestItems.values()) { + /** + * Extracts the write operation type from a BatchWriteItem request. Returns WRITE_OP_PUT if all + * requests are PutRequests, WRITE_OP_DELETE if all are DeleteRequests, WRITE_OP_MIXED if both + * types are present, or WRITE_OP_NONE if the request is empty or cannot be inspected. + */ + private int extractWriteOperationType(ExecutionAttributes executionAttributes) { + SdkRequest request = + executionAttributes.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); + if (request == null) { + return WRITE_OP_NONE; + } + Optional requestItems = request.getValueForField("RequestItems", Object.class); + if (!requestItems.isPresent() || !(requestItems.get() instanceof Map)) { + return WRITE_OP_NONE; + } + + int result = WRITE_OP_NONE; + for (Object writeRequests : ((Map) requestItems.get()).values()) { if (writeRequests instanceof Collection) { - count += ((Collection) writeRequests).size(); + for (Object writeRequest : (Collection) writeRequests) { + int opType = classifyWriteRequest(writeRequest); + if (opType == WRITE_OP_NONE) { + continue; + } + if (result == WRITE_OP_NONE) { + result = opType; + } else if (result != opType) { + return WRITE_OP_MIXED; + } + } } } - return count; + return result; + } + + private int classifyWriteRequest(Object writeRequest) { + // WriteRequest has putRequest() and deleteRequest() methods; exactly one returns non-null + Object putRequest = next(writeRequest, "PutRequest"); + if (putRequest != null) { + return WRITE_OP_PUT; + } + Object deleteRequest = next(writeRequest, "DeleteRequest"); + if (deleteRequest != null) { + return WRITE_OP_DELETE; + } + return WRITE_OP_NONE; } - private static boolean isBatch(@Nullable Long batchSize) { - return batchSize != null && batchSize > 1; + // db.operation.batch.size is captured for every batch request (including an empty batch with + // size 0); it is only omitted for a single-item batch, which is reported as a non-batch operation + private static boolean shouldEmitBatchSize(@Nullable Long batchSize) { + return batchSize != null && batchSize != 1; } @Nullable diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java index f17fabef54ca..1adcc824e061 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java @@ -80,6 +80,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteRequest; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; @@ -172,7 +173,6 @@ private void validateOperationResponse(String operation, Object response) { case "ListTables": assertListTablesRequest(span); return; - case "BatchGetItem": case "GetItem": assertDynamoDbRequest( span, @@ -183,19 +183,6 @@ private void validateOperationResponse(String operation, Object response) { singletonList( "{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}")))); return; - case "BatchWriteItem": - assertDynamoDbRequest( - span, - operation, - asList( - equalTo( - AWS_DYNAMODB_CONSUMED_CAPACITY, - singletonList( - "{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}")), - equalTo( - AWS_DYNAMODB_ITEM_COLLECTION_METRICS, - "[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]"))); - return; case "DeleteItem": case "PutItem": case "UpdateItem": @@ -294,16 +281,6 @@ private static void assertListTablesRequest(SpanDataAssert span) { @SuppressWarnings("deprecation") // uses deprecated semconv private static void assertDynamoDbRequest( SpanDataAssert span, String operation, List extraAttributes) { - assertDynamoDbRequest( - span, operation, extraAttributes, expectedDbOperationNameForSingleItemRequest(operation)); - } - - @SuppressWarnings("deprecation") // uses deprecated semconv - private static void assertDynamoDbRequest( - SpanDataAssert span, - String operation, - List extraAttributes, - String expectedStableOperationName) { List assertions = new ArrayList<>( asList( @@ -319,9 +296,7 @@ private static void assertDynamoDbRequest( equalTo(AWS_REQUEST_ID, "UNKNOWN"), equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable")), equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(DYNAMODB)), - equalTo( - maybeStable(DB_OPERATION), - emitStableDatabaseSemconv() ? expectedStableOperationName : operation))); + equalTo(maybeStable(DB_OPERATION), operation))); if (emitStableDatabaseSemconv()) { assertions.add(equalTo(DB_COLLECTION_NAME, "sometable")); } @@ -433,56 +408,7 @@ private static Stream provideArguments() { "UpdateTable", (Function) c -> c.updateTable(b -> b.tableName("sometable"))), Arguments.of( - "Scan", (Function) c -> c.scan(b -> b.tableName("sometable"))), - Arguments.of( - "BatchGetItem", - (Function) - c -> - c.batchGetItem( - b -> - b.requestItems( - ImmutableMap.of( - "sometable", - KeysAndAttributes.builder() - .keys( - singletonList( - ImmutableMap.of( - "keyOne", - AttributeValue.builder().s("value").build(), - "keyTwo", - AttributeValue.builder() - .s("differentValue") - .build()))) - .build())))), - Arguments.of( - "BatchWriteItem", - (Function) - c -> - c.batchWriteItem( - b -> - b.requestItems( - ImmutableMap.of( - "sometable", - singletonList( - WriteRequest.builder() - .putRequest( - PutRequest.builder() - .item( - ImmutableMap.of( - "key", - AttributeValue.builder() - .s("value") - .build(), - "attributeOne", - AttributeValue.builder() - .s("one") - .build(), - "attributeTwo", - AttributeValue.builder() - .s("two") - .build())) - .build()) - .build())))))); + "Scan", (Function) c -> c.scan(b -> b.tableName("sometable")))); } @ParameterizedTest @@ -566,9 +492,10 @@ void testBatchGetItemWithMultipleTablesOmitsDbCollectionName() { .doesNotContainKey(DB_COLLECTION_NAME)))); } - @Test + @ParameterizedTest + @MethodSource("batchScenarios") @SuppressWarnings("deprecation") // uses deprecated semconv - void testBatchGetItemWithMultipleItemsUsesStableBatchAttributes() { + void batchOperation(BatchScenario scenario) { DynamoDbClientBuilder builder = DynamoDbClient.builder(); configureSdkClient(builder); DynamoDbClient client = @@ -578,130 +505,310 @@ void testBatchGetItemWithMultipleItemsUsesStableBatchAttributes() { .credentialsProvider(CREDENTIALS_PROVIDER) .build(); server.enqueue( - HttpResponse.of( - HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, getResponseContent("BatchGetItem"))); + HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, scenario.responseContent)); - client.batchGetItem( - b -> - b.requestItems( - ImmutableMap.of( - "sometable", - KeysAndAttributes.builder() - .keys( - asList( - ImmutableMap.of("key", AttributeValue.builder().s("value").build()), - ImmutableMap.of( - "key", AttributeValue.builder().s("anotherValue").build()))) - .build()))); + Object response = scenario.execute.apply(client); + assertThat(response).isNotNull(); getTesting() .waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( - span -> - assertDynamoDbRequest( - span, - "BatchGetItem", - asList( - equalTo( - AWS_DYNAMODB_CONSUMED_CAPACITY, - singletonList( - "{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}")), - equalTo( - DB_OPERATION_BATCH_SIZE, - emitStableDatabaseSemconv() ? Long.valueOf(2) : null)), - "BATCH GetItem"))); - - assertDurationMetric( - getTesting(), - "io.opentelemetry.aws-sdk-2.2", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME); + span -> { + List attributes = + new ArrayList<>( + asList( + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(URL_FULL, server.httpUri() + "/"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "DynamoDb"), + equalTo(RPC_METHOD, scenario.awsOperation), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(AWS_REQUEST_ID, "UNKNOWN"), + equalTo( + maybeStable(DB_SYSTEM), maybeStableDbSystemName(DYNAMODB)), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() + ? scenario.stableOperation + : scenario.awsOperation))); + if (scenario.hasCollection) { + attributes.add( + equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable"))); + if (emitStableDatabaseSemconv()) { + attributes.add(equalTo(DB_COLLECTION_NAME, "sometable")); + } + } + attributes.addAll(scenario.extraAttributes()); + span.hasName("DynamoDb." + scenario.awsOperation) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributes); + })); + + if (scenario.assertMetric) { + assertDurationMetric( + getTesting(), + "io.opentelemetry.aws-sdk-2.2", + DB_SYSTEM_NAME, + DB_OPERATION_NAME, + DB_COLLECTION_NAME); + } } - @Test @SuppressWarnings("deprecation") // uses deprecated semconv - void testBatchWriteItemWithMultipleItemsUsesStableBatchAttributes() { - DynamoDbClientBuilder builder = DynamoDbClient.builder(); - configureSdkClient(builder); - DynamoDbClient client = - builder - .endpointOverride(server.httpUri()) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .build(); - server.enqueue( - HttpResponse.of( - HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, getResponseContent("BatchWriteItem"))); - - client.batchWriteItem( - b -> - b.requestItems( - ImmutableMap.of( - "sometable", - asList( - WriteRequest.builder() - .putRequest( - PutRequest.builder() - .item( - ImmutableMap.of( - "key", AttributeValue.builder().s("value").build())) - .build()) - .build(), - WriteRequest.builder() - .putRequest( - PutRequest.builder() - .item( - ImmutableMap.of( - "key", - AttributeValue.builder().s("anotherValue").build())) - .build()) - .build())))); - - getTesting() - .waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> - assertDynamoDbRequest( - span, - "BatchWriteItem", - asList( - equalTo( - AWS_DYNAMODB_CONSUMED_CAPACITY, + private static Stream batchScenarios() { + return Stream.of( + // BatchGetItem entries are keys, not explicit operations, so the stable operation name + // remains the raw batch operation and db.operation.batch.size is not emitted. + BatchScenario.builder("getItemEmpty") + .awsOperation("BatchGetItem") + .responseContent("{\"ConsumedCapacity\":[]}") + .execute(c -> c.batchGetItem(b -> b.requestItems(ImmutableMap.of()))) + .stableOperation("BatchGetItem") + .build(), + BatchScenario.builder("getItemSingle") + .awsOperation("BatchGetItem") + .responseContent(getResponseContent("BatchGetItem")) + .execute( + c -> + c.batchGetItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + KeysAndAttributes.builder() + .keys( + singletonList( + ImmutableMap.of( + "key", + AttributeValue.builder().s("value").build()))) + .build())))) + .stableOperation("BatchGetItem") + .hasCollection() + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .assertMetric() + .build(), + BatchScenario.builder("getItemTwo") + .awsOperation("BatchGetItem") + .responseContent(getResponseContent("BatchGetItem")) + .execute( + c -> + c.batchGetItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + KeysAndAttributes.builder() + .keys( + asList( + ImmutableMap.of( + "key", + AttributeValue.builder().s("value").build()), + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("anotherValue") + .build()))) + .build())))) + .stableOperation("BatchGetItem") + .hasCollection() + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .assertMetric() + .build(), + BatchScenario.builder("writeItemEmpty") + .awsOperation("BatchWriteItem") + .responseContent("{\"ConsumedCapacity\":[]}") + .execute(c -> c.batchWriteItem(b -> b.requestItems(ImmutableMap.of()))) + .stableOperation("BatchWriteItem") + .batchSize(0) + .build(), + // a single-item batch is not a batch, so it uses the singular item operation and emits + // no db.operation.batch.size + BatchScenario.builder("writeItemSinglePut") + .awsOperation("BatchWriteItem") + .responseContent(getResponseContent("BatchWriteItem")) + .execute( + c -> + c.batchWriteItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", singletonList( - "{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}")), - equalTo( - AWS_DYNAMODB_ITEM_COLLECTION_METRICS, - "[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]"), - equalTo( - DB_OPERATION_BATCH_SIZE, - emitStableDatabaseSemconv() ? Long.valueOf(2) : null)), - "BATCH WriteItem"))); - - assertDurationMetric( - getTesting(), - "io.opentelemetry.aws-sdk-2.2", - DB_SYSTEM_NAME, - DB_OPERATION_NAME, - DB_COLLECTION_NAME); - } - - private static String expectedDbOperationNameForSingleItemRequest(String operation) { - if (!emitStableDatabaseSemconv()) { - return operation; - } - // The parameterized Batch* requests contain one item. Stable DB semconv treats those as - // logical item operations; dedicated multi-item tests pass the BATCH operation name directly. - switch (operation) { - case "BatchGetItem": - return "GetItem"; - case "BatchWriteItem": - return "WriteItem"; - default: - return operation; - } + WriteRequest.builder() + .putRequest( + PutRequest.builder() + .item( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("value") + .build())) + .build()) + .build()))))) + .stableOperation("PutItem") + .hasCollection() + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .itemCollectionMetrics("[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]") + .assertMetric() + .build(), + // a single delete request is reported as DeleteItem + BatchScenario.builder("writeItemSingleDelete") + .awsOperation("BatchWriteItem") + .responseContent(getResponseContent("BatchWriteItem")) + .execute( + c -> + c.batchWriteItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + singletonList( + WriteRequest.builder() + .deleteRequest( + DeleteRequest.builder() + .key( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("value") + .build())) + .build()) + .build()))))) + .stableOperation("DeleteItem") + .hasCollection() + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .itemCollectionMetrics("[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]") + .assertMetric() + .build(), + // two put requests are reported as BATCH PutItem + BatchScenario.builder("writeItemTwoPuts") + .awsOperation("BatchWriteItem") + .responseContent(getResponseContent("BatchWriteItem")) + .execute( + c -> + c.batchWriteItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + asList( + WriteRequest.builder() + .putRequest( + PutRequest.builder() + .item( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("value") + .build())) + .build()) + .build(), + WriteRequest.builder() + .putRequest( + PutRequest.builder() + .item( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("anotherValue") + .build())) + .build()) + .build()))))) + .stableOperation("BATCH PutItem") + .hasCollection() + .batchSize(2) + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .itemCollectionMetrics("[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]") + .assertMetric() + .build(), + // two delete requests are reported as BATCH DeleteItem + BatchScenario.builder("writeItemTwoDeletes") + .awsOperation("BatchWriteItem") + .responseContent(getResponseContent("BatchWriteItem")) + .execute( + c -> + c.batchWriteItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + asList( + WriteRequest.builder() + .deleteRequest( + DeleteRequest.builder() + .key( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("value") + .build())) + .build()) + .build(), + WriteRequest.builder() + .deleteRequest( + DeleteRequest.builder() + .key( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("anotherValue") + .build())) + .build()) + .build()))))) + .stableOperation("BATCH DeleteItem") + .hasCollection() + .batchSize(2) + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .itemCollectionMetrics("[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]") + .assertMetric() + .build(), + // a batch mixing a put and a delete in one table collapses to bare "BATCH" + // (consistent with SQL/Cassandra mixed-operation batches) + BatchScenario.builder("writeItemMixed") + .awsOperation("BatchWriteItem") + .responseContent(getResponseContent("BatchWriteItem")) + .execute( + c -> + c.batchWriteItem( + b -> + b.requestItems( + ImmutableMap.of( + "sometable", + asList( + WriteRequest.builder() + .putRequest( + PutRequest.builder() + .item( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("value") + .build())) + .build()) + .build(), + WriteRequest.builder() + .deleteRequest( + DeleteRequest.builder() + .key( + ImmutableMap.of( + "key", + AttributeValue.builder() + .s("anotherValue") + .build())) + .build()) + .build()))))) + .stableOperation("BATCH") + .hasCollection() + .batchSize(2) + .consumedCapacity("{\"TableName\":\"sometable\",\"CapacityUnits\":1.0}") + .itemCollectionMetrics("[somekey1:[{\"ItemCollectionKey\":{\"somekey2\":{}}}]]") + .assertMetric() + .build()); } private static String getResponseContent(String operation) { @@ -727,4 +834,122 @@ private static String getResponseContent(String operation) { return ""; } } + + private static final class BatchScenario { + final String name; + final String awsOperation; + final String responseContent; + final Function execute; + final String stableOperation; + final boolean hasCollection; + final Long batchSize; + final String consumedCapacity; + final String itemCollectionMetrics; + final boolean assertMetric; + + BatchScenario(Builder builder) { + this.name = builder.name; + this.awsOperation = builder.awsOperation; + this.responseContent = builder.responseContent; + this.execute = builder.execute; + this.stableOperation = builder.stableOperation; + this.hasCollection = builder.hasCollection; + this.batchSize = builder.batchSize; + this.consumedCapacity = builder.consumedCapacity; + this.itemCollectionMetrics = builder.itemCollectionMetrics; + this.assertMetric = builder.assertMetric; + } + + @SuppressWarnings("deprecation") // uses deprecated semconv + List extraAttributes() { + List attributes = new ArrayList<>(); + if (consumedCapacity != null) { + attributes.add(equalTo(AWS_DYNAMODB_CONSUMED_CAPACITY, singletonList(consumedCapacity))); + } + if (itemCollectionMetrics != null) { + attributes.add(equalTo(AWS_DYNAMODB_ITEM_COLLECTION_METRICS, itemCollectionMetrics)); + } + if (batchSize != null) { + attributes.add( + equalTo(DB_OPERATION_BATCH_SIZE, emitStableDatabaseSemconv() ? batchSize : null)); + } + return attributes; + } + + @Override + public String toString() { + // used as the parameterized test display name + return name; + } + + static Builder builder(String name) { + return new Builder(name); + } + + static final class Builder { + private final String name; + private String awsOperation; + private String responseContent; + private Function execute; + private String stableOperation; + private boolean hasCollection; + private Long batchSize; + private String consumedCapacity; + private String itemCollectionMetrics; + private boolean assertMetric; + + Builder(String name) { + this.name = name; + } + + Builder awsOperation(String awsOperation) { + this.awsOperation = awsOperation; + return this; + } + + Builder responseContent(String responseContent) { + this.responseContent = responseContent; + return this; + } + + Builder execute(Function execute) { + this.execute = execute; + return this; + } + + Builder stableOperation(String stableOperation) { + this.stableOperation = stableOperation; + return this; + } + + Builder hasCollection() { + this.hasCollection = true; + return this; + } + + Builder batchSize(long batchSize) { + this.batchSize = batchSize; + return this; + } + + Builder consumedCapacity(String consumedCapacity) { + this.consumedCapacity = consumedCapacity; + return this; + } + + Builder itemCollectionMetrics(String itemCollectionMetrics) { + this.itemCollectionMetrics = itemCollectionMetrics; + return this; + } + + Builder assertMetric() { + this.assertMetric = true; + return this; + } + + BatchScenario build() { + return new BatchScenario(this); + } + } + } } From 1a594d342bf07f1b86b76a30ea433d8485fc908a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 18 Jun 2026 11:28:18 -0700 Subject: [PATCH 2/5] Address review comment: guard DynamoDB write fallback --- .../awssdk/v2_2/internal/DynamoDbAttributesExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java index 574906cc0497..46c8dd423bd9 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java @@ -87,7 +87,7 @@ private static String getStableOperationName( } private static String getStableWriteOperationName(@Nullable Long batchSize, int writeOpType) { - if (batchSize == null || batchSize == 0) { + if (batchSize == null || batchSize == 0 || writeOpType == WRITE_OP_NONE) { return "BatchWriteItem"; } String itemOp = writeOpType == WRITE_OP_PUT ? "PutItem" : "DeleteItem"; From fc2192604c22f1c5bf4277a52f8f3cb8d4cc3351 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 18 Jun 2026 11:30:59 -0700 Subject: [PATCH 3/5] Address review comment: guard SDK 1 DynamoDB write fallback --- .../awssdk/v1_11/internal/DynamoDbAttributesExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java index b902244a6467..64ff28fd36da 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java @@ -114,7 +114,7 @@ private static String getStableOperationName( } private static String getStableWriteOperationName(@Nullable Long batchSize, int writeOpType) { - if (batchSize == null || batchSize == 0) { + if (batchSize == null || batchSize == 0 || writeOpType == WRITE_OP_NONE) { return "BatchWriteItem"; } String itemOp = writeOpType == WRITE_OP_PUT ? "PutItem" : "DeleteItem"; From 18d76b95e0c0422af119f2c2c527f3241489a542 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 18 Jun 2026 11:33:52 -0700 Subject: [PATCH 4/5] Address review comment: simplify DynamoDB test helper --- .../awssdk/v1_11/AbstractDynamoDbClientTest.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java index 79ef381c4a10..28f0ea2e8839 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java @@ -145,7 +145,7 @@ private static Stream batchScenarios() { .build(), BatchScenario.builder("writeItemEmpty") .awsOperation("BatchWriteItem") - .execute(client -> client.batchWriteItem(writeItemRequest(0))) + .execute(client -> client.batchWriteItem(emptyWriteItemRequest())) .stableOperation("BatchWriteItem") .batchSize(0) .build(), @@ -202,15 +202,8 @@ private static BatchGetItemRequest getItemRequest(int count) { .withRequestItems(singletonMap("sometable", new KeysAndAttributes().withKeys(keys))); } - private static BatchWriteItemRequest writeItemRequest(int count) { - if (count == 0) { - return new BatchWriteItemRequest().withRequestItems(emptyMap()); - } - List writes = new ArrayList<>(); - for (int i = 0; i < count; i++) { - writes.add(putRequest("value" + i)); - } - return new BatchWriteItemRequest().withRequestItems(singletonMap("sometable", writes)); + private static BatchWriteItemRequest emptyWriteItemRequest() { + return new BatchWriteItemRequest().withRequestItems(emptyMap()); } private static BatchWriteItemRequest putItemsRequest(int count) { From a7835edeb8248c8677629ea4420d394b7fa8afb4 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 19 Jun 2026 16:39:21 -0700 Subject: [PATCH 5/5] Address review comment from laurit: use enum for write operation type --- .../internal/DynamoDbAttributesExtractor.java | 56 +++++++++--------- .../internal/DynamoDbAttributesExtractor.java | 58 ++++++++++--------- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java index 64ff28fd36da..57cd2a920d77 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java @@ -38,12 +38,6 @@ class DynamoDbAttributesExtractor implements AttributesExtractor, Res // copied from DbIncubatingAttributes.DbSystemNameIncubatingValues private static final String AWS_DYNAMODB = "aws.dynamodb"; - // write operation type classification - private static final int WRITE_OP_NONE = 0; - private static final int WRITE_OP_PUT = 1; - private static final int WRITE_OP_DELETE = 2; - private static final int WRITE_OP_MIXED = 3; - @Override public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { if (emitStableDatabaseSemconv()) { @@ -55,10 +49,10 @@ public void onStart(AttributesBuilder attributes, Context parentContext, Request String operation = getOperationName(request.getOriginalRequest()); Long batchSize = extractBatchSize(operation, request.getOriginalRequest()); - int writeOpType = + WriteOperationType writeOpType = "BatchWriteItem".equals(operation) ? extractWriteOperationType(request.getOriginalRequest()) - : WRITE_OP_NONE; + : WriteOperationType.NONE; if (emitStableDatabaseSemconv()) { attributes.put(DB_OPERATION_NAME, getStableOperationName(operation, batchSize, writeOpType)); if (shouldEmitBatchSize(batchSize)) { @@ -106,23 +100,24 @@ private static String getSingleCollectionName(Map requestItems) { @Nullable private static String getStableOperationName( - @Nullable String operation, @Nullable Long batchSize, int writeOpType) { + @Nullable String operation, @Nullable Long batchSize, WriteOperationType writeOpType) { if ("BatchWriteItem".equals(operation)) { return getStableWriteOperationName(batchSize, writeOpType); } return operation; } - private static String getStableWriteOperationName(@Nullable Long batchSize, int writeOpType) { - if (batchSize == null || batchSize == 0 || writeOpType == WRITE_OP_NONE) { + private static String getStableWriteOperationName( + @Nullable Long batchSize, WriteOperationType writeOpType) { + if (batchSize == null || batchSize == 0 || writeOpType == WriteOperationType.NONE) { return "BatchWriteItem"; } - String itemOp = writeOpType == WRITE_OP_PUT ? "PutItem" : "DeleteItem"; + String itemOp = writeOpType == WriteOperationType.PUT ? "PutItem" : "DeleteItem"; if (batchSize == 1) { return itemOp; } // mixed operations collapse to bare BATCH (consistent with SQL/Cassandra) - if (writeOpType == WRITE_OP_MIXED) { + if (writeOpType == WriteOperationType.MIXED) { return "BATCH"; } return "BATCH " + itemOp; @@ -155,28 +150,28 @@ private static long countBatchWriteItems(Map requestItems) { } /** - * Extracts the write operation type from a BatchWriteItem request. Returns WRITE_OP_PUT if all - * requests are PutRequests, WRITE_OP_DELETE if all are DeleteRequests, WRITE_OP_MIXED if both - * types are present, or WRITE_OP_NONE if the request is empty or cannot be inspected. + * Extracts the write operation type from a BatchWriteItem request. Returns PUT if all requests + * are PutRequests, DELETE if all are DeleteRequests, MIXED if both types are present, or NONE if + * the request is empty or cannot be inspected. */ - private static int extractWriteOperationType(Object request) { + private static WriteOperationType extractWriteOperationType(Object request) { Map requestItems = RequestAccess.getRequestItems(request); if (requestItems == null) { - return WRITE_OP_NONE; + return WriteOperationType.NONE; } - int result = WRITE_OP_NONE; + WriteOperationType result = WriteOperationType.NONE; for (Object writeRequests : requestItems.values()) { if (writeRequests instanceof Collection) { for (Object writeRequest : (Collection) writeRequests) { - int opType = classifyWriteRequest(writeRequest); - if (opType == WRITE_OP_NONE) { + WriteOperationType opType = classifyWriteRequest(writeRequest); + if (opType == WriteOperationType.NONE) { continue; } - if (result == WRITE_OP_NONE) { + if (result == WriteOperationType.NONE) { result = opType; } else if (result != opType) { - return WRITE_OP_MIXED; + return WriteOperationType.MIXED; } } } @@ -184,15 +179,15 @@ private static int extractWriteOperationType(Object request) { return result; } - private static int classifyWriteRequest(Object writeRequest) { + private static WriteOperationType classifyWriteRequest(Object writeRequest) { // WriteRequest has getPutRequest() and getDeleteRequest() methods; exactly one returns non-null if (RequestAccess.hasPutRequest(writeRequest)) { - return WRITE_OP_PUT; + return WriteOperationType.PUT; } if (RequestAccess.hasDeleteRequest(writeRequest)) { - return WRITE_OP_DELETE; + return WriteOperationType.DELETE; } - return WRITE_OP_NONE; + return WriteOperationType.NONE; } // db.operation.batch.size is captured for every batch request (including an empty batch with @@ -218,4 +213,11 @@ public void onEnd( Request request, @Nullable Response response, @Nullable Throwable error) {} + + private enum WriteOperationType { + NONE, + PUT, + DELETE, + MIXED + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java index 46c8dd423bd9..74740a0ad433 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java @@ -35,12 +35,6 @@ class DynamoDbAttributesExtractor implements AttributesExtractor requestItems) { } /** - * Extracts the write operation type from a BatchWriteItem request. Returns WRITE_OP_PUT if all - * requests are PutRequests, WRITE_OP_DELETE if all are DeleteRequests, WRITE_OP_MIXED if both - * types are present, or WRITE_OP_NONE if the request is empty or cannot be inspected. + * Extracts the write operation type from a BatchWriteItem request. Returns PUT if all requests + * are PutRequests, DELETE if all are DeleteRequests, MIXED if both types are present, or NONE if + * the request is empty or cannot be inspected. */ - private int extractWriteOperationType(ExecutionAttributes executionAttributes) { + private WriteOperationType extractWriteOperationType(ExecutionAttributes executionAttributes) { SdkRequest request = executionAttributes.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE); if (request == null) { - return WRITE_OP_NONE; + return WriteOperationType.NONE; } Optional requestItems = request.getValueForField("RequestItems", Object.class); if (!requestItems.isPresent() || !(requestItems.get() instanceof Map)) { - return WRITE_OP_NONE; + return WriteOperationType.NONE; } - int result = WRITE_OP_NONE; + WriteOperationType result = WriteOperationType.NONE; for (Object writeRequests : ((Map) requestItems.get()).values()) { if (writeRequests instanceof Collection) { for (Object writeRequest : (Collection) writeRequests) { - int opType = classifyWriteRequest(writeRequest); - if (opType == WRITE_OP_NONE) { + WriteOperationType opType = classifyWriteRequest(writeRequest); + if (opType == WriteOperationType.NONE) { continue; } - if (result == WRITE_OP_NONE) { + if (result == WriteOperationType.NONE) { result = opType; } else if (result != opType) { - return WRITE_OP_MIXED; + return WriteOperationType.MIXED; } } } @@ -167,17 +162,17 @@ private int extractWriteOperationType(ExecutionAttributes executionAttributes) { return result; } - private int classifyWriteRequest(Object writeRequest) { + private WriteOperationType classifyWriteRequest(Object writeRequest) { // WriteRequest has putRequest() and deleteRequest() methods; exactly one returns non-null Object putRequest = next(writeRequest, "PutRequest"); if (putRequest != null) { - return WRITE_OP_PUT; + return WriteOperationType.PUT; } Object deleteRequest = next(writeRequest, "DeleteRequest"); if (deleteRequest != null) { - return WRITE_OP_DELETE; + return WriteOperationType.DELETE; } - return WRITE_OP_NONE; + return WriteOperationType.NONE; } // db.operation.batch.size is captured for every batch request (including an empty batch with @@ -239,4 +234,11 @@ public void onEnd( ExecutionAttributes executionAttributes, @Nullable Response response, @Nullable Throwable error) {} + + private enum WriteOperationType { + NONE, + PUT, + DELETE, + MIXED + } }