Skip to content

Commit f79a54b

Browse files
committed
[WIP] Implement Distributed Tracing
1 parent 2064247 commit f79a54b

File tree

3 files changed

+110
-32
lines changed

3 files changed

+110
-32
lines changed

Package.swift

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ let package = Package(
2727
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.13.0"),
2828
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.19.0"),
2929
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.4"),
30+
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
31+
.package(url: "https://github.com/apple/swift-service-context.git", from: "1.0.0"),
3032
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
3133
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
3234
],
@@ -53,6 +55,8 @@ let package = Package(
5355
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
5456
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
5557
.product(name: "Logging", package: "swift-log"),
58+
.product(name: "Tracing", package: "swift-distributed-tracing"),
59+
.product(name: "ServiceContextModule", package: "swift-service-context"),
5660
.product(name: "Atomics", package: "swift-atomics"),
5761
.product(name: "Algorithms", package: "swift-algorithms"),
5862
]

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

+74-32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import Logging
1616
import NIOCore
1717
import NIOHTTP1
18+
import ServiceContextModule
19+
import Tracing
1820

1921
import struct Foundation.URL
2022

@@ -34,18 +36,20 @@ extension HTTPClient {
3436
public func execute(
3537
_ request: HTTPClientRequest,
3638
deadline: NIODeadline,
37-
logger: Logger? = nil
39+
logger: Logger? = nil,
40+
context: ServiceContext? = nil
3841
) async throws -> HTTPClientResponse {
3942
try await self.executeAndFollowRedirectsIfNeeded(
4043
request,
4144
deadline: deadline,
4245
logger: logger ?? Self.loggingDisabled,
46+
context: context ?? .current ?? .topLevel,
4347
redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url)
4448
)
4549
}
4650
}
4751

48-
// MARK: Connivence methods
52+
// MARK: Convenience methods
4953

