Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws-runtime/aws-http/api/aws-http.api
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AddUserAgentMetadata
}

public final class aws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric {
public static final field DDB_MAPPER Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ private fun formatMetrics(metrics: MutableSet<String>): String {
@InternalApi
public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric {
S3_EXPRESS_BUCKET("J"),
DDB_MAPPER("d"),
}
1 change: 1 addition & 0 deletions hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
implementation(project(":aws-runtime:aws-http"))
api(project(":services:dynamodb"))
api(project(":hll:hll-mapping-core"))
api(libs.kotlinx.coroutines.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ public interface DynamoDbMapper {
public fun DynamoDbMapper(
client: DynamoDbClient,
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
): DynamoDbMapper = DynamoDbMapperImpl(client, DynamoDbMapper.Config(config))
): DynamoDbMapper = DynamoDbMapperImpl.wrapping(client, DynamoDbMapper.Config(config))
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@ import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.model.internal.tableImpl
import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.InterceptorAny
import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient
import aws.sdk.kotlin.services.dynamodb.withConfig
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor

internal data class DynamoDbMapperImpl(
override val client: DynamoDbClient,
override val config: DynamoDbMapper.Config,
) : DynamoDbMapper {
internal companion object {
/**
* Wraps a low-level [DynamoDbClient] to add additional features before instantiating a new
* [DynamoDbMapperImpl].
*/
fun wrapping(client: DynamoDbClient, config: DynamoDbMapper.Config): DynamoDbMapperImpl {
Copy link
Member

Choose a reason for hiding this comment

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

Would a name like withConfig be clearer?

val wrappedClient = client.withConfig { interceptors += DdbMapperMetricInterceptor }
return DynamoDbMapperImpl(wrappedClient, config)
}
}
override fun <T, PK> getTable(name: String, schema: ItemSchema.PartitionKey<T, PK>) =
tableImpl(this, name, schema)

Expand All @@ -35,3 +50,13 @@ internal class MapperConfigBuilderImpl : DynamoDbMapper.Config.Builder {

override fun build() = MapperConfigImpl(interceptors.toList())
}

/**
* An interceptor that emits the DynamoDB Mapper business metric
*/
private object DdbMapperMetricInterceptor : HttpInterceptor {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit/naming: DdbMapperBusinessMetricInterceptor

override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any {
context.executionContext.emitBusinessMetric(AwsBusinessMetric.DDB_MAPPER)
return context.request
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper

import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec
import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter
import aws.sdk.kotlin.hll.dynamodbmapper.operations.scanPaginated
import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest
import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter
import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter
import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric
import aws.sdk.kotlin.services.dynamodb.scan
import aws.sdk.kotlin.services.dynamodb.withConfig
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.collections.get
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.runTest
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class DynamoDbMapperTest : DdbLocalTest() {
companion object {
private const val TABLE_NAME = "dummy"

private data class DummyData(var foo: String = "", var bar: Int = 0)

private val dummyConverter = SimpleItemConverter(
::DummyData,
{ this },
AttributeDescriptor("foo", DummyData::foo, DummyData::foo::set, StringConverter),
AttributeDescriptor("bar", DummyData::bar, DummyData::bar::set, IntConverter),
)

private val dummySchema = ItemSchema(dummyConverter, KeySpec.String("foo"), KeySpec.Number("bar"))
}

@BeforeAll
fun setUp() = runTest {
createTable(TABLE_NAME, dummySchema)
}

@Test
fun testBusinessMetricEmission() = runTest {
val interceptor = MetricCapturingInterceptor()

val ddb = super.ddb.withConfig { interceptors += interceptor }
interceptor.assertEmpty()

// No metric for low-level client
ddb.scan { tableName = TABLE_NAME }
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
interceptor.reset()

// Metric for high-level client
val mapper = mapper(ddb)
val table = mapper.getTable(TABLE_NAME, dummySchema)
table.scanPaginated { }.collect()
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER)
interceptor.reset()

// Still no metric for low-level client (i.e., LL wasn't modified by HL)
ddb.scan { tableName = TABLE_NAME }
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
interceptor.reset()

// Original client can be closed, mapper is unaffected
ddb.close()
table.scanPaginated { }.collect()
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER)
}
}

private class MetricCapturingInterceptor : HttpInterceptor {
private val capturedMetrics = mutableSetOf<String>()

override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) {
capturedMetrics += context.executionContext[BusinessMetrics]
}

fun assertMetric(metric: BusinessMetric, exists: Boolean = true) {
if (exists) {
assertTrue(
metric.identifier in capturedMetrics,
"Expected metrics to contain $metric. Actual values: $capturedMetrics",
)
} else {
assertFalse(
metric.identifier in capturedMetrics,
"Expected metrics *not* to contain $metric. Actual values: $capturedMetrics",
)
}
}

fun assertEmpty() {
assertTrue(capturedMetrics.isEmpty(), "Expected metrics to be empty. Actual values: $capturedMetrics")
}

fun reset() {
capturedMetrics.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ abstract class DdbLocalTest : AnnotationSpec() {
* Returns a [DynamoDbMapper] instance utilizing the DynamoDB Local instance
* @param config A function to set the configuration of the mapper before it's built
*/
fun mapper(config: DynamoDbMapper.Config.Builder.() -> Unit = { }) = DynamoDbMapper(ddb, config)
fun mapper(
ddb: DynamoDbClient? = null,
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
) = DynamoDbMapper(ddb ?: this.ddb, config)

@AfterAll
fun cleanUp() {
Expand Down
Loading