Skip to content

Commit a5e6882

Browse files
chloslaurit
andauthored
Add db.collection.name to DynamoDB spans under stable database semconv (#18853)
Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
1 parent addcf1e commit a5e6882

5 files changed

Lines changed: 143 additions & 21 deletions

File tree

instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/internal/DynamoDbAttributesExtractor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldDatabaseSemconv;
99
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv;
10+
import static io.opentelemetry.semconv.DbAttributes.DB_COLLECTION_NAME;
1011
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
1112
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
1213
import static java.util.Collections.singletonList;
@@ -55,6 +56,9 @@ public void onStart(AttributesBuilder attributes, Context parentContext, Request
5556
if (tableName != null) {
5657
attributes.put(AWS_DYNAMODB_TABLE_NAMES, singletonList(tableName));
5758
}
59+
if (emitStableDatabaseSemconv() && tableName != null) {
60+
attributes.put(DB_COLLECTION_NAME, tableName);
61+
}
5862
}
5963

6064
@Nullable

instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric;
1010
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
1111
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
12+
import static io.opentelemetry.semconv.DbAttributes.DB_COLLECTION_NAME;
1213
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
1314
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
1415
import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
@@ -28,6 +29,7 @@
2829
import io.opentelemetry.testing.internal.armeria.common.HttpResponse;
2930
import io.opentelemetry.testing.internal.armeria.common.HttpStatus;
3031
import io.opentelemetry.testing.internal.armeria.common.MediaType;
32+
import java.util.ArrayList;
3133
import java.util.List;
3234
import org.junit.jupiter.api.Test;
3335

@@ -53,10 +55,15 @@ void sendRequestWithMockedResponse() throws ReflectiveOperationException {
5355
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, ""));
5456

5557
List<AttributeAssertion> additionalAttributes =
56-
asList(
57-
equalTo(maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB),
58-
equalTo(maybeStable(DB_OPERATION), "CreateTable"),
59-
equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable")));
58+
new ArrayList<>(
59+
asList(
60+
equalTo(
61+
maybeStable(DB_SYSTEM), emitStableDatabaseSemconv() ? AWS_DYNAMODB : DYNAMODB),
62+
equalTo(maybeStable(DB_OPERATION), "CreateTable"),
63+
equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable"))));
64+
if (emitStableDatabaseSemconv()) {
65+
additionalAttributes.add(equalTo(DB_COLLECTION_NAME, "sometable"));
66+
}
6067

6168
Object response = client.createTable(new CreateTableRequest("sometable", null));
6269
assertRequestWithMockedResponse(
@@ -67,6 +74,7 @@ void sendRequestWithMockedResponse() throws ReflectiveOperationException {
6774
"io.opentelemetry.aws-sdk-1.11",
6875
DB_SYSTEM_NAME,
6976
DB_OPERATION_NAME,
77+
DB_COLLECTION_NAME,
7078
SERVER_ADDRESS,
7179
SERVER_PORT);
7280
}

instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DynamoDbAttributesExtractor.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77

88
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldDatabaseSemconv;
99
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv;
10+
import static io.opentelemetry.semconv.DbAttributes.DB_COLLECTION_NAME;
1011
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
1112
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
1213

1314
import io.opentelemetry.api.common.AttributeKey;
1415
import io.opentelemetry.api.common.AttributesBuilder;
1516
import io.opentelemetry.context.Context;
1617
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.Set;
1721
import javax.annotation.Nullable;
22+
import software.amazon.awssdk.core.SdkRequest;
1823
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
1924
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
2025

@@ -47,6 +52,39 @@ public void onStart(
4752
if (emitOldDatabaseSemconv()) {
4853
attributes.put(DB_OPERATION, operation);
4954
}
55+
if (emitStableDatabaseSemconv()) {
56+
String tableName = extractTableName(executionAttributes);
57+
if (tableName != null) {
58+
attributes.put(DB_COLLECTION_NAME, tableName);
59+
}
60+
}
61+
}
62+
63+
@Nullable
64+
private static String extractTableName(ExecutionAttributes executionAttributes) {
65+
SdkRequest request =
66+
executionAttributes.getAttribute(TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE);
67+
if (request == null) {
68+
return null;
69+
}
70+
// Single-table operations expose TableName directly.
71+
Optional<String> tableName = request.getValueForField("TableName", String.class);
72+
if (tableName.isPresent() && !tableName.get().isEmpty()) {
73+
return tableName.get();
74+
}
75+
// Batch operations (BatchGetItem, BatchWriteItem) key RequestItems by table name.
76+
// Emit db.collection.name only when the batch targets exactly one table; omit it otherwise,
77+
// as the attribute is defined as a single collection identifier.
78+
Optional<?> requestItems = request.getValueForField("RequestItems", Object.class);
79+
if (requestItems.isPresent() && requestItems.get() instanceof Map) {
80+
// The instanceof check above guarantees Map, only the generic parameters are unchecked.
81+
@SuppressWarnings("unchecked")
82+
Set<String> tables = ((Map<String, ?>) requestItems.get()).keySet();
83+
if (tables.size() == 1) {
84+
return tables.iterator().next();
85+
}
86+
}
87+
return null;
5088
}
5189

5290
@Override

instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
package io.opentelemetry.instrumentation.awssdk.v2_2;
77

8+
import static io.opentelemetry.api.common.AttributeKey.stringArrayKey;
89
import static io.opentelemetry.api.common.AttributeKey.stringKey;
10+
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv;
911
import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric;
1012
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
1113
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName;
1214
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
15+
import static io.opentelemetry.semconv.DbAttributes.DB_COLLECTION_NAME;
1316
import static io.opentelemetry.semconv.DbAttributes.DB_OPERATION_NAME;
1417
import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME;
1518
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
@@ -60,6 +63,7 @@
6063
import org.junit.jupiter.api.AfterAll;
6164
import org.junit.jupiter.api.BeforeAll;
6265
import org.junit.jupiter.api.BeforeEach;
66+
import org.junit.jupiter.api.Test;
6367
import org.junit.jupiter.params.ParameterizedTest;
6468
import org.junit.jupiter.params.provider.Arguments;
6569
import org.junit.jupiter.params.provider.MethodSource;
@@ -223,8 +227,17 @@ private void validateOperationResponse(String operation, Object response) {
223227
}
224228
}));
225229

