From c00d3af817b692e0d8fc5a5ec54ae0645d7088a7 Mon Sep 17 00:00:00 2001 From: Michael He <53622546+yiyuan-he@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:25:43 -0800 Subject: [PATCH 1/2] Add Contract Tests for SecretsManager, StepFunctions, and SNS (#958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Description of changes:* Adding contract tests for new AWS resources. Rebasing the commits in this [PR](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/936/commits/8295fb06341a29e8da4f52bf8b9130f39c2c9bc8) since there were some merge conflicts with Gen AI contract tests. *Test plan:* Screenshot 2024-11-25 at 9 16 39 PM By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../test/awssdk/base/AwsSdkBaseTest.java | 868 +++++++++++++++++- .../test/awssdk/v1/AwsSdkV1Test.java | 80 ++ .../test/awssdk/v2/AwsSdkV2Test.java | 82 +- .../test/utils/AppSignalsConstants.java | 2 + .../utils/SemanticConventionsConstants.java | 5 + .../aws-sdk/aws-sdk-v1/build.gradle.kts | 4 + .../main/java/com/amazon/sampleapp/App.java | 387 ++++++++ .../aws-sdk/aws-sdk-v2/build.gradle.kts | 4 + .../main/java/com/amazon/sampleapp/App.java | 385 ++++++++ 9 files changed, 1810 insertions(+), 7 deletions(-) diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java index 0f8652fb70..278aa4011d 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java @@ -42,7 +42,11 @@ public abstract class AwsSdkBaseTest extends ContractTestBase { LocalStackContainer.Service.S3, LocalStackContainer.Service.DYNAMODB, LocalStackContainer.Service.SQS, - LocalStackContainer.Service.KINESIS) + LocalStackContainer.Service.KINESIS, + LocalStackContainer.Service.SECRETSMANAGER, + LocalStackContainer.Service.IAM, + LocalStackContainer.Service.STEPFUNCTIONS, + LocalStackContainer.Service.SNS) .withEnv("DEFAULT_REGION", "us-west-2") .withNetwork(network) .withEnv("LOCALSTACK_HOST", "127.0.0.1") @@ -102,6 +106,12 @@ protected String getApplicationWaitPattern() { protected abstract String getBedrockAgentRuntimeSpanNamePrefix(); + protected abstract String getSecretsManagerSpanNamePrefix(); + + protected abstract String getStepFunctionsSpanNamePrefix(); + + protected abstract String getSnsSpanNamePrefix(); + protected abstract String getS3RpcServiceName(); protected abstract String getDynamoDbRpcServiceName(); @@ -118,6 +128,12 @@ protected String getApplicationWaitPattern() { protected abstract String getBedrockAgentRuntimeRpcServiceName(); + protected abstract String getSecretsManagerRpcServiceName(); + + protected abstract String getSnsRpcServiceName(); + + protected abstract String getStepFunctionsRpcServiceName(); + private String getS3ServiceName() { return "AWS::S3"; } @@ -150,6 +166,18 @@ private String getBedrockRuntimeServiceName() { return "AWS::BedrockRuntime"; } + private String getSecretsManagerServiceName() { + return "AWS::SecretsManager"; + } + + private String getStepFunctionsServiceName() { + return "AWS::StepFunctions"; + } + + protected String getSnsServiceName() { + return "AWS::SNS"; + } + private String s3SpanName(String operation) { return String.format("%s.%s", getS3SpanNamePrefix(), operation); } @@ -182,10 +210,31 @@ private String bedrockAgentRuntimeSpanName(String operation) { return String.format("%s.%s", getBedrockAgentRuntimeSpanNamePrefix(), operation); } + private String secretsManagerSpanName(String operation) { + return String.format("%s.%s", getSecretsManagerSpanNamePrefix(), operation); + } + + private String stepFunctionsSpanName(String operation) { + return String.format("%s.%s", getStepFunctionsSpanNamePrefix(), operation); + } + + private String snsSpanName(String operation) { + return String.format("%s.%s", getSnsSpanNamePrefix(), operation); + } + protected ThrowingConsumer assertAttribute(String key, String value) { return (attribute) -> { - assertThat(attribute.getKey()).isEqualTo(key); - assertThat(attribute.getValue().getStringValue()).isEqualTo(value); + var actualKey = attribute.getKey(); + var actualValue = attribute.getValue().getStringValue(); + + assertThat(actualKey).isEqualTo(key); + + // We only want to Regex Pattern Match on the Secret Id and Secret Arn + if (actualValue.contains("secret-id")) { + assertThat(actualValue).matches(value); + } else { + assertThat(actualValue).isEqualTo(value); + } }; } @@ -258,6 +307,7 @@ private void assertSpanClientAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -276,6 +326,7 @@ private void assertSpanClientAttributes( method, type, identifier, + cloudformationIdentifier, peerName, peerPort, url, @@ -293,6 +344,7 @@ private void assertSpanProducerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -310,6 +362,7 @@ private void assertSpanProducerAttributes( method, type, identifier, + cloudformationIdentifier, peerName, peerPort, url, @@ -358,6 +411,7 @@ private void assertSpanAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -371,6 +425,7 @@ private void assertSpanAttributes( var spanAttributes = span.getAttributesList(); assertThat(span.getKind()).isEqualTo(spanKind); assertThat(span.getName()).isEqualTo(spanName); + assertSemanticConventionsAttributes( spanAttributes, rpcService, method, peerName, peerPort, url, statusCode); assertAwsAttributes( @@ -381,6 +436,7 @@ private void assertSpanAttributes( method, type, identifier, + cloudformationIdentifier, awsSpanKind); for (var assertion : extraAssertions) { assertThat(spanAttributes).satisfiesOnlyOnce(assertion); @@ -396,6 +452,7 @@ private void assertAwsAttributes( String operation, String type, String identifier, + String clouformationIdentifier, String spanKind) { var assertions = @@ -406,11 +463,14 @@ private void assertAwsAttributes( .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_REMOTE_OPERATION, operation)) .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_REMOTE_SERVICE, service)) .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_SPAN_KIND, spanKind)); - if (type != null && identifier != null) { + if (type != null && identifier != null && clouformationIdentifier != null) { assertions.satisfiesOnlyOnce( assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_TYPE, type)); assertions.satisfiesOnlyOnce( assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_IDENTIFIER, identifier)); + assertions.satisfiesOnlyOnce( + assertAttribute( + AppSignalsConstants.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, clouformationIdentifier)); } } @@ -435,6 +495,7 @@ protected void assertMetricClientAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -446,6 +507,7 @@ protected void assertMetricClientAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -458,6 +520,7 @@ protected void assertMetricProducerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -469,6 +532,7 @@ protected void assertMetricProducerAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -481,6 +545,7 @@ protected void assertMetricConsumerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -492,6 +557,7 @@ protected void assertMetricConsumerAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -505,6 +571,7 @@ protected void assertMetricAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertThat(resourceScopeMetrics) .anySatisfy( @@ -524,6 +591,7 @@ protected void assertMetricAttributes( method, type, identifier, + cloudformationIdentifier, spanKind); if (expectedSum != null) { double actualSum = dataPoint.getSum(); @@ -554,6 +622,7 @@ protected void doTestS3CreateBucket() throws Exception { var localOperation = "GET /s3/createbucket/:bucketname"; var type = "AWS::S3::Bucket"; var identifier = "create-bucket"; + var cloudformationIdentifier = "create-bucket"; assertSpanClientAttributes( traces, @@ -565,6 +634,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, "create-bucket.s3.localstack", 4566, "http://create-bucket.s3.localstack:4566", @@ -579,6 +649,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -589,6 +660,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -599,6 +671,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 0.0); } @@ -617,6 +690,7 @@ protected void doTestS3CreateObject() throws Exception { var localOperation = "GET /s3/createobject/:bucketname/:objectname"; var type = "AWS::S3::Bucket"; var identifier = "put-object"; + var cloudformationIdentifier = "put-object"; assertSpanClientAttributes( traces, @@ -628,6 +702,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, "put-object.s3.localstack", 4566, "http://put-object.s3.localstack:4566", @@ -642,6 +717,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -652,6 +728,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -662,6 +739,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -679,6 +757,7 @@ protected void doTestS3GetObject() throws Exception { var localOperation = "GET /s3/getobject/:bucketName/:objectname"; var type = "AWS::S3::Bucket"; var identifier = "get-object"; + var cloudformationIdentifier = "get-object"; assertSpanClientAttributes( traces, @@ -690,6 +769,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, "get-object.s3.localstack", 4566, "http://get-object.s3.localstack:4566", @@ -704,6 +784,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -714,6 +795,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -724,6 +806,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -741,6 +824,7 @@ protected void doTestS3Error() { var localOperation = "GET /s3/error"; var type = "AWS::S3::Bucket"; var identifier = "error-bucket"; + var cloudformationIdentifier = "error-bucket"; assertSpanClientAttributes( traces, @@ -752,6 +836,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, "error-bucket.s3.test", 8080, "http://error-bucket.s3.test:8080", @@ -766,6 +851,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -776,6 +862,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -786,6 +873,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 1.0); } @@ -803,6 +891,7 @@ protected void doTestS3Fault() { var localOperation = "GET /s3/fault"; var type = "AWS::S3::Bucket"; var identifier = "fault-bucket"; + var cloudformationIdentifier = "fault-bucket"; assertSpanClientAttributes( traces, @@ -814,6 +903,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, "fault-bucket.s3.test", 8080, "http://fault-bucket.s3.test:8080", @@ -828,6 +918,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -838,6 +929,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -848,6 +940,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -873,6 +966,7 @@ protected void doTestDynamoDbCreateTable() { var localOperation = "GET /ddb/createtable/:tablename"; var type = "AWS::DynamoDB::Table"; var identifier = "some-table"; + var cloudformationIdentifier = "some-table"; assertSpanClientAttributes( traces, @@ -884,6 +978,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -898,6 +993,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 20000.0); assertMetricClientAttributes( metrics, @@ -908,6 +1004,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -918,6 +1015,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 0.0); } @@ -935,6 +1033,7 @@ protected void doTestDynamoDbPutItem() { var localOperation = "GET /ddb/putitem/:tablename/:partitionkey"; var type = "AWS::DynamoDB::Table"; var identifier = "putitem-table"; + var cloudformationIdentifier = "putitem-table"; assertSpanClientAttributes( traces, @@ -946,6 +1045,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -960,6 +1060,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -970,6 +1071,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -980,6 +1082,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); } @@ -997,6 +1100,7 @@ protected void doTestDynamoDbError() throws Exception { var localOperation = "GET /ddb/error"; var type = "AWS::DynamoDB::Table"; var identifier = "nonexistanttable"; + var cloudformationIdentifier = "nonexistanttable"; assertSpanClientAttributes( traces, @@ -1008,6 +1112,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1022,6 +1127,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1032,6 +1138,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1042,6 +1149,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1065,6 +1173,7 @@ protected void doTestDynamoDbFault() throws Exception { var localOperation = "GET /ddb/fault"; var type = "AWS::DynamoDB::Table"; var identifier = "nonexistanttable"; + var cloudformationIdentifier = "nonexistanttable"; assertSpanClientAttributes( traces, @@ -1076,6 +1185,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1090,6 +1200,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 20000.0); assertMetricClientAttributes( metrics, @@ -1100,6 +1211,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -1110,6 +1222,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1127,6 +1240,7 @@ protected void doTestSQSCreateQueue() throws Exception { var localOperation = "GET /sqs/createqueue/:queuename"; var type = "AWS::SQS::Queue"; var identifier = "some-queue"; + var cloudformationIdentifier = "some-queue"; assertSpanClientAttributes( traces, @@ -1138,6 +1252,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1152,6 +1267,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1162,6 +1278,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1172,6 +1289,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1190,6 +1308,7 @@ protected void doTestSQSSendMessage() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1201,6 +1320,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1216,6 +1336,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1226,6 +1347,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); assertMetricProducerAttributes( metrics, @@ -1236,6 +1358,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1258,6 +1381,7 @@ protected void doTestSQSReceiveMessage() throws Exception { // ReceiveMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; // Consumer traces for SQS behave like a Server span (they create the local aws service // attributes), but have RPC attributes like a client span. assertSpanConsumerAttributes( @@ -1282,6 +1406,7 @@ protected void doTestSQSReceiveMessage() throws Exception { "ReceiveMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricConsumerAttributes( metrics, @@ -1292,6 +1417,7 @@ protected void doTestSQSReceiveMessage() throws Exception { "ReceiveMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1310,6 +1436,7 @@ protected void doTestSQSError() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1321,6 +1448,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1336,6 +1464,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1346,6 +1475,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); assertMetricProducerAttributes( metrics, @@ -1356,6 +1486,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1374,6 +1505,7 @@ protected void doTestSQSFault() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1385,6 +1517,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1400,6 +1533,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1410,6 +1544,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 1.0); assertMetricProducerAttributes( metrics, @@ -1420,6 +1555,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1437,6 +1573,7 @@ protected void doTestKinesisPutRecord() throws Exception { var localOperation = "GET /kinesis/putrecord/:streamname"; var type = "AWS::Kinesis::Stream"; var identifier = "my-stream"; + var cloudformationIdentifier = "my-stream"; assertSpanClientAttributes( traces, @@ -1448,6 +1585,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1462,6 +1600,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1472,6 +1611,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1482,6 +1622,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1499,6 +1640,7 @@ protected void doTestKinesisError() throws Exception { var localOperation = "GET /kinesis/error"; var type = "AWS::Kinesis::Stream"; var identifier = "nonexistantstream"; + var cloudformationIdentifier = "nonexistantstream"; assertSpanClientAttributes( traces, @@ -1510,6 +1652,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1525,6 +1668,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1535,6 +1679,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1545,6 +1690,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1562,6 +1708,7 @@ protected void doTestKinesisFault() throws Exception { var localOperation = "GET /kinesis/fault"; var type = "AWS::Kinesis::Stream"; var identifier = "faultstream"; + var cloudformationIdentifier = "faultstream"; assertSpanClientAttributes( traces, @@ -1573,6 +1720,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1587,6 +1735,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1597,6 +1746,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -1607,6 +1757,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1625,6 +1776,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { var localOperation = "GET /bedrockagent/getknowledgeBase/:knowledgeBaseId"; String type = "AWS::Bedrock::KnowledgeBase"; String identifier = "knowledge-base-id"; + String cloudformationIdentifier = "knowledge-base-id"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetKnowledgeBase"), @@ -1635,6 +1787,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1651,6 +1804,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1661,6 +1815,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1671,6 +1826,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1688,6 +1844,7 @@ protected void doTestBedrockAgentAgentId() { var localOperation = "GET /bedrockagent/getagent/:agentId"; String type = "AWS::Bedrock::Agent"; String identifier = "test-agent-id"; + String cloudformationIdentifier = "test-agent-id"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetAgent"), @@ -1698,6 +1855,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1712,6 +1870,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1722,6 +1881,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1732,6 +1892,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1749,6 +1910,7 @@ protected void doTestBedrockAgentDataSourceId() { var localOperation = "GET /bedrockagent/get-data-source"; String type = "AWS::Bedrock::DataSource"; String identifier = "nonExistDatasourceId"; + String cloudformationIdentifier = "nonExistDatasourceId"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetDataSource"), @@ -1759,6 +1921,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1775,6 +1938,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1785,6 +1949,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1795,6 +1960,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1812,6 +1978,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { var localOperation = "GET /bedrockruntime/invokeModel/ai21Jamba"; String type = "AWS::Bedrock::Model"; String identifier = "ai21.jamba-1-5-mini-v1:0"; + String cloudformationIdentifier = "ai21.jamba-1-5-mini-v1:0"; assertSpanClientAttributes( traces, bedrockRuntimeSpanName("InvokeModel"), @@ -1822,6 +1989,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1843,6 +2011,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1853,6 +2022,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1863,6 +2033,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1880,6 +2051,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { var localOperation = "GET /bedrockruntime/invokeModel/amazonTitan"; String type = "AWS::Bedrock::Model"; String identifier = "amazon.titan-text-premier-v1:0"; + String cloudformationIdentifier = "amazon.titan-text-premier-v1:0"; assertSpanClientAttributes( traces, bedrockRuntimeSpanName("InvokeModel"), @@ -1890,6 +2062,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1914,6 +2087,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1924,6 +2098,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1934,6 +2109,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1952,6 +2128,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { var localOperation = "GET /bedrockruntime/invokeModel/anthropicClaude"; String type = "AWS::Bedrock::Model"; String identifier = "anthropic.claude-3-haiku-20240307-v1:0"; + String cloudformationIdentifier = "anthropic.claude-3-haiku-20240307-v1:0"; assertSpanClientAttributes( traces, @@ -1963,6 +2140,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1987,6 +2165,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1997,6 +2176,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2007,6 +2187,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2025,6 +2206,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { var localOperation = "GET /bedrockruntime/invokeModel/cohereCommandR"; String type = "AWS::Bedrock::Model"; String identifier = "cohere.command-r-v1:0"; + String cloudformationIdentifier = "cohere.command-r-v1:0"; assertSpanClientAttributes( traces, @@ -2036,6 +2218,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2059,6 +2242,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2069,6 +2253,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2079,6 +2264,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2097,6 +2283,7 @@ protected void doTestBedrockRuntimeMetaLlama() { var localOperation = "GET /bedrockruntime/invokeModel/metaLlama"; String type = "AWS::Bedrock::Model"; String identifier = "meta.llama3-70b-instruct-v1:0"; + String cloudformationIdentifier = "meta.llama3-70b-instruct-v1:0"; assertSpanClientAttributes( traces, @@ -2108,6 +2295,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2130,6 +2318,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2140,6 +2329,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2150,6 +2340,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2168,6 +2359,7 @@ protected void doTestBedrockRuntimeMistral() { var localOperation = "GET /bedrockruntime/invokeModel/mistralAi"; String type = "AWS::Bedrock::Model"; String identifier = "mistral.mistral-large-2402-v1:0"; + String cloudformationIdentifier = "mistral.mistral-large-2402-v1:0"; assertSpanClientAttributes( traces, @@ -2179,6 +2371,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2202,6 +2395,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2212,6 +2406,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2222,6 +2417,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2239,6 +2435,8 @@ protected void doTestBedrockGuardrailId() { var localOperation = "GET /bedrock/getguardrail"; String type = "AWS::Bedrock::Guardrail"; String identifier = "test-bedrock-guardrail"; + String cloudformationIdentifier = + "arn:aws:bedrock:us-east-1:000000000000:guardrail/test-bedrock-guardrail"; assertSpanClientAttributes( traces, bedrockSpanName("GetGuardrail"), @@ -2249,13 +2447,17 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", 200, List.of( assertAttribute( - SemanticConventionsConstants.AWS_GUARDRAIL_ID, "test-bedrock-guardrail"))); + SemanticConventionsConstants.AWS_GUARDRAIL_ID, "test-bedrock-guardrail"), + assertAttribute( + SemanticConventionsConstants.AWS_GUARDRAIL_ARN, + "arn:aws:bedrock:us-east-1:000000000000:guardrail/test-bedrock-guardrail"))); assertMetricClientAttributes( metrics, AppSignalsConstants.LATENCY_METRIC, @@ -2265,6 +2467,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2275,6 +2478,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2285,6 +2489,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2302,6 +2507,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { var localOperation = "GET /bedrockagentruntime/getmemory/:agentId"; String type = "AWS::Bedrock::Agent"; String identifier = "test-agent-id"; + String cloudformationIdentifier = "test-agent-id"; assertSpanClientAttributes( traces, bedrockAgentRuntimeSpanName("GetAgentMemory"), @@ -2312,6 +2518,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2326,6 +2533,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2336,6 +2544,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2346,6 +2555,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2364,6 +2574,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { var localOperation = "GET /bedrockagentruntime/retrieve/:knowledgeBaseId"; String type = "AWS::Bedrock::KnowledgeBase"; String identifier = "test-knowledge-base-id"; + String cloudformationIdentifier = "test-knowledge-base-id"; assertSpanClientAttributes( traces, bedrockAgentRuntimeSpanName("Retrieve"), @@ -2374,6 +2585,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2390,6 +2602,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2400,6 +2613,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2410,6 +2624,650 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSecretsManagerDescribeSecret() throws Exception { + appClient.get("/secretsmanager/describesecret/test-secret-id").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/describesecret/:secretId"; + var type = "AWS::SecretsManager::Secret"; + var identifier = "test-secret-id-[A-Za-z0-9]{6}"; + var cloudformationIdentifier = + "arn:aws:secretsmanager:us-west-2:000000000000:secret:test-secret-id-[A-Za-z0-9]{6}"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_SECRET_ARN, + "arn:aws:secretsmanager:us-west-2:000000000000:secret:test-secret-id-[A-Za-z0-9]{6}"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSecretsManagerError() throws Exception { + appClient.get("/secretsmanager/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/error"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of()); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 1.0); + } + + protected void doTestSecretsManagerFault() throws Exception { + appClient.get("/secretsmanager/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/fault"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of()); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 1.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 0.0); + } + + protected void doTestStepFunctionsDescribeStateMachine() throws Exception { + appClient.get("/sfn/describestatemachine/test-state-machine").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/describestatemachine/:name"; + var type = "AWS::StepFunctions::StateMachine"; + var identifier = "test-state-machine"; + var cloudformationIdentifier = + "arn:aws:states:us-west-2:000000000000:stateMachine:test-state-machine"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeStateMachine"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_STATE_MACHINE_ARN, + "arn:aws:states:us-west-2:000000000000:stateMachine:test-state-machine"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestStepFunctionsDescribeActivity() throws Exception { + appClient.get("/sfn/describeactivity/test-activity").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/describeactivity/:name"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "test-activity"; + var cloudformationIdentifier = "arn:aws:states:us-west-2:000000000000:activity:test-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:test-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestStepFunctionsError() throws Exception { + appClient.get("/sfn/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/error"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "nonexistent-activity"; + var cloudformationIdentifier = + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 1.0); + } + + protected void doTestStepFunctionsFault() throws Exception { + appClient.get("/sfn/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/fault"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "fault-activity"; + var cloudformationIdentifier = "arn:aws:states:us-west-2:000000000000:activity:fault-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:fault-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 1.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSnsGetTopicAttributes() throws Exception { + appClient.get("/sns/gettopicattributes/test-topic").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/gettopicattributes/:topicId"; + var type = "AWS::SNS::Topic"; + var identifier = "test-topic"; + var cloudformationIdentifier = "arn:aws:sns:us-west-2:000000000000:test-topic"; + + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_TOPIC_ARN, + "arn:aws:sns:us-west-2:000000000000:test-topic"))); + } + + protected void doTestSnsError() throws Exception { + appClient.get("/sns/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/error"; + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of()); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 5000.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 0.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 1.0); + } + + protected void doTestSnsFault() throws Exception { + appClient.get("/sns/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/fault"; + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of()); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 5000.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 1.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, 0.0); } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java index fa5a586c5d..5a53b83a1e 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java @@ -76,6 +76,21 @@ protected String getBedrockAgentRuntimeSpanNamePrefix() { return "AWSBedrockAgentRuntime"; } + @Override + protected String getSecretsManagerSpanNamePrefix() { + return "AWSSecretsManager"; + } + + @Override + protected String getStepFunctionsSpanNamePrefix() { + return "AWSStepFunctions"; + } + + @Override + protected String getSnsSpanNamePrefix() { + return "SNS"; + } + protected String getS3RpcServiceName() { return "Amazon S3"; } @@ -90,6 +105,21 @@ protected String getSqsRpcServiceName() { return "AmazonSQS"; } + @Override + protected String getSecretsManagerRpcServiceName() { + return "AWSSecretsManager"; + } + + @Override + protected String getStepFunctionsRpcServiceName() { + return "AWSStepFunctions"; + } + + @Override + protected String getSnsRpcServiceName() { + return "AmazonSNS"; + } + protected String getKinesisRpcServiceName() { return "AmazonKinesis"; } @@ -260,4 +290,54 @@ void testBedrockAgentRuntimeAgentId() { void testBedrockAgentRuntimeKnowledgeBaseId() { doTestBedrockAgentRuntimeKnowledgeBaseId(); } + + @Test + void testSecretsManagerDescribeSecret() throws Exception { + doTestSecretsManagerDescribeSecret(); + } + + @Test + void testSecretsManagerError() throws Exception { + doTestSecretsManagerError(); + } + + @Test + void testSecretsManagerFault() throws Exception { + doTestSecretsManagerFault(); + } + + @Test + void testStepFunctionsDescribeStateMachine() throws Exception { + doTestStepFunctionsDescribeStateMachine(); + } + + @Test + void testStepFunctionsDescribeActivity() throws Exception { + doTestStepFunctionsDescribeActivity(); + } + + @Test + void testStepFunctionsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testStepFunctionsFault() throws Exception { + doTestStepFunctionsFault(); + } + + @Test + void testSnsGetTopicAttributes() throws Exception { + doTestSnsGetTopicAttributes(); + } + + @Test + void testSnsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testSnsFault() throws Exception { + doTestStepFunctionsFault(); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java index 46c6b7e425..c1259ca6cc 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java @@ -75,6 +75,21 @@ protected String getBedrockAgentRuntimeSpanNamePrefix() { return "BedrockAgentRuntime"; } + @Override + protected String getSecretsManagerSpanNamePrefix() { + return "SecretsManager"; + } + + @Override + protected String getStepFunctionsSpanNamePrefix() { + return "Sfn"; + } + + @Override + protected String getSnsSpanNamePrefix() { + return "Sns"; + } + @Override protected String getS3RpcServiceName() { return "S3"; @@ -114,6 +129,21 @@ protected String getBedrockAgentRuntimeRpcServiceName() { return "BedrockAgentRuntime"; } + @Override + protected String getSecretsManagerRpcServiceName() { + return "SecretsManager"; + } + + @Override + protected String getStepFunctionsRpcServiceName() { + return "Sfn"; + } + + @Override + protected String getSnsRpcServiceName() { + return "Sns"; + } + @Test void testS3CreateBucket() throws Exception { doTestS3CreateBucket(); @@ -259,10 +289,58 @@ void testBedrockAgentRuntimeAgentId() { doTestBedrockAgentRuntimeAgentId(); } - // TODO: Enable testBedrockAgentRuntimeKnowledgeBaseId test after KnowledgeBaseId is supported in - // OTEL BedrockAgentRuntime instrumentation @Test void testBedrockAgentRuntimeKnowledgeBaseId() { doTestBedrockAgentRuntimeKnowledgeBaseId(); } + + @Test + void testSecretsManagerDescribeSecret() throws Exception { + doTestSecretsManagerDescribeSecret(); + } + + @Test + void testSecretsManagerError() throws Exception { + doTestSecretsManagerError(); + } + + @Test + void testSecretsManagerFault() throws Exception { + doTestSecretsManagerFault(); + } + + @Test + void testStepFunctionsDescribeStateMachine() throws Exception { + doTestStepFunctionsDescribeStateMachine(); + } + + @Test + void testStepFunctionsDescribeActivity() throws Exception { + doTestStepFunctionsDescribeActivity(); + } + + @Test + void testStepFunctionsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testStepFunctionsFault() throws Exception { + doTestStepFunctionsFault(); + } + + @Test + void testSnsGetTopicAttributes() throws Exception { + doTestSnsGetTopicAttributes(); + } + + @Test + void testSnsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testSnsFault() throws Exception { + doTestStepFunctionsFault(); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java index 675b69032b..0ff11305c2 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java @@ -31,6 +31,8 @@ public class AppSignalsConstants { public static final String AWS_REMOTE_OPERATION = "aws.remote.operation"; public static final String AWS_REMOTE_RESOURCE_TYPE = "aws.remote.resource.type"; public static final String AWS_REMOTE_RESOURCE_IDENTIFIER = "aws.remote.resource.identifier"; + public static final String AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER = + "aws.remote.resource.cfn.primary.identifier"; public static final String AWS_SPAN_KIND = "aws.span.kind"; public static final String AWS_REMOTE_DB_USER = "aws.remote.db.user"; diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java index f0cac6da46..51077ea6a1 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java @@ -62,6 +62,7 @@ public class SemanticConventionsConstants { public static final String AWS_DATA_SOURCE_ID = "aws.bedrock.data_source.id"; public static final String AWS_AGENT_ID = "aws.bedrock.agent.id"; public static final String AWS_GUARDRAIL_ID = "aws.bedrock.guardrail.id"; + public static final String AWS_GUARDRAIL_ARN = "aws.bedrock.guardrail.arn"; public static final String GEN_AI_REQUEST_MODEL = "gen_ai.request.model"; public static final String GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"; public static final String GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"; @@ -69,6 +70,10 @@ public class SemanticConventionsConstants { public static final String GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons"; public static final String GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"; public static final String GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"; + public static final String AWS_SECRET_ARN = "aws.secretsmanager.secret.arn"; + public static final String AWS_STATE_MACHINE_ARN = "aws.stepfunctions.state_machine.arn"; + public static final String AWS_ACTIVITY_ARN = "aws.stepfunctions.activity.arn"; + public static final String AWS_TOPIC_ARN = "aws.sns.topic.arn"; // kafka public static final String MESSAGING_CLIENT_ID = "messaging.client_id"; diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts b/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts index 6ee3f0cae1..77fad5427c 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-dynamodb") implementation("com.amazonaws:aws-java-sdk-sqs") implementation("com.amazonaws:aws-java-sdk-kinesis") + implementation("com.amazonaws:aws-java-sdk-secretsmanager") + implementation("com.amazonaws:aws-java-sdk-iam") + implementation("com.amazonaws:aws-java-sdk-stepfunctions") + implementation("com.amazonaws:aws-java-sdk-sns") implementation("com.amazonaws:aws-java-sdk-bedrock") implementation("com.amazonaws:aws-java-sdk-bedrockagent") implementation("com.amazonaws:aws-java-sdk-bedrockruntime") diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java index ad5f7a73b4..6b39559b0d 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java @@ -44,6 +44,9 @@ import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; +import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient; +import com.amazonaws.services.identitymanagement.model.CreateRoleRequest; +import com.amazonaws.services.identitymanagement.model.PutRolePolicyRequest; import com.amazonaws.services.kinesis.AmazonKinesisClient; import com.amazonaws.services.kinesis.model.CreateStreamRequest; import com.amazonaws.services.kinesis.model.PutRecordRequest; @@ -52,10 +55,30 @@ import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.DescribeSecretRequest; +import com.amazonaws.services.secretsmanager.model.ListSecretsRequest; +import com.amazonaws.services.secretsmanager.model.SecretListEntry; +import com.amazonaws.services.sns.AmazonSNSClient; +import com.amazonaws.services.sns.model.CreateTopicRequest; +import com.amazonaws.services.sns.model.GetTopicAttributesRequest; +import com.amazonaws.services.sns.model.ListTopicsRequest; +import com.amazonaws.services.sns.model.Topic; import com.amazonaws.services.sqs.AmazonSQSClient; import com.amazonaws.services.sqs.model.CreateQueueRequest; import com.amazonaws.services.sqs.model.ReceiveMessageRequest; import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.services.stepfunctions.AWSStepFunctionsClient; +import com.amazonaws.services.stepfunctions.model.ActivityListItem; +import com.amazonaws.services.stepfunctions.model.CreateActivityRequest; +import com.amazonaws.services.stepfunctions.model.CreateStateMachineRequest; +import com.amazonaws.services.stepfunctions.model.DescribeActivityRequest; +import com.amazonaws.services.stepfunctions.model.DescribeStateMachineRequest; +import com.amazonaws.services.stepfunctions.model.ListActivitiesRequest; +import com.amazonaws.services.stepfunctions.model.ListStateMachinesRequest; +import com.amazonaws.services.stepfunctions.model.StateMachineListItem; +import com.amazonaws.services.stepfunctions.model.StateMachineType; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; @@ -125,6 +148,9 @@ public static void main(String[] args) throws IOException, InterruptedException setupS3(); setupSqs(); setupKinesis(); + setupSecretsManager(); + setupStepFunctions(); + setupSns(); setupBedrock(); // Add this log line so that we only start testing after all routes are configured. @@ -518,6 +544,367 @@ private static void setupS3() { }); } + private static void setupSecretsManager() { + var secretsManagerClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + var secretName = "test-secret-id"; + String existingSecretArn = null; + try { + var listRequest = new ListSecretsRequest(); + var listResponse = secretsManagerClient.listSecrets(listRequest); + existingSecretArn = + listResponse.getSecretList().stream() + .filter(secret -> secret.getName().contains(secretName)) + .findFirst() + .map(SecretListEntry::getARN) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing secrets", e); + } + + if (existingSecretArn != null) { + logger.debug("Secret already exists, skipping creation"); + } else { + logger.info("Secret not found, creating new one"); + var createSecretRequest = new CreateSecretRequest().withName(secretName); + var createSecretResponse = secretsManagerClient.createSecret(createSecretRequest); + existingSecretArn = createSecretResponse.getARN(); + } + + String finalExistingSecretArn = existingSecretArn; + get( + "/secretsmanager/describesecret/:secretId", + (req, res) -> { + var describeRequest = new DescribeSecretRequest().withSecretId(finalExistingSecretArn); + secretsManagerClient.describeSecret(describeRequest); + return ""; + }); + + get( + "/secretsmanager/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeSecretRequest() + .withSecretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret-id"); + errorClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.debug("Error describing secret", e); + } + return ""; + }); + + get( + "/secretsmanager/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeSecretRequest() + .withSecretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:fault-secret-id"); + faultClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.debug("Error describing secret", e); + } + return ""; + }); + } + + private static void setupStepFunctions() { + var stepFunctionsClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + var iamClient = + AmazonIdentityManagementClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + + var sfnName = "test-state-machine"; + String existingStateMachineArn = null; + try { + var listRequest = new ListStateMachinesRequest(); + var listResponse = stepFunctionsClient.listStateMachines(listRequest); + existingStateMachineArn = + listResponse.getStateMachines().stream() + .filter(machine -> machine.getName().equals(sfnName)) + .findFirst() + .map(StateMachineListItem::getStateMachineArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing state machines", e); + } + + if (existingStateMachineArn != null) { + logger.debug("State machine already exists, skipping creation"); + } else { + logger.debug("State machine not found, creating new one"); + String trustPolicy = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": {" + + " \"Service\": \"states.amazonaws.com\"" + + " }," + + " \"Action\": \"sts:AssumeRole\"" + + " }" + + "]}"; + var roleRequest = + new CreateRoleRequest() + .withRoleName(sfnName + "-role") + .withAssumeRolePolicyDocument(trustPolicy); + var roleArn = iamClient.createRole(roleRequest).getRole().getArn(); + String policyDocument = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Action\": [" + + " \"lambda:InvokeFunction\"" + + " ]," + + " \"Resource\": [" + + " \"*\"" + + " ]" + + " }" + + "]}"; + var policyRequest = + new PutRolePolicyRequest() + .withRoleName(sfnName + "-role") + .withPolicyName(sfnName + "-policy") + .withPolicyDocument(policyDocument); + iamClient.putRolePolicy(policyRequest); + String stateMachineDefinition = + "{" + + " \"Comment\": \"A Hello World example of the Amazon States Language using a Pass state\"," + + " \"StartAt\": \"HelloWorld\"," + + " \"States\": {" + + " \"HelloWorld\": {" + + " \"Type\": \"Pass\"," + + " \"Result\": \"Hello World!\"," + + " \"End\": true" + + " }" + + " }" + + "}"; + var sfnRequest = + new CreateStateMachineRequest() + .withName(sfnName) + .withRoleArn(roleArn) + .withDefinition(stateMachineDefinition) + .withType(StateMachineType.STANDARD); + var createResponse = stepFunctionsClient.createStateMachine(sfnRequest); + existingStateMachineArn = createResponse.getStateMachineArn(); + } + + var activityName = "test-activity"; + String existingActivityArn = null; + try { + var listRequest = new ListActivitiesRequest(); + var listResponse = stepFunctionsClient.listActivities(listRequest); + existingActivityArn = + listResponse.getActivities().stream() + .filter(activity -> activity.getName().equals(activityName)) + .findFirst() + .map(ActivityListItem::getActivityArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing activities", e); + } + + if (existingActivityArn != null) { + logger.debug("Activity already exists, skipping creation"); + } else { + logger.debug("Activity does not exist, creating new one"); + var createRequest = new CreateActivityRequest().withName(activityName); + var createResponse = stepFunctionsClient.createActivity(createRequest); + existingActivityArn = createResponse.getActivityArn(); + } + + String finalExistingStateMachineArn = existingStateMachineArn; + String finalExistingActivityArn = existingActivityArn; + + get( + "/sfn/describestatemachine/:name", + (req, res) -> { + var describeRequest = + new DescribeStateMachineRequest().withStateMachineArn(finalExistingStateMachineArn); + stepFunctionsClient.describeStateMachine(describeRequest); + return ""; + }); + + get( + "/sfn/describeactivity/:name", + (req, res) -> { + var describeRequest = + new DescribeActivityRequest().withActivityArn(finalExistingActivityArn); + stepFunctionsClient.describeActivity(describeRequest); + return ""; + }); + + get( + "/sfn/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeActivityRequest() + .withActivityArn( + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"); + errorClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + + get( + "/sfn/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeActivityRequest() + .withActivityArn( + "arn:aws:states:us-west-2:000000000000:activity:fault-activity"); + faultClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + } + + private static void setupSns() { + var snsClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + + var topicName = "test-topic"; + String existingTopicArn = null; + + try { + var listTopicsRequest = new ListTopicsRequest(); + var listTopicsResult = snsClient.listTopics(listTopicsRequest); + existingTopicArn = + listTopicsResult.getTopics().stream() + .filter(topic -> topic.getTopicArn().contains(topicName)) + .findFirst() + .map(Topic::getTopicArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing topics", e); + } + + if (existingTopicArn != null) { + logger.debug("Topic already exists, skipping creation"); + } else { + logger.debug("Topic does not exist, creating new one"); + var createTopicRequest = new CreateTopicRequest().withName(topicName); + var createTopicResult = snsClient.createTopic(createTopicRequest); + existingTopicArn = createTopicResult.getTopicArn(); + } + + String finalExistingTopicArn = existingTopicArn; + get( + "/sns/gettopicattributes/:topicId", + (req, res) -> { + var getTopicAttributesRequest = + new GetTopicAttributesRequest().withTopicArn(finalExistingTopicArn); + snsClient.getTopicAttributes(getTopicAttributesRequest); + return ""; + }); + + get( + "/sns/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "https://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var getTopicAttributesRequest = + new GetTopicAttributesRequest() + .withTopicArn("arn:aws:sns:us-west-2:000000000000:nonexistent-topic"); + errorClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + + get( + "/sns/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var getTopicAttributesRequest = + new GetTopicAttributesRequest() + .withTopicArn("arn:aws:sns:us-west-2:000000000000:fault-topic"); + faultClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + } + private static void setupBedrock() { // Localstack does not support Bedrock related services. // We point all Bedrock related request endpoints to the local app, diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts b/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts index e50f70772f..0ba17450d6 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { implementation("software.amazon.awssdk:dynamodb") implementation("software.amazon.awssdk:sqs") implementation("software.amazon.awssdk:kinesis") + implementation("software.amazon.awssdk:secretsmanager") + implementation("software.amazon.awssdk:iam") + implementation("software.amazon.awssdk:sfn") + implementation("software.amazon.awssdk:sns") implementation("software.amazon.awssdk:bedrock") implementation("software.amazon.awssdk:bedrockagent") implementation("software.amazon.awssdk:bedrockruntime") diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java index 2982135fd4..c96b762dfd 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java @@ -60,6 +60,9 @@ import software.amazon.awssdk.services.dynamodb.model.KeyType; import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.iam.IamClient; +import software.amazon.awssdk.services.iam.model.CreateRoleRequest; +import software.amazon.awssdk.services.iam.model.PutRolePolicyRequest; import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.model.CreateStreamRequest; import software.amazon.awssdk.services.kinesis.model.DescribeStreamRequest; @@ -68,6 +71,26 @@ import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest; +import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry; +import software.amazon.awssdk.services.sfn.SfnClient; +import software.amazon.awssdk.services.sfn.model.ActivityListItem; +import software.amazon.awssdk.services.sfn.model.CreateActivityRequest; +import software.amazon.awssdk.services.sfn.model.CreateStateMachineRequest; +import software.amazon.awssdk.services.sfn.model.DescribeActivityRequest; +import software.amazon.awssdk.services.sfn.model.DescribeStateMachineRequest; +import software.amazon.awssdk.services.sfn.model.ListActivitiesRequest; +import software.amazon.awssdk.services.sfn.model.ListStateMachinesRequest; +import software.amazon.awssdk.services.sfn.model.StateMachineListItem; +import software.amazon.awssdk.services.sfn.model.StateMachineType; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.model.CreateTopicRequest; +import software.amazon.awssdk.services.sns.model.GetTopicAttributesRequest; +import software.amazon.awssdk.services.sns.model.ListTopicsRequest; +import software.amazon.awssdk.services.sns.model.Topic; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; @@ -121,7 +144,10 @@ public static void main(String[] args) throws IOException, InterruptedException setupS3(); setupSqs(); setupKinesis(); + setupSecretsManager(); + setupSfn(); setupBedrock(); + setupSns(); // Add this log line so that we only start testing after all routes are configured. awaitInitialization(); logger.info("All routes initialized"); @@ -532,6 +558,365 @@ private static void setupS3() { }); } + private static void setupSecretsManager() { + var secretsManagerClient = + SecretsManagerClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + var secretName = "test-secret-id"; + String existingSecretArn = null; + try { + var listRequest = ListSecretsRequest.builder().build(); + var listResponse = secretsManagerClient.listSecrets(listRequest); + existingSecretArn = + listResponse.secretList().stream() + .filter(secret -> secret.name().contains(secretName)) + .findFirst() + .map(SecretListEntry::arn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing secrets", e); + } + + if (existingSecretArn != null) { + logger.debug("Secret already exists, skipping creation"); + } else { + logger.debug("Secret not found, creating a new one"); + var createSecretRequest = CreateSecretRequest.builder().name(secretName).build(); + var createSecretResponse = secretsManagerClient.createSecret(createSecretRequest); + existingSecretArn = createSecretResponse.arn(); + } + + String finalExistingSecretArn = existingSecretArn; + get( + "/secretsmanager/describesecret/:secretId", + (req, res) -> { + var describeRequest = + DescribeSecretRequest.builder().secretId(finalExistingSecretArn).build(); + secretsManagerClient.describeSecret(describeRequest); + return ""; + }); + + get( + "/secretsmanager/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SecretsManagerClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeSecretRequest.builder() + .secretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret-id") + .build(); + errorClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.error("Error describing secret", e); + } + return ""; + }); + + get( + "/secretsmanager/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SecretsManagerClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeSecretRequest.builder() + .secretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:fault-secret-id") + .build(); + faultClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.error("Error describing secret", e); + } + return ""; + }); + } + + private static void setupSfn() { + var sfnClient = + SfnClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + var iamClient = + IamClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + var sfnName = "test-state-machine"; + String existingStateMachineArn = null; + try { + var listRequest = ListStateMachinesRequest.builder().build(); + var listResponse = sfnClient.listStateMachines(listRequest); + existingStateMachineArn = + listResponse.stateMachines().stream() + .filter(machine -> machine.name().equals(sfnName)) + .findFirst() + .map(StateMachineListItem::stateMachineArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing state machines", e); + } + + if (existingStateMachineArn != null) { + logger.debug("State machine already exists, skipping creation"); + } else { + logger.debug("State machine not found, creating a new one"); + String trustPolicy = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": {" + + " \"Service\": \"states.amazonaws.com\"" + + " }," + + " \"Action\": \"sts:AssumeRole\"" + + " }" + + "]}"; + var roleRequest = + CreateRoleRequest.builder() + .roleName(sfnName + "-role") + .assumeRolePolicyDocument(trustPolicy) + .build(); + var roleArn = iamClient.createRole(roleRequest).role().arn(); + String policyDocument = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Action\": [" + + " \"lambda:InvokeFunction\"" + + " ]," + + " \"Resource\": [" + + " \"*\"" + + " ]" + + " }" + + "]}"; + var policyRequest = + PutRolePolicyRequest.builder() + .roleName(sfnName + "-role") + .policyName(sfnName + "-policy") + .policyDocument(policyDocument) + .build(); + iamClient.putRolePolicy(policyRequest); + String stateMachineDefinition = + "{" + + " \"Comment\": \"A Hello World example of the Amazon States Language using a Pass state\"," + + " \"StartAt\": \"HelloWorld\"," + + " \"States\": {" + + " \"HelloWorld\": {" + + " \"Type\": \"Pass\"," + + " \"Result\": \"Hello World!\"," + + " \"End\": true" + + " }" + + " }" + + "}"; + var sfnRequest = + CreateStateMachineRequest.builder() + .name(sfnName) + .roleArn(roleArn) + .definition(stateMachineDefinition) + .type(StateMachineType.STANDARD) + .build(); + existingStateMachineArn = sfnClient.createStateMachine(sfnRequest).stateMachineArn(); + } + + var activityName = "test-activity"; + String existingActivityArn = null; + + try { + var listRequest = ListActivitiesRequest.builder().build(); + var listResponse = sfnClient.listActivities(listRequest); + existingActivityArn = + listResponse.activities().stream() + .filter(activity -> activity.name().equals(activityName)) + .findFirst() + .map(ActivityListItem::activityArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing activities", e); + } + + if (existingActivityArn != null) { + logger.debug("Activities already exists, skipping creation"); + } else { + logger.debug("Activities not found, creating a new one"); + var createRequest = CreateActivityRequest.builder().name(activityName).build(); + existingActivityArn = sfnClient.createActivity(createRequest).activityArn(); + } + + String finalExistingStateMachineArn = existingStateMachineArn; + String finalExistingActivityArn = existingActivityArn; + + get( + "/sfn/describestatemachine/:name", + (req, res) -> { + var describeRequest = + DescribeStateMachineRequest.builder() + .stateMachineArn(finalExistingStateMachineArn) + .build(); + sfnClient.describeStateMachine(describeRequest); + return ""; + }); + + get( + "/sfn/describeactivity/:name", + (req, res) -> { + var describeRequest = + DescribeActivityRequest.builder().activityArn(finalExistingActivityArn).build(); + sfnClient.describeActivity(describeRequest); + return ""; + }); + + get( + "/sfn/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SfnClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeActivityRequest.builder() + .activityArn( + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity") + .build(); + errorClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + + get( + "/sfn/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SfnClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeActivityRequest.builder() + .activityArn("arn:aws:states:us-west-2:000000000000:activity:fault-activity") + .build(); + faultClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + } + + private static void setupSns() { + var snsClient = + SnsClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + var topicName = "test-topic"; + String existingTopicArn = null; + + try { + var listRequest = ListTopicsRequest.builder().build(); + var listResponse = snsClient.listTopics(listRequest); + existingTopicArn = + listResponse.topics().stream() + .filter(topic -> topic.topicArn().contains(topicName)) + .findFirst() + .map(Topic::topicArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing topics", e); + } + + if (existingTopicArn != null) { + logger.debug("Topics already exists, skipping creation"); + } else { + logger.debug("Topics not found, creating a new one"); + var createTopicRequest = CreateTopicRequest.builder().name(topicName).build(); + var createTopicResponse = snsClient.createTopic(createTopicRequest); + existingTopicArn = createTopicResponse.topicArn(); + } + + String finalExistingTopicArn = existingTopicArn; + get( + "/sns/gettopicattributes/:topicId", + (req, res) -> { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder().topicArn(finalExistingTopicArn).build(); + snsClient.getTopicAttributes(getTopicAttributesRequest); + return ""; + }); + + get( + "/sns/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SnsClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder() + .topicArn("arn:aws:sns:us-west-2:000000000000:nonexistent-topic") + .build(); + errorClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + + get( + "/sns/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SnsClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder() + .topicArn("arn:aws:sns:us-west-2:000000000000:fault-topic") + .build(); + faultClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + } + private static void setupBedrock() { // Localstack does not support Bedrock related services. // We point all Bedrock related request endpoints to the local app, From eae57c577d8fe3c702c0f8d36db9493b19930fe2 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:52:30 -0800 Subject: [PATCH 2/2] APIGW + Lambda sample app (#961) --- sample-apps/apigateway-lambda/README.md | 58 +++++++++ .../apigateway-lambda/build.gradle.kts | 40 ++++++ .../com/amazon/sampleapp/LambdaHandler.java | 82 ++++++++++++ .../apigateway-lambda/terraform/main.tf | 119 ++++++++++++++++++ .../apigateway-lambda/terraform/variables.tf | 43 +++++++ settings.gradle.kts | 1 + 6 files changed, 343 insertions(+) create mode 100644 sample-apps/apigateway-lambda/README.md create mode 100644 sample-apps/apigateway-lambda/build.gradle.kts create mode 100644 sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java create mode 100644 sample-apps/apigateway-lambda/terraform/main.tf create mode 100644 sample-apps/apigateway-lambda/terraform/variables.tf diff --git a/sample-apps/apigateway-lambda/README.md b/sample-apps/apigateway-lambda/README.md new file mode 100644 index 0000000000..6ecc80986b --- /dev/null +++ b/sample-apps/apigateway-lambda/README.md @@ -0,0 +1,58 @@ +## API Gateway + Lambda Sample Application + +The directory contains the source code and the Infrastructure as Code (IaC) to create the sample app in your AWS account. + +### Prerequisite +Before you begin, ensure you have the following installed: +- Java 17 +- Gradle +- Terraform +- AWS CLI configured with appropriate credentials + +### Getting Started + +#### 1. Build the application +```bash +# Change to the project directory +cd sample-apps/apigateway-lambda + +# Build the application using Gradle +gradle clean build + +# Prepare the Lambda deployment package +gradle createLambdaZip +``` + +#### 2. Deploy the application +```bash +# Change to the terraform directory +cd terraform + +# Initialize Terraform +terraform init + +# (Optional) Review the deployment plan for better understanding of the components +terraform plan + +# Deploy +terraform apply +``` + +#### 3. Testing the applicating +After successful deployment, Terraform will output the API Gateway endpoint URL. You can test the application using: +```bash +curl +``` + +#### 4. Clean Up +To avoid incurring unnecessary charges, remember to destroy the resources when you are done: +```bash +terraform destroy +``` + +#### (Optional) Instrumenting with Application Signals Lambda Layer +You can choose to instrument the Lambda function with Application Signals Lambda Layer upon deployment by passing in the layer ARN to the `adot_layer_arn` variable. +You must have the layer already published to your account before executing the following command. +```bash +terraform apply -var "adot_layer_arn=" +``` \ No newline at end of file diff --git a/sample-apps/apigateway-lambda/build.gradle.kts b/sample-apps/apigateway-lambda/build.gradle.kts new file mode 100644 index 0000000000..66992540ab --- /dev/null +++ b/sample-apps/apigateway-lambda/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + java + application +} + +application { + mainClass.set("com.amazon.sampleapp.LambdaHandler") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation("com.amazonaws:aws-lambda-java-core:1.2.2") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("software.amazon.awssdk:s3:2.29.23") + implementation("org.json:json:20240303") + implementation("org.slf4j:jcl-over-slf4j:2.0.16") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "com.amazon.sampleapp.LambdaHandler" + } +} + +tasks.register("createLambdaZip") { + dependsOn("build") + from(tasks.compileJava.get()) + from(tasks.processResources.get()) + into("lib") { + from(configurations.runtimeClasspath.get()) + } + archiveFileName.set("lambda-function.zip") + destinationDirectory.set(layout.buildDirectory.dir("distributions")) +} diff --git a/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java new file mode 100644 index 0000000000..f3e11bc38d --- /dev/null +++ b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java @@ -0,0 +1,82 @@ +package com.amazon.sampleapp; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.io.IOException; +import java.util.Map; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONObject; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class LambdaHandler implements RequestHandler> { + + private final OkHttpClient client = new OkHttpClient(); + private final S3Client s3Client = S3Client.create(); + + @Override + public Map handleRequest(Object input, Context context) { + System.out.println("Executing LambdaHandler"); + + // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime + // try and get the trace id from environment variable _X_AMZN_TRACE_ID. If it's not present + // there + // then try the system property. + String traceId = + System.getenv("_X_AMZN_TRACE_ID") != null + ? System.getenv("_X_AMZN_TRACE_ID") + : System.getProperty("com.amazonaws.xray.traceHeader"); + + System.out.println("Trace ID: " + traceId); + + JSONObject responseBody = new JSONObject(); + responseBody.put("traceId", traceId); + + // Make a remote call using OkHttp + System.out.println("Making a remote call using OkHttp"); + String url = "https://www.amazon.com"; + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + responseBody.put("httpRequest", "Request successful"); + } catch (IOException e) { + context.getLogger().log("Error: " + e.getMessage()); + responseBody.put("httpRequest", "Request failed"); + } + System.out.println("Remote call done"); + + // Make a S3 HeadBucket call to check whether the bucket exists + System.out.println("Making a S3 HeadBucket call"); + String bucketName = "SomeDummyBucket"; + try { + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName).build(); + s3Client.headBucket(headBucketRequest); + responseBody.put("s3Request", "Bucket exists and is accessible: " + bucketName); + } catch (S3Exception e) { + if (e.statusCode() == 403) { + responseBody.put("s3Request", "Access denied to bucket: " + bucketName); + } else if (e.statusCode() == 404) { + responseBody.put("s3Request", "Bucket does not exist: " + bucketName); + } else { + System.err.println("Error checking bucket: " + e.awsErrorDetails().errorMessage()); + responseBody.put( + "s3Request", "Error checking bucket: " + e.awsErrorDetails().errorMessage()); + } + } + System.out.println("S3 HeadBucket call done"); + + // return a response in the ApiGateway proxy format + return Map.of( + "isBase64Encoded", + false, + "statusCode", + 200, + "body", + responseBody.toString(), + "headers", + Map.of("Content-Type", "application/json")); + } +} diff --git a/sample-apps/apigateway-lambda/terraform/main.tf b/sample-apps/apigateway-lambda/terraform/main.tf new file mode 100644 index 0000000000..6881f0e1ce --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/main.tf @@ -0,0 +1,119 @@ +### Lambda function +locals { + architecture = var.architecture == "x86_64" ? "amd64" : "arm64" +} + +resource "aws_iam_role" "lambda_role" { + name = "lambda_execution_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { Service = "lambda.amazonaws.com" } + }] + }) +} + +resource "aws_iam_policy" "s3_access" { + name = "S3ListBucketPolicy" + description = "Allow Lambda to check a given S3 bucket exists" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["s3:ListBucket"], + Resource = "*" + }] + }) +} + +resource "aws_iam_role_policy_attachment" "attach_execution_role_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy_attachment" "attach_s3_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.s3_access.arn +} + +resource "aws_iam_role_policy_attachment" "attach_xray_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" +} + +resource "aws_lambda_function" "sampleLambdaFunction" { + function_name = var.function_name + runtime = var.runtime + timeout = 300 + handler = "com.amazon.sampleapp.LambdaHandler::handleRequest" + role = aws_iam_role.lambda_role.arn + filename = "${path.module}/../build/distributions/lambda-function.zip" + source_code_hash = filebase64sha256("${path.module}/../build/distributions/lambda-function.zip") + architectures = [var.architecture] + memory_size = 512 + tracing_config { + mode = var.lambda_tracing_mode + } + layers = var.adot_layer_arn != null && var.adot_layer_arn != "" ? [var.adot_layer_arn] : [] + environment { + variables = var.adot_layer_arn != null && var.adot_layer_arn != "" ? { + AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument" + } : {} + } +} + +### API Gateway proxy to Lambda function +resource "aws_api_gateway_rest_api" "apigw_lambda_api" { + name = var.api_gateway_name +} + +resource "aws_api_gateway_resource" "apigw_lambda_resource" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + parent_id = aws_api_gateway_rest_api.apigw_lambda_api.root_resource_id + path_part = "lambda" +} + +resource "aws_api_gateway_method" "apigw_lambda_method" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = "ANY" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "apigw_lambda_integration" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = aws_api_gateway_method.apigw_lambda_method.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.sampleLambdaFunction.invoke_arn +} + +resource "aws_api_gateway_deployment" "apigw_lambda_deployment" { + depends_on = [ + aws_api_gateway_integration.apigw_lambda_integration + ] + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id +} + +resource "aws_api_gateway_stage" "test" { + stage_name = "default" + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + deployment_id = aws_api_gateway_deployment.apigw_lambda_deployment.id + xray_tracing_enabled = var.apigw_tracing_enabled +} + +resource "aws_lambda_permission" "apigw_lambda_invoke" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.sampleLambdaFunction.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.apigw_lambda_api.execution_arn}/*/*" +} + +# Output the API Gateway URL +output "invoke_url" { + value = "${aws_api_gateway_stage.test.invoke_url}/lambda" +} diff --git a/sample-apps/apigateway-lambda/terraform/variables.tf b/sample-apps/apigateway-lambda/terraform/variables.tf new file mode 100644 index 0000000000..2dc99f0685 --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/variables.tf @@ -0,0 +1,43 @@ +## Lambda function related configurations +variable "function_name" { + type = string + description = "Name of sample app function" + default = "aws-opentelemetry-distro-java" +} + +variable "architecture" { + type = string + description = "Lambda function architecture, either arm64 or x86_64" + default = "x86_64" +} + +variable "runtime" { + type = string + description = "Java runtime version used for Lambda Function" + default = "java17" +} + +variable "lambda_tracing_mode" { + type = string + description = "Lambda function tracing mode" + default = "Active" +} + +variable "adot_layer_arn" { + type = string + description = "ARN of the ADOT JAVA layer" + default = null +} + +## API Gateway related configurations +variable "api_gateway_name" { + type = string + description = "Name of API gateway to create" + default = "aws-opentelemetry-distro-java" +} + +variable "apigw_tracing_enabled" { + type = string + description = "API Gateway REST API tracing enabled or not" + default = "true" +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f4ec99f115..decf81d145 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include(":smoke-tests:spring-boot") include(":sample-apps:springboot") include(":sample-apps:spark") include(":sample-apps:spark-awssdkv1") +include(":sample-apps:apigateway-lambda") // Used for contract tests include("appsignals-tests:images:mock-collector")