Skip to content

Commit 35eba4e

Browse files
authored
Add option to include request/response metadata in tracing interceptors (#34)
OTel's docs for gRPC conventions (https://opentelemetry.io/docs/specs/semconv/rpc/grpc/) say that libraries _should_ provide the ability to opt-in to include request and response metadata. This PR adds this ability to our tracing interceptors.
1 parent 1624240 commit 35eba4e

File tree

4 files changed

+604
-18
lines changed

4 files changed

+604
-18
lines changed

Sources/GRPCOTelTracingInterceptors/Tracing/ClientOTelTracingInterceptor.swift

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ package import Tracing
3131
/// - https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
3232
public struct ClientOTelTracingInterceptor: ClientInterceptor {
3333
private let injector: ClientRequestInjector
34-
private let traceEachMessage: Bool
3534
private var serverHostname: String
3635
private var networkTransportMethod: String
3736

37+
private let traceEachMessage: Bool
38+
private var includeRequestMetadata: Bool
39+
private var includeResponseMetadata: Bool
40+
3841
/// Create a new instance of a ``ClientOTelTracingInterceptor``.
3942
///
4043
/// - Parameters:
@@ -43,15 +46,46 @@ public struct ClientOTelTracingInterceptor: ClientInterceptor {
4346
/// `network.transport` attribute in spans.
4447
/// - traceEachMessage: If `true`, each request part sent and response part received will be recorded as a separate
4548
/// event in a tracing span.
49+
///
50+
/// - Important: Be careful when setting `includeRequestMetadata` or `includeResponseMetadata` to `true`,
51+
/// as including all request/response metadata can be a security risk.
4652
public init(
4753
serverHostname: String,
4854
networkTransportMethod: String,
4955
traceEachMessage: Bool = true
56+
) {
57+
self.init(
58+
serverHostname: serverHostname,
59+
networkTransportMethod: networkTransportMethod,
60+
traceEachMessage: traceEachMessage,
61+
includeRequestMetadata: false,
62+
includeResponseMetadata: false
63+
)
64+
}
65+
66+
/// Create a new instance of a ``ClientOTelTracingInterceptor``.
67+
///
68+
/// - Parameters:
69+
/// - severHostname: The hostname of the RPC server. This will be the value for the `server.address` attribute in spans.
70+
/// - networkTransportMethod: The transport in use (e.g. "tcp", "unix"). This will be the value for the
71+
/// `network.transport` attribute in spans.
72+
/// - traceEachMessage: If `true`, each request part sent and response part received will be recorded as a separate
73+
/// event in a tracing span.
74+
/// - includeRequestMetadata: if `true`, **all** metadata keys with string values included in the request will be added to the span as attributes.
75+
/// - includeResponseMetadata: if `true`, **all** metadata keys with string values included in the response will be added to the span as attributes.
76+
public init(
77+
serverHostname: String,
78+
networkTransportMethod: String,
79+
traceEachMessage: Bool = true,
80+
includeRequestMetadata: Bool = false,
81+
includeResponseMetadata: Bool = false
5082
) {
5183
self.injector = ClientRequestInjector()
5284
self.serverHostname = serverHostname
5385
self.networkTransportMethod = networkTransportMethod
5486
self.traceEachMessage = traceEachMessage
87+
self.includeRequestMetadata = includeRequestMetadata
88+
self.includeResponseMetadata = includeResponseMetadata
5589
}
5690

5791
/// This interceptor will inject as the request's metadata whatever `ServiceContext` key-value pairs
@@ -93,12 +127,6 @@ public struct ClientOTelTracingInterceptor: ClientInterceptor {
93127
var request = request
94128
let serviceContext = ServiceContext.current ?? .topLevel
95129

96-
tracer.inject(
97-
serviceContext,
98-
into: &request.metadata,
99-
using: self.injector
100-
)
101-
102130
return try await tracer.withSpan(
103131
context.descriptor.fullyQualifiedMethod,
104132
context: serviceContext,
@@ -110,6 +138,16 @@ public struct ClientOTelTracingInterceptor: ClientInterceptor {
110138
networkTransportMethod: self.networkTransportMethod
111139
)
112140

141+
if self.includeRequestMetadata {
142+
span.setMetadataStringAttributesAsRequestSpanAttributes(request.metadata)
143+
}
144+
145+
tracer.inject(
146+
serviceContext,
147+
into: &request.metadata,
148+
using: self.injector
149+
)
150+
113151
if self.traceEachMessage {
114152
let wrappedProducer = request.producer
115153
request.producer = { writer in
@@ -131,6 +169,11 @@ public struct ClientOTelTracingInterceptor: ClientInterceptor {
131169
}
132170

133171
var response = try await next(request, context)
172+
173+
if self.includeResponseMetadata {
174+
span.setMetadataStringAttributesAsResponseSpanAttributes(response.metadata)
175+
}
176+
134177
switch response.accepted {
135178
case .success(var success):
136179
let hookedSequence:
@@ -139,14 +182,22 @@ public struct ClientOTelTracingInterceptor: ClientInterceptor {
139182
>
140183
if self.traceEachMessage {
141184
let messageReceivedCounter = Atomic(1)
142-
hookedSequence = HookedRPCAsyncSequence(wrapping: success.bodyParts) { _ in
143-
var event = SpanEvent(name: "rpc.message")
144-
event.attributes[GRPCTracingKeys.rpcMessageType] = "RECEIVED"
145-
event.attributes[GRPCTracingKeys.rpcMessageID] =
146-
messageReceivedCounter
147-
.wrappingAdd(1, ordering: .sequentiallyConsistent)
148-
.oldValue
149-
span.addEvent(event)
185+
hookedSequence = HookedRPCAsyncSequence(wrapping: success.bodyParts) { part in
186+
switch part {
187+
case .message:
188+
var event = SpanEvent(name: "rpc.message")
189+
event.attributes[GRPCTracingKeys.rpcMessageType] = "RECEIVED"
190+
event.attributes[GRPCTracingKeys.rpcMessageID] =
191+
messageReceivedCounter
192+
.wrappingAdd(1, ordering: .sequentiallyConsistent)
193+
.oldValue
194+
span.addEvent(event)
195+
196+
case .trailingMetadata(let trailingMetadata):
197+
if self.includeResponseMetadata {
198+
span.setMetadataStringAttributesAsResponseSpanAttributes(trailingMetadata)
199+
}
200+
}
150201
} onFinish: { error in
151202
if let error {
152203
if let errorCode = error.grpcErrorCode {

Sources/GRPCOTelTracingInterceptors/Tracing/ServerOTelTracingInterceptor.swift

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ package import Tracing
3030
/// - https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
3131
public struct ServerOTelTracingInterceptor: ServerInterceptor {
3232
private let extractor: ServerRequestExtractor
33-
private let traceEachMessage: Bool
3433
private var serverHostname: String
3534
private var networkTransportMethod: String
3635

36+
private let traceEachMessage: Bool
37+
private var includeRequestMetadata: Bool
38+
private var includeResponseMetadata: Bool
39+
3740
/// Create a new instance of a ``ServerOTelTracingInterceptor``.
3841
///
3942
/// - Parameters:
@@ -46,11 +49,42 @@ public struct ServerOTelTracingInterceptor: ServerInterceptor {
4649
serverHostname: String,
4750
networkTransportMethod: String,
4851
traceEachMessage: Bool = true
52+
) {
53+
self.init(
54+
serverHostname: serverHostname,
55+
networkTransportMethod: networkTransportMethod,
56+
traceEachMessage: traceEachMessage,
57+
includeRequestMetadata: false,
58+
includeResponseMetadata: false
59+
)
60+
}
61+
62+
/// Create a new instance of a ``ServerOTelTracingInterceptor``.
63+
///
64+
/// - Parameters:
65+
/// - severHostname: The hostname of the RPC server. This will be the value for the `server.address` attribute in spans.
66+
/// - networkTransportMethod: The transport in use (e.g. "tcp", "unix"). This will be the value for the
67+
/// `network.transport` attribute in spans.
68+
/// - traceEachMessage: If `true`, each response part sent and request part received will be recorded as a separate
69+
/// event in a tracing span.
70+
/// - includeRequestMetadata: if `true`, **all** metadata keys with string values included in the request will be added to the span as attributes.
71+
/// - includeResponseMetadata: if `true`, **all** metadata keys with string values included in the response will be added to the span as attributes.
72+
///
73+
/// - Important: Be careful when setting `includeRequestMetadata` or `includeResponseMetadata` to `true`,
74+
/// as including all request/response metadata can be a security risk.
75+
public init(
76+
serverHostname: String,
77+
networkTransportMethod: String,
78+
traceEachMessage: Bool = true,
79+
includeRequestMetadata: Bool = false,
80+
includeResponseMetadata: Bool = false
4981
) {
5082
self.extractor = ServerRequestExtractor()
5183
self.traceEachMessage = traceEachMessage
5284
self.serverHostname = serverHostname
5385
self.networkTransportMethod = networkTransportMethod
86+
self.includeRequestMetadata = includeRequestMetadata
87+
self.includeResponseMetadata = includeResponseMetadata
5488
}
5589

5690
/// This interceptor will extract whatever `ServiceContext` key-value pairs have been inserted into the
@@ -109,6 +143,10 @@ public struct ServerOTelTracingInterceptor: ServerInterceptor {
109143
networkTransportMethod: self.networkTransportMethod
110144
)
111145

146+
if self.includeRequestMetadata {
147+
span.setMetadataStringAttributesAsRequestSpanAttributes(request.metadata)
148+
}
149+
112150
var request = request
113151
if self.traceEachMessage {
114152
let messageReceivedCounter = Atomic(1)
@@ -128,6 +166,10 @@ public struct ServerOTelTracingInterceptor: ServerInterceptor {
128166

129167
var response = try await next(request, context)
130168

169+
if self.includeResponseMetadata {
170+
span.setMetadataStringAttributesAsResponseSpanAttributes(response.metadata)
171+
}
172+
131173
switch response.accepted {
132174
case .success(var success):
133175
let wrappedProducer = success.producer
@@ -148,11 +190,15 @@ public struct ServerOTelTracingInterceptor: ServerInterceptor {
148190
}
149191
)
150192

151-
let wrappedResult = try await wrappedProducer(
193+
let trailingMetadata = try await wrappedProducer(
152194
RPCWriter(wrapping: eventEmittingWriter)
153195
)
154196

155-
return wrappedResult
197+
if self.includeResponseMetadata {
198+
span.setMetadataStringAttributesAsResponseSpanAttributes(trailingMetadata)
199+
}
200+
201+
return trailingMetadata
156202
}
157203
}
158204

Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ enum GRPCTracingKeys {
3535
static let networkType = "network.type"
3636
static let networkPeerAddress = "network.peer.address"
3737
static let networkPeerPort = "network.peer.port"
38+
39+
fileprivate static let requestMetadataPrefix = "rpc.grpc.request.metadata."
40+
fileprivate static let responseMetadataPrefix = "rpc.grpc.response.metadata."
3841
}
3942

4043
extension Span {
@@ -124,6 +127,48 @@ extension Span {
124127
()
125128
}
126129
}
130+
131+
func setMetadataStringAttributesAsRequestSpanAttributes(_ metadata: Metadata) {
132+
self.setMetadataStringAttributesAsSpanAttributes(
133+
metadata,
134+
prefix: GRPCTracingKeys.requestMetadataPrefix
135+
)
136+
}
137+
138+
func setMetadataStringAttributesAsResponseSpanAttributes(_ metadata: Metadata) {
139+
self.setMetadataStringAttributesAsSpanAttributes(
140+
metadata,
141+
prefix: GRPCTracingKeys.responseMetadataPrefix
142+
)
143+
}
144+
145+
private func setMetadataStringAttributesAsSpanAttributes(_ metadata: Metadata, prefix: String) {
146+
for (key, value) in metadata {
147+
switch value {
148+
case .string(let stringValue):
149+
let spanKey = prefix + key.lowercased()
150+
151+
if let existingValue = self.attributes[spanKey]?.toSpanAttribute() {
152+
switch existingValue {
153+
case .stringArray(var strings):
154+
strings.append(stringValue)
155+
self.attributes[spanKey] = strings
156+
157+
case .string(let oldString):
158+
self.attributes[spanKey] = [oldString, stringValue]
159+
160+
default:
161+
()
162+
}
163+
} else {
164+
self.attributes[spanKey] = stringValue
165+
}
166+
167+
case .binary:
168+
()
169+
}
170+
}
171+
}
127172
}
128173

129174
package enum PeerAddress: Equatable {

0 commit comments

Comments
 (0)