/******************************************************************************* * Copyright 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ 'use strict'; var Probe = require('../lib/probe.js'); var aspect = require('../lib/aspect.js'); var request = require('../lib/request.js'); var am = require('../'); var util = require('util'); /** * Probe to instrument the redis npm. * * Events are fired for calls to the basic commands either in upper or lower case * but are always reported as lower case to keep the two sets of events consistent. * * Calls to batch.exec() and multi.exec() are also instrumented. Their events are * batch.exec and multi.exec to ensure they can be separately tracked. */ function RedisProbe() { Probe.call(this, 'redis'); } util.inherits(RedisProbe, Probe); RedisProbe.prototype.attach = function(name, target) { var that = this; if (name != 'redis') return; if (target.__probeAttached__) return target; target.__probeAttached__ = true; var methods = []; [ 'APPEND', 'AUTH', 'BGREWRITEAOF', 'BGSAVE', 'BITCOUNT', 'BITOP', 'BITPOS', 'BLPOP', 'BRPOP', 'BRPOPLPUSH', 'CLIENT', 'CLUSTER', 'COMMAND', 'CONFIG', 'DBSIZE', 'DEBUG', 'DECR', 'DECRBY', 'DEL', 'DISCARD', 'DUMP', 'ECHO', 'EVAL', 'EVALSHA', 'EXISTS', 'EXPIRE', 'EXPIREAT', 'FLUSHALL', 'FLUSHDB', 'GEOADD', 'GEOHASH', 'GEOPOS', 'GEODIST', 'GEORADIUS', 'GEORADIUSBYMEMBER', 'GET', 'GETBIT', 'GETRANGE', 'GETSET', 'HDEL', 'HEXISTS', 'HGET', 'HGETALL', 'HINCRBY', 'HINCRBYFLOAT', 'HKEYS', 'HLEN', 'HMGET', 'HMSET', 'HSET', 'HSETNX', 'HSTRLEN', 'HVALS', 'INCR', 'INCRBY', 'INCRBYFLOAT', 'INFO', 'KEYS', 'LASTSAVE', 'LINDEX', 'LINSERT', 'LLEN', 'LPOP', 'LPUSH', 'LPUSHX', 'LRANGE', 'LREM', 'LSET', 'LTRIM', 'MGET', 'MIGRATE', 'MONITOR', 'MOVE', 'MSET', 'MSETNX', 'OBJECT', 'PERSIST', 'PEXPIRE', 'PEXPIREAT', 'PFADD', 'PFCOUNT', 'PFMERGE', 'PING', 'PSETEX', 'PSUBSCRIBE', 'PUBSUB', 'PTTL', 'PUBLISH', 'PUNSUBSCRIBE', 'QUIT', 'RANDOMKEY', 'RENAME', 'RENAMENX', 'RESTORE', 'ROLE', 'RPOP', 'RPOPLPUSH', 'RPUSH', 'RPUSHX', 'SADD', 'SAVE', 'SCARD', 'SCRIPT', 'SDIFF', 'SDIFFSTORE', 'SELECT', 'SET', 'SETBIT', 'SETEX', 'SETNX', 'SETRANGE', 'SHUTDOWN', 'SINTER', 'SINTERSTORE', 'SISMEMBER', 'SLAVEOF', 'SLOWLOG', 'SMEMBERS', 'SMOVE', 'SORT', 'SPOP', 'SRANDMEMBER', 'SREM', 'STRLEN', 'SUBSCRIBE', 'SUNION', 'SUNIONSTORE', 'SYNC', 'TIME', 'TTL', 'TYPE', 'UNSUBSCRIBE', 'UNWATCH', 'WAIT', 'WATCH', 'ZADD', 'ZCARD', 'ZCOUNT', 'ZINCRBY', 'ZINTERSTORE', 'ZLEXCOUNT', 'ZRANGE', 'ZRANGEBYLEX', 'ZREVRANGEBYLEX', 'ZRANGEBYSCORE', 'ZRANK', 'ZREM', 'ZREMRANGEBYLEX', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE', 'ZREVRANGE', 'ZREVRANGEBYSCORE', 'ZREVRANK', 'ZSCORE', 'ZUNIONSTORE', 'SCAN', 'SSCAN', 'HSCAN', 'ZSCAN', ].map(function(m) { methods.push(m); methods.push(m.toLowerCase()); }); /* Instrument the basic set of asynchronous calls. */ aspect.around( target.RedisClient.prototype, methods, function(target, method, methodArgs, probeData) { var eventName = method.toLowerCase(); that.metricsProbeStart(probeData, eventName, methodArgs); that.requestProbeStart(probeData, eventName, methodArgs); /* REDIS commands don't have to have a callback. * All redis calls are asynchronous so we need to instrument or add a * callback to stop the timer. */ aspect.aroundCallback(methodArgs, probeData, function(target, args) { var callbackPosition = aspect.findCallbackArg(methodArgs); that.metricsProbeEnd(probeData, eventName, methodArgs); that.requestProbeEnd(probeData, eventName, methodArgs); }); }, function(target, method, methodArgs, probeData, rc) { if (aspect.findCallbackArg(methodArgs) == undefined) { var eventName = method.toLowerCase(); that.metricsProbeEnd(probeData, eventName, methodArgs); that.requestProbeEnd(probeData, eventName, methodArgs); } } ); /* Monitor all calls made as one batch/multi.exec call as a single event. * Instrument the exec method on the object returned from client.batch() * or client.multi() */ aspect.after(target.RedisClient.prototype, ['multi', 'batch', 'MULTI', 'BATCH'], {}, function( target, mode, args, probeData, client ) { // Log the event name as batch.exec or multi.exec var eventName = mode.toLowerCase() + '.exec'; aspect.around( client, ['exec', 'EXEC'], function(target, method, methodArgs, probeData) { that.metricsProbeStart(probeData, eventName, methodArgs); that.requestProbeStart(probeData, eventName, methodArgs); /* REDIS commands don't have to have a callback. * All redis calls are asynchronous so we need to instrument or add a * callback to stop the timer. */ aspect.aroundCallback(methodArgs, probeData, function() { that.metricsProbeEnd(probeData, eventName, methodArgs); that.requestProbeEnd(probeData, eventName, methodArgs); }); }, function(target, method, methodArgs, probeData, rc) { if (aspect.findCallbackArg(methodArgs) == undefined) { that.metricsProbeEnd(probeData, eventName, methodArgs); that.requestProbeEnd(probeData, eventName, methodArgs); } } ); return client; }); return target; }; /* * Lightweight metrics probe for REDIS requests * * These provide: * time: time event started * cmd: REDIS method, eg. GET, SET, INCR, etc * duration: the time for the request to respond */ RedisProbe.prototype.metricsEnd = function(probeData, cmd, methodArgs) { if (probeData && probeData.timer) { probeData.timer.stop(); am.emit('redis', { time: probeData.timer.startTimeMillis, cmd: cmd, duration: probeData.timer.timeDelta, }); } }; /* * Heavyweight request probes for redis requests * */ RedisProbe.prototype.requestStart = function(probeData, cmd, methodArgs) { probeData.req = request.startRequest('redis', cmd, false, probeData.timer); }; RedisProbe.prototype.requestEnd = function(probeData, cmd, methodArgs) { if (probeData && probeData.req) { var context = {}; context.cmd = cmd; probeData.req.stop(context); } }; module.exports = RedisProbe;