226-
assertDurationMetric(
227-
getTesting(), "io.opentelemetry.aws-sdk-2.2", DB_SYSTEM_NAME, DB_OPERATION_NAME);
230+
if ("ListTables".equals(operation)) {
231+
assertDurationMetric(
232+
getTesting(), "io.opentelemetry.aws-sdk-2.2", DB_SYSTEM_NAME, DB_OPERATION_NAME);
233+
} else {
234+
assertDurationMetric(
235+
getTesting(),
236+
"io.opentelemetry.aws-sdk-2.2",
237+
DB_SYSTEM_NAME,
238+
DB_OPERATION_NAME,
239+
DB_COLLECTION_NAME);
240+
}
228241
}
229242

230243
private static CreateTableRequest createTableRequest() {
@@ -296,6 +309,9 @@ private static void assertDynamoDbRequest(
296309
equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable")),
297310
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(DYNAMODB)),
298311
equalTo(maybeStable(DB_OPERATION), operation)));
312+
if (emitStableDatabaseSemconv()) {
313+
assertions.add(equalTo(DB_COLLECTION_NAME, "sometable"));
314+
}
299315
assertions.addAll(extraAttributes);
300316
span.hasName("DynamoDb." + operation)
301317
.hasKind(SpanKind.CLIENT)
@@ -493,6 +509,50 @@ void testSendDynamoDbAsyncRequestWithBuilderAndMockedResponse(
493509
validateOperationResponse(operation, response);
494510
}
495511

