Skip to content

Commit 8430dd4

Browse files
ktososlashmoglbrntt
authored
Introduce built-in swift-distributed-tracing support (#857)
Co-authored-by: Moritz Lang <[email protected]> Co-authored-by: George Barnett <[email protected]>
1 parent 31c8b04 commit 8430dd4

13 files changed

+487
-18
lines changed

.licenseignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*.json
1717
Package.swift
1818
**/Package.swift
19+
Package@swift-*.swift
20+
**/Package@swift-*.swift
1921
Package@-*.swift
2022
**/Package@-*.swift
2123
Package.resolved

Package.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ let package = Package(
4747
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"),
4848
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
4949
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
50+
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
5051
],
5152
targets: [
5253
.target(
@@ -70,9 +71,11 @@ let package = Package(
7071
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
7172
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
7273
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
73-
.product(name: "Logging", package: "swift-log"),
7474
.product(name: "Atomics", package: "swift-atomics"),
7575
.product(name: "Algorithms", package: "swift-algorithms"),
76+
// Observability support
77+
.product(name: "Logging", package: "swift-log"),
78+
.product(name: "Tracing", package: "swift-distributed-tracing"),
7679
],
7780
swiftSettings: strictConcurrencySettings
7881
),
@@ -89,9 +92,12 @@ let package = Package(
8992
.product(name: "NIOSSL", package: "swift-nio-ssl"),
9093
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
9194
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
92-
.product(name: "Logging", package: "swift-log"),
9395
.product(name: "Atomics", package: "swift-atomics"),
9496
.product(name: "Algorithms", package: "swift-algorithms"),
97+
// Observability support
98+
.product(name: "Logging", package: "swift-log"),
99+
.product(name: "Tracing", package: "swift-distributed-tracing"),
100+
.product(name: "InMemoryTracing", package: "swift-distributed-tracing"),
95101
],
96102
resources: [
97103
.copy("Resources/self_signed_cert.pem"),

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import Logging
1616
import NIOCore
1717
import NIOHTTP1
18+
import Tracing
1819

1920
import struct Foundation.URL
2021

@@ -36,12 +37,14 @@ extension HTTPClient {
3637
deadline: NIODeadline,
3738
logger: Logger? = nil
3839
) async throws -> HTTPClientResponse {
39-
try await self.executeAndFollowRedirectsIfNeeded(
40-
request,
41-
deadline: deadline,
42-
logger: logger ?? Self.loggingDisabled,
43-
redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url)
44-
)
40+
try await withRequestSpan(request) {
41+
try await self.executeAndFollowRedirectsIfNeeded(
42+
request,
43+
deadline: deadline,
44+
logger: logger ?? Self.loggingDisabled,
45+
redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url)
46+
)
47+
}
4548
}
4649
}
4750

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOHTTP1
16+
import Tracing
17+
18+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
19+
extension HTTPClient {
20+
@inlinable
21+
func withRequestSpan(
22+
_ request: HTTPClientRequest,
23+
_ body: () async throws -> HTTPClientResponse
24+
) async rethrows -> HTTPClientResponse {
25+
guard let tracer = self.tracer else {
26+
return try await body()
27+
}
28+
29+
return try await tracer.withSpan(request.method.rawValue, ofKind: .client) { span in
30+
let keys = self.configuration.tracing.attributeKeys
31+
span.attributes[keys.requestMethod] = request.method.rawValue
32+
// TODO: set more attributes on the span
33+
let response = try await body()
34+
35+
// set response span attributes
36+
TracingSupport.handleResponseStatusCode(span, response.status, keys: tracing.attributeKeys)
37+
38+
return response
39+
}
40+
}
41+
}

Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import NIOConcurrencyHelpers
1717
import NIOCore
1818
import NIOHTTP1
1919
import NIOSSL
20+
import Tracing
2021

2122
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2223
@usableFromInline

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import NIOPosix
2323
import NIOSSL
2424
import NIOTLS
2525
import NIOTransportServices
26+
import Tracing
2627

