From cb311053b9c2f0d2925639217fdac7225a390d41 Mon Sep 17 00:00:00 2001 From: Michael He <53622546+yiyuan-he@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:34:07 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Add=20auto-instrumentation=20support=20?= =?UTF-8?q?for=20Step=20Functions,=20SNS,=20Secre=E2=80=A6=20(#899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### *Description of changes:* Changes in ADOT package to support new AWS resources in Java V1 & V2 SDKs. Related changes for V1 SDK in upstream package: https://github.com/mxiamxia/opentelemetry-java-instrumentation/pull/11 Related changes for V2 SDK in upstream package: https://github.com/mxiamxia/opentelemetry-java-instrumentation/pull/12 These changes add auto-instrumentation support for the following AWS resources. Additionally, a new attribute for `aws.remote.resource.cfn.primary.identifier` is also added for each new resource. - Populate `aws.sns.topic.arn` in Span by extracting `TopicArn` from the request body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sns-topic.html - The CFN Id should be the ARN of the Topic. - Populate `aws.secretsmanager.secret.arn` in Span by extracting `ARN` from response body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html - The CFN Id should be the ARN of the Secret. - Populate `aws.stepfunctions.state_machine.arn` in Span by extracting `stateMachineArn` from the request body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html - The CFN Id should be the ARN of the State Machine. - Populate `aws.stepfunctions.activity.arn` in Span by extracting `activityArn` from the request body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-activity.html - The CFN Id should be the ARN of the Activity. - Populate `aws.lambda.function.name` in Span by extracting `FunctionName` from the request body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html - The CFN Id should be the ARN of the Lambda Function. - Populate `aws.lambda.resource_mapping.id` in Span by extracting `UUID` from the request body. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html - The CFN Id should be the UUID of the Event Source Mapping. ### *Test Plan:* Set up a client-server with auto-instrumentation to verify that the correct span data is being generated. Note: V1 span data is top pic and V2 span data is bottom pic for each resource. `aws.lambda.function.name` lambda-function-name-span-data-verification-v1 lambda-function-name-span-data-verification-v2 `aws.lambda.resource_mapping.id` lambda-resource-mapping-id-span-data-verification-v1 lambda-resource-mapping-id-span-data-verification-v2 `aws.secretsmanager.secret.arn` secretsmanager-secret-arn-span-data-verification-v1 secretsmanager-secret-arn-span-data-verification-v2 `aws.sns.topic.arn` sns-topic-arn-span-data-verification-v1 sns-topic-arn-span-data-verification-v2 `aws.stepfunctions.activity.arn` stepfunctions-activity-arn-span-data-verification-v1 sfn-activity-arn-span-data-verification-v2 `aws.stepfunction.state_machine.arn` stepfunctions-state-machine-arn-span-data-verification-v1 sfn-state-machine-arn-span-data-verification-v2 Metric Attribute Generator Unit Test Screenshot 2024-10-02 at 2 59 35 PM By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- awsagentprovider/build.gradle.kts | 2 + .../javaagent/providers/AwsAttributeKeys.java | 23 +++++ .../AwsMetricAttributeGenerator.java | 95 +++++++++++++++++++ .../AwsMetricAttributeGeneratorTest.java | 43 +++++++++ 4 files changed, 163 insertions(+) diff --git a/awsagentprovider/build.gradle.kts b/awsagentprovider/build.gradle.kts index 5b99011ab7..3e92ad8b1a 100644 --- a/awsagentprovider/build.gradle.kts +++ b/awsagentprovider/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation("io.opentelemetry.contrib:opentelemetry-aws-resources") // Json file reader implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1") + // Import AWS SDK v1 core for ARN parsing utilities + implementation("com.amazonaws:aws-java-sdk-core:1.12.773") // Export configuration compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java index 457e81d77d..ef2999a28a 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java @@ -38,6 +38,9 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_REMOTE_RESOURCE_IDENTIFIER = AttributeKey.stringKey("aws.remote.resource.identifier"); + static final AttributeKey AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER = + AttributeKey.stringKey("aws.remote.resource.cfn.primary.identifier"); + static final AttributeKey AWS_REMOTE_RESOURCE_TYPE = AttributeKey.stringKey("aws.remote.resource.type"); @@ -50,6 +53,26 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_CONSUMER_PARENT_SPAN_KIND = AttributeKey.stringKey("aws.consumer.parent.span.kind"); + static final AttributeKey AWS_STATE_MACHINE_ARN = + AttributeKey.stringKey("aws.stepfunctions.state_machine.arn"); + + static final AttributeKey AWS_STEP_FUNCTIONS_ACTIVITY_ARN = + AttributeKey.stringKey("aws.stepfunctions.activity.arn"); + + static final AttributeKey AWS_SNS_TOPIC_ARN = AttributeKey.stringKey("aws.sns.topic.arn"); + + static final AttributeKey AWS_SECRET_ARN = + AttributeKey.stringKey("aws.secretsmanager.secret.arn"); + + static final AttributeKey AWS_LAMBDA_NAME = + AttributeKey.stringKey("aws.lambda.function.name"); + + static final AttributeKey AWS_LAMBDA_ARN = + AttributeKey.stringKey("aws.lambda.function.arn"); + + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + AttributeKey.stringKey("aws.lambda.resource_mapping.id"); + // use the same AWS Resource attribute name defined by OTel java auto-instr for aws_sdk_v_1_1 // TODO: all AWS specific attributes should be defined in semconv package and reused cross all // otel packages. Related sim - diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java index 68ba1bc824..63c806ea98 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java @@ -42,9 +42,12 @@ import static io.opentelemetry.semconv.SemanticAttributes.SERVER_SOCKET_PORT; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AGENT_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_BUCKET_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_DATA_SOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_KNOWLEDGE_BASE_ID; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_RESOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_SERVICE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_QUEUE_NAME; @@ -54,7 +57,11 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_IDENTIFIER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_TYPE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_SERVICE; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SECRET_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SNS_TOPIC_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STATE_MACHINE_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL; @@ -67,6 +74,7 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.isDBSpan; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.isKeyPresent; +import com.amazonaws.arn.Arn; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; @@ -111,6 +119,10 @@ final class AwsMetricAttributeGenerator implements MetricAttributeGenerator { private static final String NORMALIZED_SQS_SERVICE_NAME = "AWS::SQS"; private static final String NORMALIZED_BEDROCK_SERVICE_NAME = "AWS::Bedrock"; private static final String NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME = "AWS::BedrockRuntime"; + private static final String NORMALIZED_STEPFUNCTIONS_SERVICE_NAME = "AWS::StepFunctions"; + private static final String NORMALIZED_SNS_SERVICE_NAME = "AWS::SNS"; + private static final String NORMALIZED_SECRETSMANAGER_SERVICE_NAME = "AWS::SecretsManager"; + private static final String NORMALIZED_LAMBDA_SERVICE_NAME = "AWS::Lambda"; // Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present. private static final String GRAPHQL = "graphql"; @@ -371,6 +383,18 @@ private static String normalizeRemoteServiceName(SpanData span, String serviceNa case "AmazonBedrockRuntime": // AWS SDK v1 case "BedrockRuntime": // AWS SDK v2 return NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME; + case "AWSStepFunctions": // AWS SDK v1 + case "Sfn": // AWS SDK v2 + return NORMALIZED_STEPFUNCTIONS_SERVICE_NAME; + case "AmazonSNS": + case "Sns": + return NORMALIZED_SNS_SERVICE_NAME; + case "AWSSecretsManager": // AWS SDK v1 + case "SecretsManager": // AWS SDK v2 + return NORMALIZED_SECRETSMANAGER_SERVICE_NAME; + case "AWSLambda": // AWS SDK v1 + case "Lambda": // AWS SDK v2 + return NORMALIZED_LAMBDA_SERVICE_NAME; default: return "AWS::" + serviceName; } @@ -392,6 +416,7 @@ private static String normalizeRemoteServiceName(SpanData span, String serviceNa private static void setRemoteResourceTypeAndIdentifier(SpanData span, AttributesBuilder builder) { Optional remoteResourceType = Optional.empty(); Optional remoteResourceIdentifier = Optional.empty(); + Optional cloudformationPrimaryIdentifier = Optional.empty(); if (isAwsSDKSpan(span)) { if (isKeyPresent(span, AWS_TABLE_NAME)) { @@ -434,18 +459,88 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes remoteResourceType = Optional.of(NORMALIZED_BEDROCK_SERVICE_NAME + "::Model"); remoteResourceIdentifier = Optional.ofNullable(escapeDelimiters(span.getAttributes().get(GEN_AI_REQUEST_MODEL))); + } else if (isKeyPresent(span, AWS_STATE_MACHINE_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_STEPFUNCTIONS_SERVICE_NAME + "::StateMachine"); + remoteResourceIdentifier = + getSfnResourceNameFromArn( + Optional.ofNullable( + escapeDelimiters(span.getAttributes().get(AWS_STATE_MACHINE_ARN)))); + cloudformationPrimaryIdentifier = + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_STATE_MACHINE_ARN))); + } else if (isKeyPresent(span, AWS_STEP_FUNCTIONS_ACTIVITY_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_STEPFUNCTIONS_SERVICE_NAME + "::Activity"); + remoteResourceIdentifier = + getSfnResourceNameFromArn( + Optional.ofNullable( + escapeDelimiters(span.getAttributes().get(AWS_STEP_FUNCTIONS_ACTIVITY_ARN)))); + cloudformationPrimaryIdentifier = + Optional.ofNullable( + escapeDelimiters(span.getAttributes().get(AWS_STEP_FUNCTIONS_ACTIVITY_ARN))); + } else if (isKeyPresent(span, AWS_SNS_TOPIC_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_SNS_SERVICE_NAME + "::Topic"); + remoteResourceIdentifier = + getSnsResourceNameFromArn( + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_SNS_TOPIC_ARN)))); + cloudformationPrimaryIdentifier = + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_SNS_TOPIC_ARN))); + } else if (isKeyPresent(span, AWS_SECRET_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_SECRETSMANAGER_SERVICE_NAME + "::Secret"); + remoteResourceIdentifier = + getSecretsManagerResourceNameFromArn( + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_SECRET_ARN)))); + cloudformationPrimaryIdentifier = + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_SECRET_ARN))); + } else if (isKeyPresent(span, AWS_LAMBDA_NAME)) { + remoteResourceType = Optional.of(NORMALIZED_LAMBDA_SERVICE_NAME + "::Function"); + remoteResourceIdentifier = + getLambdaResourceNameFromAribitraryName( + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_LAMBDA_NAME)))); + } else if (isKeyPresent(span, AWS_LAMBDA_RESOURCE_ID)) { + remoteResourceType = Optional.of(NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping"); + remoteResourceIdentifier = + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_LAMBDA_RESOURCE_ID))); } } else if (isDBSpan(span)) { remoteResourceType = Optional.of(DB_CONNECTION_RESOURCE_TYPE); remoteResourceIdentifier = getDbConnection(span); } + if (cloudformationPrimaryIdentifier.isEmpty()) { + cloudformationPrimaryIdentifier = remoteResourceIdentifier; + } + if (remoteResourceType.isPresent() && remoteResourceIdentifier.isPresent()) { builder.put(AWS_REMOTE_RESOURCE_TYPE, remoteResourceType.get()); builder.put(AWS_REMOTE_RESOURCE_IDENTIFIER, remoteResourceIdentifier.get()); + builder.put(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, cloudformationPrimaryIdentifier.get()); } } + // NOTE: "name" in this case can be either the lambda name or lambda arn + private static Optional getLambdaResourceNameFromAribitraryName( + Optional arbitraryName) { + if (arbitraryName != null && arbitraryName.get().startsWith("arn:aws:lambda:")) { + Arn resourceArn = Arn.fromString(arbitraryName.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } + return arbitraryName; + } + + private static Optional getSecretsManagerResourceNameFromArn(Optional stringArn) { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } + + private static Optional getSfnResourceNameFromArn(Optional stringArn) { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } + + private static Optional getSnsResourceNameFromArn(Optional stringArn) { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString()); + } + /** * RemoteResourceIdentifier is populated with rule * ^[{db.name}|]?{address}[|{port}]? diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java index 594d3f7dc9..c51188a6e8 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java @@ -26,6 +26,8 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_DATA_SOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_KNOWLEDGE_BASE_ID; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_RESOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_SERVICE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_QUEUE_NAME; @@ -35,7 +37,11 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_IDENTIFIER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_TYPE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_SERVICE; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SECRET_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SNS_TOPIC_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STATE_MACHINE_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL; @@ -770,6 +776,42 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { mockAttribute(GEN_AI_REQUEST_MODEL, "test.service_^id"); validateRemoteResourceAttributes("AWS::Bedrock::Model", "test.service_^^id"); mockAttribute(GEN_AI_REQUEST_MODEL, null); + + // Validate behaviour of AWS_STATE_MACHINE_ARN attribute, then remove it. + mockAttribute( + AWS_STATE_MACHINE_ARN, + "arn:aws:states:us-east-1:123456789012:stateMachine:test_state_machine"); + validateRemoteResourceAttributes("AWS::StepFunctions::StateMachine", "test_state_machine"); + mockAttribute(AWS_STATE_MACHINE_ARN, null); + + // Validate behaviour of AWS_STEPFUNCTIONS_ACTIVITY_ARN, then remove it. + mockAttribute( + AWS_STEP_FUNCTIONS_ACTIVITY_ARN, + "arn:aws:states:us-east-1:007003123456789012:activity:testActivity"); + validateRemoteResourceAttributes("AWS::StepFunctions::Activity", "testActivity"); + mockAttribute(AWS_STEP_FUNCTIONS_ACTIVITY_ARN, null); + + // Validate behaviour of AWS_SNS_TOPIC_ARN, then remove it. + mockAttribute(AWS_SNS_TOPIC_ARN, "arn:aws:sns:us-west-2:012345678901:testTopic"); + validateRemoteResourceAttributes("AWS::SNS::Topic", "testTopic"); + mockAttribute(AWS_SNS_TOPIC_ARN, null); + + // Validate behaviour of AWS_SECRET_ARN, then remove it. + mockAttribute( + AWS_SECRET_ARN, "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretName"); + validateRemoteResourceAttributes("AWS::SecretsManager::Secret", "secretName"); + mockAttribute(AWS_SECRET_ARN, null); + + // Validate behaviour of AWS_LAMBDA_NAME, then remove it. + mockAttribute(AWS_LAMBDA_NAME, "arn:aws:lambda:us-east-1:123456789012:function:functionName"); + validateRemoteResourceAttributes("AWS::Lambda::Function", "functionName"); + mockAttribute(AWS_LAMBDA_NAME, null); + + // Validate behaviour of AWS_LAMBDA_RESOURCE_ID + mockAttribute(AWS_LAMBDA_RESOURCE_ID, "eventSourceId"); + validateRemoteResourceAttributes("AWS::Lambda::EventSourceMapping", "eventSourceId"); + mockAttribute(AWS_LAMBDA_RESOURCE_ID, null); + mockAttribute(RPC_SYSTEM, "null"); } @@ -1175,6 +1217,7 @@ public void testNormalizeRemoteServiceName_AwsSdk() { testAwsSdkServiceNormalization("AWSBedrockAgentRuntime", "AWS::Bedrock"); testAwsSdkServiceNormalization("AWSBedrockAgent", "AWS::Bedrock"); testAwsSdkServiceNormalization("AmazonBedrockRuntime", "AWS::BedrockRuntime"); + testAwsSdkServiceNormalization("AWSStepFunctions", "AWS::StepFunctions"); // AWS SDK V2 testAwsSdkServiceNormalization("DynamoDb", "AWS::DynamoDB");