newrelic
Version:
New Relic agent
207 lines (182 loc) • 7.13 kB
JavaScript
/*
* Copyright 2026 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const semver = require('semver')
const Subscriber = require('../base.js')
const shouldTrackError = require('./should-track-error.js')
const Transaction = require('#agentlib/transaction/index.js')
const recordHttp = require('#agentlib/metrics/recorders/http.js')
const { ACTION_DELIMITER } = require('#agentlib/metrics/names.js')
const { DESTINATIONS, TYPES } = Transaction
const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT
const FRAMEWORK = 'gRPC'
module.exports = class GrpcServerSubscriber extends Subscriber {
constructor({ agent, logger }) {
super({
agent,
logger,
packageName: '@grpc/grpc-js',
channelName: 'nr_grpc_server',
})
this.agent.environment.setFramework(FRAMEWORK)
// We instrument the `grpcjs.Server.register` method in order to capture
// the handlers that are being registered. These captured handlers will
// start the transaction. Without `requireActiveTx = false`, our handler
// will never be invoked. We need our handler invoked in order to do the
// capturing.
this.requireActiveTx = false
}
handler(data, ctx) {
const { arguments: _arguments, self: server, moduleVersion } = data
const args = Array.from(_arguments)
const handlerName = args.at(0)
const handlerFn = args.at(1)
const handlerType = args.at(-1)
if (server.handlers.has(handlerName) === true) {
this.logger.debug(
`Not re-instrumenting gRPC method handler for ${handlerName}: it is already registered in the server.`
)
return server.register(...arguments)
}
const self = this
_arguments[1] = function wrappedGrpcHandler(...args) {
let ctx = self.agent.tracer.getContext()
if (ctx.transaction != null) {
// If a transaction already exists, we run the function under that
// transaction.
return handlerFn.apply(server, args)
}
ctx = self.#createTransaction(handlerName, handlerType, ctx)
const transaction = ctx.transaction
const call = args[0]
call.emit = self.agent.tracer.bindFunction(call.emit, ctx, true)
self.#acceptDtHeaders(transaction, call)
if (semver.gte(moduleVersion, '1.10.0') === true) {
self.#instrumentInterceptors(transaction, call)
} else {
self.#instrumentEventListeners(transaction, call)
}
return self.agent.tracer.bindFunction(handlerFn, ctx, true).apply(server, args)
}
return ctx
}
/**
* Reads W3C metadata from the gRPC stream and adds it to the current
* transaction.
*
* @param {Transaction} transaction The current transaction.
* @param {object} stream The gRPC stream.
*/
#acceptDtHeaders(transaction, stream) {
const { metadata } = stream
for (const [key, value] of Object.entries(metadata.getMap())) {
transaction.trace.attributes.addAttribute(
DESTINATION,
`request.headers.${key}`,
value
)
}
const headers = Object.create(null)
headers.tracestate = metadata.get('tracestate').join(',')
headers.traceparent = metadata.get('traceparent').join(',')
headers.newrelic = metadata.get('newrelic').join(',')
transaction.acceptDistributedTraceHeaders('HTTP', headers)
}
/**
* In the case of requests that do not already have an active transaction
* when the handler is invoked, this method is used to create a new one
* with a newly entered {@link TraceSegment}.
*
* @param {string} handlerName The name of the handler. Used to name the
* segment.
* @param {string} handlerType The gRPC server handler type as enumerated at
* https://github.com/grpc/grpc-node/blob/c9f8f93/packages/grpc-js/src/server-call.ts#L397
* @param {AsyncContext} currentCtx The current context handle. It is expected
* that the context will not have a transaction associated with it.
*
* @returns {AsyncContext} A new context to be utilized for the current
* request.
*/
#createTransaction(handlerName, handlerType, currentCtx) {
const transaction = new Transaction(this.agent)
currentCtx = currentCtx.enterTransaction(transaction)
currentCtx = this.createSegment({
name: handlerName,
recorder: recordHttp,
ctx: currentCtx
})
transaction.type = TYPES.WEB
transaction.baseSegment = currentCtx.segment
// Initialize with the handler name until a better name can be derived.
transaction.url = handlerName
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', transaction.url)
transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', transaction.url)
transaction.nameState.setName(
FRAMEWORK,
transaction.verb,
ACTION_DELIMITER,
transaction.url
)
// 'grpc.type' is not required in the spec, but good metadata to include.
transaction.trace.attributes.addAttribute(DESTINATION, 'grpc.type', handlerType)
return currentCtx
}
/**
* Does the same thing as {@link GrpcServerSubscriber.#instrumentInterceptors},
* but for streams that worked as event emitters (i.e. old versions of the
* module).
*
* Note: two listeners are registered as callEnd is emitted before streamEnd.
* Unlike the instrumentInterceptors case where onCallEnd is called last.
*
* @param {Transaction} transaction The current transaction.
* @param {object} handler The gRPC request handler.
*/
#instrumentEventListeners(transaction, handler) {
const { agent } = this
const { config } = agent
handler.call.once('callEnd', function nrCallEnd(statusCode) {
transaction.trace.attributes.addAttribute(
DESTINATION,
'response.status',
statusCode
)
if (shouldTrackError(statusCode, config) === true) {
const error = Error(`gRPC status code ${statusCode}`)
agent.errors.add(transaction, error)
}
})
handler.call.once('streamEnd', function nrStreamEnd() {
transaction.end()
})
}
/**
* Wraps the gRPC stream's `onCallEnd` method in order to capture the
* `response.status` metadata field, log any trackable errors, and end
* the transaction.
*
* @param {Transaction} transaction The current transaction.
* @param {object} handler The gRPC request handler.
*/
#instrumentInterceptors(transaction, handler) {
const { agent } = this
const { config } = agent
const onCallEnd = handler.call.callEventTracker.onCallEnd
handler.call.callEventTracker.onCallEnd = function wrappedOnCallEnd(...args) {
const [{ code: statusCode }] = args
transaction.trace.attributes.addAttribute(
DESTINATION,
'response.status',
statusCode
)
if (shouldTrackError(statusCode, config) === true) {
const error = Error(`gRPC status code ${statusCode}`)
agent.errors.add(transaction, error)
}
transaction.end()
return onCallEnd.apply(handler.call.callEventTracker, args)
}
}
}