5054
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
5155
extension HTTPClient {
@@ -63,12 +67,14 @@ extension HTTPClient {
6367
public func execute(
6468
_ request: HTTPClientRequest,
6569
timeout: TimeAmount,
66-
logger: Logger? = nil
70+
logger: Logger? = nil,
71+
context: ServiceContext? = nil
6772
) async throws -> HTTPClientResponse {
6873
try await self.execute(
6974
request,
7075
deadline: .now() + timeout,
71-
logger: logger
76+
logger: logger,
77+
context: context
7278
)
7379
}
7480
}
@@ -81,6 +87,7 @@ extension HTTPClient {
8187
_ request: HTTPClientRequest,
8288
deadline: NIODeadline,
8389
logger: Logger,
90+
context: ServiceContext,
8491
redirectState: RedirectState?
8592
) async throws -> HTTPClientResponse {
8693
var currentRequest = request
@@ -91,7 +98,13 @@ extension HTTPClient {
9198
while true {
9299
let preparedRequest = try HTTPClientRequest.Prepared(currentRequest, dnsOverride: configuration.dnsOverride)
93100
let response = try await {
94-
var response = try await self.executeCancellable(preparedRequest, deadline: deadline, logger: logger)
101+
var response = try await self.executeCancellable(
102+
preparedRequest,
103+
deadline: deadline,
104+
logger: logger,
105+
context: context,
106+
resendCount: history.isEmpty ? nil : history.count
107+
)
95108

96109
history.append(
97110
.init(
@@ -149,39 +162,68 @@ extension HTTPClient {
149162
private func executeCancellable(
150163
_ request: HTTPClientRequest.Prepared,
151164
deadline: NIODeadline,
152-
logger: Logger
165+
logger: Logger,
166+
context: ServiceContext,
167+
resendCount: Int?
153168
) async throws -> HTTPClientResponse {
154169
let cancelHandler = TransactionCancelHandler()
155170

156-
return try await withTaskCancellationHandler(
157-
operation: { () async throws -> HTTPClientResponse in
158-
let eventLoop = self.eventLoopGroup.any()
159-
let deadlineTask = eventLoop.scheduleTask(deadline: deadline) {
160-
cancelHandler.cancel(reason: .deadlineExceeded)
161-
}
162-
defer {
163-
deadlineTask.cancel()
164-
}
165-
return try await withCheckedThrowingContinuation {
166-
(continuation: CheckedContinuation<HTTPClientResponse, Swift.Error>) -> Void in
167-
let transaction = Transaction(
168-
request: request,
169-
requestOptions: .fromClientConfiguration(self.configuration),
170-
logger: logger,
171-
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
172-
preferredEventLoop: eventLoop,
173-
responseContinuation: continuation
174-
)
171+
return try await withSpan(request.head.method.rawValue, context: context, ofKind: .client) { span in
172+
var request = request
173+
request.head.headers.propagate(span.context)
174+
span.updateAttributes { attributes in
175+
attributes["http.request.method"] = request.head.method.rawValue
176+
attributes["server.address"] = request.poolKey.connectionTarget.host
177+
attributes["server.port"] = request.poolKey.connectionTarget.port
178+
attributes["url.full"] = request.url.absoluteString
179+
attributes["http.request.resend_count"] = resendCount
180+
}
181+
182+
do {
183+
let response = try await withTaskCancellationHandler(
184+
operation: { () async throws -> HTTPClientResponse in
185+
let eventLoop = self.eventLoopGroup.any()
186+
let deadlineTask = eventLoop.scheduleTask(deadline: deadline) {
187+
cancelHandler.cancel(reason: .deadlineExceeded)
188+
}
189+
defer {
190+
deadlineTask.cancel()
191+
}
192+
let response = try await withCheckedThrowingContinuation {
193+
(continuation: CheckedContinuation<HTTPClientResponse, Swift.Error>) -> Void in
194+
let transaction = Transaction(
195+
request: request,
196+
requestOptions: .fromClientConfiguration(self.configuration),
197+
logger: logger,
198+
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
199+
preferredEventLoop: eventLoop,
200+
responseContinuation: continuation
201+
)
175202

176-
cancelHandler.registerTransaction(transaction)
203+
cancelHandler.registerTransaction(transaction)
177204

178-
self.poolManager.executeRequest(transaction)
179-
}
180-
},
181-
onCancel: {
182-
cancelHandler.cancel(reason: .taskCanceled)
205+
self.poolManager.executeRequest(transaction)
206+
}
207+
if response.status.code >= 400 {
208+
span.setStatus(.init(code: .error))
209+
span.attributes["error.type"] = "\(response.status.code)"
210+
}
211+
span.attributes["http.response.status_code"] = "\(response.status.code)"
212+
return response
213+
},
214+
onCancel: {
215+
span.setStatus(.init(code: .error))
216+
span.attributes["error.type"] = "\(CancellationError.self)"
217+
cancelHandler.cancel(reason: .taskCanceled)
218+
}
219+
)
220+
return response
221+
} catch {
222+
span.setStatus(.init(code: .error))
223+
span.attributes["error.type"] = "\(type(of: error))"
224+
throw error
183225
}
184-
)
226+
}
185227
}
186228
}
187229

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2024 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 Instrumentation
16+
import NIOHTTP1
17+
import ServiceContextModule
18+
19+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
20+
extension HTTPHeaders {
21+
mutating func propagate(_ context: ServiceContext) {
22+
InstrumentationSystem.instrument.inject(context, into: &self, using: Self.injector)
23+
}
24+
25+
private static let injector = HTTPHeadersInjector()
26+
}
27+
28+
private struct HTTPHeadersInjector: Injector {
29+
func inject(_ value: String, forKey name: String, into headers: inout HTTPHeaders) {
30+
headers.add(name: name, value: value)
31+
}
32+
}

0 commit comments

Comments
 (0)