Skip to content

Commit 353bbc8

Browse files
authored
[Tracing] Implement trace header context propagation (#862)
1 parent c2a3a2c commit 353bbc8

File tree

4 files changed

+97
-3
lines changed

4 files changed

+97
-3
lines changed

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ extension HTTPClient {
9292

9393
// this loop is there to follow potential redirects
9494
while true {
95-
let preparedRequest = try HTTPClientRequest.Prepared(currentRequest, dnsOverride: configuration.dnsOverride)
95+
let preparedRequest =
96+
try HTTPClientRequest.Prepared(
97+
currentRequest,
98+
dnsOverride: configuration.dnsOverride,
99+
tracing: self.configuration.tracing
100+
)
96101
let response = try await {
97102
var response = try await self.executeCancellable(preparedRequest, deadline: deadline, logger: logger)
98103

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Instrumentation
1516
import NIOCore
1617
import NIOHTTP1
1718
import NIOSSL
19+
import ServiceContextModule
1820

1921
import struct Foundation.URL
2022

@@ -45,7 +47,11 @@ extension HTTPClientRequest {
4547

4648
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
4749
extension HTTPClientRequest.Prepared {
48-
init(_ request: HTTPClientRequest, dnsOverride: [String: String] = [:]) throws {
50+
init(
51+
_ request: HTTPClientRequest,
52+
dnsOverride: [String: String] = [:],
53+
tracing: HTTPClient.TracingConfiguration? = nil
54+
) throws {
4955
guard !request.url.isEmpty, let url = URL(string: request.url) else {
5056
throw HTTPClientError.invalidURL
5157
}
@@ -54,6 +60,12 @@ extension HTTPClientRequest.Prepared {
5460

5561
var headers = request.headers
5662
headers.addHostIfNeeded(for: deconstructedURL)
63+
if let tracer = tracing?.tracer,
64+
let context = ServiceContext.current
65+
{
66+
tracer.inject(context, into: &headers, using: HTTPHeadersInjector.shared)
67+
}
68+
5769
let metadata = try headers.validateAndSetTransportFraming(
5870
method: request.method,
5971
bodyLength: .init(request.body)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Atomics
16+
import InMemoryTracing
17+
import Logging
18+
import NIOConcurrencyHelpers
19+
import NIOCore
20+
import NIOEmbedded
21+
import NIOFoundationCompat
22+
import NIOHTTP1
23+
import NIOHTTPCompression
24+
import NIOPosix
25+
import NIOSSL
26+
import NIOTestUtils
27+
import NIOTransportServices
28+
import Tracing
29+
import XCTest
30+
31+
@testable @_spi(Tracing) import AsyncHTTPClient
32+
33+
#if canImport(Network)
34+
import Network
35+
#endif
36+
37+
private func makeTracedHTTPClient(tracer: InMemoryTracer) -> HTTPClient {
38+
var config = HTTPClient.Configuration()
39+
config.httpVersion = .automatic
40+
config.tracing.tracer = tracer
41+
return HTTPClient(
42+
eventLoopGroupProvider: .singleton,
43+
configuration: config
44+
)
45+
}
46+
47+
final class HTTPClientTracingInternalTests: XCTestCaseHTTPClientTestsBaseClass {
48+
49+
var tracer: InMemoryTracer!
50+
var client: HTTPClient!
51+
52+
override func setUp() {
53+
super.setUp()
54+
self.tracer = InMemoryTracer()
55+
self.client = makeTracedHTTPClient(tracer: tracer)
56+
}
57+
58+
override func tearDown() {
59+
if let client = self.client {
60+
XCTAssertNoThrow(try client.syncShutdown())
61+
self.client = nil
62+
}
63+
tracer = nil
64+
}
65+
66+
func testTrace_preparedHeaders_include_fromSpan() async throws {
67+
let url = self.defaultHTTPBinURLPrefix + "404-does-not-exist"
68+
let request = HTTPClientRequest(url: url)
69+
70+
try tracer.withSpan("operation") { span in
71+
let prepared = try HTTPClientRequest.Prepared(request, tracing: self.client.tracing)
72+
XCTAssertTrue(prepared.head.headers.count > 2)
73+
XCTAssertTrue(prepared.head.headers.contains(name: "in-memory-trace-id"))
74+
XCTAssertTrue(prepared.head.headers.contains(name: "in-memory-span-id"))
75+
}
76+
}
77+
}

Tests/AsyncHTTPClientTests/HTTPClientTracingTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
@_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift
15+
@_spi(Tracing) import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientTracingInternalTests.swift
1616
import Atomics
1717
import InMemoryTracing
1818
import Logging

0 commit comments

Comments
 (0)