diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift index 0437211c6..bbf8c948c 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift @@ -92,7 +92,12 @@ extension HTTPClient { // this loop is there to follow potential redirects while true { - let preparedRequest = try HTTPClientRequest.Prepared(currentRequest, dnsOverride: configuration.dnsOverride) + let preparedRequest = + try HTTPClientRequest.Prepared( + currentRequest, + dnsOverride: configuration.dnsOverride, + tracing: self.configuration.tracing + ) let response = try await { var response = try await self.executeCancellable(preparedRequest, deadline: deadline, logger: logger) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift index c39452897..b5649cf90 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -12,9 +12,11 @@ // //===----------------------------------------------------------------------===// +import Instrumentation import NIOCore import NIOHTTP1 import NIOSSL +import ServiceContextModule import struct Foundation.URL @@ -45,7 +47,11 @@ extension HTTPClientRequest { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest.Prepared { - init(_ request: HTTPClientRequest, dnsOverride: [String: String] = [:]) throws { + init( + _ request: HTTPClientRequest, + dnsOverride: [String: String] = [:], + tracing: HTTPClient.TracingConfiguration? = nil + ) throws { guard !request.url.isEmpty, let url = URL(string: request.url) else { throw HTTPClientError.invalidURL } @@ -54,6 +60,12 @@ extension HTTPClientRequest.Prepared { var headers = request.headers headers.addHostIfNeeded(for: deconstructedURL) + if let tracer = tracing?.tracer, + let context = ServiceContext.current + { + tracer.inject(context, into: &headers, using: HTTPHeadersInjector.shared) + } + let metadata = try headers.validateAndSetTransportFraming( method: request.method, bodyLength: .init(request.body) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift new file mode 100644 index 000000000..53f1138ba --- /dev/null +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingInternalTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2025 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 Atomics +import InMemoryTracing +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOEmbedded +import NIOFoundationCompat +import NIOHTTP1 +import NIOHTTPCompression +import NIOPosix +import NIOSSL +import NIOTestUtils +import NIOTransportServices +import Tracing +import XCTest + +@testable @_spi(Tracing) import AsyncHTTPClient + +#if canImport(Network) +import Network +#endif + +private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient { + var config = HTTPClient.Configuration() + config.httpVersion = .automatic + config.tracing.tracer = tracer + return HTTPClient( + eventLoopGroupProvider: .singleton, + configuration: config + ) +} + +final class HTTPClientTracingInternalTests: 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_preparedHeaders_include_fromSpan() async throws { + let url = self.defaultHTTPBinURLPrefix + "404-does-not-exist" + let request = HTTPClientRequest(url: url) + + try tracer.withSpan("operation") { span in + let prepared = try HTTPClientRequest.Prepared(request, tracing: self.client.tracing) + XCTAssertTrue(prepared.head.headers.count > 2) + XCTAssertTrue(prepared.head.headers.contains(name: "in-memory-trace-id")) + XCTAssertTrue(prepared.head.headers.contains(name: "in-memory-span-id")) + } + } +} diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift index dd342c2db..047c66e6d 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift +@_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientTracingInternalTests.swift import Atomics import InMemoryTracing import Logging