@instana/core
Version:
Core library for Instana's Node.js packages
267 lines (220 loc) • 8.41 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2018
*/
;
const shimmer = require('../../shimmer');
const hook = require('../../../util/hook');
const tracingUtil = require('../../tracingUtil');
const constants = require('../../constants');
const cls = require('../../cls');
let isActive = false;
exports.spanName = 'redis';
exports.batchable = true;
exports.activate = function activate() {
isActive = true;
};
exports.deactivate = function deactivate() {
isActive = false;
};
exports.init = function init() {
hook.onModuleLoad('ioredis', instrument);
};
function instrument(ioredis) {
shimmer.wrap(ioredis.prototype, 'sendCommand', instrumentSendCommand);
shimmer.wrap(ioredis.prototype, 'multi', instrumentMultiCommand);
shimmer.wrap(ioredis.prototype, 'pipeline', instrumentPipelineCommand);
// TODO: https://jsw.ibm.com/browse/INSTA-14540
// We currently have no multi/pipeline spans for clusters.
// shimmer.wrap(ioredis.Cluster.prototype, 'sendCommand', instrumentSendCommand);
// shimmer.wrap(ioredis.Cluster.prototype, 'multi', instrumentMultiCommand);
// shimmer.wrap(ioredis.Cluster.prototype, 'pipeline', instrumentPipelineCommand);
}
function instrumentSendCommand(original) {
return function wrappedInternalSendCommand(command) {
const client = this;
// We need to skip parentSpan condition because the parentSpan check is too specific in this fn
const skipExitTracingResult = cls.skipExitTracing({ isActive, skipParentSpanCheck: true, extendedResponse: true });
const parentSpan = skipExitTracingResult.parentSpan;
let callback;
if (command.promise == null || typeof command.name !== 'string' || skipExitTracingResult.skip) {
return original.apply(this, arguments);
}
// TODO: Why do we trace each sub command as separate spans (exec, hset, hget etc.)?
// https://jsw.ibm.com/browse/INSTA-14540
// NOTE: there is separate "pipeline" call from "instrumentSendCommand"
// only for "multi". Thats why we filter it out here.
if (
parentSpan &&
parentSpan.n === exports.spanName &&
(parentSpan.data.redis.operation === 'multi' || parentSpan.data.redis.operation === 'pipeline') &&
command.name !== 'multi'
) {
const parentSpanSubCommands = (parentSpan.data.redis.subCommands = parentSpan.data.redis.subCommands || []);
parentSpanSubCommands.push(command.name);
} else if (
// If allowRootExitSpan is not enabled then an EXIT SPAN can't exist alone
(!skipExitTracingResult.allowRootExitSpan && parentSpan && constants.isExitSpan(parentSpan)) ||
!parentSpan
) {
// Apart from the special case of multi/pipeline calls, redis exits can't be child spans of other exits
return original.apply(this, arguments);
}
const argsForOriginal = arguments;
const connection = `${client.options.host}:${client.options.port}`;
// TODO: https://jsw.ibm.com/browse/INSTA-14540
// if (client.isCluster) {
// connection = client.startupNodes.map(node => `${node.host}:${node.port}`).join(',');
return cls.ns.runAndReturn(() => {
const spanData = {
redis: {
connection,
operation: command.name.toLowerCase()
}
};
const span = cls.startSpan({
spanName: exports.spanName,
kind: constants.EXIT,
spanData
});
span.stack = tracingUtil.getStackTrace(wrappedInternalSendCommand);
callback = cls.ns.bind(onResult);
command.promise.then(
// make sure that the first parameter is never truthy
callback.bind(null, null),
callback
);
return original.apply(client, argsForOriginal);
function onResult(error) {
// multi commands are ended by exec. Wait for the exec result
if (command.name === 'multi') {
return;
}
span.d = Date.now() - span.ts;
if (error) {
span.ec = 1;
tracingUtil.setErrorDetails(span, error, 'redis');
}
span.transmit();
}
});
};
}
function instrumentMultiCommand(original) {
return instrumentMultiOrPipelineCommand('multi', original);
}
function instrumentPipelineCommand(original) {
return instrumentMultiOrPipelineCommand('pipeline', original);
}
function instrumentMultiOrPipelineCommand(commandName, original) {
return function wrappedInternalMultiOrPipelineCommand() {
const client = this;
const skipExitTracingResult = cls.skipExitTracing({ isActive, skipParentSpanCheck: true, extendedResponse: true });
const parentSpan = skipExitTracingResult.parentSpan;
// NOTE: multiple redis transaction can have a parent ioredis call
// If cls.skipExitTracing wants to skip the tracing then skip it
// Also if allowRootExitSpan is not enabled then an EXIT SPAN can't exist alone
if (
skipExitTracingResult.skip ||
(!skipExitTracingResult.allowRootExitSpan && parentSpan && constants.isExitSpan(parentSpan)) ||
(!skipExitTracingResult.allowRootExitSpan && !parentSpan)
) {
return original.apply(this, arguments);
}
const connection = `${client.options.host}:${client.options.port}`;
// TODO: https://jsw.ibm.com/browse/INSTA-14540
// if (client.isCluster) {
// connection = client.startupNodes.map(node => `${node.host}:${node.port}`).join(',');
// create a new cls context parent to track the multi/pipeline child calls
const clsContextForMultiOrPipeline = cls.ns.createContext();
cls.ns.enter(clsContextForMultiOrPipeline);
const spanData = {
redis: {
connection,
operation: commandName
}
};
const span = cls.startSpan({
spanName: exports.spanName,
kind: constants.EXIT,
spanData
});
span.stack = tracingUtil.getStackTrace(wrappedInternalMultiOrPipelineCommand);
const multiOrPipeline = original.apply(this, arguments);
shimmer.wrap(
multiOrPipeline,
'exec',
instrumentMultiOrPipelineExec.bind(null, clsContextForMultiOrPipeline, commandName, span)
);
return multiOrPipeline;
};
}
function instrumentMultiOrPipelineExec(clsContextForMultiOrPipeline, commandName, span, original) {
const endCallback = commandName === 'pipeline' ? pipelineCommandEndCallback : multiCommandEndCallback;
return function instrumentedExec() {
// the exec call is actually when the transmission of these commands to
// redis is happening
span.ts = Date.now();
const result = original.apply(this, arguments);
if (result.then) {
result.then(
results => {
endCallback.call(null, clsContextForMultiOrPipeline, span, null, results);
},
error => {
endCallback.call(null, clsContextForMultiOrPipeline, span, error, []);
}
);
}
return result;
};
}
function multiCommandEndCallback(clsContextForMultiOrPipeline, span, error) {
span.d = Date.now() - span.ts;
const subCommands = span.data.redis.subCommands;
let commandCount = 1;
if (subCommands) {
// remove exec call
subCommands.pop();
commandCount = subCommands.length;
}
span.b = {
s: commandCount
};
if (error) {
span.ec = commandCount;
tracingUtil.setErrorDetails(span, error, 'redis');
}
span.transmit();
cls.ns.exit(clsContextForMultiOrPipeline);
}
function pipelineCommandEndCallback(clsContextForMultiOrPipeline, span, error, results) {
span.d = Date.now() - span.ts;
const subCommands = span.data.redis.subCommands;
const commandCount = subCommands ? subCommands.length : 1;
span.b = {
s: commandCount
};
if (error) {
// ioredis docs mention that this should never be possible, but better be safe than sorry
span.ec = commandCount;
tracingUtil.setErrorDetails(span, error, 'redis');
} else {
let numberOfErrors = 0;
let sampledError;
// results is an array of the form
// [[?Error, ?Response]]
for (let i = 0; i < results.length; i++) {
if (results[i][0]) {
numberOfErrors += 1;
sampledError = sampledError || results[i][0];
}
}
if (numberOfErrors > 0) {
span.ec = numberOfErrors;
tracingUtil.setErrorDetails(span, sampledError, 'redis');
}
}
span.transmit();
cls.ns.exit(clsContextForMultiOrPipeline);
}