Skip to content

Commit 5cf4d9c

Browse files
committed
Auto extract IDs from headers on the server
Motivation: It's common for service authors to extract a tracing ID from a request header and attach that value to the logger for an RPC. This only requires a few lines of code but must be done in each RPC. gRPC should provide configuration to do this automatically. Modifications: - Add server configuration which extracts a value from headers and attaches it to a logger at the start of each RPC. - Wire up the config. Add tests. Result: Trace IDs can be automatically extracted from headers on the server and attached to loggers.
1 parent 3677a71 commit 5cf4d9c

18 files changed

+444
-36
lines changed

Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift

+9-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,10 @@ internal final class AsyncServerHandler<
191191

192192
/// A logger.
193193
@usableFromInline
194-
internal let logger: Logger
194+
internal var logger: Logger
195+
196+
@usableFromInline
197+
internal let traceIDExtractor: Server.Configuration.TraceIDExtractor?
195198

196199
/// A reference to the user info. This is shared with the interceptor pipeline and may be accessed
197200
/// from the async call context. `UserInfo` is _not_ `Sendable` and must always be accessed from
@@ -267,6 +270,7 @@ internal final class AsyncServerHandler<
267270
self.compressionEnabledOnRPC = context.encoding.isEnabled
268271
self.compressResponsesIfPossible = true
269272
self.logger = context.logger
273+
self.traceIDExtractor = context.traceIDExtractor
270274

