Skip to content

Commit 9e3ea22

Browse files
syallSichan Yoo
andauthored
feat: Telemetry for operations and HTTP (#735)
* chore: add `SelectNoAuthScheme` in test utils * fix: change `LongAsyncMeasurement` to `Int * feat: wire telemetry in Orchestrator * feat: add HTTP client telemetry * feat: wire `CRTClientEngine` with telemetry * feat: wire `URLSessionHTTPClient` with telemetry * feat: add telemetry client codegen * Fix platform related error. * Add default value --------- Co-authored-by: Sichan Yoo <[email protected]>
1 parent 38c83b1 commit 9e3ea22

File tree

22 files changed

+1036
-250
lines changed

22 files changed

+1036
-250
lines changed

Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public extension DefaultSDKRuntimeConfiguration {
9898
let connectTimeoutMs = httpClientConfiguration.connectTimeout.map { UInt32($0 * 1000) }
9999
let socketTimeout = UInt32(httpClientConfiguration.socketTimeout)
100100
let config = CRTClientEngineConfig(
101+
telemetry: httpClientConfiguration.telemetry ?? CRTClientEngine.noOpCrtClientEngineTelemetry,
101102
connectTimeoutMs: connectTimeoutMs,
102103
crtTlsOptions: httpClientConfiguration.tlsConfiguration as? CRTClientTLSOptions,
103104
socketTimeout: socketTimeout

Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift

Lines changed: 280 additions & 113 deletions
Large diffs are not rendered by default.

Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngineConfig.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* SPDX-License-Identifier: Apache-2.0.
44
*/
55

6-
import AwsCommonRuntimeKit
7-
86
struct CRTClientEngineConfig {
97

108
/// Max connections the manager can contain per endpoint
@@ -13,6 +11,9 @@ struct CRTClientEngineConfig {
1311
/// The IO channel window size to use for connections in the connection pool
1412
let windowSize: Int
1513

14+
/// HTTP Client Telemetry
15+
let telemetry: HttpTelemetry
16+
1617
/// The default is true for clients and false for servers.
1718
/// You should not change this default for clients unless
1819
/// you're testing and don't want to fool around with CA trust stores.
@@ -31,13 +32,15 @@ struct CRTClientEngineConfig {
3132
public init(
3233
maxConnectionsPerEndpoint: Int = 50,
3334
windowSize: Int = 16 * 1024 * 1024,
35+
telemetry: HttpTelemetry = CRTClientEngine.noOpCrtClientEngineTelemetry,
3436
verifyPeer: Bool = true,
3537
connectTimeoutMs: UInt32? = nil,
3638
crtTlsOptions: CRTClientTLSOptions? = nil,
3739
socketTimeout: UInt32? = nil
3840
) {
3941
self.maxConnectionsPerEndpoint = maxConnectionsPerEndpoint
4042
self.windowSize = windowSize
43+
self.telemetry = telemetry
4144
self.verifyPeer = verifyPeer
4245
self.connectTimeoutMs = connectTimeoutMs
4346
self.crtTlsOptions = crtTlsOptions

Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import enum Smithy.ByteStream
9+
import struct Smithy.Attributes
910
import AwsCommonRuntimeKit
1011

1112
extension HTTP2Stream {
@@ -19,13 +20,24 @@ extension HTTP2Stream {
1920
/// There is no recommended size for the data to write. The data will be written in chunks of `manualWriteBufferSize` bytes.
2021
/// - Parameter body: The body to write
2122
/// - Throws: Throws an error if the write fails
22-
func write(body: ByteStream) async throws {
23+
func write(body: ByteStream, telemetry: HttpTelemetry, serverAddress: String) async throws {
24+
let context = telemetry.contextManager.current()
25+
var bytesSentAttributes = Attributes()
26+
bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: serverAddress)
2327
switch body {
2428
case .data(let data):
2529
try await writeChunk(chunk: data ?? .init(), endOfStream: true)
30+
telemetry.bytesSent.add(
31+
value: data?.count ?? 0,
32+
attributes: bytesSentAttributes,
33+
context: context)
2634
case .stream(let stream):
2735
while let data = try await stream.readAsync(upToCount: manualWriteBufferSize) {
2836
try await writeChunk(chunk: data, endOfStream: false)
37+
telemetry.bytesSent.add(
38+
value: data.count,
39+
attributes: bytesSentAttributes,
40+
context: context)
2941
}
3042
try await writeChunk(chunk: .init(), endOfStream: true)
3143
case .noStream:

Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public class HttpClientConfiguration {
4343
/// Defaults to system's TLS settings if `nil`.
4444
public var tlsConfiguration: (any TLSConfiguration)?
4545

46+
/// HTTP Client Telemetry
47+
public var telemetry: HttpTelemetry?
48+
4649
/// Creates a configuration object for a SDK HTTP client.
4750
///
4851
/// Not all configuration settings may be followed by all clients.
@@ -58,12 +61,14 @@ public class HttpClientConfiguration {
5861
socketTimeout: TimeInterval = 60.0,
5962
protocolType: URIScheme = .https,
6063
defaultHeaders: Headers = Headers(),
61-
tlsConfiguration: (any TLSConfiguration)? = nil
64+
tlsConfiguration: (any TLSConfiguration)? = nil,
65+
telemetry: HttpTelemetry? = nil
6266
) {
6367
self.socketTimeout = socketTimeout
6468
self.protocolType = protocolType
6569
self.defaultHeaders = defaultHeaders
6670
self.connectTimeout = connectTimeout
6771
self.tlsConfiguration = tlsConfiguration
72+
self.telemetry = telemetry
6873
}
6974
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
import protocol SmithyHTTPAPI.HTTPClient
7+
import struct Smithy.Attributes
8+
import struct Smithy.AttributeKey
9+
10+
/// Container for HTTPClient telemetry, including configurable attributes and names.
11+
///
12+
/// Note: This is intended to be used within generated code, not directly.
13+
public class HttpTelemetry {
14+
private static var idleConnectionAttributes: Attributes {
15+
var attributes = Attributes()
16+
attributes.set(key: HttpMetricsAttributesKeys.state, value: ConnectionState.idle)
17+
return attributes
18+
}
19+
private static var acquiredConnectionAttributes: Attributes {
20+
var attributes = Attributes()
21+
attributes.set(key: HttpMetricsAttributesKeys.state, value: ConnectionState.acquired)
22+
return attributes
23+
}
24+
private static var inflightRequestsAttributes: Attributes {
25+
var attributes = Attributes()
26+
attributes.set(key: HttpMetricsAttributesKeys.state, value: RequestState.inflight)
27+
return attributes
28+
}
29+
private static var queuedRequestsAttributes: Attributes {
30+
var attributes = Attributes()
31+
attributes.set(key: HttpMetricsAttributesKeys.state, value: RequestState.queued)
32+
return attributes
33+
}
34+
35+
internal let contextManager: any TelemetryContextManager
36+
internal let tracerProvider: any TracerProvider
37+
internal let loggerProvider: any LoggerProvider
38+
39+
internal let tracerScope: String
40+
internal let tracerAttributes: Attributes?
41+
internal let spanName: String
42+
internal let spanAttributes: Attributes?
43+
44+
internal let connectionsAcquireDuration: any Histogram
45+
private let connectionsLimit: any AsyncMeasurementHandle
46+
private let connectionsUsage: any AsyncMeasurementHandle
47+
internal var httpMetricsUsage: HttpMetricsUsage
48+
internal let connectionsUptime: any Histogram
49+
private let requestsUsage: any AsyncMeasurementHandle
50+
internal let requestsQueuedDuration: any Histogram
51+
internal let bytesSent: any MonotonicCounter
52+
internal let bytesReceived: any MonotonicCounter
53+
54+
public init(
55+
httpScope: String,
56+
telemetryProvider: any TelemetryProvider = DefaultTelemetry.provider,
57+
meterScope: String? = nil,
58+
meterAttributes: Attributes? = nil,
59+
tracerScope: String? = nil,
60+
tracerAttributes: Attributes? = nil,
61+
spanName: String? = nil,
62+
spanAttributes: Attributes? = nil
63+
) {
64+
self.contextManager = telemetryProvider.contextManager
65+
self.tracerProvider = telemetryProvider.tracerProvider
66+
self.loggerProvider = telemetryProvider.loggerProvider
67+
let meterScope: String = meterScope != nil ? meterScope! : httpScope
68+
self.tracerScope = tracerScope != nil ? tracerScope! : httpScope
69+
self.tracerAttributes = tracerAttributes
70+
self.spanName = spanName != nil ? spanName! : "HTTP"
71+
self.spanAttributes = spanAttributes
72+
let httpMetricsUsage = HttpMetricsUsage()
73+
self.httpMetricsUsage = httpMetricsUsage
74+
75+
let meter = telemetryProvider.meterProvider.getMeter(scope: meterScope, attributes: meterAttributes)
76+
self.connectionsAcquireDuration = meter.createHistogram(
77+
name: "smithy.client.http.connections.acquire_duration",
78+
units: "s",
79+
description: "The time it takes a request to acquire a connection")
80+
self.connectionsLimit = meter.createAsyncUpDownCounter(
81+
name: "smithy.client.http.connections.limit",
82+
callback: { handle in
83+
handle.record(
84+
value: httpMetricsUsage.connectionsLimit,
85+
attributes: Attributes(),
86+
context: telemetryProvider.contextManager.current()
87+
)
88+
},
89+
units: "{connection}",
90+
description: "The maximum open connections allowed/configured for the HTTP client")
91+
self.connectionsUsage = meter.createAsyncUpDownCounter(
92+
name: "smithy.client.http.connections.usage",
93+
callback: { handle in
94+
handle.record(
95+
value: httpMetricsUsage.idleConnections,
96+
attributes: HttpTelemetry.idleConnectionAttributes,
97+
context: telemetryProvider.contextManager.current()
98+
)
99+
handle.record(
100+
value: httpMetricsUsage.acquiredConnections,
101+
attributes: HttpTelemetry.acquiredConnectionAttributes,
102+
context: telemetryProvider.contextManager.current()
103+
)
104+
},
105+
units: "{connection}",
106+
description: "Current state of connections pool")
107+
self.connectionsUptime = meter.createHistogram(
108+
name: "smithy.client.http.connections.uptime",
109+
units: "s",
110+
description: "The amount of time a connection has been open")
111+
self.requestsUsage = meter.createAsyncUpDownCounter(
112+
name: "smithy.client.http.requests.usage",
113+
callback: { handle in
114+
handle.record(
115+
value: httpMetricsUsage.idleConnections,
116+
attributes: HttpTelemetry.idleConnectionAttributes,
117+
context: telemetryProvider.contextManager.current()
118+
)
119+
handle.record(
120+
value: httpMetricsUsage.acquiredConnections,
121+
attributes: HttpTelemetry.acquiredConnectionAttributes,
122+
context: telemetryProvider.contextManager.current()
123+
)
124+
},
125+
units: "{request}",
126+
description: "The current state of HTTP client request concurrency")
127+
self.requestsQueuedDuration = meter.createHistogram(
128+
name: "smithy.client.http.requests.queued_duration",
129+
units: "s",
130+
description: "The amount of time a request spent queued waiting to be executed by the HTTP client")
131+
self.bytesSent = meter.createCounter(
132+
name: "smithy.client.http.bytes_sent",
133+
units: "By",
134+
description: "The total number of bytes sent by the HTTP client")
135+
self.bytesReceived = meter.createCounter(
136+
name: "smithy.client.http.bytes_received",
137+
units: "By",
138+
description: "The total number of bytes received by the HTTP client")
139+
}
140+
141+
deinit {
142+
self.connectionsLimit.stop()
143+
self.connectionsUsage.stop()
144+
self.requestsUsage.stop()
145+
}
146+
}
147+
148+
private enum ConnectionState {
149+
fileprivate static let idle = "idle"
150+
fileprivate static let acquired = "acquired"
151+
}
152+
153+
private enum RequestState {
154+
fileprivate static let inflight = "inflight"
155+
fileprivate static let queued = "queued"
156+
}
157+
158+
internal enum HttpMetricsAttributesKeys {
159+
fileprivate static let state = AttributeKey<String>(name: "state")
160+
internal static let serverAddress = AttributeKey<String>(name: "server.address")
161+
}
162+
163+
internal struct HttpMetricsUsage {
164+
public var connectionsLimit: Int = 0
165+
public var idleConnections: Int = 0
166+
public var acquiredConnections: Int = 0
167+
public var inflightRequests: Int = 0
168+
public var queuedRequests: Int = 0
169+
}

Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import protocol Smithy.LogAgent
1111
import protocol Smithy.ReadableStream
12+
import struct Smithy.Attributes
1213
import func Foundation.CFWriteStreamSetDispatchQueue
1314
import class Foundation.DispatchQueue
1415
import class Foundation.NSObject
@@ -61,6 +62,12 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
6162
/// A Logger for logging events.
6263
private let logger: LogAgent
6364

65+
/// HTTP Client Telemetry
66+
private let telemetry: HttpTelemetry
67+
68+
/// Server address
69+
private let serverAddress: String
70+
6471
/// Actor used to ensure writes are performed in series, one at a time.
6572
private actor WriteCoordinator {
6673
var task: Task<Void, Error>?
@@ -119,13 +126,17 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
119126
readableStream: ReadableStream,
120127
bridgeBufferSize: Int = 65_536,
121128
boundStreamBufferSize: Int? = nil,
122-
logger: LogAgent
129+
logger: LogAgent,
130+
telemetry: HttpTelemetry,
131+
serverAddress: String = "unknown"
123132
) {
124133
self.bridgeBufferSize = bridgeBufferSize
125134
self.boundStreamBufferSize = boundStreamBufferSize ?? bridgeBufferSize
126135
self.buffer = Data(capacity: bridgeBufferSize)
127136
self.readableStream = readableStream
128137
self.logger = logger
138+
self.telemetry = telemetry
139+
self.serverAddress = serverAddress
129140
(inputStream, outputStream) = Self.makeStreams(boundStreamBufferSize: self.boundStreamBufferSize, queue: queue)
130141
}
131142

@@ -272,6 +283,15 @@ class FoundationStreamBridge: NSObject, StreamDelegate {
272283
if writeCount > 0 {
273284
logger.info("FoundationStreamBridge: wrote \(writeCount) bytes to request body")
274285
buffer.removeFirst(writeCount)
286+
// TICK - smithy.client.http.bytes_sent
287+
var attributes = Attributes()
288+
attributes.set(
289+
key: HttpMetricsAttributesKeys.serverAddress,
290+
value: serverAddress)
291+
telemetry.bytesSent.add(
292+
value: writeCount,
293+
attributes: attributes,
294+
context: telemetry.contextManager.current())
275295
}
276296

277297
// Resume the caller now that the write is complete, returning the stream error, if any.

0 commit comments

Comments
 (0)