512+
@Test
513+
@SuppressWarnings("deprecation") // uses deprecated semconv
514+
void testBatchGetItemWithMultipleTablesOmitsDbCollectionName() {
515+
DynamoDbClientBuilder builder = DynamoDbClient.builder();
516+
configureSdkClient(builder);
517+
DynamoDbClient client =
518+
builder
519+
.endpointOverride(server.httpUri())
520+
.region(Region.AP_NORTHEAST_1)
521+
.credentialsProvider(CREDENTIALS_PROVIDER)
522+
.build();
523+
server.enqueue(
524+
HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "{\"ConsumedCapacity\":[]}"));
525+
526+
client.batchGetItem(
527+
b ->
528+
b.requestItems(
529+
ImmutableMap.of(
530+
"table1",
531+
KeysAndAttributes.builder()
532+
.keys(
533+
singletonList(
534+
ImmutableMap.of("key", AttributeValue.builder().s("v1").build())))
535+
.build(),
536+
"table2",
537+
KeysAndAttributes.builder()
538+
.keys(
539+
singletonList(
540+
ImmutableMap.of("key", AttributeValue.builder().s("v2").build())))
541+
.build())));
542+
543+
getTesting()
544+
.waitAndAssertTraces(
545+
trace ->
546+
trace.hasSpansSatisfyingExactly(
547+
span ->
548+
span.hasName("DynamoDb.BatchGetItem")
549+
.satisfies(
550+
s ->
551+
assertThat(s.getAttributes().asMap())
552+
.containsKey(stringArrayKey("aws.dynamodb.table_names"))
553+
.doesNotContainKey(DB_COLLECTION_NAME))));
554+
}
555+
496556
private static String getResponseContent(String operation) {
497557
switch (operation) {
498558
case "ListTables":

instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
package io.opentelemetry.instrumentation.awssdk.v2_2;
77

88
import static io.opentelemetry.api.common.AttributeKey.stringKey;
9+
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv;
910
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
1011
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName;
1112
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
1213
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
14+
import static io.opentelemetry.semconv.DbAttributes.DB_COLLECTION_NAME;
1315
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
1416
import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
1517
import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
@@ -22,11 +24,13 @@
2224
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD;
2325
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE;
2426
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM;
27+
import static java.util.Arrays.asList;
2528
import static java.util.Collections.singletonList;
2629
import static java.util.stream.Collectors.joining;
2730

2831
import io.opentelemetry.api.trace.SpanKind;
2932
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
33+
import io.opentelemetry.sdk.testing.assertj.AttributeAssertion;
3034
import io.opentelemetry.testing.internal.armeria.common.HttpResponse;
3135
import io.opentelemetry.testing.internal.armeria.common.HttpStatus;
3236
import io.opentelemetry.testing.internal.armeria.common.MediaType;
@@ -174,21 +178,29 @@ void testSendDynamoDbRequestWithRetries() {
174178
span -> {
175179
span.hasKind(SpanKind.CLIENT);
176180
span.hasNoParent();
177-
span.hasAttributesSatisfyingExactly(
178-
equalTo(SERVER_ADDRESS, "127.0.0.1"),
179-
equalTo(SERVER_PORT, server.httpPort()),
180-
equalTo(HTTP_REQUEST_METHOD, method),
181-
equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
182-
equalTo(
183-
stringKey("url.full"), "http://127.0.0.1:" + server.httpPort() + "/"),
184-
equalTo(RPC_SYSTEM, "aws-api"),
185-
equalTo(RPC_SERVICE, service),
186-
equalTo(RPC_METHOD, operation),
187-
equalTo(stringKey("aws.agent"), "java-aws-sdk"),
188-
equalTo(AWS_REQUEST_ID, requestId),
189-
equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable")),
190-
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(DYNAMODB)),
191-
equalTo(maybeStable(DB_OPERATION), operation));
181+
List<AttributeAssertion> attrs =
182+
new ArrayList<>(
183+
asList(
184+
equalTo(SERVER_ADDRESS, "127.0.0.1"),
185+
equalTo(SERVER_PORT, server.httpPort()),
186+
equalTo(HTTP_REQUEST_METHOD, method),
187+
equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
188+
equalTo(
189+
stringKey("url.full"),
190+
"http://127.0.0.1:" + server.httpPort() + "/"),
191+
equalTo(RPC_SYSTEM, "aws-api"),
192+
equalTo(RPC_SERVICE, service),
193+
equalTo(RPC_METHOD, operation),
194+
equalTo(stringKey("aws.agent"), "java-aws-sdk"),
195+
equalTo(AWS_REQUEST_ID, requestId),
196+
equalTo(AWS_DYNAMODB_TABLE_NAMES, singletonList("sometable")),
197+
equalTo(
198+
maybeStable(DB_SYSTEM), maybeStableDbSystemName(DYNAMODB)),
199+
equalTo(maybeStable(DB_OPERATION), operation)));
200+
if (emitStableDatabaseSemconv()) {
201+
attrs.add(equalTo(DB_COLLECTION_NAME, "sometable"));
202+
}
203+
span.hasAttributesSatisfyingExactly(attrs);
192204
if (isRecordIndividualHttpErrorEnabled()) {
193205
span.hasEventsSatisfyingExactly(
194206
event ->

0 commit comments

Comments
 (0)