Skip to content
7 changes: 7 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ endif::[]

=== Unreleased

[[release-notes-1.39.0]]
==== 1.39.0 - YYYY/MM/DD

[float]
===== Features
* Capture S3 operation details as OTel attributes - {pull}3136[#3136]

[[release-notes-1.x]]
=== Java Agent version 1.x

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ public T withSync(boolean sync) {
return thiz();
}



@Nullable
protected static String normalizeEmpty(@Nullable String value) {
return value == null || value.isEmpty() ? null : value;
Expand Down Expand Up @@ -787,6 +789,14 @@ public Map<String, Object> getOtelAttributes() {
return otelAttributes;
}

@Override
public T withOtelAttribute(String key, @Nullable Object value) {
if (value != null) {
otelAttributes.put(key, value);
}
return thiz();
}

@Override
@Nullable
public String getType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1519,10 +1519,10 @@ private <T extends AbstractSpan<T>> void testOTelSpanSerialization(T context, Fu
}

// with custom otel attributes
context.getOtelAttributes().put("attribute.string", "hello");
context.getOtelAttributes().put("attribute.long", 123L);
context.getOtelAttributes().put("attribute.boolean", false);
context.getOtelAttributes().put("attribute.float", 0.42f);
context.withOtelAttribute("attribute.string", "hello");
context.withOtelAttribute("attribute.long", 123L);
context.withOtelAttribute("attribute.boolean", false);
context.withOtelAttribute("attribute.float", 0.42f);
spanJson = toJson.apply(context);
JsonNode otelJson = spanJson.get("otel");
assertThat(otelJson).isNotNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import co.elastic.apm.agent.impl.transaction.AbstractSpan;
import co.elastic.apm.agent.impl.transaction.TraceContext;
import co.elastic.apm.agent.tracer.Outcome;

import java.util.Objects;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -89,6 +90,17 @@ public SELF hasSpanLink(AbstractSpan<?> expectedLink) {
return thiz();
}

public SELF hasOutcome(Outcome expectedOutcome) {
checkObject("Expected span to have outcome %, but was %s", expectedOutcome, actual.getOutcome());
return thiz();
}

public SELF hasOtelAttribute(String key, Object expectedValue) {
isNotNull();
checkObject("Expected span to have value '%s' for attribute '" + key + "' but was '%s' ",expectedValue, actual.getOtelAttributes().get(key));
return thiz();
}

private boolean checkSpanLinksContain(TraceContext expectedLink) {
return actual.getSpanLinks().stream()
.anyMatch(ctx -> Objects.equals(ctx.getParentId(), expectedLink.getId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.amazonaws.Request;
import com.amazonaws.http.ExecutionContext;

import javax.annotation.Nullable;

public class S3Helper extends AbstractS3InstrumentationHelper<Request<?>, ExecutionContext> {

private static final S3Helper INSTANCE = new S3Helper(GlobalTracer.get());
Expand All @@ -35,4 +37,36 @@ public static S3Helper getInstance() {
public S3Helper(Tracer tracer) {
super(tracer, SdkV1DataSource.getInstance());
}

@Nullable
@Override
protected String getObjectKey(Request<?> request, @Nullable String bucketName) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] with SDK v1 we have to use some manual parsing of an HTTP header, this is not the case with v2.

if (bucketName == null) {
return null;
}
String resourcePath = request.getResourcePath();
if (!resourcePath.startsWith(bucketName)) {
return null;
}
int start = bucketName.length();
if (start < resourcePath.length() && resourcePath.charAt(start) == '/') {
start++;
}
int end = resourcePath.length();
if (start < end) {
return resourcePath.substring(start, end);
}
return null;
}

@Nullable
@Override
protected String getCopySource(Request<?> request) {
String header = request.getHeaders().get("x-amz-copy-source");
if (header != null && header.startsWith("/")) {
header = header.substring(1);
}
return header;

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package co.elastic.apm.agent.awssdk.v1;

import co.elastic.apm.agent.awssdk.common.AbstractAwsClientIT;
import co.elastic.apm.agent.impl.transaction.AbstractSpan;
import co.elastic.apm.agent.impl.transaction.Span;
import co.elastic.apm.agent.impl.transaction.Transaction;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
Expand Down Expand Up @@ -75,40 +74,63 @@ public void setupClient() {
public void testDynamoDbClient() {
Transaction transaction = startTestRootTransaction("s3-test");

executeTest("CreateTable", "query", TABLE_NAME, () -> dynamoDB.createTable(new CreateTableRequest().withTableName(TABLE_NAME)
.withAttributeDefinitions(List.of(
new AttributeDefinition("attributeOne", ScalarAttributeType.S),
new AttributeDefinition("attributeTwo", ScalarAttributeType.N)
))
.withKeySchema(List.of(
new KeySchemaElement("attributeOne", KeyType.HASH),
new KeySchemaElement("attributeTwo", KeyType.RANGE)
))
.withBillingMode(BillingMode.PAY_PER_REQUEST)),
dbAssert);

executeTest("ListTables", "query", null, () -> dynamoDB.listTables(),
dbAssert);

executeTest("PutItem", "query", TABLE_NAME, () -> dynamoDB.putItem(
newTest(() -> dynamoDB.createTable(new CreateTableRequest().withTableName(TABLE_NAME)
.withAttributeDefinitions(List.of(
new AttributeDefinition("attributeOne", ScalarAttributeType.S),
new AttributeDefinition("attributeTwo", ScalarAttributeType.N)
))
.withKeySchema(List.of(
new KeySchemaElement("attributeOne", KeyType.HASH),
new KeySchemaElement("attributeTwo", KeyType.RANGE)
))
.withBillingMode(BillingMode.PAY_PER_REQUEST)))
.operationName("CreateTable")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.execute();
Comment on lines +77 to +91
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] this is mostly automatic refactoring with a "test builder" that allows to combine assertions on the spans in a more declarative way.


newTest(() -> dynamoDB.listTables())
.operationName("ListTables")
.action("query")
.withSpanAssertions(dbAssert)
.execute();

newTest(() -> dynamoDB.putItem(
new PutItemRequest(TABLE_NAME,
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))),
dbAssert);

executeTest("Query", "query", TABLE_NAME, () -> dynamoDB.query(
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))))
.operationName("PutItem")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.execute();

newTest(() -> dynamoDB.query(
new QueryRequest(TABLE_NAME)
.withKeyConditionExpression(KEY_CONDITION_EXPRESSION)
.withExpressionAttributeValues(Map.of(":one", new AttributeValue("valueOne")))),
dbAssert
.andThen(span -> assertThat(span.getContext().getDb()).hasStatement(KEY_CONDITION_EXPRESSION)));

executeTest("DeleteTable", "query", TABLE_NAME, () -> dynamoDB.deleteTable(TABLE_NAME),
dbAssert);

executeTestWithException(ResourceNotFoundException.class, "PutItem", "query", TABLE_NAME + "-exception", () -> dynamoDB.putItem(
.withExpressionAttributeValues(Map.of(":one", new AttributeValue("valueOne")))))
.operationName("Query")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert
.andThen(span -> assertThat(span.getContext().getDb()).hasStatement(KEY_CONDITION_EXPRESSION)))
.execute();

newTest(() -> dynamoDB.deleteTable(TABLE_NAME))
.operationName("DeleteTable")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.execute();

newTest(() -> dynamoDB.putItem(
new PutItemRequest(TABLE_NAME + "-exception",
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))),
dbAssert);
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))))
.operationName("PutItem")
.action("query")
.entityName(TABLE_NAME + "-exception")
.withSpanAssertions(dbAssert)
.executeWithException(ResourceNotFoundException.class);

