diff --git a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs index 3328d268..8cf4fcbc 100644 --- a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs +++ b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs @@ -48,7 +48,12 @@ internal sealed class AwsAttributeKeys internal static readonly string AttributeAWSDynamoTableName = "aws.table_name"; internal static readonly string AttributeAWSSQSQueueUrl = "aws.queue_url"; + internal static readonly string AttributeAWSLambdaResourceMappingId = "aws.lambda.resource_mapping.id"; internal static readonly string AttributeAWSS3Bucket = "aws.s3.bucket"; + internal static readonly string AttributeAWSSecretsManagerSecretArn = "aws.secretsmanager.secret.arn"; + internal static readonly string AttributeAWSSNSTopicArn = "aws.sns.topic.arn"; + internal static readonly string AttributeAWSStepFunctionsActivityArn = "aws.stepfunctions.activity.arn"; + internal static readonly string AttributeAWSStepFunctionsStateMachineArn = "aws.stepfunctions.state_machine.arn"; internal static readonly string AttributeAWSBedrockGuardrailId = "aws.bedrock.guardrail.id"; internal static readonly string AttributeAWSBedrockAgentId = "aws.bedrock.agent.id"; diff --git a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs index a10e65eb..c4834fe0 100644 --- a/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs +++ b/src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs @@ -42,8 +42,12 @@ internal class AwsMetricAttributeGenerator : IMetricAttributeGenerator // Normalized remote service names for supported AWS services private static readonly string NormalizedDynamoDBServiceName = "AWS::DynamoDB"; private static readonly string NormalizedKinesisServiceName = "AWS::Kinesis"; + private static readonly string NormalizedLambdaServiceName = "AWS::Lambda"; private static readonly string NormalizedS3ServiceName = "AWS::S3"; + private static readonly string NormalizedSecretsManagerServiceName = "AWS::SecretsManager"; + private static readonly string NormalizedSNSServiceName = "AWS::SNS"; private static readonly string NormalizedSQSServiceName = "AWS::SQS"; + private static readonly string NormalizedStepFunctionsName = "AWS::StepFunctions"; private static readonly string NormalizedBedrockServiceName = "AWS::Bedrock"; private static readonly string NormalizedBedrockRuntimeServiceName = "AWS::BedrockRuntime"; private static readonly string DbConnectionResourceType = "DB::Connection"; @@ -358,12 +362,20 @@ private static string NormalizeRemoteServiceName(Activity span, string serviceNa case "AmazonKinesis": // AWS SDK v1 case "Kinesis": // AWS SDK v2 return NormalizedKinesisServiceName; + case "Lambda": + return NormalizedLambdaServiceName; case "Amazon S3": // AWS SDK v1 case "S3": // AWS SDK v2 return NormalizedS3ServiceName; + case "Secrets Manager": + return NormalizedSecretsManagerServiceName; + case "SNS": + return NormalizedSNSServiceName; case "AmazonSQS": // AWS SDK v1 case "Sqs": // AWS SDK v2 return NormalizedSQSServiceName; + case "SFN": + return NormalizedStepFunctionsName; case "Bedrock": case "Bedrock Agent": case "Bedrock Agent Runtime": @@ -398,11 +410,28 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa remoteResourceType = NormalizedKinesisServiceName + "::Stream"; remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSKinesisStreamName)); } + else if (IsKeyPresent(span, AttributeAWSLambdaResourceMappingId)) + { + remoteResourceType = NormalizedLambdaServiceName + "::EventSourceMapping"; + remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSLambdaResourceMappingId)); + } else if (IsKeyPresent(span, AttributeAWSS3Bucket)) { remoteResourceType = NormalizedS3ServiceName + "::Bucket"; remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSS3Bucket)); } + else if (IsKeyPresent(span, AttributeAWSSecretsManagerSecretArn)) + { + remoteResourceType = NormalizedSecretsManagerServiceName + "::Secret"; + remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSecretsManagerSecretArn))?.Split(':').Last(); + cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSecretsManagerSecretArn)); + } + else if (IsKeyPresent(span, AttributeAWSSNSTopicArn)) + { + remoteResourceType = NormalizedSNSServiceName + "::Topic"; + remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSNSTopicArn))?.Split(':').Last(); + cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSNSTopicArn)); + } else if (IsKeyPresent(span, AttributeAWSSQSQueueName)) { remoteResourceType = NormalizedSQSServiceName + "::Queue"; @@ -415,6 +444,18 @@ private static void SetRemoteResourceTypeAndIdentifier(Activity span, ActivityTa remoteResourceIdentifier = EscapeDelimiters(GetQueueName((string?)span.GetTagItem(AttributeAWSSQSQueueUrl))); cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSSQSQueueUrl)); } + else if (IsKeyPresent(span, AttributeAWSStepFunctionsActivityArn)) + { + remoteResourceType = NormalizedStepFunctionsName + "::Activity"; + remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsActivityArn))?.Split(':').Last(); + cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsActivityArn)); + } + else if (IsKeyPresent(span, AttributeAWSStepFunctionsStateMachineArn)) + { + remoteResourceType = NormalizedStepFunctionsName + "::StateMachine"; + remoteResourceIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsStateMachineArn))?.Split(':').Last(); + cloudformationPrimaryIdentifier = EscapeDelimiters((string?)span.GetTagItem(AttributeAWSStepFunctionsStateMachineArn)); + } else if (IsKeyPresent(span, AttributeAWSBedrockGuardrailId)) { remoteResourceType = NormalizedBedrockServiceName + "::Guardrail"; diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs index 7f6beee4..c5036db4 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSSemanticConventions.cs @@ -15,6 +15,11 @@ internal static class AWSSemanticConventions public const string AttributeAWSSQSQueueName = "aws.sqs.queue_name"; public const string AttributeAWSS3BucketName = "aws.s3.bucket"; public const string AttributeAWSKinesisStreamName = "aws.kinesis.stream_name"; + public const string AttributeAWSLambdaResourceMappingId = "aws.lambda.resource_mapping.id"; + public const string AttributeAWSSecretsManagerSecretArn = "aws.secretsmanager.secret.arn"; + public const string AttributeAWSSNSTopicArn = "aws.sns.topic.arn"; + public const string AttributeAWSStepFunctionsActivityArn = "aws.stepfunctions.activity.arn"; + public const string AttributeAWSStepFunctionsStateMachineArn = "aws.stepfunctions.state_machine.arn"; // AWS Bedrock service attributes not yet defined in semantic conventions public const string AttributeAWSBedrockGuardrailId = "aws.bedrock.guardrail.id"; diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs index 6f55f3b7..6a5f07e4 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs @@ -13,6 +13,10 @@ internal class AWSServiceHelper { AWSServiceType.SQSService, new List { "QueueUrl", "QueueName" } }, { AWSServiceType.S3Service, new List { "BucketName" } }, { AWSServiceType.KinesisService, new List { "StreamName" } }, + { AWSServiceType.LambdaService, new List { "UUID" } }, + { AWSServiceType.SecretsManagerService, new List { "SecretId" } }, + { AWSServiceType.SNSService, new List { "TopicArn" } }, + { AWSServiceType.StepFunctionsService, new List { "ActivityArn", "StateMachineArn" } }, { AWSServiceType.BedrockRuntimeService, new List { "ModelId" } }, { AWSServiceType.BedrockAgentService, new List { "AgentId", "KnowledgeBaseId", "DataSourceId" } }, { AWSServiceType.BedrockAgentRuntimeService, new List { "AgentId", "KnowledgeBaseId" } }, @@ -20,6 +24,7 @@ internal class AWSServiceHelper internal static IReadOnlyDictionary> ServiceResponseParameterMap = new Dictionary>() { + { AWSServiceType.SecretsManagerService, new List { "ARN" } }, { AWSServiceType.SQSService, new List { "QueueUrl" } }, { AWSServiceType.BedrockService, new List { "GuardrailId" } }, { AWSServiceType.BedrockAgentService, new List { "AgentId", "DataSourceId" } }, @@ -32,6 +37,12 @@ internal class AWSServiceHelper { "QueueName", AWSSemanticConventions.AttributeAWSSQSQueueName }, { "BucketName", AWSSemanticConventions.AttributeAWSS3BucketName }, { "StreamName", AWSSemanticConventions.AttributeAWSKinesisStreamName }, + { "TopicArn", AWSSemanticConventions.AttributeAWSSNSTopicArn }, + { "ARN", AWSSemanticConventions.AttributeAWSSecretsManagerSecretArn }, + { "SecretId", AWSSemanticConventions.AttributeAWSSecretsManagerSecretArn }, + { "ActivityArn", AWSSemanticConventions.AttributeAWSStepFunctionsActivityArn }, + { "StateMachineArn", AWSSemanticConventions.AttributeAWSStepFunctionsStateMachineArn }, + { "UUID", AWSSemanticConventions.AttributeAWSLambdaResourceMappingId }, { "ModelId", AWSSemanticConventions.AttributeGenAiModelId }, { "GuardrailId", AWSSemanticConventions.AttributeAWSBedrockGuardrailId }, { "AgentId", AWSSemanticConventions.AttributeAWSBedrockAgentId }, diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs index db0af158..df1c06fc 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs @@ -10,6 +10,9 @@ internal class AWSServiceType internal const string SNSService = "SNS"; internal const string S3Service = "S3"; internal const string KinesisService = "Kinesis"; + internal const string LambdaService = "Lambda"; + internal const string SecretsManagerService = "Secrets Manager"; + internal const string StepFunctionsService = "SFN"; internal const string BedrockService = "Bedrock"; internal const string BedrockRuntimeService = "Bedrock Runtime"; internal const string BedrockAgentService = "Bedrock Agent"; @@ -27,9 +30,18 @@ internal static bool IsSnsService(string service) internal static bool IsS3Service(string service) => S3Service.Equals(service, StringComparison.OrdinalIgnoreCase); + internal static bool IsLambdaService(string service) + => LambdaService.Equals(service, StringComparison.OrdinalIgnoreCase); + internal static bool IsKinesisService(string service) => KinesisService.Equals(service, StringComparison.OrdinalIgnoreCase); + internal static bool IsSecretsManagerService(string service) + => SecretsManagerService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsStepFunctionsService(string service) + => StepFunctionsService.Equals(service, StringComparison.OrdinalIgnoreCase); + internal static bool IsBedrockService(string service) => BedrockService.Equals(service, StringComparison.OrdinalIgnoreCase); diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs index bf5e0cbd..18a0b19f 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs @@ -164,6 +164,20 @@ private static void AddRequestSpecificInformation(Activity activity, IRequestCon } } + // for secrets manager, only extract SecretId from request if it is a secret ARN. + if (AWSServiceType.IsSecretsManagerService(service) && parameter == "SecretId") + { + var secretId = property.GetValue(request); + if (secretId != null) + { + var secretIdString = secretId.ToString(); + if (secretIdString != null && !secretIdString.StartsWith("arn:aws:secretsmanager:")) + { + continue; + } + } + } + if (AWSServiceHelper.ParameterAttributeMap.TryGetValue(parameter, out var attribute)) { activity.SetTag(attribute, property.GetValue(request)); diff --git a/test/AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests/AwsMetricAttributesGeneratorTest.cs b/test/AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests/AwsMetricAttributesGeneratorTest.cs index 024efec9..bf0f44ed 100644 --- a/test/AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests/AwsMetricAttributesGeneratorTest.cs +++ b/test/AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests/AwsMetricAttributesGeneratorTest.cs @@ -526,7 +526,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerAddress, "abc.com" }, { AttributeServerPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when server port is string attributesCombination = new Dictionary @@ -536,7 +536,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerAddress, "abc.com" }, { AttributeServerPort, "3306" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when server port is int32 attributesCombination = new Dictionary @@ -546,7 +546,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerAddress, "abc.com" }, { AttributeServerPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate behaviour of DB_NAME with '|' char, SERVER_ADDRESS and SERVER_PORT exist attributesCombination = new Dictionary @@ -556,7 +556,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerAddress, "abc.com" }, { AttributeServerPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name^|special|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name^|special|abc.com|3306", "db_name^|special|abc.com|3306", false); // Validate behaviour of DB_NAME with '^' char, SERVER_ADDRESS and SERVER_PORT exist attributesCombination = new Dictionary @@ -566,7 +566,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerAddress, "abc.com" }, { AttributeServerPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name^^special|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name^^special|abc.com|3306", "db_name^^special|abc.com|3306", false); // Validate behaviour of DB_NAME, SERVER_ADDRESS exist attributesCombination = new Dictionary @@ -575,7 +575,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbName, "db_name" }, { AttributeServerAddress, "abc.com" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", "db_name|abc.com", false); // Validate behaviour of SERVER_ADDRESS exist attributesCombination = new Dictionary @@ -583,7 +583,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbSystem, "mysql" }, { AttributeServerAddress, "abc.com" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "abc.com", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "abc.com", "abc.com", false); // Validate behaviour of SERVER_PORT exist this.spanDataMock = this.testSource.StartActivity("test", ActivityKind.Client); @@ -604,7 +604,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeNetPeerName, "abc.com" }, { AttributeNetPeerPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when AttributeNetPeerPort is int32 attributesCombination = new Dictionary @@ -614,7 +614,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeNetPeerName, "abc.com" }, { AttributeNetPeerPort, 3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when AttributeNetPeerPort is string attributesCombination = new Dictionary @@ -624,7 +624,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeNetPeerName, "abc.com" }, { AttributeNetPeerPort, "3306" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate behaviour of DB_NAME, NET_PEER_NAME exist attributesCombination = new Dictionary @@ -633,7 +633,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbName, "db_name" }, { AttributeNetPeerName, "abc.com" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", "db_name|abc.com", false); // Validate behaviour of NET_PEER_NAME exist attributesCombination = new Dictionary @@ -641,7 +641,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbSystem, "mysql" }, { AttributeNetPeerName, "abc.com" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "abc.com", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "abc.com", "abc.com", false); // Validate behaviour of NET_PEER_PORT exist this.spanDataMock = this.testSource.StartActivity("test", ActivityKind.Client); @@ -662,7 +662,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerSocketAddress, "abc.com" }, { AttributeServerSocketPort, (long)3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when AttributeServerSocketPort is int32 attributesCombination = new Dictionary @@ -672,7 +672,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerSocketAddress, "abc.com" }, { AttributeServerSocketPort, 3306 }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate BuildDbConnection string when AttributeServerSocketPort is string attributesCombination = new Dictionary @@ -682,7 +682,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeServerSocketAddress, "abc.com" }, { AttributeServerSocketPort, "3306" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com|3306", "db_name|abc.com|3306", false); // Validate behaviour of DB_NAME, SERVER_SOCKET_ADDRESS exist attributesCombination = new Dictionary @@ -691,7 +691,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbName, "db_name" }, { AttributeServerSocketAddress, "abc.com" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|abc.com", "db_name|abc.com", false); // Validate behaviour of SERVER_SOCKET_PORT exist this.spanDataMock = this.testSource.StartActivity("test", ActivityKind.Client); @@ -722,7 +722,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbName, "db_name" }, { AttributeDbConnectionString, "mysql://test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com:3306/petclinic" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "db_name|test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", "db_name|test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", false); // Validate behaviour of DB_CONNECTION_STRING attributesCombination = new Dictionary @@ -730,7 +730,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbSystem, "mysql" }, { AttributeDbConnectionString, "mysql://test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com:3306/petclinic" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", "test-apm.cluster-cnrw3s3ddo7n.us-east-1.rds.amazonaws.com|3306", false); // Validate behaviour of DB_CONNECTION_STRING exist without port attributesCombination = new Dictionary @@ -738,7 +738,7 @@ public void TestDBClientSpanWithRemoteResourceAttributes() { AttributeDbSystem, "mysql" }, { AttributeDbConnectionString, "http://dbserver" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "dbserver|80", false); + this.ValidateRemoteResourceAttributes(attributesCombination, "DB::Connection", "dbserver|80", "dbserver|80", false); // Validate behaviour of DB_NAME and invalid DB_CONNECTION_STRING exist this.spanDataMock = this.testSource.StartActivity("test", ActivityKind.Client); @@ -876,106 +876,168 @@ public void TestSdkClientSpanWithRemoteResourceAttributes() { { AttributeAWSS3Bucket, "aws_s3_bucket_name" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::S3::Bucket", "aws_s3_bucket_name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::S3::Bucket", "aws_s3_bucket_name", "aws_s3_bucket_name"); + // when QueueName and QueueUrl are both available, QueueName is used as resource identifier. QueueUrl is always used as the CFN primary identifier. attributesCombination = new Dictionary { { AttributeAWSSQSQueueName, "aws_queue_name" }, + { AttributeAWSSQSQueueUrl, "https://sqs.us-east-2.amazonaws.com/123456789012/Queue" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SQS::Queue", "aws_queue_name"); - attributesCombination[AttributeAWSSQSQueueUrl] = "https://sqs.us-east-2.amazonaws.com/123456789012/Queue"; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SQS::Queue", "aws_queue_name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SQS::Queue", "aws_queue_name", "https://sqs.us-east-2.amazonaws.com/123456789012/Queue"); attributesCombination[AttributeAWSSQSQueueUrl] = "invalidUrl"; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SQS::Queue", "aws_queue_name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SQS::Queue", "aws_queue_name", "invalidUrl"); attributesCombination = new Dictionary { { AttributeAWSKinesisStreamName, "aws_stream_name" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Kinesis::Stream", "aws_stream_name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Kinesis::Stream", "aws_stream_name", "aws_stream_name"); attributesCombination = new Dictionary { { AttributeAWSDynamoTableName, "aws_table_name" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table_name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table_name", "aws_table_name"); // validate behavior of AttributeAWSDynamoTableName with special chars('|', '^') attributesCombination = new Dictionary { { AttributeAWSDynamoTableName, "aws_table|name" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table^|name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table^|name", "aws_table^|name"); attributesCombination = new Dictionary { { AttributeAWSDynamoTableName, "aws_table^name" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table^^name"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::DynamoDB::Table", "aws_table^^name", "aws_table^^name"); + + attributesCombination = new Dictionary + { + { AttributeAWSLambdaResourceMappingId, "aws_event_source_mapping_id" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_id", "aws_event_source_mapping_id"); + + attributesCombination = new Dictionary + { + { AttributeAWSLambdaResourceMappingId, "aws_event_source_mapping_^id" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Lambda::EventSourceMapping", "aws_event_source_mapping_^^id", "aws_event_source_mapping_^^id"); + + attributesCombination = new Dictionary + { + { AttributeAWSSecretsManagerSecretArn, "arn:aws:secretsmanager:us-west-2:123456789012:secret:aws_secret_arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SecretsManager::Secret", "aws_secret_arn", "arn:aws:secretsmanager:us-west-2:123456789012:secret:aws_secret_arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSSecretsManagerSecretArn, "arn:aws:secretsmanager:us-west-2:123456789012:secret:aws_secret_^arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SecretsManager::Secret", "aws_secret_^^arn", "arn:aws:secretsmanager:us-west-2:123456789012:secret:aws_secret_^^arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSSNSTopicArn, "arn:aws:sns:us-west-2:012345678901:aws_topic_arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SNS::Topic", "aws_topic_arn", "arn:aws:sns:us-west-2:012345678901:aws_topic_arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSSNSTopicArn, "arn:aws:sns:us-west-2:012345678901:aws_topic_^arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::SNS::Topic", "aws_topic_^^arn", "arn:aws:sns:us-west-2:012345678901:aws_topic_^^arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSStepFunctionsActivityArn, "arn:aws:states:us-west-2:012345678901:activity:aws_activity_arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::StepFunctions::Activity", "aws_activity_arn", "arn:aws:states:us-west-2:012345678901:activity:aws_activity_arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSStepFunctionsActivityArn, "arn:aws:states:us-west-2:012345678901:activity:aws_activity_^arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::StepFunctions::Activity", "aws_activity_^^arn", "arn:aws:states:us-west-2:012345678901:activity:aws_activity_^^arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSStepFunctionsStateMachineArn, "arn:aws:states:us-west-2:012345678901:stateMachine:aws_state_machine_arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::StepFunctions::StateMachine", "aws_state_machine_arn", "arn:aws:states:us-west-2:012345678901:stateMachine:aws_state_machine_arn"); + + attributesCombination = new Dictionary + { + { AttributeAWSStepFunctionsStateMachineArn, "arn:aws:states:us-west-2:012345678901:stateMachine:aws_state_machine_^arn" }, + }; + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::StepFunctions::StateMachine", "aws_state_machine_^^arn", "arn:aws:states:us-west-2:012345678901:stateMachine:aws_state_machine_^^arn"); attributesCombination = new Dictionary { { AttributeAWSBedrockGuardrailId, "aws_guardrail_id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Guardrail", "aws_guardrail_id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Guardrail", "aws_guardrail_id", "aws_guardrail_id"); attributesCombination = new Dictionary { { AttributeAWSBedrockGuardrailId, "aws_guardrail_^id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Guardrail", "aws_guardrail_^^id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Guardrail", "aws_guardrail_^^id", "aws_guardrail_^^id"); attributesCombination = new Dictionary { { AttributeGenAiModelId, "gen_ai_model_id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Model", "gen_ai_model_id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Model", "gen_ai_model_id", "gen_ai_model_id"); attributesCombination = new Dictionary { { AttributeGenAiModelId, "gen_ai_model_^id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Model", "gen_ai_model_^^id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Model", "gen_ai_model_^^id", "gen_ai_model_^^id"); attributesCombination = new Dictionary { { AttributeAWSBedrockAgentId, "aws_agent_id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Agent", "aws_agent_id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Agent", "aws_agent_id", "aws_agent_id"); attributesCombination = new Dictionary { { AttributeAWSBedrockAgentId, "aws_agent_^id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Agent", "aws_agent_^^id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::Agent", "aws_agent_^^id", "aws_agent_^^id"); attributesCombination = new Dictionary { { AttributeAWSBedrockKnowledgeBaseId, "aws_knowledge_base_id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::KnowledgeBase", "aws_knowledge_base_id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::KnowledgeBase", "aws_knowledge_base_id", "aws_knowledge_base_id"); attributesCombination = new Dictionary { { AttributeAWSBedrockKnowledgeBaseId, "aws_knowledge_base_^id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::KnowledgeBase", "aws_knowledge_base_^^id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::KnowledgeBase", "aws_knowledge_base_^^id", "aws_knowledge_base_^^id"); attributesCombination = new Dictionary { { AttributeAWSBedrockDataSourceId, "aws_data_source_id" }, + { AttributeAWSBedrockKnowledgeBaseId, "aws_knowledge_base_id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::DataSource", "aws_data_source_id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::DataSource", "aws_data_source_id", "aws_knowledge_base_id|aws_data_source_id"); attributesCombination = new Dictionary { { AttributeAWSBedrockDataSourceId, "aws_data_source_^id" }, + { AttributeAWSBedrockKnowledgeBaseId, "aws_knowledge_base_^id" }, }; - this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::DataSource", "aws_data_source_^^id"); + this.ValidateRemoteResourceAttributes(attributesCombination, "AWS::Bedrock::DataSource", "aws_data_source_^^id", "aws_knowledge_base_^^id|aws_data_source_^^id"); } - private void ValidateRemoteResourceAttributes(Dictionary attributesCombination, string type, string identifier, bool isAwsServiceTest = true) + private void ValidateRemoteResourceAttributes(Dictionary attributesCombination, string type, string identifier, string cfnIdentifier, bool isAwsServiceTest = true) { this.spanDataMock = this.testSource.StartActivity("test", ActivityKind.Client); foreach (var attribute in attributesCombination) @@ -992,8 +1054,10 @@ private void ValidateRemoteResourceAttributes(Dictionary attribu attributeMap.TryGetValue(MetricAttributeGeneratorConstants.DependencyMetric, out ActivityTagsCollection dependencyMetric); dependencyMetric.TryGetValue(AttributeAWSRemoteResourceType, out var actualAWSRemoteResourceType); dependencyMetric.TryGetValue(AttributeAWSRemoteResourceIdentifier, out var actualAWSRemoteResourceIdentifier); + dependencyMetric.TryGetValue(AttributeAWSCloudformationPrimaryIdentifier, out var actualAWSCloudformationPrimaryIdentifier); Assert.Equal(type, actualAWSRemoteResourceType); Assert.Equal(identifier, actualAWSRemoteResourceIdentifier); + Assert.Equal(cfnIdentifier, actualAWSCloudformationPrimaryIdentifier); this.spanDataMock.Dispose(); } diff --git a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/Program.cs b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/Program.cs index 148d4d67..d75af3bb 100644 --- a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/Program.cs +++ b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/Program.cs @@ -5,7 +5,10 @@ using Amazon.DynamoDBv2; using Amazon.Kinesis; using Amazon.S3; +using Amazon.SecretsManager; +using Amazon.SimpleNotificationService; using Amazon.SQS; +using Amazon.StepFunctions; using TestSimpleApp.AWSSDK.Core; var builder = WebApplication.CreateBuilder(args); @@ -20,6 +23,9 @@ .AddSingleton(provider => new AmazonS3Client(new AmazonS3Config { ServiceURL = "http://localstack:4566", ForcePathStyle = true })) .AddSingleton(provider => new AmazonSQSClient(new AmazonSQSConfig { ServiceURL = "http://localstack:4566" })) .AddSingleton(provider => new AmazonKinesisClient(new AmazonKinesisConfig { ServiceURL = "http://localstack:4566" })) + .AddSingleton(provider => new AmazonSecretsManagerClient(new AmazonSecretsManagerConfig { ServiceURL = "http://localstack:4566" })) + .AddSingleton(provider => new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localstack:4566" })) + .AddSingleton(provider => new AmazonStepFunctionsClient(new AmazonStepFunctionsConfig { ServiceURL = "http://localstack:4566" })) // Bedrock services are not supported by localstack, so we mock the API responses on the aws-application-signals-tests-testsimpleapp server. .AddSingleton(provider => new AmazonBedrockClient(new AmazonBedrockConfig { ServiceURL = "http://localhost:8080" })) .AddSingleton(provider => new AmazonBedrockRuntimeClient(new AmazonBedrockRuntimeConfig { ServiceURL = "http://localhost:8080" })) @@ -30,15 +36,24 @@ .AddKeyedSingleton("fault-s3", new AmazonS3Client(AmazonClientConfigHelper.CreateConfig(true))) .AddKeyedSingleton("fault-sqs", new AmazonSQSClient(AmazonClientConfigHelper.CreateConfig(true))) .AddKeyedSingleton("fault-kinesis", new AmazonKinesisClient(new AmazonKinesisConfig { ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("fault-secretsmanager", new AmazonSecretsManagerClient(new AmazonSecretsManagerConfig { ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("fault-sns", new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("fault-stepfunctions", new AmazonStepFunctionsClient(new AmazonStepFunctionsConfig { ServiceURL = "http://localstack:4566" })) //error client .AddKeyedSingleton("error-ddb", new AmazonDynamoDBClient(AmazonClientConfigHelper.CreateConfig())) .AddKeyedSingleton("error-s3", new AmazonS3Client(AmazonClientConfigHelper.CreateConfig())) .AddKeyedSingleton("error-sqs", new AmazonSQSClient(AmazonClientConfigHelper.CreateConfig())) .AddKeyedSingleton("error-kinesis", new AmazonKinesisClient(new AmazonKinesisConfig { ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("error-secretsmanager", new AmazonSecretsManagerClient(new AmazonSecretsManagerConfig {ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("error-sns", new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localstack:4566" })) + .AddKeyedSingleton("error-stepfunctions", new AmazonStepFunctionsClient(new AmazonStepFunctionsConfig { ServiceURL = "http://localstack:4566" })) .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(); var app = builder.Build(); @@ -121,6 +136,35 @@ app.MapGet("kinesis/fault", (KinesisTests kinesis) => kinesis.Fault()).WithName("kinesis-fault").WithOpenApi(); app.MapGet("kinesis/error", (KinesisTests kinesis) => kinesis.Error()).WithName("kinesis-error").WithOpenApi(); +app.MapGet("secretsmanager/createsecret/some-secret", (SecretsManagerTests secretsManager) => secretsManager.CreateSecret()) + .WithName("create-secret") + .WithOpenApi(); + +app.MapGet("secretsmanager/getsecretvalue/some-secret", (SecretsManagerTests secretsManager) => secretsManager.GetSecretValue()) + .WithName("get-secret-value") + .WithOpenApi(); + +app.MapGet("secretsmanager/fault", (SecretsManagerTests secretsManager) => secretsManager.Fault()).WithName("secretsmanager-fault").WithOpenApi(); +app.MapGet("secretsmanager/error", (SecretsManagerTests secretsManager) => secretsManager.Error()).WithName("secretsmanager-error").WithOpenApi(); + +app.MapGet("sns/publish/some-topic", (SNSTests sns) => sns.Publish()) + .WithName("publish") + .WithOpenApi(); + +app.MapGet("sns/fault", (SNSTests sns) => sns.Fault()).WithName("sns-fault").WithOpenApi(); +app.MapGet("sns/error", (SNSTests sns) => sns.Error()).WithName("sns-error").WithOpenApi(); + +app.MapGet("stepfunctions/describestatemachine/some-state-machine", (StepFunctionsTests stepFunctions) => stepFunctions.DescribeStateMachine()) + .WithName("describe-state-machine") + .WithOpenApi(); + +app.MapGet("stepfunctions/describeactivity/some-activity", (StepFunctionsTests stepFunctions) => stepFunctions.DescribeActivity()) + .WithName("describe-activity") + .WithOpenApi(); + +app.MapGet("stepfunctions/fault", (StepFunctionsTests stepFunctions) => stepFunctions.Fault()).WithName("stepfunctions-fault").WithOpenApi(); +app.MapGet("stepfunctions/error", (StepFunctionsTests stepFunctions) => stepFunctions.Error()).WithName("stepfunctions-error").WithOpenApi(); + app.MapGet("bedrock/getguardrail/get-guardrail", (BedrockTests bedrock) => bedrock.GetGuardrail()) .WithName("get-guardrail") .WithOpenApi(); @@ -169,6 +213,22 @@ .WithName("retrieve") .WithOpenApi(); +// Create some resources in advance to be accessed by tests +async Task PrepareAWSServer(IServiceProvider services) +{ + var snsTests = services.GetRequiredService(); + var stepfunctionsTests = services.GetRequiredService(); + + // Create a topic for the SNS tests + await snsTests.CreateTopic("test-topic"); + + // Create a state machine and activity for the Step Functions tests + await stepfunctionsTests.CreateStateMachine("test-state-machine"); + await stepfunctionsTests.CreateActivity("test-activity"); + + // TODO: create resources for Lambda event source mapping test +} + // Reroute the Bedrock API calls to our mock responses in BedrockTests. While other services use localstack to handle the requests, // we write our own responses with the necessary data to mimic the expected behavior of the Bedrock services. app.MapGet("guardrails/test-guardrail", (BedrockTests bedrock) => bedrock.GetGuardrailResponse()); @@ -185,4 +245,6 @@ app.MapPost("agents/test-agent/agentAliases/test-agent-alias/sessions/test-session/text", (BedrockTests bedrock) => bedrock.InvokeAgentResponse()); app.MapPost("knowledgebases/test-knowledge-base/retrieve", (BedrockTests bedrock) => bedrock.RetrieveResponse()); +await PrepareAWSServer(app.Services); + app.Run(); \ No newline at end of file diff --git a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SNSTests.cs b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SNSTests.cs new file mode 100644 index 00000000..6d0d6978 --- /dev/null +++ b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SNSTests.cs @@ -0,0 +1,31 @@ +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; + +namespace TestSimpleApp.AWSSDK.Core; + +public class SNSTests( + IAmazonSimpleNotificationService sns, + [FromKeyedServices("fault-sns")] IAmazonSimpleNotificationService faultSns, + [FromKeyedServices("error-sns")] IAmazonSimpleNotificationService errorSns, + ILogger logger) : ContractTest(logger) +{ + public Task CreateTopic(string name) + { + return sns.CreateTopicAsync(new CreateTopicRequest { Name = name }); + } + + public Task Publish() + { + return sns.PublishAsync(new PublishRequest { TopicArn = "arn:aws:sns:us-east-1:000000000000:test-topic", Message = "test-message" }); + } + + protected override Task CreateFault(CancellationToken cancellationToken) + { + return faultSns.GetTopicAttributesAsync(new GetTopicAttributesRequest { TopicArn = "arn:aws:sns:us-east-1:000000000000:invalid-topic" }, cancellationToken); + } + + protected override Task CreateError(CancellationToken cancellationToken) + { + return errorSns.PublishAsync(new PublishRequest { TopicArn = "arn:aws:sns:us-east-1:000000000000:test-topic-error" }); + } +} \ No newline at end of file diff --git a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SecretsManagerTests.cs b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SecretsManagerTests.cs new file mode 100644 index 00000000..ac98f848 --- /dev/null +++ b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/SecretsManagerTests.cs @@ -0,0 +1,33 @@ +using Amazon.SecretsManager; +using Amazon.SecretsManager.Model; + +namespace TestSimpleApp.AWSSDK.Core; + +public class SecretsManagerTests( + IAmazonSecretsManager secretsManager, + [FromKeyedServices("fault-secretsmanager")] IAmazonSecretsManager faultSecretsManager, + [FromKeyedServices("error-secretsmanager")] IAmazonSecretsManager errorSecretsManager, + ILogger logger) : ContractTest(logger) +{ + public Task CreateSecret() + { + return secretsManager.CreateSecretAsync(new CreateSecretRequest { + Name = "test-secret", SecretString = "{\"key\":\"test\",\"value\":\"test\"}" + }); + } + + public Task GetSecretValue() + { + return secretsManager.GetSecretValueAsync(new GetSecretValueRequest { SecretId = "test-secret" }); + } + + protected override Task CreateFault(CancellationToken cancellationToken) + { + return faultSecretsManager.CreateSecretAsync(new CreateSecretRequest { Name = "test-secret" }, cancellationToken); + } + + protected override Task CreateError(CancellationToken cancellationToken) + { + return errorSecretsManager.DescribeSecretAsync(new DescribeSecretRequest { SecretId = "arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-error" }); + } +} \ No newline at end of file diff --git a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/StepFunctionsTests.cs b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/StepFunctionsTests.cs new file mode 100644 index 00000000..7b079e1c --- /dev/null +++ b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/StepFunctionsTests.cs @@ -0,0 +1,49 @@ +using Amazon.StepFunctions; +using Amazon.StepFunctions.Model; + +namespace TestSimpleApp.AWSSDK.Core; + +public class StepFunctionsTests( + IAmazonStepFunctions stepFunctions, + [FromKeyedServices("fault-stepfunctions")] IAmazonStepFunctions faultClient, + [FromKeyedServices("error-stepfunctions")] IAmazonStepFunctions errorClient, + ILogger logger) : ContractTest(logger) +{ + public Task CreateStateMachine(string name) + { + return stepFunctions.CreateStateMachineAsync(new CreateStateMachineRequest + { + Name = name, + Definition = "{\"StartAt\":\"TestState\",\"States\":{\"TestState\":{\"Type\":\"Pass\",\"End\":true,\"Result\":\"Result\"}}}", + RoleArn = "arn:aws:iam::000000000000:role/stepfunctions-role" + }); + } + + public Task CreateActivity(string name) + { + return stepFunctions.CreateActivityAsync(new CreateActivityRequest { Name = name }); + } + + public Task DescribeStateMachine() + { + return stepFunctions.DescribeStateMachineAsync(new DescribeStateMachineRequest { StateMachineArn = "arn:aws:states:us-east-1:000000000000:stateMachine:test-state-machine" }); + } + + public Task DescribeActivity() + { + return stepFunctions.DescribeActivityAsync(new DescribeActivityRequest { ActivityArn = "arn:aws:states:us-east-1:000000000000:activity:test-activity" }); + } + + protected override Task CreateFault(CancellationToken cancellationToken) + { + return faultClient.ListStateMachineVersionsAsync(new ListStateMachineVersionsRequest + { + StateMachineArn = "arn:aws:states:us-east-1:000000000000:stateMachine:invalid-state-machine" + }, cancellationToken); + } + + protected override Task CreateError(CancellationToken cancellationToken) + { + return errorClient.DescribeStateMachineAsync(new DescribeStateMachineRequest { StateMachineArn = "arn:aws:states:us-east-1:000000000000:stateMachine:error-state-machine" }, cancellationToken); + } +} \ No newline at end of file diff --git a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/TestSimpleApp.AWSSDK.Core.csproj b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/TestSimpleApp.AWSSDK.Core.csproj index 8018bd96..f4e82c69 100644 --- a/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/TestSimpleApp.AWSSDK.Core.csproj +++ b/test/contract-tests/images/applications/TestSimpleApp.AWSSDK.Core/TestSimpleApp.AWSSDK.Core.csproj @@ -10,13 +10,16 @@ + + + - - - - + + + + diff --git a/test/contract-tests/tests/test/amazon/awssdk/awssdk_test.py b/test/contract-tests/tests/test/amazon/awssdk/awssdk_test.py index 8493b2da..f4d49028 100644 --- a/test/contract-tests/tests/test/amazon/awssdk/awssdk_test.py +++ b/test/contract-tests/tests/test/amazon/awssdk/awssdk_test.py @@ -28,6 +28,10 @@ _AWS_SQS_QUEUE_URL: str = "aws.queue_url" _AWS_SQS_QUEUE_NAME: str = "aws.sqs.queue_name" _AWS_KINESIS_STREAM_NAME: str = "aws.kinesis.stream_name" +_AWS_SECRETSMANAGER_SECRET_ARN: str = "aws.secretsmanager.secret.arn" +_AWS_SNS_TOPIC_ARN: str = "aws.sns.topic.arn" +_AWS_STEPFUNCTIONS_ACTIVITY_ARN: str = "aws.stepfunctions.activity.arn" +_AWS_STEPFUNCTIONS_STATE_MACHINE_ARN: str = "aws.stepfunctions.state_machine.arn" _AWS_BEDROCK_GUARDRAIL_ID: str = "aws.bedrock.guardrail.id" _AWS_BEDROCK_AGENT_ID: str = "aws.bedrock.agent.id" _AWS_BEDROCK_KNOWLEDGE_BASE_ID: str = "aws.bedrock.knowledge_base.id" @@ -82,9 +86,9 @@ def set_up_dependency_container(cls): ) } cls._local_stack: LocalStackContainer = ( - LocalStackContainer(image="localstack/localstack:3.0.2") + LocalStackContainer(image="localstack/localstack:4.0.0") .with_name("localstack") - .with_services("s3", "sqs", "dynamodb", "kinesis") + .with_services("s3", "secretsmanager", "sns", "sqs", "stepfunctions", "dynamodb", "kinesis") .with_env("DEFAULT_REGION", "us-west-2") .with_kwargs(network=NETWORK_NAME, networking_config=local_stack_networking_config) ) @@ -319,6 +323,214 @@ def test_kinesis_error(self): # span_name="Kinesis.CreateStream", # ) + def test_secretsmanager_create_secret(self): + self.do_test_requests( + "secretsmanager/createsecret/some-secret", + "GET", + 200, + 0, + 0, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="CreateSecret", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier=r"test-secret-[a-zA-Z0-9]{6}$", + cloudformation_primary_identifier=r"arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-[a-zA-Z0-9]{6}$", + request_response_specific_attributes={ + _AWS_SECRETSMANAGER_SECRET_ARN: r"arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-[a-zA-Z0-9]{6}$", + }, + span_name="Secrets Manager.CreateSecret", + ) + + def test_secretsmanager_get_secret_value(self): + self.do_test_requests( + "secretsmanager/getsecretvalue/some-secret", + "GET", + 200, + 0, + 0, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="GetSecretValue", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier=r"test-secret-[a-zA-Z0-9]{6}$", + cloudformation_primary_identifier=r"arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-[a-zA-Z0-9]{6}$", + request_response_specific_attributes={ + _AWS_SECRETSMANAGER_SECRET_ARN: r"arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-[a-zA-Z0-9]{6}$", + }, + span_name="Secrets Manager.GetSecretValue", + ) + + def test_secretsmanager_error(self): + self.do_test_requests( + "secretsmanager/error", + "GET", + 400, + 1, + 0, + rpc_service="Secrets Manager", + remote_service="AWS::SecretsManager", + remote_operation="DescribeSecret", + remote_resource_type="AWS::SecretsManager::Secret", + remote_resource_identifier="test-secret-error", + cloudformation_primary_identifier="arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-error", + request_response_specific_attributes={ + _AWS_SECRETSMANAGER_SECRET_ARN: "arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-error", + }, + span_name="Secrets Manager.DescribeSecret", + ) + + # TODO: https://github.com/aws-observability/aws-otel-dotnet-instrumentation/issues/83 + # def test_secretsmanager_fault(self): + # self.do_test_requests( + # "secretsmanager/fault", + # "GET", + # 500, + # 0, + # 1, + # rpc_service="Secrets Manager", + # remote_service="AWS::SecretsManager", + # remote_operation="CreateSecret", + # remote_resource_type="AWS::SecretsManager::Secret", + # remote_resource_identifier="test-secret-error", + # cloudformation_primary_identifier="arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-error", + # request_response_specific_attributes={ + # _AWS_SECRETSMANAGER_SECRET_ARN: "arn:aws:secretsmanager:us-east-1:000000000000:secret:test-secret-error", + # }, + # span_name="Secrets Manager.CreateSecret", + # ) + + def test_sns_publish(self): + self.do_test_requests( + "sns/publish/some-topic", + "GET", + 200, + 0, + 0, + remote_service="AWS::SNS", + remote_operation="Publish", + remote_resource_type="AWS::SNS::Topic", + remote_resource_identifier="test-topic", + cloudformation_primary_identifier="arn:aws:sns:us-east-1:000000000000:test-topic", + request_response_specific_attributes={ + _AWS_SNS_TOPIC_ARN: "arn:aws:sns:us-east-1:000000000000:test-topic", + }, + span_name="SNS.Publish", + ) + + def test_sns_error(self): + self.do_test_requests( + "sns/error", + "GET", + 400, + 1, + 0, + remote_service="AWS::SNS", + remote_operation="Publish", + remote_resource_type="AWS::SNS::Topic", + remote_resource_identifier="test-topic-error", + cloudformation_primary_identifier="arn:aws:sns:us-east-1:000000000000:test-topic-error", + request_response_specific_attributes={ + _AWS_SNS_TOPIC_ARN: "arn:aws:sns:us-east-1:000000000000:test-topic-error", + }, + span_name="SNS.Publish", + ) + + # TODO: https://github.com/aws-observability/aws-otel-dotnet-instrumentation/issues/83 + # def test_sns_fault(self): + # self.do_test_requests( + # "sns/fault", + # "GET", + # 500, + # 0, + # 1, + # remote_service="AWS::SNS", + # remote_operation="GetTopicAttributes", + # remote_resource_type="AWS::SNS::Topic", + # remote_resource_identifier="invalid-topic", + # cloudformation_primary_identifier="arn:aws:sns:us-east-1:000000000000:invalid-topic", + # request_response_specific_attributes={ + # _AWS_SNS_TOPIC_ARN: "arn:aws:sns:us-east-1:000000000000:invalid-topic",}, + # span_name="SNS.GetTopicAttributes" + # ) + + def test_stepfunctions_describe_state_machine(self): + self.do_test_requests( + "stepfunctions/describestatemachine/some-state-machine", + "GET", + 200, + 0, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeStateMachine", + remote_resource_type="AWS::StepFunctions::StateMachine", + remote_resource_identifier="test-state-machine", + cloudformation_primary_identifier="arn:aws:states:us-east-1:000000000000:stateMachine:test-state-machine", + request_response_specific_attributes={ + _AWS_STEPFUNCTIONS_STATE_MACHINE_ARN: "arn:aws:states:us-east-1:000000000000:stateMachine:test-state-machine", + }, + span_name="SFN.DescribeStateMachine", + ) + + def test_stepfunctions_describe_activity(self): + self.do_test_requests( + "stepfunctions/describeactivity/some-activity", + "GET", + 200, + 0, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeActivity", + remote_resource_type="AWS::StepFunctions::Activity", + remote_resource_identifier="test-activity", + cloudformation_primary_identifier="arn:aws:states:us-east-1:000000000000:activity:test-activity", + request_response_specific_attributes={ + _AWS_STEPFUNCTIONS_ACTIVITY_ARN: "arn:aws:states:us-east-1:000000000000:activity:test-activity", + }, + span_name="SFN.DescribeActivity", + ) + + def test_stepfunctions_error(self): + self.do_test_requests( + "stepfunctions/error", + "GET", + 400, + 1, + 0, + rpc_service="SFN", + remote_service="AWS::StepFunctions", + remote_operation="DescribeStateMachine", + remote_resource_type="AWS::StepFunctions::StateMachine", + remote_resource_identifier="error-state-machine", + cloudformation_primary_identifier="arn:aws:states:us-east-1:000000000000:stateMachine:error-state-machine", + request_response_specific_attributes={ + _AWS_STEPFUNCTIONS_STATE_MACHINE_ARN: "arn:aws:states:us-east-1:000000000000:stateMachine:error-state-machine", + }, + span_name="SFN.DescribeStateMachine", + ) + + + # TODO: https://github.com/aws-observability/aws-otel-dotnet-instrumentation/issues/83 + # def test_stepfunctions_fault(self): + # self.do_test_requests( + # "stepfunctions/fault", + # "GET", + # 500, + # 0, + # 1, + # rpc_service="SFN", + # remote_service="AWS::StepFunctions", + # remote_operation="ListStateMachineVersions", + # remote_resource_type="AWS::StepFunctions::StateMachine", + # remote_resource_identifier="invalid-state-machine", + # cloudformation_primary_identifier="arn:aws:states:us-east-1:000000000000:stateMachine:invalid-state-machine", + # request_response_specific_attributes={ + # _AWS_STEPFUNCTIONS_STATE_MACHINE_ARN: "arn:aws:states:us-east-1:000000000000:stateMachine:invalid-state-machine",}, + # span_name="SFN.ListStateMachineVersions", + # ) + def test_bedrock_get_guardrail(self): self.do_test_requests( "bedrock/getguardrail/get-guardrail", @@ -331,7 +543,7 @@ def test_bedrock_get_guardrail(self): remote_operation="GetGuardrail", remote_resource_type="AWS::Bedrock::Guardrail", remote_resource_identifier="test-guardrail", - cloudformation_primiary_identifier="test-guardrail", + cloudformation_primary_identifier="test-guardrail", request_response_specific_attributes={ _AWS_BEDROCK_GUARDRAIL_ID: "test-guardrail", }, @@ -590,6 +802,8 @@ def test_bedrock_agent_get_data_source(self): span_name="Bedrock Agent.GetDataSource", ) + # TODO: add contract test for Lambda event source mapping resource + @override def _assert_aws_span_attributes(self, resource_scope_spans: List[ResourceScopeSpan], path: str, **kwargs) -> None: target_spans: List[Span] = [] diff --git a/test/contract-tests/tests/test/amazon/base/contract_test_base.py b/test/contract-tests/tests/test/amazon/base/contract_test_base.py index b9bb2124..2f587a61 100644 --- a/test/contract-tests/tests/test/amazon/base/contract_test_base.py +++ b/test/contract-tests/tests/test/amazon/base/contract_test_base.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 import time +import re from logging import INFO, Logger, getLogger from typing import Dict, List from unittest import TestCase @@ -162,11 +163,27 @@ def _get_attributes_dict(self, attributes_list: List[KeyValue]) -> Dict[str, Any attributes_dict[key] = value return attributes_dict + def _is_regex(self, value: str) -> bool: + try: + re.compile(value) + return True + except re.error: + return False + def _assert_str_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, expected_value: str): self.assertIn(key, attributes_dict) actual_value: AnyValue = attributes_dict[key] self.assertIsNotNone(actual_value) - self.assertEqual(expected_value, actual_value.string_value) + if self._is_regex(expected_value): + self.assertRegex(actual_value.string_value, expected_value) + else: + self.assertEqual(expected_value, actual_value.string_value) + + def _assert_regex_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, expected_value: str): + self.assertIn(key, attributes_dict) + actual_value: AnyValue = attributes_dict[key] + self.assertIsNotNone(actual_value) + self.assertRegex(actual_value.string_value, expected_value) def _assert_int_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, expected_value: int) -> None: self.assertIn(key, attributes_dict)