lib/instrumentation/modules/redis.js (135 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
'use strict';
var semver = require('semver');
const constants = require('../../constants');
var shimmer = require('../shimmer');
var { getDBDestination } = require('../context');
const isWrappedRedisCbSym = Symbol('ElasticAPMIsWrappedRedisCb');
const TYPE = 'db';
const SUBTYPE = 'redis';
const ACTION = 'query';
module.exports = function (redis, agent, { version, enabled }) {
if (!enabled) {
return redis;
}
if (!semver.satisfies(version, '>=2.0.0 <4.0.0')) {
// Explicitly do not log.debug here, because the message is misleading for
// redis@4 and later that is being handled by @redis/client instrumentation.
return redis;
}
const ins = agent._instrumentation;
// The undocumented field on a RedisClient instance on which connection
// options are stored has changed a few times.
//
// - >=2.4.0: `client.connection_options.{host,port}`, commit eae5596a
// - >=2.3.0, <2.4.0: `client.connection_option.{host,port}`, commit d454e402
// - >=0.12.0, <2.3.0: `client.connectionOption.{host,port}`, commit 064260d1
// - <0.12.0: *maybe* `client.{host,port}`
const connOptsFromRedisClient = (rc) =>
rc.connection_options || rc.connection_option || rc.connectionOption || {};
var proto = redis.RedisClient && redis.RedisClient.prototype;
if (semver.satisfies(version, '>2.5.3')) {
agent.logger.debug(
'shimming redis.RedisClient.prototype.internal_send_command',
);
shimmer.wrap(proto, 'internal_send_command', wrapInternalSendCommand);
} else {
agent.logger.debug('shimming redis.RedisClient.prototype.send_command');
shimmer.wrap(proto, 'send_command', wrapSendCommand);
}
return redis;
function makeWrappedCallback(spanRunContext, span, origCb) {
const wrappedCallback = ins.bindFunctionToRunContext(
spanRunContext,
function (err, _reply) {
if (err) {
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE);
agent.captureError(err, { skipOutcome: true });
}
span.end();
if (origCb) {
return origCb.apply(this, arguments);
}
},
);
wrappedCallback[isWrappedRedisCbSym] = true;
return wrappedCallback;
}
function wrapInternalSendCommand(original) {
return function wrappedInternalSendCommand(commandObj) {
if (!commandObj || typeof commandObj.command !== 'string') {
// Unexpected usage. Skip instrumenting this call.
return original.apply(this, arguments);
}
if (commandObj.callback && commandObj.callback[isWrappedRedisCbSym]) {
// Avoid re-wrapping internal_send_command called *again* for commands
// queued before the client was "ready".
return original.apply(this, arguments);
}
const command = commandObj.command;
agent.logger.debug(
{ command },
'intercepted call to RedisClient.prototype.internal_send_command',
);
const span = ins.createSpan(
command.toUpperCase(),
TYPE,
SUBTYPE,
ACTION,
{ exitSpan: true },
);
if (!span) {
return original.apply(this, arguments);
}
const connOpts = connOptsFromRedisClient(this);
span._setDestinationContext(
getDBDestination(connOpts.host, connOpts.port),
);
span.setDbContext({ type: 'redis' });
const spanRunContext = ins.currRunContext().enterSpan(span);
commandObj.callback = makeWrappedCallback(
spanRunContext,
span,
commandObj.callback,
);
return ins.withRunContext(spanRunContext, original, this, ...arguments);
};
}
function wrapSendCommand(original) {
return function wrappedSendCommand(command, args, cb) {
if (typeof command !== 'string') {
// Unexpected usage. Skip instrumenting this call.
return original.apply(this, arguments);
}
let origCb = cb;
if (
!origCb &&
Array.isArray(args) &&
typeof args[args.length - 1] === 'function'
) {
origCb = args[args.length - 1];
}
if (origCb && origCb[isWrappedRedisCbSym]) {
// Avoid re-wrapping send_command called *again* for commands queued
// before the client was "ready".
return original.apply(this, arguments);
}
agent.logger.debug(
{ command },
'intercepted call to RedisClient.prototype.send_command',
);
var span = ins.createSpan(command.toUpperCase(), TYPE, SUBTYPE, ACTION, {
exitSpan: true,
});
if (!span) {
return original.apply(this, arguments);
}
const connOpts = connOptsFromRedisClient(this);
span._setDestinationContext(
getDBDestination(connOpts.host, connOpts.port),
);
span.setDbContext({ type: 'redis' });
const spanRunContext = ins.currRunContext().enterSpan(span);
const wrappedCb = makeWrappedCallback(spanRunContext, span, origCb);
if (cb) {
cb = wrappedCb;
} else if (origCb) {
args[args.length - 1] = wrappedCb;
} else {
cb = wrappedCb;
}
return ins.withRunContext(
spanRunContext,
original,
this,
command,
args,
cb,
);
};
}
};