assertThat(reporter.getSpans().size()).isEqualTo(6);

Expand All @@ -122,41 +144,64 @@ public void testDynamoDbClient() {
public void testDynamoDbClientAsync() {
Transaction transaction = startTestRootTransaction("s3-test");

executeTest("CreateTable", "query", TABLE_NAME, () -> dynamoDBAsync.createTableAsync(new CreateTableRequest().withTableName(TABLE_NAME)
.withAttributeDefinitions(List.of(
new AttributeDefinition("attributeOne", ScalarAttributeType.S),
new AttributeDefinition("attributeTwo", ScalarAttributeType.N)
))
.withKeySchema(List.of(
new KeySchemaElement("attributeOne", KeyType.HASH),
new KeySchemaElement("attributeTwo", KeyType.RANGE)
))
.withBillingMode(BillingMode.PAY_PER_REQUEST)),
dbAssert);

executeTest("ListTables", "query", null, () -> dynamoDBAsync.listTablesAsync(),
dbAssert);

executeTest("PutItem", "query", TABLE_NAME, () -> dynamoDBAsync.putItemAsync(
newTest(() -> dynamoDBAsync.createTableAsync(new CreateTableRequest().withTableName(TABLE_NAME)
.withAttributeDefinitions(List.of(
new AttributeDefinition("attributeOne", ScalarAttributeType.S),
new AttributeDefinition("attributeTwo", ScalarAttributeType.N)
))
.withKeySchema(List.of(
new KeySchemaElement("attributeOne", KeyType.HASH),
new KeySchemaElement("attributeTwo", KeyType.RANGE)
))
.withBillingMode(BillingMode.PAY_PER_REQUEST)))
.operationName("CreateTable")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.async()
.execute();

newTest(() -> dynamoDBAsync.listTablesAsync())
.operationName("ListTables")
.action("query")
.withSpanAssertions(dbAssert)
.async()
.execute();

newTest(() -> dynamoDBAsync.putItemAsync(
new PutItemRequest(TABLE_NAME,
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))),
dbAssert);

executeTest("Query", "query", TABLE_NAME, () -> dynamoDBAsync.queryAsync(
Map.of("attributeOne", new AttributeValue("valueOne"), "attributeTwo", new AttributeValue().withN("10")))))
.operationName("PutItem")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.async()
.execute();

newTest(() -> dynamoDBAsync.queryAsync(
new QueryRequest(TABLE_NAME)
.withKeyConditionExpression(KEY_CONDITION_EXPRESSION)
.withExpressionAttributeValues(Map.of(":one", new AttributeValue("valueOne")))),
dbAssert
.andThen(span -> assertThat(span.getContext().getDb()).hasStatement(KEY_CONDITION_EXPRESSION)));

executeTest("DeleteTable", "query", TABLE_NAME, () -> dynamoDBAsync.deleteTableAsync(TABLE_NAME),
dbAssert);
.withExpressionAttributeValues(Map.of(":one", new AttributeValue("valueOne")))))
.operationName("Query")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert
.andThen(span ->
assertThat(span.getContext().getDb()).hasStatement(KEY_CONDITION_EXPRESSION)))
.async()
.execute();

newTest(() -> dynamoDBAsync.deleteTableAsync(TABLE_NAME))
.operationName("DeleteTable")
.entityName(TABLE_NAME)
.action("query")
.withSpanAssertions(dbAssert)
.async()
.execute();

assertThat(reporter.getSpans().size()).isEqualTo(5);

transaction.deactivate().end();

assertThat(reporter.getSpans()).noneMatch(AbstractSpan::isSync);
}

@Override
Expand Down
Loading