Skip to content

Commit 057fca2

Browse files
authored
chore: Refactored grpc-js client instrumentation to subscriber type (#3856)
1 parent 9156a6a commit 057fca2

File tree

12 files changed

+228
-171
lines changed

12 files changed

+228
-171
lines changed

lib/context-manager/context.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ module.exports = class Context {
4848
* @param {object} params to function
4949
* @param {TraceSegment} params.segment segment to bind to context
5050
* @param {Transaction} params.transaction active transaction
51-
* @returns {Context} a newly constructed context
51+
* @returns {AsyncContext} a newly constructed context
5252
*/
5353
enterSegment({ segment, transaction = this.transaction }) {
5454
return new this.constructor({ transaction, segment, extras: this.extras })

lib/instrumentation/grpc-js/grpc.js

Lines changed: 0 additions & 115 deletions
This file was deleted.

lib/instrumentation/grpc-js/nr-hooks.js

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/instrumentations.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const InstrumentationDescriptor = require('./instrumentation-descriptor')
1111
module.exports = function instrumentations() {
1212
return {
1313
'@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
14-
'@grpc/grpc-js': { module: './instrumentation/grpc-js' },
1514
'@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
1615
'@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
1716
'@prisma/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },

lib/subscribers/base.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,17 @@ class Subscriber {
182182

183183
/**
184184
* Creates a segment with a name, parent, transaction and optional recorder.
185-
* If the segment is successfully created, it will be started and added to the context.
185+
* If the segment is successfully created, it will be started and added to the
186+
* context.
187+
*
186188
* @param {object} params - Parameters for creating the segment
187189
* @param {string} params.name - The name of the segment
188190
* @param {object} [params.recorder] - Optional recorder for the segment
189-
* @param {Context} params.ctx - The context containing the parent segment and transaction
190-
* @returns {Context} - The updated context with the new segment or existing context if segment creation fails
191+
* @param {Context} params.ctx - The context containing the parent segment and
192+
* transaction
193+
*
194+
* @returns {AsyncContext} - The updated context with the new segment or
195+
* existing context if segment creation fails
191196
*/
192197
createSegment({ name, recorder, ctx }) {
193198
const parent = ctx?.segment
@@ -210,8 +215,7 @@ class Subscriber {
210215
segment.start()
211216
this.logger.trace('Created segment %s, returning new context', name)
212217
this.addAttributes(segment)
213-
const newCtx = ctx.enterSegment({ segment })
214-
return newCtx
218+
return ctx.enterSegment({ segment })
215219
} else {
216220
this.logger.trace('Failed to create segment for %s, returning existing context', name)
217221
return ctx
@@ -232,7 +236,7 @@ class Subscriber {
232236
* This is defined on base to fulfill those use cases.
233237
* @param {SubscriberHandlerData} data event passed to handler
234238
* @param {SubscriberHandlerContext} ctx context passed to handler
235-
* @returns {Context} either new context or existing
239+
* @returns {AsyncContext} either new context or existing
236240
*/
237241
handler(data, ctx) {
238242
return ctx

lib/subscribers/grpcjs/client.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const Subscriber = require('../base.js')
9+
const shouldTrackError = require('./should-track-error.js')
10+
const recordExternal = require('#agentlib/metrics/recorders/http_external.js')
11+
12+
module.exports = class GrpcClientSubscriber extends Subscriber {
13+
constructor({ agent, logger, channelName }) {
14+
super({
15+
agent,
16+
logger,
17+
channelName,
18+
packageName: '@grpc/grpc-js',
19+
})
20+
21+
this.opaque = true
22+
}
23+
24+
handler(data, ctx) {
25+
const { arguments: args, self: client } = data
26+
const { transaction } = ctx
27+
28+
const { channel } = client
29+
const authority = channel.target?.path || channel.getDefaultAuthority
30+
// In grpc-js>=1.8.0 `.methodName` becomes `.method`.
31+
const method = client.methodName || client.method
32+
33+
const newCtx = this.createSegment({
34+
name: `External/${authority}${method}`,
35+
recorder: recordExternal(authority, 'gRPC'),
36+
ctx
37+
})
38+
39+
// Acquire the original parameters to the handler, create patched
40+
// versions of them, and update the parameters list with the patched
41+
// instances.
42+
const origMetadata = args[0]
43+
const origListener = args[1]
44+
const nrMetadata = origMetadata.clone()
45+
const nrListener = Object.assign({}, origListener)
46+
args[0] = nrMetadata
47+
args[1] = nrListener
48+
49+
this.#addDistributedTraceHeaders(transaction, nrMetadata)
50+
this.#wrapListenerCallback(nrListener, origListener, authority, method, newCtx)
51+
52+
return newCtx
53+
}
54+
55+
/**
56+
* When distributed tracing (DT) is enabled, pull DT headers from the
57+
* provided transaction and add them to the metadata object used by gRPC
58+
* when delivering data.
59+
*
60+
* @param {Transaction} transaction The transaction that contains the DT data.
61+
* @param {object} destination The gRPC metadata object to add headers to.
62+
* This object will be mutated.
63+
*/
64+
#addDistributedTraceHeaders(transaction, destination) {
65+
if (this.agent.config.distributed_tracing.enabled === false) {
66+
this.logger.debug('Distributed tracing disabled by instrumentation.')
67+
return
68+
}
69+
70+
const outboundAgentHeaders = Object.create(null)
71+
transaction.insertDistributedTraceHeaders(outboundAgentHeaders)
72+
for (const [key, value] of Object.entries(outboundAgentHeaders)) {
73+
destination.add(key, value)
74+
}
75+
}
76+
77+
/**
78+
* When the original gRPC "listener" object has an `onReceiveStatus` method
79+
* defined, wrap that method to capture tracing data and attach the wrapped
80+
* method to the patched `nrListener` instance.
81+
*
82+
* @param {object} nrListener New Relic clone of the original listener.
83+
* @param {object} origListener Original gRPC listener object.
84+
* @param {string} authority Value of the `:authority` header.
85+
* @param {string} method The gRPC method name to be invoked.
86+
* @param {AsyncContext} ctx The context to execute the `onReceiveStatus`
87+
* handler under.
88+
*/
89+
#wrapListenerCallback(nrListener, origListener, authority, method, ctx) {
90+
if (typeof origListener?.onReceiveStatus !== 'function') {
91+
return
92+
}
93+
94+
const agent = this.agent
95+
const [hostname, port] = authority.split(':')
96+
97+
nrListener.onReceiveStatus = function nrOnReceiveStatus(status) {
98+
const { segment, transaction } = ctx
99+
const { code, details } = status
100+
segment.addAttribute('grpc.statusCode', code)
101+
segment.addAttribute('grpc.statusText', details)
102+
segment.addAttribute('component', 'gRPC')
103+
104+
if (shouldTrackError(code, agent.config) === true) {
105+
agent.errors.add(transaction, details)
106+
}
107+
108+
const protocol = 'grpc:'
109+
segment.captureExternalAttributes({
110+
protocol,
111+
host: authority,
112+
port,
113+
hostname,
114+
method,
115+
path: method
116+
})
117+
118+
const boundFn = agent.tracer.bindFunction(
119+
origListener.onReceiveStatus,
120+
ctx,
121+
true
122+
)
123+
boundFn(status)
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)