From 2d9c03bcfa94080e6b92325b7dcce613f39af255 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 10 Sep 2025 16:20:19 +0900 Subject: [PATCH 01/19] [Tracing] Initial tracing support implementation --- Package.swift | 12 +- Package@swift-6.1.swift | 135 ++++++++++++++++++ .../AsyncAwait/HTTPClient+execute.swift | 30 +++- .../AsyncAwait/Transaction.swift | 37 ++++- Sources/AsyncHTTPClient/HTTPClient.swift | 129 ++++++++++++++++- Sources/AsyncHTTPClient/HTTPHandler.swift | 37 +++++ .../NIOTransportServices/NWErrorHandler.swift | 4 + Sources/AsyncHTTPClient/RequestBag.swift | 19 +++ Sources/AsyncHTTPClient/TracingSupport.swift | 117 +++++++++++++++ .../AsyncHTTPClientTests/HTTPClientBase.swift | 4 +- .../HTTPClientTracingTests.swift | 110 ++++++++++++++ 11 files changed, 621 insertions(+), 13 deletions(-) create mode 100644 Package@swift-6.1.swift create mode 100644 Sources/AsyncHTTPClient/TracingSupport.swift create mode 100644 Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift diff --git a/Package.swift b/Package.swift index 3294781a9..f462d76ee 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.1 //===----------------------------------------------------------------------===// // // This source file is part of the AsyncHTTPClient open source project @@ -35,9 +35,16 @@ let strictConcurrencySettings: [SwiftSetting] = { let package = Package( name: "async-http-client", + platforms: [ // FIXME: must remove this + .macOS("10.15") + ], products: [ .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) ], + traits: [ + .trait(name: "TracingSupport"), + .default(enabledTraits: ["TracingSupport"]), + ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), @@ -47,6 +54,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), .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"), + .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.2.0"), ], targets: [ .target( @@ -73,6 +81,8 @@ let package = Package( .product(name: "Logging", package: "swift-log"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), + // Observability support + .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), ], swiftSettings: strictConcurrencySettings ), diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift new file mode 100644 index 000000000..9ae34b921 --- /dev/null +++ b/Package@swift-6.1.swift @@ -0,0 +1,135 @@ +// swift-tools-version:6.1 +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +let strictConcurrencyDevelopment = false + +let strictConcurrencySettings: [SwiftSetting] = { + var initialSettings: [SwiftSetting] = [] + initialSettings.append(contentsOf: [ + .enableUpcomingFeature("StrictConcurrency"), + .enableUpcomingFeature("InferSendableFromCaptures"), + ]) + + if strictConcurrencyDevelopment { + // -warnings-as-errors here is a workaround so that IDE-based development can + // get tripped up on -require-explicit-sendable. + initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"])) + } + + return initialSettings +}() + +let package = Package( + name: "async-http-client", + platforms: [ // FIXME: must remove this + .macOS("10.15") + ], + products: [ + .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) + ], + traits: [ + .trait(name: "TracingSupport"), + .default(enabledTraits: ["TracingSupport"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), + .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), + .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"), + .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-atomics.git", from: "1.0.2"), + .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"), + ], + targets: [ + .target( + name: "CAsyncHTTPClient", + cSettings: [ + .define("_GNU_SOURCE") + ] + ), + .target( + name: "AsyncHTTPClient", + dependencies: [ + .target(name: "CAsyncHTTPClient"), + .product(name: "NIO", package: "swift-nio"), + .product(name: "NIOTLS", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "Algorithms", package: "swift-algorithms"), + // Observability support + .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + .product(name: "InMemoryTracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + ], + swiftSettings: strictConcurrencySettings + ), + .testTarget( + name: "AsyncHTTPClientTests", + dependencies: [ + .target(name: "AsyncHTTPClient"), + .product(name: "NIOTLS", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), + .product(name: "NIOEmbedded", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), + .product(name: "NIOHTTP2", package: "swift-nio-http2"), + .product(name: "NIOSOCKS", package: "swift-nio-extras"), + .product(name: "Logging", package: "swift-log"), + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "Algorithms", package: "swift-algorithms"), + // Observability support + .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + .product(name: "InMemoryTracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + ], + resources: [ + .copy("Resources/self_signed_cert.pem"), + .copy("Resources/self_signed_key.pem"), + .copy("Resources/example.com.cert.pem"), + .copy("Resources/example.com.private-key.pem"), + ], + swiftSettings: strictConcurrencySettings + ), + ] +) + +// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // +for target in package.targets { + switch target.type { + case .regular, .test, .executable: + var settings = target.swiftSettings ?? [] + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + settings.append(.enableUpcomingFeature("MemberImportVisibility")) + target.swiftSettings = settings + case .macro, .plugin, .system, .binary: + () // not applicable + @unknown default: + () // we don't know what to do here, do nothing + } +} +// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 5fc1be9f5..6d986d5bc 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -15,6 +15,9 @@ import Logging import NIOCore import NIOHTTP1 +#if TracingSupport +import Tracing +#endif import struct Foundation.URL @@ -36,12 +39,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 doExecute() async throws -> HTTPClientResponse { + try await self.executeAndFollowRedirectsIfNeeded( + request, + deadline: deadline, + logger: logger ?? Self.loggingDisabled, + redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url) + ) + } + + #if TracingSupport + if let tracer = self.tracer { + return try await tracer.withSpan("\(request.method)") { span -> (HTTPClientResponse) in + let attr = self.configuration.tracing.attributeKeys + span.attributes[attr.requestMethod] = request.method.rawValue + // Set more attributes on the span + return try await doExecute() + } + } + #endif + + return try await doExecute() } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 6bf8b38b7..a0251b857 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -18,6 +18,10 @@ import NIOCore import NIOHTTP1 import NIOSSL +#if TracingSupport +import Tracing +#endif // TracingSupport + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline final class Transaction: @@ -32,14 +36,20 @@ final class Transaction: let preferredEventLoop: EventLoop let requestOptions: RequestOptions + #if TracingSupport + let span: (any Span)? + #endif + private let state: NIOLockedValueBox + #if TracingSupport init( request: HTTPClientRequest.Prepared, requestOptions: RequestOptions, logger: Logger, connectionDeadline: NIODeadline, preferredEventLoop: EventLoop, + span: (any Span)?, responseContinuation: CheckedContinuation ) { self.request = request @@ -47,11 +57,36 @@ final class Transaction: self.logger = logger self.connectionDeadline = connectionDeadline self.preferredEventLoop = preferredEventLoop + self.span = span + self.state = NIOLockedValueBox(StateMachine(responseContinuation)) + } + #endif // TracingSupport + + init( + request: HTTPClientRequest.Prepared, + requestOptions: RequestOptions, + logger: Logger, + connectionDeadline: NIODeadline, + preferredEventLoop: EventLoop, + responseContinuation: CheckedContinuation + ) { + print("[swift] new transaction = \(request)") + self.request = request + self.requestOptions = requestOptions + self.logger = logger + self.connectionDeadline = connectionDeadline + self.preferredEventLoop = preferredEventLoop + self.span = nil self.state = NIOLockedValueBox(StateMachine(responseContinuation)) } func cancel() { - self.fail(CancellationError()) + let error = CancellationError() + self.fail(error) + #if TracingSupport + self.span?.recordError(error) + self.span?.end() + #endif } // MARK: Request body helpers diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index f22810378..7f90a6950 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -24,6 +24,10 @@ import NIOSSL import NIOTLS import NIOTransportServices +#if TracingSupport +import Tracing +#endif + extension Logger { private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value { "\(request.method) \(request.url)" @@ -71,7 +75,15 @@ public final class HTTPClient: Sendable { private let state: NIOLockedValueBox private let canBeShutDown: Bool - static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) + #if TracingSupport + @_spi(Tracing) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext + public var tracer: (any Tracer)? { + configuration.tracing.tracer + } + #endif // TracingSupport + + public static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) /// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration. /// @@ -672,7 +684,7 @@ public final class HTTPClient: Sendable { deadline: NIODeadline? = nil, logger: Logger? ) -> Task { - self._execute( + return self._execute( request: request, delegate: delegate, eventLoop: eventLoopPreference, @@ -699,12 +711,28 @@ public final class HTTPClient: Sendable { eventLoop eventLoopPreference: EventLoopPreference, deadline: NIODeadline? = nil, logger originalLogger: Logger?, - redirectState: RedirectState? + redirectState: RedirectState?, ) -> Task { let logger = (originalLogger ?? HTTPClient.loggingDisabled).attachingRequestInformation( request, requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed) ) + + // #if TracingSupport + // let span: (any Span)? // we may be still executing the same span, e.g. under redirection etc. + // if let activeSpan { + // span = activeSpan + // } else if let tracer = self.tracer { + // let s = tracer.startSpan(request.method.rawValue) + // let attrs = self.configuration.tracing.attributeKeys + // s.attributes[attrs.requestMethod] = request.method.rawValue + // s.attributes["loc"] = "\(#fileID):\(#line)" + // span = s + // } else { + // span = nil + // } + // #endif + let taskEL: EventLoop switch eventLoopPreference.preference { case .indifferent: @@ -740,10 +768,16 @@ public final class HTTPClient: Sendable { return nil case .shuttingDown, .shutDown: logger.debug("client is shutting down, failing request") + let error = HTTPClientError.alreadyShutdown + // #if TracingSupport + // span?.recordError(error) + // span?.end() + // #endif return Task.failedTask( eventLoop: taskEL, - error: HTTPClientError.alreadyShutdown, + error: error, logger: logger, + tracer: tracer, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) } @@ -771,8 +805,10 @@ public final class HTTPClient: Sendable { let task = Task( eventLoop: taskEL, logger: logger, + tracer: tracer, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) + do { let requestBag = try RequestBag( request: request, @@ -884,6 +920,10 @@ public final class HTTPClient: Sendable { /// A method with access to the HTTP/2 stream channel that is called when creating the stream. public var http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? + #if TracingSupport + public var tracing: TracingConfiguration = .init() + #endif + public init( tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, @@ -1012,7 +1052,88 @@ public final class HTTPClient: Sendable { self.http2ConnectionDebugInitializer = http2ConnectionDebugInitializer self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer } + + #if TracingSupport + public init( + tlsConfiguration: TLSConfiguration? = nil, + redirectConfiguration: RedirectConfiguration? = nil, + timeout: Timeout = Timeout(), + connectionPool: ConnectionPool = ConnectionPool(), + proxy: Proxy? = nil, + ignoreUncleanSSLShutdown: Bool = false, + decompression: Decompression = .disabled, + http1_1ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? = nil, + http2ConnectionDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? = nil, + http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? = nil, + tracing: TracingConfiguration = .init() + ) { + self.init( + tlsConfiguration: tlsConfiguration, + redirectConfiguration: redirectConfiguration, + timeout: timeout, + connectionPool: connectionPool, + proxy: proxy, + ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown, + decompression: decompression + ) + self.http1_1ConnectionDebugInitializer = http1_1ConnectionDebugInitializer + self.http2ConnectionDebugInitializer = http2ConnectionDebugInitializer + self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer + self.tracing = tracing + } + #endif + } + + #if TracingSupport + public struct TracingConfiguration: Sendable { + + @usableFromInline + var _tracer: Optional // erasure trick so we don't have to make Configuration @available + + /// Tracer that should be used by the HTTPClient. + /// + /// This is selected at configuration creation time, and if no tracer is passed explicitly, + /// (including `nil` in order to disable traces), the default global bootstrapped tracer will + /// be stored in this property, and used for all subsequent requests made by this client. + @inlinable + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public var tracer: (any Tracer)? { + get { + guard let _tracer else { + return nil + } + return _tracer as! (any Tracer)? + } + set { + self._tracer = newValue + } + } + + public var attributeKeys: AttributeKeys + + public init( + tracer: (any Tracer)? = InstrumentationSystem.tracer, + attributeKeys: AttributeKeys = .init() + ) { + self._tracer = tracer + self.attributeKeys = attributeKeys + } + + /// Span attribute keys that the HTTPClient should set automatically. + /// This struct allows the configuration of the attribute names (keys) which will be used for the apropriate values. + public struct AttributeKeys: Sendable { + public var requestMethod: String = "http.request.method" + public var requestBodySize: String = "http.request.body.size" + + public var responseBodySize: String = "http.response.size" + public var responseStatusCode: String = "http.status_code" + + public var httpFlavor: String = "http.flavor" + + public init() {} + } } + #endif /// Specifies how `EventLoopGroup` will be created and establishes lifecycle ownership. public enum EventLoopGroupProvider { diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 8d92d8ef7..6a2eb3dfa 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -20,6 +20,10 @@ import NIOHTTP1 import NIOPosix import NIOSSL +#if TracingSupport +import Tracing +#endif + #if compiler(>=6.0) import Foundation #else @@ -923,6 +927,10 @@ extension HTTPClient { public let eventLoop: EventLoop /// The `Logger` used by the `Task` for logging. public let logger: Logger // We are okay to store the logger here because a Task is for only one request. + + #if TracingSupport + public let tracer: (any Tracer)? // We are okay to store the tracer here because a Task is for only one request. + #endif let promise: EventLoopPromise @@ -957,6 +965,32 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger + self.tracer = nil + self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool + self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) + } + + static func failedTask( + eventLoop: EventLoop, + error: Error, + logger: Logger, + makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool + ) -> Task { + let task = self.init( + eventLoop: eventLoop, + logger: logger, + makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool + ) + task.promise.fail(error) + return task + } + + #if TracingSupport + init(eventLoop: EventLoop, logger: Logger, tracer: (any Tracer)?, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool) { + self.eventLoop = eventLoop + self.promise = eventLoop.makePromise() + self.logger = logger + self.tracer = tracer self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } @@ -965,16 +999,19 @@ extension HTTPClient { eventLoop: EventLoop, error: Error, logger: Logger, + tracer: (any Tracer)?, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool ) -> Task { let task = self.init( eventLoop: eventLoop, logger: logger, + tracer: tracer, makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool ) task.promise.fail(error) return task } + #endif /// `EventLoopFuture` for the response returned by this request. public var futureResult: EventLoopFuture { diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift index 148b4a4c4..79d0b5ed7 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift @@ -25,7 +25,11 @@ extension HTTPClient { /// A wrapper for `POSIX` errors thrown by `Network.framework`. public struct NWPOSIXError: Error, CustomStringConvertible { /// POSIX error code (enum) + #if compiler(>=6.1) + nonisolated(unsafe) public let errorCode: POSIXErrorCode + #else public let errorCode: POSIXErrorCode + #endif /// actual reason, in human readable form private let reason: String diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index f206325ee..4a2032f33 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -18,6 +18,10 @@ import NIOCore import NIOHTTP1 import NIOSSL +#if TracingSupport +import Tracing +#endif + @preconcurrency final class RequestBag: Sendable { /// Defends against the call stack getting too large when consuming body parts. @@ -50,6 +54,11 @@ final class RequestBag: Sendabl var consumeBodyPartStackDepth: Int // if a redirect occurs, we store the task for it so we can propagate cancellation var redirectTask: HTTPClient.Task? = nil + + #if TracingSupport + // The current span, representing the entire request/response made by an execute call. + var activeSpan: (any Span)? = nil + #endif // TracingSupport } private let loopBoundState: NIOLoopBoundBox @@ -62,6 +71,10 @@ final class RequestBag: Sendabl let connectionDeadline: NIODeadline + var tracer: HTTPClientTracingSupportTracerType? { + self.task.tracer + } + let requestOptions: RequestOptions let requestHead: HTTPRequestHead @@ -114,14 +127,19 @@ final class RequestBag: Sendabl // MARK: - Request - private func willExecuteRequest0(_ executor: HTTPRequestExecutor) { + // Immediately start a span for the "whole" request + self.loopBoundState.value.startRequestSpan(tracer: self.tracer) + let action = self.loopBoundState.value.state.willExecuteRequest(executor) switch action { case .cancelExecuter(let executor): executor.cancelRequest(self) + self.loopBoundState.value.failRequestSpan(error: CancellationError()) case .failTaskAndCancelExecutor(let error, let executor): self.delegate.didReceiveError(task: self.task, error) self.task.failInternal(with: error) executor.cancelRequest(self) + self.loopBoundState.value.failRequestSpan(error: CancellationError()) case .none: break } @@ -230,6 +248,7 @@ final class RequestBag: Sendabl private func receiveResponseHead0(_ head: HTTPResponseHead) { self.delegate.didVisitURL(task: self.task, self.loopBoundState.value.request, head) + self.loopBoundState.value.endRequestSpan(response: head) // runs most likely on channel eventLoop switch self.loopBoundState.value.state.receiveResponseHead(head) { diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift new file mode 100644 index 000000000..7e8fbe08c --- /dev/null +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOHTTP1 +import NIOSSL + +#if TracingSupport +import Tracing +#endif + +#if TracingSupport +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) + } +} +#endif // TracingSupport + +#if TracingSupport +typealias HTTPClientTracingSupportTracerType = any Tracer +#else +enum TracingSupportDisabledTracer {} +typealias HTTPClientTracingSupportTracerType = TracingSupportDisabledTracer +#endif + +protocol _TracingSupportOperations { + associatedtype TracerType + + /// Starts the "overall" Span that encompases the beginning of a request until receipt of the head part of the response. + mutating func startRequestSpan(tracer: TracerType?) + + /// Fails the active overall span given some internal error, e.g. timeout, pool shutdown etc. + /// This is not to be used for failing a span given a failure status coded HTTPResponse. + mutating func failRequestSpan(error: any Error) + + /// Ends the active overall span upon receipt of the response head. + /// + /// If the status code is in error range, this will automatically fail the span. + mutating func endRequestSpan(response: HTTPResponseHead) +} + +extension RequestBag.LoopBoundState: _TracingSupportOperations { } + +#if !TracingSupport +/// Operations used to start/end spans at apropriate times from the Request lifecycle. +extension RequestBag.LoopBoundState { + typealias TracerType = HTTPClientTracingSupportTracerType + + @inlinable + mutating func startRequestSpan(tracer: TracerType?) {} + + @inlinable + mutating func failRequestSpan(error: any Error) {} + + @inlinable + mutating func endRequestSpan(response: HTTPResponseHead) {} +} + +#else // TracingSupport + +extension RequestBag.LoopBoundState { + typealias TracerType = Tracer + + mutating func startRequestSpan(tracer: (any Tracer)?) { + guard let tracer else { + return + } + + assert(self.activeSpan == nil, "Unexpected active span when starting new request span! Was: \(String(describing: self.activeSpan))") + self.activeSpan = tracer.startSpan("\(request.method)") + self.activeSpan?.attributes["loc"] = "\(#fileID):\(#line)" + } + + // TODO: should be able to record the reason for the failure, e.g. timeout, cancellation etc. + mutating func failRequestSpan(error: any Error) { + guard let span = activeSpan else { + return + } + + span.recordError(error) + self.activeSpan = nil + } + + /// The request span currently ends when we receive the response head. + mutating func endRequestSpan(response: HTTPResponseHead) { + guard let span = activeSpan else { + return + } + + span.attributes["http.response.status_code"] = SpanAttribute.int64(Int64(response.status.code)) + if response.status.code >= 400 { + span.setStatus(.init(code: .error)) + } + span.end() + self.activeSpan = nil + } +} + +#endif // TracingSupport diff --git a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift index aaf072b2f..d54e7c774 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift @@ -61,9 +61,11 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { } ) backgroundLogger.logLevel = .trace + var configuration = HTTPClient.Configuration().enableFastFailureModeForTesting() + self.defaultClient = HTTPClient( eventLoopGroupProvider: .shared(self.clientGroup), - configuration: HTTPClient.Configuration().enableFastFailureModeForTesting(), + configuration: configuration, backgroundActivityLogger: backgroundLogger ) } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift new file mode 100644 index 000000000..dc91f413e --- /dev/null +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if TracingSupport + +@_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift +import Atomics +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOEmbedded +import NIOFoundationCompat +import NIOHTTP1 +import NIOHTTPCompression +import NIOPosix +import NIOSSL +import NIOTestUtils +import NIOTransportServices +import XCTest + +#if canImport(Network) +import Network +#endif + +import Tracing +import InMemoryTracing + +fileprivate func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { + var config = HTTPClient.Configuration() + config.httpVersion = .automatic + config.tracing.tracer = tracer + return HTTPClient( + eventLoopGroupProvider: .singleton, + configuration: config + ) +} + +final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { + + var tracer: InMemoryTracer! + var client: HTTPClient! + + override func setUp() { + super.setUp() + self.tracer = InMemoryTracer() + self.client = makeTracedHTTPClient(tracer: tracer) + } + + override func tearDown() { + if let client = self.client { + XCTAssertNoThrow(try client.syncShutdown()) + self.client = nil + } + tracer = nil + } + + func testTrace_get_sync() throws { + let url = self.defaultHTTPBinURLPrefix + "echo-method" + let _ = try client.get(url: url).wait() + + guard tracer.activeSpans.isEmpty else { + XCTFail("Still active spans which were not finished (\(tracer.activeSpans.count))! \(tracer.activeSpans)") + return + } + guard let span = tracer.finishedSpans.first else { + XCTFail("No span was recorded!") + return + } + + XCTAssertEqual(span.operationName, "GET") + } + + func testTrace_post_sync() throws { + let url = self.defaultHTTPBinURLPrefix + "echo-method" + let _ = try client.post(url: url).wait() + + guard let span = tracer.finishedSpans.first else { + XCTFail("No span was recorded!") + return + } + + XCTAssertEqual(span.operationName, "POST") + } + + func testTrace_execute_async() async throws { + let url = self.defaultHTTPBinURLPrefix + "echo-method" + let request = HTTPClientRequest(url: url) + let _ = try await client.execute(request, deadline: .distantFuture) + + guard let span = tracer.finishedSpans.first else { + XCTFail("No span was recorded!") + return + } + + XCTAssertEqual(span.operationName, "GET") + } +} + +#endif \ No newline at end of file From 3ce7dc6df47de8f70bf80cfe4563367751421148 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 13:53:48 +0900 Subject: [PATCH 02/19] swift-format and remove hacky platform setting --- Package.swift | 9 ++++--- Package@swift-6.1.swift | 27 ++++++++++++++----- .../AsyncAwait/HTTPClient+execute.swift | 7 ++--- .../AsyncAwait/Transaction.swift | 4 +-- Sources/AsyncHTTPClient/HTTPClient.swift | 26 +++++++++--------- Sources/AsyncHTTPClient/HTTPHandler.swift | 11 +++++--- Sources/AsyncHTTPClient/RequestBag.swift | 6 ++--- Sources/AsyncHTTPClient/TracingSupport.swift | 21 ++++++++------- .../AsyncHTTPClientTests/HTTPClientBase.swift | 2 +- .../HTTPClientTracingTests.swift | 8 +++--- 10 files changed, 72 insertions(+), 49 deletions(-) diff --git a/Package.swift b/Package.swift index f462d76ee..32a7f93d7 100644 --- a/Package.swift +++ b/Package.swift @@ -35,9 +35,6 @@ let strictConcurrencySettings: [SwiftSetting] = { let package = Package( name: "async-http-client", - platforms: [ // FIXME: must remove this - .macOS("10.15") - ], products: [ .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) ], @@ -82,7 +79,11 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support - .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + .product( + name: "Tracing", + package: "swift-distributed-tracing", + condition: .when(traits: ["TracingSupport"]) + ), ], swiftSettings: strictConcurrencySettings ), diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 9ae34b921..4d62c2748 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -35,9 +35,6 @@ let strictConcurrencySettings: [SwiftSetting] = { let package = Package( name: "async-http-client", - platforms: [ // FIXME: must remove this - .macOS("10.15") - ], products: [ .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) ], @@ -82,8 +79,16 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support - .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), - .product(name: "InMemoryTracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + .product( + name: "Tracing", + package: "swift-distributed-tracing", + condition: .when(traits: ["TracingSupport"]) + ), + .product( + name: "InMemoryTracing", + package: "swift-distributed-tracing", + condition: .when(traits: ["TracingSupport"]) + ), ], swiftSettings: strictConcurrencySettings ), @@ -104,8 +109,16 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support - .product(name: "Tracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), - .product(name: "InMemoryTracing", package: "swift-distributed-tracing", condition: .when(traits: ["TracingSupport"])), + .product( + name: "Tracing", + package: "swift-distributed-tracing", + condition: .when(traits: ["TracingSupport"]) + ), + .product( + name: "InMemoryTracing", + package: "swift-distributed-tracing", + condition: .when(traits: ["TracingSupport"]) + ), ], resources: [ .copy("Resources/self_signed_cert.pem"), diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 6d986d5bc..bf0c07b10 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -15,12 +15,13 @@ import Logging import NIOCore import NIOHTTP1 + +import struct Foundation.URL + #if TracingSupport import Tracing #endif -import struct Foundation.URL - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClient { /// Execute arbitrary HTTP requests. @@ -58,7 +59,7 @@ extension HTTPClient { } } #endif - + return try await doExecute() } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index a0251b857..beb31ce01 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -20,7 +20,7 @@ import NIOSSL #if TracingSupport import Tracing -#endif // TracingSupport +#endif // TracingSupport @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline @@ -60,7 +60,7 @@ final class Transaction: self.span = span self.state = NIOLockedValueBox(StateMachine(responseContinuation)) } - #endif // TracingSupport + #endif // TracingSupport init( request: HTTPClientRequest.Prepared, diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 7f90a6950..b01a32f03 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -81,7 +81,7 @@ public final class HTTPClient: Sendable { public var tracer: (any Tracer)? { configuration.tracing.tracer } - #endif // TracingSupport + #endif // TracingSupport public static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) @@ -684,7 +684,7 @@ public final class HTTPClient: Sendable { deadline: NIODeadline? = nil, logger: Logger? ) -> Task { - return self._execute( + self._execute( request: request, delegate: delegate, eventLoop: eventLoopPreference, @@ -718,7 +718,7 @@ public final class HTTPClient: Sendable { requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed) ) - // #if TracingSupport + // #if TracingSupport // let span: (any Span)? // we may be still executing the same span, e.g. under redirection etc. // if let activeSpan { // span = activeSpan @@ -732,7 +732,7 @@ public final class HTTPClient: Sendable { // span = nil // } // #endif - + let taskEL: EventLoop switch eventLoopPreference.preference { case .indifferent: @@ -1086,19 +1086,19 @@ public final class HTTPClient: Sendable { #if TracingSupport public struct TracingConfiguration: Sendable { - - @usableFromInline - var _tracer: Optional // erasure trick so we don't have to make Configuration @available - /// Tracer that should be used by the HTTPClient. - /// + @usableFromInline + var _tracer: Optional // erasure trick so we don't have to make Configuration @available + + /// Tracer that should be used by the HTTPClient. + /// /// This is selected at configuration creation time, and if no tracer is passed explicitly, /// (including `nil` in order to disable traces), the default global bootstrapped tracer will /// be stored in this property, and used for all subsequent requests made by this client. @inlinable @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - public var tracer: (any Tracer)? { - get { + public var tracer: (any Tracer)? { + get { guard let _tracer else { return nil } @@ -1121,13 +1121,13 @@ public final class HTTPClient: Sendable { /// Span attribute keys that the HTTPClient should set automatically. /// This struct allows the configuration of the attribute names (keys) which will be used for the apropriate values. - public struct AttributeKeys: Sendable { + public struct AttributeKeys: Sendable { public var requestMethod: String = "http.request.method" public var requestBodySize: String = "http.request.body.size" public var responseBodySize: String = "http.response.size" public var responseStatusCode: String = "http.status_code" - + public var httpFlavor: String = "http.flavor" public init() {} diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 6a2eb3dfa..13778b9d4 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -927,9 +927,9 @@ extension HTTPClient { public let eventLoop: EventLoop /// The `Logger` used by the `Task` for logging. public let logger: Logger // We are okay to store the logger here because a Task is for only one request. - + #if TracingSupport - public let tracer: (any Tracer)? // We are okay to store the tracer here because a Task is for only one request. + public let tracer: (any Tracer)? // Ok to store the tracer here because a Task is for only one request. #endif let promise: EventLoopPromise @@ -986,7 +986,12 @@ extension HTTPClient { } #if TracingSupport - init(eventLoop: EventLoop, logger: Logger, tracer: (any Tracer)?, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool) { + init( + eventLoop: EventLoop, + logger: Logger, + tracer: (any Tracer)?, + makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool + ) { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index 4a2032f33..c4af63cf5 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -54,11 +54,11 @@ final class RequestBag: Sendabl var consumeBodyPartStackDepth: Int // if a redirect occurs, we store the task for it so we can propagate cancellation var redirectTask: HTTPClient.Task? = nil - - #if TracingSupport + + #if TracingSupport // The current span, representing the entire request/response made by an execute call. var activeSpan: (any Span)? = nil - #endif // TracingSupport + #endif // TracingSupport } private let loopBoundState: NIOLoopBoundBox diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift index 7e8fbe08c..2c57d80af 100644 --- a/Sources/AsyncHTTPClient/TracingSupport.swift +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -25,14 +25,14 @@ import Tracing #if TracingSupport 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) } } -#endif // TracingSupport +#endif // TracingSupport #if TracingSupport typealias HTTPClientTracingSupportTracerType = any Tracer @@ -52,12 +52,12 @@ protocol _TracingSupportOperations { mutating func failRequestSpan(error: any Error) /// Ends the active overall span upon receipt of the response head. - /// + /// /// If the status code is in error range, this will automatically fail the span. mutating func endRequestSpan(response: HTTPResponseHead) } -extension RequestBag.LoopBoundState: _TracingSupportOperations { } +extension RequestBag.LoopBoundState: _TracingSupportOperations {} #if !TracingSupport /// Operations used to start/end spans at apropriate times from the Request lifecycle. @@ -66,7 +66,7 @@ extension RequestBag.LoopBoundState { @inlinable mutating func startRequestSpan(tracer: TracerType?) {} - + @inlinable mutating func failRequestSpan(error: any Error) {} @@ -74,7 +74,7 @@ extension RequestBag.LoopBoundState { mutating func endRequestSpan(response: HTTPResponseHead) {} } -#else // TracingSupport +#else // TracingSupport extension RequestBag.LoopBoundState { typealias TracerType = Tracer @@ -84,13 +84,16 @@ extension RequestBag.LoopBoundState { return } - assert(self.activeSpan == nil, "Unexpected active span when starting new request span! Was: \(String(describing: self.activeSpan))") + assert( + self.activeSpan == nil, + "Unexpected active span when starting new request span! Was: \(String(describing: self.activeSpan))" + ) self.activeSpan = tracer.startSpan("\(request.method)") self.activeSpan?.attributes["loc"] = "\(#fileID):\(#line)" } // TODO: should be able to record the reason for the failure, e.g. timeout, cancellation etc. - mutating func failRequestSpan(error: any Error) { + mutating func failRequestSpan(error: any Error) { guard let span = activeSpan else { return } @@ -114,4 +117,4 @@ extension RequestBag.LoopBoundState { } } -#endif // TracingSupport +#endif // TracingSupport diff --git a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift index d54e7c774..865fdf02c 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift @@ -62,7 +62,7 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { ) backgroundLogger.logLevel = .trace var configuration = HTTPClient.Configuration().enableFastFailureModeForTesting() - + self.defaultClient = HTTPClient( eventLoopGroupProvider: .shared(self.clientGroup), configuration: configuration, diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index dc91f413e..a233073cb 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -#if TracingSupport +#if TracingSupport @_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift import Atomics @@ -36,7 +36,7 @@ import Network import Tracing import InMemoryTracing -fileprivate func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { +private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { var config = HTTPClient.Configuration() config.httpVersion = .automatic config.tracing.tracer = tracer @@ -80,7 +80,7 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { XCTAssertEqual(span.operationName, "GET") } - + func testTrace_post_sync() throws { let url = self.defaultHTTPBinURLPrefix + "echo-method" let _ = try client.post(url: url).wait() @@ -107,4 +107,4 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { } } -#endif \ No newline at end of file +#endif From 83efc6358b282551d7a7a42b26e63805a8b820b0 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 14:47:04 +0900 Subject: [PATCH 03/19] [Tracing] fix availability issues --- Sources/AsyncHTTPClient/HTTPClient.swift | 49 +++++++++++++++---- Sources/AsyncHTTPClient/HTTPHandler.swift | 17 +++++-- Sources/AsyncHTTPClient/RequestBag.swift | 19 ++++--- Sources/AsyncHTTPClient/TracingSupport.swift | 44 +++++++++++------ .../AsyncHTTPClientTests/HTTPClientBase.swift | 2 +- .../HTTPClientTracingTests.swift | 8 +++ 6 files changed, 106 insertions(+), 33 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index b01a32f03..c53190d16 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -77,7 +77,7 @@ public final class HTTPClient: Sendable { #if TracingSupport @_spi(Tracing) - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { configuration.tracing.tracer } @@ -768,16 +768,22 @@ public final class HTTPClient: Sendable { return nil case .shuttingDown, .shutDown: logger.debug("client is shutting down, failing request") - let error = HTTPClientError.alreadyShutdown - // #if TracingSupport - // span?.recordError(error) - // span?.end() - // #endif + #if TracingSupport + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + return Task.failedTask( + eventLoop: taskEL, + error: HTTPClientError.alreadyShutdown, + logger: logger, + tracer: tracer, + makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool + ) + } + #endif // TracingSupport + return Task.failedTask( eventLoop: taskEL, - error: error, + error: HTTPClientError.alreadyShutdown, logger: logger, - tracer: tracer, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) } @@ -802,12 +808,29 @@ public final class HTTPClient: Sendable { } }() + let task: HTTPClient.Task + #if TracingSupport + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + task = Task( + eventLoop: taskEL, + logger: logger, + tracer: tracer, + makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool + ) + } else { + task = Task( + eventLoop: taskEL, + logger: logger, + makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool + ) + } + #else let task = Task( eventLoop: taskEL, logger: logger, - tracer: tracer, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) + #endif // TracingSupport do { let requestBag = try RequestBag( @@ -1109,8 +1132,16 @@ public final class HTTPClient: Sendable { } } + /// Configuration for tracing attributes set by the HTTPClient. public var attributeKeys: AttributeKeys + + public init() { + self._tracer = nil + self.attributeKeys = .init() + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public init( tracer: (any Tracer)? = InstrumentationSystem.tracer, attributeKeys: AttributeKeys = .init() diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 13778b9d4..87d324a3f 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -929,7 +929,15 @@ extension HTTPClient { public let logger: Logger // We are okay to store the logger here because a Task is for only one request. #if TracingSupport - public let tracer: (any Tracer)? // Ok to store the tracer here because a Task is for only one request. + let anyTracer: Optional // Ok to store the tracer here because a Task is for only one request. + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public var tracer: (any Tracer)? { + get { + print("[swift][\(#fileID):\(#line)] _tracer = \(anyTracer)") + return anyTracer as! (any Tracer)? + } + } #endif let promise: EventLoopPromise @@ -965,7 +973,7 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger - self.tracer = nil + self.anyTracer = nil self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } @@ -986,6 +994,7 @@ extension HTTPClient { } #if TracingSupport + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) init( eventLoop: EventLoop, logger: Logger, @@ -995,11 +1004,13 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger - self.tracer = tracer + print("[swift] set any tracer on TASK = \(tracer)") + self.anyTracer = tracer self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) static func failedTask( eventLoop: EventLoop, error: Error, diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index c4af63cf5..378759228 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -69,12 +69,16 @@ final class RequestBag: Sendabl self.task.logger } - let connectionDeadline: NIODeadline - - var tracer: HTTPClientTracingSupportTracerType? { + var anyTracer: (any Sendable)? { + self.task.anyTracer + } + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + var tracer: (any Tracer)? { self.task.tracer } + let connectionDeadline: NIODeadline + let requestOptions: RequestOptions let requestHead: HTTPRequestHead @@ -97,6 +101,8 @@ final class RequestBag: Sendabl self.eventLoopPreference = eventLoopPreference self.task = task + assert(task.anyTracer != nil, "tracer was nil!") + let loopBoundState = LoopBoundState( request: request, state: StateMachine(redirectHandler: redirectHandler), @@ -128,18 +134,19 @@ final class RequestBag: Sendabl private func willExecuteRequest0(_ executor: HTTPRequestExecutor) { // Immediately start a span for the "whole" request - self.loopBoundState.value.startRequestSpan(tracer: self.tracer) + print("[swift] WILL EXECUTE \(self.anyTracer)") + self.loopBoundState.value.startRequestSpan(tracer: self.anyTracer) let action = self.loopBoundState.value.state.willExecuteRequest(executor) switch action { case .cancelExecuter(let executor): executor.cancelRequest(self) - self.loopBoundState.value.failRequestSpan(error: CancellationError()) + self.loopBoundState.value.failRequestSpanAsCancelled() case .failTaskAndCancelExecutor(let error, let executor): self.delegate.didReceiveError(task: self.task, error) self.task.failInternal(with: error) executor.cancelRequest(self) - self.loopBoundState.value.failRequestSpan(error: CancellationError()) + self.loopBoundState.value.failRequestSpan(error: error) case .none: break } diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift index 2c57d80af..594222387 100644 --- a/Sources/AsyncHTTPClient/TracingSupport.swift +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -34,22 +34,24 @@ struct HTTPHeadersInjector: Injector, @unchecked Sendable { } #endif // TracingSupport -#if TracingSupport -typealias HTTPClientTracingSupportTracerType = any Tracer -#else -enum TracingSupportDisabledTracer {} -typealias HTTPClientTracingSupportTracerType = TracingSupportDisabledTracer -#endif +// #if TracingSupport +// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +// typealias HTTPClientTracingSupportTracerType = any Tracer +// #else +// enum TracingSupportDisabledTracer {} +// typealias HTTPClientTracingSupportTracerType = TracingSupportDisabledTracer +// #endif protocol _TracingSupportOperations { - associatedtype TracerType + // associatedtype TracerType /// Starts the "overall" Span that encompases the beginning of a request until receipt of the head part of the response. - mutating func startRequestSpan(tracer: TracerType?) + mutating func startRequestSpan(tracer: Any?) /// Fails the active overall span given some internal error, e.g. timeout, pool shutdown etc. /// This is not to be used for failing a span given a failure status coded HTTPResponse. mutating func failRequestSpan(error: any Error) + mutating func failRequestSpanAsCancelled() // because CancellationHandler availability... /// Ends the active overall span upon receipt of the response head. /// @@ -65,7 +67,7 @@ extension RequestBag.LoopBoundState { typealias TracerType = HTTPClientTracingSupportTracerType @inlinable - mutating func startRequestSpan(tracer: TracerType?) {} + mutating func startRequestSpan(tracer: Any?) {} @inlinable mutating func failRequestSpan(error: any Error) {} @@ -77,10 +79,14 @@ extension RequestBag.LoopBoundState { #else // TracingSupport extension RequestBag.LoopBoundState { - typealias TracerType = Tracer - - mutating func startRequestSpan(tracer: (any Tracer)?) { - guard let tracer else { + // typealias TracerType = Tracer + + mutating func startRequestSpan(tracer: Any?) { + guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *), + let tracer = tracer as? (any Tracer)?, + let tracer else { + // print("[swift][\(#fileID):\(#line)] MISSING TRACER: \(tracer)") + fatalError("[swift][\(#fileID):\(#line)] MISSING TRACER: \(tracer)") return } @@ -92,13 +98,23 @@ extension RequestBag.LoopBoundState { self.activeSpan?.attributes["loc"] = "\(#fileID):\(#line)" } - // TODO: should be able to record the reason for the failure, e.g. timeout, cancellation etc. + mutating func failRequestSpanAsCancelled() { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + let error = CancellationError() + failRequestSpan(error: error) + } else { + fatalError("Unexpected configuration; expected availability of CancellationError") + } + } + mutating func failRequestSpan(error: any Error) { guard let span = activeSpan else { return } span.recordError(error) + span.end() + self.activeSpan = nil } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift index 865fdf02c..5650bea48 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift @@ -61,7 +61,7 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { } ) backgroundLogger.logLevel = .trace - var configuration = HTTPClient.Configuration().enableFastFailureModeForTesting() + let configuration = HTTPClient.Configuration().enableFastFailureModeForTesting() self.defaultClient = HTTPClient( eventLoopGroupProvider: .shared(self.clientGroup), diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index a233073cb..f3179182a 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -85,6 +85,10 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { let url = self.defaultHTTPBinURLPrefix + "echo-method" let _ = try client.post(url: url).wait() + guard tracer.activeSpans.isEmpty else { + XCTFail("Still active spans which were not finished (\(tracer.activeSpans.count))! \(tracer.activeSpans)") + return + } guard let span = tracer.finishedSpans.first else { XCTFail("No span was recorded!") return @@ -98,6 +102,10 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { let request = HTTPClientRequest(url: url) let _ = try await client.execute(request, deadline: .distantFuture) + guard tracer.activeSpans.isEmpty else { + XCTFail("Still active spans which were not finished (\(tracer.activeSpans.count))! \(tracer.activeSpans)") + return + } guard let span = tracer.finishedSpans.first else { XCTFail("No span was recorded!") return From 5ede6ae1aed1c38b6523f898721cb93057c534ab Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 14:56:32 +0900 Subject: [PATCH 04/19] [Tracing] Further try to simplify availaabilty woes for call-sites --- .../AsyncAwait/Transaction.swift | 11 ---------- Sources/AsyncHTTPClient/HTTPClient.swift | 9 ++++---- Sources/AsyncHTTPClient/HTTPHandler.swift | 9 +++----- Sources/AsyncHTTPClient/RequestBag.swift | 11 +++++++--- Sources/AsyncHTTPClient/TracingSupport.swift | 21 +++++++++---------- 5 files changed, 25 insertions(+), 36 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index beb31ce01..91005b5ce 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -36,10 +36,6 @@ final class Transaction: let preferredEventLoop: EventLoop let requestOptions: RequestOptions - #if TracingSupport - let span: (any Span)? - #endif - private let state: NIOLockedValueBox #if TracingSupport @@ -57,7 +53,6 @@ final class Transaction: self.logger = logger self.connectionDeadline = connectionDeadline self.preferredEventLoop = preferredEventLoop - self.span = span self.state = NIOLockedValueBox(StateMachine(responseContinuation)) } #endif // TracingSupport @@ -70,23 +65,17 @@ final class Transaction: preferredEventLoop: EventLoop, responseContinuation: CheckedContinuation ) { - print("[swift] new transaction = \(request)") self.request = request self.requestOptions = requestOptions self.logger = logger self.connectionDeadline = connectionDeadline self.preferredEventLoop = preferredEventLoop - self.span = nil self.state = NIOLockedValueBox(StateMachine(responseContinuation)) } func cancel() { let error = CancellationError() self.fail(error) - #if TracingSupport - self.span?.recordError(error) - self.span?.end() - #endif } // MARK: Request body helpers diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index c53190d16..de464856e 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -778,7 +778,7 @@ public final class HTTPClient: Sendable { makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) } - #endif // TracingSupport + #endif // TracingSupport return Task.failedTask( eventLoop: taskEL, @@ -808,7 +808,7 @@ public final class HTTPClient: Sendable { } }() - let task: HTTPClient.Task + let task: HTTPClient.Task #if TracingSupport if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { task = Task( @@ -825,12 +825,12 @@ public final class HTTPClient: Sendable { ) } #else - let task = Task( + task = Task( eventLoop: taskEL, logger: logger, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) - #endif // TracingSupport + #endif // TracingSupport do { let requestBag = try RequestBag( @@ -1135,7 +1135,6 @@ public final class HTTPClient: Sendable { /// Configuration for tracing attributes set by the HTTPClient. public var attributeKeys: AttributeKeys - public init() { self._tracer = nil self.attributeKeys = .init() diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 87d324a3f..b1e1c7dc1 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -929,13 +929,12 @@ extension HTTPClient { public let logger: Logger // We are okay to store the logger here because a Task is for only one request. #if TracingSupport - let anyTracer: Optional // Ok to store the tracer here because a Task is for only one request. - + let anyTracer: (any Sendable)? = nil // Ok to store the tracer here because a Task is for only one request. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { get { - print("[swift][\(#fileID):\(#line)] _tracer = \(anyTracer)") - return anyTracer as! (any Tracer)? + anyTracer as! (any Tracer)? } } #endif @@ -973,7 +972,6 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger - self.anyTracer = nil self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } @@ -1004,7 +1002,6 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger - print("[swift] set any tracer on TASK = \(tracer)") self.anyTracer = tracer self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index 378759228..f004a1e70 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -69,13 +69,21 @@ final class RequestBag: Sendabl self.task.logger } + // Available unconditionally, so we can simplify callsites which can just try to pass this value + // regardless if the real tracer exists or not. var anyTracer: (any Sendable)? { + #if TracingSupport self.task.anyTracer + #else + nil + #endif } + #if TracingSupport @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) var tracer: (any Tracer)? { self.task.tracer } + #endif // TracingSupport let connectionDeadline: NIODeadline @@ -101,8 +109,6 @@ final class RequestBag: Sendabl self.eventLoopPreference = eventLoopPreference self.task = task - assert(task.anyTracer != nil, "tracer was nil!") - let loopBoundState = LoopBoundState( request: request, state: StateMachine(redirectHandler: redirectHandler), @@ -134,7 +140,6 @@ final class RequestBag: Sendabl private func willExecuteRequest0(_ executor: HTTPRequestExecutor) { // Immediately start a span for the "whole" request - print("[swift] WILL EXECUTE \(self.anyTracer)") self.loopBoundState.value.startRequestSpan(tracer: self.anyTracer) let action = self.loopBoundState.value.state.willExecuteRequest(executor) diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift index 594222387..05539b219 100644 --- a/Sources/AsyncHTTPClient/TracingSupport.swift +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -46,12 +46,12 @@ protocol _TracingSupportOperations { // associatedtype TracerType /// Starts the "overall" Span that encompases the beginning of a request until receipt of the head part of the response. - mutating func startRequestSpan(tracer: Any?) + mutating func startRequestSpan(tracer: T?) /// Fails the active overall span given some internal error, e.g. timeout, pool shutdown etc. /// This is not to be used for failing a span given a failure status coded HTTPResponse. mutating func failRequestSpan(error: any Error) - mutating func failRequestSpanAsCancelled() // because CancellationHandler availability... + mutating func failRequestSpanAsCancelled() // because CancellationHandler availability... /// Ends the active overall span upon receipt of the response head. /// @@ -64,14 +64,15 @@ extension RequestBag.LoopBoundState: _TracingSupportOperations {} #if !TracingSupport /// Operations used to start/end spans at apropriate times from the Request lifecycle. extension RequestBag.LoopBoundState { - typealias TracerType = HTTPClientTracingSupportTracerType - @inlinable - mutating func startRequestSpan(tracer: Any?) {} + mutating func startRequestSpan(tracer: T?) {} @inlinable mutating func failRequestSpan(error: any Error) {} + @inlinable + mutating func failRequestSpanAsCancelled() {} + @inlinable mutating func endRequestSpan(response: HTTPResponseHead) {} } @@ -81,12 +82,10 @@ extension RequestBag.LoopBoundState { extension RequestBag.LoopBoundState { // typealias TracerType = Tracer - mutating func startRequestSpan(tracer: Any?) { - guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *), - let tracer = tracer as? (any Tracer)?, - let tracer else { - // print("[swift][\(#fileID):\(#line)] MISSING TRACER: \(tracer)") - fatalError("[swift][\(#fileID):\(#line)] MISSING TRACER: \(tracer)") + mutating func startRequestSpan(tracer: T?) { + guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *), + let tracer = tracer as! (any Tracer)? + else { return } From f7835c347af9a6016e6e51aaeecf7e70756692bc Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 14:58:19 +0900 Subject: [PATCH 05/19] fix licenseignore to handle Package@swift-6.1.swift --- .licenseignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.licenseignore b/.licenseignore index edceaab62..151ce9245 100644 --- a/.licenseignore +++ b/.licenseignore @@ -16,6 +16,8 @@ *.json Package.swift **/Package.swift +Package@swift-*.swift +**/Package@swift-*.swift Package@-*.swift **/Package@-*.swift Package.resolved From b74631534dcded95206a063ce006bd101ac57587 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 15:05:17 +0900 Subject: [PATCH 06/19] [CI] attempt to add CI action to test with disabled package traits --- .github/workflows/main.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ba8b35006..177187db7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,21 @@ jobs: linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + unit-tests-no-package-traits: + name: 'Unit tests (disable default package traits)' + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors --disable-default-traits" + linux_5_9_enabled: false + linux_nightly_6_0_enabled: false + linux_nightly_6_1_enabled: false + linux_nightly_next_enabled: false + linux_nightly_main_enabled: false + windows_6_0_enabled: false + windows_6_1_enabled: false + windows_nightly_6_0_enabled: false + windows_nightly_6_1_enabled: false + static-sdk: name: Static SDK # Workaround https://github.com/nektos/act/issues/1875 From 939a544cf584ce7c90364ffe21b12e1f698dbe71 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 15:07:00 +0900 Subject: [PATCH 07/19] Revert "[CI] attempt to add CI action to test with disabled package traits" This reverts commit 886db6f8922d5d56574d048da722ceaced926da2. --- .github/workflows/main.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 177187db7..ba8b35006 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,21 +17,6 @@ jobs: linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" - unit-tests-no-package-traits: - name: 'Unit tests (disable default package traits)' - uses: apple/swift-nio/.github/workflows/unit_tests.yml@main - with: - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors --disable-default-traits" - linux_5_9_enabled: false - linux_nightly_6_0_enabled: false - linux_nightly_6_1_enabled: false - linux_nightly_next_enabled: false - linux_nightly_main_enabled: false - windows_6_0_enabled: false - windows_6_1_enabled: false - windows_nightly_6_0_enabled: false - windows_nightly_6_1_enabled: false - static-sdk: name: Static SDK # Workaround https://github.com/nektos/act/issues/1875 From 18252c7c58c256ca6633ad7770fb9c463108a526 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 15:08:50 +0900 Subject: [PATCH 08/19] fixup: Package.swift should be 5.10 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 32a7f93d7..8a2539a1b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.1 +// swift-tools-version:5.10 //===----------------------------------------------------------------------===// // // This source file is part of the AsyncHTTPClient open source project From 19ac881ddd550703e3d242e4c7f65fc948f4c5ea Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 15:12:38 +0900 Subject: [PATCH 09/19] minor compile fixup --- Sources/AsyncHTTPClient/HTTPHandler.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index b1e1c7dc1..5da331dc2 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -929,7 +929,7 @@ extension HTTPClient { public let logger: Logger // We are okay to store the logger here because a Task is for only one request. #if TracingSupport - let anyTracer: (any Sendable)? = nil // Ok to store the tracer here because a Task is for only one request. + let anyTracer: (any Sendable)? // Ok to store the tracer here because a Task is for only one request. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { @@ -972,6 +972,9 @@ extension HTTPClient { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger + #if TracingSupport + self.anyTracer = nil + #endif self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } From c191e11dd40dcbe68e16b049b31934f8bc651f2c Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 15:15:47 +0900 Subject: [PATCH 10/19] build: fix the 6.0 build, there's no package traits below 6.1 --- Package.swift | 14 +++++--------- Sources/AsyncHTTPClient/HTTPClient.swift | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Package.swift b/Package.swift index 8a2539a1b..09116b0b4 100644 --- a/Package.swift +++ b/Package.swift @@ -38,10 +38,6 @@ let package = Package( products: [ .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) ], - traits: [ - .trait(name: "TracingSupport"), - .default(enabledTraits: ["TracingSupport"]), - ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), @@ -79,11 +75,8 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support - .product( - name: "Tracing", - package: "swift-distributed-tracing", - condition: .when(traits: ["TracingSupport"]) - ), + .product(name: "Tracing", package: "swift-distributed-tracing"), + .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), ], swiftSettings: strictConcurrencySettings ), @@ -103,6 +96,9 @@ let package = Package( .product(name: "Logging", package: "swift-log"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), + // Observability support + .product(name: "Tracing", package: "swift-distributed-tracing"), + .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), ], resources: [ .copy("Resources/self_signed_cert.pem"), diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index de464856e..8749fea31 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -711,7 +711,7 @@ public final class HTTPClient: Sendable { eventLoop eventLoopPreference: EventLoopPreference, deadline: NIODeadline? = nil, logger originalLogger: Logger?, - redirectState: RedirectState?, + redirectState: RedirectState? ) -> Task { let logger = (originalLogger ?? HTTPClient.loggingDisabled).attachingRequestInformation( request, From 072574a6b2635a43a6298628fdd53f783480b9ec Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 12 Sep 2025 18:48:15 +0900 Subject: [PATCH 11/19] depend on distributor tracing 1.3.0 Co-authored-by: Moritz Lang <16192401+slashmo@users.noreply.github.com> --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 09116b0b4..78d319f99 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), .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"), - .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"), ], targets: [ .target( From 3bbfd5676763858a843bdcbb8919ff08dccb2508 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Fri, 12 Sep 2025 22:07:37 +0900 Subject: [PATCH 12/19] Cleanup some leftovers and address some review comments --- .../AsyncAwait/HTTPClient+execute.swift | 14 ++++++++---- .../AsyncAwait/Transaction.swift | 3 +-- Sources/AsyncHTTPClient/HTTPClient.swift | 22 ++++--------------- .../NIOTransportServices/NWErrorHandler.swift | 4 ---- Sources/AsyncHTTPClient/TracingSupport.swift | 11 +--------- .../AsyncHTTPClientTests/HTTPClientBase.swift | 3 +-- 6 files changed, 17 insertions(+), 40 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index bf0c07b10..7ae6f51e4 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -40,7 +40,7 @@ extension HTTPClient { deadline: NIODeadline, logger: Logger? = nil ) async throws -> HTTPClientResponse { - func doExecute() async throws -> HTTPClientResponse { + try await withRequestSpan(request) { try await self.executeAndFollowRedirectsIfNeeded( request, deadline: deadline, @@ -48,19 +48,25 @@ extension HTTPClient { redirectState: RedirectState(self.configuration.redirectConfiguration.mode, initialURL: request.url) ) } + } + @inlinable + func withRequestSpan( + _ request: HTTPClientRequest, + _ body: () async throws -> ReturnType + ) async rethrows -> ReturnType { #if TracingSupport if let tracer = self.tracer { - return try await tracer.withSpan("\(request.method)") { span -> (HTTPClientResponse) in + return try await tracer.withSpan("\(request.method)") { span in let attr = self.configuration.tracing.attributeKeys span.attributes[attr.requestMethod] = request.method.rawValue // Set more attributes on the span - return try await doExecute() + return try await body() } } #endif - return try await doExecute() + return try await body() } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 91005b5ce..2fc3bcf4b 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -74,8 +74,7 @@ final class Transaction: } func cancel() { - let error = CancellationError() - self.fail(error) + self.fail(CancellationError()) } // MARK: Request body helpers diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 8749fea31..3dccb414e 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -66,9 +66,11 @@ public final class HTTPClient: Sendable { /// /// All HTTP transactions will occur on loops owned by this group. public let eventLoopGroup: EventLoopGroup - let configuration: Configuration let poolManager: HTTPConnectionPool.Manager + @usableFromInline + let configuration: Configuration + /// Shared thread pool used for file IO. It is lazily created on first access of ``Task/fileIOThreadPool``. private let fileIOThreadPool: NIOLockedValueBox @@ -76,14 +78,13 @@ public final class HTTPClient: Sendable { private let canBeShutDown: Bool #if TracingSupport - @_spi(Tracing) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { configuration.tracing.tracer } #endif // TracingSupport - public static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) + static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) /// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration. /// @@ -718,21 +719,6 @@ public final class HTTPClient: Sendable { requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed) ) - // #if TracingSupport - // let span: (any Span)? // we may be still executing the same span, e.g. under redirection etc. - // if let activeSpan { - // span = activeSpan - // } else if let tracer = self.tracer { - // let s = tracer.startSpan(request.method.rawValue) - // let attrs = self.configuration.tracing.attributeKeys - // s.attributes[attrs.requestMethod] = request.method.rawValue - // s.attributes["loc"] = "\(#fileID):\(#line)" - // span = s - // } else { - // span = nil - // } - // #endif - let taskEL: EventLoop switch eventLoopPreference.preference { case .indifferent: diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift index 79d0b5ed7..148b4a4c4 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/NWErrorHandler.swift @@ -25,11 +25,7 @@ extension HTTPClient { /// A wrapper for `POSIX` errors thrown by `Network.framework`. public struct NWPOSIXError: Error, CustomStringConvertible { /// POSIX error code (enum) - #if compiler(>=6.1) - nonisolated(unsafe) public let errorCode: POSIXErrorCode - #else public let errorCode: POSIXErrorCode - #endif /// actual reason, in human readable form private let reason: String diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift index 05539b219..d264e6938 100644 --- a/Sources/AsyncHTTPClient/TracingSupport.swift +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -34,14 +34,6 @@ struct HTTPHeadersInjector: Injector, @unchecked Sendable { } #endif // TracingSupport -// #if TracingSupport -// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -// typealias HTTPClientTracingSupportTracerType = any Tracer -// #else -// enum TracingSupportDisabledTracer {} -// typealias HTTPClientTracingSupportTracerType = TracingSupportDisabledTracer -// #endif - protocol _TracingSupportOperations { // associatedtype TracerType @@ -99,8 +91,7 @@ extension RequestBag.LoopBoundState { mutating func failRequestSpanAsCancelled() { if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - let error = CancellationError() - failRequestSpan(error: error) + failRequestSpan(error: CancellationError()) } else { fatalError("Unexpected configuration; expected availability of CancellationError") } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift index 5650bea48..15620dd24 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientBase.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientBase.swift @@ -61,11 +61,10 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase { } ) backgroundLogger.logLevel = .trace - let configuration = HTTPClient.Configuration().enableFastFailureModeForTesting() self.defaultClient = HTTPClient( eventLoopGroupProvider: .shared(self.clientGroup), - configuration: configuration, + configuration: HTTPClient.Configuration().enableFastFailureModeForTesting(), backgroundActivityLogger: backgroundLogger ) } From 91ea66eabd96d9d6699098fea0cf6f68c8fbcef9 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 17 Sep 2025 18:00:14 +0900 Subject: [PATCH 13/19] Remove package trait, and cleanup sources; Handle failed HTTP responses --- Package.swift | 4 +- Package@swift-6.1.swift | 148 ------------------ .../AsyncAwait/HTTPClient+execute.swift | 5 +- .../AsyncAwait/HTTPClient+tracing.swift | 45 ++++++ .../AsyncAwait/Transaction.swift | 5 - Sources/AsyncHTTPClient/HTTPClient.swift | 56 ++----- Sources/AsyncHTTPClient/HTTPHandler.swift | 50 +----- .../AsyncHTTPClient/RequestBag+Tracing.swift | 73 +++++++++ Sources/AsyncHTTPClient/RequestBag.swift | 18 +-- Sources/AsyncHTTPClient/TracingSupport.swift | 115 +++----------- .../HTTPClientTracingTests.swift | 46 +++++- .../RequestBagTests.swift | 2 +- 12 files changed, 206 insertions(+), 361 deletions(-) delete mode 100644 Package@swift-6.1.swift create mode 100644 Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift create mode 100644 Sources/AsyncHTTPClient/RequestBag+Tracing.swift diff --git a/Package.swift b/Package.swift index 78d319f99..d88e51a76 100644 --- a/Package.swift +++ b/Package.swift @@ -71,10 +71,10 @@ let package = Package( .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), .product(name: "NIOSOCKS", package: "swift-nio-extras"), .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), - .product(name: "Logging", package: "swift-log"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support + .product(name: "Logging", package: "swift-log"), .product(name: "Tracing", package: "swift-distributed-tracing"), .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), ], @@ -93,10 +93,10 @@ let package = Package( .product(name: "NIOSSL", package: "swift-nio-ssl"), .product(name: "NIOHTTP2", package: "swift-nio-http2"), .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "Logging", package: "swift-log"), .product(name: "Atomics", package: "swift-atomics"), .product(name: "Algorithms", package: "swift-algorithms"), // Observability support + .product(name: "Logging", package: "swift-log"), .product(name: "Tracing", package: "swift-distributed-tracing"), .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), ], diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift deleted file mode 100644 index 4d62c2748..000000000 --- a/Package@swift-6.1.swift +++ /dev/null @@ -1,148 +0,0 @@ -// swift-tools-version:6.1 -//===----------------------------------------------------------------------===// -// -// This source file is part of the AsyncHTTPClient open source project -// -// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import PackageDescription - -let strictConcurrencyDevelopment = false - -let strictConcurrencySettings: [SwiftSetting] = { - var initialSettings: [SwiftSetting] = [] - initialSettings.append(contentsOf: [ - .enableUpcomingFeature("StrictConcurrency"), - .enableUpcomingFeature("InferSendableFromCaptures"), - ]) - - if strictConcurrencyDevelopment { - // -warnings-as-errors here is a workaround so that IDE-based development can - // get tripped up on -require-explicit-sendable. - initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"])) - } - - return initialSettings -}() - -let package = Package( - name: "async-http-client", - products: [ - .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) - ], - traits: [ - .trait(name: "TracingSupport"), - .default(enabledTraits: ["TracingSupport"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), - .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), - .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"), - .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-atomics.git", from: "1.0.2"), - .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"), - ], - targets: [ - .target( - name: "CAsyncHTTPClient", - cSettings: [ - .define("_GNU_SOURCE") - ] - ), - .target( - name: "AsyncHTTPClient", - dependencies: [ - .target(name: "CAsyncHTTPClient"), - .product(name: "NIO", package: "swift-nio"), - .product(name: "NIOTLS", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), - .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), - .product(name: "Logging", package: "swift-log"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms"), - // Observability support - .product( - name: "Tracing", - package: "swift-distributed-tracing", - condition: .when(traits: ["TracingSupport"]) - ), - .product( - name: "InMemoryTracing", - package: "swift-distributed-tracing", - condition: .when(traits: ["TracingSupport"]) - ), - ], - swiftSettings: strictConcurrencySettings - ), - .testTarget( - name: "AsyncHTTPClientTests", - dependencies: [ - .target(name: "AsyncHTTPClient"), - .product(name: "NIOTLS", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOEmbedded", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - .product(name: "NIOTestUtils", package: "swift-nio"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "Logging", package: "swift-log"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms"), - // Observability support - .product( - name: "Tracing", - package: "swift-distributed-tracing", - condition: .when(traits: ["TracingSupport"]) - ), - .product( - name: "InMemoryTracing", - package: "swift-distributed-tracing", - condition: .when(traits: ["TracingSupport"]) - ), - ], - resources: [ - .copy("Resources/self_signed_cert.pem"), - .copy("Resources/self_signed_key.pem"), - .copy("Resources/example.com.cert.pem"), - .copy("Resources/example.com.private-key.pem"), - ], - swiftSettings: strictConcurrencySettings - ), - ] -) - -// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // -for target in package.targets { - switch target.type { - case .regular, .test, .executable: - var settings = target.swiftSettings ?? [] - // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md - settings.append(.enableUpcomingFeature("MemberImportVisibility")) - target.swiftSettings = settings - case .macro, .plugin, .system, .binary: - () // not applicable - @unknown default: - () // we don't know what to do here, do nothing - } -} -// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 7ae6f51e4..2ef77dca4 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -15,13 +15,10 @@ import Logging import NIOCore import NIOHTTP1 +import Tracing import struct Foundation.URL -#if TracingSupport -import Tracing -#endif - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClient { /// Execute arbitrary HTTP requests. diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift new file mode 100644 index 000000000..5abc24f61 --- /dev/null +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOCore +import NIOHTTP1 +import Tracing + +import struct Foundation.URL + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension HTTPClient { + @inlinable + func withRequestSpan( + _ request: HTTPClientRequest, + _ body: () async throws -> HTTPClientResponse + ) async rethrows -> HTTPClientResponse { + guard let tracer = self.tracer else { + return try await body() + } + + return try await tracer.withSpan(request.method.rawValue, ofKind: .client) { span in + let keys = self.configuration.tracing.attributeKeys + span.attributes[keys.requestMethod] = request.method.rawValue + // TODO: set more attributes on the span + let response = try await body() + + // set response span attributes + TracingSupport.handleResponseStatusCode(span, response.status, keys: tracing.attributeKeys) + + return response + } + } +} diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 2fc3bcf4b..1a2168571 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -17,10 +17,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL - -#if TracingSupport import Tracing -#endif // TracingSupport @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline @@ -38,7 +35,6 @@ final class Transaction: private let state: NIOLockedValueBox - #if TracingSupport init( request: HTTPClientRequest.Prepared, requestOptions: RequestOptions, @@ -55,7 +51,6 @@ final class Transaction: self.preferredEventLoop = preferredEventLoop self.state = NIOLockedValueBox(StateMachine(responseContinuation)) } - #endif // TracingSupport init( request: HTTPClientRequest.Prepared, diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 3dccb414e..f3cd91f3f 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -23,10 +23,7 @@ import NIOPosix import NIOSSL import NIOTLS import NIOTransportServices - -#if TracingSupport import Tracing -#endif extension Logger { private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value { @@ -77,12 +74,17 @@ public final class HTTPClient: Sendable { private let state: NIOLockedValueBox private let canBeShutDown: Bool - #if TracingSupport + /// Tracer configured for this HTTPClient at configuration time. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { configuration.tracing.tracer } - #endif // TracingSupport + + /// Access to tracing configuration in order to get configured attribute keys etc. + @usableFromInline + package var tracing: TracingConfiguration { + self.configuration.tracing + } static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) @@ -748,28 +750,17 @@ public final class HTTPClient: Sendable { ] ) - let failedTask: Task? = self.state.withLockedValue { state in + let failedTask: Task? = self.state.withLockedValue { state -> (Task?) in switch state { case .upAndRunning: return nil case .shuttingDown, .shutDown: logger.debug("client is shutting down, failing request") - #if TracingSupport - if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { - return Task.failedTask( - eventLoop: taskEL, - error: HTTPClientError.alreadyShutdown, - logger: logger, - tracer: tracer, - makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool - ) - } - #endif // TracingSupport - return Task.failedTask( eventLoop: taskEL, error: HTTPClientError.alreadyShutdown, logger: logger, + tracing: tracing, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) } @@ -794,29 +785,13 @@ public final class HTTPClient: Sendable { } }() - let task: HTTPClient.Task - #if TracingSupport - if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { - task = Task( - eventLoop: taskEL, - logger: logger, - tracer: tracer, - makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool - ) - } else { - task = Task( + let task: HTTPClient.Task = + Task( eventLoop: taskEL, logger: logger, + tracing: self.tracing, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool ) - } - #else - task = Task( - eventLoop: taskEL, - logger: logger, - makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool - ) - #endif // TracingSupport do { let requestBag = try RequestBag( @@ -929,9 +904,8 @@ public final class HTTPClient: Sendable { /// A method with access to the HTTP/2 stream channel that is called when creating the stream. public var http2StreamChannelDebugInitializer: (@Sendable (Channel) -> EventLoopFuture)? - #if TracingSupport + /// Configuration how distributed traces are created and handled. public var tracing: TracingConfiguration = .init() - #endif public init( tlsConfiguration: TLSConfiguration? = nil, @@ -1062,7 +1036,6 @@ public final class HTTPClient: Sendable { self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer } - #if TracingSupport public init( tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, @@ -1090,10 +1063,8 @@ public final class HTTPClient: Sendable { self.http2StreamChannelDebugInitializer = http2StreamChannelDebugInitializer self.tracing = tracing } - #endif } - #if TracingSupport public struct TracingConfiguration: Sendable { @usableFromInline @@ -1149,7 +1120,6 @@ public final class HTTPClient: Sendable { public init() {} } } - #endif /// Specifies how `EventLoopGroup` will be created and establishes lifecycle ownership. public enum EventLoopGroupProvider { diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 5da331dc2..f9b337565 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -19,10 +19,7 @@ import NIOCore import NIOHTTP1 import NIOPosix import NIOSSL - -#if TracingSupport import Tracing -#endif #if compiler(>=6.0) import Foundation @@ -928,16 +925,11 @@ extension HTTPClient { /// The `Logger` used by the `Task` for logging. public let logger: Logger // We are okay to store the logger here because a Task is for only one request. - #if TracingSupport - let anyTracer: (any Sendable)? // Ok to store the tracer here because a Task is for only one request. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public var tracer: (any Tracer)? { - get { - anyTracer as! (any Tracer)? - } + tracing.tracer } - #endif + let tracing: TracingConfiguration let promise: EventLoopPromise @@ -968,66 +960,36 @@ extension HTTPClient { self.makeOrGetFileIOThreadPool() } - init(eventLoop: EventLoop, logger: Logger, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool) { - self.eventLoop = eventLoop - self.promise = eventLoop.makePromise() - self.logger = logger - #if TracingSupport - self.anyTracer = nil - #endif - self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool - self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) - } - - static func failedTask( - eventLoop: EventLoop, - error: Error, - logger: Logger, - makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool - ) -> Task { - let task = self.init( - eventLoop: eventLoop, - logger: logger, - makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool - ) - task.promise.fail(error) - return task - } - - #if TracingSupport - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) init( eventLoop: EventLoop, logger: Logger, - tracer: (any Tracer)?, + tracing: TracingConfiguration, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool ) { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger - self.anyTracer = tracer + self.tracing = tracing self.makeOrGetFileIOThreadPool = makeOrGetFileIOThreadPool self.state = NIOLockedValueBox(State(isCancelled: false, taskDelegate: nil)) } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) static func failedTask( eventLoop: EventLoop, error: Error, logger: Logger, - tracer: (any Tracer)?, + tracing: TracingConfiguration, makeOrGetFileIOThreadPool: @escaping @Sendable () -> NIOThreadPool ) -> Task { let task = self.init( eventLoop: eventLoop, logger: logger, - tracer: tracer, + tracing: tracing, makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool ) task.promise.fail(error) return task } - #endif /// `EventLoopFuture` for the response returned by this request. public var futureResult: EventLoopFuture { diff --git a/Sources/AsyncHTTPClient/RequestBag+Tracing.swift b/Sources/AsyncHTTPClient/RequestBag+Tracing.swift new file mode 100644 index 000000000..729b6256a --- /dev/null +++ b/Sources/AsyncHTTPClient/RequestBag+Tracing.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOHTTP1 +import NIOSSL +import Tracing + +extension RequestBag.LoopBoundState { + + /// Starts the "overall" Span that encompases the beginning of a request until receipt of the head part of the response. + mutating func startRequestSpan(tracer: T?) { + guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *), + let tracer = tracer as! (any Tracer)? + else { + return + } + + assert( + self.activeSpan == nil, + "Unexpected active span when starting new request span! Was: \(String(describing: self.activeSpan))" + ) + self.activeSpan = tracer.startSpan("\(request.method)", ofKind: .client) + } + + /// Fails the active overall span given some internal error, e.g. timeout, pool shutdown etc. + /// This is not to be used for failing a span given a failure status coded HTTPResponse. + mutating func failRequestSpanAsCancelled() { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + failRequestSpan(error: CancellationError()) + } else { + failRequestSpan(error: HTTPRequestCancellationError()) + } + } + + mutating func failRequestSpan(error: any Error) { + guard let span = activeSpan else { + return + } + + span.recordError(error) + span.end() + + self.activeSpan = nil + } + + /// Ends the active overall span upon receipt of the response head. + /// + /// If the status code is in error range, this will automatically fail the span. + mutating func endRequestSpan(response: HTTPResponseHead) { + guard let span = activeSpan else { + return + } + + TracingSupport.handleResponseStatusCode(span, response.status, keys: tracing.attributeKeys) + + span.end() + self.activeSpan = nil + } +} diff --git a/Sources/AsyncHTTPClient/RequestBag.swift b/Sources/AsyncHTTPClient/RequestBag.swift index f004a1e70..ff3ed8442 100644 --- a/Sources/AsyncHTTPClient/RequestBag.swift +++ b/Sources/AsyncHTTPClient/RequestBag.swift @@ -17,10 +17,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL - -#if TracingSupport import Tracing -#endif @preconcurrency final class RequestBag: Sendable { @@ -55,10 +52,10 @@ final class RequestBag: Sendabl // if a redirect occurs, we store the task for it so we can propagate cancellation var redirectTask: HTTPClient.Task? = nil - #if TracingSupport + // - Distributed tracing + var tracing: HTTPClient.TracingConfiguration // The current span, representing the entire request/response made by an execute call. var activeSpan: (any Span)? = nil - #endif // TracingSupport } private let loopBoundState: NIOLoopBoundBox @@ -72,18 +69,12 @@ final class RequestBag: Sendabl // Available unconditionally, so we can simplify callsites which can just try to pass this value // regardless if the real tracer exists or not. var anyTracer: (any Sendable)? { - #if TracingSupport - self.task.anyTracer - #else - nil - #endif + self.task.tracing._tracer } - #if TracingSupport @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) var tracer: (any Tracer)? { self.task.tracer } - #endif // TracingSupport let connectionDeadline: NIODeadline @@ -112,7 +103,8 @@ final class RequestBag: Sendabl let loopBoundState = LoopBoundState( request: request, state: StateMachine(redirectHandler: redirectHandler), - consumeBodyPartStackDepth: 0 + consumeBodyPartStackDepth: 0, + tracing: task.tracing ) self.loopBoundState = NIOLoopBoundBox.makeBoxSendingValue(loopBoundState, eventLoop: task.eventLoop) self.connectionDeadline = connectionDeadline diff --git a/Sources/AsyncHTTPClient/TracingSupport.swift b/Sources/AsyncHTTPClient/TracingSupport.swift index d264e6938..feb564ffb 100644 --- a/Sources/AsyncHTTPClient/TracingSupport.swift +++ b/Sources/AsyncHTTPClient/TracingSupport.swift @@ -17,110 +17,37 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL - -#if TracingSupport import Tracing -#endif - -#if TracingSupport -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) - } -} -#endif // TracingSupport -protocol _TracingSupportOperations { - // associatedtype TracerType - - /// Starts the "overall" Span that encompases the beginning of a request until receipt of the head part of the response. - mutating func startRequestSpan(tracer: T?) - - /// Fails the active overall span given some internal error, e.g. timeout, pool shutdown etc. - /// This is not to be used for failing a span given a failure status coded HTTPResponse. - mutating func failRequestSpan(error: any Error) - mutating func failRequestSpanAsCancelled() // because CancellationHandler availability... - - /// Ends the active overall span upon receipt of the response head. - /// - /// If the status code is in error range, this will automatically fail the span. - mutating func endRequestSpan(response: HTTPResponseHead) -} - -extension RequestBag.LoopBoundState: _TracingSupportOperations {} - -#if !TracingSupport -/// Operations used to start/end spans at apropriate times from the Request lifecycle. -extension RequestBag.LoopBoundState { - @inlinable - mutating func startRequestSpan(tracer: T?) {} - - @inlinable - mutating func failRequestSpan(error: any Error) {} - - @inlinable - mutating func failRequestSpanAsCancelled() {} +// MARK: - Centralized span attribute handling +@usableFromInline +struct TracingSupport { @inlinable - mutating func endRequestSpan(response: HTTPResponseHead) {} -} - -#else // TracingSupport - -extension RequestBag.LoopBoundState { - // typealias TracerType = Tracer - - mutating func startRequestSpan(tracer: T?) { - guard #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *), - let tracer = tracer as! (any Tracer)? - else { - return - } - - assert( - self.activeSpan == nil, - "Unexpected active span when starting new request span! Was: \(String(describing: self.activeSpan))" - ) - self.activeSpan = tracer.startSpan("\(request.method)") - self.activeSpan?.attributes["loc"] = "\(#fileID):\(#line)" - } - - mutating func failRequestSpanAsCancelled() { - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - failRequestSpan(error: CancellationError()) - } else { - fatalError("Unexpected configuration; expected availability of CancellationError") + static func handleResponseStatusCode( + _ span: Span, + _ status: HTTPResponseStatus, + keys: HTTPClient.TracingConfiguration.AttributeKeys + ) { + if status.code >= 400 { + span.setStatus(.init(code: .error)) } + span.attributes[keys.responseStatusCode] = SpanAttribute.int64(Int64(status.code)) } +} - mutating func failRequestSpan(error: any Error) { - guard let span = activeSpan else { - return - } - - span.recordError(error) - span.end() +// MARK: - HTTPHeadersInjector - self.activeSpan = nil - } +struct HTTPHeadersInjector: Injector, @unchecked Sendable { + static let shared: HTTPHeadersInjector = HTTPHeadersInjector() - /// The request span currently ends when we receive the response head. - mutating func endRequestSpan(response: HTTPResponseHead) { - guard let span = activeSpan else { - return - } + private init() {} - span.attributes["http.response.status_code"] = SpanAttribute.int64(Int64(response.status.code)) - if response.status.code >= 400 { - span.setStatus(.init(code: .error)) - } - span.end() - self.activeSpan = nil + func inject(_ value: String, forKey name: String, into headers: inout HTTPHeaders) { + headers.add(name: name, value: value) } } -#endif // TracingSupport +// MARK: - Errors + +internal struct HTTPRequestCancellationError: Error {} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index f3179182a..6d64d5269 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -12,10 +12,9 @@ // //===----------------------------------------------------------------------===// -#if TracingSupport - @_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift import Atomics +import InMemoryTracing import Logging import NIOConcurrencyHelpers import NIOCore @@ -27,15 +26,13 @@ import NIOPosix import NIOSSL import NIOTestUtils import NIOTransportServices +import Tracing import XCTest #if canImport(Network) import Network #endif -import Tracing -import InMemoryTracing - private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { var config = HTTPClient.Configuration() config.httpVersion = .automatic @@ -97,6 +94,24 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { XCTAssertEqual(span.operationName, "POST") } + func testTrace_post_sync_404_error() throws { + let url = self.defaultHTTPBinURLPrefix + "404-not-existent" + let _ = try client.post(url: url).wait() + + guard tracer.activeSpans.isEmpty else { + XCTFail("Still active spans which were not finished (\(tracer.activeSpans.count))! \(tracer.activeSpans)") + return + } + guard let span = tracer.finishedSpans.first else { + XCTFail("No span was recorded!") + return + } + + XCTAssertEqual(span.operationName, "POST") + XCTAssertTrue(span.errors.isEmpty, "Should have recorded error") + XCTAssertEqual(span.attributes.get(client.tracing.attributeKeys.responseStatusCode), 404) + } + func testTrace_execute_async() async throws { let url = self.defaultHTTPBinURLPrefix + "echo-method" let request = HTTPClientRequest(url: url) @@ -113,6 +128,23 @@ final class HTTPClientTracingTests: XCTestCaseHTTPClientTestsBaseClass { XCTAssertEqual(span.operationName, "GET") } -} -#endif + func testTrace_execute_async_404_error() async throws { + let url = self.defaultHTTPBinURLPrefix + "404-does-not-exist" + let request = HTTPClientRequest(url: url) + let _ = try await client.execute(request, deadline: .distantFuture) + + guard tracer.activeSpans.isEmpty else { + XCTFail("Still active spans which were not finished (\(tracer.activeSpans.count))! \(tracer.activeSpans)") + return + } + guard let span = tracer.finishedSpans.first else { + XCTFail("No span was recorded!") + return + } + + XCTAssertEqual(span.operationName, "GET") + XCTAssertTrue(span.errors.isEmpty, "Should have recorded error") + XCTAssertEqual(span.attributes.get(client.tracing.attributeKeys.responseStatusCode), 404) + } +} diff --git a/Tests/AsyncHTTPClientTests/RequestBagTests.swift b/Tests/AsyncHTTPClientTests/RequestBagTests.swift index 2b0c2f6e4..f1600fceb 100644 --- a/Tests/AsyncHTTPClientTests/RequestBagTests.swift +++ b/Tests/AsyncHTTPClientTests/RequestBagTests.swift @@ -994,7 +994,7 @@ extension HTTPClient.Task { eventLoop: EventLoop, logger: Logger ) { - self.init(eventLoop: eventLoop, logger: logger) { + self.init(eventLoop: eventLoop, logger: logger, tracing: .init()) { preconditionFailure("thread pool not needed in tests") } } From 7d774e408ac36fbbe69cf9546c41b34a18b8f14d Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 17 Sep 2025 18:23:28 +0900 Subject: [PATCH 14/19] main module does not need InMemoryTracer --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index d88e51a76..3cff98089 100644 --- a/Package.swift +++ b/Package.swift @@ -76,7 +76,6 @@ let package = Package( // Observability support .product(name: "Logging", package: "swift-log"), .product(name: "Tracing", package: "swift-distributed-tracing"), - .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), ], swiftSettings: strictConcurrencySettings ), From 07c6c5799f1fe72d4ec0415b9a40ade6be5495fb Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 17 Sep 2025 19:04:24 +0900 Subject: [PATCH 15/19] make attribute key customization private for now --- Sources/AsyncHTTPClient/HTTPClient.swift | 28 ++++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index f3cd91f3f..7d6aa4bf5 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -1089,35 +1089,29 @@ public final class HTTPClient: Sendable { } } + // TODO: Open up customization of keys we use? /// Configuration for tracing attributes set by the HTTPClient. - public var attributeKeys: AttributeKeys + @usableFromInline + package var attributeKeys: AttributeKeys public init() { self._tracer = nil self.attributeKeys = .init() } - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - public init( - tracer: (any Tracer)? = InstrumentationSystem.tracer, - attributeKeys: AttributeKeys = .init() - ) { - self._tracer = tracer - self.attributeKeys = attributeKeys - } - /// Span attribute keys that the HTTPClient should set automatically. /// This struct allows the configuration of the attribute names (keys) which will be used for the apropriate values. - public struct AttributeKeys: Sendable { - public var requestMethod: String = "http.request.method" - public var requestBodySize: String = "http.request.body.size" + @usableFromInline + package struct AttributeKeys: Sendable { + @usableFromInline package var requestMethod: String = "http.request.method" + @usableFromInline package var requestBodySize: String = "http.request.body.size" - public var responseBodySize: String = "http.response.size" - public var responseStatusCode: String = "http.status_code" + @usableFromInline package var responseBodySize: String = "http.response.size" + @usableFromInline package var responseStatusCode: String = "http.status_code" - public var httpFlavor: String = "http.flavor" + @usableFromInline package var httpFlavor: String = "http.flavor" - public init() {} + @usableFromInline package init() {} } } From 552286b94cd75ea694621dc3bf7df265cea6533e Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 17 Sep 2025 19:18:30 +0900 Subject: [PATCH 16/19] remove duplicated func --- .../AsyncAwait/HTTPClient+execute.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 2ef77dca4..0437211c6 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -46,25 +46,6 @@ extension HTTPClient { ) } } - - @inlinable - func withRequestSpan( - _ request: HTTPClientRequest, - _ body: () async throws -> ReturnType - ) async rethrows -> ReturnType { - #if TracingSupport - if let tracer = self.tracer { - return try await tracer.withSpan("\(request.method)") { span in - let attr = self.configuration.tracing.attributeKeys - span.attributes[attr.requestMethod] = request.method.rawValue - // Set more attributes on the span - return try await body() - } - } - #endif - - return try await body() - } } // MARK: Connivence methods From 8b30965232f8ea2888181d19d3308f7ac0c1a4d2 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 19 Sep 2025 11:30:16 +0900 Subject: [PATCH 17/19] Fix attribute key to be body response size specifcially Co-authored-by: George Barnett --- Sources/AsyncHTTPClient/HTTPClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 7d6aa4bf5..fdb453e7e 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -1106,7 +1106,7 @@ public final class HTTPClient: Sendable { @usableFromInline package var requestMethod: String = "http.request.method" @usableFromInline package var requestBodySize: String = "http.request.body.size" - @usableFromInline package var responseBodySize: String = "http.response.size" + @usableFromInline package var responseBodySize: String = "http.response.body.size" @usableFromInline package var responseStatusCode: String = "http.status_code" @usableFromInline package var httpFlavor: String = "http.flavor" From 622a5491e88982809f3cb59353b88e5fff5e7fd3 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Sat, 20 Sep 2025 10:18:04 +0900 Subject: [PATCH 18/19] Address @glbrntt review comments; minor cleanups --- .../AsyncAwait/HTTPClient+tracing.swift | 4 ---- .../AsyncAwait/Transaction.swift | 17 ----------------- 2 files changed, 21 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift index 5abc24f61..0be737619 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+tracing.swift @@ -12,13 +12,9 @@ // //===----------------------------------------------------------------------===// -import Logging -import NIOCore import NIOHTTP1 import Tracing -import struct Foundation.URL - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClient { @inlinable diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 1a2168571..c0c22bfee 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -35,23 +35,6 @@ final class Transaction: private let state: NIOLockedValueBox - init( - request: HTTPClientRequest.Prepared, - requestOptions: RequestOptions, - logger: Logger, - connectionDeadline: NIODeadline, - preferredEventLoop: EventLoop, - span: (any Span)?, - responseContinuation: CheckedContinuation - ) { - self.request = request - self.requestOptions = requestOptions - self.logger = logger - self.connectionDeadline = connectionDeadline - self.preferredEventLoop = preferredEventLoop - self.state = NIOLockedValueBox(StateMachine(responseContinuation)) - } - init( request: HTTPClientRequest.Prepared, requestOptions: RequestOptions, From 6610fa32f33d2923dfd970ce4aaadb7b7e547192 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 22 Sep 2025 17:43:45 +0900 Subject: [PATCH 19/19] Update copyright year Co-authored-by: George Barnett --- Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index 6d64d5269..dd342c2db 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the AsyncHTTPClient open source project // -// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors +// Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information