Skip to content

Commit 1c44d5d

Browse files
authored
misc: tweak metrics to support service-level benchmarks (#908)
1 parent f62b7cf commit 1c44d5d

File tree

7 files changed

+72
-17
lines changed

7 files changed

+72
-17
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "73448090-7b5b-4c5a-875a-730efacacf9d",
3+
"type": "misc",
4+
"description": "Tweak metrics to better support service-level benchmarks",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#968"
7+
]
8+
}

runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/metrics/Histogram.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public typealias DoubleHistogram = Histogram<Double>
3939
*/
4040
@InternalApi
4141
public fun DoubleHistogram.recordSeconds(value: Duration, attributes: Attributes = emptyAttributes(), context: Context? = null) {
42-
val ms = value.inWholeMilliseconds.toDouble()
43-
val sec = ms / 1000
42+
val ms = value.inWholeNanoseconds.toDouble()
43+
val sec = ms / 1_000_000_000
4444
record(sec, attributes, context)
4545
}
4646

runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package aws.smithy.kotlin.runtime.http.engine.okhttp
66

77
import aws.smithy.kotlin.runtime.ExperimentalApi
8+
import aws.smithy.kotlin.runtime.http.engine.EngineAttributes
89
import aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics
910
import aws.smithy.kotlin.runtime.net.HostResolver
1011
import aws.smithy.kotlin.runtime.net.toHostAddress
@@ -54,6 +55,8 @@ internal class HttpEngineEventListener(
5455
private var signaledConnectAcquireDuration = false
5556
private var dnsStartTime: TimeMark? = null
5657

58+
private var requestTimeEnd: TimeMark? = null
59+
5760
private inline fun trace(crossinline msg: MessageSupplier) {
5861
logger.trace { msg() }
5962
}
@@ -158,14 +161,22 @@ internal class HttpEngineEventListener(
158161

159162
override fun requestBodyStart(call: Call) = trace { "sending request body" }
160163

161-
override fun requestBodyEnd(call: Call, byteCount: Long) =
164+
override fun requestBodyEnd(call: Call, byteCount: Long) {
165+
requestTimeEnd = TimeSource.Monotonic.markNow()
162166
trace { "finished sending request body: bytesSent=$byteCount" }
167+
}
163168

164169
override fun requestFailed(call: Call, ioe: IOException) = trace(ioe) { "request failed" }
165170

166171
override fun requestHeadersStart(call: Call) = trace { "sending request headers" }
167172

168-
override fun requestHeadersEnd(call: Call, request: Request) = trace { "finished sending request headers" }
173+
override fun requestHeadersEnd(call: Call, request: Request) {
174+
if (request.body == null) {
175+
requestTimeEnd = TimeSource.Monotonic.markNow()
176+
}
177+
178+
trace { "finished sending request headers" }
179+
}
169180

170181
override fun responseBodyStart(call: Call) = trace { "response body available" }
171182

@@ -174,7 +185,13 @@ internal class HttpEngineEventListener(
174185

175186
override fun responseFailed(call: Call, ioe: IOException) = trace(ioe) { "response failed" }
176187

177-
override fun responseHeadersStart(call: Call) = trace { "response headers start" }
188+
override fun responseHeadersStart(call: Call) {
189+
requestTimeEnd?.elapsedNow()?.let { ttfb ->
190+
metrics.timeToFirstByteDuration.recordSeconds(ttfb)
191+
call.request().tag<SdkRequestTag>()?.execContext?.set(EngineAttributes.TimeToFirstByte, ttfb)
192+
}
193+
trace { "response headers start" }
194+
}
178195

179196
override fun responseHeadersEnd(call: Call, response: Response) {
180197
val contentLength = response.body.contentLength()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.http.engine
7+
8+
import aws.smithy.kotlin.runtime.InternalApi
9+
import aws.smithy.kotlin.runtime.util.AttributeKey
10+
import kotlin.time.Duration
11+
12+
/**
13+
* Common attributes related to HTTP engines.
14+
*/
15+
@InternalApi
16+
public object EngineAttributes {
17+
/**
18+
* The time between sending the request completely and receiving the first byte of the response. This effectively
19+
* measures the time spent waiting on a response.
20+
*/
21+
public val TimeToFirstByte: AttributeKey<Duration> = AttributeKey("TimeToFirstByte")
22+
}

runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ public class HttpClientMetrics(
116116
"The total number of bytes received by the HTTP client",
117117
)
118118

119+
public val timeToFirstByteDuration: DoubleHistogram = meter.createDoubleHistogram(
120+
"smithy.client.http.time_to_first_byte",
121+
"s",
122+
"The amount of time after a request has been sent spent waiting on a response from the remote server",
123+
)
124+
119125
/**
120126
* The maximum number of connections configured for the client
121127
*/

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
99
import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext
1010
import aws.smithy.kotlin.runtime.client.RequestInterceptorContext
1111
import aws.smithy.kotlin.runtime.client.ResponseInterceptorContext
12+
import aws.smithy.kotlin.runtime.http.engine.EngineAttributes
1213
import aws.smithy.kotlin.runtime.http.operation.OperationMetrics
1314
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1415
import aws.smithy.kotlin.runtime.http.response.HttpResponse
1516
import aws.smithy.kotlin.runtime.telemetry.metrics.recordSeconds
16-
import aws.smithy.kotlin.runtime.util.attributesOf
17-
import aws.smithy.kotlin.runtime.util.merge
18-
import aws.smithy.kotlin.runtime.util.mutableAttributesOf
17+
import aws.smithy.kotlin.runtime.util.*
1918
import kotlin.time.ExperimentalTime
2019
import kotlin.time.TimeMark
2120
import kotlin.time.TimeSource
@@ -39,7 +38,7 @@ internal class OperationTelemetryInterceptor(
3938
private var callStart: TimeMark? = null
4039
private var serializeStart: TimeMark? = null
4140
private var deserializeStart: TimeMark? = null
42-
private var transmitStart: TimeMark? = null
41+
private var attemptStart: TimeMark? = null
4342
private var attempts = 0
4443

4544
private val perRpcAttributes = attributesOf {
@@ -94,6 +93,13 @@ internal class OperationTelemetryInterceptor(
9493
if (attempts > 1) {
9594
metrics.rpcRetryCount.add(1L, perRpcAttributes, metrics.provider.contextManager.current())
9695
}
96+
97+
val attemptDuration = attemptStart?.elapsedNow() ?: return
98+
metrics.rpcAttemptDuration.recordSeconds(attemptDuration, perRpcAttributes, metrics.provider.contextManager.current())
99+
100+
context.executionContext.takeOrNull(EngineAttributes.TimeToFirstByte)?.let { ttfb ->
101+
metrics.rpcAttemptOverheadDuration.recordSeconds(attemptDuration - ttfb, perRpcAttributes)
102+
}
97103
}
98104

99105
override fun readBeforeDeserialization(context: ProtocolResponseInterceptorContext<Any, HttpRequest, HttpResponse>) {
@@ -105,12 +111,7 @@ internal class OperationTelemetryInterceptor(
105111
metrics.deserializationDuration.recordSeconds(deserializeDuration, perRpcAttributes, metrics.provider.contextManager.current())
106112
}
107113

108-
override fun readBeforeTransmit(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) {
109-
transmitStart = timeSource.markNow()
110-
}
111-
112-
override fun readAfterTransmit(context: ProtocolResponseInterceptorContext<Any, HttpRequest, HttpResponse>) {
113-
val serviceCallDuration = transmitStart?.elapsedNow() ?: return
114-
metrics.rpcAttemptDuration.recordSeconds(serviceCallDuration, perRpcAttributes, metrics.provider.contextManager.current())
114+
override fun readBeforeAttempt(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) {
115+
attemptStart = timeSource.markNow()
115116
}
116117
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public class OperationMetrics(
3434
public val rpcRetryCount: MonotonicCounter = meter.createMonotonicCounter("smithy.client.retries", "{count}", "The number of retries for an operation")
3535
public val rpcRequestSize: LongHistogram = meter.createLongHistogram("smithy.client.request.size", "By", "Size of the serialized request message")
3636
public val rpcResponseSize: LongHistogram = meter.createLongHistogram("smithy.client.response.size", "By", "Size of the serialized response message")
37-
public val rpcAttemptDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.attempt_duration", "s", "The time it takes to connect to the service, send the request, and receive the HTTP status code and headers from the response for an operation")
37+
public val rpcAttemptDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.attempt_duration", "s", "The time it takes to connect to complete an entire call attempt, including identity resolution, endpoint resolution, signing, sending the request, and receiving the HTTP status code and headers from the response for an operation")
38+
public val rpcAttemptOverheadDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.attempt_overhead_duration", "s", "The time it takes to execute an attempt minus the time spent waiting for a response from the remote server")
3839
public val serializationDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.serialization_duration", "s", "The time it takes to serialize a request message body")
3940
public val deserializationDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.deserialization_duration", "s", "The time it takes to deserialize a response message body")
4041
public val resolveEndpointDuration: DoubleHistogram = meter.createDoubleHistogram("smithy.client.resolve_endpoint_duration", "s", "The time it takes to resolve an endpoint for a request")

0 commit comments

Comments
 (0)