From 3a9b0e0040bdb789f1ffc017702aa774a195fa3f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 10 Sep 2025 18:12:43 +0200 Subject: [PATCH] [Only for discussion] Simple/naive tracing --- Package.swift | 2 + .../AsyncAwait/HTTPClient+execute.swift | 38 +++++++++-- Sources/AsyncHTTPClient/HTTPClient.swift | 63 +++++++++++++++---- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/Package.swift b/Package.swift index 3294781a9..42593c259 100644 --- a/Package.swift +++ b/Package.swift @@ -45,6 +45,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"), .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), + .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.2.2"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), ], @@ -71,6 +72,7 @@ let package = Package( .product(name: "NIOSOCKS", package: "swift-nio-extras"), .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), .product(name: "Logging", package: "swift-log"), + .product(name: "Tracing", package: "swift-distributed-tracing"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), ], diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 5fc1be9f5..c3b8e695a 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -13,11 +13,22 @@ //===----------------------------------------------------------------------===// import Logging +import Tracing import NIOCore import NIOHTTP1 import struct Foundation.URL +private struct HTTPHeadersInjector: Injector, @unchecked Sendable { + static let shared: HTTPHeadersInjector = HTTPHeadersInjector() + + private init() {} + + func inject(_ value: String, forKey name: String, into headers: inout HTTPHeaders) { + headers.add(name: name, value: value) + } +} + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClient { /// Execute arbitrary HTTP requests. @@ -36,12 +47,27 @@ extension HTTPClient { deadline: NIODeadline, logger: Logger? = nil ) async throws -> HTTPClientResponse { - try await self.executeAndFollowRedirectsIfNeeded( - request, - deadline: deadline, - logger: logger ?? Self.loggingDisabled, - redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url) - ) + func perform(request: HTTPClientRequest) async throws -> HTTPClientResponse { + try await self.executeAndFollowRedirectsIfNeeded( + request, + deadline: deadline, + logger: logger ?? Self.loggingDisabled, + redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url) + ) + } + if let tracer = self.configuration.tracer { + return try await tracer.withSpan("HTTPClient.execute") { span in + var request = request + tracer.inject( + span.context, + into: &request.headers, + using: HTTPHeadersInjector.shared + ) + return try await perform(request: request) + } + } else { + return try await perform(request: request) + } } } diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index f22810378..8aa76b37c 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -15,6 +15,7 @@ import Atomics import Foundation import Logging +import Tracing import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 @@ -807,7 +808,7 @@ public final class HTTPClient: Sendable { public struct Configuration { /// TLS configuration, defaults to `TLSConfiguration.makeClientConfiguration()`. public var tlsConfiguration: Optional - + /// Sometimes it can be useful to connect to one host e.g. `x.example.com` but /// request and validate the certificate chain as if we would connect to `y.example.com`. /// ``dnsOverride`` allows to do just that by mapping host names which we will request and validate the certificate chain, to a different @@ -817,7 +818,7 @@ public final class HTTPClient: Sendable { /// `url` of `https://example.com/`, the ``HTTPClient`` will actually open a connection to `localhost` instead of `example.com`. /// ``HTTPClient`` will still request certificates from the server for `example.com` and validate them as if we would connect to `example.com`. public var dnsOverride: [String: String] = [:] - + /// Enables following 3xx redirects automatically. /// /// Following redirects are supported: @@ -841,24 +842,24 @@ public final class HTTPClient: Sendable { /// Ignore TLS unclean shutdown error, defaults to `false`. @available( *, - deprecated, - message: + deprecated, + message: "AsyncHTTPClient now correctly supports handling unexpected SSL connection drops. This property is ignored" ) public var ignoreUncleanSSLShutdown: Bool { get { false } set {} } - + /// What HTTP versions to use. /// /// Set to ``HTTPVersion-swift.struct/automatic`` by default which will use HTTP/2 if run over https and the server supports it, otherwise HTTP/1 public var httpVersion: HTTPVersion - + /// Whether ``HTTPClient`` will let Network.framework sit in the `.waiting` state awaiting new network changes, or fail immediately. Defaults to `true`, /// which is the recommended setting. Only set this to `false` when attempting to trigger a particular error path. public var networkFrameworkWaitForConnectivity: Bool - + /// The maximum number of times each connection can be used before it is replaced with a new one. Use `nil` (the default) /// if no limit should be applied to each connection. /// @@ -870,20 +871,35 @@ public final class HTTPClient: Sendable { } } } - + /// Whether ``HTTPClient`` will use Multipath TCP or not /// By default, don't use it public var enableMultipath: Bool - + /// A method with access to the HTTP/1 connection channel that is called when creating the connection. public var http1_1ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? - + /// A method with access to the HTTP/2 connection channel that is called when creating the connection. public var http2ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? - + /// A method with access to the HTTP/2 stream channel that is called when creating the stream. public var http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? - + + private var anyTracer: (any Sendable)? + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public var tracer: (any Tracer)? { + get { + guard let anyTracer else { + return nil + } + return anyTracer as! (any Tracer)? + } + set { + self.anyTracer = newValue + } + } + public init( tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, @@ -903,6 +919,29 @@ public final class HTTPClient: Sendable { self.networkFrameworkWaitForConnectivity = true self.enableMultipath = false } + + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public init( + tlsConfiguration: TLSConfiguration? = nil, + redirectConfiguration: RedirectConfiguration? = nil, + timeout: Timeout = Timeout(), + connectionPool: ConnectionPool = ConnectionPool(), + proxy: Proxy? = nil, + ignoreUncleanSSLShutdown: Bool = false, + decompression: Decompression = .disabled, + tracer: (any Tracer)? = InstrumentationSystem.tracer + ) { + self.init( + tlsConfiguration: tlsConfiguration, + redirectConfiguration: redirectConfiguration, + timeout: timeout, + connectionPool: connectionPool, + proxy: proxy, + ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown, + decompression: decompression + ) + self.anyTracer = tracer + } public init( tlsConfiguration: TLSConfiguration? = nil,