Skip to content

Commit c15a988

Browse files
committed
wip: early attempt at invocation-scoped metrics provider
1 parent 6944364 commit c15a988

File tree

10 files changed

+289
-5
lines changed

10 files changed

+289
-5
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
description = "Telemetry provider for invocation-scoped metrics"
6+
extra["displayName"] = "Smithy :: Kotlin :: Observability :: Invocation-scoped Metrics Provider"
7+
extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry.ism"
8+
9+
apply(plugin = "kotlinx-atomicfu")
10+
11+
kotlin {
12+
sourceSets {
13+
commonMain {
14+
dependencies {
15+
api(project(":runtime:observability:telemetry-api"))
16+
implementation(project(":runtime:observability:telemetry-defaults"))
17+
}
18+
}
19+
20+
jvmMain {
21+
dependencies {
22+
implementation(project(":runtime:protocol:http-client")) // for operation-telemetry attributes
23+
}
24+
}
25+
26+
all {
27+
languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi")
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.ism.context
6+
7+
import aws.smithy.kotlin.runtime.collections.Attributes
8+
import aws.smithy.kotlin.runtime.http.operation.OperationAttributes
9+
import aws.smithy.kotlin.runtime.telemetry.context.Context
10+
import aws.smithy.kotlin.runtime.telemetry.context.ContextManager
11+
import aws.smithy.kotlin.runtime.telemetry.context.Scope
12+
import aws.smithy.kotlin.runtime.telemetry.ism.metrics.MetricRecord
13+
import aws.smithy.kotlin.runtime.telemetry.ism.metrics.ScopeMetrics
14+
15+
public interface SpanListener {
16+
public fun onNewSpan(parentContext: Context?, name: String, attributes: Attributes): Context
17+
public fun onCloseSpan(context: Context)
18+
}
19+
20+
internal interface ContextStorage {
21+
fun get(): Context
22+
fun getAndSet(value: Context): Context
23+
fun requireAndSet(expect: Context, update: Context)
24+
}
25+
26+
internal expect fun ContextStorage(initialContext: Context): ContextStorage
27+
28+
public class IsmContextManager private constructor() : ContextManager {
29+
public companion object {
30+
public fun createWithScopeListener(): Pair<IsmContextManager, SpanListener> {
31+
val manager = IsmContextManager()
32+
val listener = object : SpanListener {
33+
override fun onNewSpan(parentContext: Context?, name: String, attributes: Attributes) =
34+
manager.onNewSpan(parentContext, name, attributes)
35+
36+
override fun onCloseSpan(context: Context) = manager.onCloseSpan(context)
37+
}
38+
return manager to listener
39+
}
40+
}
41+
42+
private val rootContext = object : HierarchicalContext(null) { }
43+
private val storage = ContextStorage(rootContext)
44+
45+
override fun current(): Context = storage.get()
46+
47+
private fun onNewSpan(parentContext: Context?, name: String, attributes: Attributes): Context =
48+
when (parentContext) {
49+
rootContext -> {
50+
val service = attributes.getOrNull(OperationAttributes.RpcService)
51+
val operation = attributes.getOrNull(OperationAttributes.RpcOperation)
52+
val sdkInvocationId = attributes.getOrNull(OperationAttributes.AwsInvocationId)
53+
if (service != null && operation != null && sdkInvocationId != null) {
54+
OperationContext(service, operation, sdkInvocationId)
55+
} else {
56+
OtherContext(parentContext)
57+
}
58+
}
59+
60+
is OperationContext, is ChildContext -> ChildContext(parentContext)
61+
62+
else -> OtherContext(parentContext)
63+
}
64+
65+
private fun onCloseSpan(context: Context) {
66+
TODO()
67+
}
68+
69+
private abstract inner class HierarchicalContext(val parent: Context?) : Context {
70+
override fun makeCurrent(): Scope {
71+
storage.requireAndSet(parent ?: Context.None, this)
72+
return IsmScope(this)
73+
}
74+
}
75+
76+
private inner class OperationContext(
77+
val service: String,
78+
val operation: String,
79+
val sdkInvocationId: String,
80+
val records: MutableList<MetricRecord<*>> = mutableListOf(),
81+
val childScopes: MutableMap<String, ScopeMetrics> = mutableMapOf(),
82+
) : HierarchicalContext(rootContext)
83+
84+
private inner class ChildContext(
85+
parent: Context,
86+
val records: MutableList<MetricRecord<*>> = mutableListOf(),
87+
val childScopes: MutableMap<String, ScopeMetrics> = mutableMapOf(),
88+
) : HierarchicalContext(parent)
89+
90+
private inner class OtherContext(parent: Context?) : HierarchicalContext(parent)
91+
92+
private inner class IsmScope(val context: Context) : Scope {
93+
override fun close() {
94+
storage.requireAndSet(context, Context.None)
95+
}
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.ism.metrics
6+
7+
import aws.smithy.kotlin.runtime.ExperimentalApi
8+
import aws.smithy.kotlin.runtime.collections.Attributes
9+
import aws.smithy.kotlin.runtime.telemetry.context.Context
10+
import aws.smithy.kotlin.runtime.telemetry.metrics.Meter
11+
import aws.smithy.kotlin.runtime.telemetry.metrics.MeterProvider
12+
import aws.smithy.kotlin.runtime.time.Instant
13+
14+
public interface MetricRecord<T> {
15+
// Group these into higher-level type?
16+
public val name: String
17+
public val units: String?
18+
public val description: String?
19+
20+
public val value: T
21+
public val attributes: Attributes
22+
public val context: Context
23+
24+
public val timestamp: Instant
25+
}
26+
27+
private data class MetricRecordImpl<T>(
28+
override val name: String,
29+
override val units: String?,
30+
override val description: String?,
31+
override val value: T,
32+
override val attributes: Attributes,
33+
override val context: Context,
34+
override val timestamp: Instant,
35+
) : MetricRecord<T>
36+
37+
public fun <T> MetricRecord(
38+
name: String,
39+
units: String?,
40+
description: String?,
41+
value: T,
42+
attributes: Attributes,
43+
context: Context,
44+
timestamp: Instant,
45+
): MetricRecord<T> = MetricRecordImpl(name, units, description, value, attributes, context, timestamp)
46+
47+
public interface ScopeMetrics {
48+
public val records: Map<String, List<MetricRecord<*>>> // Feels like this should be keyed by typed attributes
49+
public val childScopes: Map<String, ScopeMetrics>
50+
}
51+
52+
private data class ScopeMetricsImpl(
53+
override val records: Map<String, List<MetricRecord<*>>>,
54+
override val childScopes: Map<String, ScopeMetrics>,
55+
) : ScopeMetrics
56+
57+
public fun ScopeMetrics(
58+
records: Map<String, List<MetricRecord<*>>>,
59+
childScopes: Map<String, ScopeMetrics>,
60+
): ScopeMetrics =
61+
ScopeMetricsImpl(records, childScopes)
62+
63+
public interface OperationMetrics : ScopeMetrics {
64+
public val service: String
65+
public val operation: String
66+
public val sdkInvocationId: String
67+
}
68+
69+
private data class OperationMetricsImpl(
70+
override val service: String,
71+
override val operation: String,
72+
override val sdkInvocationId: String,
73+
override val records: Map<String, List<MetricRecord<*>>>,
74+
override val childScopes: Map<String, ScopeMetrics>,
75+
) : OperationMetrics
76+
77+
public fun OperationMetrics(
78+
service: String,
79+
operation: String,
80+
sdkInvocationId: String,
81+
records: Map<String, List<MetricRecord<*>>>,
82+
childScopes: Map<String, ScopeMetrics>,
83+
): OperationMetrics = OperationMetricsImpl(service, operation, sdkInvocationId, records, childScopes)
84+
85+
@ExperimentalApi
86+
public interface OperationMetricsCollector {
87+
public fun onOperationMetrics(metrics: OperationMetrics)
88+
}
89+
90+
@ExperimentalApi
91+
public class IsmMetricsProvider(private val collector: OperationMetricsCollector) : MeterProvider {
92+
override fun getOrCreateMeter(scope: String): Meter {
93+
TODO("Not yet implemented")
94+
}
95+
}
96+
97+
@ExperimentalApi
98+
private class OperationMeter(private val collector: OperationMetricsCollector) {
99+
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.ism.trace
6+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.ism.context
6+
7+
import aws.smithy.kotlin.runtime.telemetry.context.Context
8+
import kotlinx.atomicfu.atomic
9+
import kotlinx.atomicfu.update
10+
11+
internal actual fun ContextStorage(initialContext: Context): ContextStorage = ThreadLocalContextStorage(initialContext)
12+
13+
private class ThreadLocalContextStorage(private val initialContext: Context) : ContextStorage {
14+
private val threadLocal = ThreadLocal.withInitial { AtomicHolder(initialContext) }
15+
16+
override fun get(): Context = threadLocal.get().get()
17+
override fun getAndSet(value: Context): Context = threadLocal.get().getAndSet(value)
18+
override fun requireAndSet(expect: Context, update: Context) = threadLocal.get().requireAndSet(expect, update)
19+
}
20+
21+
private class AtomicHolder(initialContext: Context) : ContextStorage {
22+
private val current = atomic(initialContext)
23+
24+
override fun get(): Context = current.value
25+
26+
override fun getAndSet(value: Context): Context = current.getAndSet(value)
27+
28+
override fun requireAndSet(expect: Context, update: Context) = current.update { existing ->
29+
check(existing == expect) { "Invalid state when updating context! Expected = $expect, actual = $existing" }
30+
update
31+
}
32+
}

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/OperationTelemetryInterceptor.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import aws.smithy.kotlin.runtime.collections.merge
1414
import aws.smithy.kotlin.runtime.collections.mutableAttributesOf
1515
import aws.smithy.kotlin.runtime.collections.takeOrNull
1616
import aws.smithy.kotlin.runtime.http.engine.EngineAttributes
17+
import aws.smithy.kotlin.runtime.http.operation.OperationAttributes
1718
import aws.smithy.kotlin.runtime.http.operation.OperationMetrics
1819
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1920
import aws.smithy.kotlin.runtime.http.response.HttpResponse
@@ -44,8 +45,8 @@ internal class OperationTelemetryInterceptor(
4445
private var attempts = 0
4546

4647
private val perRpcAttributes = attributesOf {
47-
"rpc.service" to service
48-
"rpc.method" to operation
48+
OperationAttributes.RpcService to service
49+
OperationAttributes.RpcOperation to operation
4950
}
5051

5152
override fun readBeforeExecution(context: RequestInterceptorContext<Any>) {

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public object HttpOperationContext {
5151
public val OperationMetrics: AttributeKey<OperationMetrics> = AttributeKey("aws.smithy.kotlin#OperationMetrics")
5252

5353
/**
54-
* Cached attribute level attributes (e.g. rpc.method, rpc.service, etc)
54+
* Cached operation-level attributes (e.g. rpc.method, rpc.service, etc). See [OperationAttributes] for more
55+
* details.
5556
*/
5657
public val OperationAttributes: AttributeKey<Attributes> = AttributeKey("aws.smithy.kotlin#OperationAttributes")
5758

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.http.operation
6+
7+
import aws.smithy.kotlin.runtime.InternalApi
8+
import aws.smithy.kotlin.runtime.collections.AttributeKey
9+
10+
@InternalApi
11+
public object OperationAttributes {
12+
public val RpcService: AttributeKey<String> = AttributeKey("rpc.service")
13+
public val RpcOperation: AttributeKey<String> = AttributeKey("rpc.operation")
14+
public val AwsInvocationId: AttributeKey<String> = AttributeKey("aws.invocation_id")
15+
}

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/OperationTelemetry.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ internal fun <I, O> SdkHttpOperation<I, O>.instrument(): Pair<TraceSpan, Corouti
6262
val parentCtx = telemetry.provider.contextManager.current()
6363

6464
val opAttributes = attributesOf {
65-
"rpc.service" to serviceName
66-
"rpc.method" to opName
65+
OperationAttributes.RpcService to serviceName
66+
OperationAttributes.RpcOperation to opName
67+
OperationAttributes.AwsInvocationId to this@instrument.context[HttpOperationContext.SdkInvocationId]
6768
}
6869

6970
val initialAttributes = telemetry.attributes.toMutableAttributes().apply {

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ include(":runtime:crt-util")
4545
include(":runtime:observability:logging-slf4j2")
4646
include(":runtime:observability:telemetry-api")
4747
include(":runtime:observability:telemetry-defaults")
48+
include(":runtime:observability:telemetry-provider-ism")
4849
include(":runtime:observability:telemetry-provider-otel")
4950
include(":runtime:protocol:aws-protocol-core")
5051
include(":runtime:protocol:aws-event-stream")

0 commit comments

Comments
 (0)