Merge pull request #36 from spenceralger/trace_logging

Trace logging
This commit is contained in:
spenceralger
2014-01-28 16:09:14 -08:00
36 changed files with 221 additions and 147 deletions

View File

@ -154,6 +154,16 @@ Options:
`path`:::
`String` -- Location of the file to write log messages to. It is created if it does not exists. Default is `"elasticsearch.log"`
`"tracer"`::
Logs in a format that can be executed with bash, where everything is commented except the trace commands which are formatted as curl calls. By default all of the urls are rewritten to protect production systems and to making the scripts easier to reuse/send to other people. In order to control the urls written specify the curlHost and curlPort configs.
+
Options:
`curlHost`:::
`Sting` -- Default is `"localhost"`.
`curlPort`:::
`Sting` -- Default is `9200`.
`"stream"`::
Send log messages to a <a href="http://nodejs.org/api/stream.html#stream_class_stream_writable">WriteableStream</a>
+

View File

@ -1,6 +1,6 @@
module.exports = {
unit: {
src: 'test/unit'
src: 'test/unit/index.js'
},
integration_master: {

View File

@ -31,7 +31,7 @@ fi
if [[ "$NODE_UNIT" != "0" ]]; then
group "running unit tests"
if [[ -n "$JENKINS" ]]; then
$MOCHA test/unit/tests/*.js --reporter $MOCHA_REPORTER 2> test/junit-node-unit.xml
$MOCHA test/unit/index.js --reporter $MOCHA_REPORTER 2> test/junit-node-unit.xml
if [ "$?" -gt "0" ]; then
echo "non-zero exit code: $RESULT"
cat test/junit-node-unit.xml

View File

@ -269,45 +269,32 @@ Log.prototype.debug = function (/* ...msg */) {
*/
Log.prototype.trace = function (method, requestUrl, body, responseBody, responseStatus) {
if (this.listenerCount('trace')) {
if (typeof requestUrl === 'string') {
requestUrl = url.parse(requestUrl, true, true);
} else if (requestUrl.path) {
requestUrl.query = url.parse(requestUrl.path, true, false).query;
}
requestUrl = _.defaults({
host: 'localhost:9200',
query: _.defaults(requestUrl.query || {}, {
pretty: true
})
}, requestUrl);
delete requestUrl.auth;
if (!requestUrl.pathname && requestUrl.path) {
requestUrl.pathname = requestUrl.path.split('?').shift();
}
requestUrl = url.format(requestUrl);
var message = '<- ' + responseStatus + '\n' + prettyJSON(responseBody);
/* jshint quotmark: double */
var curlCall = "curl '" + requestUrl.replace(/'/g, "\\'") + "' -X" + method.toUpperCase();
if (body) {
curlCall += " -d '" + prettyJSON(body) + "'";
}
/* jshint quotmark: single */
return this.emit('trace', message, curlCall);
return this.emit('trace', Log.normalizeTraceArgs(method, requestUrl, body, responseBody, responseStatus));
}
};
function prettyJSON(body) {
try {
return JSON.stringify(JSON.parse(body), null, ' ').replace(/'/g, '\\u0027');
} catch (e) {
return body || '';
Log.normalizeTraceArgs = function (method, requestUrl, body, responseBody, responseStatus) {
if (typeof requestUrl === 'string') {
requestUrl = url.parse(requestUrl, true, true);
} else {
requestUrl = _.clone(requestUrl);
if (requestUrl.path) {
requestUrl.query = url.parse(requestUrl.path, true, false).query;
}
if (!requestUrl.pathname && requestUrl.path) {
requestUrl.pathname = requestUrl.path.split('?').shift();
}
}
}
delete requestUrl.auth;
return {
method: method,
url: url.format(requestUrl),
body: body,
status: responseStatus,
response: responseBody
};
};
module.exports = Log;

View File

@ -38,7 +38,7 @@ LoggerAbstract.prototype.timestamp = function () {
function indent(text, spaces) {
var space = _.repeat(' ', spaces || 2);
return text.split(/\r?\n/).map(function (line) {
return (text || '').split(/\r?\n/).map(function (line) {
return space + line;
}).join('\n');
}
@ -144,9 +144,37 @@ LoggerAbstract.prototype.onDebug = _.handler(function (msg) {
* @param {String} msg - The message to be logged
* @return {undefined}
*/
LoggerAbstract.prototype.onTrace = _.handler(function (message, curlCall) {
this.write('TRACE', curlCall + '\n' + message);
LoggerAbstract.prototype.onTrace = _.handler(function (requestDetails) {
this.write('TRACE', this._formatTraceMessage(requestDetails));
});
LoggerAbstract.prototype._formatTraceMessage = function (req) {
return '-> ' + req.method + ' ' + req.url + '\n' +
this._prettyJson(req.body) + '\n' +
'<- ' + req.status + '\n' +
this._prettyJson(req.response);
/*
-> GET https://sldfkjsdlfksjdf:9200/slsdkfjlxckvxhclks?sdlkj=sdlfkje
{
asdflksjdf
}
<- 502
{
sldfksjdlf
}
*/
};
LoggerAbstract.prototype._prettyJson = function (body) {
try {
if (typeof object === 'string') {
body = JSON.parse(body);
}
return JSON.stringify(body, null, ' ').replace(/'/g, '\\u0027');
} catch (e) {
return typeof body === 'string' ? body : '';
}
};
module.exports = LoggerAbstract;

View File

@ -94,6 +94,6 @@ Console.prototype.onDebug = _.handler(function (msg) {
* @private
* @return {undefined}
*/
Console.prototype.onTrace = _.handler(function (msg, curlCall) {
this.write('TRACE', curlCall + '\n' + msg, 'log');
Console.prototype.onTrace = _.handler(function (msg) {
this.write('TRACE', this._formatTraceMessage(msg), 'log');
});

View File

@ -112,6 +112,6 @@ Stdio.prototype.onDebug = _.handler(function (msg) {
* @private
* @return {undefined}
*/
Stdio.prototype.onTrace = _.handler(function (message, curlCall) {
this.write('TRACE', curlCall + '\n' + message, process.stdout, this.colors.trace);
Stdio.prototype.onTrace = _.handler(function (message) {
this.write('TRACE', this._formatTraceMessage(message), process.stdout, this.colors.trace);
});

View File

@ -15,6 +15,7 @@ module.exports = Tracer;
var StreamLogger = require('./stream');
var fs = require('fs');
var _ = require('../utils');
var url = require('url');
function Tracer(log, config) {
if (config.path === false) {
@ -23,13 +24,36 @@ function Tracer(log, config) {
config.stream = fs.createWriteStream(config.path || 'elasticsearch-tracer.log');
}
this.curlHost = config.curlHost || 'localhost';
this.curlPort = config.curlPort || 9200;
StreamLogger.call(this, log, config);
}
_.inherits(Tracer, StreamLogger);
Tracer.prototype.onTrace = _.handler(function (message, curlCall) {
this.write('TRACE', message, curlCall);
});
var usefulUrlFields = ['protocol', 'slashes', 'port', 'hostname', 'pathname', 'query'];
Tracer.prototype._formatTraceMessage = function (req) {
var reqUrl = _.pick(url.parse(req.url, true, false), usefulUrlFields);
var originalHost = url.format(_.pick(reqUrl, 'protocol', 'hostname', 'port'));
reqUrl.port = this.curlPort;
reqUrl.hostname = this.curlHost;
reqUrl.query = _.defaults(reqUrl.query || {}, { pretty: true });
/* jshint quotmark: double */
var curlCall =
"# " + originalHost + "\n" +
"curl '" + url.format(reqUrl).replace(/'/g, "\\'") + "' -X" + req.method.toUpperCase() +
(req.body ? " -d '" + this._prettyJson(req.body) + "'" : "");
/* jshint quotmark: single */
return {
curl: curlCall,
msg: '-> ' + req.status + '\n' + req.response
};
};
function comment(str) {
return _.map(str.split(/\r?\n/g), function (line) {
@ -37,10 +61,11 @@ function comment(str) {
}).join('\n');
}
Tracer.prototype.write = function (label, message, curlCall) {
this.stream.write(
comment(label + ': ' + this.timestamp()) + '\n' + (curlCall ? curlCall + '\n' : '') +
comment(message) + '\n\n',
'utf8'
);
Tracer.prototype.write = function (label, msg) {
var lead = comment(label + ': ' + this.timestamp()) + '\n';
if (typeof msg === 'string') {
this.stream.write(lead + comment(msg) + '\n\n', 'utf8');
} else {
this.stream.write(lead + msg.curl + '\n' + comment(msg.msg) + '\n\n', 'utf8');
}
};

16
test/unit/browser.js Normal file
View File

@ -0,0 +1,16 @@
var exclude = [
'file_logger.js',
'http_connector.js',
'stdio_logger.js',
'console_logger.js',
'stream_logger.js',
'tracer_logger.js',
'transport_with_server.js'
];
var _ = require('lodash');
var specDir = __dirname + '/specs';
_(require('fs').readdirSync(specDir)).difference(exclude).each(function (file) {
require(specDir + '/' + file);
});

View File

@ -0,0 +1,3 @@
require('./angular');
require('./generic');
require('./jquery');

View File

@ -230,22 +230,8 @@ module.exports = function (makeLogger) {
stub(logger, 'write', function (label, msg) {
expect(label).to.eql('TRACE');
});
logger.onTrace('message');
logger.onTrace(Log.normalizeTraceArgs('GET', 'http://place/thing?me=true', '{}', '{"ok": true}', 200));
expect(logger.write.callCount).to.eql(1);
});
it('joins the message and curl call with a newline', function () {
var logger = makeLogger();
// tracer logger has custom trace logic...
if (!(logger instanceof TracerLogger)) {
stub(logger, 'write', function (label, msg) {
expect(msg).to.eql('curlcall\nmessage');
});
logger.onTrace('message', 'curlcall');
expect(logger.write.callCount).to.eql(1);
}
});
});
};

View File

@ -1,3 +1,5 @@
require('fs').readdirSync(__dirname + '/tests').forEach(function (file) {
require(__dirname + '/tests/' + file);
var specDir = __dirname + '/specs';
require('fs').readdirSync(specDir).forEach(function (file) {
require(specDir + '/' + file);
});

View File

@ -182,12 +182,13 @@ describe('Log class', function () {
expect(call.args[0]).to.eql('here');
});
it('should emit a trace event for trace events, with message and curlCall args', function () {
it('should emit a trace event for trace events, with normalized request details arg', function () {
log.trace('GET', 'http://localhost:9200/_cluster/nodes', '', '', 200);
expect(call.event).to.eql('trace');
expect(call.args.length).to.be(2);
expect(call.args[0]).to.match(/^<- 200/);
expect(call.args[1]).to.match(/^curl /);
expect(call.args.length).to.be(1);
expect(call.args[0]).to.have.property('method', 'GET');
expect(call.args[0]).to.have.property('url', 'http://localhost:9200/_cluster/nodes');
expect(call.args[0]).to.have.property('status', 200);
});
});

View File

@ -57,10 +57,10 @@ describe('Stdio Logger', function () {
var logger = makeLogger();
stub(process.stdout, 'write');
var withoutColor = 'Elasticsearch TRACE: ' + now + '\n curl\n msg\n\n';
var withoutColor = 'Elasticsearch DEBUG: ' + now + '\n be weary\n\n';
logger.color = true;
logger.onTrace('msg', 'curl');
logger.onDebug('be weary');
expect(process.stdout.write.lastCall.args[0]).to.not.eql(withoutColor);
expect(chalk.stripColor(process.stdout.write.lastCall.args[0])).to.eql(withoutColor);
});

View File

@ -0,0 +1,74 @@
describe('Tracer Logger', function () {
var Log = require('../../../src/lib/log');
var TracerLogger = require('../../../src/lib/loggers/tracer');
var sinon = require('sinon');
var expect = require('expect.js');
var parentLog;
beforeEach(function () {
parentLog = new Log();
});
afterEach(function () {
parentLog.close();
});
function makeLogger(parent, levels) {
parent = parent || parentLog;
var config = {
levels: Log.parseLevels(levels || 'trace')
};
return new TracerLogger(parent, config);
}
var stub = require('../../utils/auto_release_stub').make();
// require('../generic_logger_tests')(makeLogger);
describe('#formatTraceMessage', function () {
it('includes the original host', function () {
var logger = makeLogger();
var formatted = logger._formatTraceMessage({
method: 'DELETE',
url: 'https://originalHost.com:9522/path/to/thing?qs=100',
body: '{ "yes": true }',
status: 333,
response: '{ "ok": false }'
});
expect(formatted.curl).to.match(/-XDELETE/);
expect(formatted.curl).to.match(/https:\/\/originalHost\.com:9522\/?\s+/i);
expect(formatted.curl).to.match(/https:\/\/localhost:9200\/path\/to\/thing\?(pretty=true|qs=100|&){3}/);
});
});
describe('#write', function () {
var logger;
beforeEach(function () {
logger = makeLogger();
stub(logger.stream, 'write');
});
it('detects tracer logs by listening messages wrapped in objects', function () {
logger.write('TRACE', { msg: 'msgtext', curl: 'curlcall' });
expect(logger.stream.write.callCount).to.be(1);
expect(logger.stream.write.lastCall.args[0]).to.contain('msgtext');
expect(logger.stream.write.lastCall.args[0]).to.contain('curlcall');
});
it('comments everthing except the curl call', function () {
logger.write('TRACE', { msg: 'comment me', curl: 'no comment' });
expect(logger.stream.write.callCount).to.be(1);
expect(logger.stream.write.lastCall.args[0]).to.match(/^# +comment me/m);
expect(logger.stream.write.lastCall.args[0]).to.match(/^no comment/m);
});
it('just comments when it gets a string', function () {
logger.write('TRACE', 'log me');
expect(logger.stream.write.callCount).to.be(1);
expect(logger.stream.write.lastCall.args[0]).to.match(/^# +log me/m);
});
});
});

View File

@ -508,14 +508,15 @@ describe('Transport Class', function () {
it('promise is always pulled from the defer created by this.defer()', function () {
var fakePromise = {};
var origDefer = Transport.prototype.defer;
Transport.prototype.defer = function () {
return {
resolve: _.noop,
reject: _.noop,
promise: fakePromise
};
};
var tran = new Transport({});
var tran = new Transport({
defer: function () {
return {
resolve: _.noop,
reject: _.noop,
promise: fakePromise
};
}
});
shortCircuitRequest(tran);
var ret = tran.request({});
Transport.prototype.defer = origDefer;

View File

@ -1,43 +0,0 @@
describe('Tracer Logger', function () {
var Log = require('../../../src/lib/log');
var TracerLogger = require('../../../src/lib/loggers/tracer');
var sinon = require('sinon');
var expect = require('expect.js');
var parentLog;
beforeEach(function () {
parentLog = new Log();
});
afterEach(function () {
parentLog.close();
});
function makeLogger(parent, levels) {
parent = parent || parentLog;
var config = {
levels: Log.parseLevels(levels || 'trace')
};
return new TracerLogger(parent, config);
}
var stub = require('../../utils/auto_release_stub').make();
require('../generic_logger_tests')(makeLogger);
describe('#write', function () {
it('comments out everything accept the curlCall', function () {
var logger = makeLogger();
stub(logger.stream, 'write', function (string) {
expect(string.replace(/(^#.*$|^curlcall$)/mg, '').trim()).to.be('');
});
logger.onTrace('message', 'curlcall');
expect(logger.stream.write.callCount).to.eql(1);
});
});
});

View File

@ -5,30 +5,14 @@ var _ = require('lodash');
var async = require('async');
var path = require('path');
var root = path.join(__dirname, '../../..');
var glob = require('glob');
var browserify = require('browserify');
var pkg = require(root + '/package.json');
var testFiles = {
unit: 'test/unit/tests/!(' + [
'file_logger',
'http_connector',
'stdio_logger',
'console_logger',
'stream_logger',
'tracer_logger',
'transport_with_server',
].join('|') + ')*.js',
build: 'test/unit/browser_builds/*.js'
unit: 'test/unit/browser.js',
build: 'test/unit/browser_builds/index.js'
};
// resolve the test file globs
_.transform(testFiles, function (out, pattern, name) {
out[name] = _.map(glob.sync(pattern), function (filename) {
return path.resolve(root, filename);
});
}, testFiles);
// generic aliasify instance
var aliasify = require('aliasify').configure({
aliases: pkg.browser,