From d0ce5d3e5455ee2dc5d2742d34e8eedabf8a3a23 Mon Sep 17 00:00:00 2001 From: Daniel Grumberg Date: Mon, 15 Dec 2025 11:33:05 +0000 Subject: [PATCH] Make `withSpan` set span status to `.error` if operation closure throws **Motivation:** Span status is relied upon by application developers to quickly discover spans that have thrown a exception. **Modifications:** Ensure that all tracer protocols set the span status to `.error` when the underlying operation closure throws during execution. **Result:** When an error is thrown from a `withSpan` operation closure the underlying span has an `.error` span status code on return. --- Sources/Tracing/TracerProtocol+Legacy.swift | 5 ++++ Sources/Tracing/TracerProtocol.swift | 6 ++++ Tests/TracingTests/TestTracer.swift | 2 +- Tests/TracingTests/TracerTests.swift | 32 ++++++++++++++------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Sources/Tracing/TracerProtocol+Legacy.swift b/Sources/Tracing/TracerProtocol+Legacy.swift index f20e218..e270936 100644 --- a/Sources/Tracing/TracerProtocol+Legacy.swift +++ b/Sources/Tracing/TracerProtocol+Legacy.swift @@ -229,6 +229,7 @@ extension LegacyTracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -333,6 +334,7 @@ extension LegacyTracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -383,6 +385,7 @@ extension LegacyTracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -437,6 +440,7 @@ extension LegacyTracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -485,6 +489,7 @@ extension LegacyTracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } diff --git a/Sources/Tracing/TracerProtocol.swift b/Sources/Tracing/TracerProtocol.swift index cf7e27e..86f0467 100644 --- a/Sources/Tracing/TracerProtocol.swift +++ b/Sources/Tracing/TracerProtocol.swift @@ -180,6 +180,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -231,6 +232,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -284,6 +286,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -321,6 +324,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -375,6 +379,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } @@ -425,6 +430,7 @@ extension Tracer { } } catch { span.recordError(error) + span.setStatus(.init(code: .error)) throw error // rethrow } } diff --git a/Tests/TracingTests/TestTracer.swift b/Tests/TracingTests/TestTracer.swift index a8b27ee..4adaece 100644 --- a/Tests/TracingTests/TestTracer.swift +++ b/Tests/TracingTests/TestTracer.swift @@ -120,7 +120,7 @@ extension ServiceContext { final class TestSpan: Span { let kind: SpanKind - private var status: SpanStatus? + var status: SpanStatus? package let startTimestampNanosSinceEpoch: UInt64 package private(set) var endTimestampNanosSinceEpoch: UInt64? diff --git a/Tests/TracingTests/TracerTests.swift b/Tests/TracingTests/TracerTests.swift index 222b66a..9bba5e5 100644 --- a/Tests/TracingTests/TracerTests.swift +++ b/Tests/TracingTests/TracerTests.swift @@ -78,7 +78,11 @@ struct TracerTests { let tracer = TestTracer() var spanEnded = false - tracer.onEndSpan = { _ in spanEnded = true } + var spanStatus: SpanStatus? = nil + tracer.onEndSpan = { span in + spanEnded = true + spanStatus = span.status + } do { _ = try tracer.withAnySpan("hello", context: .topLevel) { _ in @@ -86,6 +90,7 @@ struct TracerTests { } } catch { #expect(spanEnded == true) + #expect(spanStatus?.code == .error) #expect(error as? ExampleSpanError == ExampleSpanError()) return } @@ -117,7 +122,11 @@ struct TracerTests { let tracer = TestTracer() var spanEnded = false - tracer.onEndSpan = { _ in spanEnded = true } + var spanStatus: SpanStatus? = nil + tracer.onEndSpan = { span in + spanEnded = true + spanStatus = span.status + } func operation(span: any Tracing.Span) throws -> String { throw ExampleSpanError() @@ -127,6 +136,7 @@ struct TracerTests { _ = try tracer.withAnySpan("hello", operation) } catch { #expect(spanEnded == true) + #expect(spanStatus?.code == .error) #expect(error as? ExampleSpanError == ExampleSpanError()) return } @@ -181,8 +191,8 @@ struct TracerTests { func withSpan_automaticBaggagePropagation_async_throws() async throws { let tracer = TestTracer() - let spanEnded: LockedValueBox = .init(false) - tracer.onEndSpan = { _ in spanEnded.withValue { $0 = true } } + let endedSpan: LockedValueBox = .init(nil) + tracer.onEndSpan = { span in endedSpan.withValue { $0 = span } } let operation: @Sendable (any Tracing.Span) async throws -> String = { _ in throw ExampleSpanError() @@ -191,7 +201,7 @@ struct TracerTests { do { _ = try await tracer.withAnySpan("hello", operation) } catch { - #expect(spanEnded.withValue { $0 } == true) + #expect(endedSpan.withValue { $0?.status?.code } == .error) #expect(error as? ExampleSpanError == ExampleSpanError()) return } @@ -202,8 +212,8 @@ struct TracerTests { func static_Tracer_withSpan_automaticBaggagePropagation_async_throws() async throws { let tracer = TestTracer() - let spanEnded: LockedValueBox = .init(false) - tracer.onEndSpan = { _ in spanEnded.withValue { $0 = true } } + let endedSpan: LockedValueBox = .init(nil) + tracer.onEndSpan = { span in endedSpan.withValue { $0 = span } } let operation: @Sendable (any Tracing.Span) async throws -> String = { _ in throw ExampleSpanError() @@ -212,7 +222,7 @@ struct TracerTests { do { _ = try await tracer.withSpan("hello", operation) } catch { - #expect(spanEnded.withValue { $0 } == true) + #expect(endedSpan.withValue { $0?.status?.code } == .error) #expect(error as? ExampleSpanError == ExampleSpanError()) return } @@ -223,8 +233,8 @@ struct TracerTests { func static_Tracer_withSpan_automaticBaggagePropagation_throws() async throws { let tracer = TestTracer() - let spanEnded: LockedValueBox = .init(false) - tracer.onEndSpan = { _ in spanEnded.withValue { $0 = true } } + let endedSpan: LockedValueBox = .init(nil) + tracer.onEndSpan = { span in endedSpan.withValue { $0 = span } } let operation: @Sendable (any Tracing.Span) async throws -> String = { _ in throw ExampleSpanError() @@ -233,7 +243,7 @@ struct TracerTests { do { _ = try await tracer.withSpan("hello", operation) } catch { - #expect(spanEnded.withValue { $0 } == true) + #expect(endedSpan.withValue { $0?.status?.code } == .error) #expect(error as? ExampleSpanError == ExampleSpanError()) return }