Skip to content

Commit cf18554

Browse files
iamjochemluin
authored andcommitted
feat: truncate large/long debug output arguments (#523)
* wrap the debug node_module so that debug output args are always truncated (avoids potentially MBs of data being outout to console) * make calling the wrapped debug instance (more or less) a no-op if debug.enabled is currently FALSE
1 parent 8228714 commit cf18554

File tree

8 files changed

+201
-6
lines changed

8 files changed

+201
-6
lines changed

lib/cluster/delay_queue.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
var Deque = require('denque');
4-
var debug = require('debug')('ioredis:delayqueue');
4+
var debug = require('../utils/debug')('ioredis:delayqueue');
55

66
function DelayQueue() {
77
this.queues = {};

lib/cluster/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var Redis = require('../redis');
66
var utils = require('../utils');
77
var util = require('util');
88
var EventEmitter = require('events').EventEmitter;
9-
var debug = require('debug')('ioredis:cluster');
9+
var debug = require('../utils/debug')('ioredis:cluster');
1010
var _ = require('../utils/lodash');
1111
var ScanStream = require('../scan_stream');
1212
var Commander = require('../commander');

lib/connectors/sentinel_connector.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var util = require('util');
55
var net = require('net');
66
var utils = require('../utils');
77
var Connector = require('./connector');
8-
var debug = require('debug')('ioredis:SentinelConnector');
8+
var debug = require('../utils/debug')('ioredis:SentinelConnector');
99
var Redis;
1010

1111
function SentinelConnector(options) {

lib/redis.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var Command = require('./command');
99
var Commander = require('./commander');
1010
var utils = require('./utils');
1111
var eventHandler = require('./redis/event_handler');
12-
var debug = require('debug')('ioredis:redis');
12+
var debug = require('./utils/debug')('ioredis:redis');
1313
var Connector = require('./connectors/connector');
1414
var SentinelConnector = require('./connectors/sentinel_connector');
1515
var ScanStream = require('./scan_stream');

lib/redis/event_handler.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
var debug = require('debug')('ioredis:connection');
3+
var debug = require('../utils/debug')('ioredis:connection');
44
var Command = require('../command');
55
var utils = require('../utils');
66
var _ = require('../utils/lodash');

lib/redis/parser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
var _ = require('../utils/lodash');
44
var Command = require('../command');
55
var SubscriptionSet = require('../subscription_set');
6-
var debug = require('debug')('ioredis:reply');
6+
var debug = require('../utils/debug')('ioredis:reply');
77
var Parser = require('redis-parser');
88
var ReplyError = require('../reply_error');
99

lib/utils/debug.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
var debug = require('debug');
2+
3+
var MAX_ARGUMENT_LENGTH = 200;
4+
5+
/**
6+
* helper function that tried to get a string value for
7+
* arbitrary "debug" arg
8+
*
9+
* @param {mixed} v
10+
* @return {String|undefined}
11+
*/
12+
function getStringValue(v) {
13+
if (v === null)
14+
return;
15+
16+
switch (typeof v) {
17+
case 'boolean': return;
18+
case 'number': return;
19+
20+
case 'object':
21+
if (Buffer.isBuffer(v)) {
22+
return v.toString('hex');
23+
}
24+
25+
if (Array.isArray(v)) {
26+
return v.join(',');
27+
}
28+
29+
try {
30+
return JSON.stringify(v);
31+
} catch (e) {
32+
return;
33+
}
34+
35+
case 'string': return v;
36+
}
37+
}
38+
39+
/**
40+
* helper function that redacts a string representation of a "debug" arg
41+
*
42+
* @param {String} str
43+
* @param {Number} max_len
44+
* @return {String}
45+
*/
46+
function genRedactedString(str, max_len) {
47+
var len = str.length;
48+
49+
return len <= max_len ? str : str.slice(0, max_len) + ' ... <REDACTED full-length="' + len + '">';
50+
}
51+
52+
/**
53+
* a wrapper for the `debug` module, used to generate
54+
* "debug functions" that trim the values in their output
55+
*
56+
* @param {String}
57+
* @return {Function}
58+
*/
59+
module.exports = function genDebugFunction(name) {
60+
61+
var fn = debug(name);
62+
63+
function wrappedDebug() {
64+
if (!fn.enabled) {
65+
return; // no-op
66+
}
67+
68+
var args = [].slice.call(arguments);
69+
var i = 1, l = args.length, str, len;
70+
71+
// we skip the first arg because that is the message
72+
for (; i < l; i += 1) {
73+
str = getStringValue(args[i]);
74+
len = str && str.length || 0;
75+
76+
if (len > MAX_ARGUMENT_LENGTH) {
77+
args[i] = genRedactedString(str, MAX_ARGUMENT_LENGTH);
78+
}
79+
}
80+
81+
return fn.apply(null, args);
82+
}
83+
84+
Object.defineProperties(wrappedDebug, {
85+
namespace: { get: function () {
86+
return fn.namespace;
87+
} },
88+
enabled: { get: function () {
89+
return fn.enabled;
90+
} },
91+
useColors: { get: function () {
92+
return fn.useColors;
93+
} },
94+
color: { get: function () {
95+
return fn.color;
96+
} },
97+
destroy: { get: function () {
98+
return fn.destroy;
99+
} },
100+
log: {
101+
get: function () {
102+
return fn.log;
103+
},
104+
set: function (l) {
105+
fn.log = l;
106+
}
107+
}
108+
109+
});
110+
111+
return wrappedDebug;
112+
};
113+
114+
// expose private stuff for unit-testing
115+
module.exports.MAX_ARGUMENT_LENGTH = MAX_ARGUMENT_LENGTH;
116+
module.exports.getStringValue = getStringValue;
117+
module.exports.genRedactedString = genRedactedString;

test/unit/debug.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
var real_debug = require('debug');
4+
5+
var debug = require('../../lib/utils/debug');
6+
7+
describe('utils/debug', function () {
8+
9+
afterEach(function () {
10+
real_debug.enable(process.env.DEBUG || '');
11+
});
12+
13+
describe('.exports.getStringValue', function () {
14+
it('should return a string or undefined', function () {
15+
expect(debug.getStringValue(true)).to.be.undefined;
16+
expect(debug.getStringValue(undefined)).to.be.undefined;
17+
expect(debug.getStringValue(null)).to.be.undefined;
18+
expect(debug.getStringValue(false)).to.be.undefined;
19+
expect(debug.getStringValue(1)).to.be.undefined;
20+
expect(debug.getStringValue(1.1)).to.be.undefined;
21+
expect(debug.getStringValue(-1)).to.be.undefined;
22+
expect(debug.getStringValue(-1.1)).to.be.undefined;
23+
24+
expect(debug.getStringValue('abc')).to.be.a('string');
25+
expect(debug.getStringValue(Buffer.from ? Buffer.from('abc') : new Buffer('abc'))).to.be.a('string');
26+
expect(debug.getStringValue(new Date())).to.be.a('string');
27+
expect(debug.getStringValue({ foo: { bar: 'qux' } })).to.be.a('string');
28+
});
29+
});
30+
31+
// describe('.exports.genRedactedString', function () {
32+
// it('should return a string, truncated if applicable', function () {
33+
// // TODO: the unit test underneath already tests the actual string truncation logic .
34+
// // .. so this one is not needed?
35+
// });
36+
// });
37+
38+
describe('.exports', function () {
39+
it('should return a function', function () {
40+
expect(debug('test')).to.be.a('function');
41+
});
42+
43+
it('should output to console if DEBUG is set', function () {
44+
var dbg_ns = 'ioredis:debugtest';
45+
46+
real_debug.enable(dbg_ns);
47+
48+
var logspy = spy();
49+
var fn = debug(dbg_ns);
50+
51+
fn.log = logspy;
52+
53+
expect(fn.enabled).to.equal(true);
54+
expect(fn.namespace).to.equal(dbg_ns);
55+
56+
var data = [], i = 0;
57+
58+
while (i < 1000) {
59+
data.push(String(i)); i += 1;
60+
}
61+
62+
var datastr = JSON.stringify(data);
63+
64+
fn('my message %s', { json: data });
65+
expect(logspy.called).to.equal(true);
66+
67+
var args = logspy.getCall(0).args;
68+
69+
var wanted_arglen = 30 // " ... <REDACTED full-length="">"
70+
+ debug.MAX_ARGUMENT_LENGTH // max-length of redacted string
71+
+ datastr.length.toString().length; // length of string of string length (inception much?)
72+
73+
expect(args.length).to.be.above(1);
74+
expect(args[1]).to.be.a('string');
75+
expect(args[1].length).to.equal(wanted_arglen);
76+
});
77+
});
78+
});

0 commit comments

Comments
 (0)