271275
self.userInfoRef = Ref(UserInfo())
272276
self.handlerStateMachine = .init()
@@ -295,6 +299,10 @@ internal final class AsyncServerHandler<
295299
internal func receiveMetadata(_ headers: HPACKHeaders) {
296300
switch self.interceptorStateMachine.interceptRequestMetadata() {
297301
case .intercept:
302+
if let extractor = self.traceIDExtractor, let id = headers.first(name: extractor.headerName) {
303+
self.logger[metadataKey: extractor.loggerKey] = "\(id)"
304+
self.interceptors?.logger[metadataKey: extractor.loggerKey] = "\(id)"
305+
}
298306
self.interceptors?.receive(.metadata(headers))
299307
case .cancel:
300308
self.cancel(error: nil)

Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import Logging
1617
import NIOCore
1718
import NIOHPACK
1819

@@ -56,6 +57,9 @@ public final class BidirectionalStreamingServerHandler<
5657
@usableFromInline
5758
internal var state: State = .idle
5859

60+
@usableFromInline
61+
internal var logger: Logger
62+
5963
@usableFromInline
6064
internal enum State {
6165
// No headers have been received.
@@ -85,6 +89,7 @@ public final class BidirectionalStreamingServerHandler<
8589

8690
let userInfoRef = Ref(UserInfo())
8791
self.userInfoRef = userInfoRef
92+
self.logger = context.logger
8893
self.interceptors = ServerInterceptorPipeline(
8994
logger: context.logger,
9095
eventLoop: context.eventLoop,
@@ -102,6 +107,11 @@ public final class BidirectionalStreamingServerHandler<
102107

103108
@inlinable
104109
public func receiveMetadata(_ headers: HPACKHeaders) {
110+
if let extractor = self.context.traceIDExtractor,
111+
let id = headers.first(name: extractor.headerName) {
112+
self.logger[metadataKey: extractor.loggerKey] = "\(id)"
113+
self.interceptors.logger[metadataKey: extractor.loggerKey] = "\(id)"
114+
}
105115
self.interceptors.receive(.metadata(headers))
106116
}
107117

@@ -164,7 +174,7 @@ public final class BidirectionalStreamingServerHandler<
164174
let context = _StreamingResponseCallContext<Request, Response>(
165175
eventLoop: self.context.eventLoop,
166176
headers: headers,
167-
logger: self.context.logger,
177+
logger: self.logger,
168178
userInfoRef: self.userInfoRef,
169179
compressionIsEnabled: self.context.encoding.isEnabled,
170180
closeFuture: self.context.closeFuture,

Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import Logging
1617
import NIOCore
1718
import NIOHPACK
1819

@@ -56,6 +57,9 @@ public final class ClientStreamingServerHandler<
5657
@usableFromInline
5758
internal var state: State = .idle
5859

60+
@usableFromInline
61+
internal var logger: Logger
62+
5963
@usableFromInline
6064
internal enum State {
6165
// Nothing has happened yet.
@@ -86,6 +90,7 @@ public final class ClientStreamingServerHandler<
8690

8791
let userInfoRef = Ref(UserInfo())
8892
self.userInfoRef = userInfoRef
93+
self.logger = context.logger
8994
self.interceptors = ServerInterceptorPipeline(
9095
logger: context.logger,
9196
eventLoop: context.eventLoop,
@@ -103,6 +108,11 @@ public final class ClientStreamingServerHandler<
103108

104109
@inlinable
105110
public func receiveMetadata(_ headers: HPACKHeaders) {
111+
if let extractor = self.context.traceIDExtractor,
112+
let id = headers.first(name: extractor.headerName) {
113+
self.logger[metadataKey: extractor.loggerKey] = "\(id)"
114+
self.interceptors.logger[metadataKey: extractor.loggerKey] = "\(id)"
115+
}
106116
self.interceptors.receive(.metadata(headers))
107117
}
108118

@@ -165,7 +175,7 @@ public final class ClientStreamingServerHandler<
165175
let context = UnaryResponseCallContext<Response>(
166176
eventLoop: self.context.eventLoop,
167177
headers: headers,
168-
logger: self.context.logger,
178+
logger: self.logger,
169179
userInfoRef: self.userInfoRef,
170180
closeFuture: self.context.closeFuture
171181
)

Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import Logging
1617
import NIOCore
1718
import NIOHPACK
1819

@@ -52,6 +53,9 @@ public final class ServerStreamingServerHandler<
5253
@usableFromInline
5354
internal var state: State = .idle
5455

56+
@usableFromInline
57+
internal var logger: Logger
58+
5559
@usableFromInline
5660
internal enum State {
5761
// Initial state. Nothing has happened yet.
@@ -82,6 +86,7 @@ public final class ServerStreamingServerHandler<
8286

8387
let userInfoRef = Ref(UserInfo())
8488
self.userInfoRef = userInfoRef
89+
self.logger = context.logger
8590
self.interceptors = ServerInterceptorPipeline(
8691
logger: context.logger,
8792
eventLoop: context.eventLoop,
@@ -99,6 +104,11 @@ public final class ServerStreamingServerHandler<
99104

100105
@inlinable
101106
public func receiveMetadata(_ headers: HPACKHeaders) {
107+
if let extractor = self.context.traceIDExtractor,
108+
let id = headers.first(name: extractor.headerName) {
109+
self.logger[metadataKey: extractor.loggerKey] = "\(id)"
110+
self.interceptors.logger[metadataKey: extractor.loggerKey] = "\(id)"
111+
}
102112
self.interceptors.receive(.metadata(headers))
103113
}
104114

@@ -161,7 +171,7 @@ public final class ServerStreamingServerHandler<
161171
let context = _StreamingResponseCallContext<Request, Response>(
162172
eventLoop: self.context.eventLoop,
163173
headers: headers,
164-
logger: self.context.logger,
174+
logger: self.logger,
165175
userInfoRef: self.userInfoRef,
166176
compressionIsEnabled: self.context.encoding.isEnabled,
167177
closeFuture: self.context.closeFuture,

Sources/GRPC/CallHandlers/UnaryServerHandler.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import Logging
1617
import NIOCore
1718
import NIOHPACK
1819

@@ -51,6 +52,9 @@ public final class UnaryServerHandler<
5152
@usableFromInline
5253
internal var state: State = .idle
5354

55+
@usableFromInline
56+
internal var logger: Logger
57+
5458
@usableFromInline
5559
internal enum State {
5660
// Initial state. Nothing has happened yet.
@@ -77,6 +81,7 @@ public final class UnaryServerHandler<
7781
self.serializer = responseSerializer
7882
self.deserializer = requestDeserializer
7983
self.context = context
84+
self.logger = context.logger
8085

8186
let userInfoRef = Ref(UserInfo())
8287
self.userInfoRef = userInfoRef
@@ -97,6 +102,11 @@ public final class UnaryServerHandler<
97102

98103
@inlinable
99104
public func receiveMetadata(_ metadata: HPACKHeaders) {
105+
if let extractor = self.context.traceIDExtractor,
106+
let id = metadata.first(name: extractor.headerName) {
107+
self.logger[metadataKey: extractor.loggerKey] = "\(id)"
108+
self.interceptors.logger[metadataKey: extractor.loggerKey] = "\(id)"
109+
}
100110
self.interceptors.receive(.metadata(metadata))
101111
}
102112

@@ -159,7 +169,7 @@ public final class UnaryServerHandler<
159169
let context = UnaryResponseCallContext<Response>(
160170
eventLoop: self.context.eventLoop,
161171
headers: headers,
162-
logger: self.context.logger,
172+
logger: self.logger,
163173
userInfoRef: self.userInfoRef,
164174
closeFuture: self.context.closeFuture
165175
)

Sources/GRPC/GRPCServerPipelineConfigurator.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan
142142
errorDelegate: self.configuration.errorDelegate,
143143
normalizeHeaders: normalizeHeaders,
144144
maximumReceiveMessageLength: self.configuration.maximumReceiveMessageLength,
145-
logger: logger
145+
logger: logger,
146+
traceIDExtractor: self.configuration.traceIDExtractor
146147
)
147148
}
148149

Sources/GRPC/GRPCServerRequestRoutingHandler.swift

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public struct CallHandlerContext {
5959
internal var allocator: ByteBufferAllocator
6060
@usableFromInline
6161
internal var closeFuture: EventLoopFuture<Void>
62+
@usableFromInline
63+
internal var traceIDExtractor: Server.Configuration.TraceIDExtractor?
6264
}
6365

6466
/// A call URI split into components.

Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe
2323
typealias OutboundOut = HTTP2Frame.FramePayload
2424

2525
private var logger: Logger
26+
private let traceIDExtractor: Server.Configuration.TraceIDExtractor?
2627
private var state: HTTP2ToRawGRPCStateMachine
2728
private let errorDelegate: ServerErrorDelegate?
2829
private var context: ChannelHandlerContext!
@@ -73,9 +74,11 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe
7374
errorDelegate: ServerErrorDelegate?,
7475
normalizeHeaders: Bool,
7576
maximumReceiveMessageLength: Int,
76-
logger: Logger
77+
logger: Logger,
78+
traceIDExtractor: Server.Configuration.TraceIDExtractor?
7779
) {
7880
self.logger = logger
81+
self.traceIDExtractor = traceIDExtractor
7982
self.errorDelegate = errorDelegate
8083
self.servicesByName = servicesByName
8184
self.encoding = encoding
@@ -127,7 +130,8 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe
127130
closeFuture: context.channel.closeFuture,
128131
services: self.servicesByName,
129132
encoding: self.encoding,
130-
normalizeHeaders: self.normalizeHeaders
133+
normalizeHeaders: self.normalizeHeaders,
134+
traceIDExtractor: self.traceIDExtractor
131135
)
132136

133137
switch receiveHeaders {

Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift

+12-6
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ extension HTTP2ToRawGRPCStateMachine.State {
277277
closeFuture: EventLoopFuture<Void>,
278278
services: [Substring: CallHandlerProvider],
279279
encoding: ServerMessageEncoding,
280-
normalizeHeaders: Bool
280+
normalizeHeaders: Bool,
281+
traceIDExtractor: Server.Configuration.TraceIDExtractor?
281282
) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveHeadersAction {
282283
// Extract and validate the content type. If it's nil we need to close.
283284
guard let contentType = self.extractContentType(from: headers) else {
@@ -326,7 +327,8 @@ extension HTTP2ToRawGRPCStateMachine.State {
326327
remoteAddress: remoteAddress,
327328
responseWriter: responseWriter,
328329
allocator: allocator,
329-
closeFuture: closeFuture
330+
closeFuture: closeFuture,
331+
traceIDExtractor: traceIDExtractor
330332
)
331333

332334
// We have a matching service, hopefully we have a provider for the method too.
@@ -865,7 +867,8 @@ extension HTTP2ToRawGRPCStateMachine {
865867
closeFuture: EventLoopFuture<Void>,
866868
services: [Substring: CallHandlerProvider],
867869
encoding: ServerMessageEncoding,
868-
normalizeHeaders: Bool
870+
normalizeHeaders: Bool,
871+
traceIDExtractor: Server.Configuration.TraceIDExtractor?
869872
) -> ReceiveHeadersAction {
870873
return self.withStateAvoidingCoWs { state in
871874
state.receive(
@@ -879,7 +882,8 @@ extension HTTP2ToRawGRPCStateMachine {
879882
closeFuture: closeFuture,
880883
services: services,
881884
encoding: encoding,
882-
normalizeHeaders: normalizeHeaders
885+
normalizeHeaders: normalizeHeaders,
886+
traceIDExtractor: traceIDExtractor
883887
)
884888
}
885889
}
@@ -974,7 +978,8 @@ extension HTTP2ToRawGRPCStateMachine.State {
974978
closeFuture: EventLoopFuture<Void>,
975979
services: [Substring: CallHandlerProvider],
976980
encoding: ServerMessageEncoding,
977-
normalizeHeaders: Bool
981+
normalizeHeaders: Bool,
982+
traceIDExtractor: Server.Configuration.TraceIDExtractor?
978983
) -> HTTP2ToRawGRPCStateMachine.ReceiveHeadersAction {
979984
switch self {
980985
// These are the only states in which we can receive headers. Everything else is invalid.
@@ -991,7 +996,8 @@ extension HTTP2ToRawGRPCStateMachine.State {
991996
closeFuture: closeFuture,
992997
services: services,
993998
encoding: encoding,
994-
normalizeHeaders: normalizeHeaders
999+
normalizeHeaders: normalizeHeaders,
1000+
traceIDExtractor: traceIDExtractor
9951001
)
9961002
self = stateAndAction.state
9971003
return stateAndAction.action

Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal final class ServerInterceptorPipeline<Request, Response> {
3636

3737
/// A logger.
3838
@usableFromInline
39-
internal let logger: Logger
39+
internal var logger: Logger
4040

4141
/// A reference to a 'UserInfo'.
4242
@usableFromInline

Sources/GRPC/Server.swift

+24-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Foundation
1717
import Logging
1818
import NIOCore
1919
import NIOExtras
20+
import NIOHPACK
2021
import NIOHTTP1
2122
import NIOHTTP2
2223
import NIOPosix
@@ -272,11 +273,9 @@ extension Server {
272273
return Array(self.serviceProvidersByName.values)
273274
}
274275
set {
275-
self
276-
.serviceProvidersByName = Dictionary(
277-
uniqueKeysWithValues: newValue
278-
.map { ($0.serviceName, $0) }
279-
)
276+
self.serviceProvidersByName = Dictionary(
277+
uniqueKeysWithValues: newValue.map { ($0.serviceName, $0) }
278+
)
280279
}
281280
}
282281

@@ -353,6 +352,10 @@ extension Server {
353352
/// available to service providers via `context`. Defaults to a no-op logger.
354353
public var logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() })
355354

355+
/// Configuration for extracting trace IDs from metadata and injecting the extracted ID into
356+
/// a logger.
357+
public var traceIDExtractor: TraceIDExtractor?
358+
356359
/// A channel initializer which will be run after gRPC has initialized each accepted channel.
357360
/// This may be used to add additional handlers to the pipeline and is intended for debugging.
358361
/// This is analogous to `NIO.ServerBootstrap.childChannelInitializer`.
@@ -451,6 +454,22 @@ extension Server {
451454
}
452455
}
453456

457+
extension Server.Configuration {
458+
/// Configuration for extracting IDs from metadata and inserting them into a logger.
459+
public struct TraceIDExtractor {
460+
/// The name of the header field to extract an ID from. Header lookups are case insensitive.
461+
public var headerName: String
462+
463+
/// Extracted IDs will be set on the logger with this key.
464+
public var loggerKey: String
465+
466+
public init(headerName: String, loggerKey: String) {
467+
self.headerName = headerName
468+
self.loggerKey = loggerKey
469+
}
470+
}
471+
}
472+
454473
extension ServerBootstrapProtocol {
455474
fileprivate func bind(to target: BindTarget) -> EventLoopFuture<Channel> {
456475
switch target.wrapped {

0 commit comments

Comments
 (0)