Skip to content

Commit 1f2b867

Browse files
committed
emit DDB_MAPPER business metric
1 parent 4240741 commit 1f2b867

File tree

7 files changed

+142
-2
lines changed

7 files changed

+142
-2
lines changed

aws-runtime/aws-http/api/aws-http.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public final class aws/sdk/kotlin/runtime/http/interceptors/AddUserAgentMetadata
141141
}
142142

143143
public final class aws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric {
144+
public static final field DDB_MAPPER Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric;
144145
public static final field S3_EXPRESS_BUCKET Laws/sdk/kotlin/runtime/http/interceptors/AwsBusinessMetric;
145146
public static fun getEntries ()Lkotlin/enums/EnumEntries;
146147
public fun getIdentifier ()Ljava/lang/String;

aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/interceptors/BusinessMetricsInterceptor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ private fun formatMetrics(metrics: MutableSet<String>): String {
6464
@InternalApi
6565
public enum class AwsBusinessMetric(public override val identifier: String) : BusinessMetric {
6666
S3_EXPRESS_BUCKET("J"),
67+
DDB_MAPPER("d"),
6768
}

hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ kotlin {
2626
sourceSets {
2727
commonMain {
2828
dependencies {
29+
implementation(project(":aws-runtime:aws-http"))
2930
api(project(":services:dynamodb"))
3031
api(project(":hll:hll-mapping-core"))
3132
api(libs.kotlinx.coroutines.core)

hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbMapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ public interface DynamoDbMapper {
112112
public fun DynamoDbMapper(
113113
client: DynamoDbClient,
114114
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
115-
): DynamoDbMapper = DynamoDbMapperImpl(client, DynamoDbMapper.Config(config))
115+
): DynamoDbMapper = DynamoDbMapperImpl.wrapping(client, DynamoDbMapper.Config(config))

hll/dynamodb-mapper/dynamodb-mapper/common/src/aws/sdk/kotlin/hll/dynamodbmapper/internal/DynamoDbMapperImpl.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@ import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper
88
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
99
import aws.sdk.kotlin.hll.dynamodbmapper.model.internal.tableImpl
1010
import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.InterceptorAny
11+
import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric
1112
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient
13+
import aws.sdk.kotlin.services.dynamodb.withConfig
14+
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
15+
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
16+
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
1217

1318
internal data class DynamoDbMapperImpl(
1419
override val client: DynamoDbClient,
1520
override val config: DynamoDbMapper.Config,
1621
) : DynamoDbMapper {
22+
internal companion object {
23+
/**
24+
* Wraps a low-level [DynamoDbClient] to add additional features before instantiating a new
25+
* [DynamoDbMapperImpl].
26+
*/
27+
fun wrapping(client: DynamoDbClient, config: DynamoDbMapper.Config): DynamoDbMapperImpl {
28+
val wrappedClient = client.withConfig { interceptors += DdbMapperMetricInterceptor }
29+
return DynamoDbMapperImpl(wrappedClient, config)
30+
}
31+
}
1732
override fun <T, PK> getTable(name: String, schema: ItemSchema.PartitionKey<T, PK>) =
1833
tableImpl(this, name, schema)
1934

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

3651
override fun build() = MapperConfigImpl(interceptors.toList())
3752
}
53+
54+
/**
55+
* An interceptor that emits the DynamoDB Mapper business metric
56+
*/
57+
private object DdbMapperMetricInterceptor : HttpInterceptor {
58+
override suspend fun modifyBeforeSerialization(context: RequestInterceptorContext<Any>): Any {
59+
context.executionContext.emitBusinessMetric(AwsBusinessMetric.DDB_MAPPER)
60+
return context.request
61+
}
62+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.hll.dynamodbmapper
6+
7+
import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor
8+
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
9+
import aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec
10+
import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter
11+
import aws.sdk.kotlin.hll.dynamodbmapper.operations.scanPaginated
12+
import aws.sdk.kotlin.hll.dynamodbmapper.testutils.DdbLocalTest
13+
import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter
14+
import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter
15+
import aws.sdk.kotlin.runtime.http.interceptors.AwsBusinessMetric
16+
import aws.sdk.kotlin.services.dynamodb.scan
17+
import aws.sdk.kotlin.services.dynamodb.withConfig
18+
import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric
19+
import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetrics
20+
import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
21+
import aws.smithy.kotlin.runtime.collections.get
22+
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
23+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
24+
import kotlinx.coroutines.flow.collect
25+
import kotlinx.coroutines.test.runTest
26+
import kotlin.test.assertFalse
27+
import kotlin.test.assertTrue
28+
29+
class DynamoDbMapperTest : DdbLocalTest() {
30+
companion object {
31+
private const val TABLE_NAME = "dummy"
32+
33+
private data class DummyData(var foo: String = "", var bar: Int = 0)
34+
35+
private val dummyConverter = SimpleItemConverter(
36+
::DummyData,
37+
{ this },
38+
AttributeDescriptor("foo", DummyData::foo, DummyData::foo::set, StringConverter),
39+
AttributeDescriptor("bar", DummyData::bar, DummyData::bar::set, IntConverter),
40+
)
41+
42+
private val dummySchema = ItemSchema(dummyConverter, KeySpec.String("foo"), KeySpec.Number("bar"))
43+
}
44+
45+
@BeforeAll
46+
fun setUp() = runTest {
47+
createTable(TABLE_NAME, dummySchema)
48+
}
49+
50+
@Test
51+
fun testBusinessMetricEmission() = runTest {
52+
val interceptor = MetricCapturingInterceptor()
53+
54+
val ddb = super.ddb.withConfig { interceptors += interceptor }
55+
interceptor.assertEmpty()
56+
57+
// No metric for low-level client
58+
ddb.scan { tableName = TABLE_NAME }
59+
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
60+
interceptor.reset()
61+
62+
// Metric for high-level client
63+
val mapper = mapper(ddb)
64+
val table = mapper.getTable(TABLE_NAME, dummySchema)
65+
table.scanPaginated { }.collect()
66+
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER)
67+
interceptor.reset()
68+
69+
// Still no metric for low-level client (i.e., LL wasn't modified by HL)
70+
ddb.scan { tableName = TABLE_NAME }
71+
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER, exists = false)
72+
interceptor.reset()
73+
74+
// Original client can be closed, mapper is unaffected
75+
ddb.close()
76+
table.scanPaginated { }.collect()
77+
interceptor.assertMetric(AwsBusinessMetric.DDB_MAPPER)
78+
}
79+
}
80+
81+
private class MetricCapturingInterceptor : HttpInterceptor {
82+
private val capturedMetrics = mutableSetOf<String>()
83+
84+
override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) {
85+
capturedMetrics += context.executionContext[BusinessMetrics]
86+
}
87+
88+
fun assertMetric(metric: BusinessMetric, exists: Boolean = true) {
89+
if (exists) {
90+
assertTrue(
91+
metric.identifier in capturedMetrics,
92+
"Expected metrics to contain $metric. Actual values: $capturedMetrics",
93+
)
94+
} else {
95+
assertFalse(
96+
metric.identifier in capturedMetrics,
97+
"Expected metrics *not* to contain $metric. Actual values: $capturedMetrics",
98+
)
99+
}
100+
}
101+
102+
fun assertEmpty() {
103+
assertTrue(capturedMetrics.isEmpty(), "Expected metrics to be empty. Actual values: $capturedMetrics")
104+
}
105+
106+
fun reset() {
107+
capturedMetrics.clear()
108+
}
109+
}

hll/dynamodb-mapper/dynamodb-mapper/common/test/aws/sdk/kotlin/hll/dynamodbmapper/testutils/DdbLocalTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ abstract class DdbLocalTest : AnnotationSpec() {
104104
* Returns a [DynamoDbMapper] instance utilizing the DynamoDB Local instance
105105
* @param config A function to set the configuration of the mapper before it's built
106106
*/
107-
fun mapper(config: DynamoDbMapper.Config.Builder.() -> Unit = { }) = DynamoDbMapper(ddb, config)
107+
fun mapper(
108+
ddb: DynamoDbClient? = null,
109+
config: DynamoDbMapper.Config.Builder.() -> Unit = { },
110+
) = DynamoDbMapper(ddb ?: this.ddb, config)
108111

109112
@AfterAll
110113
fun cleanUp() {

0 commit comments

Comments
 (0)