2728
extension Logger {
2829
private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value {
@@ -62,15 +63,29 @@ public final class HTTPClient: Sendable {
6263
///
6364
/// All HTTP transactions will occur on loops owned by this group.
6465
public let eventLoopGroup: EventLoopGroup
65-
let configuration: Configuration
6666
let poolManager: HTTPConnectionPool.Manager
6767

68+
@usableFromInline
69+
let configuration: Configuration
70+
6871
/// Shared thread pool used for file IO. It is lazily created on first access of ``Task/fileIOThreadPool``.
6972
private let fileIOThreadPool: NIOLockedValueBox<NIOThreadPool?>
7073

7174
private let state: NIOLockedValueBox<State>
7275
private let canBeShutDown: Bool
7376

77+
/// Tracer configured for this HTTPClient at configuration time.
78+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
79+
public var tracer: (any Tracer)? {
80+
configuration.tracing.tracer
81+
}
82+
83+
/// Access to tracing configuration in order to get configured attribute keys etc.
84+
@usableFromInline
85+
package var tracing: TracingConfiguration {
86+
self.configuration.tracing
87+
}
88+
7489
static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() })
7590

7691
/// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration.
@@ -705,6 +720,7 @@ public final class HTTPClient: Sendable {
705720
request,
706721
requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed)
707722
)
723+
708724
let taskEL: EventLoop
709725
switch eventLoopPreference.preference {
710726
case .indifferent:
@@ -734,7 +750,7 @@ public final class HTTPClient: Sendable {
734750
]
735751
)
736752

