From 165c8370ac86e5088e7d61452636ee8793377178 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 23 May 2024 10:52:51 -0400 Subject: [PATCH 01/26] feat: business metrics --- aws-runtime/aws-http/api/aws-http.api | 23 ++++ .../runtime/http/AwsUserAgentMetadata.kt | 3 +- .../BusinessMetricsInterceptor.kt | 60 +++++++++++ .../runtime/http/AwsUserAgentMetadataTest.kt | 2 +- .../BusinessMetricsInterceptorTest.kt | 102 ++++++++++++++++++ .../runtime/http/middleware/UserAgentTest.kt | 2 +- .../aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt | 1 + .../codegen/BusinessMetricsIntegration.kt | 56 ++++++++++ ...tlin.codegen.integration.KotlinIntegration | 1 + 9 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt create mode 100644 aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt create mode 100644 codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index 1be03dc3e03..3d64454ab83 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -162,6 +162,29 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor : public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { + public fun ()V + public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V +} + public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt index c1ffdeffc9b..a5bf36461f6 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt @@ -14,6 +14,7 @@ import kotlin.jvm.JvmInline internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV" public const val AWS_APP_ID_ENV: String = "AWS_SDK_UA_APP_ID" +private const val USER_AGENT_SPEC_VERSION = "2.1" // non-standard environment variables/properties public const val AWS_APP_ID_PROP: String = "aws.userAgentAppId" @@ -65,7 +66,7 @@ public data class AwsUserAgentMetadata( get() = buildList { add(sdkMetadata) customMetadata?.extras?.takeIf { it.containsKey("internal") }?.let { add("md/internal") } - add(uaPair("ua", "2.0")) // User agent specification version 2.0 + add(uaPair("ua", USER_AGENT_SPEC_VERSION)) add(apiMetadata) add(osMetadata) add(languageMetadata) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt new file mode 100644 index 00000000000..02ae8e6fe79 --- /dev/null +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.runtime.http.interceptors + +import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT +import aws.smithy.kotlin.runtime.businessmetrics.businessMetrics +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder + +/** + * Appends business metrics to the `User-Agent` header. + */ +public class BusinessMetricsInterceptor : HttpInterceptor { + override suspend fun modifyBeforeTransmit(context: ProtocolRequestInterceptorContext): HttpRequest { + context.executionContext.getOrNull(businessMetrics)?.let { metrics -> + val metricsString = formatMetrics(metrics) + val currentUserAgentHeader = context.protocolRequest.headers[USER_AGENT] + val modifiedRequest = context.protocolRequest.toBuilder() + + modifiedRequest.headers[USER_AGENT] = currentUserAgentHeader + metricsString + + return modifiedRequest.build() + } + return context.protocolRequest + } +} + +/** + * Makes sure the metrics do not exceed the maximum size and truncates them if so. + */ +private fun formatMetrics(metrics: MutableSet): String { + val metricsString = metrics.joinToString(",", "m/") + val metricsByteArray = metricsString.toByteArray() + val maxSize = 1024 + + if (metricsByteArray.size <= maxSize) return metricsString + + val commaByte = ','.code.toByte() + var lastCommaIndex: Int? = null + + for (i in 0..1023) { + if (metricsByteArray[i] == commaByte) { + lastCommaIndex = i + } + } + + lastCommaIndex?.let { + return metricsByteArray.decodeToString( + 0, + lastCommaIndex, + true, + ) + } + + throw IllegalStateException("Business metrics are incorrectly formatted: $metricsString") +} diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt index 2cc28792ca2..e27f6f7ef71 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt @@ -45,7 +45,7 @@ class AwsUserAgentMetadataTest { val expected = listOf( "aws-sdk-kotlin/1.2.3", "md/internal", - "ua/2.0", + "ua/2.1", "api/test-service#1.2.3", "os/linux#ubuntu-20.04", "lang/kotlin#1.4.31", diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt new file mode 100644 index 00000000000..7e77374c4cc --- /dev/null +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.runtime.http.interceptors + +import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.operation.ExecutionContext +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BusinessMetricsInterceptorTest { + @Test + fun noBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertFalse(userAgentHeader.endsWith("m/")) + } + + @Test + fun businessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.emitBusinessMetric(BusinessMetrics.S3_EXPRESS_BUCKET) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertTrue( + userAgentHeader.endsWith( + "m/${BusinessMetrics.S3_EXPRESS_BUCKET.identifier}", + ), + ) + } + + @Test + fun multipleBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.emitBusinessMetric(BusinessMetrics.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(BusinessMetrics.GZIP_REQUEST_COMPRESSION) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertTrue( + userAgentHeader.endsWith( + "m/${BusinessMetrics.S3_EXPRESS_BUCKET.identifier},${BusinessMetrics.GZIP_REQUEST_COMPRESSION.identifier}", + ), + ) + } + + @Test + fun truncateBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics] = mutableSetOf() + + for (i in 0..1024) { + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics].add(i.toString()) + } + + val rawMetrics = executionContext[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics] + val rawMetricsString = rawMetrics.joinToString(",", "m/") + val rawMetricsByteArray = rawMetricsString.toByteArray() + val maxSize = 1024 + + assertTrue(rawMetricsByteArray.size >= maxSize) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + val truncatedMetrics = "m/" + userAgentHeader.substringAfter("m/") + + assertTrue(truncatedMetrics.toByteArray().size <= maxSize) + assertFalse(truncatedMetrics.endsWith(",")) + } +} + +private fun interceptorContext(executionContext: ExecutionContext): ProtocolRequestInterceptorContext = + object : ProtocolRequestInterceptorContext { + override val protocolRequest: HttpRequest = HttpRequest( + HttpMethod.GET, + Url.parse("https://test.aws.com?foo=bar"), + Headers { + append(USER_AGENT, "aws-sdk-kotlin/1.2.3 ua/2.1 api/test-service#1.2.3...") + }, + ) + override val executionContext: ExecutionContext = executionContext + override val request: Any = Unit + } diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt index 05c5ae170b9..a2fcd4259cd 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt @@ -47,7 +47,7 @@ class UserAgentTest { assertTrue(request.headers.contains(X_AMZ_USER_AGENT)) assertEquals("aws-sdk-kotlin/1.2.3", request.headers[X_AMZ_USER_AGENT]) assertTrue( - request.headers[USER_AGENT]!!.startsWith("aws-sdk-kotlin/1.2.3 ua/2.0 api/test-service#1.2.3"), + request.headers[USER_AGENT]!!.startsWith("aws-sdk-kotlin/1.2.3 ua/2.1 api/test-service#1.2.3"), "$USER_AGENT header didn't start with expected value. Found: ${request.headers[USER_AGENT]}", ) } diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index e36fb00cd69..1b3195589cb 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -60,6 +60,7 @@ object AwsRuntimeTypes { object Interceptors : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP, "interceptors") { val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor") val UnsupportedSigningAlgorithmInterceptor = symbol("UnsupportedSigningAlgorithmInterceptor") + val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") } object Retries { diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt new file mode 100644 index 00000000000..5af34925f59 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.codegen + +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.integration.SectionWriter +import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding +import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointBusinessMetrics +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.shapes.OperationShape + +/** + * Renders the addition of the [BusinessMetricsInterceptor] and endpoint business metrics emitters + */ +class BusinessMetricsIntegration : KotlinIntegration { + override val sectionWriters: List + get() = listOf( + SectionWriterBinding(EndpointBusinessMetrics, endpointBusinessMetricsSectionWriter), + ) + + private val endpointBusinessMetricsSectionWriter = SectionWriter { writer, _ -> + writer.write( + "if (endpoint.attributes.contains(#T)) request.context.#T(#T.SERVICE_ENDPOINT_OVERRIDE)", + RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride, + RuntimeTypes.Core.BusinessMetrics.EmitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, + ) + + writer.write( + "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", + RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint, + RuntimeTypes.Core.BusinessMetrics.EmitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, + ) + } + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + userAgentBusinessMetricsMiddleware + + private val userAgentBusinessMetricsMiddleware = object : ProtocolMiddleware { + override val name: String = "UserAgentBusinessMetrics" + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + AwsRuntimeTypes.Http.Interceptors.BusinessMetricsInterceptor, + ) + } + } +} diff --git a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index d7c3a928606..36e2c226408 100644 --- a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -42,3 +42,4 @@ aws.sdk.kotlin.codegen.customization.cloudfrontkeyvaluestore.BackfillSigV4ACusto aws.sdk.kotlin.codegen.customization.s3.express.SigV4S3ExpressAuthSchemeIntegration aws.sdk.kotlin.codegen.customization.s3.express.S3ExpressIntegration aws.sdk.kotlin.codegen.customization.s3.S3ExpiresIntegration +aws.sdk.kotlin.codegen.BusinessMetricsIntegration From 489e4329985265a544af7fc1e2c119d1db256b82 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 24 May 2024 09:53:07 -0400 Subject: [PATCH 02/26] Fix incorrect import --- .../runtime/http/interceptors/BusinessMetricsInterceptor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index 02ae8e6fe79..3761e212ce9 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -10,6 +10,7 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.toBuilder +import kotlin.text.toByteArray /** * Appends business metrics to the `User-Agent` header. From 2c3bc06ec96382be2061e0a9829716ffbc541dc4 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 24 May 2024 10:08:47 -0400 Subject: [PATCH 03/26] use native function instead of JVM only --- .../runtime/http/interceptors/BusinessMetricsInterceptor.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index 3761e212ce9..48f2c88d9ed 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -10,7 +10,6 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.toBuilder -import kotlin.text.toByteArray /** * Appends business metrics to the `User-Agent` header. @@ -35,7 +34,7 @@ public class BusinessMetricsInterceptor : HttpInterceptor { */ private fun formatMetrics(metrics: MutableSet): String { val metricsString = metrics.joinToString(",", "m/") - val metricsByteArray = metricsString.toByteArray() + val metricsByteArray = metricsString.encodeToByteArray() val maxSize = 1024 if (metricsByteArray.size <= maxSize) return metricsString From 16a750f27dfdf4d87b0e6350eeb392e7f601b938 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 24 May 2024 16:16:34 -0400 Subject: [PATCH 04/26] update smithy version in libs.versions for smithy projections --- .../aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt index 5af34925f59..d908c401cbd 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -27,14 +27,14 @@ class BusinessMetricsIntegration : KotlinIntegration { writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.SERVICE_ENDPOINT_OVERRIDE)", RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride, - RuntimeTypes.Core.BusinessMetrics.EmitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, ) writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint, - RuntimeTypes.Core.BusinessMetrics.EmitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a3b9130c6f..dfda1ba5103 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.2" -smithy-kotlin-codegen-version = "0.32.2" +smithy-kotlin-runtime-version = "1.2.4-SNAPSHOT" +smithy-kotlin-codegen-version = "0.32.4-SNAPSHOT" # codegen smithy-version = "1.47.0" From 77d617c056abeea63936b1708bc42de908ced7ac Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 6 Jun 2024 01:17:26 -0400 Subject: [PATCH 05/26] small fixes to keep in line with smithy kotlin --- .../runtime/http/interceptors/BusinessMetricsInterceptor.kt | 1 + .../aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt | 2 +- gradle/libs.versions.toml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index 48f2c88d9ed..070e4c992a4 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -33,6 +33,7 @@ public class BusinessMetricsInterceptor : HttpInterceptor { * Makes sure the metrics do not exceed the maximum size and truncates them if so. */ private fun formatMetrics(metrics: MutableSet): String { + if (metrics.isEmpty()) return "" val metricsString = metrics.joinToString(",", "m/") val metricsByteArray = metricsString.encodeToByteArray() val maxSize = 1024 diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt index d908c401cbd..b832577192d 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -33,7 +33,7 @@ class BusinessMetricsIntegration : KotlinIntegration { writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", - RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint, + RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPointAccountId, RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dfda1ba5103..2bbdd480aa0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.4-SNAPSHOT" -smithy-kotlin-codegen-version = "0.32.4-SNAPSHOT" +smithy-kotlin-runtime-version = "1.2.7-SNAPSHOT" +smithy-kotlin-codegen-version = "0.32.7-SNAPSHOT" # codegen smithy-version = "1.47.0" From cfc9823e914d7cfba8811b8789d1b7e8f8c30c1c Mon Sep 17 00:00:00 2001 From: 0marperez Date: Tue, 11 Jun 2024 14:42:19 -0400 Subject: [PATCH 06/26] pr feedback --- aws-runtime/aws-http/api/aws-http.api | 9 ++++ .../runtime/http/AwsUserAgentMetadata.kt | 1 + .../BusinessMetricsInterceptor.kt | 30 ++++++++------ .../BusinessMetricsInterceptorTest.kt | 41 +++++++++++++------ .../aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt | 1 + .../codegen/BusinessMetricsIntegration.kt | 19 ++++++--- 6 files changed, 72 insertions(+), 29 deletions(-) diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index 3d64454ab83..e6355231cb9 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -48,6 +48,7 @@ public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata$Companion { public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataKt { public static final field AWS_APP_ID_ENV Ljava/lang/String; public static final field AWS_APP_ID_PROP Ljava/lang/String; + public static final field BUSINESS_METRICS_MAX_SIZE I } public final class aws/sdk/kotlin/runtime/http/ExecutionEnvMetadata { @@ -185,6 +186,14 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInter public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getIdentifier ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; + public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; +} + public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt index a5bf36461f6..31de4c9921e 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt @@ -15,6 +15,7 @@ import kotlin.jvm.JvmInline internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV" public const val AWS_APP_ID_ENV: String = "AWS_SDK_UA_APP_ID" private const val USER_AGENT_SPEC_VERSION = "2.1" +public const val BUSINESS_METRICS_MAX_SIZE: Int = 1024 // non-standard environment variables/properties public const val AWS_APP_ID_PROP: String = "aws.userAgentAppId" diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index 070e4c992a4..fe2c33b6661 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -4,8 +4,11 @@ */ package aws.sdk.kotlin.runtime.http.interceptors +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_SIZE import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT -import aws.smithy.kotlin.runtime.businessmetrics.businessMetrics +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor import aws.smithy.kotlin.runtime.http.request.HttpRequest @@ -16,7 +19,7 @@ import aws.smithy.kotlin.runtime.http.request.toBuilder */ public class BusinessMetricsInterceptor : HttpInterceptor { override suspend fun modifyBeforeTransmit(context: ProtocolRequestInterceptorContext): HttpRequest { - context.executionContext.getOrNull(businessMetrics)?.let { metrics -> + context.executionContext.getOrNull(BusinessMetrics)?.let { metrics -> val metricsString = formatMetrics(metrics) val currentUserAgentHeader = context.protocolRequest.headers[USER_AGENT] val modifiedRequest = context.protocolRequest.toBuilder() @@ -36,18 +39,13 @@ private fun formatMetrics(metrics: MutableSet): String { if (metrics.isEmpty()) return "" val metricsString = metrics.joinToString(",", "m/") val metricsByteArray = metricsString.encodeToByteArray() - val maxSize = 1024 - if (metricsByteArray.size <= maxSize) return metricsString + if (metricsByteArray.size <= BUSINESS_METRICS_MAX_SIZE) return metricsString - val commaByte = ','.code.toByte() - var lastCommaIndex: Int? = null - - for (i in 0..1023) { - if (metricsByteArray[i] == commaByte) { - lastCommaIndex = i - } - } + val lastCommaIndex = metricsByteArray + .sliceArray(0 until 1024) + .indexOfLast { it == ','.code.toByte() } + .takeIf { it != -1 } lastCommaIndex?.let { return metricsByteArray.decodeToString( @@ -59,3 +57,11 @@ private fun formatMetrics(metrics: MutableSet): String { throw IllegalStateException("Business metrics are incorrectly formatted: $metricsString") } + +/** + * SDK specific business metrics + */ +@InternalApi +public enum class SdkBusinessMetric(public override val identifier: String) : BusinessMetric { + S3_EXPRESS_BUCKET("J"), +} diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt index 7e77374c4cc..e37d64e8d7b 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -4,8 +4,10 @@ */ package aws.sdk.kotlin.runtime.http.interceptors +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_SIZE import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT -import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics +import aws.smithy.kotlin.runtime.businessmetrics.SdkBusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.collections.get @@ -15,6 +17,7 @@ import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.operation.ExecutionContext import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -32,7 +35,7 @@ class BusinessMetricsInterceptorTest { @Test fun businessMetrics() = runTest { val executionContext = ExecutionContext() - executionContext.emitBusinessMetric(BusinessMetrics.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(SdkBusinessMetric.S3_EXPRESS_BUCKET) val interceptor = BusinessMetricsInterceptor() val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) @@ -40,7 +43,7 @@ class BusinessMetricsInterceptorTest { assertTrue( userAgentHeader.endsWith( - "m/${BusinessMetrics.S3_EXPRESS_BUCKET.identifier}", + "m/${SdkBusinessMetric.S3_EXPRESS_BUCKET.identifier}", ), ) } @@ -48,8 +51,8 @@ class BusinessMetricsInterceptorTest { @Test fun multipleBusinessMetrics() = runTest { val executionContext = ExecutionContext() - executionContext.emitBusinessMetric(BusinessMetrics.S3_EXPRESS_BUCKET) - executionContext.emitBusinessMetric(BusinessMetrics.GZIP_REQUEST_COMPRESSION) + executionContext.emitBusinessMetric(SdkBusinessMetric.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION) val interceptor = BusinessMetricsInterceptor() val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) @@ -57,7 +60,7 @@ class BusinessMetricsInterceptorTest { assertTrue( userAgentHeader.endsWith( - "m/${BusinessMetrics.S3_EXPRESS_BUCKET.identifier},${BusinessMetrics.GZIP_REQUEST_COMPRESSION.identifier}", + "m/${SdkBusinessMetric.S3_EXPRESS_BUCKET.identifier},${SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION.identifier}", ), ) } @@ -65,27 +68,41 @@ class BusinessMetricsInterceptorTest { @Test fun truncateBusinessMetrics() = runTest { val executionContext = ExecutionContext() - executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics] = mutableSetOf() + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf() for (i in 0..1024) { - executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics].add(i.toString()) + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics].add(i.toString()) } - val rawMetrics = executionContext[aws.smithy.kotlin.runtime.businessmetrics.businessMetrics] + val rawMetrics = executionContext[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] val rawMetricsString = rawMetrics.joinToString(",", "m/") val rawMetricsByteArray = rawMetricsString.toByteArray() - val maxSize = 1024 - assertTrue(rawMetricsByteArray.size >= maxSize) + assertTrue(rawMetricsByteArray.size >= BUSINESS_METRICS_MAX_SIZE) val interceptor = BusinessMetricsInterceptor() val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) val userAgentHeader = request.headers[USER_AGENT]!! val truncatedMetrics = "m/" + userAgentHeader.substringAfter("m/") - assertTrue(truncatedMetrics.toByteArray().size <= maxSize) + assertTrue(truncatedMetrics.toByteArray().size <= BUSINESS_METRICS_MAX_SIZE) assertFalse(truncatedMetrics.endsWith(",")) } + + @Test + fun malformedBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf( + "A".repeat(BUSINESS_METRICS_MAX_SIZE), + ) + + val interceptor = BusinessMetricsInterceptor() + + assertFailsWith("Business metrics are incorrectly formatted:") { + interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + } + } } private fun interceptorContext(executionContext: ExecutionContext): ProtocolRequestInterceptorContext = diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index 1b3195589cb..9261838407b 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -61,6 +61,7 @@ object AwsRuntimeTypes { val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor") val UnsupportedSigningAlgorithmInterceptor = symbol("UnsupportedSigningAlgorithmInterceptor") val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") + val SdkBusinessMetric = symbol("SdkBusinessMetric") } object Retries { diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt index b832577192d..7468ac52db0 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -6,6 +6,7 @@ package aws.sdk.kotlin.codegen import software.amazon.smithy.kotlin.codegen.core.KotlinWriter import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.integration.SectionWriter import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding @@ -24,19 +25,27 @@ class BusinessMetricsIntegration : KotlinIntegration { ) private val endpointBusinessMetricsSectionWriter = SectionWriter { writer, _ -> + writer.write("") writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.SERVICE_ENDPOINT_OVERRIDE)", - RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride, + RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride, RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, - RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, ) - writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", - RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPointAccountId, + RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, + ) + writer.write( + "if (endpoint.attributes.contains(#T.SigningService) && endpoint.attributes[#T.SigningService] == \"s3express\") request.context.#T(#T.S3_EXPRESS_BUCKET)", + AwsSigningAttributes, + AwsSigningAttributes, RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, - RuntimeTypes.Core.BusinessMetrics.BusinessMetrics, + AwsRuntimeTypes.Http.Interceptors.SdkBusinessMetric, ) + writer.write("") } override fun customizeMiddleware( From 9ecd76e4280ce45eb70d76849531d9ff6c8e2de4 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Tue, 11 Jun 2024 15:28:09 -0400 Subject: [PATCH 07/26] remove unused import --- .../runtime/http/interceptors/BusinessMetricsInterceptorTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt index e37d64e8d7b..4cc3e7893a5 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -6,7 +6,6 @@ package aws.sdk.kotlin.runtime.http.interceptors import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_SIZE import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT -import aws.smithy.kotlin.runtime.businessmetrics.SdkBusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext From 0d985634f2bdc75e086af5bb28c44b947c59185e Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 13 Jun 2024 14:02:04 -0400 Subject: [PATCH 08/26] feedback --- .../kotlin/runtime/http/AwsUserAgentMetadata.kt | 2 +- .../interceptors/BusinessMetricsInterceptor.kt | 8 ++++---- .../BusinessMetricsInterceptorTest.kt | 16 ++++++++-------- .../aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt | 2 +- .../kotlin/codegen/BusinessMetricsIntegration.kt | 11 +++++++---- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt index 31de4c9921e..34229356df7 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt @@ -15,7 +15,7 @@ import kotlin.jvm.JvmInline internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV" public const val AWS_APP_ID_ENV: String = "AWS_SDK_UA_APP_ID" private const val USER_AGENT_SPEC_VERSION = "2.1" -public const val BUSINESS_METRICS_MAX_SIZE: Int = 1024 +public const val BUSINESS_METRICS_MAX_LENGTH: Int = 1024 // non-standard environment variables/properties public const val AWS_APP_ID_PROP: String = "aws.userAgentAppId" diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt index fe2c33b6661..8a0917e05cf 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -4,7 +4,7 @@ */ package aws.sdk.kotlin.runtime.http.interceptors -import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_SIZE +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric @@ -40,7 +40,7 @@ private fun formatMetrics(metrics: MutableSet): String { val metricsString = metrics.joinToString(",", "m/") val metricsByteArray = metricsString.encodeToByteArray() - if (metricsByteArray.size <= BUSINESS_METRICS_MAX_SIZE) return metricsString + if (metricsByteArray.size <= BUSINESS_METRICS_MAX_LENGTH) return metricsString val lastCommaIndex = metricsByteArray .sliceArray(0 until 1024) @@ -59,9 +59,9 @@ private fun formatMetrics(metrics: MutableSet): String { } /** - * SDK specific business metrics + * AWS SDK specific business metrics */ @InternalApi -public enum class SdkBusinessMetric(public override val identifier: String) : BusinessMetric { +public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric { S3_EXPRESS_BUCKET("J"), } diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt index 4cc3e7893a5..1d07cabbb7e 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -4,7 +4,7 @@ */ package aws.sdk.kotlin.runtime.http.interceptors -import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_SIZE +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric @@ -34,7 +34,7 @@ class BusinessMetricsInterceptorTest { @Test fun businessMetrics() = runTest { val executionContext = ExecutionContext() - executionContext.emitBusinessMetric(SdkBusinessMetric.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(AwsBusinessMetric.S3_EXPRESS_BUCKET) val interceptor = BusinessMetricsInterceptor() val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) @@ -42,7 +42,7 @@ class BusinessMetricsInterceptorTest { assertTrue( userAgentHeader.endsWith( - "m/${SdkBusinessMetric.S3_EXPRESS_BUCKET.identifier}", + "m/${AwsBusinessMetric.S3_EXPRESS_BUCKET.identifier}", ), ) } @@ -50,7 +50,7 @@ class BusinessMetricsInterceptorTest { @Test fun multipleBusinessMetrics() = runTest { val executionContext = ExecutionContext() - executionContext.emitBusinessMetric(SdkBusinessMetric.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(AwsBusinessMetric.S3_EXPRESS_BUCKET) executionContext.emitBusinessMetric(SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION) val interceptor = BusinessMetricsInterceptor() @@ -59,7 +59,7 @@ class BusinessMetricsInterceptorTest { assertTrue( userAgentHeader.endsWith( - "m/${SdkBusinessMetric.S3_EXPRESS_BUCKET.identifier},${SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION.identifier}", + "m/${AwsBusinessMetric.S3_EXPRESS_BUCKET.identifier},${SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION.identifier}", ), ) } @@ -77,14 +77,14 @@ class BusinessMetricsInterceptorTest { val rawMetricsString = rawMetrics.joinToString(",", "m/") val rawMetricsByteArray = rawMetricsString.toByteArray() - assertTrue(rawMetricsByteArray.size >= BUSINESS_METRICS_MAX_SIZE) + assertTrue(rawMetricsByteArray.size >= BUSINESS_METRICS_MAX_LENGTH) val interceptor = BusinessMetricsInterceptor() val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) val userAgentHeader = request.headers[USER_AGENT]!! val truncatedMetrics = "m/" + userAgentHeader.substringAfter("m/") - assertTrue(truncatedMetrics.toByteArray().size <= BUSINESS_METRICS_MAX_SIZE) + assertTrue(truncatedMetrics.toByteArray().size <= BUSINESS_METRICS_MAX_LENGTH) assertFalse(truncatedMetrics.endsWith(",")) } @@ -93,7 +93,7 @@ class BusinessMetricsInterceptorTest { val executionContext = ExecutionContext() executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf( - "A".repeat(BUSINESS_METRICS_MAX_SIZE), + "A".repeat(BUSINESS_METRICS_MAX_LENGTH), ) val interceptor = BusinessMetricsInterceptor() diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index 9261838407b..b4debfc4bd8 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -61,7 +61,7 @@ object AwsRuntimeTypes { val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor") val UnsupportedSigningAlgorithmInterceptor = symbol("UnsupportedSigningAlgorithmInterceptor") val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") - val SdkBusinessMetric = symbol("SdkBusinessMetric") + val AwsBusinessMetric = symbol("AwsBusinessMetric") } object Retries { diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt index 7468ac52db0..73575ddf075 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -19,6 +19,9 @@ import software.amazon.smithy.model.shapes.OperationShape * Renders the addition of the [BusinessMetricsInterceptor] and endpoint business metrics emitters */ class BusinessMetricsIntegration : KotlinIntegration { + override val order: Byte + get() = super.order + override val sectionWriters: List get() = listOf( SectionWriterBinding(EndpointBusinessMetrics, endpointBusinessMetricsSectionWriter), @@ -29,21 +32,21 @@ class BusinessMetricsIntegration : KotlinIntegration { writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.SERVICE_ENDPOINT_OVERRIDE)", RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride, - RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, ) writer.write( "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId, - RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, ) writer.write( "if (endpoint.attributes.contains(#T.SigningService) && endpoint.attributes[#T.SigningService] == \"s3express\") request.context.#T(#T.S3_EXPRESS_BUCKET)", AwsSigningAttributes, AwsSigningAttributes, - RuntimeTypes.Core.BusinessMetrics.emitBusinessMetrics, - AwsRuntimeTypes.Http.Interceptors.SdkBusinessMetric, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, + AwsRuntimeTypes.Http.Interceptors.AwsBusinessMetric, ) writer.write("") } From 53a324d85e6c4faa8243181012dcc0716dbac48a Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 13 Jun 2024 23:48:39 -0400 Subject: [PATCH 09/26] misc: dynamodb e2e tests --- .../dynamodb/e2eTest/src/DynamoDbTestUtils.kt | 11 ++ .../dynamodb/e2eTest/src/PaginatorTest.kt | 114 ++++++++++++++++++ services/eventbridge/package.json | 4 - 3 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt create mode 100644 services/dynamodb/e2eTest/src/PaginatorTest.kt delete mode 100644 services/eventbridge/package.json diff --git a/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt new file mode 100644 index 00000000000..a184a3e51e3 --- /dev/null +++ b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt @@ -0,0 +1,11 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.services.dynamodb + +/** + * Checks if a Dynamo DB table exists based on its name + */ +internal suspend fun String.exists(client: DynamoDbClient): Boolean = + client.listTables {}.tableNames?.contains(this) ?: false diff --git a/services/dynamodb/e2eTest/src/PaginatorTest.kt b/services/dynamodb/e2eTest/src/PaginatorTest.kt new file mode 100644 index 00000000000..c8e795f3da3 --- /dev/null +++ b/services/dynamodb/e2eTest/src/PaginatorTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.services.dynamodb + +import aws.sdk.kotlin.services.dynamodb.model.* +import aws.sdk.kotlin.services.dynamodb.paginators.scanPaginated +import aws.sdk.kotlin.services.dynamodb.waiters.waitUntilTableExists +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PaginatorTest { + private val client = DynamoDbClient { region = "us-west-2" } + private val table = "testTable" + + @BeforeAll + private fun setUp(): Unit = runBlocking { + if (!table.exists(client)) { + client.createTable { + tableName = table + attributeDefinitions = listOf( + AttributeDefinition { + attributeName = "Artist" + attributeType = ScalarAttributeType.S + }, + AttributeDefinition { + attributeName = "SongTitle" + attributeType = ScalarAttributeType.S + }, + ) + keySchema = listOf( + KeySchemaElement { + attributeName = "Artist" + keyType = KeyType.Hash + }, + KeySchemaElement { + attributeName = "SongTitle" + keyType = KeyType.Range + }, + ) + provisionedThroughput = ProvisionedThroughput { + readCapacityUnits = 5 + writeCapacityUnits = 5 + } + tableClass = TableClass.Standard + } + + client.waitUntilTableExists { + tableName = table + } + } + } + + @AfterAll + private fun cleanUp(): Unit = runBlocking { + if (table.exists(client)) { + client.deleteTable { + tableName = table + } + } + client.close() + } + + @Test + fun scanPaginatedRespectsExclusiveStartKey() = runTest( + timeout = 20.seconds, + ) { + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Bar"), + ) + } + + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Baz"), + ) + } + + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Qux"), + ) + } + + client.scanPaginated { + tableName = table + exclusiveStartKey = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Bar"), + ) + }.collect { scan -> + assertEquals(2, scan.count) + + // NOTE: Items are returned in alphabetical order + assertEquals((AttributeValue.S("Baz")), scan.items?.get(0)?.get("SongTitle")) + assertEquals((AttributeValue.S("Qux")), scan.items?.get(1)?.get("SongTitle")) + } + } +} diff --git a/services/eventbridge/package.json b/services/eventbridge/package.json deleted file mode 100644 index eda983e38eb..00000000000 --- a/services/eventbridge/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "sdkId": "EventBridge", - "enableEndpointAuthProvider": true -} From 24cf5e09a7830e1c83ba112802bb829e76d57344 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 13 Jun 2024 23:55:42 -0400 Subject: [PATCH 10/26] re-add accidentally removed file --- services/eventbridge/package.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 services/eventbridge/package.json diff --git a/services/eventbridge/package.json b/services/eventbridge/package.json new file mode 100644 index 00000000000..c01686999a9 --- /dev/null +++ b/services/eventbridge/package.json @@ -0,0 +1,4 @@ +{ + "sdkId": "EventBridge", + "enableEndpointAuthProvider": true +} \ No newline at end of file From cbae5847c731249b634fc4656521e7229a8c06f2 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Thu, 13 Jun 2024 23:56:39 -0400 Subject: [PATCH 11/26] add missing blank space in re-added file --- services/eventbridge/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/eventbridge/package.json b/services/eventbridge/package.json index c01686999a9..eda983e38eb 100644 --- a/services/eventbridge/package.json +++ b/services/eventbridge/package.json @@ -1,4 +1,4 @@ { "sdkId": "EventBridge", "enableEndpointAuthProvider": true -} \ No newline at end of file +} From 3f457b9b53c1d67ba3b2eb19ec5c4069fc47fb19 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 14 Jun 2024 13:08:20 -0400 Subject: [PATCH 12/26] feedback and test pagination --- .../dynamodb/e2eTest/src/DynamoDbTestUtils.kt | 4 ++-- .../dynamodb/e2eTest/src/PaginatorTest.kt | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt index a184a3e51e3..5fa942ec56a 100644 --- a/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt +++ b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt @@ -7,5 +7,5 @@ package aws.sdk.kotlin.services.dynamodb /** * Checks if a Dynamo DB table exists based on its name */ -internal suspend fun String.exists(client: DynamoDbClient): Boolean = - client.listTables {}.tableNames?.contains(this) ?: false +internal suspend fun DynamoDbClient.tableExists(name: String): Boolean = + this.listTables {}.tableNames?.contains(name) ?: false diff --git a/services/dynamodb/e2eTest/src/PaginatorTest.kt b/services/dynamodb/e2eTest/src/PaginatorTest.kt index c8e795f3da3..510bf8cf4e6 100644 --- a/services/dynamodb/e2eTest/src/PaginatorTest.kt +++ b/services/dynamodb/e2eTest/src/PaginatorTest.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance +import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds @@ -19,11 +20,11 @@ import kotlin.time.Duration.Companion.seconds @TestInstance(TestInstance.Lifecycle.PER_CLASS) class PaginatorTest { private val client = DynamoDbClient { region = "us-west-2" } - private val table = "testTable" + private val table = "testTable${Random.nextInt()}" @BeforeAll private fun setUp(): Unit = runBlocking { - if (!table.exists(client)) { + if (!client.tableExists(table)) { client.createTable { tableName = table attributeDefinitions = listOf( @@ -61,7 +62,7 @@ class PaginatorTest { @AfterAll private fun cleanUp(): Unit = runBlocking { - if (table.exists(client)) { + if (client.tableExists(table)) { client.deleteTable { tableName = table } @@ -97,18 +98,24 @@ class PaginatorTest { ) } + val results = mutableListOf?>() + client.scanPaginated { tableName = table exclusiveStartKey = mapOf( "Artist" to AttributeValue.S("Foo"), "SongTitle" to AttributeValue.S("Bar"), ) + limit = 1 }.collect { scan -> - assertEquals(2, scan.count) - - // NOTE: Items are returned in alphabetical order - assertEquals((AttributeValue.S("Baz")), scan.items?.get(0)?.get("SongTitle")) - assertEquals((AttributeValue.S("Qux")), scan.items?.get(1)?.get("SongTitle")) + if (scan.items?.isNotEmpty() == true) { + results.add(scan.items.first()) + } } + + assertEquals(2, results.size) + // NOTE: Items are returned in alphabetical order + assertEquals((AttributeValue.S("Baz")), results[0]?.get("SongTitle")) + assertEquals((AttributeValue.S("Qux")), results[1]?.get("SongTitle")) } } From 31b640d3ce212f4ec9d31ed97da0a0164d3cd353 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 14 Jun 2024 13:12:16 -0400 Subject: [PATCH 13/26] depend on snapshot version of smithy --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 086ba41f86f..e5207470e9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.7" -smithy-kotlin-codegen-version = "0.32.7" +smithy-kotlin-runtime-version = "1.2.8-SNAPSHOT" +smithy-kotlin-codegen-version = "0.32.8-SNAPSHOT" # codegen smithy-version = "1.49.0" From 2242491311cdb47ef56ca5e9dcdca79de7b963d7 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 14 Jun 2024 13:47:31 -0400 Subject: [PATCH 14/26] api dump --- aws-runtime/aws-http/api/aws-http.api | 18 +++++++++--------- gradle/libs.versions.toml | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index e6355231cb9..e5808232a81 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -48,7 +48,7 @@ public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata$Companion { public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataKt { public static final field AWS_APP_ID_ENV Ljava/lang/String; public static final field AWS_APP_ID_PROP Ljava/lang/String; - public static final field BUSINESS_METRICS_MAX_SIZE I + public static final field BUSINESS_METRICS_MAX_LENGTH I } public final class aws/sdk/kotlin/runtime/http/ExecutionEnvMetadata { @@ -140,6 +140,14 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AddUserAgentMetadata public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getIdentifier ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; + public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; +} + public final class aws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public static final field INSTANCE Laws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor; public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -186,14 +194,6 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInter public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { - public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getIdentifier ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; - public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/SdkBusinessMetric; -} - public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5207470e9c..086ba41f86f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.8-SNAPSHOT" -smithy-kotlin-codegen-version = "0.32.8-SNAPSHOT" +smithy-kotlin-runtime-version = "1.2.7" +smithy-kotlin-codegen-version = "0.32.7" # codegen smithy-version = "1.49.0" From d165743a20515c54c6bd1112c469dfbbc9f71f48 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Fri, 14 Jun 2024 14:43:53 -0400 Subject: [PATCH 15/26] use snapshot version so CI passes --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 086ba41f86f..e5207470e9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.7" -smithy-kotlin-codegen-version = "0.32.7" +smithy-kotlin-runtime-version = "1.2.8-SNAPSHOT" +smithy-kotlin-codegen-version = "0.32.8-SNAPSHOT" # codegen smithy-version = "1.49.0" From bbccd019af9c21a0754d3e96b1fd4fcc87356d07 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Mon, 17 Jun 2024 11:42:30 -0400 Subject: [PATCH 16/26] use latest released version of smithy kotlin --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5207470e9c..1febc66b324 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.8-SNAPSHOT" -smithy-kotlin-codegen-version = "0.32.8-SNAPSHOT" +smithy-kotlin-runtime-version = "1.2.8" +smithy-kotlin-codegen-version = "0.32.8" # codegen smithy-version = "1.49.0" From 369b908a1a8ff23e59e7aeb85faf9fddbfac5e5a Mon Sep 17 00:00:00 2001 From: 0marperez Date: Mon, 17 Jun 2024 12:32:41 -0400 Subject: [PATCH 17/26] use latest smithy kotlin version --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5207470e9c..1febc66b324 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.8-SNAPSHOT" -smithy-kotlin-codegen-version = "0.32.8-SNAPSHOT" +smithy-kotlin-runtime-version = "1.2.8" +smithy-kotlin-codegen-version = "0.32.8" # codegen smithy-version = "1.49.0" From 1a4f99b6c0863d7dc2ea505d720cc86c1927abd0 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Mon, 17 Jun 2024 12:46:35 -0400 Subject: [PATCH 18/26] fix CI ? --- .../http/interceptors/BusinessMetricsInterceptorTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt index 1d07cabbb7e..8469d6fd4ea 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -75,7 +75,7 @@ class BusinessMetricsInterceptorTest { val rawMetrics = executionContext[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] val rawMetricsString = rawMetrics.joinToString(",", "m/") - val rawMetricsByteArray = rawMetricsString.toByteArray() + val rawMetricsByteArray = rawMetricsString.encodeToByteArray() assertTrue(rawMetricsByteArray.size >= BUSINESS_METRICS_MAX_LENGTH) @@ -84,7 +84,7 @@ class BusinessMetricsInterceptorTest { val userAgentHeader = request.headers[USER_AGENT]!! val truncatedMetrics = "m/" + userAgentHeader.substringAfter("m/") - assertTrue(truncatedMetrics.toByteArray().size <= BUSINESS_METRICS_MAX_LENGTH) + assertTrue(truncatedMetrics.encodeToByteArray().size <= BUSINESS_METRICS_MAX_LENGTH) assertFalse(truncatedMetrics.endsWith(",")) } From 559c6bf134a1a8b80604ffce25ce5c0d2272ab3a Mon Sep 17 00:00:00 2001 From: 0marperez Date: Mon, 17 Jun 2024 13:15:47 -0400 Subject: [PATCH 19/26] trigger CI From e8137c2a20d947083492b8637253d2a5c30224f5 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Tue, 18 Jun 2024 12:22:45 -0400 Subject: [PATCH 20/26] remove scripts not intended for commit --- services/dynamodb/e2eTest/src/PaginatorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/dynamodb/e2eTest/src/PaginatorTest.kt b/services/dynamodb/e2eTest/src/PaginatorTest.kt index 510bf8cf4e6..9e89512f6e9 100644 --- a/services/dynamodb/e2eTest/src/PaginatorTest.kt +++ b/services/dynamodb/e2eTest/src/PaginatorTest.kt @@ -109,7 +109,7 @@ class PaginatorTest { limit = 1 }.collect { scan -> if (scan.items?.isNotEmpty() == true) { - results.add(scan.items.first()) + results.add(scan.items.single()) } } From b9cc831c4695577f875e1eb7b271615cee64a7a9 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Tue, 18 Jun 2024 12:26:16 -0400 Subject: [PATCH 21/26] update smithy kotlin version --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1febc66b324..e6b70418eed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.8" -smithy-kotlin-codegen-version = "0.32.8" +smithy-kotlin-runtime-version = "1.2.9" +smithy-kotlin-codegen-version = "0.32.9" # codegen smithy-version = "1.49.0" From 19919188cd66953dd4d6d29b8943c8635458af11 Mon Sep 17 00:00:00 2001 From: 0marperez Date: Tue, 18 Jun 2024 12:27:07 -0400 Subject: [PATCH 22/26] upgrade smithy kotlin version --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1febc66b324..e6b70418eed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.8" -smithy-kotlin-codegen-version = "0.32.8" +smithy-kotlin-runtime-version = "1.2.9" +smithy-kotlin-codegen-version = "0.32.9" # codegen smithy-version = "1.49.0" From 7568a83882c40a45fdeac06f713e6dd031977f9a Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 21 Jun 2024 09:57:44 -0500 Subject: [PATCH 23/26] Upgrade to latest version of smithy-kotlin --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 086ba41f86f..eb79e7d699b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,8 @@ coroutines-version = "1.7.3" atomicfu-version = "0.23.1" # smithy-kotlin codegen and runtime are versioned separately -smithy-kotlin-runtime-version = "1.2.7" -smithy-kotlin-codegen-version = "0.32.7" +smithy-kotlin-runtime-version = "1.2.10" +smithy-kotlin-codegen-version = "0.32.10" # codegen smithy-version = "1.49.0" From 24d999e792504a2605a6ad936cfe4c6b59556c74 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 21 Jun 2024 10:14:29 -0500 Subject: [PATCH 24/26] CI From b8c5238c2e4ea68a956f4fc2df165708790cd477 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 21 Jun 2024 10:58:32 -0500 Subject: [PATCH 25/26] Revert "fix: revert dynamodb e2e test (#1334)" This reverts commit 6e4ce488fd5072d31263aad26c7318eaa0262fae. --- .../dynamodb/e2eTest/src/DynamoDbTestUtils.kt | 11 ++ .../dynamodb/e2eTest/src/PaginatorTest.kt | 121 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt create mode 100644 services/dynamodb/e2eTest/src/PaginatorTest.kt diff --git a/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt new file mode 100644 index 00000000000..5fa942ec56a --- /dev/null +++ b/services/dynamodb/e2eTest/src/DynamoDbTestUtils.kt @@ -0,0 +1,11 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.services.dynamodb + +/** + * Checks if a Dynamo DB table exists based on its name + */ +internal suspend fun DynamoDbClient.tableExists(name: String): Boolean = + this.listTables {}.tableNames?.contains(name) ?: false diff --git a/services/dynamodb/e2eTest/src/PaginatorTest.kt b/services/dynamodb/e2eTest/src/PaginatorTest.kt new file mode 100644 index 00000000000..9e89512f6e9 --- /dev/null +++ b/services/dynamodb/e2eTest/src/PaginatorTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.services.dynamodb + +import aws.sdk.kotlin.services.dynamodb.model.* +import aws.sdk.kotlin.services.dynamodb.paginators.scanPaginated +import aws.sdk.kotlin.services.dynamodb.waiters.waitUntilTableExists +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PaginatorTest { + private val client = DynamoDbClient { region = "us-west-2" } + private val table = "testTable${Random.nextInt()}" + + @BeforeAll + private fun setUp(): Unit = runBlocking { + if (!client.tableExists(table)) { + client.createTable { + tableName = table + attributeDefinitions = listOf( + AttributeDefinition { + attributeName = "Artist" + attributeType = ScalarAttributeType.S + }, + AttributeDefinition { + attributeName = "SongTitle" + attributeType = ScalarAttributeType.S + }, + ) + keySchema = listOf( + KeySchemaElement { + attributeName = "Artist" + keyType = KeyType.Hash + }, + KeySchemaElement { + attributeName = "SongTitle" + keyType = KeyType.Range + }, + ) + provisionedThroughput = ProvisionedThroughput { + readCapacityUnits = 5 + writeCapacityUnits = 5 + } + tableClass = TableClass.Standard + } + + client.waitUntilTableExists { + tableName = table + } + } + } + + @AfterAll + private fun cleanUp(): Unit = runBlocking { + if (client.tableExists(table)) { + client.deleteTable { + tableName = table + } + } + client.close() + } + + @Test + fun scanPaginatedRespectsExclusiveStartKey() = runTest( + timeout = 20.seconds, + ) { + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Bar"), + ) + } + + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Baz"), + ) + } + + client.putItem { + tableName = table + item = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Qux"), + ) + } + + val results = mutableListOf?>() + + client.scanPaginated { + tableName = table + exclusiveStartKey = mapOf( + "Artist" to AttributeValue.S("Foo"), + "SongTitle" to AttributeValue.S("Bar"), + ) + limit = 1 + }.collect { scan -> + if (scan.items?.isNotEmpty() == true) { + results.add(scan.items.single()) + } + } + + assertEquals(2, results.size) + // NOTE: Items are returned in alphabetical order + assertEquals((AttributeValue.S("Baz")), results[0]?.get("SongTitle")) + assertEquals((AttributeValue.S("Qux")), results[1]?.get("SongTitle")) + } +} From 615e6ad47508750a3c4984a03b538135405490dc Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 21 Jun 2024 10:59:07 -0500 Subject: [PATCH 26/26] Revert "fix: revert business metrics (#1333)" This reverts commit af41ebfcc780fe100d9ee72452c4fb471619275a. --- aws-runtime/aws-http/api/aws-http.api | 32 +++++ .../runtime/http/AwsUserAgentMetadata.kt | 4 +- .../BusinessMetricsInterceptor.kt | 67 ++++++++++ .../runtime/http/AwsUserAgentMetadataTest.kt | 2 +- .../BusinessMetricsInterceptorTest.kt | 118 ++++++++++++++++++ .../runtime/http/middleware/UserAgentTest.kt | 2 +- .../aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt | 2 + .../codegen/BusinessMetricsIntegration.kt | 68 ++++++++++ ...tlin.codegen.integration.KotlinIntegration | 1 + 9 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt create mode 100644 aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt create mode 100644 codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt diff --git a/aws-runtime/aws-http/api/aws-http.api b/aws-runtime/aws-http/api/aws-http.api index 1be03dc3e03..e5808232a81 100644 --- a/aws-runtime/aws-http/api/aws-http.api +++ b/aws-runtime/aws-http/api/aws-http.api @@ -48,6 +48,7 @@ public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata$Companion { public final class aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataKt { public static final field AWS_APP_ID_ENV Ljava/lang/String; public static final field AWS_APP_ID_PROP Ljava/lang/String; + public static final field BUSINESS_METRICS_MAX_LENGTH I } public final class aws/sdk/kotlin/runtime/http/ExecutionEnvMetadata { @@ -139,6 +140,14 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AddUserAgentMetadata public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { + public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public fun getIdentifier ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; + public static fun values ()[Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric; +} + public final class aws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public static final field INSTANCE Laws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor; public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -162,6 +171,29 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AwsSpanInterceptor : public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } +public final class aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { + public fun ()V + public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeRetryLoop (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun modifyBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun readAfterAttempt (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterDeserialization (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterExecution (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;)V + public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readAfterTransmit (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeAttempt (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)V + public fun readBeforeExecution (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSerialization (Laws/smithy/kotlin/runtime/client/RequestInterceptorContext;)V + public fun readBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V + public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V +} + public final class aws/sdk/kotlin/runtime/http/interceptors/UnsupportedSigningAlgorithmInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt index c1ffdeffc9b..34229356df7 100644 --- a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt @@ -14,6 +14,8 @@ import kotlin.jvm.JvmInline internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV" public const val AWS_APP_ID_ENV: String = "AWS_SDK_UA_APP_ID" +private const val USER_AGENT_SPEC_VERSION = "2.1" +public const val BUSINESS_METRICS_MAX_LENGTH: Int = 1024 // non-standard environment variables/properties public const val AWS_APP_ID_PROP: String = "aws.userAgentAppId" @@ -65,7 +67,7 @@ public data class AwsUserAgentMetadata( get() = buildList { add(sdkMetadata) customMetadata?.extras?.takeIf { it.containsKey("internal") }?.let { add("md/internal") } - add(uaPair("ua", "2.0")) // User agent specification version 2.0 + add(uaPair("ua", USER_AGENT_SPEC_VERSION)) add(apiMetadata) add(osMetadata) add(languageMetadata) diff --git a/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt new file mode 100644 index 00000000000..8a0917e05cf --- /dev/null +++ b/aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.runtime.http.interceptors + +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH +import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder + +/** + * Appends business metrics to the `User-Agent` header. + */ +public class BusinessMetricsInterceptor : HttpInterceptor { + override suspend fun modifyBeforeTransmit(context: ProtocolRequestInterceptorContext): HttpRequest { + context.executionContext.getOrNull(BusinessMetrics)?.let { metrics -> + val metricsString = formatMetrics(metrics) + val currentUserAgentHeader = context.protocolRequest.headers[USER_AGENT] + val modifiedRequest = context.protocolRequest.toBuilder() + + modifiedRequest.headers[USER_AGENT] = currentUserAgentHeader + metricsString + + return modifiedRequest.build() + } + return context.protocolRequest + } +} + +/** + * Makes sure the metrics do not exceed the maximum size and truncates them if so. + */ +private fun formatMetrics(metrics: MutableSet): String { + if (metrics.isEmpty()) return "" + val metricsString = metrics.joinToString(",", "m/") + val metricsByteArray = metricsString.encodeToByteArray() + + if (metricsByteArray.size <= BUSINESS_METRICS_MAX_LENGTH) return metricsString + + val lastCommaIndex = metricsByteArray + .sliceArray(0 until 1024) + .indexOfLast { it == ','.code.toByte() } + .takeIf { it != -1 } + + lastCommaIndex?.let { + return metricsByteArray.decodeToString( + 0, + lastCommaIndex, + true, + ) + } + + throw IllegalStateException("Business metrics are incorrectly formatted: $metricsString") +} + +/** + * AWS SDK specific business metrics + */ +@InternalApi +public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric { + S3_EXPRESS_BUCKET("J"), +} diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt index 2cc28792ca2..e27f6f7ef71 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt @@ -45,7 +45,7 @@ class AwsUserAgentMetadataTest { val expected = listOf( "aws-sdk-kotlin/1.2.3", "md/internal", - "ua/2.0", + "ua/2.1", "api/test-service#1.2.3", "os/linux#ubuntu-20.04", "lang/kotlin#1.4.31", diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt new file mode 100644 index 00000000000..8469d6fd4ea --- /dev/null +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptorTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.runtime.http.interceptors + +import aws.sdk.kotlin.runtime.http.BUSINESS_METRICS_MAX_LENGTH +import aws.sdk.kotlin.runtime.http.middleware.USER_AGENT +import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.operation.ExecutionContext +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class BusinessMetricsInterceptorTest { + @Test + fun noBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertFalse(userAgentHeader.endsWith("m/")) + } + + @Test + fun businessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.emitBusinessMetric(AwsBusinessMetric.S3_EXPRESS_BUCKET) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertTrue( + userAgentHeader.endsWith( + "m/${AwsBusinessMetric.S3_EXPRESS_BUCKET.identifier}", + ), + ) + } + + @Test + fun multipleBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.emitBusinessMetric(AwsBusinessMetric.S3_EXPRESS_BUCKET) + executionContext.emitBusinessMetric(SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + + assertTrue( + userAgentHeader.endsWith( + "m/${AwsBusinessMetric.S3_EXPRESS_BUCKET.identifier},${SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION.identifier}", + ), + ) + } + + @Test + fun truncateBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf() + + for (i in 0..1024) { + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics].add(i.toString()) + } + + val rawMetrics = executionContext[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] + val rawMetricsString = rawMetrics.joinToString(",", "m/") + val rawMetricsByteArray = rawMetricsString.encodeToByteArray() + + assertTrue(rawMetricsByteArray.size >= BUSINESS_METRICS_MAX_LENGTH) + + val interceptor = BusinessMetricsInterceptor() + val request = interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + val userAgentHeader = request.headers[USER_AGENT]!! + val truncatedMetrics = "m/" + userAgentHeader.substringAfter("m/") + + assertTrue(truncatedMetrics.encodeToByteArray().size <= BUSINESS_METRICS_MAX_LENGTH) + assertFalse(truncatedMetrics.endsWith(",")) + } + + @Test + fun malformedBusinessMetrics() = runTest { + val executionContext = ExecutionContext() + + executionContext.attributes[aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics] = mutableSetOf( + "A".repeat(BUSINESS_METRICS_MAX_LENGTH), + ) + + val interceptor = BusinessMetricsInterceptor() + + assertFailsWith("Business metrics are incorrectly formatted:") { + interceptor.modifyBeforeTransmit(interceptorContext(executionContext)) + } + } +} + +private fun interceptorContext(executionContext: ExecutionContext): ProtocolRequestInterceptorContext = + object : ProtocolRequestInterceptorContext { + override val protocolRequest: HttpRequest = HttpRequest( + HttpMethod.GET, + Url.parse("https://test.aws.com?foo=bar"), + Headers { + append(USER_AGENT, "aws-sdk-kotlin/1.2.3 ua/2.1 api/test-service#1.2.3...") + }, + ) + override val executionContext: ExecutionContext = executionContext + override val request: Any = Unit + } diff --git a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt index 05c5ae170b9..a2fcd4259cd 100644 --- a/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt +++ b/aws-runtime/aws-http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt @@ -47,7 +47,7 @@ class UserAgentTest { assertTrue(request.headers.contains(X_AMZ_USER_AGENT)) assertEquals("aws-sdk-kotlin/1.2.3", request.headers[X_AMZ_USER_AGENT]) assertTrue( - request.headers[USER_AGENT]!!.startsWith("aws-sdk-kotlin/1.2.3 ua/2.0 api/test-service#1.2.3"), + request.headers[USER_AGENT]!!.startsWith("aws-sdk-kotlin/1.2.3 ua/2.1 api/test-service#1.2.3"), "$USER_AGENT header didn't start with expected value. Found: ${request.headers[USER_AGENT]}", ) } diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt index e36fb00cd69..b4debfc4bd8 100644 --- a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/AwsRuntimeTypes.kt @@ -60,6 +60,8 @@ object AwsRuntimeTypes { object Interceptors : RuntimeTypePackage(AwsKotlinDependency.AWS_HTTP, "interceptors") { val AddUserAgentMetadataInterceptor = symbol("AddUserAgentMetadataInterceptor") val UnsupportedSigningAlgorithmInterceptor = symbol("UnsupportedSigningAlgorithmInterceptor") + val BusinessMetricsInterceptor = symbol("BusinessMetricsInterceptor") + val AwsBusinessMetric = symbol("AwsBusinessMetric") } object Retries { diff --git a/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt new file mode 100644 index 00000000000..73575ddf075 --- /dev/null +++ b/codegen/aws-sdk-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/BusinessMetricsIntegration.kt @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.sdk.kotlin.codegen + +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.integration.SectionWriter +import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding +import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointBusinessMetrics +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.shapes.OperationShape + +/** + * Renders the addition of the [BusinessMetricsInterceptor] and endpoint business metrics emitters + */ +class BusinessMetricsIntegration : KotlinIntegration { + override val order: Byte + get() = super.order + + override val sectionWriters: List + get() = listOf( + SectionWriterBinding(EndpointBusinessMetrics, endpointBusinessMetricsSectionWriter), + ) + + private val endpointBusinessMetricsSectionWriter = SectionWriter { writer, _ -> + writer.write("") + writer.write( + "if (endpoint.attributes.contains(#T)) request.context.#T(#T.SERVICE_ENDPOINT_OVERRIDE)", + RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, + RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, + ) + writer.write( + "if (endpoint.attributes.contains(#T)) request.context.#T(#T.ACCOUNT_ID_BASED_ENDPOINT)", + RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, + RuntimeTypes.Core.BusinessMetrics.SmithyBusinessMetric, + ) + writer.write( + "if (endpoint.attributes.contains(#T.SigningService) && endpoint.attributes[#T.SigningService] == \"s3express\") request.context.#T(#T.S3_EXPRESS_BUCKET)", + AwsSigningAttributes, + AwsSigningAttributes, + RuntimeTypes.Core.BusinessMetrics.emitBusinessMetric, + AwsRuntimeTypes.Http.Interceptors.AwsBusinessMetric, + ) + writer.write("") + } + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + userAgentBusinessMetricsMiddleware + + private val userAgentBusinessMetricsMiddleware = object : ProtocolMiddleware { + override val name: String = "UserAgentBusinessMetrics" + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + AwsRuntimeTypes.Http.Interceptors.BusinessMetricsInterceptor, + ) + } + } +} diff --git a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index d7c3a928606..36e2c226408 100644 --- a/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -42,3 +42,4 @@ aws.sdk.kotlin.codegen.customization.cloudfrontkeyvaluestore.BackfillSigV4ACusto aws.sdk.kotlin.codegen.customization.s3.express.SigV4S3ExpressAuthSchemeIntegration aws.sdk.kotlin.codegen.customization.s3.express.S3ExpressIntegration aws.sdk.kotlin.codegen.customization.s3.S3ExpiresIntegration +aws.sdk.kotlin.codegen.BusinessMetricsIntegration