Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ee4936a

Browse files
authoredJan 23, 2025··
Conform client's tracing interceptor to OTel's conventions (grpc#25)
This PR conforms the client tracing interceptor to follow recommendations/conventions laid out in both: - https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans - https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
1 parent 0eb173d commit ee4936a

12 files changed

+1027
-323
lines changed
 

‎Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ let dependencies: [Package.Dependency] = [
4747
),
4848
.package(
4949
url: "https://github.com/grpc/grpc-swift-protobuf.git",
50-
exact: "1.0.0-beta.3"
50+
branch: "main"
5151
),
5252
.package(
5353
url: "https://github.com/apple/swift-protobuf.git",

‎Sources/GRPCInterceptors/ClientTracingInterceptor.swift

Lines changed: 0 additions & 140 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
internal struct HookedRPCAsyncSequence<Wrapped: AsyncSequence & Sendable>: AsyncSequence, Sendable
18+
where Wrapped.Element: Sendable {
19+
private let wrapped: Wrapped
20+
21+
private let forEachElement: @Sendable (Wrapped.Element) -> Void
22+
private let onFinish: @Sendable ((any Error)?) -> Void
23+
24+
init(
25+
wrapping sequence: Wrapped,
26+
forEachElement: @escaping @Sendable (Wrapped.Element) -> Void,
27+
onFinish: @escaping @Sendable ((any Error)?) -> Void
28+
) {
29+
self.wrapped = sequence
30+
self.forEachElement = forEachElement
31+
self.onFinish = onFinish
32+
}
33+
34+
func makeAsyncIterator() -> HookedAsyncIterator {
35+
HookedAsyncIterator(
36+
self.wrapped,
37+
forEachElement: self.forEachElement,
38+
onFinish: self.onFinish
39+
)
40+
}
41+
42+
struct HookedAsyncIterator: AsyncIteratorProtocol {
43+
typealias Element = Wrapped.Element
44+
45+
private var wrapped: Wrapped.AsyncIterator
46+
private let forEachElement: @Sendable (Wrapped.Element) -> Void
47+
private let onFinish: @Sendable ((any Error)?) -> Void
48+
49+
init(
50+
_ sequence: Wrapped,
51+
forEachElement: @escaping @Sendable (Wrapped.Element) -> Void,
52+
onFinish: @escaping @Sendable ((any Error)?) -> Void
53+
) {
54+
self.wrapped = sequence.makeAsyncIterator()
55+
self.forEachElement = forEachElement
56+
self.onFinish = onFinish
57+
}
58+
59+
mutating func next(
60+
isolation actor: isolated (any Actor)?
61+
) async throws(Wrapped.Failure) -> Wrapped.Element? {
62+
do {
63+
if let element = try await self.wrapped.next(isolation: actor) {
64+
self.forEachElement(element)
65+
return element
66+
} else {
67+
self.onFinish(nil)
68+
return nil
69+
}
70+
} catch {
71+
self.onFinish(error)
72+
throw error
73+
}
74+
}
75+
76+
mutating func next() async throws -> Wrapped.Element? {
77+
try await self.next(isolation: nil)
78+
}
79+
}
80+
}

‎Sources/GRPCInterceptors/HookedWriter.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,22 @@ internal import Tracing
1818

1919
struct HookedWriter<Element: Sendable>: RPCWriterProtocol {
2020
private let writer: any RPCWriterProtocol<Element>
21-
private let beforeEachWrite: @Sendable () -> Void
2221
private let afterEachWrite: @Sendable () -> Void
2322

2423
init(
2524
wrapping other: some RPCWriterProtocol<Element>,
26-
beforeEachWrite: @Sendable @escaping () -> Void,
2725
afterEachWrite: @Sendable @escaping () -> Void
2826
) {
2927
self.writer = other
30-
self.beforeEachWrite = beforeEachWrite
3128
self.afterEachWrite = afterEachWrite
3229
}
3330

3431
func write(_ element: Element) async throws {
35-
self.beforeEachWrite()
3632
try await self.writer.write(element)
3733
self.afterEachWrite()
3834
}
3935

4036
func write(contentsOf elements: some Sequence<Element>) async throws {
41-
self.beforeEachWrite()
4237
try await self.writer.write(contentsOf: elements)
4338
self.afterEachWrite()
4439
}

‎Sources/GRPCInterceptors/OnFinishAsyncSequence.swift

Lines changed: 0 additions & 56 deletions
This file was deleted.
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright 2024-2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
public import GRPCCore
18+
internal import Synchronization
19+
package import Tracing
20+
21+
/// A client interceptor that injects tracing information into the request.
22+
///
23+
/// The tracing information is taken from the current `ServiceContext`, and injected into the request's
24+
/// metadata. It will then be picked up by the server-side ``ServerTracingInterceptor``.
25+
///
26+
/// For more information, refer to the documentation for `swift-distributed-tracing`.
27+
public struct ClientOTelTracingInterceptor: ClientInterceptor {
28+
private let injector: ClientRequestInjector
29+
private let traceEachMessage: Bool
30+
private var serverHostname: String
31+
private var networkTransportMethod: String
32+
33+
/// Create a new instance of a ``ClientOTelTracingInterceptor``.
34+
///
35+
/// - Parameters:
36+
/// - severHostname: The hostname of the RPC server. This will be the value for the `server.address` attribute in spans.
37+
/// - networkTransportMethod: The transport in use (e.g. "tcp", "unix"). This will be the value for the
38+
/// `network.transport` attribute in spans.
39+
/// - traceEachMessage: If `true`, each request part sent and response part received will be recorded as a separate
40+
/// event in a tracing span. Otherwise, only the request/response start and end will be recorded as events.
41+
public init(
42+
serverHostname: String,
43+
networkTransportMethod: String,
44+
traceEachMessage: Bool = true
45+
) {
46+
self.injector = ClientRequestInjector()
47+
self.serverHostname = serverHostname
48+
self.networkTransportMethod = networkTransportMethod
49+
self.traceEachMessage = traceEachMessage
50+
}
51+
52+
/// This interceptor will inject as the request's metadata whatever `ServiceContext` key-value pairs
53+
/// have been made available by the tracing implementation bootstrapped in your application.
54+
///
55+
/// Which key-value pairs are injected will depend on the specific tracing implementation
56+
/// that has been configured when bootstrapping `swift-distributed-tracing` in your application.
57+
///
58+
/// It will also inject all required and recommended span and event attributes, and set span status, as defined by OpenTelemetry's
59+
/// documentation on:
60+
/// - https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans
61+
/// - https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
62+
public func intercept<Input, Output>(
63+
request: StreamingClientRequest<Input>,
64+
context: ClientContext,
65+
next: (
66+
StreamingClientRequest<Input>,
67+
ClientContext
68+
) async throws -> StreamingClientResponse<Output>
69+
) async throws -> StreamingClientResponse<Output> where Input: Sendable, Output: Sendable {
70+
try await self.intercept(
71+
tracer: InstrumentationSystem.tracer,
72+
request: request,
73+
context: context,
74+
next: next
75+
)
76+
}
77+
78+
/// Same as ``intercept(request:context:next:)``, but allows specifying a `Tracer` for testing purposes.
79+
package func intercept<Input, Output>(
80+
tracer: any Tracer,
81+
request: StreamingClientRequest<Input>,
82+
context: ClientContext,
83+
next: (
84+
StreamingClientRequest<Input>,
85+
ClientContext
86+
) async throws -> StreamingClientResponse<Output>
87+
) async throws -> StreamingClientResponse<Output> where Input: Sendable, Output: Sendable {
88+
var request = request
89+
let serviceContext = ServiceContext.current ?? .topLevel
90+
91+
tracer.inject(
92+
serviceContext,
93+
into: &request.metadata,
94+
using: self.injector
95+
)
96+
97+
return try await tracer.withSpan(
98+
context.descriptor.fullyQualifiedMethod,
99+
context: serviceContext,
100+
ofKind: .client
101+
) { span in
102+
span.setOTelClientSpanGRPCAttributes(
103+
context: context,
104+
serverHostname: self.serverHostname,
105+
networkTransportMethod: self.networkTransportMethod
106+
)
107+
108+
if self.traceEachMessage {
109+
let wrappedProducer = request.producer
110+
request.producer = { writer in
111+
let messageSentCounter = Atomic(1)
112+
let eventEmittingWriter = HookedWriter(
113+
wrapping: writer,
114+
afterEachWrite: {
115+
var event = SpanEvent(name: "rpc.message")
116+
event.attributes[GRPCTracingKeys.rpcMessageType] = "SENT"
117+
event.attributes[GRPCTracingKeys.rpcMessageID] =
118+
messageSentCounter
119+
.wrappingAdd(1, ordering: .sequentiallyConsistent)
120+
.oldValue
121+
span.addEvent(event)
122+
}
123+
)
124+
try await wrappedProducer(RPCWriter(wrapping: eventEmittingWriter))
125+
}
126+
}
127+
128+
var response = try await next(request, context)
129+
switch response.accepted {
130+
case .success(var success):
131+
let hookedSequence:
132+
HookedRPCAsyncSequence<
133+
RPCAsyncSequence<StreamingClientResponse<Output>.Contents.BodyPart, any Error>
134+
>
135+
if self.traceEachMessage {
136+
let messageReceivedCounter = Atomic(1)
137+
hookedSequence = HookedRPCAsyncSequence(wrapping: success.bodyParts) { _ in
138+
var event = SpanEvent(name: "rpc.message")
139+
event.attributes[GRPCTracingKeys.rpcMessageType] = "RECEIVED"
140+
event.attributes[GRPCTracingKeys.rpcMessageID] =
141+
messageReceivedCounter
142+
.wrappingAdd(1, ordering: .sequentiallyConsistent)
143+
.oldValue
144+
span.addEvent(event)
145+
} onFinish: { error in
146+
if let error {
147+
if let errorCode = error.grpcErrorCode {
148+
span.attributes[GRPCTracingKeys.grpcStatusCode] = errorCode.rawValue
149+
}
150+
span.setStatus(SpanStatus(code: .error))
151+
span.recordError(error)
152+
} else {
153+
span.attributes[GRPCTracingKeys.grpcStatusCode] = 0
154+
}
155+
}
156+
} else {
157+
hookedSequence = HookedRPCAsyncSequence(wrapping: success.bodyParts) { _ in
158+
// Nothing to do if traceEachMessage is false
159+
} onFinish: { error in
160+
if let error {
161+
if let errorCode = error.grpcErrorCode {
162+
span.attributes[GRPCTracingKeys.grpcStatusCode] = errorCode.rawValue
163+
}
164+
span.setStatus(SpanStatus(code: .error))
165+
span.recordError(error)
166+
} else {
167+
span.attributes[GRPCTracingKeys.grpcStatusCode] = 0
168+
}
169+
}
170+
}
171+
172+
success.bodyParts = RPCAsyncSequence(wrapping: hookedSequence)
173+
response.accepted = .success(success)
174+
175+
case .failure(let error):
176+
span.attributes[GRPCTracingKeys.grpcStatusCode] = error.code.rawValue
177+
span.setStatus(SpanStatus(code: .error))
178+
span.recordError(error)
179+
}
180+
181+
return response
182+
}
183+
}
184+
}
185+
186+
/// An injector responsible for injecting the required instrumentation keys from the `ServiceContext` into
187+
/// the request metadata.
188+
struct ClientRequestInjector: Instrumentation.Injector {
189+
typealias Carrier = Metadata
190+
191+
func inject(_ value: String, forKey key: String, into carrier: inout Carrier) {
192+
carrier.addString(value, forKey: key)
193+
}
194+
}
195+
196+
extension Error {
197+
var grpcErrorCode: RPCError.Code? {
198+
if let rpcError = self as? RPCError {
199+
return rpcError.code
200+
} else if let rpcError = self as? any RPCErrorConvertible {
201+
return rpcError.rpcErrorCode
202+
} else {
203+
return nil
204+
}
205+
}
206+
}

‎Sources/GRPCInterceptors/ServerTracingInterceptor.swift renamed to ‎Sources/GRPCInterceptors/Tracing/ServerTracingInterceptor.swift

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,43 +90,28 @@ public struct ServerTracingInterceptor: ServerInterceptor {
9090
success.producer = { writer in
9191
let eventEmittingWriter = HookedWriter(
9292
wrapping: writer,
93-
beforeEachWrite: {
94-
span.addEvent("Sending response part")
95-
},
9693
afterEachWrite: {
9794
span.addEvent("Sent response part")
9895
}
9996
)
10097

101-
let wrappedResult: Metadata
102-
do {
103-
wrappedResult = try await wrappedProducer(
104-
RPCWriter(wrapping: eventEmittingWriter)
105-
)
106-
} catch {
107-
span.addEvent("Error encountered")
108-
throw error
109-
}
98+
let wrappedResult = try await wrappedProducer(
99+
RPCWriter(wrapping: eventEmittingWriter)
100+
)
110101

111102
span.addEvent("Sent response end")
112103
return wrappedResult
113104
}
114105
} else {
115106
success.producer = { writer in
116-
let wrappedResult: Metadata
117-
do {
118-
wrappedResult = try await wrappedProducer(writer)
119-
} catch {
120-
span.addEvent("Error encountered")
121-
throw error
122-
}
123-
107+
let wrappedResult = try await wrappedProducer(writer)
124108
span.addEvent("Sent response end")
125109
return wrappedResult
126110
}
127111
}
128112

129113
response = .init(accepted: .success(success))
114+
130115
case .failure:
131116
span.addEvent("Sent error response")
132117
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
internal import GRPCCore
18+
internal import Tracing
19+
20+
enum GRPCTracingKeys {
21+
static let rpcSystem = "rpc.system"
22+
static let rpcMethod = "rpc.method"
23+
static let rpcService = "rpc.service"
24+
static let rpcMessageID = "rpc.message.id"
25+
static let rpcMessageType = "rpc.message.type"
26+
static let grpcStatusCode = "rpc.grpc.status_code"
27+
28+
static let serverAddress = "server.address"
29+
static let serverPort = "server.port"
30+
31+
static let clientAddress = "client.address"
32+
static let clientPort = "client.port"
33+
34+
static let networkTransport = "network.transport"
35+
static let networkType = "network.type"
36+
static let networkPeerAddress = "network.peer.address"
37+
static let networkPeerPort = "network.peer.port"
38+
}
39+
40+
extension Span {
41+
// See: https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
42+
func setOTelClientSpanGRPCAttributes(
43+
context: ClientContext,
44+
serverHostname: String,
45+
networkTransportMethod: String
46+
) {
47+
self.attributes[GRPCTracingKeys.rpcSystem] = "grpc"
48+
self.attributes[GRPCTracingKeys.serverAddress] = serverHostname
49+
self.attributes[GRPCTracingKeys.networkTransport] = networkTransportMethod
50+
self.attributes[GRPCTracingKeys.rpcService] = context.descriptor.service.fullyQualifiedService
51+
self.attributes[GRPCTracingKeys.rpcMethod] = context.descriptor.method
52+
53+
// Set server address information
54+
switch PeerAddress(context.remotePeer) {
55+
case .ipv4(let address, let port):
56+
self.attributes[GRPCTracingKeys.networkType] = "ipv4"
57+
self.attributes[GRPCTracingKeys.networkPeerAddress] = address
58+
self.attributes[GRPCTracingKeys.networkPeerPort] = port
59+
self.attributes[GRPCTracingKeys.serverPort] = port
60+
61+
case .ipv6(let address, let port):
62+
self.attributes[GRPCTracingKeys.networkType] = "ipv6"
63+
self.attributes[GRPCTracingKeys.networkPeerAddress] = address
64+
self.attributes[GRPCTracingKeys.networkPeerPort] = port
65+
self.attributes[GRPCTracingKeys.serverPort] = port
66+
67+
case .unixDomainSocket(let path):
68+
self.attributes[GRPCTracingKeys.networkPeerAddress] = path
69+
70+
case .none:
71+
// We don't recognise this address format, so don't populate any fields.
72+
()
73+
}
74+
}
75+
}
76+
77+
package enum PeerAddress: Equatable {
78+
case ipv4(address: String, port: Int?)
79+
case ipv6(address: String, port: Int?)
80+
case unixDomainSocket(path: String)
81+
82+
package init?(_ address: String) {
83+
// We expect this address to be of one of these formats:
84+
// - ipv4:<host>:<port> for ipv4 addresses
85+
// - ipv6:[<host>]:<port> for ipv6 addresses
86+
// - unix:<uds-pathname> for UNIX domain sockets
87+
88+
// First get the first component so that we know what type of address we're dealing with
89+
let addressComponents = address.split(separator: ":", maxSplits: 1)
90+
91+
guard addressComponents.count > 1 else {
92+
// This is some unexpected/unknown format
93+
return nil
94+
}
95+
96+
// Check what type the transport is...
97+
switch addressComponents[0] {
98+
case "ipv4":
99+
let ipv4AddressComponents = addressComponents[1].split(separator: ":")
100+
if ipv4AddressComponents.count == 2, let port = Int(ipv4AddressComponents[1]) {
101+
self = .ipv4(address: String(ipv4AddressComponents[0]), port: port)
102+
} else {
103+
return nil
104+
}
105+
106+
case "ipv6":
107+
if addressComponents[1].first == "[" {
108+
// At this point, we are looking at an address with format: [<address>]:<port>
109+
// We drop the first character ('[') and split by ']:' to keep two components: the address
110+
// and the port.
111+
let ipv6AddressComponents = addressComponents[1].dropFirst().split(separator: "]:")
112+
if ipv6AddressComponents.count == 2, let port = Int(ipv6AddressComponents[1]) {
113+
self = .ipv6(address: String(ipv6AddressComponents[0]), port: port)
114+
} else {
115+
return nil
116+
}
117+
} else {
118+
return nil
119+
}
120+
121+
case "unix":
122+
// Whatever comes after "unix:" is the <pathname>
123+
self = .unixDomainSocket(path: String(addressComponents[1]))
124+
125+
default:
126+
// This is some unexpected/unknown format
127+
return nil
128+
}
129+
}
130+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import GRPCInterceptors
18+
import Testing
19+
20+
@Suite("PeerAddress tests")
21+
struct PeerAddressTests {
22+
@Test("IPv4 addresses are correctly parsed")
23+
func testIPv4() {
24+
let address = PeerAddress("ipv4:10.1.2.80:567")
25+
#expect(address == .ipv4(address: "10.1.2.80", port: 567))
26+
}
27+
28+
@Test("IPv6 addresses are correctly parsed")
29+
func testIPv6() {
30+
let address = PeerAddress("ipv6:[2001::130F:::09C0:876A:130B]:1234")
31+
#expect(address == .ipv6(address: "2001::130F:::09C0:876A:130B", port: 1234))
32+
}
33+
34+
@Test("Unix domain sockets are correctly parsed")
35+
func testUDS() {
36+
let address = PeerAddress("unix:some-path")
37+
#expect(address == .unixDomainSocket(path: "some-path"))
38+
}
39+
40+
@Test(
41+
"Unrecognised addresses return nil",
42+
arguments: [
43+
"",
44+
"unknown",
45+
"in-process:1234",
46+
"ipv4:",
47+
"ipv4:1234",
48+
"ipv6:",
49+
"ipv6:123:456:789:123",
50+
"ipv6:123:456:789]:123",
51+
"ipv6:123:456:789]",
52+
"unix",
53+
]
54+
)
55+
func testOther(address: String) {
56+
let address = PeerAddress(address)
57+
#expect(address == nil)
58+
}
59+
}

‎Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift

Lines changed: 491 additions & 89 deletions
Large diffs are not rendered by default.

‎Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ final class TestTracer: Tracer {
2323

2424
private let testSpans: Mutex<[String: TestSpan]> = .init([:])
2525

26-
func getEventsForTestSpan(ofOperationName operationName: String) -> [SpanEvent] {
27-
let span = self.testSpans.withLock({ $0[operationName] })
28-
return span?.events ?? []
26+
func getSpan(ofOperation operationName: String) -> TestSpan? {
27+
self.testSpans.withLock { $0[operationName] }
28+
}
29+
30+
func getEventsForTestSpan(ofOperation operationName: String) -> [SpanEvent] {
31+
self.getSpan(ofOperation: operationName)?.events ?? []
2932
}
3033

3134
func extract<Carrier, Extract>(
@@ -75,6 +78,7 @@ final class TestSpan: Span, Sendable {
7578
var attributes: Tracing.SpanAttributes
7679
var status: Tracing.SpanStatus?
7780
var events: [Tracing.SpanEvent] = []
81+
var errors: [TracingInterceptorTestError]
7882
}
7983

8084
private let state: Mutex<State>
@@ -98,13 +102,26 @@ final class TestSpan: Span, Sendable {
98102
self.state.withLock { $0.events }
99103
}
100104

105+
var status: SpanStatus? {
106+
self.state.withLock { $0.status }
107+
}
108+
109+
var errors: [TracingInterceptorTestError] {
110+
self.state.withLock { $0.errors }
111+
}
112+
101113
init(
102114
context: ServiceContextModule.ServiceContext,
103115
operationName: String,
104116
attributes: Tracing.SpanAttributes = [:],
105117
isRecording: Bool = true
106118
) {
107-
let state = State(context: context, operationName: operationName, attributes: attributes)
119+
let state = State(
120+
context: context,
121+
operationName: operationName,
122+
attributes: attributes,
123+
errors: []
124+
)
108125
self.state = Mutex(state)
109126
self.isRecording = isRecording
110127
}
@@ -122,12 +139,8 @@ final class TestSpan: Span, Sendable {
122139
attributes: Tracing.SpanAttributes,
123140
at instant: @autoclosure () -> Instant
124141
) where Instant: Tracing.TracerInstant {
125-
self.setStatus(
126-
.init(
127-
code: .error,
128-
message: "Error: \(error), attributes: \(attributes), at instant: \(instant())"
129-
)
130-
)
142+
// For the purposes of these tests, we don't really care about the error being thrown
143+
self.state.withLock { $0.errors.append(TracingInterceptorTestError.testError) }
131144
}
132145

133146
func addLink(_ link: Tracing.SpanLink) {
@@ -137,7 +150,7 @@ final class TestSpan: Span, Sendable {
137150
}
138151

139152
func end<Instant>(at instant: @autoclosure () -> Instant) where Instant: Tracing.TracerInstant {
140-
self.setStatus(.init(code: .ok, message: "Ended at instant: \(instant())"))
153+
// no-op
141154
}
142155
}
143156

@@ -192,3 +205,33 @@ struct TestWriter<WriterElement: Sendable>: RPCWriterProtocol {
192205
}
193206
}
194207
}
208+
209+
struct TestSpanEvent: Equatable, CustomDebugStringConvertible {
210+
var name: String
211+
var attributes: SpanAttributes
212+
213+
var debugDescription: String {
214+
var attributesDescription = ""
215+
self.attributes.forEach { key, value in
216+
attributesDescription += " \(key): \(value),"
217+
}
218+
219+
return """
220+
(name: \(self.name), attributes: [\(attributesDescription)])
221+
"""
222+
}
223+
224+
init(_ name: String, _ attributes: SpanAttributes) {
225+
self.name = name
226+
self.attributes = attributes
227+
}
228+
229+
init(_ spanEvent: SpanEvent) {
230+
self.name = spanEvent.name
231+
self.attributes = spanEvent.attributes
232+
}
233+
}
234+
235+
enum TracingInterceptorTestError: Error, Equatable {
236+
case testError
237+
}

‎dev/license-check.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ check_copyright_headers() {
8888

8989
actual_sha=$(head -n "$((drop_first + expected_lines))" "$filename" \
9090
| tail -n "$expected_lines" \
91-
| sed -e 's/201[56789]-20[12][0-9]/YEARS/' -e 's/20[12][0-9]/YEARS/' \
91+
| sed -e 's/20[12][0-9]-20[12][0-9]/YEARS/' -e 's/20[12][0-9]/YEARS/' \
9292
| shasum \
9393
| awk '{print $1}')
9494

0 commit comments

Comments
 (0)
Please sign in to comment.