Skip to content

Commit 4d519b6

Browse files
committed
[WIP] Set SpanStatus based on response code
1 parent 8f99545 commit 4d519b6

File tree

3 files changed

+131
-3
lines changed

3 files changed

+131
-3
lines changed

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ public class HTTPClient {
402402
// TODO: net.peer.ip / Not required, but recommended
403403

404404
var request = request
405-
InstrumentationSystem.instrument.inject(context.baggage, into: &request.headers, using: HTTPHeadersInjector())
405+
InstrumentationSystem.instrument.inject(span.context.baggage, into: &request.headers, using: HTTPHeadersInjector())
406406

407407
let logger = context.logger.attachingRequestInformation(request, requestID: globalRequestID.add(1))
408408

@@ -479,7 +479,6 @@ public class HTTPClient {
479479
"ahc-request": "\(request.method) \(request.url)",
480480
"ahc-channel-el": "\(connection.channel.eventLoop)",
481481
"ahc-task-el": "\(taskEL)"])
482-
483482
let channel = connection.channel
484483
let future: EventLoopFuture<Void>
485484
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
@@ -513,10 +512,15 @@ public class HTTPClient {
513512
}
514513
.and(task.futureResult)
515514
.always { result in
516-
if case let .success((_, response)) = result, let httpResponse = response as? HTTPClient.Response {
515+
switch result {
516+
case .success(let (_, response)):
517+
guard let httpResponse = response as? HTTPClient.Response else { return }
518+
span.status = .init(httpResponse.status)
517519
span.attributes.http.statusCode = Int(httpResponse.status.code)
518520
span.attributes.http.statusText = httpResponse.status.reasonPhrase
519521
span.attributes.http.responseContentLength = httpResponse.body?.readableBytes ?? 0
522+
case .failure(let error):
523+
span.recordError(error)
520524
}
521525
span.end()
522526
setupComplete.succeed(())

Diff for: Sources/AsyncHTTPClient/Utils.swift

+35
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import NIOHTTP1
2121
import NIOHTTPCompression
2222
import NIOSSL
2323
import NIOTransportServices
24+
import TracingInstrumentation
2425

2526
internal extension String {
2627
var isIPAddress: Bool {
@@ -147,3 +148,37 @@ extension Connection {
147148
}.recover { _ in }
148149
}
149150
}
151+
152+
extension SpanStatus {
153+
/// Map status code to canonical code according to OTel spec
154+
///
155+
/// - SeeAlso: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#status
156+
init(_ responseStatus: HTTPResponseStatus) {
157+
switch responseStatus.code {
158+
case 100...399:
159+
self = SpanStatus(canonicalCode: .ok)
160+
case 400, 402, 405 ... 428, 430 ... 498:
161+
self = SpanStatus(canonicalCode: .invalidArgument, message: responseStatus.reasonPhrase)
162+
case 401:
163+
self = SpanStatus(canonicalCode: .unauthenticated, message: responseStatus.reasonPhrase)
164+
case 403:
165+
self = SpanStatus(canonicalCode: .permissionDenied, message: responseStatus.reasonPhrase)
166+
case 404:
167+
self = SpanStatus(canonicalCode: .notFound, message: responseStatus.reasonPhrase)
168+
case 429:
169+
self = SpanStatus(canonicalCode: .resourceExhausted, message: responseStatus.reasonPhrase)
170+
case 499:
171+
self = SpanStatus(canonicalCode: .cancelled, message: responseStatus.reasonPhrase)
172+
case 500, 505 ... 599:
173+
self = SpanStatus(canonicalCode: .internal, message: responseStatus.reasonPhrase)
174+
case 501:
175+
self = SpanStatus(canonicalCode: .unimplemented, message: responseStatus.reasonPhrase)
176+
case 503:
177+
self = SpanStatus(canonicalCode: .unavailable, message: responseStatus.reasonPhrase)
178+
case 504:
179+
self = SpanStatus(canonicalCode: .deadlineExceeded, message: responseStatus.reasonPhrase)
180+
default:
181+
self = SpanStatus(canonicalCode: .unknown, message: responseStatus.reasonPhrase)
182+
}
183+
}
184+
}

Diff for: Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+89
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import Network
1818
#endif
1919
import Baggage
20+
import Instrumentation
21+
import TracingInstrumentation
2022
import Logging
2123
import NIO
2224
import NIOConcurrencyHelpers
@@ -2608,4 +2610,91 @@ class HTTPClientTests: XCTestCase {
26082610

26092611
XCTAssertThrowsError(try future.wait())
26102612
}
2613+
2614+
// MARK: - Tracing -
2615+
2616+
func testSemanticHTTPAttributesSet() throws {
2617+
let tracer = TestTracer()
2618+
InstrumentationSystem.bootstrap(tracer)
2619+
2620+
let localHTTPBin = HTTPBin(ssl: true)
2621+
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
2622+
configuration: HTTPClient.Configuration(certificateVerification: .none))
2623+
defer {
2624+
XCTAssertNoThrow(try localClient.syncShutdown())
2625+
XCTAssertNoThrow(try localHTTPBin.shutdown())
2626+
}
2627+
2628+
let url = "https://localhost:\(localHTTPBin.port)/get"
2629+
let response = try localClient.get(url: url, context: testContext()).wait()
2630+
XCTAssertEqual(.ok, response.status)
2631+
2632+
print(tracer.recordedSpans.map(\.attributes))
2633+
}
2634+
}
2635+
2636+
private final class TestTracer: TracingInstrument {
2637+
private(set) var recordedSpans = [TestSpan]()
2638+
2639+
func startSpan(
2640+
named operationName: String,
2641+
context: BaggageContextCarrier,
2642+
ofKind kind: SpanKind,
2643+
at timestamp: Timestamp?
2644+
) -> Span {
2645+
let span = TestSpan(operationName: operationName,
2646+
kind: kind,
2647+
startTimestamp: timestamp ?? .now(),
2648+
context: context.baggage)
2649+
recordedSpans.append(span)
2650+
return span
2651+
}
2652+
2653+
func extract<Carrier, Extractor>(
2654+
_ carrier: Carrier,
2655+
into context: inout BaggageContext,
2656+
using extractor: Extractor
2657+
)
2658+
where
2659+
Carrier == Extractor.Carrier,
2660+
Extractor: ExtractorProtocol {}
2661+
2662+
func inject<Carrier, Injector>(
2663+
_ context: BaggageContext,
2664+
into carrier: inout Carrier,
2665+
using injector: Injector
2666+
)
2667+
where
2668+
Carrier == Injector.Carrier,
2669+
Injector: InjectorProtocol {}
2670+
2671+
final class TestSpan: Span {
2672+
let operationName: String
2673+
let kind: SpanKind
2674+
var status: SpanStatus?
2675+
let context: BaggageContext
2676+
private(set) var isRecording = false
2677+
2678+
var attributes: SpanAttributes = [:]
2679+
2680+
let startTimestamp: Timestamp
2681+
var endTimestamp: Timestamp?
2682+
2683+
func addEvent(_ event: SpanEvent) {}
2684+
2685+
func addLink(_ link: SpanLink) {}
2686+
2687+
func recordError(_ error: Error) {}
2688+
2689+
func end(at timestamp: Timestamp) {
2690+
self.endTimestamp = timestamp
2691+
}
2692+
2693+
init(operationName: String, kind: SpanKind, startTimestamp: Timestamp, context: BaggageContext) {
2694+
self.operationName = operationName
2695+
self.kind = kind
2696+
self.startTimestamp = startTimestamp
2697+
self.context = context
2698+
}
2699+
}
26112700
}

0 commit comments

Comments
 (0)