737-
let failedTask: Task<Delegate.Response>? = self.state.withLockedValue { state in
753+
let failedTask: Task<Delegate.Response>? = self.state.withLockedValue { state -> (Task<Delegate.Response>?) in
738754
switch state {
739755
case .upAndRunning:
740756
return nil
@@ -744,6 +760,7 @@ public final class HTTPClient: Sendable {
744760
eventLoop: taskEL,
745761
error: HTTPClientError.alreadyShutdown,
746762
logger: logger,
763+
tracing: tracing,
747764
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool
748765
)
749766
}
@@ -768,11 +785,14 @@ public final class HTTPClient: Sendable {
768785
}
769786
}()
770787

771-
let task = Task<Delegate.Response>(
772-
eventLoop: taskEL,
773-
logger: logger,
774-
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool
775-
)
788+
let task: HTTPClient.Task<Delegate.Response> =
789+
Task<Delegate.Response>(
790+
eventLoop: taskEL,
791+
logger: logger,
792+
tracing: self.tracing,
793+
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool
794+
)
795+
776796
do {
777797
let requestBag = try RequestBag(
778798
request: request,
@@ -884,6 +904,9 @@ public final class HTTPClient: Sendable {
884904
/// A method with access to the HTTP/2 stream channel that is called when creating the stream.
885905
public var http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)?
886906

907+
/// Configuration how distributed traces are created and handled.
908+
public var tracing: TracingConfiguration = .init()
909+
887910
public init(
888911
tlsConfiguration: TLSConfiguration? = nil,
889912
redirectConfiguration: RedirectConfiguration? = nil,
@@ -1012,6 +1035,84 @@ public final class HTTPClient: Sendable {
10121035
self.http2ConnectionDebugInitializer = http2ConnectionDebugInitializer
10131036
self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer
10141037
}
1038+
1039+
public init(
1040+
tlsConfiguration: TLSConfiguration? = nil,
1041+
redirectConfiguration: RedirectConfiguration? = nil,
1042+
timeout: Timeout = Timeout(),
1043+
connectionPool: ConnectionPool = ConnectionPool(),
1044+
proxy: Proxy? = nil,
1045+
ignoreUncleanSSLShutdown: Bool = false,
1046+
decompression: Decompression = .disabled,
1047+
http1_1ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
1048+
http2ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
1049+
http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture<Void>)? = nil,
1050+
tracing: TracingConfiguration = .init()
1051+
) {
1052+
self.init(
1053+
tlsConfiguration: tlsConfiguration,
1054+
redirectConfiguration: redirectConfiguration,
1055+
timeout: timeout,
1056+
connectionPool: connectionPool,
1057+
proxy: proxy,
1058+
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
1059+
decompression: decompression
1060+
)
1061+
self.http1_1ConnectionDebugInitializer = http1_1ConnectionDebugInitializer
1062+
self.http2ConnectionDebugInitializer = http2ConnectionDebugInitializer
1063+
self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer
1064+
self.tracing = tracing
1065+
}
1066+
}
1067+
1068+
public struct TracingConfiguration: Sendable {
1069+
1070+
@usableFromInline
1071+
var _tracer: Optional<any Sendable> // erasure trick so we don't have to make Configuration @available
1072+
1073+
/// Tracer that should be used by the HTTPClient.
1074+
///
1075+
/// This is selected at configuration creation time, and if no tracer is passed explicitly,
1076+
/// (including `nil` in order to disable traces), the default global bootstrapped tracer will
1077+
/// be stored in this property, and used for all subsequent requests made by this client.
1078+
@inlinable
1079+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
1080+
public var tracer: (any Tracer)? {
1081+
get {
1082+
guard let _tracer else {
1083+
return nil
1084+
}
1085+
return _tracer as! (any Tracer)?
1086+
}
1087+
set {
1088+
self._tracer = newValue
1089+
}
1090+
}
1091+
1092+
// TODO: Open up customization of keys we use?
1093+
/// Configuration for tracing attributes set by the HTTPClient.
1094+
@usableFromInline
1095+
package var attributeKeys: AttributeKeys
1096+
1097+
public init() {
1098+
self._tracer = nil
1099+
self.attributeKeys = .init()
1100+
}
1101+
1102+
/// Span attribute keys that the HTTPClient should set automatically.
1103+
/// This struct allows the configuration of the attribute names (keys) which will be used for the apropriate values.
1104+
@usableFromInline
1105+
package struct AttributeKeys: Sendable {
1106+
@usableFromInline package var requestMethod: String = "http.request.method"
1107+
@usableFromInline package var requestBodySize: String = "http.request.body.size"
1108+
1109+
@usableFromInline package var responseBodySize: String = "http.response.body.size"
1110+
@usableFromInline package var responseStatusCode: String = "http.status_code"
1111+
1112+
@usableFromInline package var httpFlavor: String = "http.flavor"
1113+
1114+
@usableFromInline package init() {}
1115+
}
10151116
}
10161117

10171118
/// Specifies how `EventLoopGroup` will be created and establishes lifecycle ownership.

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import NIOCore
1919
import NIOHTTP1
2020
import NIOPosix
2121
import NIOSSL
22+
import Tracing
2223

2324
#if compiler(>=6.0)
2425
import Foundation
@@ -924,6 +925,12 @@ extension HTTPClient {
924925
/// The `Logger` used by the `Task` for logging.
925926
public let logger: Logger // We are okay to store the logger here because a Task is for only one request.
926927

928+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
929+
public var tracer: (any Tracer)? {
930+
tracing.tracer
931+
}
932+
let tracing: TracingConfiguration
933+
927934
let promise: EventLoopPromise<Response>
928935

929936
struct State: Sendable {
@@ -953,10 +960,16 @@ extension HTTPClient {
953960
self.makeOrGetFileIOThreadPool()
954961
}
955962

956-
init(eventLoop: EventLoop, logger: Logger, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool) {
963+
init(
964+
eventLoop: EventLoop,
965+
logger: Logger,
966+
tracing: TracingConfiguration,
967+
makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool
968+
) {
957969
self.eventLoop = eventLoop
958970
self.promise = eventLoop.makePromise()
959971
self.logger = logger
972+
self.tracing = tracing
960973
self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool
961974
self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil))
962975
}
@@ -965,11 +978,13 @@ extension HTTPClient {
965978
eventLoop: EventLoop,
966979
error: Error,
967980
logger: Logger,
981+
tracing: TracingConfiguration,
968982
makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool
969983
) -> Task<Response> {
970984
let task = self.init(
971985
eventLoop: eventLoop,
972986
logger: logger,
987+
tracing: tracing,
973988
makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool
974989
)
975990
task.promise.fail(error)

0 commit comments

Comments
 (0)