UNPKG

@instana/core

Version:
534 lines (452 loc) 15.4 kB
/* * (c) Copyright IBM Corp. 2022 */ 'use strict'; const shimmer = require('../../shimmer'); const hook = require('../../../util/hook'); const tracingUtil = require('../../tracingUtil'); const constants = require('../../constants'); const cls = require('../../cls'); let logger; let Metadata; let isActive = false; const TYPES = { UNARY: 'unary', SERVER_STREAM: 'serverStream', CLIENT_STREAM: 'clientStream', BIDI: 'bidi' }; const ALL_TYPES = [TYPES.UNARY, TYPES.SERVER_STREAM, TYPES.CLIENT_STREAM, TYPES.BIDI]; const TYPES_WITH_CALLBACK = [TYPES.UNARY, TYPES.CLIENT_STREAM]; const TYPES_WITH_CALL_END = [TYPES.SERVER_STREAM, TYPES.BIDI]; exports.init = function (config) { logger = config.logger; hook.onModuleLoad('@grpc/grpc-js', instrumentModule); hook.onFileLoad(/\/@grpc\/grpc-js\/build\/src\/server\.js/, instrumentServer); hook.onFileLoad(/\/@grpc\/grpc-js\/build\/src\/client\.js/, instrumentClient); }; function instrumentModule(grpc) { Metadata = grpc.Metadata; } function instrumentServer(serverModule) { shimmer.wrap(serverModule.Server.prototype, 'register', shimServerRegister); } function instrumentClient(clientModule) { class InstanaWrapper extends clientModule.Client { constructor(address) { super(...arguments); try { const hostAndPort = splitHostPort(address); if (hostAndPort.port && typeof hostAndPort.port === 'number') { hostAndPort.port = hostAndPort.port.toString(); } this._hostAndPort = hostAndPort; } catch (e) { this._hostAndPort = {}; logger.warn(`Failed to parse GRPC-JS destination addresss: ${address}`); } } } clientModule.Client = InstanaWrapper; const fnArr = [ { name: 'makeUnaryRequest', responseStream: false, requestStream: false }, { name: 'makeServerStreamRequest', responseStream: true, requestStream: false }, { name: 'makeClientStreamRequest', responseStream: false, requestStream: true }, { name: 'makeBidiStreamRequest', responseStream: true, requestStream: true } ]; fnArr.forEach(fnObj => { const { name, responseStream, requestStream } = fnObj; shimmer.wrap(clientModule.Client.prototype, name, function (origFn) { return function (method) { const originalArgs = copyArgs(arguments); const skipTracingResult = cls.skipExitTracing({ isActive, extendedResponse: true }); if (skipTracingResult.skip) { if (skipTracingResult.suppressed) { modifyArgs(name, originalArgs, null); return origFn.apply(this, originalArgs); } return origFn.apply(this, arguments); } /** * https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/client.ts#L281 */ return instrumentedClientMethod( this, origFn, originalArgs, this._hostAndPort, method, requestStream, responseStream, (args, span) => modifyArgs(name, args, span) ); }; }); }); } // See https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/client.ts function modifyArgs(name, originalArgs, span) { const wrapCallback = (newArgs, originalCb) => { newArgs[newArgs.length - 1] = cls.ns.bind(function (err) { span.d = Date.now() - span.ts; if (err) { const errorMessage = err.details || err.message; if (typeof errorMessage === 'string' && errorMessage.toLowerCase().includes('cancelled')) { // No-op, we do not want to mark cancelled calls as erroneous. } else { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'rpc'); } } span.transmit(); originalCb.apply(this, arguments); }); }; const setInstanaHeaders = metadata => { if (metadata && metadata.set) { // NOTE: if the span is null, the tracing is suppressed if (span) { metadata.set(constants.spanIdHeaderName, span.s); metadata.set(constants.traceIdHeaderName, span.t); metadata.set(constants.traceLevelHeaderName, '1'); } else { metadata.set(constants.traceLevelHeaderName, '0'); } } }; const checkMetadataOptionsAndCallback = (method, serialize, deserialize, argument, metadata, options, callback) => { const arg1 = metadata; const arg2 = options; const arg3 = callback; let newMetadata; let newOptions; let newCallback; if (typeof arg1 === 'function') { newMetadata = new Metadata(); newCallback = arg1; originalArgs[originalArgs.length - 1] = newMetadata; originalArgs.push(newCallback); } else if (typeof arg2 === 'function') { if (arg1 instanceof Metadata) { newMetadata = arg1; newCallback = arg2; originalArgs[originalArgs.length - 2] = newMetadata; originalArgs[originalArgs.length - 1] = newCallback; } else { newMetadata = new Metadata(); newOptions = arg1; newCallback = arg2; originalArgs[originalArgs.length - 2] = newMetadata; originalArgs[originalArgs.length - 1] = newOptions; originalArgs.push(newCallback); } } else { // CASE: makeUnaryCall(...., metadata) -> no callback if (arg1 instanceof Metadata && !arg2 && !arg3) { setInstanaHeaders(arg1, span); return; } // NOTE: Do nothing, because the format we expect is different if (!(arg1 instanceof Metadata && arg2 instanceof Object && typeof arg3 === 'function')) { return; } newMetadata = arg1; newOptions = arg2; newCallback = arg3; } if (span) { wrapCallback(originalArgs, newCallback); } setInstanaHeaders(newMetadata, span); }; const checkMetadataAndOptions = (method, serialize, deserialize, argument, metadata, options) => { const arg1 = metadata; const arg2 = options; let newMetadata; let newOptions; if (arg1 instanceof Metadata) { newMetadata = arg1; } else if (!arg1 && !arg2) { newOptions = {}; newMetadata = new Metadata(); originalArgs.push(newMetadata); originalArgs.push(newOptions); } else { newOptions = arg1; newMetadata = new Metadata(); originalArgs[originalArgs.length - 1] = newMetadata; originalArgs.push(newOptions); } setInstanaHeaders(newMetadata, span); }; if (name === 'makeClientStreamRequest') { return checkMetadataOptionsAndCallback( originalArgs[0], originalArgs[1], originalArgs[2], null, originalArgs[3], originalArgs[4], originalArgs[5] ); } if (name === 'makeUnaryRequest') { return checkMetadataOptionsAndCallback(...originalArgs); } return checkMetadataAndOptions(...originalArgs); } function copyArgs(args) { const originalArgs = new Array(args.length); for (let i = 0; i < args.length; i++) { originalArgs[i] = args[i]; } return originalArgs; } function shimServerRegister(originalFunction) { return function (name, handler, serialize, deserialize, type) { // NOTE: Ensure any new types won't run through our instrumention if (ALL_TYPES.indexOf(type) < 0) { logger.warn(`Failed to instrument GRPC-JS entry ${name}, type is unsupported: ${type}`); return originalFunction.apply(this, arguments); } const originalArgs = copyArgs(arguments); /** * Override the original handler e.g. `function unaryCall` (see server.js) * to instrument the incoming server call for the target type. * * name e.g. /instana.node.grpc.test.TestService/MakeUnaryCal * type e.g. unary */ const originalHandler = originalArgs[1]; originalArgs[1] = createInstrumentedServerHandler(name, type, originalHandler); return originalFunction.apply(this, originalArgs); }; } function createInstrumentedServerHandler(name, type, originalHandler) { return function (call) { const originalThis = this; const originalArgs = arguments; const parentSpan = cls.getCurrentSpan(); /* * The deprecated grpc module has used the server implementation in c++. * That's why we have not auto instrumented the incoming http 2 server call. * The `parentSpan` was always empty. * * But for grpc-js we autoinstrument the incoming http2 call. * The `parentSpan` for grpc-js is always the parent http2 server entry span. * We want to use the concept of priorisation and cancel the parent span. * * Any other parent span which is not node.http.server should result in an error, * because it signalises that something wrong happend. Usually this case * should not happen. * * Note that cancelling the incoming HTTP entry span does not reliably work when * the grpc-js call is cancelled quickly on the client side because the grpc-js server * instrumentation might never be triggered. In that case we end up with an HTTP * entry span from the connection attempt made by the grpc-js client before the call * is cancelled. */ if (parentSpan) { if (parentSpan.n !== 'node.http.server') { logger.warn( // eslint-disable-next-line prefer-template 'Cannot start a GRPC-JS entry span when another span is already active. Currently, the following span is ' + 'active: ' + JSON.stringify(parentSpan) ); return originalHandler.apply(originalThis, originalArgs); } parentSpan.cancel(); } return cls.ns.runAndReturn(() => { const metadata = call.metadata; const level = readMetadata(metadata, constants.traceLevelHeaderName); if (level === '0') { cls.setTracingLevel('0'); } if (!isActive || cls.tracingSuppressed()) { return originalHandler.apply(originalThis, originalArgs); } cls.ns.bindEmitter(call); const incomingTraceId = readMetadata(metadata, constants.traceIdHeaderName); const incomingSpanId = readMetadata(metadata, constants.spanIdHeaderName); const span = cls.startSpan({ spanName: 'rpc-server', kind: constants.ENTRY, traceId: incomingTraceId, parentSpanId: incomingSpanId }); span.data.rpc = { call: dropLeadingSlash(name), flavor: 'grpc' }; if (TYPES_WITH_CALLBACK.indexOf(type) >= 0) { const originalCallback = originalArgs[1]; originalArgs[1] = cls.ns.bind(function (err) { if (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'rpc'); } span.d = Date.now() - span.ts; span.transmit(); return originalCallback.apply(this, arguments); }); } if (TYPES_WITH_CALL_END.indexOf(type) >= 0) { const originalEnd = call.end; call.end = function () { span.d = Date.now() - span.ts; process.nextTick(() => { // If the server emits an error, grpc calls call.end before the 'error' event handlers are processed, so we // give on('error') a chance to fire and mark the span erroneous before transmitting it. span.transmit(); }); return originalEnd.apply(this, arguments); }; call.on('error', err => { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'rpc'); }); call.on('cancelled', () => { span.d = Date.now() - span.ts; span.transmit(); }); } return originalHandler.apply(originalThis, originalArgs); }); }; } function instrumentedClientMethod( ctx, originalFunction, originalArgs, hostAndPort, rpcPath, requestStream, responseStream, modifyArgsFn ) { return cls.ns.runAndReturn(() => { const span = cls.startSpan({ spanName: 'rpc-client', kind: constants.EXIT }); span.ts = Date.now(); span.stack = tracingUtil.getStackTrace(instrumentedClientMethod); span.data.rpc = { host: hostAndPort.host, port: hostAndPort.port, call: dropLeadingSlash(rpcPath), flavor: 'grpc' }; modifyArgsFn(originalArgs, span); const call = originalFunction.apply(ctx, originalArgs); if (requestStream || responseStream) { cls.ns.bindEmitter(call); } if (responseStream) { call.on('end', () => { span.d = Date.now() - span.ts; span.transmit(); }); call.on('error', err => { span.d = Date.now() - span.ts; const errorMessage = err.details || err.message; if (typeof errorMessage === 'string' && errorMessage.toLowerCase().includes('cancelled')) { // No-op, we do not want to mark cancelled calls as erroneous. } else { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'rpc'); } span.transmit(); }); } return call; }); } function readMetadata(metadata, key) { // The grpc library normalizes keys internally to lower-case, so we do not need to take care of reading // them case-insensitive ourselves. const values = metadata.get(key); if (values && values.length > 0) { return values[0]; } return null; } // Copied from https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/uri-parser.ts const NUMBER_REGEX = /^\d+$/; function splitHostPort(path) { if (typeof path !== 'string') { return { host: null, port: null }; } if (path.startsWith('[')) { const hostEnd = path.indexOf(']'); if (hostEnd === -1) { return { host: null, port: null }; } const host = path.substring(1, hostEnd); /* Only an IPv6 address should be in bracketed notation, and an IPv6 * address should have at least one colon */ if (host.indexOf(':') === -1) { return { host: null, port: null }; } if (path.length > hostEnd + 1) { if (path[hostEnd + 1] === ':') { const portString = path.substring(hostEnd + 2); if (NUMBER_REGEX.test(portString)) { return { host: host, port: +portString }; } else { return { host: null, port: null }; } } else { return { host: null, port: null }; } } else { return { host, port: null }; } } else { const splitPath = path.split(':'); /* Exactly one colon means that this is host:port. Zero colons means that * there is no port. And multiple colons means that this is a bare IPv6 * address with no port */ if (splitPath.length === 2) { if (NUMBER_REGEX.test(splitPath[1])) { return { host: splitPath[0], port: +splitPath[1] }; } else { return { host: null, port: null }; } } else { return { host: path, port: null }; } } } function dropLeadingSlash(rpcPath) { if (typeof rpcPath === 'string') { if (rpcPath[0] === '/') { return rpcPath.substr(1); } return rpcPath; } return 'unknown'; } // NOTE: Only exposed for tests. Not nice, but acceptable exports.modifyArgs = modifyArgs; exports.instrumentModule = instrumentModule; exports.activate = function () { isActive = true; }; exports.deactivate = function () { isActive = false; };