Skip to content

Instantly share code, notes, and snippets.

@kazaff
Created March 5, 2014 00:35
Show Gist options
  • Save kazaff/9358877 to your computer and use it in GitHub Desktop.
Save kazaff/9358877 to your computer and use it in GitHub Desktop.
This gist exceeds the recommended number of files (~10). To access all files, please clone this gist.
.DS_Store
node_modules/
.idea/
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" />
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/message_middleware.iml" filepath="$PROJECT_DIR$/.idea/message_middleware.iml" />
</modules>
</component>
</project>
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
</component>
</project>
/**
* Created by @kazaff on 14-3-4.
*
*/
#!/usr/bin/env node
// -*- mode: js -*-
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var readline = require('readline');
var sprintf = require('util').format;
var nopt = require('nopt');
///--- Globals
var BUCKETS = {};
var REQUEST_IDS = {};
var OPTS = {
'average': Boolean,
'help': Boolean,
'end': Date,
'max-latency': Number,
'max-requests': Number,
'output': String,
'percentile': [Number, Array],
'period': Number,
'requests': Boolean,
'start': Date
};
var SHORT_OPTS = {
'a': ['--average'],
'h': ['--help'],
'i': ['--period'],
'e': ['--end'],
'l': ['--max-latency'],
'n': ['--max-requests'],
'o': ['--output'],
'p': ['--percentile'],
'r': ['--requests'],
's': ['--start']
};
///--- Functions
function percentile(p, vals) {
p = parseInt(p, 10);
return vals[(Math.round(((p / 100) * vals.length) + 1 / 2) - 1)].latency;
}
function report(buckets, output) {
Object.keys(buckets).sort(function (a, b) {
return parseInt(a, 10) - parseInt(b, 10);
}).forEach(function (k) {
var avg = 0;
var perc = [];
var req = buckets[k].length;
var sum = 0;
var t = Math.round(buckets[k]._time);
buckets[k] = buckets[k].sort(function (a, b) {
return a.latency - b.latency;
});
buckets[k].forEach(function (v) {
sum += v.latency;
});
if (sum > 0 && req > 0) {
if (output.average)
output.average.push([t, Math.round(sum / req)]);
if (output.requests)
output.requests.push([t, buckets[k].length]);
Object.keys(output.percentile).forEach(function (p) {
var _p = percentile(p, buckets[k]);
output.percentile[p].push([t, _p]);
});
}
});
return output;
}
function usage(code, message) {
var str = '';
Object.keys(SHORT_OPTS).forEach(function (k) {
if (!Array.isArray(SHORT_OPTS[k]))
return;
var opt = SHORT_OPTS[k][0].replace('--', '');
var type = OPTS[opt].name || 'string';
if (type && type === 'boolean')
type = '';
type = type.toLowerCase();
str += ' [--' + opt + ' ' + type + ']';
});
str += ' [file ...]';
if (message)
console.error(message);
console.error('usage: ' + path.basename(process.argv[1]) + str);
process.exit(code);
}
///--- Mainline
var parsed;
try {
parsed = nopt(OPTS, SHORT_OPTS, process.argv, 2);
} catch (e) {
usage(1, e.toString());
}
if (parsed.help)
usage(0);
if (!parsed.average && !parsed.percentile)
usage(1, '--average or --percentile required');
if (parsed.argv.remain.length < 1)
usage(1, 'log file required');
var config = {
average: parsed.average || false,
maxLatency: parsed['max-latency'] || 1000,
maxRequests: parsed['max-requests'] || 10000,
percentile: parsed.percentile || [],
period: parsed.period || 60,
requests: parsed.requests || false,
start: parsed.start ? (parsed.start.getTime() / 1000) : 0,
end: parsed.end ? (parsed.end.getTime() / 1000) : Number.MAX_VALUE
};
var buckets = {};
var done = 0;
parsed.argv.remain.forEach(function (f) {
var stream = readline.createInterface({
input: fs.createReadStream(f),
output: null
})
stream.on('line', function (l) {
var record;
var t = -1;
try {
record = JSON.parse(l);
} catch (e) {
}
if (!record)
return;
var t = -1;
if (record.time)
t = (new Date(record.time).getTime() / 1000);
if (record._audit !== true ||
REQUEST_IDS[record.req_id] ||
t < config.start ||
t > config.end) {
console.error('Skipping %s', l);
}
REQUEST_IDS[record.req_id] = true;
record.time = t;
var b = Math.round(record.time / config.period) + '';
if (!buckets[b])
buckets[b] = [];
buckets[b].push(record);
buckets[b]._time = record.time // good enough
});
stream.on('end', function () {
if (++done === parsed.argv.remain.length) {
console.error('Generating report...');
var output = {
average: config.average ? [] : false,
requests: config.requests ? [] : false,
percentile: {}
};
config.percentile.forEach(function (p) {
output.percentile[p] = [];
});
output = report(buckets, output);
var finalOutput = [];
if (output.average) {
finalOutput.push({
name: 'avg',
values: output.average
});
}
if (output.requests) {
finalOutput.push({
name: 'n',
values: output.requests
});
}
Object.keys(output.percentile).forEach(function (k) {
finalOutput.push({
name: 'p' + k,
values: output.percentile[k]
});
});
console.log(JSON.stringify(finalOutput));
}
});
});
.dir-locals.el
.gitmodules
.travis.yml
Makefile
deps
docs
examples
test
tools
#!/usr/bin/env node
// -*- mode: js -*-
var fs = require('fs');
var path = require('path');
var spawn = require('child_process').spawn;
var readline = require('readline');
var sprintf = require('util').format;
var nopt = require('nopt');
///--- Globals
var BUCKETS = {};
var REQUEST_IDS = {};
var OPTS = {
'average': Boolean,
'help': Boolean,
'end': Date,
'max-latency': Number,
'max-requests': Number,
'output': String,
'percentile': [Number, Array],
'period': Number,
'requests': Boolean,
'start': Date
};
var SHORT_OPTS = {
'a': ['--average'],
'h': ['--help'],
'i': ['--period'],
'e': ['--end'],
'l': ['--max-latency'],
'n': ['--max-requests'],
'o': ['--output'],
'p': ['--percentile'],
'r': ['--requests'],
's': ['--start']
};
///--- Functions
function percentile(p, vals) {
p = parseInt(p, 10);
return vals[(Math.round(((p / 100) * vals.length) + 1 / 2) - 1)].latency;
}
function report(buckets, output) {
Object.keys(buckets).sort(function (a, b) {
return parseInt(a, 10) - parseInt(b, 10);
}).forEach(function (k) {
var avg = 0;
var perc = [];
var req = buckets[k].length;
var sum = 0;
var t = Math.round(buckets[k]._time);
buckets[k] = buckets[k].sort(function (a, b) {
return a.latency - b.latency;
});
buckets[k].forEach(function (v) {
sum += v.latency;
});
if (sum > 0 && req > 0) {
if (output.average)
output.average.push([t, Math.round(sum / req)]);
if (output.requests)
output.requests.push([t, buckets[k].length]);
Object.keys(output.percentile).forEach(function (p) {
var _p = percentile(p, buckets[k]);
output.percentile[p].push([t, _p]);
});
}
});
return output;
}
function usage(code, message) {
var str = '';
Object.keys(SHORT_OPTS).forEach(function (k) {
if (!Array.isArray(SHORT_OPTS[k]))
return;
var opt = SHORT_OPTS[k][0].replace('--', '');
var type = OPTS[opt].name || 'string';
if (type && type === 'boolean')
type = '';
type = type.toLowerCase();
str += ' [--' + opt + ' ' + type + ']';
});
str += ' [file ...]';
if (message)
console.error(message);
console.error('usage: ' + path.basename(process.argv[1]) + str);
process.exit(code);
}
///--- Mainline
var parsed;
try {
parsed = nopt(OPTS, SHORT_OPTS, process.argv, 2);
} catch (e) {
usage(1, e.toString());
}
if (parsed.help)
usage(0);
if (!parsed.average && !parsed.percentile)
usage(1, '--average or --percentile required');
if (parsed.argv.remain.length < 1)
usage(1, 'log file required');
var config = {
average: parsed.average || false,
maxLatency: parsed['max-latency'] || 1000,
maxRequests: parsed['max-requests'] || 10000,
percentile: parsed.percentile || [],
period: parsed.period || 60,
requests: parsed.requests || false,
start: parsed.start ? (parsed.start.getTime() / 1000) : 0,
end: parsed.end ? (parsed.end.getTime() / 1000) : Number.MAX_VALUE
};
var buckets = {};
var done = 0;
parsed.argv.remain.forEach(function (f) {
var stream = readline.createInterface({
input: fs.createReadStream(f),
output: null
})
stream.on('line', function (l) {
var record;
var t = -1;
try {
record = JSON.parse(l);
} catch (e) {
}
if (!record)
return;
var t = -1;
if (record.time)
t = (new Date(record.time).getTime() / 1000);
if (record._audit !== true ||
REQUEST_IDS[record.req_id] ||
t < config.start ||
t > config.end) {
console.error('Skipping %s', l);
}
REQUEST_IDS[record.req_id] = true;
record.time = t;
var b = Math.round(record.time / config.period) + '';
if (!buckets[b])
buckets[b] = [];
buckets[b].push(record);
buckets[b]._time = record.time // good enough
});
stream.on('end', function () {
if (++done === parsed.argv.remain.length) {
console.error('Generating report...');
var output = {
average: config.average ? [] : false,
requests: config.requests ? [] : false,
percentile: {}
};
config.percentile.forEach(function (p) {
output.percentile[p] = [];
});
output = report(buckets, output);
var finalOutput = [];
if (output.average) {
finalOutput.push({
name: 'avg',
values: output.average
});
}
if (output.requests) {
finalOutput.push({
name: 'n',
values: output.requests
});
}
Object.keys(output.percentile).forEach(function (k) {
finalOutput.push({
name: 'p' + k,
values: output.percentile[k]
});
});
console.log(JSON.stringify(finalOutput));
}
});
});

restify Changelog

2.6.3 (not yet released)

2.6.2

  • #508 add server option: ciphers to pass down to https(tls)
  • #502 server.on('request') not emitting
  • #496 static plugin incorrectly handling directories; revert back to 2.6.0 version
  • #495 don't override client response code with custom error object
  • #494 socket connecting detection logic incorrect
  • #492 client false needs to actually disable retries
  • changed indent from four to eight
  • #505 fix audit logger plugin bug
  • #510 request timeout support
  • #523 added Access-Control-Allow-Credentials to the preflight handler

2.6.1

  • #478 Add req.timers to audit logging plugin.
  • #487 RequestCaptureStream: dumpDefault, haveNonRawStreams, zero ring after dump
  • #407 - bunyan 0.21.3
  • Add CSV/TSV parser (Dominik Lessel)
  • Add req.timers: a list of hrtime's for each handler
  • Set TCP SO_KEEPALIVE when default KeepAliveAgent is on (client)

2.6.0

  • EXPERIMENTAL: Native websocket support via watershed (Josh Clulow)
  • Pass entire route, not just route.name to after (Dingman)
  • Type coercion bug in Cache Control API (Chris Cannell)

2.5.1

  • GH-401 RegEx routes stomp one another, resulting in 404
  • GH-389 StringClient should handle garbage servers that send neither Content-Length nor Transfer-Encoding: chunked headers.

2.5.0

  • Pick up http-signature@0.10.0 (breaking change, to those using it); see TritonDataCenter/node-http-signature#10
  • GH-388 JSON client blows up on bad content
  • GH-379 Static plugin: NotAuthorizedError for file path with parentheses (Ricardo Stuven)
  • GH-370 Add charSet option for static file plugin (Jonathan Dahan)

2.4.1

  • Support node 0.10.X TLS options in client(s)

2.4.0

  • GH-368 Route //.*/ does not match request /? (Ben Hutchison)
  • GH-366 req.accepts() not working with short-hand mime types
  • GH-362 Empty body throws TypeError in StringClient (Bryan Donovan)
  • GH-355 Serve gzip encoded files from static if they are available (Nathanael Anderson)
  • GH-338 turn req.body into an Object when content-type is JSON (Daan Kuijsten)
  • GH-336 res.charSet() back in
  • dependency version bumps
  • 0.10.X support in tests (add necessary resume() calls)
  • client should log request/response pairs

2.3.5

  • bunyan@0.20.0
  • GH-346 server.toString() crashes (Alex Whitman)
  • GH-193 support next('name_of_route')

2.3.4

  • GH-343 default to 'identity' for accept-encoding
  • GH-342 client support for PATCH
  • Pick up spdy@1.4.6 (doesn't ship all the example garbage)

2.3.3

  • Stop logging client_req in bunyan output
  • GH-319 make DTrace optional
  • GH-335 Content-Type'd routes not accepting array (Pedro Palazón)

2.3.2

  • pick up bunyan 0.18.3
  • server.address() returning null causes server.url to deref null

2.3.1

  • GH-335 Content-Type'd routes not correct when only Accept-Extension varies, part deux (switch to negotiator, drop mimeparse).

2.3.0

  • GH-335 Content-Type'd routes not correct when only Accept-Extension varies
  • GH-332 Cache-Control max-age should show minutes (Ben Hutchison)
  • GH-329 Wrong values in res.methods on some cases
  • GH-327 server.versionedUse('1.2.3', function (req, res, next) {}) (Tim Kuijsten)
  • GH-326 non-default origins not working, Chrome requires allow/origin and allow users to append to CORS array (John Fieber/Damon Oehlman)
  • GH-323 /? broken
  • GH-322 add req.route, which contains the original params for the route (Tim Kuijsten)
  • GH-312 bodyParser() should return buffers when data is binary (Tim Kuijsten)
  • GH-318 Allow use of 'requestBodyOnGet' option in bodyParser (@geebee)

2.2.1

  • GH-283 broke versioned, typed routes. Fix.
  • node-http-signature@0.9.11

2.2.0

  • GH-316 drop clone, and just shallow copy (Trent Mick)
  • GH-284 preflight requests not working without access-control-request-headers
  • GH-283 versioned routes should use maximum match, not first (Colin O'Brien)
  • dtrace probes for restify clients
  • node-dtrace-provider@0.2.8
  • backoff@2.0.0 and necessary changes

2.1.1

  • revert to backoff@1.2.0

2.1.0

  • GH-284 built-in CORS
  • GH-290 next.ifError
  • GH-291 fix overwriting options.type in createJSONClient (Trent Mick)
  • GH-297 default document serving in static plugin (Adam Argo)
  • GH-299 gzip plugin doesn't play nice with content-length (Ben Hale)
  • GH-301 support private keys w/passphrase (Erik Kristensen)
  • GH-302 responseTime cleanup
  • Move to node-backoff and rework retry logic in HttpClient
  • Support keep-alive by default in HttpClient

2.0.4

  • GH-280 req.params cached by router
  • RequestCaptureStream should support outputting to multiple streams
  • Default uncaughtException handler should check if headers have been sent

2.0.2/2.0.3

  • GH-278 missing events on route errors
  • Undo RestError constructorOpt from 2.0.1

2.0.1

  • GH-269 plugin to make curl happy
  • RestError not honoring constructorOpt from cause
  • GH-271 bump to dtrace 0.2.6 (fix build on Mountain Lion)

Legacy Releases

1.4.2

  • logging typo (Pedro Candel)
  • response beforeSend event (Paul Bouzakis)

1.4.1

  • GH-130 Allow restify and express to coexist.
  • GH-129 format HttpErrors as well as RestErrors (Domenic Denicola)
  • GH-127 add absolute uri to request (Paul Bouzakis)
  • GH-124 req.query is undefined if no query string was sent
  • GH-123 Generated DTrace probe names should be valid
  • GH-122 Response._writeHead can cause infinite loop (Trent Mick)
  • GH-120 Allow server.patch (Paul Bouzakis)
  • GH-119 defaultResponseHeaders not settable
  • GH-113 document return next(false)

1.4.0

  • GH-116 More friendly error objects (Domenic Denicola)
  • GH-115 Client hangs on server "hard kills" (i.e., RST)
  • GH-111 JSON parser only works on objects (not arrays)
  • GH-110 emit expectContinue (Paul Bouzakis)
  • Fix "undefined" log message in string_client.js
  • GH-107
    • Go back to hacking up http.prototype for performance reasons
    • Default to keep-alive on for HTTP/1.1 requests
    • Misc fixes after refactoring.
  • GH-109 routes not honoring regex flags.
  • GH-108 server missing listening event.
  • Audit logger optionally logs request/response bodies
  • Require http-signature@0.9.9/ctype@0.5.0 (node 0.7 compatible)

1.3.0

  • GH-100 Make DTrace an optional dependency, and stub it out if not found.
  • res.link API not allowing sprintf style sets.
  • Support for socketPath in client API (alternative to url).
  • OPTIONS api not returning access-control-allow-methods header (Steve Mason).
  • Allow null passwords in HTTP basic auth (Andrew Robinson).
  • set req.files on multipart file uploads (Andrew Robinson).

1.2.0

  • Don't rely on instanceof checks for Errors in response.
  • Change route.run log level from trace to debug on next(err).
  • Add res.link API (wrap up sending a Link: response header).
  • GH-98 req.secure needs to return a boolean, not an object
  • GH-97 Malformed URI results in server crash
  • GH-94 leverage qs module for object notation in query string.

1.1.1

  • dependency version bumps
  • res.header accepts sprintf-style arguments
  • GH-95 Make restify compatible with node-logging (Andrew Robinson)
  • GH-93 Minimal port of express pre-conditions (Dominic Barnes)
  • GH-92 Make X-Response-Time configurable (Shaun Berryman)
  • GH-87 server.listen on port as string (Andrew Sliwinski)

1.1.0

  • GH-86 Bunyan version bump.
  • Conditional Request plugin tests and fixes for some errors (Mike Williams).
  • GH-83 pluggable storage engine for throttling, and LRU for default engine.
  • GH-77 server.on('uncaughtException', function (req, res, route, err) {});
  • GH-79 Docs typo(s).

1.0.1

  • Version bump bunyan to 0.6.4.

1.0.0

  • Makefile restructure (use Joyent templates)
  • GH-20 HttpClient connectTimeout.
  • Allow parser plugins to allow "override" params
  • Proper handling of Expect: 100
  • multipart/form-data plugin
  • Added a 'header' event on res.writeHead
  • GH-72 Wrong server name in response header on 404/405/...
  • RegExp mounts throw a TypeError
  • Allow pre handlers to update request url
  • try/catch around route running
  • Bundled audit logger (via bunyan)
  • strict adherence to RFC3986 for URL encoding
  • range versioning changed to be an array of explicit versions
  • Switch from log4js to bunyan
  • Official version of ConditionalRequest plugin (Falco Nogatz)
  • order formatters on response such that JSON/text are before custom ones
  • RestErrors can use format strings
  • date plugin has bad log check

1.0.0-rc2

  • GH-66 Support for charSets in responses
  • GH-65 Initial version of etag plugin (Falco Nogatz)
  • GH-68 res.header() can serialize Date objects to RFC1123
  • GH-67 Set some default response headers earlier (access-control-*)
  • http-client should auto insert the date header
  • GH-64 Support for a pre-routing chain
  • JsonClient should "upcast" errors to RestErrors if it can
  • GH-63 res.send(204) returning a body of 204
  • GH-61 Make bodyParser merging into req.params optional
  • Make Error objects backwards compatible with older restify (httpCode/restCode)
  • GH-57, GH-62 range versioning on server (Diego Torres)
  • GH-59 routes with just '/' are returning 404
  • DTrace *-done actually firing content-length (was always 0)
  • [Issue 56] Support streaming downloads
  • Modify server.on('after') to emit the Route object, rather than the name.

1.0.0-rc1

(Started maintaining this log 21 January 2012. For earlier change information you'll have to dig into the commit history.)

// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var Stream = require('stream').Stream;
var util = require('util');
var assert = require('assert-plus');
var bunyan = require('bunyan');
var LRU = require('lru-cache');
var uuid = require('node-uuid');
///--- Globals
var sprintf = util.format;
var DEFAULT_REQ_ID = uuid.v4();
var STR_FMT = '[object %s<level=%d, limit=%d, maxRequestIds=%d>]';
///--- Helpers
function appendStream(streams, s) {
assert.arrayOfObject(streams, 'streams');
assert.object(s, 'stream');
if (s instanceof Stream) {
streams.push({
raw: false,
stream: s
});
} else {
assert.optionalBool(s.raw, 'stream.raw');
assert.object(s.stream, 'stream.stream');
streams.push(s);
}
}
///--- API
/**
* A Bunyan stream to capture records in a ring buffer and only pass through
* on a higher-level record. E.g. buffer up all records but only dump when
* getting a WARN or above.
*
* @param {Object} options contains the parameters:
* - {Object} stream The stream to which to write when dumping captured
* records. One of `stream` or `streams` must be specified.
* - {Array} streams One of `stream` or `streams` must be specified.
* - {Number|String} level The level at which to trigger dumping captured
* records. Defaults to bunyan.WARN.
* - {Number} maxRecords Number of records to capture. Default 100.
* - {Number} maxRequestIds Number of simultaneous request id capturing
* buckets to maintain. Default 1000.
* - {Boolean} dumpDefault If true, then dump captured records on the
* *default* request id when dumping. I.e. dump records logged without
* a "req_id" field. Default false.
*/
function RequestCaptureStream(opts) {
assert.object(opts, 'options');
assert.optionalObject(opts.stream, 'options.stream');
assert.optionalArrayOfObject(opts.streams, 'options.streams');
assert.optionalNumber(opts.level, 'options.level');
assert.optionalNumber(opts.maxRecords, 'options.maxRecords');
assert.optionalNumber(opts.maxRequestIds, 'options.maxRequestIds');
assert.optionalBool(opts.dumpDefault, 'options.dumpDefault');
var self = this;
Stream.call(this);
this.level = opts.level ? bunyan.resolveLevel(opts.level) : bunyan.WARN;
this.limit = opts.maxRecords || 100;
this.maxRequestIds = opts.maxRequestIds || 1000;
this.requestMap = LRU({
max: self.maxRequestIds
});
this.dumpDefault = opts.dumpDefault;
this._offset = -1;
this._rings = [];
this.streams = [];
if (opts.stream)
appendStream(this.streams, opts.stream);
if (opts.streams)
opts.streams.forEach(appendStream.bind(null, this.streams));
this.haveNonRawStreams = false;
for (var i = 0; i < this.streams.length; i++) {
if (!this.streams[i].raw) {
this.haveNonRawStreams = true;
break;
}
}
}
util.inherits(RequestCaptureStream, Stream);
RequestCaptureStream.prototype.write = function write(record) {
var req_id = record.req_id || DEFAULT_REQ_ID;
var ring;
var self = this;
if (!(ring = this.requestMap.get(req_id))) {
if (++this._offset > this.maxRequestIds)
this._offset = 0;
if (this._rings.length <= this._offset) {
this._rings.push(new bunyan.RingBuffer({
limit: self.limit
}));
}
ring = this._rings[this._offset];
ring.records.length = 0;
this.requestMap.set(req_id, ring);
}
assert.ok(ring, 'no ring found');
if (record.level >= this.level) {
var i, r, ser;
for (i = 0; i < ring.records.length; i++) {
r = ring.records[i];
if (this.haveNonRawStreams) {
ser = JSON.stringify(r,
bunyan.safeCycles()) + '\n';
}
self.streams.forEach(function (s) {
s.stream.write(s.raw ? r : ser);
});
}
ring.records.length = 0;
if (this.dumpDefault) {
var defaultRing = self.requestMap.get(DEFAULT_REQ_ID);
for (i = 0; i < defaultRing.records.length; i++) {
r = defaultRing.records[i];
if (this.haveNonRawStreams) {
ser = JSON.stringify(r,
bunyan.safeCycles()) + '\n';
}
self.streams.forEach(function (s) {
s.stream.write(s.raw ? r : ser);
});
}
defaultRing.records.length = 0;
}
} else {
ring.write(record);
}
};
RequestCaptureStream.prototype.toString = function toString() {
return (sprintf(STR_FMT,
this.constructor.name,
this.level,
this.limit,
this.maxRequestIds));
};
///--- Serializers
function clientReq(req) {
if (!req)
return (req);
var host;
try {
host = req.host.split(':')[0];
} catch (e) {
host = false;
}
return ({
method: req ? req.method : false,
url: req ? req.path : false,
address: host,
port: req ? req.port : false,
headers: req ? req.headers : false
});
}
function clientRes(res) {
if (!res || !res.statusCode)
return (res);
return ({
statusCode: res.statusCode,
headers: res.headers
});
}
var SERIALIZERS = {
err: bunyan.stdSerializers.err,
req: bunyan.stdSerializers.req,
res: bunyan.stdSerializers.res,
client_req: clientReq,
client_res: clientRes
};
///--- Exports
module.exports = {
RequestCaptureStream: RequestCaptureStream,
serializers: SERIALIZERS,
createLogger: function createLogger(name) {
return (bunyan.createLogger({
name: name,
serializers: SERIALIZERS,
streams: [
{
level: 'warn',
stream: process.stderr
},
{
level: 'debug',
type: 'raw',
stream: new RequestCaptureStream({
stream: process.stderr
})
}
]
}));
}
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var http = require('http');
var https = require('https');
var os = require('os');
var querystring = require('querystring');
var url = require('url');
var util = require('util');
var zlib = require('zlib');
var assert = require('assert-plus');
var backoff = require('backoff');
var mime = require('mime');
var once = require('once');
var tunnelAgent = require('tunnel-agent');
var uuid = require('node-uuid');
var dtrace = require('../dtrace');
var errors = require('../errors');
var bunyan = require('../bunyan_helper');
// Use native KeepAlive in Node as of 0.11.6
var semver = require('semver');
var nodeVersion = process.version;
var nativeKeepAlive = semver.satisfies(nodeVersion, '>=0.11.6');
var KeepAliveAgent;
var KeepAliveAgentSecure;
var httpMaxSockets = http.globalAgent.maxSockets;
var httpsMaxSockets = https.globalAgent.maxSockets;
if (!nativeKeepAlive) {
KeepAliveAgent = require('keep-alive-agent');
KeepAliveAgentSecure = KeepAliveAgent.Secure;
} else {
KeepAliveAgent = http.Agent;
KeepAliveAgentSecure = https.Agent;
// maxSockets defaults to Infinity, but that doesn't
// lend itself well to KeepAlive, since sockets will
// never be reused.
httpMaxSockets = Math.min(httpMaxSockets, 1024);
httpsMaxSockets = Math.min(httpsMaxSockets, 1024);
}
///--- Globals
/* JSSTYLED */
var VERSION = JSON.parse(fs.readFileSync(require('path').normalize(__dirname + '/../../package.json'), 'utf8')).version;
///--- Helpers
function cloneRetryOptions(options, defaults) {
if (options === false) {
return (false);
}
assert.optionalObject(options, 'options.retry');
var r = options || {};
assert.optionalNumber(r.minTimeout, 'options.retry.minTimeout');
assert.optionalNumber(r.maxTimeout, 'options.retry.maxTimeout');
assert.optionalNumber(r.retries, 'options.retry.retries');
assert.optionalObject(defaults, 'defaults');
defaults = defaults || {};
return ({
minTimeout: r.minTimeout || defaults.minTimeout || 1000,
maxTimeout: r.maxTimeout || defaults.maxTimeout || Infinity,
retries: r.retries || defaults.retries || 4
});
}
function defaultUserAgent() {
var UA = 'restify/' + VERSION +
' (' + os.arch() + '-' + os.platform() + '; ' +
'v8/' + process.versions.v8 + '; ' +
'OpenSSL/' + process.versions.openssl + ') ' +
'node/' + process.versions.node;
return (UA);
}
function ConnectTimeoutError(ms) {
if (Error.captureStackTrace)
Error.captureStackTrace(this, ConnectTimeoutError);
this.message = 'connect timeout after ' + ms + 'ms';
this.name = 'ConnectTimeoutError';
}
util.inherits(ConnectTimeoutError, Error);
function RequestTimeoutError(ms) {
if (Error.captureStackTrace)
Error.captureStackTrace(this, RequestTimeoutError);
this.message = 'request timeout after ' + ms + 'ms';
this.name = 'RequestTimeoutError';
}
util.inherits(RequestTimeoutError, Error);
function rawRequest(opts, cb) {
assert.object(opts, 'options');
assert.object(opts.log, 'options.log');
assert.func(cb, 'callback');
cb = once(cb);
var id = dtrace.nextId();
var log = opts.log;
var proto = opts.protocol === 'https:' ? https : http;
var connectionTimer;
var requestTimer;
if (opts.cert && opts.key)
opts.agent = false;
if (opts.connectTimeout) {
connectionTimer = setTimeout(function connectTimeout() {
connectionTimer = null;
if (req) {
req.abort();
}
var err = new ConnectTimeoutError(opts.connectTimeout);
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, err.toString()]);
});
cb(err, req);
}, opts.connectTimeout);
}
dtrace._rstfy_probes['client-request'].fire(function () {
return ([
opts.method,
opts.path,
opts.headers,
id
]);
});
var req = proto.request(opts, function onResponse(res) {
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
dtrace._rstfy_probes['client-response'].fire(function () {
return ([ id, res.statusCode, res.headers ]);
});
log.trace({client_res: res}, 'Response received');
res.log = log;
var err;
if (res.statusCode >= 400)
err = errors.codeToHttpError(res.statusCode);
req.removeAllListeners('error');
req.removeAllListeners('socket');
req.emit('result', (err || null), res);
});
req.log = log;
req.on('error', function onError(err) {
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, (err || {}).toString()]);
});
log.trace({err: err}, 'Request failed');
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
cb(err, req);
if (req) {
process.nextTick(function () {
req.emit('result', err, null);
});
}
});
req.once('upgrade', function onUpgrade(res, socket, _head) {
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
dtrace._rstfy_probes['client-response'].fire(function () {
return ([ id, res.statusCode, res.headers ]);
});
log.trace({client_res: res}, 'upgrade response received');
res.log = log;
var err;
if (res.statusCode >= 400)
err = errors.codeToHttpError(res.statusCode);
req.removeAllListeners('error');
req.removeAllListeners('socket');
req.emit('upgradeResult', (err || null), res, socket, _head);
});
req.once('socket', function onSocket(socket) {
var _socket = socket;
if (opts.protocol === 'https:' && socket.socket) {
_socket = socket.socket;
}
if (_socket.writable && !_socket._connecting) {
clearTimeout(connectionTimer);
cb(null, req);
return;
}
_socket.once('connect', function onConnect() {
clearTimeout(connectionTimer);
if (opts._keep_alive) {
_socket.setKeepAlive(true);
socket.setKeepAlive(true);
}
if (opts.requestTimeout) {
requestTimer = setTimeout(function requestTimeout() {
requestTimer = null;
var err = new RequestTimeoutError(opts.requestTimeout);
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, err.toString()]);
});
cb(err, req);
if (req) {
req.abort();
process.nextTick(function () {
req.emit('result', err, null);
});
}
}, opts.requestTimeout);
}
cb(null, req);
});
});
if (opts.signRequest)
opts.signRequest(req);
if (log.trace())
log.trace({client_req: opts}, 'request sent');
} // end `rawRequest`
///--- API
function HttpClient(options) {
assert.object(options, 'options');
assert.optionalObject(options.headers, 'options.headers');
assert.object(options.log, 'options.log');
assert.optionalFunc(options.signRequest, 'options.signRequest');
assert.optionalString(options.socketPath, 'options.socketPath');
assert.optionalString(options.url, 'options.url');
EventEmitter.call(this);
var self = this;
this.agent = options.agent;
this.ca = options.ca;
this.cert = options.cert;
this.ciphers = options.ciphers;
this.connectTimeout = options.connectTimeout || false;
this.requestTimeout = options.requestTimeout || false;
this.headers = options.headers || {};
this.log = options.log;
if (!this.log.serializers) {
// Ensure logger has a reasonable serializer for `client_res`
// and `client_req` logged in this module.
this.log = this.log.child({serializers: bunyan.serializers});
}
this.key = options.key;
this.name = options.name || 'HttpClient';
this.passphrase = options.passphrase;
this.pfx = options.pfx;
if (options.rejectUnauthorized !== undefined) {
this.rejectUnauthorized = options.rejectUnauthorized;
} else {
this.rejectUnauthorized = true;
}
if (process.env.https_proxy) {
this.proxy = url.parse(process.env.https_proxy);
} else if (process.env.http_proxy) {
this.proxy = url.parse(process.env.http_proxy);
} else if (options.proxy) {
this.proxy = options.proxy;
} else {
this.proxy = false;
}
this.retry = cloneRetryOptions(options.retry);
this.signRequest = options.signRequest || false;
this.socketPath = options.socketPath || false;
this.url = options.url ? url.parse(options.url) : {};
if (options.accept) {
if (options.accept.indexOf('/') === -1)
options.accept = mime.lookup(options.accept);
this.headers.accept = options.accept;
}
if (options.contentType) {
if (options.contentType.indexOf('/') === -1)
options.type = mime.lookup(options.contentType);
this.headers['content-type'] = options.contentType;
}
if (options.userAgent !== false) {
this.headers['user-agent'] = options.userAgent ||
defaultUserAgent();
}
if (options.version)
this.headers['accept-version'] = options.version;
if (this.agent === undefined) {
var Agent;
var maxSockets;
if (this.proxy) {
if (this.url.protocol == 'https:') {
if (this.proxy.protocol === 'https:') {
Agent = tunnelAgent.httpsOverHttps;
} else {
Agent = tunnelAgent.httpsOverHttp;
}
} else {
if (this.proxy.protocol === 'https:') {
Agent = tunnelAgent.httpOverHttps;
} else {
Agent = tunnelAgent.httpOverHttp;
}
}
} else if (this.url.protocol === 'https:') {
Agent = KeepAliveAgentSecure;
maxSockets = httpsMaxSockets;
} else {
Agent = KeepAliveAgent;
maxSockets = httpMaxSockets;
}
if (this.proxy) {
this.agent = new Agent({
proxy: self.proxy,
rejectUnauthorized: self.rejectUnauthorized,
ca: self.ca
});
} else {
this.agent = new Agent({
cert: self.cert,
ca: self.ca,
ciphers: self.ciphers,
key: self.key,
maxSockets: maxSockets,
// require('keep-alive-agent')
maxKeepAliveRequests: 0,
maxKeepAliveTime: 0,
// native keepalive
keepAliveMsecs: 1000,
keepAlive: true,
passphrase: self.passphrase,
pfx: self.pfx,
rejectUnauthorized: self.rejectUnauthorized
});
this._keep_alive = true;
}
}
}
util.inherits(HttpClient, EventEmitter);
module.exports = HttpClient;
HttpClient.prototype.close = function close() {
var sockets = this.agent.sockets;
Object.keys((sockets || {})).forEach(function (k) {
if (Array.isArray(sockets[k])) {
sockets[k].forEach(function (s) {
s.end();
});
}
});
sockets = this.agent.idleSockets || this.agent.freeSockets;
Object.keys((sockets || {})).forEach(function (k) {
sockets[k].forEach(function (s) {
s.end();
});
});
};
HttpClient.prototype.del = function del(options, callback) {
var opts = this._options('DELETE', options);
return (this.read(opts, callback));
};
HttpClient.prototype.get = function get(options, callback) {
var opts = this._options('GET', options);
return (this.read(opts, callback));
};
HttpClient.prototype.head = function head(options, callback) {
var opts = this._options('HEAD', options);
return (this.read(opts, callback));
};
HttpClient.prototype.opts = function http_options(options, callback) {
var _opts = this._options('OPTIONS', options);
return (this.read(_opts, callback));
};
HttpClient.prototype.post = function post(options, callback) {
var opts = this._options('POST', options);
return (this.request(opts, callback));
};
HttpClient.prototype.put = function put(options, callback) {
var opts = this._options('PUT', options);
return (this.request(opts, callback));
};
HttpClient.prototype.patch = function patch(options, callback) {
var opts = this._options('PATCH', options);
return (this.request(opts, callback));
};
HttpClient.prototype.read = function read(options, callback) {
var r = this.request(options, function readRequestCallback(err, req) {
if (!err)
req.end();
return (callback(err, req));
});
return (r);
};
HttpClient.prototype.basicAuth = function basicAuth(username, password) {
if (username === false) {
delete this.headers.authorization;
} else {
assert.string(username, 'username');
assert.string(password, 'password');
var buffer = new Buffer(username + ':' + password, 'utf8');
this.headers.authorization = 'Basic ' +
buffer.toString('base64');
}
return (this);
};
HttpClient.prototype.request = function request(opts, cb) {
assert.object(opts, 'options');
assert.func(cb, 'callback');
cb = once(cb);
if (opts.retry === false) {
rawRequest(opts, cb);
return;
}
var call;
var retry = cloneRetryOptions(opts.retry);
opts._keep_alive = this._keep_alive;
call = backoff.call(rawRequest, opts, cb);
call.setStrategy(new backoff.ExponentialStrategy({
initialDelay: retry.minTimeout,
maxDelay: retry.maxTimeout
}));
call.failAfter(retry.retries);
call.on('backoff', this.emit.bind(this, 'attempt'));
call.start();
};
HttpClient.prototype._options = function (method, options) {
if (typeof (options) !== 'object')
options = { path: options };
var self = this;
var opts = {
agent: options.agent || self.agent,
ca: options.ca || self.ca,
cert: options.cert || self.cert,
ciphers: options.ciphers || self.ciphers,
connectTimeout: options.connectTimeout || self.connectTimeout,
requestTimeout: options.requestTimeout || self.requestTimeout,
headers: options.headers || {},
key: options.key || self.key,
log: options.log || self.log,
method: method,
passphrase: options.passphrase || self.passphrase,
path: options.path || self.path,
pfx: options.pfx || self.pfx,
rejectUnauthorized: options.rejectUnauthorized ||
self.rejectUnauthorized,
retry: options.retry !== false ? options.retry : false,
signRequest: options.signRequest || self.signRequest
};
if (!opts.retry && opts.retry !== false)
opts.retry = self.retry;
// Backwards compatibility with restify < 1.0
if (options.query &&
Object.keys(options.query).length &&
opts.path.indexOf('?') === -1) {
opts.path += '?' + querystring.stringify(options.query);
}
if (this.socketPath)
opts.socketPath = this.socketPath;
Object.keys(this.url).forEach(function (k) {
if (!opts[k])
opts[k] = self.url[k];
});
Object.keys(self.headers).forEach(function (k) {
if (!opts.headers[k])
opts.headers[k] = self.headers[k];
});
if (!opts.headers.date)
opts.headers.date = new Date().toUTCString();
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
if (opts.headers['content-type'])
delete opts.headers['content-type'];
if (opts.headers['content-md5'])
delete opts.headers['content-md5'];
if (opts.headers['content-length'] && method !== 'DELETE')
delete opts.headers['content-length'];
if (opts.headers['transfer-encoding'])
delete opts.headers['transfer-encoding'];
}
return (opts);
};
// vim: set ts=4 sts=4 sw=4 et:
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var HttpClient = require('./http_client');
var JsonClient = require('./json_client');
var StringClient = require('./string_client');
///--- Exports
module.exports = {
HttpClient: HttpClient,
JsonClient: JsonClient,
StringClient: StringClient
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var util = require('util');
var assert = require('assert-plus');
var codeToHttpError = require('../errors/http_error').codeToHttpError;
var RestError = require('../errors').RestError;
var StringClient = require('./string_client');
///--- API
function JsonClient(options) {
assert.object(options, 'options');
options.accept = 'application/json';
options.name = options.name || 'JsonClient';
options.contentType = 'application/json';
StringClient.call(this, options);
this._super = StringClient.prototype;
}
util.inherits(JsonClient, StringClient);
module.exports = JsonClient;
JsonClient.prototype.write = function write(options, body, callback) {
assert.ok(body !== undefined, 'body');
assert.object(body, 'body');
body = JSON.stringify(body !== null ? body : {});
return (this._super.write.call(this, options, body, callback));
};
JsonClient.prototype.parse = function parse(req, callback) {
var log = this.log;
function parseResponse(err, req2, res, data) {
var obj;
try {
if (data && !/^\s*$/.test(data))
obj = JSON.parse(data);
} catch (e) {
// Not really sure what else we can do here, besides
// make the client just keep going.
log.trace(e, 'Invalid JSON in response');
}
obj = obj || {};
if (res && res.statusCode >= 400) {
// Upcast error to a RestError (if we can)
// Be nice and handle errors like
// { error: { code: '', message: '' } }
// in addition to { code: '', message: '' }.
if (obj.code || (obj.error && obj.error.code)) {
var _c = obj.code ||
(obj.error ? obj.error.code : '') ||
'';
var _m = obj.message ||
(obj.error ? obj.error.message : '') ||
'';
err = new RestError({
message: _m,
restCode: _c,
statusCode: res.statusCode
});
err.name = err.restCode;
if (!/Error$/.test(err.name))
err.name += 'Error';
} else if (!err) {
err = codeToHttpError(res.statusCode,
obj.message || '', data);
}
}
if (err)
err.body = obj;
callback((err || null), req2, res, obj);
}
return (this._super.parse.call(this, req, parseResponse));
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var zlib = require('zlib');
var assert = require('assert-plus');
var qs = require('querystring');
var util = require('util');
var HttpClient = require('./http_client');
///--- Helpers
///--- API
function StringClient(options) {
assert.object(options, 'options');
assert.optionalObject(options.gzip, 'options.gzip');
options.accept = options.accept || 'text/plain';
options.name = options.name || 'StringClient';
options.contentType =
options.contentType || 'application/x-www-form-urlencoded';
HttpClient.call(this, options);
this.gzip = options.gzip;
}
util.inherits(StringClient, HttpClient);
module.exports = StringClient;
StringClient.prototype.post = function post(options, body, callback) {
var opts = this._options('POST', options);
if (typeof (body) === 'function') {
callback = body;
body = null;
}
return (this.write(opts, body, callback));
};
StringClient.prototype.put = function put(options, body, callback) {
var opts = this._options('PUT', options);
if (typeof (body) === 'function') {
callback = body;
body = null;
}
return (this.write(opts, body, callback));
};
StringClient.prototype.patch = function patch(options, body, callback) {
var opts = this._options('PATCH', options);
if (typeof (body) === 'function') {
callback = body;
body = null;
}
return (this.write(opts, body, callback));
};
StringClient.prototype.read = function read(options, callback) {
var self = this;
this.request(options, function _parse(err, req) {
if (err)
return (callback(err, req));
req.once('result', self.parse(req, callback));
return (req.end());
});
return (this);
};
StringClient.prototype.write = function write(options, body, callback) {
if (body !== null && typeof (body) !== 'string')
body = qs.stringify(body);
var self = this;
function _write(data) {
if (data) {
var hash = crypto.createHash('md5');
hash.update(data, 'utf8');
options.headers['content-md5'] = hash.digest('base64');
}
self.request(options, function (err, req) {
if (err) {
callback(err, req);
return;
}
req.once('result', self.parse(req, callback));
req.end(data);
});
}
options.headers = options.headers || {};
if (this.gzip)
options.headers['accept-encoding'] = 'gzip';
if (body) {
if (this.gzip) {
options.headers['content-encoding'] = 'gzip';
zlib.gzip(body, function (err, data) {
if (err) {
callback(err, null);
return;
}
options.headers['content-length'] = data.length;
_write(data);
});
} else {
options.headers['content-length'] =
Buffer.byteLength(body);
_write(body);
}
} else {
_write();
}
return (this);
};
StringClient.prototype.parse = function parse(req, callback) {
function parseResponse(err, res) {
if (res) {
function done() {
res.log.trace('body received:\n%s', body);
res.body = body;
if (hash && md5 !== hash.digest('base64')) {
err = new Error('BadDigest');
callback(err, req, res);
return;
}
if (err) {
err.body = body;
err.message = body;
}
callback(err, req, res, body);
}
var body = '';
var gz;
var hash;
var md5 = res.headers['content-md5'];
if (md5 && req.method !== 'HEAD')
hash = crypto.createHash('md5');
if (res.headers['content-encoding'] === 'gzip') {
gz = zlib.createGunzip();
gz.on('data', function (chunk) {
body += chunk.toString('utf8');
});
gz.once('end', done);
res.once('end', gz.end.bind(gz));
} else {
res.setEncoding('utf8');
res.once('end', done);
}
res.on('data', function onData(chunk) {
if (hash)
hash.update(chunk);
if (gz) {
gz.write(chunk);
} else {
body += chunk;
}
});
} else {
callback(err, req, null, null);
}
}
return (parseResponse);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Globals
var ID = 0;
var MAX_INT = Math.pow(2, 32) - 1;
var PROBES = {
// server_name, route_name, id, method, url, headers (json)
'route-start': ['char *', 'char *', 'int', 'char *', 'char *', 'json'],
// server_name, route_name, handler_name, id
'handler-start': ['char *', 'char *', 'char *', 'int'],
// server_name, route_name, handler_name, id
'handler-done': ['char *', 'char *', 'char *', 'int'],
// server_name, route_name, id, statusCode, headers (json)
'route-done': ['char *', 'char *', 'int', 'int', 'json'],
// Client probes
// method, url, headers, id
'client-request': ['char *', 'char *', 'json', 'int'],
// id, statusCode, headers
'client-response': ['int', 'int', 'json'],
// id, Error.toString()
'client-error': ['id', 'char *']
};
var PROVIDER;
///--- API
module.exports = function exportStaticProvider() {
if (!PROVIDER) {
try {
var dtrace = require('dtrace-provider');
PROVIDER = dtrace.createDTraceProvider('restify');
} catch (e) {
PROVIDER = {
fire: function () {
},
enable: function () {
},
addProbe: function () {
var p = {
fire: function () {
}
};
return (p);
},
removeProbe: function () {
},
disable: function () {
}
};
}
PROVIDER._rstfy_probes = {};
Object.keys(PROBES).forEach(function (p) {
var args = PROBES[p].splice(0);
args.unshift(p);
var probe = PROVIDER.addProbe.apply(PROVIDER, args);
PROVIDER._rstfy_probes[p] = probe;
});
PROVIDER.enable();
PROVIDER.nextId = function nextId() {
if (++ID >= MAX_INT)
ID = 1;
return (ID);
};
}
return (PROVIDER);
}();
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var http = require('http');
var util = require('util');
var assert = require('assert-plus');
var WError = require('verror').WError;
///--- Globals
var slice = Function.prototype.call.bind(Array.prototype.slice);
///--- Helpers
function codeToErrorName(code) {
code = parseInt(code, 10);
var status = http.STATUS_CODES[code];
if (!status)
return (false);
var pieces = status.split(/\s+/);
var str = '';
pieces.forEach(function (s) {
str += s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
});
str = str.replace(/\W+/g, '');
if (!/\w+Error$/.test(str))
str += 'Error';
return (str);
}
///--- Error Base class
function HttpError(options) {
assert.object(options, 'options');
options.constructorOpt = options.constructorOpt || HttpError;
WError.apply(this, arguments);
var self = this;
var code = parseInt((options.statusCode || 500), 10);
this.statusCode = code;
this.body = options.body || {
code: codeToErrorName(code),
message: options.message || self.message
};
this.message = options.message || self.message;
}
util.inherits(HttpError, WError);
///--- Exports
module.exports = {
HttpError: HttpError,
codeToHttpError: function codeToHttpError(code, message, body) {
var err;
var name = codeToErrorName(code);
if (!name) {
err = new HttpError({
statusCode: code,
message: message,
body: body
});
err.name = 'Http' + code + 'Error';
} else {
err = new module.exports[name]({
body: body,
message: message,
constructorOpt: codeToHttpError,
statusCode: code
});
}
return (err);
}
};
// Export all the 4xx and 5xx HTTP Status codes as Errors
var codes = Object.keys(http.STATUS_CODES);
codes.forEach(function (code) {
if (code < 400)
return;
var name = codeToErrorName(code);
module.exports[name] = function (cause, message) {
var index = 1;
var opts = {
statusCode: code
};
if (cause && cause instanceof Error) {
opts.cause = cause;
opts.constructorOpt = arguments.callee;
} else if (typeof (cause) === 'object') {
opts.body = cause.body;
opts.cause = cause.cause;
opts.constructorOpt = cause.constructorOpt;
opts.message = cause.message;
opts.statusCode = cause.statusCode || code;
} else {
opts.constructorOpt = arguments.callee;
index = 0;
}
var args = slice(arguments, index);
args.unshift(opts);
HttpError.apply(this, args);
};
util.inherits(module.exports[name], HttpError);
module.exports[name].displayName =
module.exports[name].prototype.name =
name;
});
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var httpErrors = require('./http_error');
var restErrors = require('./rest_error');
module.exports = {};
Object.keys(httpErrors).forEach(function (k) {
module.exports[k] = httpErrors[k];
});
// Note some of the RestErrors overwrite plain HTTP errors.
Object.keys(restErrors).forEach(function (k) {
module.exports[k] = restErrors[k];
});
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var util = require('util');
var assert = require('assert-plus');
var httpErrors = require('./http_error');
///--- Globals
var slice = Function.prototype.call.bind(Array.prototype.slice);
var HttpError = httpErrors.HttpError;
var CODES = {
BadDigest: 400,
BadMethod: 405,
Internal: 500, // Don't have InternalErrorError
InvalidArgument: 409,
InvalidContent: 400,
InvalidCredentials: 401,
InvalidHeader: 400,
InvalidVersion: 400,
MissingParameter: 409,
NotAuthorized: 403,
PreconditionFailed: 412,
RequestExpired: 400,
RequestThrottled: 429,
ResourceNotFound: 404,
WrongAccept: 406
};
///--- API
function RestError(options) {
assert.object(options, 'options');
options.constructorOpt = options.constructorOpt || RestError;
HttpError.apply(this, arguments);
var self = this;
this.restCode = options.restCode || 'Error';
this.body = options.body || {
code: self.restCode,
message: options.message || self.message
};
}
util.inherits(RestError, HttpError);
///--- Exports
module.exports = {
RestError: RestError
};
Object.keys(CODES).forEach(function (k) {
var name = k;
if (!/\w+Error$/.test(name))
name += 'Error';
module.exports[name] = function (cause, message) {
var index = 1;
var opts = {
restCode: (k === 'Internal' ? 'InternalError' : k),
statusCode: CODES[k]
};
opts.constructorOpt = arguments.callee;
if (cause && cause instanceof Error) {
opts.cause = cause;
} else if (typeof (cause) === 'object') {
opts.body = cause.body;
opts.cause = cause.cause;
opts.message = cause.message;
opts.statusCode = cause.statusCode || CODES[k];
} else {
index = 0;
}
var args = slice(arguments, index);
args.unshift(opts);
RestError.apply(this, args);
};
util.inherits(module.exports[name], RestError);
module.exports[name].displayName =
module.exports[name].prototype.name =
name;
});
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
function formatBinary(req, res, body) {
if (body instanceof Error)
res.statusCode = body.statusCode || 500;
if (!Buffer.isBuffer(body))
body = new Buffer(body.toString());
res.setHeader('Content-Length', body.length);
return (body);
}
module.exports = formatBinary;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
module.exports = {
'application/javascript; q=0.1': require('./jsonp'),
'application/json; q=0.4': require('./json'),
'text/plain; q=0.3': require('./text'),
'application/octet-stream; q=0.2': require('./binary')
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
function formatJSON(req, res, body) {
if (body instanceof Error) {
// snoop for RestError or HttpError, but don't rely on
// instanceof
res.statusCode = body.statusCode || 500;
if (body.body) {
body = body.body;
} else {
body = {
message: body.message
};
}
} else if (Buffer.isBuffer(body)) {
body = body.toString('base64');
}
var data = JSON.stringify(body);
res.setHeader('Content-Length', Buffer.byteLength(data));
return (data);
}
module.exports = formatJSON;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
function formatJSONP(req, res, body) {
if (!body) {
res.setHeader('Content-Length', 0);
return (null);
}
if (body instanceof Error) {
if ((body.restCode || body.httpCode) && body.body) {
body = body.body;
} else {
body = {
message: body.message
};
}
}
if (Buffer.isBuffer(body))
body = body.toString('base64');
var cb = req.query.callback || req.query.jsonp;
var data;
if (cb) {
data = cb + '(' + JSON.stringify(body) + ');';
} else {
data = JSON.stringify(body);
}
res.setHeader('Content-Length', Buffer.byteLength(data));
return (data);
}
module.exports = formatJSONP;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
function formatText(req, res, body) {
if (body instanceof Error) {
res.statusCode = body.statusCode || 500;
body = body.message;
} else if (typeof (body) === 'object') {
body = JSON.stringify(body);
} else {
body = body.toString();
}
res.setHeader('Content-Length', Buffer.byteLength(body));
return (body);
}
module.exports = formatText;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
module.exports = function httpDate(now) {
if (!now)
now = new Date();
return (now.toUTCString());
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
//
// Restify supports both a client and server API, and in the essence of not
// loading the kitchen sink on clients, the exports here is chunked up into
// client and server; note clients will have to opt in by setting the env
// var "RESTIFY_CLIENT_ONLY", but if you're in that boat, it's not hard to do,
// and enables much faster load times
//
var shallowCopy = require('./utils').shallowCopy;
function createClient(options) {
var assert = require('assert-plus');
var bunyan = require('./bunyan_helper');
var clients = require('./clients');
assert.object(options, 'options');
var client;
var opts = shallowCopy(options);
opts.agent = options.agent;
opts.name = opts.name || 'restify';
opts.type = opts.type || 'application/octet-stream';
opts.log = opts.log || bunyan.createLogger(opts.name);
switch (opts.type) {
case 'json':
client = new clients.JsonClient(opts);
break;
case 'string':
client = new clients.StringClient(opts);
break;
case 'http':
default:
client = new clients.HttpClient(opts);
break;
}
return (client);
}
function createJsonClient(options) {
options = options ? shallowCopy(options) : {};
options.type = 'json';
return (createClient(options));
}
function createStringClient(options) {
options = options ? shallowCopy(options) : {};
options.type = 'string';
return (createClient(options));
}
function createHttpClient(options) {
options = options ? shallowCopy(options) : {};
options.type = 'http';
return (createClient(options));
}
function createServer(options) {
var bunyan = require('./bunyan_helper');
var InternalError = require('./errors').InternalError;
var Router = require('./router');
var Server = require('./server');
var opts = shallowCopy(options || {});
var server;
opts.name = opts.name || 'restify';
opts.log = opts.log || bunyan.createLogger(opts.name);
opts.router = opts.router || new Router(opts);
server = new Server(opts);
server.on('uncaughtException', function (req, res, route, e) {
if (this.listeners('uncaughtException').length > 1 ||
res._headerSent) {
return (false);
}
res.send(new InternalError(e, e.message || 'unexpected error'));
return (true);
});
return (server);
}
/**
* Returns a string representation of a URL pattern , with its
* parameters filled in by the passed hash.
*
* If a key is not found in the hash for a param, it is left alone.
*
* @param {Object} a hash of parameter names to values for substitution.
*/
function realizeUrl(pattern, params) {
var p = pattern.replace(/\/:([^/]+)/g, function (match, k) {
return (params.hasOwnProperty(k) ? '/' + params[k] : match);
});
return (require('./utils').sanitizePath(p));
}
///--- Exports
module.exports = {
// Client API
createClient: createClient,
createJsonClient: createJsonClient,
createJSONClient: createJsonClient,
createStringClient: createStringClient,
createHttpClient: createHttpClient,
get HttpClient() {
return (require('./clients').HttpClient);
},
get JsonClient() {
return (require('./clients').JsonClient);
},
get StringClient() {
return (require('./clients').StringClient);
},
// Miscellaneous API
get bunyan() {
return (require('./bunyan_helper'));
},
errors: {}
};
var errors = require('./errors');
Object.keys(errors).forEach(function (k) {
module.exports.errors[k] = errors[k];
module.exports[k] = errors[k];
});
if (!process.env.RESTIFY_CLIENT_ONLY) {
module.exports.createServer = createServer;
module.exports.httpDate = require('./http_date');
module.exports.realizeUrl = realizeUrl;
module.exports.formatters = require('./formatters');
module.exports.plugins = {};
var plugins = require('./plugins');
Object.keys(plugins).forEach(function (k) {
module.exports.plugins[k] = plugins[k];
module.exports[k] = plugins[k];
});
}
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var mime = require('mime');
var NotAcceptableError = require('../errors').NotAcceptableError;
/**
* Returns a plugin that will check the client's Accept header can be handled
* by this server.
*
* Note you can get the set of types allowed from a restify server by doing
* `server.acceptable`.
*
* @param {String} array of accept types.
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function acceptParser(acceptable) {
if (!Array.isArray(acceptable))
acceptable = [acceptable];
assert.arrayOfString(acceptable, 'acceptable');
acceptable = acceptable.filter(function (a) {
return (a);
}).map(function (a) {
return ((a.indexOf('/') === -1) ? mime.lookup(a) : a);
}).filter(function (a) {
return (a);
});
var e = new NotAcceptableError('Server accepts: ' + acceptable.join());
function parseAccept(req, res, next) {
if (req.accepts(acceptable)) {
next();
return;
}
res.json(e);
next(false);
}
return (parseAccept);
}
module.exports = acceptParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var bunyan = require('bunyan');
var HttpError = require('../errors').HttpError;
///--- API
/**
* Returns a Bunyan audit logger suitable to be used in a server.on('after')
* event. I.e.:
*
* server.on('after', restify.auditLogger({ log: myAuditStream }));
*
* This logs at the INFO level.
*
* @param {Object} options at least a bunyan logger (log).
* @return {Function} to be used in server.after.
*/
function auditLogger(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
var log = options.log.child({
audit: true,
serializers: {
err: bunyan.stdSerializers.err,
req: function auditRequestSerializer(req) {
if (!req)
return (false);
var timers = {};
(req.timers || []).forEach(function (time) {
var t = time.time;
var _t = Math.floor((1000000 * t[0]) +
(t[1] / 1000));
timers[time.name] = _t;
});
return ({
method: req.method,
url: req.url,
headers: req.headers,
httpVersion: req.httpVersion,
trailers: req.trailers,
version: req.version(),
body: options.body === true ?
req.body : undefined,
timers: timers
});
},
res: function auditResponseSerializer(res) {
if (!res)
return (false);
var body;
if (options.body === true) {
if (res._body instanceof HttpError) {
body = res._body.body;
} else {
body = res._body;
}
}
return ({
statusCode: res.statusCode,
headers: res._headers,
trailer: res._trailer || false,
body: body
});
}
}
});
function audit(req, res, route, err) {
var latency = res.get('Response-Time');
if (typeof (latency) !== 'number')
latency = Date.now() - req._time;
var obj = {
remoteAddress: req.connection.remoteAddress,
remotePort: req.connection.remotePort,
req_id: req.getId(),
req: req,
res: res,
err: err,
latency: latency,
secure: req.secure,
_audit: true
};
log.info(obj, 'handled: %d', res.statusCode);
return (true);
}
return (audit);
}
///-- Exports
module.exports = auditLogger;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var httpSignature = require('http-signature');
var errors = require('../errors');
///--- Globals
var InvalidHeaderError = errors.InvalidHeaderError;
var OPTIONS = {
algorithms: [
'rsa-sha1',
'rsa-sha256',
'rsa-sha512',
'dsa-sha1',
'hmac-sha1',
'hmac-sha256',
'hmac-sha512'
]
};
///--- Helpers
function parseBasic(string) {
var decoded;
var index;
var pieces;
decoded = (new Buffer(string, 'base64')).toString('utf8');
if (!decoded)
throw new InvalidHeaderError('Authorization header invalid');
index = decoded.indexOf(':');
if (index === -1) {
pieces = [decoded];
} else {
pieces = [decoded.slice(0, index), decoded.slice(index + 1)];
}
if (!pieces || typeof (pieces[0]) !== 'string')
throw new InvalidHeaderError('Authorization header invalid');
// Allows for usernameless authentication
if (!pieces[0])
pieces[0] = null;
// Allows for passwordless authentication
if (!pieces[1])
pieces[1] = null;
return ({
username: pieces[0],
password: pieces[1]
});
}
function parseSignature(request) {
try {
return (httpSignature.parseRequest(request, {
algorithms: OPTIONS.algorithms
}));
} catch (e) {
throw new InvalidHeaderError('Authorization header invalid: ' +
e.message);
}
}
/**
* Returns a plugin that will parse the client's Authorization header.
*
* Subsequent handlers will see `req.authorization`, which looks like:
*
* {
* scheme: <Basic|Signature|...>,
* credentials: <Undecoded value of header>,
* basic: {
* username: $user
* password: $password
* }
* }
*
* `req.username` will also be set, and defaults to 'anonymous'.
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function authorizationParser() {
function parseAuthorization(req, res, next) {
req.authorization = {};
req.username = 'anonymous';
if (!req.headers.authorization)
return (next());
var pieces = req.headers.authorization.split(' ', 2);
if (!pieces || pieces.length !== 2) {
var e = new InvalidHeaderError('BasicAuth content ' +
'is invalid.');
return (next(e));
}
req.authorization.scheme = pieces[0];
req.authorization.credentials = pieces[1];
try {
switch (pieces[0].toLowerCase()) {
case 'basic':
req.authorization.basic = parseBasic(pieces[1]);
req.username = req.authorization.basic.username;
break;
case 'signature':
req.authorization.signature =
parseSignature(req);
req.username =
req.authorization.signature.keyId;
break;
default:
break;
}
} catch (e2) {
return (next(e2));
}
return (next());
}
return (parseAuthorization);
}
module.exports = authorizationParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var errors = require('../errors');
var bodyReader = require('./body_reader');
var jsonParser = require('./json_body_parser');
var formParser = require('./form_body_parser');
var multipartParser = require('./multipart_parser');
var fieldedTextParser = require('./fielded_text_body_parser.js');
///--- Globals
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
///--- API
function bodyParser(options) {
assert.optionalObject(options, 'options');
options = options || {};
options.bodyReader = true;
var read = bodyReader(options);
var parseForm = formParser(options);
var parseJson = jsonParser(options);
var parseMultipart = multipartParser(options);
var parseFieldedText = fieldedTextParser(options);
function parseBody(req, res, next) {
// Allow use of 'requestBodyOnGet' flag to allow for merging of
// the request body of a GET request into req.params
if (req.method === 'HEAD') {
next();
return;
}
if (req.method === 'GET') {
if (!options.requestBodyOnGet) {
next();
return;
}
}
if (req.contentLength() === 0 && !req.isChunked()) {
next();
return;
}
var parser;
var type = req.contentType();
switch (type) {
case 'application/json':
parser = parseJson[0];
break;
case 'application/x-www-form-urlencoded':
parser = parseForm[0];
break;
case 'multipart/form-data':
parser = parseMultipart;
break;
case 'text/tsv':
parser = parseFieldedText;
break;
case 'text/tab-separated-values':
parser = parseFieldedText;
break;
case 'text/csv':
parser = parseFieldedText;
break;
default:
break;
}
if (parser) {
parser(req, res, next);
} else if (options && options.rejectUnknown) {
next(new UnsupportedMediaTypeError(type));
} else {
next();
}
}
return ([read, parseBody]);
}
module.exports = bodyParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var zlib = require('zlib');
var assert = require('assert-plus');
var errors = require('../errors');
///--- Globals
var BadDigestError = errors.BadDigestError;
var InvalidContentError = errors.InvalidContentError;
var RequestEntityTooLargeError = errors.RequestEntityTooLargeError;
var MD5_MSG = 'Content-MD5 \'%s\' didn\'t match \'%s\'';
///--- Helpers
function createBodyWriter(req) {
var contentType = req.contentType();
if (!contentType ||
contentType === 'application/json' ||
contentType === 'application/x-www-form-urlencoded' ||
contentType === 'multipart/form-data' ||
contentType.substr(0, 5) === 'text/') {
req.body = '';
return (function (chunk) {
req.body += chunk.toString('utf8');
});
}
req.body = new Buffer(0);
return (function (chunk) {
req.body = Buffer.concat([req.body, chunk]);
});
}
///--- API
function bodyReader(options) {
options = options || {};
assert.object(options, 'options');
var maxBodySize = options.maxBodySize || 0;
function readBody(req, res, next) {
if ((req.getContentLength() === 0 && !req.isChunked()) ||
req.contentType() === 'multipart/form-data' ||
req.contentType() === 'application/octet-stream') {
next();
return;
}
var bodyWriter = createBodyWriter(req);
function done() {
if (maxBodySize && bytesReceived > maxBodySize) {
var msg = 'Request body size exceeds ' +
maxBodySize;
next(new RequestEntityTooLargeError(msg));
return;
}
if (!req.body.length) {
next();
return;
}
if (hash && md5 !== (digest = hash.digest('base64'))) {
next(new BadDigestError(MD5_MSG, md5, digest));
return;
}
next();
}
var bytesReceived = 0;
var digest;
var gz;
var hash;
var md5;
if ((md5 = req.headers['content-md5']))
hash = crypto.createHash('md5');
if (req.headers['content-encoding'] === 'gzip') {
gz = zlib.createGunzip();
gz.on('data', function onData(chunk) {
bodyWriter(chunk);
});
gz.once('end', done);
req.once('end', gz.end.bind(gz));
} else {
req.once('end', done);
}
req.on('data', function onRequestData(chunk) {
if (maxBodySize) {
bytesReceived += chunk.length;
if (bytesReceived > maxBodySize)
return;
}
if (hash)
hash.update(chunk, 'binary');
if (gz) {
gz.write(chunk);
} else {
bodyWriter(chunk);
}
});
req.once('error', next);
req.resume();
}
return (readBody);
}
module.exports = bodyReader;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var shallowCopy = require('../utils').shallowCopy;
///--- API
function requestLogger(options) {
assert.optionalObject(options);
options = options || {};
var props;
if (options.properties) {
props = shallowCopy(options.properties);
} else {
props = {};
}
if (options.serializers)
props.serializers = options.serializers;
function bunyan(req, res, next) {
if (!req.log && !options.log) {
next();
return;
}
var log = req.log || options.log;
props.req_id = req.getId();
req.log = log.child(props, props.serializers ? false : true);
if (props.req_id)
delete props.req_id;
next();
}
return (bunyan);
}
///--- Exports
module.exports = requestLogger;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var errors = require('../errors');
///--- Globals
var BadRequestError = errors.BadRequestError;
var PreconditionFailedError = errors.PreconditionFailedError;
var IF_MATCH_FAIL = 'if-match \'%s\' didn\'t match etag \'%s\'';
var IF_NO_MATCH_FAIL = 'if-none-match \'%s\' matched etag \'%s\'';
var IF_MOD_FAIL = 'object was modified at \'%s\'; if-modified-since \'%s\'';
var IF_UNMOD_FAIL = 'object was modified at \'%s\'; if-unmodified-since \'%s\'';
///--- API
// Reference RFC2616 section 14 for an explanation of what this all does.
function checkIfMatch(req, res, next) {
var clientETags;
var cur;
var etag = res.etag || res.getHeader('etag') || '';
var ifMatch;
var matched = false;
if ((ifMatch = req.headers['if-match'])) {
/* JSSTYLED */
clientETags = ifMatch.split(/\s*,\s*/);
for (var i = 0; i < clientETags.length; i++) {
cur = clientETags[i];
// only strong comparison
/* JSSTYLED */
cur = cur.replace(/^W\//, '');
/* JSSTYLED */
cur = cur.replace(/^"(\w*)"$/, '$1');
if (cur === '*' || cur === etag) {
matched = true;
break;
}
}
if (!matched) {
var err = new PreconditionFailedError(IF_MATCH_FAIL,
ifMatch,
etag);
return (next(err));
}
}
return (next());
}
function checkIfNoneMatch(req, res, next) {
var clientETags;
var cur;
var etag = res.etag || res.getHeader('etag') || '';
var ifNoneMatch;
var matched = false;
if ((ifNoneMatch = req.headers['if-none-match'])) {
/* JSSTYLED */
clientETags = ifNoneMatch.split(/\s*,\s*/);
for (var i = 0; i < clientETags.length; i++) {
cur = clientETags[i];
// ignore weak validation
/* JSSTYLED */
cur = cur.replace(/^W\//, '');
/* JSSTYLED */
cur = cur.replace(/^"(\w*)"$/, '$1');
if (cur === '*' || cur === etag) {
matched = true;
break;
}
}
if (!matched)
return (next());
if (req.method !== 'GET' && req.method !== 'HEAD') {
var err = new PreconditionFailedError(IF_NO_MATCH_FAIL,
ifNoneMatch,
etag);
return (next(err));
}
res.send(304);
return (next(false));
}
return (next());
}
function checkIfModified(req, res, next) {
var code;
var err;
var ctime = req.header('if-modified-since');
var mtime = res.mtime || res.header('Last-Modified') || '';
if (!mtime || !ctime) {
next();
return;
}
try {
//
// TODO handle Range header modifications
//
// Note: this is not technically correct as per 2616 -
// 2616 only specifies semantics for GET requests, not
// any other method - but using if-modified-since with a
// PUT or DELETE seems like returning 412 is sane
//
if (Date.parse(mtime) <= Date.parse(ctime)) {
switch (req.method) {
case 'GET':
case 'HEAD':
code = 304;
break;
default:
err = new PreconditionFailedError(IF_MOD_FAIL,
mtime,
ctime);
break;
}
}
} catch (e) {
next(new BadRequestError(e.message));
return;
}
if (code !== undefined) {
res.send(code);
next(false);
return;
}
next(err);
}
function checkIfUnmodified(req, res, next) {
var err;
var ctime = req.headers['if-unmodified-since'];
var mtime = res.mtime || res.header('Last-Modified') || '';
if (!mtime || !ctime) {
next();
return;
}
try {
if (Date.parse(mtime) > Date.parse(ctime)) {
err = new PreconditionFailedError(IF_UNMOD_FAIL,
mtime,
ctime);
}
} catch (e) {
next(new BadRequestError(e.message));
return;
}
next(err);
}
///--- Exports
/**
* Returns a set of plugins that will compare an already set ETag header with
* the client's If-Match and If-None-Match header, and an already set
* Last-Modified header with the client's If-Modified-Since and
* If-Unmodified-Since header.
*/
function conditionalRequest() {
var chain = [
checkIfMatch,
checkIfNoneMatch,
checkIfModified,
checkIfUnmodified
];
return (chain);
}
module.exports = conditionalRequest;
// Copyright 2013 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
///--- Globals
var ALLOW_HEADERS = [
'accept',
'accept-version',
'content-type',
'request-id',
'origin',
'x-api-version',
'x-request-id'
];
var EXPOSE_HEADERS = [
'api-version',
'content-length',
'content-md5',
'content-type',
'date',
'request-id',
'response-time'
];
// Normal
var AC_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
var AC_ALLOW_CREDS = 'Access-Control-Allow-Credentials';
var AC_EXPOSE_HEADERS = 'Access-Control-Expose-Headers';
///--- Internal Functions
function matchOrigin(req, origins) {
var origin = req.headers['origin'];
function belongs(o) {
if (origin === o || o === '*') {
origin = o;
return (true);
}
return (false);
}
return ((origin && origins.some(belongs)) ? origin : false);
}
///--- API
//
// From http://www.w3.org/TR/cors/#resource-processing-model
//
// If "simple" request (paraphrased):
//
// 1. If the Origin header is not set, or if the value of Origin is not a
// case-sensitive match to any values listed in `opts.origins`, do not
// send any CORS headers
//
// 2. If the resource supports credentials add a single
// 'Access-Control-Allow-Credentials' header with the value as "true", and
// ensure 'AC-Allow-Origin' is not '*', but is the request header value,
// otherwise add a single Access-Control-Allow-Origin header, with either the
// value of the Origin header or the string "*" as value
//
// 3. Add Access-Control-Expose-Headers as appropriate
//
// Pre-flight requests are handled by the router internally
//
function cors(opts) {
assert.optionalObject(opts, 'options');
opts = opts || {};
assert.optionalArrayOfString(opts.origins, 'options.origins');
assert.optionalBool(opts.credentials, 'options.credentials');
assert.optionalArrayOfString(opts.headers, 'options.headers');
cors.credentials = opts.credentials;
cors.origins = opts.origins;
var headers = (opts.headers || []).slice(0);
var origins = opts.origins || ['*'];
EXPOSE_HEADERS.forEach(function (h) {
if (headers.indexOf(h) === -1)
headers.push(h);
});
// Handler for simple requests
function restifyCORSSimple(req, res, next) {
var origin;
if (!(origin = matchOrigin(req, origins))) {
next();
return;
}
function corsOnHeader() {
origin = req.headers['origin'];
if (opts.credentials) {
res.setHeader(AC_ALLOW_ORIGIN, origin);
res.setHeader(AC_ALLOW_CREDS, 'true');
} else {
res.setHeader(AC_ALLOW_ORIGIN, origin);
}
res.setHeader(AC_EXPOSE_HEADERS, headers.join(', '));
}
res.once('header', corsOnHeader);
next();
}
return (restifyCORSSimple);
}
///--- Exports
module.exports = cors;
// All of these are needed for the pre-flight code over in lib/router.js
cors.ALLOW_HEADERS = ALLOW_HEADERS;
cors.credentials = false;
cors.origins = [];
cors.matchOrigin = matchOrigin;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var errors = require('../errors');
///--- Globals
var InvalidHeaderError = errors.InvalidHeaderError;
var RequestExpiredError = errors.RequestExpiredError;
var BAD_MSG = 'Date header is invalid';
var OLD_MSG = 'Date header %s is too old';
///--- API
/**
* Returns a plugin that will parse the Date header (if present) and check for
* an "expired" request, where expired means the request originated at a time
* before ($now - $clockSkew). The default clockSkew allowance is 5m (thanks
* Kerberos!)
*
* @param {Number} clockSkew optional age of time (in seconds).
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function dateParser(clockSkew) {
if (!clockSkew)
clockSkew = 300;
assert.number(clockSkew, 'clockSkew');
clockSkew = clockSkew * 1000;
function parseDate(req, res, next) {
if (!req.headers.date)
return (next());
var e;
var date = req.headers.date;
var log = req.log;
try {
var now = Date.now();
var sent = new Date(date).getTime();
if (log.trace()) {
log.trace({
allowedSkew: clockSkew,
now: now,
sent: sent
}, 'Checking clock skew');
}
if ((now - sent) > clockSkew) {
e = new RequestExpiredError(OLD_MSG, date);
return (next(e));
}
} catch (err) {
log.trace({
err: err
}, 'Bad Date header: %s', date);
e = new InvalidHeaderError(BAD_MSG, date);
return (next(e));
}
return (next());
}
return (parseDate);
}
module.exports = dateParser;
/**
* Dependencies
*/
var csv = require('csv');
var assert = require('assert-plus');
var bodyReader = require('./body_reader');
var errors = require('../errors');
///--- API
/**
* Returns a plugin that will parse the HTTP request body if the
* contentType is `text/csv` or `text/tsv`
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function fieldedTextParser(options) {
assert.optionalObject(options, 'options');
options = options || {};
function parseFieldedText(req, res, next) {
var contentType = req.getContentType();
if (contentType !== 'text/csv' &&
contentType !== 'text/tsv' &&
contentType !== 'text/tab-separated-values' || !req.body) {
next();
return;
}
var hDelimiter = req.headers['x-content-delimiter'];
var hEscape = req.headers['x-content-escape'];
var hQuote = req.headers['x-content-quote'];
var hColumns = req.headers['x-content-columns'];
var delimiter = (contentType === 'text/tsv') ? '\t' : ',';
delimiter = (hDelimiter) ? hDelimiter : delimiter;
var escape = (hEscape) ? hEscape : '\\';
var quote = (hQuote) ? hQuote : '"';
var columns = (hColumns) ? hColumns : true;
var parsedBody = [];
csv()
.from(req.body, {
delimiter: delimiter,
quote: quote,
escape: escape,
columns: columns
})
.on('record', function (row, index) {
row.index = index;
parsedBody.push(row);
})
.on('end', function (count) {
req.body = parsedBody;
return (next());
})
.on('error', function (error) {
return (next(error));
});
}
var chain = [];
if (!options.bodyReader) {
chain.push(bodyReader(options));
}
chain.push(parseFieldedText);
return (chain);
}
module.exports = fieldedTextParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var assert = require('assert-plus');
var querystring = require('qs');
var bodyReader = require('./body_reader');
var errors = require('../errors');
///--- Globals
var MIME_TYPE = 'application/x-www-form-urlencoded';
///--- API
/**
* Returns a plugin that will parse the HTTP request body IFF the
* contentType is application/x-www-form-urlencoded.
*
* If req.params already contains a given key, that key is skipped and an
* error is logged.
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function urlEncodedBodyParser(options) {
options = options || {};
assert.object(options, 'options');
var override = options.overrideParams;
function parseUrlEncodedBody(req, res, next) {
if (req.getContentType() !== MIME_TYPE || !req.body) {
next();
return;
}
try {
var params = querystring.parse(req.body);
if (options.mapParams !== false) {
var keys = Object.keys(params);
keys.forEach(function (k) {
var p = req.params[k];
if (p && !override)
return (false);
req.params[k] = params[k];
return (true);
});
} else {
req._body = req.body;
req.body = params;
}
} catch (e) {
next(new errors.InvalidContentError(e.message));
return;
}
req.log.trace('req.params now: %j', req.params);
next();
}
var chain = [];
if (!options.bodyReader)
chain.push(bodyReader(options));
chain.push(parseUrlEncodedBody);
return (chain);
}
module.exports = urlEncodedBodyParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var httpDate = require('../http_date');
///--- Globals
var ALLOW_HEADERS = [
'Accept',
'Accept-Version',
'Content-Length',
'Content-MD5',
'Content-Type',
'Date',
'Api-Version',
'Response-Time'
].join(', ');
var EXPOSE_HEADERS = [
'Api-Version',
'Request-Id',
'Response-Time'
].join(', ');
///--- API
function setHeaders(req, res) {
var hash;
var now = new Date();
var methods;
if (!res.getHeader('Access-Control-Allow-Origin'))
res.setHeader('Access-Control-Allow-Origin', '*');
if (!res.getHeader('Access-Control-Allow-Headers'))
res.setHeader('Access-Control-Allow-Headers', ALLOW_HEADERS);
if (!res.getHeader('Access-Control-Allow-Methods')) {
if (res.methods && res.methods.length > 0) {
methods = res.methods.join(', ');
res.setHeader('Access-Control-Allow-Methods', methods);
}
}
if (!res.getHeader('Access-Control-Expose-Headers'))
res.setHeader('Access-Control-Expose-Headers', EXPOSE_HEADERS);
if (!res.getHeader('Connection')) {
res.setHeader('Connection',
req.isKeepAlive() ? 'Keep-Alive' : 'close');
}
if (res._data && !res.getHeader('Content-MD5')) {
hash = crypto.createHash('md5');
hash.update(res._data);
res.setHeader('Content-MD5', hash.digest('base64'));
}
if (!res.getHeader('Date'))
res.setHeader('Date', httpDate(now));
if (res.etag && !res.getHeader('Etag'))
res.setHeader('Etag', res.etag);
if (!res.getHeader('Server'))
res.setHeader('Server', res.serverName);
if (res.version && !res.getHeader('Api-Version'))
res.setHeader('Api-Version', res.version);
if (!res.getHeader('Request-Id'))
res.setHeader('Request-Id', req.getId());
if (!res.getHeader('Response-Time'))
res.setHeader('Response-Time', now.getTime() - req._time);
}
function fullResponse() {
function restifyResponseHeaders(req, res, next) {
res.once('header', function () {
// Restify 1.0 compatibility
if (res.defaultResponseFormatters)
res.defaultResponseFormatters(res._data);
res.emit('beforeSend', res._data, res._body);
// end backwards-compatibility
return (setHeaders(req, res));
});
return (next());
}
return (restifyResponseHeaders);
}
///--- Exports
module.exports = fullResponse;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var zlib = require('zlib');
var assert = require('assert-plus');
function _writeHead(originalFunction) {
this.removeHeader('Content-Length');
originalFunction.apply(this, Array.prototype.slice.call(arguments, 1));
}
///--- API
function gzipResponse(opts) {
assert.optionalObject(opts, 'options');
function gzip(req, res, next) {
if (!req.acceptsEncoding('gzip')) {
next();
return;
}
var gz = zlib.createGzip(opts);
gz.on('data', res.write.bind(res));
gz.once('end', res.end.bind(res));
gz.on('drain', res.emit.bind(res, 'drain'));
var origWrite = res.write;
var origEnd = res.end;
var origWriteHead = res.writeHead;
res.handledGzip = function _handledGzip() {
res.write = origWrite;
res.end = origEnd;
res.writeHead = origWriteHead;
};
res.write = gz.write.bind(gz);
res.end = gz.end.bind(gz);
res.writeHead = _writeHead.bind(res, res.writeHead);
res.setHeader('Content-Encoding', 'gzip');
next();
}
return (gzip);
}
///--- Exports
module.exports = gzipResponse;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Exports
module.exports = {
acceptParser: require('./accept'),
auditLogger: require('./audit'),
authorizationParser: require('./authorization'),
bodyParser: require('./body_parser'),
conditionalRequest: require('./conditional_request'),
CORS: require('./cors'),
dateParser: require('./date'),
jsonp: require('./jsonp'),
urlEncodedBodyParser: require('./form_body_parser'),
requestLogger: require('./bunyan'),
gzipResponse: require('./gzip'),
fullResponse: require('./full_response'),
jsonBodyParser: require('./json_body_parser'),
multipartBodyParser: require('./multipart_parser'),
queryParser: require('./query'),
sanitizePath: require('./pre/pre_path'),
serveStatic: require('./static'),
throttle: require('./throttle'),
pre: {
pause: require('./pre/pause'),
sanitizePath: require('./pre/pre_path'),
userAgentConnection: require('./pre/user_agent')
}
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var bodyReader = require('./body_reader');
var errors = require('../errors');
///--- API
/**
* Returns a plugin that will parse the HTTP request body IFF the
* contentType is application/json.
*
* If req.params already contains a given key, that key is skipped and an
* error is logged.
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function jsonBodyParser(options) {
assert.optionalObject(options, 'options');
options = options || {};
var override = options.overrideParams;
function parseJson(req, res, next) {
if (req.getContentType() !== 'application/json' || !req.body) {
next();
return;
}
var params;
try {
params = JSON.parse(req.body);
} catch (e) {
next(new errors.InvalidContentError('Invalid JSON: ' +
e.message));
return;
}
if (options.mapParams !== false) {
if (Array.isArray(params)) {
req.params = params;
} else if (typeof (params) === 'object') {
Object.keys(params).forEach(function (k) {
var p = req.params[k];
if (p && !override)
return (false);
req.params[k] = params[k];
return (true);
});
} else {
req.params = params;
}
} else {
req._body = req.body;
}
req.body = params;
next();
}
var chain = [];
if (!options.bodyReader)
chain.push(bodyReader(options));
chain.push(parseJson);
return (chain);
}
module.exports = jsonBodyParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var qs = require('qs');
///--- API
function jsonp() {
function _jsonp(req, res, next) {
var q = req.getQuery();
// If the query plugin wasn't used, we need to hack it in now
if (typeof (q) === 'string')
req.query = qs.parse(q);
if (req.query.callback || req.query.jsonp)
res.setHeader('Content-Type', 'application/javascript');
next();
}
return (_jsonp);
}
module.exports = jsonp;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
var formidable = require('formidable');
var errors = require('../errors');
///--- Globals
var BadRequestError = errors.BadRequestError;
///--- API
/**
* Returns a plugin that will parse the HTTP request body IFF the
* contentType is multipart/form-data
*
* If req.params already contains a given key, that key is skipped and an
* error is logged.
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function multipartBodyParser(options) {
if (!options)
options = {};
assert.object(options, 'options');
var override = options.overrideParams;
function parseMultipartBody(req, res, next) {
if (req.getContentType() !== 'multipart/form-data' ||
(req.getContentLength() === 0 && !req.isChunked()))
return (next());
var form = new formidable.IncomingForm();
form.keepExtensions = options.keepExtensions ? true : false;
if (options.uploadDir)
form.uploadDir = options.uploadDir;
form.parse(req, function (err, fields, files) {
if (err)
return (next(new BadRequestError(err.message)));
req.body = fields;
req.files = files;
if (options.mapParams !== false) {
Object.keys(fields).forEach(function (k) {
if (req.params[k] && !override)
return (false);
req.params[k] = fields[k];
return (true);
});
Object.keys(files).forEach(function (f) {
if (req.params[f] && !override)
return (false);
var fs = require('fs');
return fs.readFile(
files[f].path,
'utf8',
function (ex, data) {
if (ex) {
return (false);
}
req.params[f] = data;
return (true);
});
});
}
return (next());
});
return (false);
}
return (parseMultipartBody);
}
module.exports = multipartBodyParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Helpers
function pauseStream(stream) {
function _buffer(chunk) {
stream.__buffered.push(chunk);
}
function _catchEnd(chunk) {
stream.__rstfy_ended = true;
}
stream.__rstfy_ended = false;
stream.__rstfy_paused = true;
stream.__buffered = [];
stream.on('data', _buffer);
stream.once('end', _catchEnd);
stream.pause();
stream._resume = stream.resume;
stream.resume = function _rstfy_resume() {
if (!stream.__rstfy_paused)
return;
stream.removeListener('data', _buffer);
stream.removeListener('end', _catchEnd);
stream.__buffered.forEach(stream.emit.bind(stream, 'data'));
stream.__buffered.length = 0;
stream._resume();
stream.resume = stream._resume;
if (stream.__rstfy_ended)
stream.emit('end');
};
}
///--- Exports
module.exports = function pause() {
function prePause(req, res, next) {
pauseStream(req);
next();
}
return (prePause);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
///--- Helpers
/**
* Cleans up sloppy URLs on the request object, like /foo////bar/// to /foo/bar.
*
*/
function strip(path) {
var cur;
var next;
var str = '';
for (var i = 0; i < path.length; i++) {
cur = path.charAt(i);
if (i !== path.length - 1)
next = path.charAt(i + 1);
if (cur === '/' && (next === '/' || (next === '?' && i > 0)))
continue;
str += cur;
}
return (str);
}
///--- Exports
module.exports = function sanitizePath(options) {
options = options || {};
function _sanitizePath(req, res, next) {
req.url = strip(req.url);
next();
}
return (_sanitizePath);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
///--- API
/**
* This basically exists for curl. curl on HEAD requests usually
* just sits there and hangs, unless you explicitly set
* Connection:close. And in general, you probably want to set
* Connection: close to curl anyway.
*
* Also, because curl spits out an annoying message to stderr about
* remaining bytes if content-length is set, this plugin also drops
* the content-length header (some user agents handle it and want it,
* curl does not).
*
* To be slightly more generic, the options block takes a user
* agent regexp, however.
*/
function userAgentConnection(opts) {
assert.optionalObject(opts, 'options');
opts = opts || {};
assert.optionalObject(opts.userAgentRegExp, 'options.userAgentRegExp');
var re = opts.userAgentRegExp;
if (!re)
re = /^curl.+/;
function handleUserAgent(req, res, next) {
var ua = req.headers['user-agent'];
if (ua && re.test(ua))
res.setHeader('Connection', 'close');
if (req.method === 'HEAD') {
res.once('header',
res.removeHeader.bind(res, 'content-length'));
}
next();
}
return (handleUserAgent);
}
module.exports = userAgentConnection;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var qs = require('qs');
var url = require('url');
var assert = require('assert-plus');
/**
* Returns a plugin that will parse the query string, and merge the results
* into req.params.
*
* @return {Function} restify handler.
* @throws {TypeError} on bad input
*/
function queryParser(options) {
if (!options)
options = {};
assert.object(options, 'options');
function parseQueryString(req, res, next) {
if (!req.getQuery()) {
req.query = {};
return (next());
}
req._query = req.query = qs.parse(req.getQuery());
if (options.mapParams !== false) {
Object.keys(req.query).forEach(function (k) {
if (req.params[k] && !options.overrideParams)
return (false);
req.params[k] = req.query[k];
return (true);
});
}
return (next());
}
return (parseQueryString);
}
module.exports = queryParser;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var fs = require('fs');
var path = require('path');
var escapeRE = require('escape-regexp-component');
var assert = require('assert-plus');
var mime = require('mime');
var errors = require('../errors');
///--- Globals
var MethodNotAllowedError = errors.MethodNotAllowedError;
var NotAuthorizedError = errors.NotAuthorizedError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
///--- Functions
function serveStatic(opts) {
opts = opts || {};
assert.object(opts, 'options');
assert.string(opts.directory, 'options.directory');
assert.optionalNumber(opts.maxAge, 'options.maxAge');
assert.optionalObject(opts.match, 'options.match');
assert.optionalString(opts.charSet, 'options.charSet');
var p = path.normalize(opts.directory).replace(/\\/g, '/');
var re = new RegExp('^' + escapeRE(p) + '/?.*');
function serveFileFromStats(file, err, stats, isGzip, req, res, next) {
if (err) {
next(new ResourceNotFoundError(err,
req.path()));
return;
} else if (!stats.isFile()) {
next(new ResourceNotFoundError(req.path()));
return;
}
if (res.handledGzip && isGzip) {
res.handledGzip();
}
var fstream = fs.createReadStream(file + (isGzip ? '.gz' : ''));
var maxAge = opts.maxAge === undefined ? 3600 : opts.maxAge;
fstream.once('open', function (fd) {
res.cache({maxAge: maxAge});
res.set('Content-Length', stats.size);
res.set('Content-Type', mime.lookup(file));
res.set('Last-Modified', stats.mtime);
if (opts.charSet) {
var type = res.getHeader('Content-Type') +
'; charset=' + opts.charSet;
res.setHeader('Content-Type', type);
}
if (opts.etag) {
res.set('ETag', opts.etag(stats, opts));
}
res.writeHead(200);
fstream.pipe(res);
fstream.once('end', function () {
next(false);
});
});
}
function serveNormal(file, req, res, next) {
fs.stat(file, function (err, stats) {
if (!err && stats.isDirectory() && opts.default) {
// Serve an index.html page or similar
file = path.join(file, opts.default);
fs.stat(file, function (dirErr, dirStats) {
serveFileFromStats(file,
dirErr,
dirStats,
false,
req,
res,
next);
});
} else {
serveFileFromStats(file,
err,
stats,
false,
req,
res,
next);
}
});
}
function serve(req, res, next) {
var file = path.join(opts.directory,
decodeURIComponent(req.path()));
if (req.method !== 'GET' && req.method !== 'HEAD') {
next(new MethodNotAllowedError(req.method));
return;
}
if (!re.test(file.replace(/\\/g, '/'))) {
next(new NotAuthorizedError(req.path()));
return;
}
if (opts.match && !opts.match.test(file)) {
next(new NotAuthorizedError(req.path()));
return;
}
if (opts.gzip && req.acceptsEncoding('gzip')) {
fs.stat(file + '.gz', function (err, stats) {
if (!err) {
res.setHeader('Content-Encoding', 'gzip');
serveFileFromStats(file,
err,
stats,
true,
req,
res,
next);
} else {
serveNormal(file, req, res, next);
}
});
} else {
serveNormal(file, req, res, next);
}
}
return (serve);
}
module.exports = serveStatic;
// Copyright 2012 Mark Cavage <mcavage@gmail.com> All rights reserved.
var sprintf = require('util').format;
var assert = require('assert-plus');
var LRU = require('lru-cache');
var errors = require('../errors');
///--- Globals
var TooManyRequestsError = errors.TooManyRequestsError;
var MESSAGE = 'You have exceeded your request rate of %s r/s.';
///--- Helpers
function xor() {
var x = false;
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] && !x)
x = true;
else if (arguments[i] && x)
return (false);
}
return (x);
}
///--- Internal Class (TokenBucket)
/**
* An implementation of the Token Bucket algorithm.
*
* Basically, in network throttling, there are two "mainstream"
* algorithms for throttling requests, Token Bucket and Leaky Bucket.
* For restify, I went with Token Bucket. For a good description of the
* algorithm, see: http://en.wikipedia.org/wiki/Token_bucket
*
* In the options object, you pass in the total tokens and the fill rate.
* Practically speaking, this means "allow `fill rate` requests/second,
* with bursts up to `total tokens`". Note that the bucket is initialized
* to full.
*
* Also, in googling, I came across a concise python implementation, so this
* is just a port of that. Thanks http://code.activestate.com/recipes/511490 !
*
* @param {Object} options contains the parameters:
* - {Number} capacity the maximum burst.
* - {Number} fillRate the rate to refill tokens.
*/
function TokenBucket(options) {
assert.object(options, 'options');
assert.number(options.capacity, 'options.capacity');
assert.number(options.fillRate, 'options.fillRate');
this.tokens = this.capacity = options.capacity;
this.fillRate = options.fillRate;
this.time = Date.now();
}
/**
* Consume N tokens from the bucket.
*
* If there is not capacity, the tokens are not pulled from the bucket.
*
* @param {Number} tokens the number of tokens to pull out.
* @return {Boolean} true if capacity, false otherwise.
*/
TokenBucket.prototype.consume = function consume(tokens) {
if (tokens <= this._fill()) {
this.tokens -= tokens;
return (true);
}
return (false);
};
/**
* Fills the bucket with more tokens.
*
* Rather than do some whacky setTimeout() deal, we just approximate refilling
* the bucket by tracking elapsed time from the last time we touched the bucket.
*
* Simply, we set the bucket size to min(totalTokens,
* current + (fillRate * elapsed time)).
*
* @return {Number} the current number of tokens in the bucket.
*/
TokenBucket.prototype._fill = function _fill() {
var now = Date.now();
if (now < this.time) // reset account for clock drift (like DST)
this.time = now - 1000;
if (this.tokens < this.capacity) {
var delta = this.fillRate * ((now - this.time) / 1000);
this.tokens = Math.min(this.capacity, this.tokens + delta);
}
this.time = now;
return (this.tokens);
};
///--- Internal Class (TokenTable)
// Just a wrapper over LRU that supports put/get to store token -> bucket
// mappings
function TokenTable(options) {
assert.object(options, 'options');
this.table = new LRU(options.size || 10000);
}
TokenTable.prototype.put = function put(key, value) {
this.table.set(key, value);
};
TokenTable.prototype.get = function get(key) {
return (this.table.get(key));
};
///--- Exported API
/**
* Creates an API rate limiter that can be plugged into the standard
* restify request handling pipeline.
*
* This throttle gives you three options on which to throttle:
* username, IP address and 'X-Forwarded-For'. IP/XFF is a /32 match,
* so keep that in mind if using it. Username takes the user specified
* on req.username (which gets automagically set for supported Authorization
* types; otherwise set it yourself with a filter that runs before this).
*
* In both cases, you can set a `burst` and a `rate` (in requests/seconds),
* as an integer/float. Those really translate to the `TokenBucket`
* algorithm, so read up on that (or see the comments above...).
*
* In either case, the top level options burst/rate set a blanket throttling
* rate, and then you can pass in an `overrides` object with rates for
* specific users/IPs. You should use overrides sparingly, as we make a new
* TokenBucket to track each.
*
* On the `options` object ip and username are treated as an XOR.
*
* An example options object with overrides:
*
* {
* burst: 10, // Max 10 concurrent requests (if tokens)
* rate: 0.5, // Steady state: 1 request / 2 seconds
* ip: true, // throttle per IP
* overrides: {
* '192.168.1.1': {
* burst: 0,
* rate: 0 // unlimited
* }
* }
*
*
* @param {Object} options required options with:
* - {Number} burst (required).
* - {Number} rate (required).
* - {Boolean} ip (optional).
* - {Boolean} username (optional).
* - {Boolean} xff (optional).
* - {Object} overrides (optional).
* - {Object} tokensTable: a storage engine this plugin will
* use to store throttling keys -> bucket mappings.
* If you don't specify this, the default is to
* use an in-memory O(1) LRU, with 10k distinct
* keys. Any implementation just needs to support
* put/get.
* - {Number} maxKeys: If using the default implementation,
* you can specify how large you want the table to
* be. Default is 10000.
* @return {Function} of type f(req, res, next) to be plugged into a route.
* @throws {TypeError} on bad input.
*/
function throttle(options) {
assert.object(options, 'options');
assert.number(options.burst, 'options.burst');
assert.number(options.rate, 'options.rate');
if (!xor(options.ip, options.xff, options.username))
throw new Error('(ip ^ username ^ xff)');
var burst = options.burst;
var rate = options.rate;
var table = options.tokensTable ||
new TokenTable({size: options.maxKeys});
function rateLimit(req, res, next) {
var attr;
if (options.ip) {
attr = req.connection.remoteAddress;
} else if (options.xff) {
attr = req.headers['x-forwarded-for'];
} else if (options.username) {
attr = req.username;
} else {
req.log.warn({config: options},
'Invalid throttle configuration');
return (next());
}
// Before bothering with overrides, see if this request
// even matches
if (!attr)
return (next());
// Check the overrides
if (options.overrides &&
options.overrides[attr] &&
options.overrides[attr].burst !== undefined &&
options.overrides[attr].rate !== undefined) {
burst = options.overrides[attr].burst;
rate = options.overrides[attr].rate;
}
if (!rate || !burst)
return (next());
var bucket = table.get(attr);
if (!bucket) {
bucket = new TokenBucket({
capacity: burst,
fillRate: rate
});
table.put(attr, bucket);
}
req.log.trace('Throttle(%s): num_tokens= %d',
attr, bucket.tokens);
if (!bucket.consume(1)) {
req.log.info({
address: req.connection.remoteAddress || '?',
method: req.method,
url: req.url,
user: req.username || '?'
}, 'Throttling');
// Until https://github.com/joyent/node/pull/2371 is in
var msg = sprintf(MESSAGE, rate);
return (next(new TooManyRequestsError(msg)));
}
return (next());
}
return (rateLimit);
}
module.exports = throttle;
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var http = require('http');
var url = require('url');
var sprintf = require('util').format;
var assert = require('assert-plus');
var mime = require('mime');
var Negotatior = require('negotiator');
var uuid = require('node-uuid');
var utils = require('./utils');
///--- Globals
var Request = http.IncomingMessage;
var parseAccept = utils.parseAccept;
var sanitizePath = utils.sanitizePath;
///-- Helpers
function negotiator(req) {
var h = req.headers;
if (!req._negotatiator) {
req._negotiator = new Negotatior({
headers: {
accept: h.accept || '*/*',
'accept-encoding': h['accept-encoding'] ||
'identity'
}
});
}
return (req._negotiator);
}
///--- API
///--- Patches
Request.prototype.absoluteUri = function absoluteUri(path) {
assert.string(path, 'path');
var protocol = this.secure ? 'https://' : 'http://';
var hostname = this.headers['host'];
return (url.resolve(protocol + hostname + this.path + '/', path));
};
Request.prototype.accepts = function accepts(types) {
if (typeof (types) === 'string')
types = [types];
types = types.map(function (t) {
assert.string(t, 'type');
if (t.indexOf('/') === -1)
t = mime.lookup(t);
return (t);
});
negotiator(this);
return (this._negotiator.preferredMediaType(types));
};
Request.prototype.acceptsEncoding = function acceptsEncoding(types) {
if (typeof (types) === 'string')
types = [types];
assert.arrayOfString(types, 'types');
negotiator(this);
return (this._negotiator.preferredEncoding(types));
};
Request.prototype.getContentLength = function getContentLength() {
if (this._clen !== undefined)
return (this._clen === false ? undefined : this._clen);
// We should not attempt to read and parse the body of an
// Upgrade request, so force Content-Length to zero:
if (this.isUpgradeRequest())
return (0);
var len = this.header('content-length');
if (!len) {
this._clen = false;
} else {
this._clen = parseInt(len, 10);
}
return (this._clen === false ? undefined : this._clen);
};
Request.prototype.contentLength = Request.prototype.getContentLength;
Request.prototype.getContentType = function getContentType() {
if (this._contentType !== undefined)
return (this._contentType);
var index;
var type = this.headers['content-type'];
if (!type) {
// RFC2616 section 7.2.1
this._contentType = 'application/octet-stream';
} else {
if ((index = type.indexOf(';')) === -1) {
this._contentType = type;
} else {
this._contentType = type.substring(0, index);
}
}
return (this._contentType);
};
Request.prototype.contentType = Request.prototype.getContentType;
Request.prototype.date = function date() {
if (this._date !== undefined)
return (this._date);
this._date = new Date(this._time);
return (this._date);
};
Request.prototype.getHref = function getHref() {
if (this._href !== undefined)
return (this._href);
this._href = this.getUrl().href;
return (this._href);
};
Request.prototype.href = Request.prototype.getHref;
Request.prototype.getId = function getId() {
if (this._id !== undefined)
return (this._id);
this._id = this.headers['request-id'] ||
this.headers['x-request-id'] ||
uuid.v1();
return (this._id);
};
Request.prototype.id = Request.prototype.getId;
Request.prototype.getPath = function getPath() {
if (this._path !== undefined)
return (this._path);
this._path = this.getUrl().pathname;
return (this._path);
};
Request.prototype.path = Request.prototype.getPath;
Request.prototype.getQuery = function getQuery() {
if (this._query !== undefined)
return (this._query);
this._query = this.getUrl().query || {};
return (this._query);
};
Request.prototype.query = Request.prototype.getQuery;
Request.prototype.time = function time() {
return (this._time);
};
Request.prototype.getUrl = function getUrl() {
if (this._url !== undefined)
return (this._url);
this._url = url.parse(this.url);
return (this._url);
};
Request.prototype.getVersion = function getVersion() {
if (this._version !== undefined)
return (this._version);
this._version =
this.headers['accept-version'] ||
this.headers['x-api-version'] ||
'*';
return (this._version);
};
Request.prototype.version = Request.prototype.getVersion;
Request.prototype.header = function header(name, value) {
assert.string(name, 'name');
name = name.toLowerCase();
if (name === 'referer' || name === 'referrer')
name = 'referer';
return (this.headers[name] || value);
};
Request.prototype.trailer = function trailer(name, value) {
assert.string(name, 'name');
name = name.toLowerCase();
if (name === 'referer' || name === 'referrer')
name = 'referer';
return ((this.trailers || {})[name] || value);
};
Request.prototype.is = function is(type) {
assert.string(type, 'type');
var contentType = this.getContentType();
var matches = true;
if (!contentType)
return (false);
if (type.indexOf('/') === -1)
type = mime.lookup(type);
if (type.indexOf('*') !== -1) {
type = type.split('/');
contentType = contentType.split('/');
matches &= (type[0] === '*' || type[0] === contentType[0]);
matches &= (type[1] === '*' || type[1] === contentType[1]);
} else {
matches = (contentType === type);
}
return (matches);
};
Request.prototype.isChunked = function isChunked() {
return (this.headers['transfer-encoding'] === 'chunked');
};
Request.prototype.isKeepAlive = function isKeepAlive() {
if (this._keepAlive !== undefined)
return (this._keepAlive);
if (this.headers.connection) {
this._keepAlive = /keep-alive/i.test(this.headers.connection);
} else {
this._keepAlive = this.httpVersion === '1.0' ? false : true;
}
return (this._keepAlive);
};
Request.prototype.isSecure = function isSecure() {
if (this._secure !== undefined)
return (this._secure);
this._secure = this.connection.encrypted ? true : false;
return (this._secure);
};
Request.prototype.isUpgradeRequest = function isUpgradeRequest() {
if (this._upgradeRequest !== undefined)
return (this._upgradeRequest);
else
return (false);
};
Request.prototype.isUpload = function isUpload() {
var m = this.method;
return (m === 'PATH' || m === 'POST' || m === 'PUT');
};
Request.prototype.toString = function toString() {
var headers = '';
var self = this;
var str;
Object.keys(this.headers).forEach(function (k) {
headers += sprintf('%s: %s\n', k, self.headers[k]);
});
str = sprintf('%s %s HTTP/%s\n%s',
this.method,
this.url,
this.httpVersion,
headers);
return (str);
};
Request.prototype.userAgent = function userAgent() {
return (this.headers['user-agent']);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var crypto = require('crypto');
var http = require('http');
var sprintf = require('util').format;
var assert = require('assert-plus');
var mime = require('mime');
var once = require('once');
var errors = require('./errors');
var httpDate = require('./http_date');
///--- Globals
var HttpError = errors.HttpError;
var RestError = errors.RestError;
var Response = http.ServerResponse;
///--- API
Response.prototype.cache = function cache(type, options) {
if (typeof (type) !== 'string') {
options = type;
type = 'public';
}
if (options && options.maxAge !== undefined) {
assert.number(options.maxAge, 'options.maxAge');
type += ', max-age=' + options.maxAge;
}
return (this.header('Cache-Control', type));
};
Response.prototype.charSet = function charSet(type) {
assert.string(type, 'charset');
this._charSet = type;
return (this);
};
Response.prototype.format = function format(body, cb) {
var log = this.log;
var formatter;
var type = this.contentType || this.getHeader('Content-Type');
var self = this;
if (!type) {
for (var i = 0; i < this.acceptable.length; i++) {
if (this.req.accepts(this.acceptable[i])) {
type = this.acceptable[i];
break;
}
}
if (!type) {
// The importance of a status code outside of the
// 2xx range probably outweighs that of unable being to
// format the response body
if (this.statusCode >= 200 && this.statusCode < 300)
this.statusCode = 406;
return (null);
}
} else if (type.indexOf(';') !== '-1') {
type = type.split(';')[0];
}
if (!(formatter = this.formatters[type])) {
if (type.indexOf('/') === -1)
type = mime.lookup(type);
if (this.acceptable.indexOf(type) === -1)
type = 'application/octet-stream';
formatter = this.formatters[type] || this.formatters['*/*'];
if (!formatter) {
log.warn({
req: self.req
}, 'no formatter found. Returning 500.');
this.statusCode = 500;
return (null);
}
}
if (this._charSet) {
type = type + '; charset=' + this._charSet;
}
this.setHeader('Content-Type', type);
if (body instanceof Error && body.statusCode !== undefined)
this.statusCode = body.statusCode;
return (formatter.call(this, this.req, this, body, cb));
};
Response.prototype.get = function get(name) {
assert.string(name, 'name');
return (this.getHeader(name));
};
Response.prototype.getHeaders = function getHeaders() {
return (this._headers || {});
};
Response.prototype.headers = Response.prototype.getHeaders;
Response.prototype.header = function header(name, value) {
assert.string(name, 'name');
if (value === undefined)
return (this.getHeader(name));
if (value instanceof Date) {
value = httpDate(value);
} else if (arguments.length > 2) {
// Support res.header('foo', 'bar %s', 'baz');
var arg = Array.prototype.slice.call(arguments).slice(2);
value = sprintf(value, arg);
}
this.setHeader(name, value);
return (value);
};
Response.prototype.json = function json(code, object, headers) {
if (!/application\/json/.test(this.header('content-type')))
this.header('Content-Type', 'application/json');
return (this.send(code, object, headers));
};
Response.prototype.link = function link(l, rel) {
assert.string(l, 'link');
assert.string(rel, 'rel');
var _link = sprintf('<%s>; rel="%s"', l, rel);
return (this.header('Link', _link));
};
Response.prototype.send = function send(code, body, headers) {
var isHead = (this.req.method === 'HEAD');
var log = this.log;
var self = this;
if (code === undefined) {
this.statusCode = 200;
} else if (code.constructor.name === 'Number') {
this.statusCode = code;
if (body instanceof Error) {
body.statusCode = this.statusCode;
}
} else {
headers = body;
body = code;
code = null;
}
headers = headers || {};
if (log.trace()) {
var _props = {
code: self.statusCode,
headers: headers
};
if (body instanceof Error) {
_props.err = body;
} else {
_props.body = body;
}
log.trace(_props, 'response::send entered');
}
this._body = body;
function _cb(err, _body) {
self._data = _body;
Object.keys(headers).forEach(function (k) {
self.setHeader(k, headers[k]);
});
self.writeHead(self.statusCode);
if (self._data && !(isHead || code === 204 || code === 304))
self.write(self._data);
self.end();
if (log.trace())
log.trace({res: self}, 'response sent');
}
if (body) {
var ret = this.format(body, _cb);
if (!(ret instanceof Response)) {
_cb(null, ret);
}
} else {
_cb(null, null);
}
return (this);
};
Response.prototype.set = function set(name, val) {
var self = this;
if (arguments.length === 2) {
assert.string(name, 'name');
this.header(name, val);
} else {
assert.object(name, 'object');
Object.keys(name).forEach(function (k) {
self.header(k, name[k]);
});
}
return (this);
};
Response.prototype.status = function status(code) {
assert.number(code, 'code');
this.statusCode = code;
return (code);
};
Response.prototype.toString = function toString() {
var headers = this.getHeaders();
var headerString = '';
var str;
Object.keys(headers).forEach(function (k) {
headerString += k + ': ' + headers[k] + '\n';
});
str = sprintf('HTTP/1.1 %s %s\n%s',
this.statusCode,
http.STATUS_CODES[this.statusCode],
headerString);
return (str);
};
if (!Response.prototype.hasOwnProperty('_writeHead'))
Response.prototype._writeHead = Response.prototype.writeHead;
Response.prototype.writeHead = function restifyWriteHead() {
this.emit('header');
if (this.statusCode === 204 || this.statusCode === 304) {
this.removeHeader('Content-Length');
this.removeHeader('Content-MD5');
this.removeHeader('Content-Type');
this.removeHeader('Content-Encoding');
}
this._writeHead.apply(this, arguments);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var EventEmitter = require('events').EventEmitter;
var url = require('url');
var util = require('util');
var assert = require('assert-plus');
var deepEqual = require('deep-equal');
var LRU = require('lru-cache');
var Negotiator = require('negotiator');
var semver = require('semver');
var cors = require('./plugins/cors');
var errors = require('./errors');
var utils = require('./utils');
///--- Globals
var DEF_CT = 'application/octet-stream';
var maxSatisfying = semver.maxSatisfying;
var BadRequestError = errors.BadRequestError;
var InternalError = errors.InternalError;
var InvalidArgumentError = errors.InvalidArgumentError;
var InvalidVersionError = errors.InvalidVersionError;
var MethodNotAllowedError = errors.MethodNotAllowedError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
var UnsupportedMediaTypeError = errors.UnsupportedMediaTypeError;
var shallowCopy = utils.shallowCopy;
///--- Helpers
function createCachedRoute(o, path, version, route) {
if (!o.hasOwnProperty(path))
o[path] = {};
if (!o[path].hasOwnProperty(version))
o[path][version] = route;
}
function matchURL(re, req) {
var i = 0;
var result = re.exec(req.path());
var params = {};
if (!result)
return (false);
// This means the user original specified a regexp match, not a url
// string like /:foo/:bar
if (!re.restifyParams) {
for (i = 1; i < result.length; i++)
params[(i - 1)] = result[i];
return (params);
}
// This was a static string, like /foo
if (re.restifyParams.length === 0)
return (params);
// This was the "normal" case, of /foo/:id
re.restifyParams.forEach(function (p) {
if (++i < result.length)
params[p] = decodeURIComponent(result[i]);
});
return (params);
}
function compileURL(options) {
if (options.url instanceof RegExp)
return (options.url);
assert.string(options.url, 'url');
var params = [];
var pattern = '^';
var re;
var _url = url.parse(options.url).pathname;
_url.split('/').forEach(function (frag) {
if (frag.length <= 0)
return (false);
pattern += '\\/+';
if (frag.charAt(0) === ':') {
if (options.urlParamPattern) {
pattern += '(' + options.urlParamPattern + ')';
} else {
// Strictly adhere to RFC3986
pattern += '([a-zA-Z0-9-_~\\.%@]+)';
}
params.push(frag.slice(1));
} else {
pattern += frag;
}
return (true);
});
if (pattern === '^')
pattern += '\\/';
pattern += '$';
re = new RegExp(pattern, options.flags);
re.restifyParams = params;
return (re);
}
///--- API
function Router(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
EventEmitter.call(this);
this.cache = LRU({max: 100});
this.contentType = options.contentType || [];
if (!Array.isArray(this.contentType))
this.contentType = [this.contentType];
assert.arrayOfString(this.contentType, 'options.contentType');
this.log = options.log;
this.mounts = {};
this.name = 'RestifyRouter';
// A list of methods to routes
this.routes = {
DELETE: [],
GET: [],
HEAD: [],
OPTIONS: [],
PATCH: [],
POST: [],
PUT: []
};
// So we can retrun 405 vs 404, we maintain a reverse mapping of URLs
// to method
this.reverse = {};
this.versions = options.versions || options.version || [];
if (!Array.isArray(this.versions))
this.versions = [this.versions];
assert.arrayOfString(this.versions, 'options.versions');
this.versions.forEach(function (v) {
if (semver.valid(v))
return (true);
throw new InvalidArgumentError('%s is not a valid semver', v);
});
this.versions.sort();
}
util.inherits(Router, EventEmitter);
module.exports = Router;
Router.prototype.mount = function mount(options) {
assert.object(options, 'options');
assert.string(options.method, 'options.method');
assert.string(options.name, 'options.name');
var exists;
var name = options.name;
var route;
var routes = this.routes[options.method];
var self = this;
var type = options.contentType || self.contentType;
var versions = options.versions || options.version || self.versions;
if (type) {
if (!Array.isArray(type))
type = [type];
type.filter(function (t) {
return (t);
}).sort().join();
}
if (versions) {
if (!Array.isArray(versions))
versions = [versions];
versions.sort();
}
exists = routes.some(function (r) {
return (r.name === name);
});
if (exists)
return (false);
route = {
name: name,
method: options.method,
path: compileURL({
url: options.path || options.url,
flags: options.flags,
urlParamPattern: options.urlParamPattern
}),
spec: options,
types: type,
versions: versions
};
routes.push(route);
if (!this.reverse[route.path.source])
this.reverse[route.path.source] = [];
if (this.reverse[route.path.source].indexOf(route.method) === -1)
this.reverse[route.path.source].push(route.method);
this.mounts[route.name] = route;
this.emit('mount',
route.method,
route.path,
route.types,
route.versions);
return (route.name);
};
Router.prototype.unmount = function unmount(name) {
var route = this.mounts[name];
if (!route) {
this.log.warn('router.unmount(%s): route does not exist', name);
return (false);
}
var reverse = this.reverse[route.path.source];
var routes = this.routes[route.method];
this.routes[route.method] = routes.filter(function (r) {
return (r.name !== route.name);
});
this.reverse[route.path.source] = reverse.filter(function (r) {
return (r !== route.method);
});
if (this.reverse[route.path.source].length === 0)
delete this.reverse[route.path.source];
delete this.mounts[name];
return (name);
};
Router.prototype.get = function get(name, req, cb) {
var params;
var route = false;
var routes = this.routes[req.method] || [];
for (var i = 0; i < routes.length; i++) {
if (routes[i].name === name) {
route = routes[i];
try {
params = matchURL(route.path, req);
} catch (e) {
}
break;
}
}
if (route) {
cb(null, route, params || {});
} else {
cb(new InternalError());
}
};
Router.prototype.find = function find(req, res, callback) {
var candidates = [];
var ct = req.headers['content-type'] || DEF_CT;
var cacheKey = req.method + req.url + req.version() + ct;
var cacheVal;
var neg;
var params;
var r;
var reverse;
var routes = this.routes[req.method] || [];
var typed;
var versioned;
if ((cacheVal = this.cache.get(cacheKey))) {
res.methods = cacheVal.methods.slice();
callback(null, cacheVal, shallowCopy(cacheVal.params));
return;
}
for (var i = 0; i < routes.length; i++) {
try {
params = matchURL(routes[i].path, req);
} catch (e) {
this.log.trace({err: e}, 'error parsing URL');
callback(new BadRequestError(e.message));
return;
}
if (params === false)
continue;
reverse = this.reverse[routes[i].path.source];
if (routes[i].types.length && req.isUpload()) {
candidates.push({
p: params,
r: routes[i]
});
typed = true;
continue;
}
// GH-283: we want to find the latest version for a given route,
// not the first one. However, if neither the client nor
// server specified any version, we're done, because neither
// cared
if (routes[i].versions.length === 0 && req.version() === '*') {
r = routes[i];
break;
}
if (routes[i].versions.length > 0) {
candidates.push({
p: params,
r: routes[i]
});
versioned = true;
}
}
if (!r) {
// If upload and typed
if (typed) {
/* JSSTYLED */
var _t = ct.split(/\s*,\s*/);
candidates = candidates.filter(function (c) {
neg = new Negotiator({
headers: {
accept: c.r.types.join(', ')
}
});
var tmp = neg.preferredMediaType(_t);
return (tmp && tmp.length);
});
// Pick the first one in case not versioned
if (candidates.length) {
r = candidates[0].r;
params = candidates[0].p;
}
}
if (versioned) {
candidates.forEach(function (c) {
var k = c.r.versions;
var v = semver.maxSatisfying(k, req.version());
if (v) {
if (!r ||
r.versions.some(function (v2) {
return (semver.gt(v, v2));
})) {
r = c.r;
params = c.p;
}
}
});
}
}
// In order, we check if the route exists, in which case, we're good.
// Otherwise we look to see if ver was set to false; that would tell us
// we indeed did find a matching route (method+url), but the version
// field didn't line up, so we return bad version. If no route and no
// version, we now need to go walk the reverse map and look at whether
// we should return 405 or 404. If it was an OPTIONS request, we need
// to handle this having been a preflight request.
if (params && r) {
cacheVal = {
methods: reverse,
name: r.name,
params: params,
spec: r.spec
};
this.cache.set(cacheKey, cacheVal);
res.methods = reverse.slice();
callback(null, cacheVal, shallowCopy(params));
return;
}
if (typed) {
callback(new UnsupportedMediaTypeError(ct));
return;
}
if (versioned) {
callback(new InvalidVersionError('%s is not supported by %s %s',
req.version() || '?',
req.method,
req.path()));
return;
}
// This is a very generic preflight handler - it does
// not handle requiring authentication, nor does it do
// any special checking for extra user headers. The
// user will need to defined their own .opts handler to
// do that
function preflight(methods) {
var headers = req.headers['access-control-request-headers'];
var method = req.headers['access-control-request-method'];
var origin = req.headers['origin'];
if (req.method !== 'OPTIONS' || !origin || !method ||
methods.indexOf(method) === -1) {
return (false);
}
// Last, check request-headers
var ok = true;
/* JSSTYLED */
(headers || '').split(/\s*,\s*/).forEach(function (h) {
if (!h)
return;
h = h.toLowerCase();
ok = cors.ALLOW_HEADERS.indexOf(h) !== -1 && ok;
});
if (!ok)
return (false);
// Verify the incoming origin against the whitelist. The result will
// either be the matching origin, or `*`.
origin = cors.matchOrigin(req, cors.origins);
if (origin) {
res.setHeader('Access-Control-Allow-Origin', origin);
if (cors.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Methods',
methods.join(', '));
res.setHeader('Access-Control-Allow-Headers',
cors.ALLOW_HEADERS.join(', '));
res.setHeader('Access-Control-Max-Age', 3600);
return (true);
}
// Check for 405 instead of 404
var urls = Object.keys(this.reverse);
for (i = 0; i < urls.length; i++) {
if (matchURL(new RegExp(urls[i]), req)) {
res.methods = this.reverse[urls[i]].slice();
res.setHeader('Allow', res.methods.join(', '));
if (preflight(res.methods)) {
callback(null, { name: 'preflight' });
return;
}
var err = new MethodNotAllowedError('%s is not allowed',
req.method);
callback(err);
return;
}
}
callback(new ResourceNotFoundError('%s does not exist', req.url));
};
Router.prototype.toString = function toString() {
var self = this;
var str = this.name + ':\n';
Object.keys(this.routes).forEach(function (k) {
var routes = self.routes[k].map(function (r) {
return (r.name);
});
str += '\t\t' + k + ': [' + routes.join(', ') + ']\n';
});
return (str);
};
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var domain = require('domain');
var EventEmitter = require('events').EventEmitter;
var http = require('http');
var https = require('https');
var url = require('url');
var util = require('util');
var assert = require('assert-plus');
var mime = require('mime');
var once = require('once');
var spdy = require('spdy');
var uuid = require('node-uuid');
var dtrace = require('./dtrace');
var errors = require('./errors');
var formatters = require('./formatters');
var shallowCopy = require('./utils').shallowCopy;
var upgrade = require('./upgrade');
var semver = require('semver');
var maxSatisfying = semver.maxSatisfying;
// Ensure these are loaded
require('./request');
require('./response');
///--- Globals
var sprintf = util.format;
var BadMethodError = errors.BadMethodError;
var InvalidVersionError = errors.InvalidVersionError;
var ResourceNotFoundError = errors.ResourceNotFoundError;
var PROXY_EVENTS = [
'clientError',
'close',
'connection',
'error',
'listening',
'secureConnection'
];
///--- Helpers
function argumentsToChain(args, start) {
assert.ok(args);
args = Array.prototype.slice.call(args, start);
if (args.length < 0)
throw new TypeError('handler (function) required');
var chain = [];
function process(handlers) {
for (var i = 0; i < handlers.length; i++) {
if (Array.isArray(handlers[i])) {
process(handlers[i], 0);
} else {
assert.func(handlers[i], 'handler');
chain.push(handlers[i]);
}
}
return (chain);
}
return (process(args));
}
function mergeFormatters(fmt) {
var arr = [];
var defaults = Object.keys(formatters).length;
var i = 0;
var obj = {};
function addFormatter(src, k) {
assert.func(src[k], 'formatter');
var q;
var t = k;
if (k.indexOf(';') !== -1) {
/* JSSTYLED */
var tmp = k.split(/\s*;\s*/);
t = tmp[0];
if (tmp[1].indexOf('q=') !== -1) {
q = parseFloat(tmp[1].split('=')[1], 10) * 10;
}
}
if (k.indexOf('/') === -1)
k = mime.lookup(k);
obj[t] = src[k];
arr.push({
q: q || (i + defaults),
t: t
});
i++;
}
Object.keys(formatters).forEach(addFormatter.bind(this, formatters));
Object.keys(fmt || {}).forEach(addFormatter.bind(this, fmt || {}));
arr = arr.sort(function (a, b) {
return (b.q - a.q);
}).map(function (a) {
return (a.t);
});
return ({
formatters: obj,
acceptable: arr
});
}
function ifError(n) {
function _ifError(err) {
if (err) {
err._restify_next = n;
throw err;
}
}
return (_ifError);
}
function emitRouteError(server, req, res, err) {
var name;
if (err.name === 'ResourceNotFoundError') {
name = 'NotFound';
} else if (err.name === 'InvalidVersionError') {
name = 'VersionNotAllowed';
} else {
name = err.name.replace(/Error$/, '');
}
if (server.listeners(name).length > 0) {
server.emit(name, req, res, once(function () {
server.emit('after', req, res, null);
}));
} else {
res.send(err);
server.emit('after', req, res, null);
}
}
function optionsError(err, req, res) {
var code = err.statusCode;
var ok = false;
if (code === 404 && req.method === 'OPTIONS' && req.url === '*') {
res.send(200);
ok = true;
}
return (ok);
}
///--- API
function Server(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
assert.object(options.router, 'options.router');
var self = this;
EventEmitter.call(this);
this.before = [];
this.chain = [];
this.log = options.log;
this.name = options.name || 'restify';
this.router = options.router;
this.routes = {};
this.secure = false;
this.versions = options.versions || options.version || [];
var fmt = mergeFormatters(options.formatters);
this.acceptable = fmt.acceptable;
this.formatters = fmt.formatters;
if (options.spdy) {
this.spdy = true;
this.server = spdy.createServer(options.spdy);
} else if ((options.cert || options.certificate) && options.key) {
this.ca = options.ca;
this.certificate = options.certificate || options.cert;
this.key = options.key;
this.passphrase = options.passphrase || null;
this.secure = true;
this.server = https.createServer({
ca: self.ca,
cert: self.certificate,
key: self.key,
passphrase: self.passphrase,
rejectUnauthorized: options.rejectUnauthorized,
requestCert: options.requestCert,
ciphers: options.ciphers
});
} else {
this.server = http.createServer();
}
this.router.on('mount', this.emit.bind(this, 'mount'));
if (!options.handleUpgrades && PROXY_EVENTS.indexOf('upgrade') === -1)
PROXY_EVENTS.push('upgrade');
PROXY_EVENTS.forEach(function (e) {
self.server.on(e, self.emit.bind(self, e));
});
// Now the things we can't blindly proxy
this.server.on('checkContinue', function onCheckContinue(req, res) {
if (self.listeners('checkContinue').length > 0) {
self.emit('checkContinue', req, res);
return;
}
if (!options.noWriteContinue)
res.writeContinue();
self._setupRequest(req, res);
self._handle(req, res, true);
});
if (options.handleUpgrades) {
this.server.on('upgrade', function onUpgrade(req, socket, head) {
req._upgradeRequest = true;
var res = upgrade.createResponse(req, socket, head);
self._setupRequest(req, res);
self._handle(req, res);
});
}
this.server.on('request', function onRequest(req, res) {
self.emit('request', req, res);
/* JSSTYLED */
if (/^\/socket.io.*/.test(req.url))
return;
self._setupRequest(req, res);
self._handle(req, res);
});
this.__defineGetter__('maxHeadersCount', function () {
return (self.server.maxHeadersCount);
});
this.__defineSetter__('maxHeadersCount', function (c) {
self.server.maxHeadersCount = c;
return (c);
});
this.__defineGetter__('url', function () {
if (self.socketPath)
return ('http://' + self.socketPath);
var addr = self.address();
var str = '';
if (self.spdy) {
str += 'spdy://';
} else if (self.secure) {
str += 'https://';
} else {
str += 'http://';
}
if (addr) {
str += addr.address;
str += ':';
str += addr.port;
} else {
str += '169.254.0.1:0000';
}
return (str);
});
}
util.inherits(Server, EventEmitter);
module.exports = Server;
Server.prototype.address = function address() {
return (this.server.address());
};
/**
* Gets the server up and listening.
*
* You can call like:
* server.listen(80)
* server.listen(80, '127.0.0.1')
* server.listen('/tmp/server.sock')
*
* @param {Function} callback optionally get notified when listening.
* @throws {TypeError} on bad input.
*/
Server.prototype.listen = function listen() {
var args = Array.prototype.slice.call(arguments);
return (this.server.listen.apply(this.server, args));
};
/**
* Shuts down this server, and invokes callback (optionally) when done.
*
* @param {Function} callback optional callback to invoke when done.
*/
Server.prototype.close = function close(callback) {
if (callback)
assert.func(callback, 'callback');
this.server.once('close', function onClose() {
return (callback ? callback() : false);
});
return (this.server.close());
};
// Register all the routing methods
/**
* Mounts a chain on the given path against this HTTP verb
*
* @param {Object} options the URL to handle, at minimum.
* @return {Route} the newly created route.
*/
[
'del',
'get',
'head',
'opts',
'post',
'put',
'patch'
].forEach(function (method) {
Server.prototype[method] = function (opts) {
if (opts instanceof RegExp || typeof (opts) === 'string') {
opts = {
path: opts
};
} else if (typeof (opts) === 'object') {
opts = shallowCopy(opts);
} else {
throw new TypeError('path (string) required');
}
if (arguments.length < 2)
throw new TypeError('handler (function) required');
var chain = [];
var route;
var self = this;
function addHandler(h) {
assert.func(h, 'handler');
chain.push(h);
}
if (method === 'del')
method = 'DELETE';
if (method === 'opts')
method = 'OPTIONS';
opts.method = method.toUpperCase();
opts.versions = opts.versions || opts.version || self.versions;
if (!Array.isArray(opts.versions))
opts.versions = [opts.versions];
if (!opts.name) {
opts.name = method + '-' + (opts.path || opts.url);
if (opts.versions.length > 0) {
opts.name += '-' + opts.versions.join('--');
}
opts.name = opts.name.replace(/\W/g, '').toLowerCase();
if (this.router.mounts[opts.name]) // GH-401
opts.name += uuid.v4().substr(0, 7);
} else {
opts.name = opts.name.replace(/\W/g, '').toLowerCase();
}
if (!(route = this.router.mount(opts)))
return (false);
this.chain.forEach(addHandler);
argumentsToChain(arguments, 1).forEach(addHandler);
this.routes[route] = chain;
return (route);
};
});
/**
* Minimal port of the functionality offered by Express.js Route Param
* Pre-conditions
* @link http://expressjs.com/guide.html#route-param%20pre-conditions
*
* This basically piggy-backs on the `server.use` method. It attaches a
* new middleware function that only fires if the specified parameter exists
* in req.params
*
* Exposes an API:
* server.param("user", function (req, res, next) {
* // load the user's information here, always making sure to call next()
* });
*
* @param {String} The name of the URL param to respond to
* @param {Function} The middleware function to execute
*/
Server.prototype.param = function param(name, fn) {
this.use(function _param(req, res, next) {
if (req.params && req.params[name]) {
fn.call(this, req, res, next, req.params[name], name);
} else {
next();
}
});
return (this);
};
/**
* Piggy-backs on the `server.use` method. It attaches a new middleware
* function that only fires if the specified version matchtes the request.
*
* Note that if the client does not request a specific version, the middleware
* function always fires. If you don't want this set a default version with a
* pre handler on requests where the client omits one.
*
* Exposes an API:
* server.versionedUse("version", function (req, res, next, ver) {
* // do stuff that only applies to routes of this API version
* });
*
* @param {String|Array} The version or versions of the URL to respond to
* @param {Function} The middleware function to execute, the fourth parameter
* will be the selected version
*/
Server.prototype.versionedUse = function versionedUse(versions, fn) {
if (!Array.isArray(versions))
versions = [versions];
assert.arrayOfString(versions, 'versions');
versions.forEach(function (v) {
if (!semver.valid(v))
throw new TypeError('%s is not a valid semver', v);
});
this.use(function _versionedUse(req, res, next) {
var ver;
if (req.version() === '*' ||
(ver = maxSatisfying(versions,
req.version()) || false)) {
fn.call(this, req, res, next, ver);
} else {
next();
}
});
return (this);
};
/**
* Removes a route from the server.
*
* You pass in the route 'blob' you got from a mount call.
*
* @param {String} name the route name.
* @return {Boolean} true if route was removed, false if not.
* @throws {TypeError} on bad input.
*/
Server.prototype.rm = function rm(route) {
var r = this.router.unmount(route);
if (r && this.routes[r])
delete this.routes[r];
return (r);
};
/**
* Installs a list of handlers to run _before_ the "normal" handlers of all
* routes.
*
* You can pass in any combination of functions or array of functions.
*
* @throws {TypeError} on input error.
*/
Server.prototype.use = function use() {
var self = this;
(argumentsToChain(arguments) || []).forEach(function (h) {
self.chain.push(h);
});
return (this);
};
/**
* Gives you hooks to run _before_ any routes are located. This gives you
* a chance to intercept the request and change headers, etc., that routing
* depends on. Note that req.params will _not_ be set yet.
*/
Server.prototype.pre = function pre() {
var self = this;
argumentsToChain(arguments).forEach(function (h) {
self.before.push(h);
});
return (this);
};
Server.prototype.toString = function toString() {
var LINE_FMT = '\t%s: %s\n';
var SUB_LINE_FMT = '\t\t%s: %s\n';
var self = this;
var str = '';
function handlersToString(arr) {
var s = '[' + arr.map(function (b) {
return (b.name || 'function');
}).join(', ') + ']';
return (s);
}
str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', '));
str += sprintf(LINE_FMT, 'Name', this.name);
str += sprintf(LINE_FMT, 'Pre', handlersToString(this.before));
str += sprintf(LINE_FMT, 'Router', this.router.toString());
str += sprintf(LINE_FMT, 'Routes', '');
Object.keys(this.routes).forEach(function (k) {
var handlers = handlersToString(self.routes[k]);
str += sprintf(SUB_LINE_FMT, k, handlers);
});
str += sprintf(LINE_FMT, 'Secure', this.secure);
str += sprintf(LINE_FMT, 'Url', this.url);
str += sprintf(LINE_FMT, 'Version', this.versions.join());
return (str);
};
///--- Private methods
Server.prototype._handle = function _handle(req, res) {
var self = this;
function routeAndRun() {
self._route(req, res, function (route, context) {
req.context = req.params = context;
req.route = route.spec;
var r = route ? route.name : null;
var chain = self.routes[r];
self._run(req, res, route, chain, function done(e) {
self.emit('after', req, res, route, e);
});
});
}
if (this.before.length > 0) {
this._run(req, res, null, this.before, function (err) {
if (!err) {
routeAndRun();
}
});
} else {
routeAndRun();
}
};
Server.prototype._route = function _route(req, res, name, cb) {
var self = this;
if (typeof (name) === 'function') {
cb = name;
name = null;
} else {
this.router.get(name, req, function (err, route, ctx) {
if (err) {
emitRouteError(self, req, res, err);
} else {
cb(route, ctx);
}
});
}
this.router.find(req, res, function onRoute(err, route, ctx) {
var r = route ? route.name : null;
if (err) {
if (optionsError(err, req, res)) {
self.emit('after', req, res, err);
} else {
emitRouteError(self, req, res, err);
}
} else if (r === 'preflight') {
res.writeHead(200);
res.end();
self.emit('after', req, res, null);
} else if (!r || !self.routes[r]) {
err = new ResourceNotFoundError(req.path());
emitRouteError(self, res, res, err);
} else {
cb(route, ctx);
}
});
};
// The goofy checks in next() are to make sure we fire the DTrace
// probes after an error might have been sent, as in a handler
// return next(new Error) is basically shorthand for sending an
// error via res.send(), so we do that before firing the dtrace
// probe (namely so the status codes get updated in the
// response).
//
// Callers can stop the chain from proceding if they do
// return next(false); This is useful for non-errors, but where
// a response was sent and you don't want the chain to keep
// going
Server.prototype._run = function _run(req, res, route, chain, cb) {
var d;
var i = -1;
var id = dtrace.nextId();
var log = this.log;
var self = this;
var t;
function next(arg) {
var done = false;
if (arg) {
if (arg instanceof Error) {
log.trace({err: arg}, 'next(err=%s)',
(arg.name || 'Error'));
res.send(arg);
done = true;
} else if (typeof (arg) === 'string') {
if (req._rstfy_chained_route) {
var _e = new errors.InternalError();
log.error({
err: _e
}, 'Multiple next("chain") calls not ' +
'supported');
res.send(_e);
return (false);
}
self._route(req, res, arg, function (r, ctx) {
req.context = req.params = ctx;
req.route = r.spec;
var _c = chain.slice(0, i + 1);
function _uniq(fn) {
return (_c.indexOf(fn) === -1);
}
var _routes = self.routes[r.name] || [];
var _chain = _routes.filter(_uniq);
req._rstfy_chained_route = true;
self._run(req, res, r, _chain, cb);
});
}
}
if (arg === false)
done = true;
// Fire DTrace done for the previous handler.
if ((i + 1) > 0 && chain[i] && !chain[i]._skip) {
var _name = chain[i].name || ('handler-' + i);
req.timers.push({
name: _name,
time: process.hrtime(t)
});
dtrace._rstfy_probes['handler-done'].fire(function () {
return ([
self.name,
route !== null ? route.name : 'pre',
_name,
id
]);
});
}
// Run the next handler up
if (!done && chain[++i]) {
if (chain[i]._skip)
return (next());
t = process.hrtime();
if (log.trace())
log.trace('running %s', chain[i].name || '?');
dtrace._rstfy_probes['handler-start'].fire(function () {
return ([
self.name,
route !== null ? route.name : 'pre',
chain[i].name || ('handler-' + i),
id
]);
});
var n = once(next);
n.ifError = ifError(n);
return (chain[i].call(self, req, res, n));
}
dtrace._rstfy_probes['route-done'].fire(function () {
return ([
self.name,
route !== null ? route.name : 'pre',
id,
res.statusCode || 200,
res.headers()
]);
});
if (route === null) {
self.emit('preDone', req, res);
} else {
self.emit('done', req, res, route);
}
return (cb ? cb(arg) : true);
}
var n1 = once(next);
n1.ifError = ifError(n1);
dtrace._rstfy_probes['route-start'].fire(function () {
return ([
self.name,
route !== null ? route.name : 'pre',
id,
req.method,
req.href(),
req.headers
]);
});
req.timers = [];
d = domain.create();
d.add(req);
d.add(res);
d.on('error', function onError(err) {
if (err._restify_next) {
err._restify_next(err);
} else {
log.trace({err: err}, 'uncaughtException');
self.emit('uncaughtException', req, res, route, err);
}
});
d.run(n1);
};
Server.prototype._setupRequest = function _setupRequest(req, res) {
req.log = res.log = this.log;
req._time = res._time = Date.now();
res.acceptable = this.acceptable;
res.formatters = this.formatters;
res.req = req;
res.serverName = this.name;
res.version = this.router.versions[this.router.versions.length - 1];
};
// vim: set et ts=8 sts=8 sw=8:
// Copyright (c) 2013, Joyent, Inc. All rights reserved.
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var assert = require('assert-plus');
function InvalidUpgradeStateError(msg) {
if (Error.captureStackTrace)
Error.captureStackTrace(this, InvalidUpgradeStateError);
this.message = msg;
this.name = 'InvalidUpgradeStateError';
}
util.inherits(InvalidUpgradeStateError, Error);
//
// The Node HTTP Server will, if we handle the 'upgrade' event, swallow any
// Request with the 'Connection: upgrade' header set. While doing this it
// detaches from the 'data' events on the Socket and passes the socket to
// us, so that we may take over handling for the connection.
//
// Unfortunately, the API does not presently provide a http.ServerResponse
// for us to use in the event that we do not wish to upgrade the connection.
// This factory method provides a skeletal implementation of a
// restify-compatible response that is sufficient to allow the existing
// request handling path to work, while allowing us to perform _at most_ one
// of either:
//
// - Return a basic HTTP Response with a provided Status Code and
// close the socket.
// - Upgrade the connection and stop further processing.
//
// To determine if an upgrade is requested, a route handler would check for
// the 'claimUpgrade' method on the Response. The object this method
// returns will have the 'socket' and 'head' Buffer emitted with the
// 'upgrade' event by the http.Server. If the upgrade is not possible, such
// as when the HTTP head (or a full request) has already been sent by some
// other handler, this method will throw.
//
function createServerUpgradeResponse(req, socket, head) {
return (new ServerUpgradeResponse(socket, head));
}
function ServerUpgradeResponse(socket, head) {
assert.object(socket, 'socket');
assert.buffer(head, 'head');
EventEmitter.call(this);
this.sendDate = true;
this.statusCode = 400;
this._upgrade = {
socket: socket,
head: head
};
this._headWritten = false;
this._upgradeClaimed = false;
}
util.inherits(ServerUpgradeResponse, EventEmitter);
function notImplemented(method) {
if (!method.throws) {
return function () {
return (method.returns);
};
} else {
return function () {
throw (new Error('Method ' + method.name + ' is not ' +
'implemented!'));
};
}
}
var NOT_IMPLEMENTED = [
{ name: 'writeContinue', throws: true },
{ name: 'setHeader', throws: false, returns: null },
{ name: 'getHeader', throws: false, returns: null },
{ name: 'getHeaders', throws: false, returns: {} },
{ name: 'removeHeader', throws: false, returns: null },
{ name: 'addTrailer', throws: false, returns: null },
{ name: 'cache', throws: false, returns: 'public' },
{ name: 'format', throws: true },
{ name: 'set', throws: false, returns: null },
{ name: 'get', throws: false, returns: null },
{ name: 'headers', throws: false, returns: {} },
{ name: 'header', throws: false, returns: null },
{ name: 'json', throws: false, returns: null },
{ name: 'link', throws: false, returns: null }
];
NOT_IMPLEMENTED.forEach(function (method) {
ServerUpgradeResponse.prototype[method.name] = notImplemented(method);
});
ServerUpgradeResponse.prototype._writeHeadImpl =
function _writeHeadImpl(statusCode, reason) {
if (this._headWritten)
return;
this._headWritten = true;
if (this._upgradeClaimed)
throw new InvalidUpgradeStateError('Upgrade already claimed!');
var head = [
'HTTP/1.1 ' + statusCode + ' ' + reason,
'Connection: close'
];
if (this.sendDate)
head.push('Date: ' + new Date().toUTCString());
this._upgrade.socket.write(head.join('\r\n') + '\r\n');
};
ServerUpgradeResponse.prototype.status = function status(code) {
assert.number(code, 'code');
this.statusCode = code;
return (code);
};
ServerUpgradeResponse.prototype.send = function send(code, body) {
if (typeof (code) === 'number')
this.statusCode = code;
else
body = code;
if (typeof (body) === 'object') {
if (typeof (body.statusCode) === 'number')
this.statusCode = body.statusCode;
if (typeof (body.message) === 'string')
this.statusReason = body.message;
}
return (this.end());
};
ServerUpgradeResponse.prototype.end = function end() {
this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
this._upgrade.socket.end('\r\n');
return (true);
};
ServerUpgradeResponse.prototype.write = function write() {
this._writeHeadImpl(this.statusCode, 'Connection Not Upgraded');
return (true);
};
ServerUpgradeResponse.prototype.writeHead =
function writeHead(statusCode, reason) {
assert.number(statusCode, 'statusCode');
assert.optionalString(reason, 'reason');
this.statusCode = statusCode;
if (!reason)
reason = 'Connection Not Upgraded';
if (this._headWritten)
throw new Error('Head already written!');
return (this._writeHeadImpl(statusCode, reason));
};
ServerUpgradeResponse.prototype.claimUpgrade = function claimUpgrade() {
if (this._upgradeClaimed)
throw new InvalidUpgradeStateError('Upgrade already claimed!');
if (this._headWritten)
throw new InvalidUpgradeStateError('Upgrade already aborted!');
this._upgradeClaimed = true;
return (this._upgrade);
};
module.exports = {
createResponse: createServerUpgradeResponse,
InvalidUpgradeStateError: InvalidUpgradeStateError
};
// vim: set et ts=8 sts=8 sw=8:
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
/**
* Cleans up sloppy URL paths, like /foo////bar/// to /foo/bar.
*
* @param {String} path the HTTP resource path.
* @return {String} Cleaned up form of path.
*/
function sanitizePath(path) {
assert.ok(path);
// Be nice like apache and strip out any //my//foo//bar///blah
path = path.replace(/\/\/+/g, '/');
// Kill a trailing '/'
if (path.lastIndexOf('/') === (path.length - 1) && path.length > 1)
path = path.substr(0, path.length - 1);
return (path);
}
/**
* Return a shallow copy of the given object;
*/
function shallowCopy(obj) {
if (!obj) {
return (obj);
}
var copy = {};
Object.keys(obj).forEach(function (k) {
copy[k] = obj[k];
});
return (copy);
}
///--- Exports
module.exports = {
sanitizePath: sanitizePath,
shallowCopy: shallowCopy
};
Copyright (c) 2011 Mark Cavage, All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE
#!/usr/bin/env node
// -*- mode: js -*-
//
// bunyan -- filter and pretty-print JSON logs, like Bunyan logs.
//
// See <https://github.com/trentm/node-bunyan>.
//
var VERSION = '0.22.1';
var p = console.log;
var util = require('util');
var pathlib = require('path');
var vm = require('vm');
var http = require('http');
var fs = require('fs');
var warn = console.warn;
var child_process = require('child_process'),
spawn = child_process.spawn,
exec = child_process.exec,
execFile = child_process.execFile;
var assert = require('assert');
var nodeSpawnSupportsStdio = (
Number(process.version.split('.')[0]) >= 0 ||
Number(process.version.split('.')[1]) >= 8);
//---- globals and constants
// Internal debug logging via `console.warn`.
var _DEBUG = false;
// Output modes.
var OM_LONG = 1;
var OM_JSON = 2;
var OM_INSPECT = 3;
var OM_SIMPLE = 4;
var OM_SHORT = 5;
var OM_BUNYAN = 6;
var OM_FROM_NAME = {
'long': OM_LONG,
'paul': OM_LONG, /* backward compat */
'json': OM_JSON,
'inspect': OM_INSPECT,
'simple': OM_SIMPLE,
'short': OM_SHORT,
'bunyan': OM_BUNYAN
};
// Levels
var TRACE = 10;
var DEBUG = 20;
var INFO = 30;
var WARN = 40;
var ERROR = 50;
var FATAL = 60;
var levelFromName = {
'trace': TRACE,
'debug': DEBUG,
'info': INFO,
'warn': WARN,
'error': ERROR,
'fatal': FATAL
};
var nameFromLevel = {};
var upperNameFromLevel = {};
var upperPaddedNameFromLevel = {};
Object.keys(levelFromName).forEach(function (name) {
var lvl = levelFromName[name];
nameFromLevel[lvl] = name;
upperNameFromLevel[lvl] = name.toUpperCase();
upperPaddedNameFromLevel[lvl] = (
name.length === 4 ? ' ' : '') + name.toUpperCase();
});
// The current raw input line being processed. Used for `uncaughtException`.
var currLine = null;
// Child dtrace process, if any. Used for signal-handling.
var child = null;
// Whether ANSI codes are being used. Used for signal-handling.
var usingAnsiCodes = false;
// Global ref to options used only by 'uncaughtException' handler.
var gOptsForUncaughtException;
// Pager child process, and output stream to which to write.
var pager = null;
var stdout = process.stdout;
//---- support functions
function getVersion() {
return VERSION;
}
var format = util.format;
if (!format) {
/* BEGIN JSSTYLED */
// If not node 0.6, then use its `util.format`:
// <https://github.com/joyent/node/blob/master/lib/util.js#L22>:
var inspect = util.inspect;
var formatRegExp = /%[sdj%]/g;
format = function format(f) {
if (typeof f !== 'string') {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function (x) {
if (i >= len)
return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j': return JSON.stringify(args[i++]);
case '%%': return '%';
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (x === null || typeof x !== 'object') {
str += ' ' + x;
} else {
str += ' ' + inspect(x);
}
}
return str;
};
/* END JSSTYLED */
}
function indent(s) {
return ' ' + s.split(/\r?\n/).join('\n ');
}
function objCopy(obj) {
if (obj === null) {
return null;
} else if (Array.isArray(obj)) {
return obj.slice();
} else {
var copy = {};
Object.keys(obj).forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
}
function printHelp() {
/* BEGIN JSSTYLED */
p('Usage:');
p(' bunyan [OPTIONS] [FILE ...]');
p(' ... | bunyan [OPTIONS]');
p(' bunyan [OPTIONS] -p PID');
p('');
p('Filter and pretty-print Bunyan log file content.');
p('');
p('General options:');
p(' -h, --help print this help info and exit');
p(' --version print version of this command and exit');
p('');
p('Runtime log snooping (via DTrace, only on supported platforms):');
p(' -p PID Process bunyan:log-* probes from the process');
p(' with the given PID. Can be used multiple times,');
p(' or specify all processes with "*", or a set of');
p(' processes whose command & args match a pattern');
p(' with "-p NAME".');
p('');
p('Filtering options:');
p(' -l, --level LEVEL');
p(' Only show messages at or above the specified level.');
p(' You can specify level *names* or numeric values.');
p(' (See "Log Levels" below.)');
p(' -c, --condition CONDITION');
p(' Run each log message through the condition and');
p(' only show those that return truish. E.g.:');
p(' -c \'this.pid == 123\'');
p(' -c \'this.level == DEBUG\'');
p(' -c \'this.msg.indexOf("boom") != -1\'');
p(' "CONDITION" must be legal JS code. `this` holds');
p(' the log record. The TRACE, DEBUG, ... FATAL values');
p(' are defined to help with comparing `this.level`.');
p(' --strict Suppress all but legal Bunyan JSON log lines. By default');
p(' non-JSON, and non-Bunyan lines are passed through.');
p('');
p('Output options:');
p(' --pager Pipe output into `less` (or $PAGER if set), if');
p(' stdout is a TTY. This overrides $BUNYAN_NO_PAGER.');
p(' Note: Paging is only supported on node >=0.8.');
p(' --no-pager Do not pipe output into a pager.');
p(' --color Colorize output. Defaults to try if output');
p(' stream is a TTY.');
p(' --no-color Force no coloring (e.g. terminal doesn\'t support it)');
p(' -o, --output MODE');
p(' Specify an output mode/format. One of');
p(' long: (the default) pretty');
p(' json: JSON output, 2-space indent');
p(' json-N: JSON output, N-space indent, e.g. "json-4"');
p(' bunyan: 0 indented JSON, bunyan\'s native format');
p(' inspect: node.js `util.inspect` output');
p(' short: like "long", but more concise');
p(' -j shortcut for `-o json`');
p('');
p('Log Levels:');
p(' Either numeric values or their associated strings are valid for the');
p(' -l|--level argument. However, -c|--condition scripts will see a numeric');
p(' "level" value, not a string.');
p('');
Object.keys(levelFromName).forEach(function (name) {
var n = name;
while (n.length < 6)
n += ' ';
p(' %s %d', n, levelFromName[name]);
});
p('');
p('Environment Variables:');
p(' BUNYAN_NO_COLOR Set to a non-empty value to force no output ');
p(' coloring. See "--no-color".');
p(' BUNYAN_NO_PAGER Disable piping output to a pager. ');
p(' See "--no-pager".');
p('');
p('See <https://github.com/trentm/node-bunyan> for more complete docs.');
p('Please report bugs to <https://github.com/trentm/node-bunyan/issues>.');
/* END JSSTYLED */
}
/*
* If the user specifies multiple input sources, we want to print out records
* from all sources in a single, chronologically ordered stream. To do this
* efficiently, we first assume that all records within each source are ordered
* already, so we need only keep track of the next record in each source and
* the time of the last record emitted. To avoid excess memory usage, we
* pause() streams that are ahead of others.
*
* 'streams' is an object indexed by source name (file name) which specifies:
*
* stream Actual stream object, so that we can pause and resume it.
*
* records Array of log records we've read, but not yet emitted. Each
* record includes 'line' (the raw line), 'rec' (the JSON
* record), and 'time' (the parsed time value).
*
* done Whether the stream has any more records to emit.
*/
var streams = {};
function gotRecord(file, line, rec, opts, stylize)
{
var time = new Date(rec.time);
streams[file]['records'].push({ line: line, rec: rec, time: time });
emitNextRecord(opts, stylize);
}
function filterRecord(rec, opts)
{
if (opts.level && rec.level < opts.level) {
return false;
}
if (opts.conditions) {
for (var i = 0; i < opts.conditions.length; i++) {
var pass = opts.conditions[i].runInNewContext(rec);
if (!pass)
return false;
}
}
return true;
}
function emitNextRecord(opts, stylize)
{
var ofile, ready, minfile, rec;
for (;;) {
/*
* Take a first pass through the input streams to see if we have a
* record from all of them. If not, we'll pause any streams for
* which we do already have a record (to avoid consuming excess
* memory) and then wait until we have records from the others
* before emitting the next record.
*
* As part of the same pass, we look for the earliest record
* we have not yet emitted.
*/
minfile = undefined;
ready = true;
for (ofile in streams) {
if (streams[ofile].stream === null ||
(!streams[ofile].done && streams[ofile].records.length === 0)) {
ready = false;
break;
}
if (streams[ofile].records.length > 0 &&
(minfile === undefined ||
streams[minfile].records[0].time >
streams[ofile].records[0].time)) {
minfile = ofile;
}
}
if (!ready || minfile === undefined) {
for (ofile in streams) {
if (!streams[ofile].stream || streams[ofile].done)
continue;
if (streams[ofile].records.length > 0) {
if (!streams[ofile].paused) {
streams[ofile].paused = true;
streams[ofile].stream.pause();
}
} else if (streams[ofile].paused) {
streams[ofile].paused = false;
streams[ofile].stream.resume();
}
}
return;
}
/*
* Emit the next record for 'minfile', and invoke ourselves again to
* make sure we emit as many records as we can right now.
*/
rec = streams[minfile].records.shift();
emitRecord(rec.rec, rec.line, opts, stylize);
}
}
/**
* Parse the command-line options and arguments into an object.
*
* {
* 'args': [...] // arguments
* 'help': true, // true if '-h' option given
* // etc.
* }
*
* @return {Object} The parsed options. `.args` is the argument list.
* @throws {Error} If there is an error parsing argv.
*/
function parseArgv(argv) {
var parsed = {
args: [],
help: false,
color: null,
paginate: null,
outputMode: OM_LONG,
jsonIndent: 2,
level: null,
conditions: null,
strict: false,
pids: null,
pidsType: null
};
// Turn '-iH' into '-i -H', except for argument-accepting options.
var args = argv.slice(2); // drop ['node', 'scriptname']
var newArgs = [];
var optTakesArg = {'d': true, 'o': true, 'c': true, 'l': true, 'p': true};
for (var i = 0; i < args.length; i++) {
if (args[i].charAt(0) === '-' && args[i].charAt(1) !== '-' &&
args[i].length > 2)
{
var splitOpts = args[i].slice(1).split('');
for (var j = 0; j < splitOpts.length; j++) {
newArgs.push('-' + splitOpts[j]);
if (optTakesArg[splitOpts[j]]) {
var optArg = splitOpts.slice(j+1).join('');
if (optArg.length) {
newArgs.push(optArg);
}
break;
}
}
} else {
newArgs.push(args[i]);
}
}
args = newArgs;
var condDefines = [];
Object.keys(upperNameFromLevel).forEach(function (lvl) {
condDefines.push(
format('Object.prototype.%s = %s;', upperNameFromLevel[lvl], lvl));
});
condDefines = condDefines.join('\n') + '\n';
var endOfOptions = false;
while (args.length > 0) {
var arg = args.shift();
switch (arg) {
case '--':
endOfOptions = true;
break;
case '-h': // display help and exit
case '--help':
parsed.help = true;
break;
case '--version':
parsed.version = true;
break;
case '--strict':
parsed.strict = true;
break;
case '--color':
parsed.color = true;
break;
case '--no-color':
parsed.color = false;
break;
case '--pager':
parsed.paginate = true;
break;
case '--no-pager':
parsed.paginate = false;
break;
case '-o':
case '--output':
var name = args.shift();
var idx = name.lastIndexOf('-');
if (idx !== -1) {
var indentation = Number(name.slice(idx+1));
if (! isNaN(indentation)) {
parsed.jsonIndent = indentation;
name = name.slice(0, idx);
}
}
parsed.outputMode = OM_FROM_NAME[name];
if (parsed.outputMode === undefined) {
throw new Error('unknown output mode: "'+name+'"');
}
break;
case '-j': // output with JSON.stringify
parsed.outputMode = OM_JSON;
break;
case '-p':
if (!parsed.pids) {
parsed.pids = [];
}
var pidArg = args.shift();
var pid = +(pidArg);
if (!isNaN(pid) || pidArg === '*') {
if (parsed.pidsType && parsed.pidsType !== 'num') {
throw new Error(format('cannot mix PID name and '
+ 'number arguments: "%s"', pidArg));
}
parsed.pidsType = 'num';
if (!parsed.pids) {
parsed.pids = [];
}
parsed.pids.push(isNaN(pid) ? pidArg : pid);
} else {
if (parsed.pidsType && parsed.pidsType !== 'name') {
throw new Error(format('cannot mix PID name and '
+ 'number arguments: "%s"', pidArg));
}
parsed.pidsType = 'name';
parsed.pids = pidArg;
}
break;
case '-l':
case '--level':
var levelArg = args.shift();
var level = +(levelArg);
if (isNaN(level)) {
level = +levelFromName[levelArg.toLowerCase()];
}
if (isNaN(level)) {
throw new Error('unknown level value: "'+levelArg+'"');
}
parsed.level = level;
break;
case '-c':
case '--condition':
var condition = args.shift();
parsed.conditions = parsed.conditions || [];
var scriptName = 'bunyan-condition-'+parsed.conditions.length;
var code = condDefines + condition;
try {
var script = vm.createScript(code, scriptName);
} catch (compileErr) {
throw new Error(format('illegal CONDITION code: %s\n'
+ ' CONDITION script:\n'
+ '%s\n'
+ ' Error:\n'
+ '%s',
compileErr, indent(code), indent(compileErr.stack)));
}
// Ensure this is a reasonably safe CONDITION.
try {
script.runInNewContext(minValidRecord);
} catch (condErr) {
throw new Error(format(
/* JSSTYLED */
'CONDITION code cannot safely filter a minimal Bunyan log record\n'
+ ' CONDITION script:\n'
+ '%s\n'
+ ' Minimal Bunyan log record:\n'
+ '%s\n'
+ ' Filter error:\n'
+ '%s',
indent(code),
indent(JSON.stringify(minValidRecord, null, 2)),
indent(condErr.stack)
));
}
parsed.conditions.push(script);
break;
default: // arguments
if (!endOfOptions && arg.length > 0 && arg[0] === '-') {
throw new Error('unknown option "'+arg+'"');
}
parsed.args.push(arg);
break;
}
}
//TODO: '--' handling and error on a first arg that looks like an option.
return parsed;
}
function isInteger(s) {
return (s.search(/^-?[0-9]+$/) == 0);
}
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
// Suggested colors (some are unreadable in common cases):
// - Good: cyan, yellow (limited use), grey, bold, green, magenta, red
// - Bad: blue (not visible on cmd.exe)
var colors = {
'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39]
};
function stylizeWithColor(str, color) {
if (!str)
return '';
var codes = colors[color];
if (codes) {
return '\033[' + codes[0] + 'm' + str +
'\033[' + codes[1] + 'm';
} else {
return str;
}
}
function stylizeWithoutColor(str, color) {
return str;
}
/**
* Is this a valid Bunyan log record.
*/
function isValidRecord(rec) {
if (rec.v == null ||
rec.level == null ||
rec.name == null ||
rec.hostname == null ||
rec.pid == null ||
rec.time == null ||
rec.msg == null) {
// Not valid Bunyan log.
return false;
} else {
return true;
}
}
var minValidRecord = {
v: 0, //TODO: get this from bunyan.LOG_VERSION
level: INFO,
name: 'name',
hostname: 'hostname',
pid: 123,
time: Date.now(),
msg: 'msg'
};
/**
* Parses the given log line and either emits it right away (for invalid
* records) or enqueues it for emitting later when it's the next line to show.
*/
function handleLogLine(file, line, opts, stylize) {
currLine = line; // intentionally global
// Emit non-JSON lines immediately.
var rec;
if (!line) {
if (!opts.strict) emit(line + '\n');
return;
} else if (line[0] !== '{') {
if (!opts.strict) emit(line + '\n'); // not JSON
return;
} else {
try {
rec = JSON.parse(line);
} catch (e) {
if (!opts.strict) emit(line + '\n');
return;
}
}
if (!isValidRecord(rec)) {
if (!opts.strict) emit(line + '\n');
return;
}
if (!filterRecord(rec, opts))
return;
if (file === null)
return emitRecord(rec, line, opts, stylize);
return gotRecord(file, line, rec, opts, stylize);
}
/**
* Print out a single result, considering input options.
*/
function emitRecord(rec, line, opts, stylize) {
var short = false;
switch (opts.outputMode) {
case OM_SHORT:
short = true;
/* jsl:fall-thru */
case OM_LONG:
// [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...)
// msg*
// --
// long and multi-line extras
// ...
// If 'msg' is single-line, then it goes in the top line.
// If 'req', show the request.
// If 'res', show the response.
// If 'err' and 'err.stack' then show that.
if (!isValidRecord(rec)) {
return emit(line + '\n');
}
delete rec.v;
/*
* We assume the Date is formatted according to ISO8601, in which
* case we can safely chop off the date information.
*/
if (short && rec.time[10] == 'T') {
var time = rec.time.substr(11);
time = stylize(time, 'XXX');
} else {
var time = stylize('[' + rec.time + ']', 'XXX');
}
delete rec.time;
var nameStr = rec.name;
delete rec.name;
if (rec.component) {
nameStr += '/' + rec.component;
}
delete rec.component;
if (!short)
nameStr += '/' + rec.pid;
delete rec.pid;
var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level);
if (opts.color) {
var colorFromLevel = {
10: 'grey', // TRACE
20: 'grey', // DEBUG
30: 'cyan', // INFO
40: 'magenta', // WARN
50: 'red', // ERROR
60: 'inverse', // FATAL
};
level = stylize(level, colorFromLevel[rec.level]);
}
delete rec.level;
var src = '';
if (rec.src && rec.src.file) {
var s = rec.src;
if (s.func) {
src = format(' (%s:%d in %s)', s.file, s.line, s.func);
} else {
src = format(' (%s:%d)', s.file, s.line);
}
src = stylize(src, 'green');
}
delete rec.src;
var hostname = rec.hostname;
delete rec.hostname;
var extras = [];
var details = [];
if (rec.req_id) {
extras.push('req_id=' + rec.req_id);
}
delete rec.req_id;
var onelineMsg;
if (rec.msg.indexOf('\n') !== -1) {
onelineMsg = '';
details.push(indent(stylize(rec.msg, 'cyan')));
} else {
onelineMsg = ' ' + stylize(rec.msg, 'cyan');
}
delete rec.msg;
if (rec.req && typeof (rec.req) === 'object') {
var req = rec.req;
delete rec.req;
var headers = req.headers;
var s = format('%s %s HTTP/%s%s', req.method,
req.url,
req.httpVersion || '1.1',
(headers ?
'\n' + Object.keys(headers).map(function (h) {
return h + ': ' + headers[h];
}).join('\n') :
'')
);
delete req.url;
delete req.method;
delete req.httpVersion;
delete req.headers;
if (req.body) {
s += '\n\n' + (typeof (req.body) === 'object'
? JSON.stringify(req.body, null, 2) : req.body);
delete req.body;
}
if (req.trailers && Object.keys(req.trailers) > 0) {
s += '\n' + Object.keys(req.trailers).map(function (t) {
return t + ': ' + req.trailers[t];
}).join('\n');
}
delete req.trailers;
details.push(indent(s));
// E.g. for extra 'foo' field on 'req', add 'req.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'req.foo' key.
Object.keys(req).forEach(function (k) {
rec['req.' + k] = req[k];
})
}
if (rec.client_req && typeof (rec.client_req) === 'object') {
var client_req = rec.client_req;
delete rec.client_req;
var headers = client_req.headers;
var hostHeaderLine = '';
var s = '';
if (client_req.address) {
hostHeaderLine = 'Host: ' + client_req.address;
if (client_req.port)
hostHeaderLine += ':' + client_req.port;
hostHeaderLine += '\n';
}
delete client_req.headers;
delete client_req.address;
delete client_req.port;
s += format('%s %s HTTP/%s\n%s%s', client_req.method,
client_req.url,
client_req.httpVersion || '1.1',
hostHeaderLine,
(headers ?
Object.keys(headers).map(
function (h) {
return h + ': ' + headers[h];
}).join('\n') :
''));
delete client_req.method;
delete client_req.url;
delete client_req.httpVersion;
if (client_req.body) {
s += '\n\n' + (typeof (client_req.body) === 'object' ?
JSON.stringify(client_req.body, null, 2) :
client_req.body);
delete client_req.body;
}
// E.g. for extra 'foo' field on 'client_req', add
// 'client_req.foo' at top-level. This *does* have the potential
// to stomp on a literal 'client_req.foo' key.
Object.keys(client_req).forEach(function (k) {
rec['client_req.' + k] = client_req[k];
})
details.push(indent(s));
}
function _res(res) {
var s = '';
if (res.header) {
s += res.header.trimRight();
} else if (typeof(res.headers) === 'string') {
s += res.headers.trimRight();
} else if (res.headers) {
if (res.statusCode) {
s += format('HTTP/1.1 %s %s\n', res.statusCode,
http.STATUS_CODES[res.statusCode]);
}
var headers = res.headers;
s += Object.keys(headers).map(
function (h) { return h + ': ' + headers[h]; }).join('\n');
}
delete res.header;
delete res.headers;
delete res.statusCode;
if (res.body) {
s += '\n\n' + (typeof (res.body) === 'object'
? JSON.stringify(res.body, null, 2) : res.body);
delete res.body;
}
if (res.trailer) {
s += '\n' + res.trailer;
}
delete res.trailer;
if (s) {
details.push(indent(s));
}
// E.g. for extra 'foo' field on 'res', add 'res.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'res.foo' key.
Object.keys(res).forEach(function (k) {
rec['res.' + k] = res[k];
});
}
if (rec.res && typeof (rec.res) === 'object') {
_res(rec.res);
delete rec.res;
}
if (rec.client_res && typeof (rec.client_res) === 'object') {
_res(rec.client_res);
delete rec.res;
}
if (rec.err && rec.err.stack) {
details.push(indent(rec.err.stack));
delete rec.err;
}
var leftover = Object.keys(rec);
for (var i = 0; i < leftover.length; i++) {
var key = leftover[i];
var value = rec[key];
var stringified = false;
if (typeof (value) !== 'string') {
value = JSON.stringify(value, null, 2);
stringified = true;
}
if (value.indexOf('\n') !== -1 || value.length > 50) {
details.push(indent(key + ': ' + value));
} else if (!stringified && (value.indexOf(' ') != -1 ||
value.length === 0))
{
extras.push(key + '=' + JSON.stringify(value));
} else {
extras.push(key + '=' + value);
}
}
extras = stylize(
(extras.length ? ' (' + extras.join(', ') + ')' : ''), 'grey');
details = stylize(
(details.length ? details.join('\n --\n') + '\n' : ''), 'grey');
if (!short)
emit(format('%s %s: %s on %s%s:%s%s\n%s',
time,
level,
nameStr,
hostname || '<no-hostname>',
src,
onelineMsg,
extras,
details));
else
emit(format('%s %s %s:%s%s\n%s',
time,
level,
nameStr,
onelineMsg,
extras,
details));
break;
case OM_INSPECT:
emit(util.inspect(rec, false, Infinity, true) + '\n');
break;
case OM_BUNYAN:
emit(JSON.stringify(rec, null, 0) + '\n');
break;
case OM_JSON:
emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n');
break;
case OM_SIMPLE:
/* JSSTYLED */
// <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
if (!isValidRecord(rec)) {
return emit(line + '\n');
}
emit(format('%s - %s\n',
upperNameFromLevel[rec.level] || 'LVL' + rec.level,
rec.msg));
break;
default:
throw new Error('unknown output mode: '+opts.outputMode);
}
}
var stdoutFlushed = true;
function emit(s) {
try {
stdoutFlushed = stdout.write(s);
} catch (e) {
// Handle any exceptions in stdout writing in `stdout.on('error', ...)`.
}
}
/**
* A hacked up version of 'process.exit' that will first drain stdout
* before exiting. *WARNING: This doesn't stop event processing.* IOW,
* callers have to be careful that code following this call isn't
* accidentally executed.
*
* In node v0.6 "process.stdout and process.stderr are blocking when they
* refer to regular files or TTY file descriptors." However, this hack might
* still be necessary in a shell pipeline.
*/
function drainStdoutAndExit(code) {
if (_DEBUG) warn('(drainStdoutAndExit(%d))', code);
stdout.on('drain', function () {
cleanupAndExit(code);
});
if (stdoutFlushed) {
cleanupAndExit(code);
}
}
/**
* Process all input from stdin.
*
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function ()`
*/
function processStdin(opts, stylize, callback) {
var leftover = ''; // Left-over partial line from last chunk.
var stdin = process.stdin;
stdin.resume();
stdin.setEncoding('utf8');
stdin.on('data', function (chunk) {
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(null, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(null, lines[i], opts, stylize);
}
});
stdin.on('end', function () {
if (leftover) {
handleLogLine(null, leftover, opts, stylize);
leftover = '';
}
callback();
});
}
/**
* Process bunyan:log-* probes from the given pid.
*
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function (code)`
*/
function processPids(opts, stylize, callback) {
var leftover = ''; // Left-over partial line from last chunk.
/**
* Get the PIDs to dtrace.
*
* @param cb {Function} `function (errCode, pids)`
*/
function getPids(cb) {
if (opts.pidsType === 'num') {
return cb(null, opts.pids);
}
if (process.platform === 'sunos') {
execFile('/bin/pgrep', ['-lf', opts.pids],
function (pidsErr, stdout, stderr) {
if (pidsErr) {
warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s',
opts.pids, pidsErr.message, stdout, stderr);
return cb(1);
}
var pids = stdout.trim().split('\n')
.map(function (line) {
return line.trim().split(/\s+/)[0]
})
.filter(function (pid) {
return Number(pid) !== process.pid
});
if (pids.length === 0) {
warn('bunyan: error: no matching PIDs found for "%s"',
opts.pids);
return cb(2);
}
cb(null, pids);
}
);
} else {
var regex = opts.pids;
if (regex && /[a-zA-Z0-9_]/.test(regex[0])) {
// 'foo' -> '[f]oo' trick to exclude the 'grep' PID from its
// own search.
regex = '[' + regex[0] + ']' + regex.slice(1);
}
exec(format('ps -A -o pid,command | grep \'%s\'', regex),
function (pidsErr, stdout, stderr) {
if (pidsErr) {
warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s',
opts.pids, pidsErr.message, stdout, stderr);
return cb(1);
}
var pids = stdout.trim().split('\n')
.map(function (line) {
return line.trim().split(/\s+/)[0];
})
.filter(function (pid) {
return Number(pid) !== process.pid;
});
if (pids.length === 0) {
warn('bunyan: error: no matching PIDs found for "%s"',
opts.pids);
return cb(2);
}
cb(null, pids);
}
);
}
}
getPids(function (errCode, pids) {
if (errCode) {
return callback(errCode);
}
var probes = pids.map(function (pid) {
if (!opts.level)
return format('bunyan%s:::log-*', pid);
var rval = [], l;
for (l in levelFromName) {
if (levelFromName[l] >= opts.level)
rval.push(format('bunyan%s:::log-%s', pid, l));
}
if (rval.length != 0)
return rval.join(',');
warn('bunyan: error: level (%d) exceeds maximum logging level',
opts.level);
return drainStdoutAndExit(1);
}).join(',');
var argv = ['dtrace', '-Z', '-x', 'strsize=4k',
'-x', 'switchrate=10hz', '-qn',
format('%s{printf("%s", copyinstr(arg0))}', probes)];
//console.log('dtrace argv: %s', argv);
var dtrace = spawn(argv[0], argv.slice(1),
// Share the stderr handle to have error output come
// straight through. Only supported in v0.8+.
{stdio: ['pipe', 'pipe', process.stderr]});
dtrace.on('error', function (e) {
if (e.syscall === 'spawn' && e.errno === 'ENOENT') {
console.error('bunyan: error: could not spawn "dtrace" ' +
'("bunyan -p" is only supported on platforms with dtrace)');
} else {
console.error('bunyan: error: unexpected dtrace error: %s', e);
}
callback(1);
})
child = dtrace; // intentionally global
function finish(code) {
if (leftover) {
handleLogLine(null, leftover, opts, stylize);
leftover = '';
}
callback(code);
}
dtrace.stdout.setEncoding('utf8');
dtrace.stdout.on('data', function (chunk) {
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(null, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(null, lines[i], opts, stylize);
}
});
if (nodeSpawnSupportsStdio) {
dtrace.on('exit', finish);
} else {
// Fallback (for < v0.8) to pipe the dtrace process' stderr to
// this stderr. Wait for all of (1) process 'exit', (2) stderr
// 'end', and (2) stdout 'end' before returning to ensure all
// stderr is flushed (issue #54).
var returnCode = null;
var eventsRemaining = 3;
function countdownToFinish(code) {
returnCode = code;
eventsRemaining--;
if (eventsRemaining == 0) {
finish(returnCode);
}
}
dtrace.stderr.pipe(process.stderr);
dtrace.stderr.on('end', countdownToFinish);
dtrace.stderr.on('end', countdownToFinish);
dtrace.on('exit', countdownToFinish);
}
});
}
/**
* Process all input from the given log file.
*
* @param file {String} Log file path to process.
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function ()`
*/
function processFile(file, opts, stylize, callback) {
var stream = fs.createReadStream(file);
if (/\.gz$/.test(file)) {
stream = stream.pipe(require('zlib').createGunzip());
}
// Manually decode streams - lazy load here as per node/lib/fs.js
var decoder = new (require('string_decoder').StringDecoder)('utf8');
streams[file].stream = stream;
stream.on('error', function (err) {
streams[file].done = true;
callback(err);
});
var leftover = ''; // Left-over partial line from last chunk.
stream.on('data', function (data) {
var chunk = decoder.write(data);
if (!chunk.length) {
return;
}
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(file, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(file, lines[i], opts, stylize);
}
});
stream.on('end', function () {
streams[file].done = true;
if (leftover) {
handleLogLine(file, leftover, opts, stylize);
leftover = '';
} else {
emitNextRecord(opts, stylize);
}
callback();
});
}
/**
* From node async module.
*/
/* BEGIN JSSTYLED */
function asyncForEach(arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
arr.forEach(function (x) {
iterator(x, function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed === arr.length) {
callback();
}
}
});
});
};
/* END JSSTYLED */
/**
* Cleanup and exit properly.
*
* Warning: this doesn't stop processing, i.e. process exit might be delayed.
* It is up to the caller to ensure that no subsequent bunyan processing
* is done after calling this.
*
* @param code {Number} exit code.
* @param signal {String} Optional signal name, if this was exitting because
* of a signal.
*/
var cleanedUp = false;
function cleanupAndExit(code, signal) {
// Guard one call.
if (cleanedUp) {
return;
}
cleanedUp = true;
if (_DEBUG) warn('(bunyan: cleanupAndExit)');
// Clear possibly interrupted ANSI code (issue #59).
if (usingAnsiCodes) {
stdout.write('\033[0m');
}
// Kill possible dtrace child.
if (child) {
child.kill(signal);
}
if (pager) {
// Let pager know that output is done, then wait for pager to exit.
stdout.end();
pager.on('exit', function (pagerCode) {
if (_DEBUG)
warn('(bunyan: pager exit -> process.exit(%s))',
pagerCode || code);
process.exit(pagerCode || code);
});
} else {
if (_DEBUG) warn('(bunyan: process.exit(%s))', code);
process.exit(code);
}
}
//---- mainline
process.on('SIGINT', function () { cleanupAndExit(1, 'SIGINT'); });
process.on('SIGQUIT', function () { cleanupAndExit(1, 'SIGQUIT'); });
process.on('SIGTERM', function () { cleanupAndExit(1, 'SIGTERM'); });
process.on('SIGHUP', function () { cleanupAndExit(1, 'SIGHUP'); });
process.on('uncaughtException', function (err) {
function _indent(s) {
var lines = s.split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
lines[i] = '* ' + lines[i];
}
return lines.join('\n');
}
var title = encodeURIComponent(format(
'Bunyan %s crashed: %s', getVersion(), String(err)));
var e = console.error;
e('* * *');
e('* The Bunyan CLI crashed!');
e('*');
if (err.name === 'ReferenceError' && gOptsForUncaughtException.conditions) {
e('* A "ReferenceError" is often the result of given');
e('* `-c CONDITION` code that doesn\'t guard against undefined');
e('* values. If that is not the problem:');
e('*');
}
e('* Please report this issue and include the details below:');
e('*');
e('* https://github.com/trentm/node-bunyan/issues/new?title=%s', title);
e('*');
e('* * *');
e('* platform:', process.platform);
e('* node version:', process.version);
e('* bunyan version:', getVersion());
e('* argv: %j', process.argv);
e('* log line: %j', currLine);
e('* stack:');
e(_indent(err.stack));
e('* * *');
process.exit(1);
});
function main(argv) {
try {
var opts = parseArgv(argv);
} catch (e) {
warn('bunyan: error: %s', e.message);
return drainStdoutAndExit(1);
}
gOptsForUncaughtException = opts; // intentionally global
if (opts.help) {
printHelp();
return;
}
if (opts.version) {
console.log('bunyan ' + getVersion());
return;
}
if (opts.pid && opts.args.length > 0) {
warn('bunyan: error: can\'t use both "-p PID" (%s) and file (%s) args',
opts.pid, opts.args.join(' '));
return drainStdoutAndExit(1);
}
if (opts.color === null) {
if (process.env.BUNYAN_NO_COLOR &&
process.env.BUNYAN_NO_COLOR.length > 0) {
opts.color = false;
} else {
opts.color = process.stdout.isTTY;
}
}
usingAnsiCodes = opts.color; // intentionally global
var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor);
// Pager.
var nodeVer = process.versions.node.split('.').map(Number);
var paginate = (
process.stdout.isTTY &&
process.stdin.isTTY &&
!opts.pids && // Don't page if following process output.
opts.args.length > 0 && // Don't page if no file args to process.
process.platform !== 'win32' &&
(nodeVer[0] > 0 || nodeVer[1] >= 8) &&
(opts.paginate === true ||
(opts.paginate !== false &&
(!process.env.BUNYAN_NO_PAGER ||
process.env.BUNYAN_NO_PAGER.length === 0))));
if (paginate) {
var pagerCmd = process.env.PAGER || 'less';
/* JSSTYLED */
assert.ok(pagerCmd.indexOf('"') === -1 && pagerCmd.indexOf("'") === -1,
'cannot parse PAGER quotes yet');
var argv = pagerCmd.split(/\s+/g);
var env = objCopy(process.env);
if (env.LESS === undefined) {
// git's default is LESS=FRSX. I don't like the 'S' here because
// lines are *typically* wide with bunyan output and scrolling
// horizontally is a royal pain. Note a bug in Mac's `less -F`,
// such that SIGWINCH can kill it. If that rears too much then
// I'll remove 'F' from here.
env.LESS = 'FRX';
}
if (_DEBUG) warn('(pager: argv=%j, env.LESS=%j)', argv, env.LESS);
// `pager` and `stdout` intentionally global.
pager = spawn(argv[0], argv.slice(1),
// Share the stderr handle to have error output come
// straight through. Only supported in v0.8+.
{env: env, stdio: ['pipe', 1, 2]});
stdout = pager.stdin;
// Early termination of the pager: just stop.
pager.on('exit', function (pagerCode) {
if (_DEBUG) warn('(bunyan: pager exit)');
pager = null;
stdout.end()
stdout = process.stdout;
cleanupAndExit(pagerCode);
});
}
// Stdout error handling. (Couldn't setup until `stdout` was determined.)
stdout.on('error', function (err) {
if (_DEBUG) warn('(stdout error event: %s)', err);
if (err.code === 'EPIPE') {
drainStdoutAndExit(0);
} else if (err.toString() === 'Error: This socket is closed.') {
// Could get this if the pager closes its stdin, but hasn't
// exited yet.
drainStdoutAndExit(1);
} else {
warn(err);
drainStdoutAndExit(1);
}
});
var retval = 0;
if (opts.pids) {
processPids(opts, stylize, function (code) {
cleanupAndExit(code);
});
} else if (opts.args.length > 0) {
var files = opts.args;
files.forEach(function (file) {
streams[file] = { stream: null, records: [], done: false }
});
asyncForEach(files,
function (file, next) {
processFile(file, opts, stylize, function (err) {
if (err) {
warn('bunyan: %s', err.message);
retval += 1;
}
next();
});
},
function (err) {
if (err) {
warn('bunyan: unexpected error: %s', err.stack || err);
return drainStdoutAndExit(1);
}
cleanupAndExit(retval);
}
);
} else {
processStdin(opts, stylize, function () {
cleanupAndExit(retval);
});
}
}
if (require.main === module) {
// HACK guard for <https://github.com/trentm/json/issues/24>.
// We override the `process.stdout.end` guard that core node.js puts in
// place. The real fix is that `.end()` shouldn't be called on stdout
// in node core. Node v0.6.9 fixes that. Only guard for v0.6.0..v0.6.8.
var nodeVer = process.versions.node.split('.').map(Number);
if ([0, 6, 0] <= nodeVer && nodeVer <= [0, 6, 8]) {
var stdout = process.stdout;
stdout.end = stdout.destroy = stdout.destroySoon = function () {
/* pass */
};
}
main(process.argv);
}
#!/usr/bin/env node
// Standalone semver comparison program.
// Exits successfully and prints matching version(s) if
// any supplied version is valid and passes all tests.
var argv = process.argv.slice(2)
, versions = []
, range = []
, gt = []
, lt = []
, eq = []
, inc = null
, version = require("../package.json").version
, loose = false
, semver = require("../semver")
, reverse = false
main()
function main () {
if (!argv.length) return help()
while (argv.length) {
var a = argv.shift()
var i = a.indexOf('=')
if (i !== -1) {
a = a.slice(0, i)
argv.unshift(a.slice(i + 1))
}
switch (a) {
case "-rv": case "-rev": case "--rev": case "--reverse":
reverse = true
break
case "-l": case "--loose":
loose = true
break
case "-v": case "--version":
versions.push(argv.shift())
break
case "-i": case "--inc": case "--increment":
switch (argv[0]) {
case "major": case "minor": case "patch": case "prerelease":
inc = argv.shift()
break
default:
inc = "patch"
break
}
break
case "-r": case "--range":
range.push(argv.shift())
break
case "-h": case "--help": case "-?":
return help()
default:
versions.push(a)
break
}
}
versions = versions.filter(function (v) {
return semver.valid(v, loose)
})
if (!versions.length) return fail()
if (inc && (versions.length !== 1 || range.length))
return failInc()
for (var i = 0, l = range.length; i < l ; i ++) {
versions = versions.filter(function (v) {
return semver.satisfies(v, range[i], loose)
})
if (!versions.length) return fail()
}
return success(versions)
}
function failInc () {
console.error("--inc can only be used on a single version with no range")
fail()
}
function fail () { process.exit(1) }
function success () {
var compare = reverse ? "rcompare" : "compare"
versions.sort(function (a, b) {
return semver[compare](a, b, loose)
}).map(function (v) {
return semver.clean(v, loose)
}).map(function (v) {
return inc ? semver.inc(v, inc, loose) : v
}).forEach(function (v,i,_) { console.log(v) })
}
function help () {
console.log(["SemVer " + version
,""
,"A JavaScript implementation of the http://semver.org/ specification"
,"Copyright Isaac Z. Schlueter"
,""
,"Usage: semver [options] <version> [<version> [...]]"
,"Prints valid versions sorted by SemVer precedence"
,""
,"Options:"
,"-r --range <range>"
," Print versions that match the specified range."
,""
,"-i --increment [<level>]"
," Increment a version by the specified level. Level can"
," be one of: major, minor, patch, or prerelease"
," Default level is 'patch'."
," Only one version may be specified."
,""
,"-l --loose"
," Interpret versions and ranges loosely"
,""
,"Program exits successfully if any valid version satisfies"
,"all supplied ranges, and prints all satisfying versions."
,""
,"If no satisfying versions are found, then exits failure."
,""
,"Versions are printed in ascending order, so supplying"
,"multiple versions to the utility will just sort them."
].join("\n"))
}
// Copyright (c) 2012, Mark Cavage. All rights reserved.
var assert = require('assert');
var Stream = require('stream').Stream;
var util = require('util');
///--- Globals
var NDEBUG = process.env.NODE_NDEBUG || false;
var UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
///--- Messages
var ARRAY_TYPE_REQUIRED = '%s ([%s]) required';
var TYPE_REQUIRED = '%s (%s) is required';
///--- Internal
function capitalize(str) {
return (str.charAt(0).toUpperCase() + str.slice(1));
}
function uncapitalize(str) {
return (str.charAt(0).toLowerCase() + str.slice(1));
}
function _() {
return (util.format.apply(util, arguments));
}
function _assert(arg, type, name, stackFunc) {
if (!NDEBUG) {
name = name || type;
stackFunc = stackFunc || _assert.caller;
var t = typeof (arg);
if (t !== type) {
throw new assert.AssertionError({
message: _(TYPE_REQUIRED, name, type),
actual: t,
expected: type,
operator: '===',
stackStartFunction: stackFunc
});
}
}
}
function _instanceof(arg, type, name, stackFunc) {
if (!NDEBUG) {
name = name || type;
stackFunc = stackFunc || _instanceof.caller;
if (!(arg instanceof type)) {
throw new assert.AssertionError({
message: _(TYPE_REQUIRED, name, type.name),
actual: _getClass(arg),
expected: type.name,
operator: 'instanceof',
stackStartFunction: stackFunc
});
}
}
}
function _getClass(object) {
return (Object.prototype.toString.call(object).slice(8, -1));
};
///--- API
function array(arr, type, name) {
if (!NDEBUG) {
name = name || type;
if (!Array.isArray(arr)) {
throw new assert.AssertionError({
message: _(ARRAY_TYPE_REQUIRED, name, type),
actual: typeof (arr),
expected: 'array',
operator: 'Array.isArray',
stackStartFunction: array.caller
});
}
for (var i = 0; i < arr.length; i++) {
_assert(arr[i], type, name, array);
}
}
}
function bool(arg, name) {
_assert(arg, 'boolean', name, bool);
}
function buffer(arg, name) {
if (!Buffer.isBuffer(arg)) {
throw new assert.AssertionError({
message: _(TYPE_REQUIRED, name || '', 'Buffer'),
actual: typeof (arg),
expected: 'buffer',
operator: 'Buffer.isBuffer',
stackStartFunction: buffer
});
}
}
function func(arg, name) {
_assert(arg, 'function', name);
}
function number(arg, name) {
_assert(arg, 'number', name);
if (!NDEBUG && (isNaN(arg) || !isFinite(arg))) {
throw new assert.AssertionError({
message: _(TYPE_REQUIRED, name, 'number'),
actual: arg,
expected: 'number',
operator: 'isNaN',
stackStartFunction: number
});
}
}
function object(arg, name) {
_assert(arg, 'object', name);
}
function stream(arg, name) {
_instanceof(arg, Stream, name);
}
function date(arg, name) {
_instanceof(arg, Date, name);
}
function regexp(arg, name) {
_instanceof(arg, RegExp, name);
}
function string(arg, name) {
_assert(arg, 'string', name);
}
function uuid(arg, name) {
string(arg, name);
if (!NDEBUG && !UUID_REGEXP.test(arg)) {
throw new assert.AssertionError({
message: _(TYPE_REQUIRED, name, 'uuid'),
actual: 'string',
expected: 'uuid',
operator: 'test',
stackStartFunction: uuid
});
}
}
///--- Exports
module.exports = {
bool: bool,
buffer: buffer,
date: date,
func: func,
number: number,
object: object,
regexp: regexp,
stream: stream,
string: string,
uuid: uuid
};
Object.keys(module.exports).forEach(function (k) {
if (k === 'buffer')
return;
var name = 'arrayOf' + capitalize(k);
if (k === 'bool')
k = 'boolean';
if (k === 'func')
k = 'function';
module.exports[name] = function (arg, name) {
array(arg, k, name);
};
});
Object.keys(module.exports).forEach(function (k) {
var _name = 'optional' + capitalize(k);
var s = uncapitalize(k.replace('arrayOf', ''));
if (s === 'bool')
s = 'boolean';
if (s === 'func')
s = 'function';
if (k.indexOf('arrayOf') !== -1) {
module.exports[_name] = function (arg, name) {
if (!NDEBUG && arg !== undefined) {
array(arg, s, name);
}
};
} else {
module.exports[_name] = function (arg, name) {
if (!NDEBUG && arg !== undefined) {
_assert(arg, s, name);
}
};
}
});
// Reexport built-in assertions
Object.keys(assert).forEach(function (k) {
if (k === 'AssertionError') {
module.exports[k] = assert[k];
return;
}
module.exports[k] = function () {
if (!NDEBUG) {
assert[k].apply(assert[k], arguments);
}
};
});
{
"author": {
"name": "Mark Cavage",
"email": "mcavage@gmail.com"
},
"name": "assert-plus",
"description": "Extra assertions on top of node's assert module",
"version": "0.1.5",
"main": "./assert.js",
"devDependencies": {},
"optionalDependencies": {},
"repository": {
"type": "git",
"url": "https://github.com/mcavage/node-assert-plus.git"
},
"engines": {
"node": ">=0.8"
},
"readme": "# node-assert-plus\n\nThis library is a super small wrapper over node's assert module that has two\nthings: (1) the ability to disable assertions with the environment variable\nNODE_NDEBUG, and (2) some API wrappers for argument testing. Like\n`assert.string(myArg, 'myArg')`. As a simple example, most of my code looks\nlike this:\n\n var assert = require('assert-plus');\n\n function fooAccount(options, callback) {\n\t assert.object(options, 'options');\n\t\tassert.number(options.id, 'options.id);\n\t\tassert.bool(options.isManager, 'options.isManager');\n\t\tassert.string(options.name, 'options.name');\n\t\tassert.arrayOfString(options.email, 'options.email');\n\t\tassert.func(callback, 'callback');\n\n // Do stuff\n\t\tcallback(null, {});\n }\n\n# API\n\nAll methods that *aren't* part of node's core assert API are simply assumed to\ntake an argument, and then a string 'name' that's not a message; `AssertionError`\nwill be thrown if the assertion fails with a message like:\n\n AssertionError: foo (string) is required\n\tat test (/home/mark/work/foo/foo.js:3:9)\n\tat Object.<anonymous> (/home/mark/work/foo/foo.js:15:1)\n\tat Module._compile (module.js:446:26)\n\tat Object..js (module.js:464:10)\n\tat Module.load (module.js:353:31)\n\tat Function._load (module.js:311:12)\n\tat Array.0 (module.js:484:10)\n\tat EventEmitter._tickCallback (node.js:190:38)\n\nfrom:\n\n function test(foo) {\n\t assert.string(foo, 'foo');\n }\n\nThere you go. You can check that arrays are of a homogenous type with `Arrayof$Type`:\n\n function test(foo) {\n\t assert.arrayOfString(foo, 'foo');\n }\n\nYou can assert IFF an argument is not `undefined` (i.e., an optional arg):\n\n assert.optionalString(foo, 'foo');\n\nLastly, you can opt-out of assertion checking altogether by setting the\nenvironment variable `NODE_NDEBUG=1`. This is pseudo-useful if you have\nlots of assertions, and don't want to pay `typeof ()` taxes to v8 in\nproduction.\n\nThe complete list of APIs is:\n\n* assert.bool\n* assert.buffer\n* assert.func\n* assert.number\n* assert.object\n* assert.string\n* assert.arrayOfBool\n* assert.arrayOfFunc\n* assert.arrayOfNumber\n* assert.arrayOfObject\n* assert.arrayOfString\n* assert.optionalBool\n* assert.optionalBuffer\n* assert.optionalFunc\n* assert.optionalNumber\n* assert.optionalObject\n* assert.optionalString\n* assert.optionalArrayOfBool\n* assert.optionalArrayOfFunc\n* assert.optionalArrayOfNumber\n* assert.optionalArrayOfObject\n* assert.optionalArrayOfString\n* assert.AssertionError\n* assert.fail\n* assert.ok\n* assert.equal\n* assert.notEqual\n* assert.deepEqual\n* assert.notDeepEqual\n* assert.strictEqual\n* assert.notStrictEqual\n* assert.throws\n* assert.doesNotThrow\n* assert.ifError\n\n# Installation\n\n npm install assert-plus\n\n## License\n\nThe MIT License (MIT)\nCopyright (c) 2012 Mark Cavage\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n## Bugs\n\nSee <https://github.com/mcavage/node-assert-plus/issues>.\n",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/mcavage/node-assert-plus/issues"
},
"homepage": "https://github.com/mcavage/node-assert-plus",
"dependencies": {},
"_id": "assert-plus@0.1.5",
"dist": {
"shasum": "42153a9882e8f171e5d186d7a3372042306b43cf"
},
"_from": "assert-plus@0.1.5",
"_resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
}

node-assert-plus

This library is a super small wrapper over node's assert module that has two things: (1) the ability to disable assertions with the environment variable NODE_NDEBUG, and (2) some API wrappers for argument testing. Like assert.string(myArg, 'myArg'). As a simple example, most of my code looks like this:

var assert = require('assert-plus');

function fooAccount(options, callback) {
    assert.object(options, 'options');
	assert.number(options.id, 'options.id);
	assert.bool(options.isManager, 'options.isManager');
	assert.string(options.name, 'options.name');
	assert.arrayOfString(options.email, 'options.email');
	assert.func(callback, 'callback');

    // Do stuff
	callback(null, {});
}

API

All methods that aren't part of node's core assert API are simply assumed to take an argument, and then a string 'name' that's not a message; AssertionError will be thrown if the assertion fails with a message like:

AssertionError: foo (string) is required
at test (/home/mark/work/foo/foo.js:3:9)
at Object.<anonymous> (/home/mark/work/foo/foo.js:15:1)
at Module._compile (module.js:446:26)
at Object..js (module.js:464:10)
at Module.load (module.js:353:31)
at Function._load (module.js:311:12)
at Array.0 (module.js:484:10)
at EventEmitter._tickCallback (node.js:190:38)

from:

function test(foo) {
    assert.string(foo, 'foo');
}

There you go. You can check that arrays are of a homogenous type with Arrayof$Type:

function test(foo) {
    assert.arrayOfString(foo, 'foo');
}

You can assert IFF an argument is not undefined (i.e., an optional arg):

assert.optionalString(foo, 'foo');

Lastly, you can opt-out of assertion checking altogether by setting the environment variable NODE_NDEBUG=1. This is pseudo-useful if you have lots of assertions, and don't want to pay typeof () taxes to v8 in production.

The complete list of APIs is:

  • assert.bool
  • assert.buffer
  • assert.func
  • assert.number
  • assert.object
  • assert.string
  • assert.arrayOfBool
  • assert.arrayOfFunc
  • assert.arrayOfNumber
  • assert.arrayOfObject
  • assert.arrayOfString
  • assert.optionalBool
  • assert.optionalBuffer
  • assert.optionalFunc
  • assert.optionalNumber
  • assert.optionalObject
  • assert.optionalString
  • assert.optionalArrayOfBool
  • assert.optionalArrayOfFunc
  • assert.optionalArrayOfNumber
  • assert.optionalArrayOfObject
  • assert.optionalArrayOfString
  • assert.AssertionError
  • assert.fail
  • assert.ok
  • assert.equal
  • assert.notEqual
  • assert.deepEqual
  • assert.notDeepEqual
  • assert.strictEqual
  • assert.notStrictEqual
  • assert.throws
  • assert.doesNotThrow
  • assert.ifError

Installation

npm install assert-plus

License

The MIT License (MIT) Copyright (c) 2012 Mark Cavage

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Bugs

See https://github.com/mcavage/node-assert-plus/issues.

// Based on jshint defaults: http://goo.gl/OpjUs
{
// If the scan should stop on first error.
"passfail": false,
// Maximum errors before stopping.
"maxerr": 50,
// Predefined globals
// If the standard browser globals should be predefined.
"browser": false,
// If the Node.js environment globals should be predefined.
"node": true,
// If the Rhino environment globals should be predefined.
"rhino": false,
// If CouchDB globals should be predefined.
"couch": false,
// If the Windows Scripting Host environment globals should be predefined.
"wsh": false,
// If jQuery globals should be predefined.
"jquery": false,
// If Prototype and Scriptaculous globals should be predefined.
"prototypejs": false,
// If MooTools globals should be predefined.
"mootools": false,
// If Dojo Toolkit globals should be predefined.
"dojo": false,
// Custom predefined globals.
"predef": [],
// Development
// If debugger statements should be allowed.
"debug": false,
// If logging globals should be predefined (console, alert, etc.).
"devel": false,
// ECMAScript 5
// If ES5 syntax should be allowed.
"es5": false,
// Require the "use strict"; pragma.
"strict": false,
// If global "use strict"; should be allowed (also enables strict).
"globalstrict": false,
// The Good Parts
// If automatic semicolon insertion should be tolerated.
"asi": false,
// If line breaks should not be checked, e.g. `return [\n] x`.
"laxbreak": false,
// If bitwise operators (&, |, ^, etc.) should not be allowed.
"bitwise": false,
// If assignments inside if, for and while should be allowed. Usually
// conditions and loops are for comparison, not assignments.
"boss": true,
// If curly braces around all blocks should be required.
"curly": true,
// If === should be required.
"eqeqeq": false,
// If == null comparisons should be tolerated.
"eqnull": false,
// If eval should be allowed.
"evil": true,
// If ExpressionStatement should be allowed as Programs.
"expr": false,
// If `for in` loops must filter with `hasOwnPrototype`.
"forin": false,
// If immediate invocations must be wrapped in parens, e.g.
// `( function(){}() );`.
"immed": false,
// If use before define should not be tolerated.
"latedef": false,
// If functions should be allowed to be defined within loops.
"loopfunc": false,
// If arguments.caller and arguments.callee should be disallowed.
"noarg": false,
// If the . should not be allowed in regexp literals.
"regexp": false,
// If unescaped first/last dash (-) inside brackets should be tolerated.
"regexdash": false,
// If script-targeted URLs should be tolerated.
"scripturl": false,
// If variable shadowing should be tolerated.
"shadow": false,
// If `new function () { ... };` and `new Object;` should be tolerated.
"supernew": false,
// If variables should be declared before used.
"undef": true,
// If `this` inside a non-constructor function is valid.
"validthis": false,
// If smarttabs should be tolerated
// (http://www.emacswiki.org/emacs/SmartTabs).
"smarttabs": false,
// If the `__proto__` property should be allowed.
"proto": false,
// If one case switch statements should be allowed.
"onecase": false,
// If non-standard (but widely adopted) globals should be predefined.
"nonstandard": false,
// Allow multiline strings.
"multistr": false,
// If line breaks should not be checked around commas.
"laxcomma": false,
// If semicolons may be ommitted for the trailing statements inside of a
// one-line blocks.
"lastsemic": false,
// If the `__iterator__` property should be allowed.
"iterator": false,
// If only function scope should be used for scope tests.
"funcscope": false,
// If es.next specific syntax should be allowed.
"esnext": false,
// Style preferences
// If constructor names must be capitalized.
"newcap": true,
// If empty blocks should be disallowed.
"noempty": false,
// If using `new` for side-effects should be disallowed.
"nonew": false,
// If names should be checked for leading or trailing underscores
// (object._attribute would be disallowed).
"nomen": false,
// If only one var statement per function should be allowed.
"onevar": false,
// If increment and decrement (`++` and `--`) should not be allowed.
"plusplus": false,
// If all forms of subscript notation are tolerated.
"sub": true,
// If trailing whitespace rules apply.
"trailing": true,
// If strict whitespace rules apply.
"white": false,
// Specify indentation.
"indent": 4
}
language: node_js
node_js:
- "0.6"
- "0.7"
- "0.8"
- "0.10"
notifications:
email:
- turcotte.mat@gmail.com

Changelog

2.3.0

  • Add four new methods to FunctionCall to query the state of the call.
    • isPending
    • isRunning
    • isCompleted
    • isAborted

2.2.0

  • To match Backoff default behavior, FunctionCall no longer sets a default failAfter of 5, i.e. the maximum number of backoffs is now unbounded by default.

2.1.0

  • Backoff.backoff now accepts an optional error argument that is re-emitted as the last argument of the backoff and fail events. This provides some context to the listeners as to why a given backoff operation was attempted.
  • The backoff event emitted by the FunctionCall class now contains, as its last argument, the error that caused the backoff operation to be attempted. This provides some context to the listeners as to why a given backoff operation was attempted.

2.0.0

  • FunctionCall.call renamed into FunctionCall.start.
  • backoff.call no longer invokes the wrapped function on nextTick. That way, the first attempt is not delayed until the end of the current event loop.

1.2.1

  • Make FunctionCall.backoffFactory a private member.

1.2.0

  • Add backoff.call and the associated FunctionCall class.

1.1.0

  • Add a Backoff.failAfter.

1.0.0

  • Rename start and done events backoff and ready.
  • Remove deprecated backoff.fibonnaci.

0.2.1

  • Create backoff.fibonacci.
  • Deprecate backoff.fibonnaci.
  • Expose fibonacci and exponential strategies.

0.2.0

  • Provide exponential and fibonacci backoffs.

0.1.0

  • Change initialTimeout and maxTimeout to initialDelay and maxDelay.
  • Use fibonnaci backoff.
�PNG

IHDR��˰( �IDATx��]h\�u�T%���&��A�nQ�o,_PQ�C���C]�\�����fn�;�RP�SP�J���J�� N�Jբ�-��=�A�
*������ﳖf�֑�3�C���F�33��9��^k�vB!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�RdB�y2�˴a�J<�lg��CQFM�3mӴi�xJR�H=4mʹ�����l�]���v.�B� q{����H�i�L;��A^�v�h;�tH� ���R�[���4��~9�]�q�L�1�)����� ʰ�y�9�@$V��5m1�ҽ.e��<]�3eڎ�߭�`j�K��r��&�ͤg;�clCf+�B�?��o9��9q �$8L��c��(��v��r�o�DYk�<!`D:�j;W�3>[^ǎ8!ǜUq ~�A���3A4��{��r���Zee�V~2T�^��Fg�b.�tL� N�w�i�������mT���W����(�Q��1�vn���'�x��(�8"�%�X����١��q���k�k9�z��i�;c��H�s�`R���p`�^��m���-r��;"�(���Β��rL���"Wc^��0�~�p��^�`���i�2�*g�v;�K��1�阨��K�\�:l�bô�#wr;���u�;�yc;S1Q;��q^mB����5���7�#�x��q�s�ʘqȫp��j������H�V����Xj�%�S'+��yDې纼��,��u�&�j��s���hWg��N���~{"<54Q^3�O�8ox�(l��y�v.J'1�^�9�#p�?���AsM9������M�C�M�%�?�pC��׽3qz���ϔ��]G���:�p ��?���F� �t��ת��K�ܦ֍��3������#9�7�1ݕ�޻e��S�ܻX�X�'�x�&��L۝�Ƚ�/��-�9��W�'�!]�( �u_��Z�R������Γ;'c��r]��zp9Q"�k=���V(�6v��w�0!μ�YN����`�K:3�B:�Sҳ�g�5�ÇD�r��ʍ8',�VG��M����i�6��EK#��Th ��k'i�KH���'����٥���s�e����9���y�:������r�0��&�r��9c�ދ֦0ƞ�W�9>�E殘\���� n�|���,���8�+�e�f!�ȠM�
B��"
:֎#��sL������Du����G2^���H���[Q@^17v��4:V/@�e��s#�-U�>Q�Aw�ɸ��k7D�a/;"�q�e��t̊-�8��y>? �&����:ҩ�'*�~���2$,�u����@� �������j����X:�u<]��<&L�p�V�,�t)8X$����
&�Ǽ��������e�&�k9��UǼÈ<A1�UVm7m�z����DC�|�<�.a\���t|<���,�B9.�2�����ʎ/舼�v������ qK�kمi'�.�gx]AK�B�1q�&2�z��� #��#s���׬�;cⰙWy#$u�A7�B~ �ҎhH�v���X�1��%A��ԫWu�"�<J�W�RP�P� �5v�H�����WZ5m�~߇.�ag7+�� �$�Z�R3LdC$�q�Ct�FN�1!i���[1�U!7Q�|N�]�01����a�M;b�D��������7?x�g$}��ԍ4J",���tDmfL~� ��'��(A�A�Ϧ��jM!�M�1ܕp�蜶CRQ��t������'���k�9T{����HO^?�R�Xw:Qq��b��ԥ-�v9Z#GB���e�p� �v!�v�y���$��/�Q�)���0?7M����t�s��z�eHd8��<��MAIױ�;���-M��am�AR��\�U�r��;�����0t�G��'�������u���;���4����jϨ�{}=O �-��W�k�}������c��-��u���<�c�"W�Z?W;=�㯹>���>#�a� 0^=�N��n;m簔96�ѱJ�8�BN���$ 1K;��C+�'��;����0)�g-yn�#� � ZkyO�ܦ�G_PrE?�kǣ��y+v����m��0;y�&�������� ���1u���lgS:|Gm�D��F�+F���Ǭ����-ѹM��2��0H�b����rD�H��H[
���A���з'�SZ�������. ��8�!�8��8�nyJ^Wk�-���J��nr���E�5�9{��4�>���ĸ��ޓn� !7���÷>���� b��Nx���,&��K��"ܣ1�a<(�V�(�=��.x���"!G}�w���^v��&&��Ԇe�؛ފz�&9x��޳�@ǷD�۞��^��vV�c��!�H�c›+舶!҈�[��|7,�x�����՘q�Ѷn�p��7��ռ��.ꪻ���M��-L#�/ϧP��xY��.���n�4�Iߥ��W�ߙ8=�kn;t���Ɖ6^7T�Τ�W}�v`+�rߪ��9�3 �(�$i �F̟��rq�"1�M����^X��z�Z&鲈LrA{�p+����n��_+�?�F�l�F5��} &ŝ�U����P�c'h�NG�
��܉m��16��m� w�;�";��
:��A�}۹O��]plG�j�h;��b�s�^_�$"�ai�E�eM9"r9��񌵇AqDDƂ�^�����Y��B��A㐟�%X��i�Q�*n��g���c^�M�h|�tDYO�1�E��m<>j�Q���I}��Ω��*����E��C�/�6��� A���iD�Ժ;Ý���A�d�1yW�ul�Q�h1���5T�j96Q���Wn��UG!�ؿ<���k��%B�b]#�)q�4����z�ed��������7w��a�s��C�E����f@��X�f�~��I��H��c�b�p UMh*���'��h��j=��#N��:�Ü2I�6�.�D�tlg��C��D�7�F�<3�p�A/�@�ѱ�܆���D�X��ӯ;4S��SưFH�L8"��.�Ov�23{��ַ�I��
D�Qz=��n#��Һ�<�$N0*���IG��*�/�ҥF�f����yg�մt�DV#�S� �.C�ȣl�v�t7���J��@�}��٢��"s��.E�,���I1�r�Iޭ�6.��T��QLvs+za��:�Zu�YF_kA�XI���e ��_����6ʷ1n��r�
�AѠeޱ��b;�
;��nLt�HDⷼ�����R��Xt�^������ի^�]w��}O4n�R�Ǎ�����q��y:���7J�X�oRp�]bes��oD�)�yFt�ւ�qpd�����CH��,8������B���j]y���㶂]��ǽ�����6�^�9�]и�2Iۈ:r[��A��Ed/Q��:s�Lu�br�/e;��$I�U:�"0��ާ���3��f� �tK�pk���\͵�� ��U,9��{����p��.�نr��� �I;:{"�sW����v��Bq�ױ]2��}��P�u^m����/ao�~_�8��PгsH���s�k�P����|.8��i!��4�K�~q��De1z����K^�c$h�@���'Eı5% z$ ��b\�^o7����\~�����R��\{��ݤ�$y�Wz� ��bB7�_�_�zꜭ�~t��G���?D�3���!/��C�7"p���qպ�C�S�Z:���!b����`hc98�r'�H�O��la#ևm���Qk '_�EmgSlg��C�̱Ui�g�~�~���0�<+��+�sTx;s�vy�];~�R������N��&Ǻi��T������o�w����V7%�?/iv_�Jg�E�����J�>K�q�9�3B��St��6�I`�
&�M�A`B�� �9v ���5�8��Di�M���/Y�E�F�9�yX���HFg;Ŏ���c��M�v��IV��J��(�UNw�֠�b��0�i� <��a@z�AGJ'����|�ף�2�uw�Y4� KΚ���y�$ǹ�w'��� �|�H��b��7ݥg2��es�T1� G��ՠ9������]�I��Q�-�976RQ!�ۧ�$Ϙ��q��qp�摮+��޳)T�%}�d����?1NZ;�Eԋs�q��|貦cQ�>��_ŭ����0tD�R���ïұ�E�p����D�o��Uڌ�/!B�_�h��R#��� 1��y�B��b=0f�c�|�^�ń&G�oٴ�De1Cg=#Q�K�8j�Q�i;:y�::u���r�}��_�-��Z�%[�A��ı{���VM�WA�T�_��A_�5s�9�<=� J��`����"�c2yiC�Kĵ�N;'_s9��b9���9�� H ��1o����Yї�@4�W"D����vH"�{�}a�.5�*�=O�cr��^�8���.&Y��G-�.�?��K'��IL�ʳ(�����)����N�l{Go�Vt3�7l�fk��E�<`2�3���n�
�D#�0�F�}/��t�8����(��<�^{G�clW$:��+̀)t�e�3����.#��G�(:?%�$�HX�Δ�f).�h���+4BW[@�C39������5��؊��-�ip���Hw�ڛ�����0^~'<��Dm3��b,ܝ$s]���(\T.e7o`lYg$k�V������3`�9�8�s�q�Kؖ�����0�������{ah�E��p-�MT&�k9-��ڎ�슱��`{%����(�Im���c�t<�|4�����٠uv�'�'%/���h\<Zf�c�R���&xF_q��~ ��ҿiq�hCN��$k��:��!3S�ņ9� ڶ���Ly7���l�n���;�N�!�q��� {��K�fu�e�jo��u�+R��$"�c(tA|;>n���K��!���񏜌"�YŮ�Qҙ�a�v�2i��F�6�nlv�=��^�ѤH�P29��:��
��mD�J3v��L�SA7�o�c;~��^�I2[�$�����$���Y'�X��=�+ř'$�%�(ݤcZ���l��ת�����a�w�T�Z/��Pہ�t�.�ۙ�U&I�
���Q��A_i���f�#�*��@�G��%*S�@��B��;>%��R��NW�rWvb^�s��4��6^
^�'�<��i6�m�*�*�V�1v^��ʱ��3(���S�3�C!ڋ�g�v���*��[�:���xS�Ƀ19y
iu�Ѽ4��A�0Q�[��R!+E!��:r��&b���T�ZwKs�T�vD�K�0�1#���S�u��ٶA�eb���޸�ޏ&JV����D�04K)�3+�>(�sY^��+M��
sX�q׊c'�{o~�l����jwB�묭�V����$7���_GpQ"A�m^
:V���A��[�ʆ){�q!wwGK�`wEЗ���>�q<�t��D�Y�#�N���ol�ۢT��m�l��5�v�� �>��ǷD�i;$I���.*�� {�Fc�=�8n7I�+����YXZ�8:#��Rg��O]n�IX���D�q��1^�GU� #.t�<�r����aQ���m؝�h�0����K���bD��
w3B/���h�Ӟ+<[�
:z���B��
���A��Ϝ9�Fpp���9� ��sf/j;�p?�������r�e��<;�!Z���K�ȳtw�Ӏ���ՅPA���VZ�G�;�c~ǙRD�M�Pз��͏0l3�+JRul�b]��m
c�a�;�N���gE\�嶈������)�ITg��nن.M+�����W�d �;:s��G�!$,�>�1QI�� �Q:!�!ۘ���F~�!C-�) ��"�)��γ@!�B!�B!��� 3���P��J��"��A~vC�i^�5L�岴��C����\��~ ��Frr��y-�
�X�-B��>�������Z����i�m�a��������[����A��RjS��F��~�Q-ob�i�m�ϥ����y}�r߾p�s�9Ŝ�bN1g���9Ŝ�b��F1��S�)�s�9Ŝb�B�\�Qc�h����oo�b^��_Ý�E�xw�_)���6���-X{���[�}���I1OO̱��I�� \8\�$/��?��+��\��(�|�1���؛ ��13�E��<j���I��W��{ 񽺻�~ǥ�bN1o�����ʹ�����F��/6���w���W]]?g�Vŝb�
�O��w���A~�CP�V;�Kn�����p��� S��'���<�r ��/4~��o2�N1ϵ�c��_����O�Ʒ�>�4{6b���]�f�ٴ����;��S{�`Įx"�t�_�Ͻ��i����[��O Ӎ?��o5��ǿ���/�xͷ�Y�������Y���W?����7�_������;�Ho�b�s��?�]��_�!���8����"i�\=ﮘ�:~���������o�����{ qM��VP�r��|���O�~�����s�)ب�`���?�>���}1G���wm�� �º�Gj������_P̳s�m��д3sq�H�&Pg��ul�2n,u�H���CG
)(�$p�tW�!*x�ޘ�!�:�7R��+��Z��qS��y��U�yS�q��� ���N�?�C�
�E >��h;<����s8��nԾO� �ᙏ�e߃������v�k�k���)����Q̛�/�� �~B�l�5Z���?R?���R�֎��9l��=�c�ރN� ������`��Dzʩ�/HG�E��W)���*ҽDe��iܴ�7��6��b#wNJ �pĸ� �~b��NN[��@�9s_�ѐ�r�2��~���I���L�s.lֵ�3s���5���0���;��F5��p~�6*���߰P������߄������UӞm�\5�yt���Ct�;~�_�f�9���w\�[E��`�r����ۆ�k�]��� �1��ټ��]1m��b���?�2b���6�UP�r���O���!�x)T|N\�[�z�� ��k�Kn*��<-K1w#�G\ hD"p�pp8�z��]�� ����������yl�1m�i;����f�\{t�09>J��!���>��1"}���ϣ��Vd^O:8� ���4������ャ��� ��E/YS��h�������hD�=b��U�u��s�>������$$�WSnc��q�x�2Q�~��C �:j��*�p�n���\w���1���}\7؈?�B?/�b�1�92J�x�� �{�� �b���F�n�W�&�𞎋#s��:>�b��1���3mδQӺ8f.b��N�L��8�=a<�7�� ���3\�X*��3G�];:+TSY�����3w�-���;������B�q���!�k���88=|/��Gq}1.N1�����C�5���}�~A}���������M§���ߑa��u�<}-�<�b�(��[������#����NN�#j�D��ѨN�_�<�"�N��ͮ��� ⎨ ���>|���������[���9�F86\7|W�ԈN�N&��Ԏ�?�����pa?�<�_?�bN1��v��tw�%l���5DgQ�w`���^ػvX]{���dL�u<��c��ֶ���N��L����Š9�.z�0V�'z�[Dиi ��d'����5�s{�8��*u,�@��Mӫ��qx����)�/����-�cw΂F%��:��w�>���L�ĝ��p�1c,v�~� �7�^KL1? �ہ��v������`?x�N�u߯i��� Ϲ��<lݟ����?~�}Ŝ�J�Y���9Ŝ��bN1��S�)�s66�9ŜbN1��S��(�bN1g����Q�)�s�9Ŝ��b^h1�J-��)-ob�i� K��(��|���a9 �<=���^����.��5;|������~��k�V�6F�M�����h'�0'ّ�ܶ��ݯü&ljC�XB!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B!�B���?L�cF��iIEND�B`�
�PNG

IHDR���v��BFIDATx��p��Y�� ��(D�wW,`@ &c�Ε|+:�p��P��3q�.Q�r �Ip@� ��oQ�'#11 @)��q�5Etԩ:U����j��C��~�}���ծ,ۻᆱ�|f�h�}W��=���9����o�u��gF�|(����ڋ͉������|�a�D�R��;Ɠ���r>{�'ߵ�r�r�O���Ws�����t�m��;֛�]���{9��I�S��� ��բ��+Kf|�w";[�:@EJV���+̞�e�k�� aY�<ē�C�3�5�-rE��Lt��Ld��}O>;�l��L��O ͙��5/8��ş�� �����%�PHXV�g�9Y3{�/f��Md/)5O �L�k@b�[.+ ��y�r����̉��������O�鸶��όN Rʣ
�s�4G�T�<�5g�K3���+�/��i�S�e<�晉�`Q�>��c�@M��7��S��ޒ)����<t��3㙫���# ��ZQx�o��3��ʨ�z]I�yZpx� ��=E��O��"���L��k��|�A�xf��OEo>�,��Ws�f�܉�?�.�t๰�(���-�Y�j��xO>��O84:�\�̗�^�PP�D�\�iGQn�G�ģrh*!{�ܗ5�L>3�{5�ޛ��;���` E������w��,s�N��JԮ{�r���uY>yZ�:n9��sK*��cz�Si��T,+7��-��԰mu˃b��W�y�c��4�މ�bo�kXBS"S%����i�)��\�8���vQx^�v�{4 ��� 8W���&H� 5�8�*��;Ų%��H�HY,�m�쬊��'3�>����'�UY:�d4��,�ݔ-ȫ�^���֦��,Fꏶ΋b򡒴k{<:rr<��+4/g��ɓ��(���,c��Df�����2�J7���MQX�� Cź��=�<+��������z�\� ԏ�|f�'ߵR����33d�4�-Xh�'�"�<���i�'�h:mR�)��Y�ڊ���ł^��u�xom <�"T}_�N ��^��IY�Y��Ka��S����˹\o�k�'���S�]h�3A9s��5�T:�f�l'�L��_\D\�I�3�-�cl��h�����"O
����(,gx���]9�$X���xR���n ڨ8�� ���B_��v%�m��oV7� ��nm�M;wLE>6�tV,��SK���^[@�h��^�o�TC� |6�@p.W����r����i@�谜y�fېX9Hp*怜�<�T,��#s}(���o]U�)N-.xz��EL�w�Pp����>Hpj���t��S��|fD�i'>��,be�(HnL�Eӧ�Ѥ�}���4Y2�HPp��[����h ����"}flX(��`Q:k³#�W�m�+e��ꄲ�s'�k�T|��� �Bd��[�U�a��JnO¬�����SŮ�E ��Ci��i?:Z�w<����#��s�~{�2��[�f�¦KM�W�9����%�,R����Ƭ��#�?{�y��F0� �<s N�d%���F>q;@��m�=�D�JW�Z7u�\<)8�o�T� � �` E��E�!dž݃��[,+��F��u���Rp���a����GR��@V�Ʊ��,����:�B��U)�:�v�w���B(>�t�]F���Yʵ�m��t [�*�A;O Y���){&����O R�Y��ڱ���eO��.!z?�T
��j*(c3���֧RQ���.OB��u�|����l(B�u.��]�&@�E�=y��|i�hwA[鯌�x�'��,����V� O&�XT�RϜ}R�C�gh_:膍 ��J��Xu��|�tQ\n��w����o���[��)+�oaʒ0�� έ< Hu&�MSD���<���x 8�I3�6��)�e-_�|0��͝f� ����3����g�G�;��-�@�e��,�t8���J}�Oi$2�;P��bI����ܔe:�n�]�I��:tzμ�
�I�K�t9��N�I3��5G�B����9�{Z����T�`�<�5g��̙��5�&x俤c�M��]p�;�X>~-��_,+V�S\w�fi�� ��пR�Df���-��4T��|�i�94g߻a��Hھ\O��ޒ�� ��u�P�zA?�B��,��.&���xY�]�f�s��=���K���2\�ri�)�m9Q9
��8k��hJ��i�ú��2�־���Ơ��ԝ.�Y�;��T��]��� �M�\j䗰��u9����5�Q�&>W9hZ(��G�Z�ݷ���̯v��ۂ7.Q]M��w�e��_²�%ゥ̒�\�� �B�n�#����@�(�<���Sy�yf��b�t��7�u�X0&��WR[I
���KB�Vɟԟ�P�@�<���.m��N�=G�C~�S
�S7J[�a��F[d��M��v�����Z �����t)eQ|l�,\��5�ϧ�2��B�͐А9O銊�r�7��*�\��3�Ec�|7����s���Vl$�:Q�H��8YL�5��R� ����`|"�:��e�&��k����Y8W���_�((-�����鬵�]�i'Nmǃ��.��OV�0fik<3�DO>;�Ϣ���Ws�|0ˉo�Ԝr�T"�DiJ^ ��l��q(4m�Y���m�&Aь�l���&�mK�3ҵ����2U���?�IPl·}^�Ms�����钿�Q�p�9�8���f���oh��'n�#8+��$�E\�(�9똉���*�d�ZJ�m�*0H�i��+!�N���
��q��b�\$dm��B����茍 �llH�f�SfY���{���#����W�ݡ��evͧ,�=(�݈ �މ�,>���
E��~��.��NY�^+�`
�]�ht��Y�L���8��m.�T5�ٔ�f��ʲ �¹-�Iz��E��ۻ�K,*ݶȵ�q�_�.Y2�lA�H;#,B�2Z��%�ܛ��+����ι�N5�Ӓx���7k�);~r��H�Y�� ����������\0� *�^2�Ld����a�O5� �$d�Zp �ӥ���q��
#G�k�h�C!���.!��d\��m��q�p�igVn��� 84J)&ky"�X�l�;W[�a� ܾ�`p�Q�{qa��Ş|v>�^���3�*�h�~��i*�F�O��"K�l�*���V
TjՅ��[�K0�Q� �&_�$,�c�\��%��H@"֋�� @�eq�impXmKm�` 8�}�Z}OV����#�Z�K�i��7�1��>4X��%�S����?Ѩ/g�sͶ�%Lf�0��M� !��ݜn�,�lZ�UJsq��u��8�bbS~��qQiɶ���v�� i\�.[ۚ{�j���
 N�M-X�7ظq���<�Р��ޢ���*�ݚV�yO>���\��gz�/fz�]+��uD%�!JK2c"󸭤��~�}ð��U�\6,͑��Id��ivbH��ji�Ľ'��T�������$�-8mѰc�۱���mN[4�q�hj�м[t� �E���j����k��`B�|��>���ɛ�cvҎ��X���.��KKȾ{��f<�G�cSVNm�+������o���{�V������o�0m��M���4� �2�-3����fO�rʢ�&<Uw�A�%n��.Y��*"Xy5-�k�o��n�������2��Mn+����� '5�]�����ޔ:k
MhR26�;�a�nd���v
�N��/%n�\����w �M*��jmM���6�kԲj��pi*f�P���K 8W�|�nC�FG����4j�]���*���\�ۖ0��Z�ip���qkSll�5:���aG��A�?{,�d7�#��ڪ���F�=Y'� @��3�ݲ�.(�{i �k���&�9�6��[�\,4�Y��͸ߞ|�B����$m��V_m�BB�� 񗱳��+�k���$��]pwk<gcCh׎ǵF|1�h����{/8� ����'�=���h�;�όHXڙ�{g�� ��E𮭚
��t&6��d��Άlo��ߣ
"fI�bӶEۢ�7dBr�J蛪@�u�l�7� *�+n�4��j���t*9a �G66����־f��u4hl8?����qw[v���f�{��ހ+�K=t ;��/��С(���� ��˙~�B��;��y����ް��f�܊ J;�r����-40Ɋ)A�N�z���h[��*Xc:�`��@�K5Z x�s����@lޱ��a���ǽ������Ьȍ�b��ւ�����$l?3��vf<suϵ庋"s� N���k��Pl���F�u>y����}�<�Z�j!��XJp�-��X\ԙ�����*���d�.��t������@�נ-�`l���ɴ���Xѡ��70=�s�3�A� �6��/���U^��з9B�|
m���5�T��j�oZ~��#�,g�c���f�'�2dbR+��{mVw[�n懹b�Q� N�@��#��N�y*Ѥv���_�i��5脮 ?�?�a�|����:'�A3�=����Ǻ+���`�N��z �Ws�Ea�Z7-��#��C��.�0�0 Fذ}�K“��0��&�]���K�ӪBE�Y.�٠����nm�/�Ba9�/�sZ]X�Hp������� �ZVJ��Yw�3�R���5[�츄O�Q��5\���_��ib��p24)�kw쵂~���j�����Nd�X PA�t�g����z���5 Q��gƉ��0a�����Q�1��D*��k�Cj�r�|��"g���N������� T�R���D}"m�����Y����@
�T�f}?Lovܕ�n���񴐹��ذ��0�t��V6����{��캦�7o�fm|�Q�2%�\�cE!�,k�N
gɚ ��A��*m��r��Wι�����zp���L�l��l�c ��$���
7Y8/Z���g��-�$�o���ȏ�]����k��7��0j �xf����<�7-pr ����P-`�amm�Ƈ������Z�;��{�Y6�5�-t�V`P�Dv6:��'@���`��M��Ԩ'l��H�.;���t�$ ����}�9j�׌+g��s��j� SU���S���³��o�����z����ZUN�&�j���llXu %�ע�B~����)�y��G��U �Tߓ{ҊK�<�f�2���X��̪|2{&��줠�T#x�mrЪ)�񂳿�؜�"Dk%,g��� Kk�e>Xg��&���V���?���BG+ζ���j�S�*��|�چY�Z���л�Wձ�j^0���t3@U4��<~K��e̮wW��n�:�qۑ�WQy�ƅm�fpʮˏ�A��v�+xξ�Q��y�y�B��NQ��W�U���u���4uM��J�.JU��ei�d���՘��r X��鏑[��E'��O���m�#���s�|���ߊ�Y����V��I~! M�k��w|�om�h�`̶6H؞�E� �'Bk� ����oP��1!�(���Ғ[Ɩ� сŲ�74!9�[6G����h�}O�Q�EQ�s �Y���%o�%Sg��xJY8{�/f����z�r=h�j��b�S"��V'�H�Lpv�d�O_���*Y`[��n��䣠�4��٪7���>�,��f��>��-l�n���zЎd�x`%���%Զ��%���E�7����{>}��o�w�c}/��$.�4FQR����|�^�ut��f�Z�{�;�u���q�9�=G��6j�҄];f�y[Im�%�tR_,�Z�瞓e|D�l�NFh m��k��6�k@�uZA@�p&z�Y�6���� 7c�D���tV/Ȳ�@3km�[55nt���M.���g�����n�m)-�:U MƢ�k��؜re�f���D�� ��S��eo匄h�T�m�k�,��єк���۪֠5�߳k�lu�%gѼ��9����U�7ʰ��h3XD��HHd� �� �i%���F��a[�ct��q�?gl�o&/\�;��
NJׂ�̩SФ\q{S��I�]se�g�-�*�<
�y5��™��`�sW�uZ ��a�o���!���L����6�xA�Km"?� -Rݱ���ӯx}D�Kȗg�t2˜�� �O�m�sǂ9$4Wȧ�
Ta0�I�?e�� n�ƒY�����ݎ Ν
cł?9�` h�� #��Q��d��u�H���u���3�(��Y+Mp��GN��ֳ�V�8�4���j����Z-5�(���KT T�D���"��-v��՘x��G.�u��P�_0��^D���=���c-��:O �χ�e��wq ƆY�p�Y���aZ8VHlhMB��7_����]����I���gHdz��;�]�$8K>����*�ښ VN}1��#��Nɲ��n��0�����D�BV��&�U�s1�y���s����M�&8�b��N�Xѳ�T���鞨�8��O6pl8Ppj\@hB������c�^�郢S�3�e��������@J��lm�b 9.8%T�evץ(Ц���>�-�,L����Ԥu RV-��[�
λ����^�ݿ��8'���r����<�񽡇<Ip�R e����к =Ap���'D�􆓞XL�������u 8[�\��)@��+�W���5I��6�U��<�M� ?V�IP�
�D����S����B�`yX�����pHhbф&�{ �n�{���{O#8�J�$8�[�a��2k% ����m�ޱ��wc>~����Ζ"���>E�,��&
������U?�\[�ɸ���T?h��?
O�Ģ���I,Hm���sU�L��{� ��\������^l�^����.���c.�TF^p*2�F��hB5���[�r I.�/dV�UM,~r�Ie�O"2L(��s�/8+�{��ڮ.�˼�P��;�r�%h��]/���g���ے�~�ҰY;���-^|ƃ�X4���bo�B�Ә_��xv��Jw��N�N�xf(��T��R)+��W;�L"C.�4
U&�s� ;讉�5;af�E���VS�����Ø�T���F}I�=XZ���3��ԠBx�=���P� ,%��P�֘���1%�?����q����{���$0����'7B�y�r愢�}�Mm��[��&��'X-��H$N:�����m;v�[,4�(Ix�Z4��P�֘��_����b@i�lQ�gV�;��Q����r�-
�[qe��J�^��v.�6%����hA:m����^b!M���G�6��Ebs"�؛�6�pf��b�D�Rt��xר|6�F�h�P���xM���L\ �0��i2��|ݾ��^+Ya�QsZ��$0��k Dk�r�u4`�pV� -��lp�BKxr�yj��X�^�vіݽ`һ� �i.4��7��a����shR����7L\.7�����̅���ӑ��Fsr��\��O3{�&�he��Z��6����gö����u��@,�es�&��?�c6�wm� �H�4�l#��Ș.��0��Y67���.]0��w兜&������a�g3��--p�ذv;�5�:0`�M��םA�k�;T�3ʕ��Feљ}胃d��Q�T!�9��5W�?����@M�ZtǮ]�eSVN������~W�ռ������F cĭJ��(+��H����I��ȕ�ڕ��C j[jG��k�,�r���S�r�B�n�����BC��{�]+:f��OɢY��{:Aȟ$�M����[.�h��@���l��&���y�P���u�:����\�3�I�554�|��*����� Od�ƅ��z�}�}�[m��.��jg�t�mM�jp��Q�� ;�K�}��f";(A�
��SB���"k��^���f�Ģ�T�&3}�����F�ʲy��;֪��`�ܶ�j��7�A ay� Nx���Ωo&�lbSr����5kc�E�c��l ���vs�d h6d(�_���}OBs�Q_�g"{N�����(�{�Uqd��d��k%M������q�җ\��">���&$���)��x>r���4��)�y�.�Z�"4�84�yۥ$��t�XS�]S�7D�C����� X9o}��G��O��,���IIQ�M�"6/V�PV���e�BWZ�GS���w��(�.֦JX��1Y�[�d�f`������|Z��E�ߐEMhr��aq)s���+����|���˙��|fJ~��8�J(�$t�wf�X�Ѥu�5g�C<xN��}��M��A��{a�#q.� =���H���GB[囶r4�y������œ�݄��9Oj��Y6�yޕ{ � ��}��� �����G͎wH~�H��l�\)]��mG�'��'+��,�/@r}O�R}�
E4��T��8���\���&�� )���d�'[�����*��f��JI��OJ��J��Pʿs����y�r�у)�\h�r*��M��Y���7��݊���0��R~��M�'+|��҄�7�`J��}\Eg�S�OU�䑻Zq��K����?t!u�oz�����|�K���� ��RE�g{��I��B6�u��à�(wc�]�h�:L{���k��x��ۿ���kg���o��{�����Q��f(�o~c�yO��91��4D�qޣ������W��w��T)��d�Ac���/d�
+�s;u���o�*6s]/
���-U��&6���X������T��a����z�����/�kg?��?�D�I�R����KUŦ��4���O�w���_��!�J���!6��M�&b�I� 6��M�&b�IAl"6)�M�&b���Dl"6��M
�IAl"6��M�&b�BAl"6�!6���qq�0>��^'!6��
|���t0�X�g����(���� ����
�j�������T��o}ۙ�o��_�{݌bS�A?���-��9�L�{�W*j��:j���R��u�m��v����|��6�q�Y�����h�W �_�eQ��������p��Eb ���9���*|����{�lb��|O���W��~���v_ת����HHl�˜�a=�، !M��I����X�N����DҳN^$�N;��W�͵?�����?z�f��6r���u3�M��A�Eڳ.@�{�����7?Sx�ן�&_=�ô��&6ծ�x�Fl>Q����{־��w�^}�?�m��;���oF��������rs��f�/���>r���~1����Z�=� ���`N}T,���CS���������G>�����_+|�3�iϽ��?��O����LHi���# �,P�}Y��t�z�7>�����q+����mm,U��&i�-���~��/E����=�/�P��A�?1�o�����; ���Q=}��^���_���*����p��/Gu�;�bS�=^G*�����/G�TG�Y��|t/lq���,�����9�-�% �jmB?���m"�bS��cw�� �^�7���oݽ��LPl����J��?�k��B��u���^ؗe� �°/�c��é�~cd� �B�����g��Ģ�ߧ����~�Ql��>]����E���{����
��7 ?w�g �͏�w�����������������_f^�V�ُ�T���~,�7��\�������^\l�����G��OBl�e�~�đ��K%��y��ȥ—}�W�N;'��-Ѡ��]��7�>���T�MMާ��-����?����4��;^�>��ֺ�'y�Pm����PT:����Pl�*��Ϗߜ�z��Pm�����ϼow�V[��w>�s?%5�ɦ�w���Q����zR}j���ڣ�����wG�����7����GQl�^4��NTǚ uOV��o�E��6��o����}-"���c?\x�����O��MJd�3�X�v�yYr�4�Wj��,8L�H��Ԅ�J�?������磶�1C�4ѩ ʥF��޸;& 6�+6�%����^��}�=��I�� ?-�զ}�k] Ŧ>���]T�/�^�Lc���������c�~��������o�-6e�Q�.��=#�ko�ߐ{���{%d���b��B�g���� _^lk?��� �p�+��+ ��?������%Ҕ0�K��� ΃�f�Ww��]�������I ����y�.����0�}ߵ�]~��#���\y�{�WE�#���� �����y��˩�l���b�h�-&B{�KV*3O#6%N$R�{���`%H�h��-5�����4b���ewłH�IPJjp��T�UI�5(k��i����b� M/*T4�����������i���9g�d�֡Ŧd臣�^V̹��E��P,h��$0�Ql.e��bS"ҿ��E���0ja�mh[�[#5Y����!z�Ŧ&J�!��i(��7�&lϳ_c��c�2,�+6g^�D�NԎ�%S�޿�C?�9�YYX�of �fՂ�,-��6��?�M�)��V躡��/�%�$
�~��]Q�g+�e(4�BJ3�R��N�M���XH�����Vl��߶'6B�C�L���|�n�T�}�7����ͪ�YĦNՑ�ѿ��ɷ�w�<~��vE�'�������'����'�X(���Z� oxî%T���D���ݔ}���*�G�Z�v@��4�T�'��-�K�?���ZQ���<�� 'z/���7e��_�Ӡ�Gq5�M �`C�*a��~^���S ��r�� ?ڶ���8�E�_��р�Eǎ�Rl�+���������N�īIP� |����W%�2ݗ%H%(�k��}����R*6��ÈMY3�8=(H���:j���Dl�^l��±Ic�ΡeӋ�P$V���X
Y/��?�Y����D��,�W�Fl�2 T��������:zV����3���
e�YŦ~��x�[�����f\lj����߻����?������bSBT��j>�����B�9�����X6D4Y,���l�s����VG��mo�Ħ��Ԉ�b� ��B�,���5�j�`*K��$B� ~~`����? 6��
-�u�c����ӊ��� L�NQ�IH T��(6�m��4�M?a�z� 5)k�T����J�;�Z)� �S8�j�l�y��a�8�ؔżZ��=Yص��IOcF �&��O,y�-_�럩qQ.E�Z�Hz1����+�M�I$� {�wO ��ج�Ϧ>+��Js�\���q^}L�.5�5�ٔ�P�'���g�v�����0��o�>q(�~m�{���h+^����������_Mlʢ������� �l�>X�|6�5�Ր/�bmI\J�xӼ~J|�Pl>��f�e귵5�Jp��$0%��"׶��P$���w�UL�5�~��[Hk$6��g�o����Q��N4����5���ٔ; ����_�a|a�ݲ���r�P;
E��>������D�ڠ|�R,6��gS��,��5E���ƌ�
��LFl�.E56iѭ1O�� 9�O?��=�"ז�����۲\LBK����~�ج����+7��h�W���~�9!�b�o�K���򵜘�`��o޽����H�Fl����= U���υ��fz��1YN���?�{_�N ��gS�Q������;͔�Ħ�]���U�VH�V��#���W5N�M4޿3 BZQk@�B~�S��V���_�ků-w �޹^B!.%Be�� .��H��ɪ�\��`l����q�7՟�Q�)�qգ�P�� H�GQl��UG>�;^�;M�z�}0e��"D������D�>͌�z���=�o�GUl�3�c�=�/�mu�i��X�1C��b3�!-���e��c�_4k�$+�Ha;�91u_�^{��e��_ K-���/�.�����1��.�h�����e4��=J��/��>&�F�)��-j�@�DY3%�ER���yꥷF�E��y�����{w��{i�\BQ�V�%f�����ri��=�Z�2�����TADu��5$��������6�۰�B����rWd��l�: ��ATE"Bg��'W]���Qۦ�o�T��H� �z������0P�G3����I*���@���1 �R}��~���u}N��b�� _7��T��TX��x
�]�ĩ�R �#�6��{+Qh��\u뭗��MHp����M��N3�>��D���M~~�Cn~����&5 T�+��Fl>QV�W�R;��k��u�S�W}>�5�{�M��0S�ڹ��Ԃ[c���Q{V?Qѿ�k�Ώ���c����4v�_h��j����s�}����u#S)PGM �pK]/����S���?�V�|@e�� SI(JH*�\�SBR����#����z�����K$��&�Hlf\��q�W�q����PH��q�����Dl"6)�M�&���Dl"6��M�&b�I� 6��M�&b�IAlR(�M�&b���DlR��M
b���lv��<[��Vb����J����V*�?W�J̛�:<Hl*i��aڋ��<Il*{��Y�0�*6�_J3����OljNL�8/ �$���> *J��IGYlfj|g����;w6�w�g �\h��kw��a�˽
��J|��
��1�Ii����$߹��˧*�2�|�e#�H��<���\�2{�]���4��w>����,�D�~��z���h�s�'����X� 1�)��CU4� ���EB��@�r�
4'm<h��C ͹b�ƣhJ��x��MZ��W{�U�� ��W:���G�tt��IC�<
h��\�r��՘qXŚYhnZ$IB+4u���n�p��¡���bY+�E��$�w�ٱ�C?�q&m�8i�sŲe?!���AnW]9�K ��<HHh.���M�-|0�]��'�fw����x�Bhލ]��@=��vA�V� U�r% �r�� �'xD�FV%m�Į�V���+�]�p]c
�`�
��s�d-�� �Vф>��F�O�8i����Çj�9��[潶� �O�h!�Mm�-�����`�=Y�:ro�m����u�M�}���O}���5f���6v\ �ݰE��yT Kذ+Y2Ő�k�8f
9�G�՗�,������a����Z��J�+r��$,eM�-�xT����V�,��������I=�4ݷ���`���D'@���v6gc�9�.�� x4)�-�N��6y��xR��K���GS�MY����݄z�q�W�m��{�xŲ ���_~V9{/˘�`���v�IU*����M�d�}6%8�[�P{�M`�%�ae��#m6��xT0��[�r�׶�1,:)D�M6M� Z҅���Ӫ M44i����ns�
Y6����ڜ�Ά�����Z�k&8%436Q(´ͮ͸��qc�L&,���b�照�X���t��`��W���5�9,��6)�N�$��;���b�����j4yl#XR�Y�ɾ�� )��V��y�nxY@g��3���ZM�&�^�w� ��ȐM�q�qʄ沉Oε�Z�����5-r&���� ��H�\:@���OH�ī���Bs�� �A���U�"4�#*�ȱޕ���M���o��gΕ����Pgl�������Д��ix�u�& �e��d�D�L���m�՗��Mh�Z]��[ ���
j�7��+_�6G
&BW��� �HhjQ��M(��@<��s��d"4�?�z����-��^��"Iã,*|�f:�Kă�~d]�}��Iꀬ�+��v#��k>������q���ֵM�VA�@z�p{�����돭n�Pk��Ѣg8x/K綵�eG�M�0a��d1l�J�+m�3i��A��smj[=c�e���c�:��┽�fmџ��1d���IB��6y��ݗX��1�-|�TY�Vb�����#hj��ٲ���O����tފY�m�N�F˺ �륣�,�#�{'MlJ�짶��q�G5j{Z��W��f�W�ǩ`D���s!�$'l"-c�&Vd���c�ħ,i�<�Ԡ�3c1!�h�gu�mNE�����)Ԁ![�LآF�R�/��!��&�[��hKV���L�Hh��&V6]�XK�*�(ǣk8�~�l�o��G���Baޕ���Z]��σ�ϲ�gML���bTBs���134v�ЂH\�"��������IB��E3�@d��J�xd���s��=��X� &
&L(��.��-`���T{:i�}N�!{=��/4v�9Ҭ�<����HX��&�R�uD��� ���j�i�D�� ���k�����{�tb�hq�)�e�>G�D[��P<tX4ӌ?��dp͟3���������hp���ڸB�DKm� ����Kv�z�E�#�۾�����9{O�A���Z�n�� m����-�r� 4�&4�{I�wHn�(� f� 4u�G�k{��&5h�R��Y;�_�,ڲ��ϋڐ�m�hOݮlm�Y�;ɣh=�u�#��b��T��b%U��'|F ms��DN�%}O N� �if�P��#����6���}~��@�\u{��C�8,&�����Si��z��ʧ ɚ�c�s�ݑ~hkV�X�L\7�9g�,d�>�i��ךM�B���L�<"�J�u��٘+�v�صI��@�s�&� +�J�k�m"a�H/'���<�9[$����N���-x����;�`"0tʐ�b��>Kv=�J'����M�����]�z4�������x�t�SV�BP��+45\3�9��ǫJ���9e�ʜ-|V[�-I(<$Zd��1t������A.�yt �+�aBR��(�3�T�\)o��-��Π�S�����+'l<鷅P��5��}$}&\4��Z��@F�>����3�:�s�i*��4�j��dm��3�|~�q�@�����fj;�ґt�Z��#K��8-.�Ƞ��w��7�B<J�#'���u�\��G�Z�!7��X���jӑ
��7�Uh{*>ph��B[�wc�d�T�� Oj��|de�ʒ��^u�}�j�\4�6gmo!h��k�.#6Qx�Mhtd�M�[V�I��l2N��vز N�gj��:HS N���!?/�� �'<?> �%8e�41r��t:iC~�����\�1���w�C�璍#ύ��Jܼa��*�$tR<�ϫ�N�����!>��gmQ;�8y�0��_k�a�lf4������kg��bs��C<"��|��MOα��Ei��.a��.��&$4 ��������oS��ÔHwm��c�}`�q���Y��O����l#�*c�
Mm�wƮ+W��d�b<p��C�h�^��^�A���>L>�>g���j��Y�����[q@ 3SEhΙ��kMpB� S�T�ZzK�m���M�����;�>ٻƑ���Tc�uG�@ˢ�:�>�Ů�5��]/8��Mp�}lO��?��-,jA�-F�����Yk��K�6��:� �u�[�.�$��&L!]�S[�A]�W���H��?�^����[�8�x\ ���/`H� �K<�T�YG�P���[�'���2�B���
���n�6;0aDb3��̂����l�������SP,���m�? ��Mr�#��$�>�;�&�D~�s��a�m ��/,�����_���#ã�s&8}��k&B��0�ؖM�ba����\٢9d�s���� L�ۮle?a�U��W�N Z�v)>R��a��&�L҉"�{��C�H�`�O���t�5;�B��8w�E���7T;�r�&��(���1���-W�NLJ��s��w�t�@h=��� �ӻg��c��9�Ǐ� ��*!k��ڱ2A=��[�:��]W��cK���Z �� ���v�uU*9{���:��䏕 jM.&$�;�@p�#�+�����EW��B�Q�����<�؄�B3�Ρ�8������V0J�6z��G�vWX<L�x��Ⱥ�0&4uM[�K�mt�'e�bb�;x/+����U�d�G[*2]V�m�G�Zp���l�8ς+�ce��ј���B۲���i�G �g�_���� �"�^̹�~��߱��#W��1k�R�k��ϩ�<�T�k�� ]97���-���a��u��ڞ��,����+����!�]4Yl��!��-�P���̀�/����[�&|�zrޕ�ҝ����M��x�#˘r6�m/MWx,M�R!yk�R���D�$%6�u��#�y,P %W�MY����C ��2-�Z���-�` �z�f�S���\ɏ��:i(�D[��
��4ڲԶ��DH�[�j�XtX��Y����ou,��IEND�B`�
�PNG

IHDR�9J�5�IDATx�����Y��W���1Ҥ��%C�X�`��F����[��� ��6�k���zG����4���Ь�Iƌ2 Np@��Ԗɰ�Lp���Z�5t��?��;�s�>����u�pϹ���]���{��}�~�g����~4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4MӴ��g&�r-���p�t����-j��p����H���t��o1��鶶�n�A�u-�[Y#ݞ�놺9С�y��u����C��[��a�Į�w�����Ǐ��ٳ/�z�^�jiP��UKCꥡ,����k^��n�޽���nǎg�[�n�k��۶m��j��t��^:��J:�[�t\�N�놺�(]�u�V�f��w��u�j4>����D��zר��&6�n�������W|�ߨ�3g�p��Ӊ'j�A�Ҡzij�� ��P�Vҝ<y�j�ӧO���W^��^j)ݩS�����V�Q�z�(O+�n��q�:���f�t���Z�P77u
Z��ַ�:Q���5�?����=��3���V� ���Iu
Z��,Y���?�5V5�Ih�cH^����Z���}�S���i��@�B+5C���K�.�F�*����R��u뭷F��Ih)���ZBK -���RBKh)%����!���Ca���+����R-�`��i)%��d���߿�փ�J�hh}�k_����j�C�U��T�x��BF��>�,K)Uz�<Q��s�M~��U�B�[
�ZF�*}@�B}������v�Z���`����'�N��kCl�U��5kV�bŊb�ƍH͞=�X�|yq�-�X�U�DGpÆ �L��+�Zqϣ�Vm9�+��Һ ��V+N��0���cǬ̪�W����x�y�>����Xe������*a���3������B`l߾}{�}��̡EO:�H��ց*yrlBp�3ڡ܏>��9��'4� 9ւ֎;���2=� �wttt2�A�Ѱ����;ؽ{�Z��X�6�/��,�I�����%���{� ����_�{���e˖J��!���.��#G���N�/^\�\��������] wE%|��'+�5k�T��lٲ��.�̛��8 s�Ν���!=����P)�ϟ_ɋ<GFF������\T|ʰz��ʾ8�)�뮫��w��sɡ�sP����|����B>�☤���;>�t�M��b��-|��7�|%ڄz�A�o�����6�[�)��$��o�ͣG���{|4ɏ6�r��}�#�9�}�Њ��|�(�v�|�2��IIKe���P`�qg*R@��JGA��w��(���x6=�<|H%�>�3�"}��?���#�{u�KGq�c�#6�O8C��ٙ��ǡ�u޼ygA�2�����şI[ZK�,��a�[@*�� �1]���y޴�Q (�)v�՛�Z�˽������;Bk4�0��!Bi��J��D�8��t4�����/�9��B�(s�����& ;2�#O����T���Ȉ�iف�=�����O�
-5����sZ���T&b�oѹL�چ���f�#��VC�7��۹��[-�dE ��j5�l��Mj�EE��\T�\�c�o�$Oʓ��#�q�B���"�Xo&���~�l��t~���`8��RӥnY����ޭZЊ���ڵk�k7���f�.tX��b$��>��jP�������r�V:��c���R�(O��rp�X�RP׎2Qq� ���p�X:�΢||f[�����Rh)W�xӧ�� �)�A�j�(���Ӛ
��3&r���;!�����P�^bbEĦ �������F5iX�m��j��(=����
�g��Vn*.e
hQi�)7ocF^�Ĝ'NJ(y�(!��Gh�~�Vܓ�p�O��ջ��o�ùoMu��9f�!�ZB�rψ�����p6z84�TD�P$*��Ƀ���=�ZTp��196��� zT��0 ��,�1K1�ʌFΥڔw aB@�9p>q���֔^��� Zt\�'�|�ϴ �����oq.��w÷A ���Ƀ{���π0�c'b�ph1��\�KGFTFf�<��|�D%a;4݄��3��7�� �!=�@�&6,���$?��Mo�r�)?&���|a
����r�����QT�@+|h�_L����G�k�o�7���݈%���b�UDah#�\�WU�E��`���Y��UޕR=-��򮔚Nh�e'%���RJh)����-qogttt�� -%��R3r�A%���RJh��WW��ZBKM�sZBK)��w�3�Zt���o7��=�Z��ѕR�+����-&����j���+b���J���xg]�bՇ���'Ү�h�D����VO@+
q�;�U|v��R������s�^�ѭ+b�n�O�;�0�{��ރ֊��L���}M)�a��/����ȓ��8�4�h��RJ -%���RBKh�i������wg�+�ᡡ�7���BK)�%�ԅB�+V�ZJ �f.ZBKh)%���ZBK����^�]h -����ꙵ���ZJ -��zZ�N�b9�&�ZJ -����9-����rʻ�h5�ʻК�v~�+���[;�?y����������Y\���O]�����8U<��U���|�������������X�6��|1����^�Z�;-�����1�P��;�[�;Yyp�V��� ?w���'�}�8�Og��uI�����βRo�����:�h;v����ۋ9s朙H������G*���jS��w�x?O}���łs'��%��
Z������܉�g����DY�Vg�����Nj+VT�_�zu_����8��:u9���Q
-��h=��/U����|^ϐ�i��﫼��W~m�9�C�z��z�����������y��;�C�f$E������=�L���[���s��!C�G~�O��ȍcs���ɫ+�F��9h�R���)'F�黎�Z��^s��L]&��~�>�i����?�x����L�=��}A��?�a�<<Hȝ�>��>R���ϞSĈ� �;-��x����샏Eل�����]��梅 ��o�=���W����c��A�S!"|H�S�>�e=��� �<�Y�>z���/��� _.���7*��τɛ��ZLx�?�$?�K����'/D�\���V���X7n�X̝;�� ��Z_�������g�_�^���K�>F��Q|2��w�p(�{�v�?>˻�FH0�����]IK(��qM(yQҰ�#���?e��I�~��V|Yh ��A��=9Q٩��3�R~d��礡bFEd�FzF1��D�Т�<�q�H�7��#�/9'����s.z�8oz����H���`:y0� �кph�w�}H,^�����^z����e��NUfS�TM��6-�O}?ad��t�謥�p�����t�L>��O`�`Fd�a ߠ��)[��ۡ��Uޝ��Ahу�v�8�TX��w�#�����yjU`#�tT�w� �}�S�#9z��n�Uz����FKh]��-[v^�� ���E���Jx��{ZQϣӖ�D���-� `�( P� ���+�rE��V~�8�<#zd��pEE�V��A�t��r�=�ܡ�>Uh� �E�Q!�H�'Z� 2�bll�X�|yCX\}���?�^�n�ܹ�1ڃZ�����O��^��B+&:����D�A��#@Tt���HMh ��B��B���b$����4��?6��G�%MzO��I��q���yOZ��8F~.���H�2Q
��L�`A��۷CCC�qO�kW�ȡ���B�]�Њ�G���}=hE��{Y��n-�.�*/
-��Qhq�b69fޅ��9b�H��`7f�/�����p*��PbT� ��fv�O����L��8�d[���T��ř�_21c�DG%��{��b�޽wO�?���E؜\�oe;�f�����*�Fb��i=�Y���(#'"#�@�(J�'tX9��/-��VG�Eo)�SQ��JL%$4��RxN�}��T�!�?���"L�sG`�~���b6_3��sڋ���ɏ2-΅<9ǘ ��Ǥ�Q����裏k֬��0�ǵsh1���I��>���f�/�z��p�+����}�����t&#��Z�& >Ͻq�`�SL�ZB��bdEō��sK!|��tĕ>'��LC����4�����6�r�t�����|�E�4�<�8'� p��fTƶHù������� �SQ��=L>~�9_���wq:� �}7m!m:C1�Z��78.���o9N��[k'�N�<Y<���^]#��Z� �y����ɫ'\0�s]�])傹Jh�p�RBKh)����:hmٲ�/WyWBKh)Ճк��[�r�w%���RBKh)���ZJh -����R�-���^�����+%��Z[�͛w��{���rE �T�C�kW�PBKh)%���ZJ)�TB�@���R��b��^����;�,�j}�@�رc=��o�z]t{ Z###�n3X���'Vy�i��^���S�]v�d�zK�jX����;w�|�^��
��v3WS��u�9��CCCo�8q�U�P=�ŋ��V3CӶ`�R]�p�ZJh -%���Rm�t�����RBK -%����RBKh)����ZJhuZ��?��3g�x���ZBK�EL�߼y�˝��SzNK -���ڴdɒ�[\�C���wE %���jfE ���ZJh)�%�����b����ZJh)�%���RBK9{Ph -%�ڴʻR�򮄖�h�rE %�T���K/{��{�7�#���RBK�"��RBK -%���ZBKh -����ZJh)uq��r�޽��>}� -���Z�純�R3
Z7]{�/�޽�k�\C�G���P�+�X�ݶmۼVB�1��RӯcǎE�USO?��j�j˖-Ŝ9s�L��넖Z�'�r�ʚ���"��RBKu�v��QZ����RBKu�R>�fͪ
��{�k$���꘎=Z�p� �NT�{��jZ###�kٲe^�u!6�e˖�:�5V��֞k�����'Oz�g����Ckll�k#�|NK�"��>������'���w;(�v�w%�TO��n����ի�&>�%���Rݫ}��MBk׮]^�%���Rݭ%K��g�.\cRh -%�T׋f�ƍ^ �%�T�C��U޹� �*�.��H�3/�-^����Z�|y�:Ȅ�x�ʼn��L�sZ 9�RQ?�p7A��*�V��w���1ZW-���8���Vݰ���pE���=��g�~�?"��R����(�"w$4#����RJ�Zӽ���ZBK)�%�.����������v����ZJ)�5?�7���=��^|��ߚ1��Uޅ�RBk®�����;��9�jt�^�V'��_�n�k<�%��Z�xN����'��ŭ[�/n�}{q�~�x��+�d����}zG��''�������{���je���=Z�x��w﮿.~�{� �+>�����G�(���k�;��j׊BK�ހ����ѱ�� �Vmm��b���.�������n.��}[_���= ���/����\z�;&a���y�u����k>R���?T|��ͪ#����7U��~l��� V �H�q�&���R>�%��Ѯ��\���u�~Ψ��F~u�9��{>�^rU>9�~��+@��b�Ez�ŵ��0j��{�K 
-���Ъ/�t�1��d� I� b��C��#6�9���/T����7��ZBK)�%�j�{G���L���� ��0H�T���rh�is"l4�Ph ��Sz��g�Ih ���L���i8��H+B��H+���h�����f}e�v�U�=o�d?�ռ�۱cǙW^yEh�7g�aԩ0E8A+��׋��X�!D�Mb��~+�2� �`�S���1.�2���r\wnVs���k?�)�%�.X��c�H�����_��Ih�9Fc�uO ��V:cp�'��2�81������k�3jz3 Z�N�*v����ľ�>�5M?T&h��m��#���"8 �C��5�̂"��Iq�v�B����q����V�=����g�U��Acccō7��Ih͘�x��z�~��Ѣ�G�/��S��-@`؆��Č��4�-��?}�?Z�=����b;3 �م|��箈!��
�j H��|�8yhZ8R�;���
� ��Z��s�w�y����"���$T�� ����{��G�5k������W�^�*�M��<�w��X@�\�<�E珑?�y�*}N��'� �+[P_ AV���v�ɱS?�������%����wE�C���Fω����UE�Le�Gȋ�T�jТR�O<d��Lj���Ӌ�-��Z�Q (8e:���%a�ȋsN���rM8W�sm�U;y3�7fjѻ�<"o�/���=>B���ۨgϞ}�5ڴi���_Q�{!�'��Z�$���GFcC{�M�E�x�?=��m��G�o�Z9��qql�c4ꀂ^��K�x����\�ǖ�)ii@'�a!v��� ��!�&l�8'�'�#��Z9��r����PN�*�)F��dH�����~� o߾ �{�s��)�8��S����к�*^�{.1[�C}Ĩ%F&�7wC, Ã��X1!�my�>�k%<�q҉�9 ��gL���4D���M���i�)V�
��ґ*��� � ����+V�蹷��ThQǩ�t��mF���
Ȥ�16f�Qqi��.F-�&���F5�&b�#,�?Q���&n�?p8�tv� z���(�^e4DQ�Z#ҐbԔ��ؔ?O�y=�%�T�C�СC����ى}w
�i�r��s*]F��h� i��ʡ�C�t4挢"��l>f�i��k�D FW��r�r6�)# �i*�V�>�viܟcW��#�*����;V .^�x:Ã}�ʻr������šO˦Q!T�u!���"���"#�j�E�K�ivR�^�o9țI�j'�bY�#��Og��v�{1rJ���L��쟆�bjqz�b�SZ�'b�۷�2c�ܹZ3f�\���{RiT��;�H�_+��B��H3��6Bi�$h� �5��w���# ��c��C��� 3F�0�z|&oFc�!@�Z�}���B�X2rd����Z��4�I9b� ׄ�gt���e���ؗ��w�ys\F�q@hMm���?^���LNy���>Wlذᑉ��Z��q����vn�Eh�]��� �Z�0YL1"�C2��F������ά�tͲ�ń�j+b>#-�8�:P!Fh餏|� zw�g�C��|Y�"e!���-�x-[�8���� PQ.� y��U<��g&�p~���j� y��,�V.����3�}Z���^�Ih -�\Ʃ��E���;:B�� ���!LM'��e��H4��HC��D88���EăNit#�<<H���ޱG��e��K�ߝ���ǒm��N�ZBK)�ՃТAg�N� -�1���*�@�D�&)!���?�������ՠ���.� #y�9�FY"?�f{�A�r�8?"�Od��BKh)%�zZ��_���;!hFI1�(}�0�V<�Nz4�@}@+��+�G�<<�B��w>kH�-�Bi�<}4_%������۶m;ˊ�BK)�ի��~q̺M�Y��t��Z@��L��4z�/��B/��5kA+'a�����FY�D��8�j�I�Vh�ʻRB�[�5�e˖�xx���U�M *����A�5��o.���sʿ���/^?���RhU;?Œ�
-W�PJhu ��r�AF)�tw�A����T{���x�A��"����#�� �Z����BKh)���z����K��H��_��� &Z�c���A�D�z�bBD��>��wlkZL�H�^����{B|�X �@�Q����A� %<K3y���R��U��VyH�s���������VL�`20!�����@�)�|����2�k��$�F����3�ZBK)��ï&aT(�0�N�yyF�@u��X�&�M>�ݗX�/����K_�K/�7-eɗ|bԘ��x5Q'��ZJ)���b�� ,x}b�=BK)�5�к��k�}a���BKh�R���ڃB�1�R��-q�w%��[��ET����f�zZ����S�R�����G����m��o7��|��ހ��]Eg�k��nh)%���RBK ���TBK -�����ӧ�#G�<?QWN�sZJh)���*�JuŊJh)��̊J -%�T�b��믿��N��Oh)����ꄦm�A���ZJh)�%���ZBK�[]�ʻZJh1�cɒ%/�����9� �u;v�8��+�x���Z>��\C -5�f
-%���R������RBKh -%���RB�k���ZJh)�U��׭[�/x�B -%�����b%��L���;w�|�6C -%�T/@�1��RBK��sZBK -%�TϬ�!���RBK -%��V���ѣ�)���.����;_�.�����)�Bk���
SS۷o�Z -WyW=-Wy�����5kV]h;v�k%�|NK�"���]��&�V�X�5��#�K���W -%�TO��'�� ���q���i -%�T��̙3��������`�]BKh)���Nw�q�y���-�����>=���A�����-��zZ{����O�<酞����+'�5w������^�%����Խ��; ��7zM�օڼ����<u��Z -�~�8qbZ����-W�PBKu�V�ZUY�k��s�aÆG&|�
���Z�v�7t�]w�ׯ�Z��.`��i[{�С����9� -����Fk�)��+�^��U��\͞=�a�c�ב#G����A+�0Κ�Ţ�\����:���kqq�i��h�������8������k��b��(��R�{~v]7Bk�y�Ύ��5�ֺ����8��o_4��Z�w��W�yvGh)%��yNKh ��XCh)�К�Uޅ��ZJ ��YCh -����ZJh -����RBK)%��V�B���ł ^gQ�i[�]h)%����jZ]�"��RJh5�ʻ�ZBK)��3�� -�%��ZM���GGG�:���ZB�K��/>_|�w$�-�O�=�w��R�����Ɇ!>���/j��/�ce�/l�|�+�N�3Z�/~ѩ��u�������s�<�N�IBk����D�Tt�%[M������������~����b� ����_���b���L.�:o����_����?|����߸�s�0�7Ŋ�~S��i�ׯ����1�߿��4�?���|:�&�Z]�ʻК~h�*zq����7=^u��������^!���ma���.'_� !O�h$v��U�^�:�P-�: -|_ ?�یvC ?N;����j-*������y�0�qL*&!�j�Fl',�A�p]�$�����;�WF2��8���v��j ��[?Y�\��-�K:Fm�N"��9�(�1�F��ϔ��O?v�yq�"�؟�?�Ws:��י���䓇X����f����OW&^�����[}�`.u��CݢN��jТް/~Cڼ��-�a�ϱ/u�m�㨟l�/���y��"O�҆�O��E1�%�r���1PZ^>W�{&Ck���ǿ}���UZTB�n�n��?�ɘ2���?"��K�-�����������U+*�I�r
���to%� :9iHK�E��ҡwV��|�~�ֳ�8��Z�1�q�x_Z\�����Ɛr��" ���p�Q�ȃsEl'���~�r=��v�<9�q����r�o�NYI��5�ڷ�z��m۶CCC�^9�E�O�����R�U�V���+��p����H��K�U�,����s:�@����b[t )lj>�~L�Q�)�;���J�s�~�I�F|���� ����q-�+b�1�����J��C�V>S�h\�g�_*���o�|���GψGX~�m�@+�P�?� �S`�<)p����h @����#��3��G��q6�A�Ӟ�����^��@��c�hHp��N�����d{zs����H�x�ղe��Yv͚5������������z�c0���:�(c��@�_�h���T���(�zQ ��Њ:�E~�s�eӟ��I�����V�� mFt���Ϩ(���H�G�,oSRh���/~M�!��Z�cJ�l���A�2RaK�L¡��T�t���K�- _�]���Z�V4�LȠl���p������#<B>1�K��v�qL�9Ĺp�����ǠyDcB��gF���{Z@`pp��^��ih��A� ���i}�N�4=�.��/�6?P��>�B�hE�,D�/|%�?�Ko��B���QZ��Eh�1��Y{�y!���4�TtP�ch����y��'b�" u/g[���0�
��>�Az�~��� �,��rFo��M��s��7n\����cV�5F�����[�dIS����K���F����_�xq% XMs�ν(Т�(�:����e5h�7�$9$�M�`��FG�sjZy�>�.��<�z����`�ЪY�1���v���q��QK\��yz��Y8#,B�3f�s�f*z�0�
-�R�N�l-���g��|_ Zʫ��X����k~؀�(��to���i13��&:K��ׄ3�a�A�N~#�i}&�G>�݀>P�q�-u����@�"� �>�?P:c�xv�h<�? xLʈ{HK�^�O��{�F�0�tT��O�*��)�7���Q|��H+F�)4�ՠ�<�r�рp=��S��� ��I�����كLq߽{w122R̚5��EG�r�. -�;ԝj�Ÿ�u��N� �.�,F��:�m��VE�J=�N�:T ���wg�+�ᡡ�7N�8!��@+f�q��ƒQV4�|���G�� D��9HO>8 7 oL/O+'�C�h(n��� j����`��r�'=4ʂ�r>�/*>�w8 @�T��@�cFȒ�L@�G�#I� �����1 ��!�s���<�3�S���Ge>�u�x饗�;v+W�쫑�x�R���O݉�e��)�
�$߇O���-R������\5-���Ih�1���i+�nz�1� ZT,*@� "������6* �4*a�h�ه�O�i�/�~No��b��9�����[h��|�g�!�t��9�P.FN�H�c3Bd[<8�7<��rqޜ�O��<�<���k�1a�G4lX��F�b<���+b;v�X�vm_,�K�!l>Hݡ~2
��_
�:#*|�:���h � �cT�>���>~~Ly���y�y]O���+�/?�Gy������B�sUD���hXp�hxґօ.1�*��`.�����Ac��
-�����QF[�p��Ȓ�)u�b��t��� -��q_�:�h�pZ:rQBK��D�A#B#M�0 �Ήي�=肹�X���D�Oh)������sh�f�ݻw�0���RJh -�*�BK)��k��������+%���rE ���@k˖-Ŝ9s�L�]'���ZJ��sZBKh -���RBKh)%���Z�xJ -�%��c��)OygeapTJuV,iՅ��:o޼�ccc �����3K��מX�����뾕� ڵkWO.��f�zZ<�u�wTB)uq�[�Y�����)��f�ꅆ�
ZJ��/�ܻw���=g:����RJ��ڃJ -���R=��'O<����e��RJM'���߿�Tﳩ�RW��Z�<��TW<\��ZBKu�qq�Z"NY��G�m)OM�KW+�(��ӧ��;p�@K��A�~�Z�,��P�V�q��㚵��߶V̺�t��X�Q�u��y�-�t|�w�>_hi������ߺQ�V�Z�Z��k�����v(���yǠ���|g�<��͛_��@7�b�T7�pë��+���5���ȑ#�WK�`���[I�ăv����TKCZIW�ںf:�Y+��m��+o�N9]�:��V�Y7��c�8�����[�J��o�(]��u�t��֍ҵ��J�j;T%���ۦ� �Tk&�=-��� �p���H���th��t{j�Y�b���v����ִ��Q�d�lK��*/y�@qՄ�iS���:rO���p��ȞՑ�6�_ ��~��h_�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4m� �wq�� ��H�)��sB��i5M�f��"�h���7_�q���z������iB�ۡ��c��Ϫi�63�F���7߇��
�� �^{lB�8��d� ��.ӏ�Z��� �Y�}E�������<�K����C�>��H�uB�����2_�R�mʾ[S��܃��#�6�79.,��Hy���zi����hty�3eC͈�L-Eh���w�L�<�I�ϔ��e��9P�3V6�l�=�����/R|����I���ٲ|��j�9e��'��W%����(��X���c �Δe��<VZ�ǒ���e�?i�*��L�]pi����^(AvE �Ѥ��z6��� ��l�������,�E���8�;��w���VV�,�V�O'e�L uIy�yMB�p�:��S��HY��lTe�TBk �Vk�f��in �7oT'�:[�6%�2ݼr����[Z�6���ߚ7�sU
���/g�/M@���Ǹg����� "���P mF�7 ��|�ym�#������st��i��F��l�We��I�U�P:XE ������r۫5ҧк�
��n�饶(K3\B�LR�Uɨkt
к����2��KH|74X�\6��9�%H���WY�4M�.�~�lXoj0Һ'�>��@�_>ZU滨�=�mL��Z��Ы5��Ү.A�B���̠��*��X2����X#%�Y��%�%�HY�����i�FT��@��o$�z��Ҽl$�b�U�`hD�,����naЊ�`ݙl�h9�Y�m�f��lv��e��M�����`I�5�l�{Z�s��lz @�
j��շ+J��l�߫ߝ��j �Ѥ��f���䵁��F��g���r�ɈcO ���� ��ɑ�@ �7ʴ�K���K��Z�穲<@�����(0�Q�o�#�������߽\^���xR�8��ɵz��揕y����4MӴ���Z��Ht�A�y���bWe��s�)G6�&ƒ��cߞ���V��o8w�ļR�4�n�L��*�/)!�G�Gj��w��r��������qO9�ʟ�N���S��e�۳k�i���6�l���#���v����ii��i�9#F�,�z��Y�6�@��O��+GR��p9�j��X5MӚ�8��T��UWӴ~�K� ��h�`�XcD���#.4���.TO T&�U1���6�u��i�6u� /�`j��i�d���BK�4M��i�>ܪi����]�%�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�4M�2���� "!@��IEND�B`�
#!/usr/bin/env node
var backoff = require('../index');
var testBackoff = backoff.exponential({
initialDelay: 10,
maxDelay: 1000
});
testBackoff.on('backoff', function(number, delay) {
console.log('Backoff start: ' + number + ' ' + delay + 'ms');
});
testBackoff.on('ready', function(number, delay) {
console.log('Backoff done: ' + number + ' ' + delay + 'ms');
if (number < 15) {
testBackoff.backoff();
}
});
testBackoff.backoff();
#!/usr/bin/env node
var backoff = require('../index');
var strategy = new backoff.ExponentialStrategy();
for (var i = 0; i < 10; i++) {
console.log(strategy.next());
}
#!/usr/bin/env node
var backoff = require('../index');
var testBackoff = backoff.exponential({
initialDelay: 10,
maxDelay: 1000
});
testBackoff.failAfter(5);
testBackoff.on('backoff', function(number, delay) {
console.log('Backoff start: ' + number + ' ' + delay + 'ms');
});
testBackoff.on('ready', function(number, delay) {
console.log('Backoff done: ' + number + ' ' + delay + 'ms');
testBackoff.backoff(); // Launch a new backoff.
});
testBackoff.on('fail', function() {
console.log('Backoff failure.');
});
testBackoff.backoff();
#!/usr/bin/env node
var backoff = require('../index');
var testBackoff = backoff.fibonacci({
initialDelay: 10,
maxDelay: 1000
});
testBackoff.on('backoff', function(number, delay) {
console.log('Backoff start: ' + number + ' ' + delay + 'ms');
});
testBackoff.on('ready', function(number, delay) {
console.log('Backoff done: ' + number + ' ' + delay + 'ms');
if (number < 15) {
testBackoff.backoff();
}
});
testBackoff.backoff();
#!/usr/bin/env node
var backoff = require('../index');
var strategy = new backoff.FibonacciStrategy();
for (var i = 0; i < 10; i++) {
console.log(strategy.next());
}
#!/usr/bin/env node
var backoff = require('../index.js'),
util = require('util'),
http = require('http');
var URL = 'http://www.iana.org/domains/example/';
function get(options, callback) {
http.get(options, function(res) {
res.setEncoding('utf8');
res.data = '';
res.on('data', function (chunk) {
res.data += chunk;
});
res.on('end', function() {
callback(null, res);
});
res.on('close', function(err) {
callback(err, res);
});
}).on('error', function(err) {
callback(err, null);
});
}
var call = backoff.call(get, URL, function(err, res) {
// Notice how the call is captured inside the closure.
console.log('Retries: ' + call.getResults().length);
if (err) {
console.log('Error: ' + err.message);
} else {
console.log('Status: ' + res.statusCode);
}
});
// Called when function is called with function's args.
call.on('call', function(url) {
console.log('call: ' + util.inspect(arguments));
});
// Called with results each time function returns.
call.on('callback', function(err, res) {
console.log('callback: ' + util.inspect(arguments));
});
// Called on backoff.
call.on('backoff', function(number, delay) {
console.log('backoff: ' + util.inspect(arguments));
});
call.setStrategy(new backoff.ExponentialStrategy());
call.failAfter(2);
call.start();
#!/usr/bin/env node
var backoff = require('../index');
var randomizedBackoff = backoff.fibonacci({
randomisationFactor: 0.4,
initialDelay: 10,
maxDelay: 1000
});
randomizedBackoff.on('backoff', function(number, delay) {
console.log('Backoff start: ' + number + ' ' + delay + 'ms');
});
randomizedBackoff.on('ready', function(number, delay) {
console.log('Backoff done: ' + number + ' ' + delay + 'ms');
if (number < 15) {
randomizedBackoff.backoff();
}
});
randomizedBackoff.backoff();
#!/usr/bin/env node
var backoff = require('../index.js');
var fibonacciBackoff = backoff.fibonacci({
randomisationFactor: 0,
initialDelay: 10,
maxDelay: 300
});
fibonacciBackoff.failAfter(10);
fibonacciBackoff.on('backoff', function(number, delay) {
// Do something when backoff starts, e.g. show to the
// user the delay before next reconnection attempt.
console.log(number + ' ' + delay + 'ms');
});
fibonacciBackoff.on('ready', function(number, delay) {
// Do something when backoff ends, e.g. retry a failed
// operation (DNS lookup, API call, etc.).
fibonacciBackoff.backoff();
});
fibonacciBackoff.on('fail', function() {
// Do something when the maximum number of backoffs is
// reached, e.g. ask the user to check its connection.
console.log('fail');
});
fibonacciBackoff.backoff();
#!/usr/bin/env node
var backoff = require('../index');
var backoff = backoff.exponential();
backoff.on('ready', function(number, delay) {
console.log('Backoff done: ' + number + ' ' + delay + 'ms');
if (number < 15) {
backoff.backoff();
}
});
backoff.backoff();
setInterval(function() {
backoff.reset();
backoff.backoff();
}, 5000);
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var Backoff = require('./lib/backoff');
var ExponentialBackoffStrategy = require('./lib/strategy/exponential');
var FibonacciBackoffStrategy = require('./lib/strategy/fibonacci');
var FunctionCall = require('./lib/function_call.js');
module.exports.Backoff = Backoff;
module.exports.FunctionCall = FunctionCall;
module.exports.FibonacciStrategy = FibonacciBackoffStrategy;
module.exports.ExponentialStrategy = ExponentialBackoffStrategy;
/**
* Constructs a Fibonacci backoff.
* @param options Fibonacci backoff strategy arguments.
* @return The fibonacci backoff.
* @see FibonacciBackoffStrategy
*/
module.exports.fibonacci = function(options) {
return new Backoff(new FibonacciBackoffStrategy(options));
};
/**
* Constructs an exponential backoff.
* @param options Exponential strategy arguments.
* @return The exponential backoff.
* @see ExponentialBackoffStrategy
*/
module.exports.exponential = function(options) {
return new Backoff(new ExponentialBackoffStrategy(options));
};
/**
* Constructs a FunctionCall for the given function and arguments.
* @param fn The function to wrap in a backoff handler.
* @param vargs The function's arguments (var args).
* @param callback The function's callback.
* @return The FunctionCall instance.
*/
module.exports.call = function(fn, vargs, callback) {
var args = Array.prototype.slice.call(arguments);
fn = args[0];
vargs = args.slice(1, args.length - 1);
callback = args[args.length - 1];
return new FunctionCall(fn, vargs, callback);
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var events = require('events');
var util = require('util');
/**
* Backoff driver.
* @param backoffStrategy Backoff delay generator/strategy.
* @constructor
*/
function Backoff(backoffStrategy) {
events.EventEmitter.call(this);
this.backoffStrategy_ = backoffStrategy;
this.maxNumberOfRetry_ = -1;
this.backoffNumber_ = 0;
this.backoffDelay_ = 0;
this.timeoutID_ = -1;
this.handlers = {
backoff: this.onBackoff_.bind(this)
};
}
util.inherits(Backoff, events.EventEmitter);
/**
* Sets a limit, greater than 0, on the maximum number of backoffs. A 'fail'
* event will be emitted when the limit is reached.
* @param maxNumberOfRetry The maximum number of backoffs.
*/
Backoff.prototype.failAfter = function(maxNumberOfRetry) {
if (maxNumberOfRetry < 1) {
throw new Error('Maximum number of retry must be greater than 0. ' +
'Actual: ' + maxNumberOfRetry);
}
this.maxNumberOfRetry_ = maxNumberOfRetry;
};
/**
* Starts a backoff operation.
* @param err Optional paramater to let the listeners know why the backoff
* operation was started.
*/
Backoff.prototype.backoff = function(err) {
if (this.timeoutID_ !== -1) {
throw new Error('Backoff in progress.');
}
if (this.backoffNumber_ === this.maxNumberOfRetry_) {
this.emit('fail', err);
this.reset();
} else {
this.backoffDelay_ = this.backoffStrategy_.next();
this.timeoutID_ = setTimeout(this.handlers.backoff, this.backoffDelay_);
this.emit('backoff', this.backoffNumber_, this.backoffDelay_, err);
}
};
/**
* Handles the backoff timeout completion.
* @private
*/
Backoff.prototype.onBackoff_ = function() {
this.timeoutID_ = -1;
this.emit('ready', this.backoffNumber_, this.backoffDelay_);
this.backoffNumber_++;
};
/**
* Stops any backoff operation and resets the backoff delay to its inital
* value.
*/
Backoff.prototype.reset = function() {
this.backoffNumber_ = 0;
this.backoffStrategy_.reset();
clearTimeout(this.timeoutID_);
this.timeoutID_ = -1;
};
module.exports = Backoff;
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var events = require('events');
var util = require('util');
var Backoff = require('./backoff');
var FibonacciBackoffStrategy = require('./strategy/fibonacci');
/**
* Returns true if the specified value is a function
* @param val Variable to test.
* @return Whether variable is a function.
*/
function isFunction(val) {
return typeof val == 'function';
}
/**
* Manages the calling of a function in a backoff loop.
* @param fn Function to wrap in a backoff handler.
* @param args Array of function's arguments.
* @param callback Function's callback.
* @constructor
*/
function FunctionCall(fn, args, callback) {
events.EventEmitter.call(this);
if (!isFunction(fn)) {
throw new Error('fn should be a function.' +
'Actual: ' + typeof fn);
}
if (!isFunction(callback)) {
throw new Error('callback should be a function.' +
'Actual: ' + typeof fn);
}
this.function_ = fn;
this.arguments_ = args;
this.callback_ = callback;
this.results_ = [];
this.backoff_ = null;
this.strategy_ = null;
this.failAfter_ = -1;
this.state_ = FunctionCall.State_.PENDING;
}
util.inherits(FunctionCall, events.EventEmitter);
/**
* Enum of states in which the FunctionCall can be.
* @private
*/
FunctionCall.State_ = {
PENDING: 0,
RUNNING: 1,
COMPLETED: 2,
ABORTED: 3
};
/**
* @return Whether the call is pending.
*/
FunctionCall.prototype.isPending = function() {
return this.state_ == FunctionCall.State_.PENDING;
};
/**
* @return Whether the call is in progress.
*/
FunctionCall.prototype.isRunning = function() {
return this.state_ == FunctionCall.State_.RUNNING;
};
/**
* @return Whether the call is completed.
*/
FunctionCall.prototype.isCompleted = function() {
return this.state_ == FunctionCall.State_.COMPLETED;
};
/**
* @return Whether the call is aborted.
*/
FunctionCall.prototype.isAborted = function() {
return this.state_ == FunctionCall.State_.ABORTED;
};
/**
* Sets the backoff strategy.
* @param strategy The backoff strategy to use.
* @return Itself for chaining.
*/
FunctionCall.prototype.setStrategy = function(strategy) {
if (!this.isPending()) {
throw new Error('FunctionCall in progress.');
}
this.strategy_ = strategy;
return this;
};
/**
* Returns all intermediary results returned by the wrapped function since
* the initial call.
* @return An array of intermediary results.
*/
FunctionCall.prototype.getResults = function() {
return this.results_.concat();
};
/**
* Sets the backoff limit.
* @param maxNumberOfRetry The maximum number of backoffs.
* @return Itself for chaining.
*/
FunctionCall.prototype.failAfter = function(maxNumberOfRetry) {
if (!this.isPending()) {
throw new Error('FunctionCall in progress.');
}
this.failAfter_ = maxNumberOfRetry;
return this;
};
/**
* Aborts the call.
*/
FunctionCall.prototype.abort = function() {
if (this.isCompleted()) {
throw new Error('FunctionCall already completed.');
}
if (this.isRunning()) {
this.backoff_.reset();
}
this.state_ = FunctionCall.State_.ABORTED;
};
/**
* Initiates the call to the wrapped function.
* @param backoffFactory Optional factory function used to create the backoff
* instance.
*/
FunctionCall.prototype.start = function(backoffFactory) {
if (this.isAborted()) {
throw new Error('FunctionCall aborted.');
} else if (!this.isPending()) {
throw new Error('FunctionCall already started.');
}
var strategy = this.strategy_ || new FibonacciBackoffStrategy();
this.backoff_ = backoffFactory ?
backoffFactory(strategy) :
new Backoff(strategy);
this.backoff_.on('ready', this.doCall_.bind(this));
this.backoff_.on('fail', this.doCallback_.bind(this));
this.backoff_.on('backoff', this.handleBackoff_.bind(this));
if (this.failAfter_ > 0) {
this.backoff_.failAfter(this.failAfter_);
}
this.state_ = FunctionCall.State_.RUNNING;
this.doCall_();
};
/**
* Calls the wrapped function.
* @private
*/
FunctionCall.prototype.doCall_ = function() {
var eventArgs = ['call'].concat(this.arguments_);
events.EventEmitter.prototype.emit.apply(this, eventArgs);
var callback = this.handleFunctionCallback_.bind(this);
this.function_.apply(null, this.arguments_.concat(callback));
};
/**
* Calls the wrapped function's callback with the last result returned by the
* wrapped function.
* @private
*/
FunctionCall.prototype.doCallback_ = function() {
var args = this.results_[this.results_.length - 1];
this.callback_.apply(null, args);
};
/**
* Handles wrapped function's completion. This method acts as a replacement
* for the original callback function.
* @private
*/
FunctionCall.prototype.handleFunctionCallback_ = function() {
if (this.isAborted()) {
return;
}
var args = Array.prototype.slice.call(arguments);
this.results_.push(args); // Save callback arguments.
events.EventEmitter.prototype.emit.apply(this, ['callback'].concat(args));
if (args[0]) {
this.backoff_.backoff(args[0]);
} else {
this.state_ = FunctionCall.State_.COMPLETED;
this.doCallback_();
}
};
/**
* Handles backoff event.
* @param number Backoff number.
* @param delay Backoff delay.
* @param err The error that caused the backoff.
* @private
*/
FunctionCall.prototype.handleBackoff_ = function(number, delay, err) {
this.emit('backoff', number, delay, err);
};
module.exports = FunctionCall;
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var util = require('util');
var BackoffStrategy = require('./strategy');
/**
* Exponential backoff strategy.
* @extends BackoffStrategy
*/
function ExponentialBackoffStrategy(options) {
BackoffStrategy.call(this, options);
this.backoffDelay_ = 0;
this.nextBackoffDelay_ = this.getInitialDelay();
}
util.inherits(ExponentialBackoffStrategy, BackoffStrategy);
/** @inheritDoc */
ExponentialBackoffStrategy.prototype.next_ = function() {
this.backoffDelay_ = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
this.nextBackoffDelay_ = this.backoffDelay_ * 2;
return this.backoffDelay_;
};
/** @inheritDoc */
ExponentialBackoffStrategy.prototype.reset_ = function() {
this.backoffDelay_ = 0;
this.nextBackoffDelay_ = this.getInitialDelay();
};
module.exports = ExponentialBackoffStrategy;
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var util = require('util');
var BackoffStrategy = require('./strategy');
/**
* Fibonacci backoff strategy.
* @extends BackoffStrategy
*/
function FibonacciBackoffStrategy(options) {
BackoffStrategy.call(this, options);
this.backoffDelay_ = 0;
this.nextBackoffDelay_ = this.getInitialDelay();
}
util.inherits(FibonacciBackoffStrategy, BackoffStrategy);
/** @inheritDoc */
FibonacciBackoffStrategy.prototype.next_ = function() {
var backoffDelay = Math.min(this.nextBackoffDelay_, this.getMaxDelay());
this.nextBackoffDelay_ += this.backoffDelay_;
this.backoffDelay_ = backoffDelay;
return backoffDelay;
};
/** @inheritDoc */
FibonacciBackoffStrategy.prototype.reset_ = function() {
this.nextBackoffDelay_ = this.getInitialDelay();
this.backoffDelay_ = 0;
};
module.exports = FibonacciBackoffStrategy;
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var events = require('events');
var util = require('util');
function isDef(value) {
return value !== undefined && value !== null;
}
/**
* Abstract class defining the skeleton for all backoff strategies.
* @param options Backoff strategy options.
* @param options.randomisationFactor The randomisation factor, must be between
* 0 and 1.
* @param options.initialDelay The backoff initial delay, in milliseconds.
* @param options.maxDelay The backoff maximal delay, in milliseconds.
* @constructor
*/
function BackoffStrategy(options) {
options = options || {};
if (isDef(options.initialDelay) && options.initialDelay < 1) {
throw new Error('The initial timeout must be greater than 0.');
} else if (isDef(options.maxDelay) && options.maxDelay < 1) {
throw new Error('The maximal timeout must be greater than 0.');
}
this.initialDelay_ = options.initialDelay || 100;
this.maxDelay_ = options.maxDelay || 10000;
if (this.maxDelay_ <= this.initialDelay_) {
throw new Error('The maximal backoff delay must be ' +
'greater than the initial backoff delay.');
}
if (isDef(options.randomisationFactor) &&
(options.randomisationFactor < 0 || options.randomisationFactor > 1)) {
throw new Error('The randomisation factor must be between 0 and 1.');
}
this.randomisationFactor_ = options.randomisationFactor || 0;
}
/**
* Retrieves the maximal backoff delay.
* @return The maximal backoff delay, in milliseconds.
*/
BackoffStrategy.prototype.getMaxDelay = function() {
return this.maxDelay_;
};
/**
* Retrieves the initial backoff delay.
* @return The initial backoff delay, in milliseconds.
*/
BackoffStrategy.prototype.getInitialDelay = function() {
return this.initialDelay_;
};
/**
* Template method that computes the next backoff delay.
* @return The backoff delay, in milliseconds.
*/
BackoffStrategy.prototype.next = function() {
var backoffDelay = this.next_();
var randomisationMultiple = 1 + Math.random() * this.randomisationFactor_;
var randomizedDelay = Math.round(backoffDelay * randomisationMultiple);
return randomizedDelay;
};
/**
* Computes the next backoff delay.
* @return The backoff delay, in milliseconds.
* @protected
*/
BackoffStrategy.prototype.next_ = function() {
throw new Error('BackoffStrategy.next_() unimplemented.');
};
/**
* Template method that resets the backoff delay to its initial value.
*/
BackoffStrategy.prototype.reset = function() {
this.reset_();
};
/**
* Resets the backoff delay to its initial value.
* @protected
*/
BackoffStrategy.prototype.reset_ = function() {
throw new Error('BackoffStrategy.reset_() unimplemented.');
};
module.exports = BackoffStrategy;
Copyright (C) 2012 Mathieu Turcotte
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "backoff",
"description": "Fibonacci and exponential backoffs.",
"version": "2.3.0",
"license": "MIT",
"author": {
"name": "Mathieu Turcotte",
"email": "turcotte.mat@gmail.com"
},
"keywords": [
"backoff",
"retry",
"fibonacci",
"exponential"
],
"repository": {
"type": "git",
"url": "https://github.com/MathieuTurcotte/node-backoff.git"
},
"devDependencies": {
"sinon": "1.7",
"nodeunit": "0.8",
"jshint": "2.0"
},
"scripts": {
"pretest": "node_modules/jshint/bin/jshint lib/ tests/ examples/ index.js",
"test": "node_modules/nodeunit/bin/nodeunit tests/"
},
"engines": {
"node": ">= 0.6"
},
"readme": "# Backoff for Node.js\n[![Build Status](https://secure.travis-ci.org/MathieuTurcotte/node-backoff.png?branch=master)](http://travis-ci.org/MathieuTurcotte/node-backoff)\n[![NPM version](https://badge.fury.io/js/backoff.png)](http://badge.fury.io/js/backoff)\n\nFibonacci and exponential backoffs for Node.js.\n\n## Installation\n\n```\nnpm install backoff\n```\n\n## Unit tests\n\n```\nnpm test\n```\n\n## Usage\n\n### Object Oriented\n\nThe usual way to instantiate a new `Backoff` object is to use one predefined\nfactory method: `backoff.fibonacci([options])`, `backoff.exponential([options])`.\n\n`Backoff` inherits from `EventEmitter`. When a backoff starts, a `backoff`\nevent is emitted and, when a backoff ends, a `ready` event is emitted.\nHandlers for these two events are called with the current backoff number and\ndelay.\n\n``` js\nvar backoff = require('backoff');\n\nvar fibonacciBackoff = backoff.fibonacci({\n randomisationFactor: 0,\n initialDelay: 10,\n maxDelay: 300\n});\n\nfibonacciBackoff.failAfter(10);\n\nfibonacciBackoff.on('backoff', function(number, delay) {\n // Do something when backoff starts, e.g. show to the\n // user the delay before next reconnection attempt.\n console.log(number + ' ' + delay + 'ms');\n});\n\nfibonacciBackoff.on('ready', function(number, delay) {\n // Do something when backoff ends, e.g. retry a failed\n // operation (DNS lookup, API call, etc.). If it fails\n // again then backoff, otherwise reset the backoff\n // instance.\n fibonacciBackoff.backoff();\n});\n\nfibonacciBackoff.on('fail', function() {\n // Do something when the maximum number of backoffs is\n // reached, e.g. ask the user to check its connection.\n console.log('fail');\n});\n\nfibonacciBackoff.backoff();\n```\n\nThe previous example would print the following.\n\n```\n0 10ms\n1 10ms\n2 20ms\n3 30ms\n4 50ms\n5 80ms\n6 130ms\n7 210ms\n8 300ms\n9 300ms\nfail\n```\n\nNote that `Backoff` objects are meant to be instantiated once and reused\nseveral times by calling `reset` after a successful \"retry\".\n\n### Functional\n\nIt's also possible to avoid some boilerplate code when invoking an asynchronous\nfunction in a backoff loop by using `backoff.call(fn, [args, ...], callback)`.\n\nTypical usage looks like the following.\n\n``` js\nvar call = backoff.call(get, 'https://duplika.ca/', function(err, res) {\n console.log('Retries: ' + call.getResults().length);\n\n if (err) {\n console.log('Error: ' + err.message);\n } else {\n console.log('Status: ' + res.statusCode);\n }\n});\n\ncall.setStrategy(new backoff.ExponentialStrategy());\ncall.failAfter(10);\ncall.start();\n```\n\n## API\n\n### backoff.fibonacci([options])\n\nConstructs a Fibonacci backoff (10, 10, 20, 30, 50, etc.).\n\nSee bellow for options description.\n\n### backoff.exponential([options])\n\nConstructs an exponential backoff (10, 20, 40, 80, etc.).\n\nThe options are the following.\n\n- randomisationFactor: defaults to 0, must be between 0 and 1\n- initialDelay: defaults to 100 ms\n- maxDelay: defaults to 10000 ms\n\nWith these values, the backoff delay will increase from 100 ms to 10000 ms. The\nrandomisation factor controls the range of randomness and must be between 0\nand 1. By default, no randomisation is applied on the backoff delay.\n\n### backoff.call(fn, [args, ...], callback)\n\n- fn: function to call in a backoff handler, i.e. the wrapped function\n- args: function's arguments\n- callback: function's callback accepting an error as its first argument\n\nConstructs a `FunctionCall` instance for the given function. The wrapped\nfunction will get retried until it succeds or reaches the maximum number\nof backoffs. In both cases, the callback function will be invoked with the\nlast result returned by the wrapped function.\n\nIt is the caller's responsability to initiate the call by invoking the\n`start` method on the returned `FunctionCall` instance.\n\n### Class Backoff\n\n#### new Backoff(strategy)\n\n- strategy: the backoff strategy to use\n\nConstructs a new backoff object from a specific backoff strategy. The backoff\nstrategy must implement the `BackoffStrategy`interface defined bellow.\n\n#### backoff.failAfter(numberOfBackoffs)\n\n- numberOfBackoffs: maximum number of backoffs before the fail event gets\nemitted, must be greater than 0\n\nSets a limit on the maximum number of backoffs that can be performed before\na fail event gets emitted and the backoff instance is reset. By default, there\nis no limit on the number of backoffs that can be performed.\n\n#### backoff.backoff([err])\n\nStarts a backoff operation. If provided, the error parameter will be emitted\nas the last argument of the `backoff` and `fail` events to let the listeners\nknow why the backoff operation was attempted.\n\nAn error will be thrown an error if a backoff operation is already in progress.\n\nIn practice, this method should be called after a failed attempt to perform a\nsensitive operation (connecting to a database, downloading a resource over the\nnetwork, etc.).\n\n#### backoff.reset()\n\nResets the backoff delay to the initial backoff delay and stop any backoff\noperation in progress. After reset, a backoff instance can and should be\nreused.\n\nIn practice, this method should be called after having successfully completed\nthe sensitive operation guarded by the backoff instance or if the client code\nrequest to stop any reconnection attempt.\n\n#### Event: 'backoff'\n\n- number: number of backoffs since last reset, starting at 0\n- delay: backoff delay in milliseconds\n- err: optional error parameter passed to `backoff.backoff([err])`\n\nEmitted when a backoff operation is started. Signals to the client how long\nthe next backoff delay will be.\n\n#### Event: 'ready'\n\n- number: number of backoffs since last reset, starting at 0\n- delay: backoff delay in milliseconds\n\nEmitted when a backoff operation is done. Signals that the failing operation\nshould be retried.\n\n#### Event: 'fail'\n\n- err: optional error parameter passed to `backoff.backoff([err])`\n\nEmitted when the maximum number of backoffs is reached. This event will only\nbe emitted if the client has set a limit on the number of backoffs by calling\n`backoff.failAfter(numberOfBackoffs)`. The backoff instance is automatically\nreset after this event is emitted.\n\n### Interface BackoffStrategy\n\nA backoff strategy must provide the following methods.\n\n#### strategy.next()\n\nComputes and returns the next backoff delay.\n\n#### strategy.reset()\n\nResets the backoff delay to its initial value.\n\n### Class ExponentialStrategy\n\nExponential (10, 20, 40, 80, etc.) backoff strategy implementation.\n\n#### new ExponentialStrategy([options])\n\nThe options are the following.\n\n- randomisationFactor: defaults to 0, must be between 0 and 1\n- initialDelay: defaults to 100 ms\n- maxDelay: defaults to 10000 ms\n\n### Class FibonacciStrategy\n\nFibonnaci (10, 10, 20, 30, 50, etc.) backoff strategy implementation.\n\n#### new FibonacciStrategy([options])\n\nThe options are the following.\n\n- randomisationFactor: defaults to 0, must be between 0 and 1\n- initialDelay: defaults to 100 ms\n- maxDelay: defaults to 10000 ms\n\n### Class FunctionCall\n\nThis class manages the calling of an asynchronous function within a backoff\nloop.\n\nThis class should rarely be instantiated directly since the factory method\n`backoff.call(fn, [args, ...], callback)` offers a more convenient and safer\nway to create `FunctionCall` instances.\n\n#### new FunctionCall(fn, args, callback)\n\n- fn: asynchronous function to call\n- args: an array containing fn's args\n- callback: fn's callback\n\nConstructs a function handler for the given asynchronous function.\n\n#### call.isPending()\n\nReturns whether the call is pending, i.e. hasn't been started.\n\n#### call.isRunning()\n\nReturns whether the call is in progress.\n\n#### call.isCompleted()\n\nReturns whether the call is completed.\n\n#### call.isAborted()\n\nReturns whether the call is aborted.\n\n#### call.setStrategy(strategy)\n\n- strategy: strategy instance to use, defaults to `FibonacciStrategy`.\n\nSets the backoff strategy to use. This method should be called before\n`call.start()` otherwise an exception will be thrown.\n\n#### call.failAfter(maxNumberOfBackoffs)\n\n- maxNumberOfBackoffs: maximum number of backoffs before the call is aborted\n\nSets the maximum number of backoffs before the call is aborted. By default,\nthere is no limit on the number of backoffs that can be performed.\n\nThis method should be called before `call.start()` otherwise an exception will\nbe thrown..\n\n#### call.getResults()\n\nRetrieves all intermediary results returned by the wrapped function. This\nmethod can be called at any point in time during the call life cycle, i.e.\nbefore, during and after the wrapped function invocation.\n\nReturns an array of arrays containing the results returned by the wrapped\nfunction for each call. For example, to get the error code returned by the\nsecond call, one would do the following.\n\n``` js\nvar results = call.getResults();\nvar error = results[1][0];\n```\n\n#### call.start()\n\nInitiates the call the wrapped function. This method should only be called\nonce otherwise an exception will be thrown.\n\n\n#### call.abort()\n\nAborts the call.\n\nPast results can be retrieved using `call.getResults()`. This method can be\ncalled at any point in time during the call life cycle, i.e. before, during\nand after the wrapped function invocation.\n\n#### Event: 'call'\n\n- args: wrapped function's arguments\n\nEmitted each time the wrapped function is called.\n\n#### Event: 'callback'\n\n- results: wrapped function's return values\n\nEmitted each time the wrapped function invokes its callback.\n\n#### Event: 'backoff'\n\n- number: backoff number, starts at 0\n- delay: backoff delay in milliseconds\n- err: the error that triggered the backoff operation\n\nEmitted each time a backoff operation is started.\n\n## License\n\nThis code is free to use under the terms of the [MIT license](http://mturcotte.mit-license.org/).\n",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/MathieuTurcotte/node-backoff/issues"
},
"homepage": "https://github.com/MathieuTurcotte/node-backoff",
"_id": "backoff@2.3.0",
"dist": {
"shasum": "9f896eabaea1e92fb9b69c7cf39f2d7043397a71"
},
"_from": "backoff@2.3.0",
"_resolved": "https://registry.npmjs.org/backoff/-/backoff-2.3.0.tgz"
}

Backoff for Node.js

Build Status NPM version

Fibonacci and exponential backoffs for Node.js.

Installation

npm install backoff

Unit tests

npm test

Usage

Object Oriented

The usual way to instantiate a new Backoff object is to use one predefined factory method: backoff.fibonacci([options]), backoff.exponential([options]).

Backoff inherits from EventEmitter. When a backoff starts, a backoff event is emitted and, when a backoff ends, a ready event is emitted. Handlers for these two events are called with the current backoff number and delay.

var backoff = require('backoff');

var fibonacciBackoff = backoff.fibonacci({
    randomisationFactor: 0,
    initialDelay: 10,
    maxDelay: 300
});

fibonacciBackoff.failAfter(10);

fibonacciBackoff.on('backoff', function(number, delay) {
    // Do something when backoff starts, e.g. show to the
    // user the delay before next reconnection attempt.
    console.log(number + ' ' + delay + 'ms');
});

fibonacciBackoff.on('ready', function(number, delay) {
    // Do something when backoff ends, e.g. retry a failed
    // operation (DNS lookup, API call, etc.). If it fails
    // again then backoff, otherwise reset the backoff
    // instance.
    fibonacciBackoff.backoff();
});

fibonacciBackoff.on('fail', function() {
    // Do something when the maximum number of backoffs is
    // reached, e.g. ask the user to check its connection.
    console.log('fail');
});

fibonacciBackoff.backoff();

The previous example would print the following.

0 10ms
1 10ms
2 20ms
3 30ms
4 50ms
5 80ms
6 130ms
7 210ms
8 300ms
9 300ms
fail

Note that Backoff objects are meant to be instantiated once and reused several times by calling reset after a successful "retry".

Functional

It's also possible to avoid some boilerplate code when invoking an asynchronous function in a backoff loop by using backoff.call(fn, [args, ...], callback).

Typical usage looks like the following.

var call = backoff.call(get, 'https://duplika.ca/', function(err, res) {
    console.log('Retries: ' + call.getResults().length);

    if (err) {
        console.log('Error: ' + err.message);
    } else {
        console.log('Status: ' + res.statusCode);
    }
});

call.setStrategy(new backoff.ExponentialStrategy());
call.failAfter(10);
call.start();

API

backoff.fibonacci([options])

Constructs a Fibonacci backoff (10, 10, 20, 30, 50, etc.).

See bellow for options description.

backoff.exponential([options])

Constructs an exponential backoff (10, 20, 40, 80, etc.).

The options are the following.

  • randomisationFactor: defaults to 0, must be between 0 and 1
  • initialDelay: defaults to 100 ms
  • maxDelay: defaults to 10000 ms

With these values, the backoff delay will increase from 100 ms to 10000 ms. The randomisation factor controls the range of randomness and must be between 0 and 1. By default, no randomisation is applied on the backoff delay.

backoff.call(fn, [args, ...], callback)

  • fn: function to call in a backoff handler, i.e. the wrapped function
  • args: function's arguments
  • callback: function's callback accepting an error as its first argument

Constructs a FunctionCall instance for the given function. The wrapped function will get retried until it succeds or reaches the maximum number of backoffs. In both cases, the callback function will be invoked with the last result returned by the wrapped function.

It is the caller's responsability to initiate the call by invoking the start method on the returned FunctionCall instance.

Class Backoff

new Backoff(strategy)

  • strategy: the backoff strategy to use

Constructs a new backoff object from a specific backoff strategy. The backoff strategy must implement the BackoffStrategyinterface defined bellow.

backoff.failAfter(numberOfBackoffs)

  • numberOfBackoffs: maximum number of backoffs before the fail event gets emitted, must be greater than 0

Sets a limit on the maximum number of backoffs that can be performed before a fail event gets emitted and the backoff instance is reset. By default, there is no limit on the number of backoffs that can be performed.

backoff.backoff([err])

Starts a backoff operation. If provided, the error parameter will be emitted as the last argument of the backoff and fail events to let the listeners know why the backoff operation was attempted.

An error will be thrown an error if a backoff operation is already in progress.

In practice, this method should be called after a failed attempt to perform a sensitive operation (connecting to a database, downloading a resource over the network, etc.).

backoff.reset()

Resets the backoff delay to the initial backoff delay and stop any backoff operation in progress. After reset, a backoff instance can and should be reused.

In practice, this method should be called after having successfully completed the sensitive operation guarded by the backoff instance or if the client code request to stop any reconnection attempt.

Event: 'backoff'

  • number: number of backoffs since last reset, starting at 0
  • delay: backoff delay in milliseconds
  • err: optional error parameter passed to backoff.backoff([err])

Emitted when a backoff operation is started. Signals to the client how long the next backoff delay will be.

Event: 'ready'

  • number: number of backoffs since last reset, starting at 0
  • delay: backoff delay in milliseconds

Emitted when a backoff operation is done. Signals that the failing operation should be retried.

Event: 'fail'

  • err: optional error parameter passed to backoff.backoff([err])

Emitted when the maximum number of backoffs is reached. This event will only be emitted if the client has set a limit on the number of backoffs by calling backoff.failAfter(numberOfBackoffs). The backoff instance is automatically reset after this event is emitted.

Interface BackoffStrategy

A backoff strategy must provide the following methods.

strategy.next()

Computes and returns the next backoff delay.

strategy.reset()

Resets the backoff delay to its initial value.

Class ExponentialStrategy

Exponential (10, 20, 40, 80, etc.) backoff strategy implementation.

new ExponentialStrategy([options])

The options are the following.

  • randomisationFactor: defaults to 0, must be between 0 and 1
  • initialDelay: defaults to 100 ms
  • maxDelay: defaults to 10000 ms

Class FibonacciStrategy

Fibonnaci (10, 10, 20, 30, 50, etc.) backoff strategy implementation.

new FibonacciStrategy([options])

The options are the following.

  • randomisationFactor: defaults to 0, must be between 0 and 1
  • initialDelay: defaults to 100 ms
  • maxDelay: defaults to 10000 ms

Class FunctionCall

This class manages the calling of an asynchronous function within a backoff loop.

This class should rarely be instantiated directly since the factory method backoff.call(fn, [args, ...], callback) offers a more convenient and safer way to create FunctionCall instances.

new FunctionCall(fn, args, callback)

  • fn: asynchronous function to call
  • args: an array containing fn's args
  • callback: fn's callback

Constructs a function handler for the given asynchronous function.

call.isPending()

Returns whether the call is pending, i.e. hasn't been started.

call.isRunning()

Returns whether the call is in progress.

call.isCompleted()

Returns whether the call is completed.

call.isAborted()

Returns whether the call is aborted.

call.setStrategy(strategy)

  • strategy: strategy instance to use, defaults to FibonacciStrategy.

Sets the backoff strategy to use. This method should be called before call.start() otherwise an exception will be thrown.

call.failAfter(maxNumberOfBackoffs)

  • maxNumberOfBackoffs: maximum number of backoffs before the call is aborted

Sets the maximum number of backoffs before the call is aborted. By default, there is no limit on the number of backoffs that can be performed.

This method should be called before call.start() otherwise an exception will be thrown..

call.getResults()

Retrieves all intermediary results returned by the wrapped function. This method can be called at any point in time during the call life cycle, i.e. before, during and after the wrapped function invocation.

Returns an array of arrays containing the results returned by the wrapped function for each call. For example, to get the error code returned by the second call, one would do the following.

var results = call.getResults();
var error = results[1][0];

call.start()

Initiates the call the wrapped function. This method should only be called once otherwise an exception will be thrown.

call.abort()

Aborts the call.

Past results can be retrieved using call.getResults(). This method can be called at any point in time during the call life cycle, i.e. before, during and after the wrapped function invocation.

Event: 'call'

  • args: wrapped function's arguments

Emitted each time the wrapped function is called.

Event: 'callback'

  • results: wrapped function's return values

Emitted each time the wrapped function invokes its callback.

Event: 'backoff'

  • number: backoff number, starts at 0
  • delay: backoff delay in milliseconds
  • err: the error that triggered the backoff operation

Emitted each time a backoff operation is started.

License

This code is free to use under the terms of the MIT license.

/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var sinon = require('sinon');
var backoff = require('../index');
exports["API"] = {
"backoff.fibonnaci should be a function that returns a backoff instance": function(test) {
test.ok(backoff.fibonacci, 'backoff.fibonacci should be defined.');
test.equal(typeof backoff.fibonacci, 'function',
'backoff.fibonacci should be a function.');
test.equal(backoff.fibonacci().constructor.name, 'Backoff');
test.done();
},
"backoff.exponential should be a function that returns a backoff instance": function(test) {
test.ok(backoff.exponential, 'backoff.exponential should be defined.');
test.equal(typeof backoff.exponential, 'function',
'backoff.exponential should be a function.');
test.equal(backoff.exponential().constructor.name, 'Backoff');
test.done();
},
"backoff.call should be a function that returns a FunctionCall instance": function(test) {
var fn = function() {};
var callback = function() {};
test.ok(backoff.Backoff, 'backoff.call should be defined.');
test.equal(typeof backoff.call, 'function',
'backoff.call should be a function.');
test.equal(backoff.call(fn, 1, 2, 3, callback).constructor.name,
'FunctionCall');
test.done();
},
"backoff.Backoff should be defined and a function": function(test) {
test.ok(backoff.Backoff, 'backoff.Backoff should be defined.');
test.equal(typeof backoff.Backoff, 'function',
'backoff.Backoff should be a function.');
test.done();
},
"backoff.FunctionCall should be defined and a function": function(test) {
test.ok(backoff.FunctionCall,
'backoff.FunctionCall should be defined.');
test.equal(typeof backoff.FunctionCall, 'function',
'backoff.FunctionCall should be a function.');
test.done();
},
"backoff.FibonacciStrategy should be defined and a function": function(test) {
test.ok(backoff.FibonacciStrategy,
'backoff.FibonacciStrategy should be defined.');
test.equal(typeof backoff.FibonacciStrategy, 'function',
'backoff.FibonacciStrategy should be a function.');
test.done();
},
"backoff.ExponentialStrategy should be defined and a function": function(test) {
test.ok(backoff.ExponentialStrategy,
'backoff.ExponentialStrategy should be defined.');
test.equal(typeof backoff.ExponentialStrategy, 'function',
'backoff.ExponentialStrategy should be a function.');
test.done();
}
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var sinon = require('sinon');
var Backoff = require('../lib/backoff');
var BackoffStrategy = require('../lib/strategy/strategy');
exports["Backoff"] = {
setUp: function(callback) {
this.backoffStrategy = sinon.stub(new BackoffStrategy());
this.backoff = new Backoff(this.backoffStrategy);
this.clock = sinon.useFakeTimers();
this.spy = new sinon.spy();
callback();
},
tearDown: function(callback) {
this.clock.restore();
callback();
},
"the backoff event should be emitted when backoff starts": function(test) {
this.backoffStrategy.next.returns(10);
this.backoff.on('backoff', this.spy);
this.backoff.backoff();
test.ok(this.spy.calledOnce,
'Backoff event should be emitted when backoff starts.');
test.done();
},
"the ready event should be emitted on backoff completion": function(test) {
this.backoffStrategy.next.returns(10);
this.backoff.on('ready', this.spy);
this.backoff.backoff();
this.clock.tick(10);
test.ok(this.spy.calledOnce,
'Ready event should be emitted when backoff ends.');
test.done();
},
"the backoff event should be passed the backoff delay": function(test) {
this.backoffStrategy.next.returns(989);
this.backoff.on('backoff', this.spy);
this.backoff.backoff();
test.equal(this.spy.getCall(0).args[1], 989, 'Backoff event should ' +
'carry the backoff delay as its second argument.');
test.done();
},
"the ready event should be passed the backoff delay": function(test) {
this.backoffStrategy.next.returns(989);
this.backoff.on('ready', this.spy);
this.backoff.backoff();
this.clock.tick(989);
test.equal(this.spy.getCall(0).args[1], 989, 'Ready event should ' +
'carry the backoff delay as its second argument.');
test.done();
},
"the fail event should be emitted when backoff limit is reached": function(test) {
var err = new Error('Fail');
this.backoffStrategy.next.returns(10);
this.backoff.on('fail', this.spy);
this.backoff.failAfter(2);
// Consume first 2 backoffs.
for (var i = 0; i < 2; i++) {
this.backoff.backoff();
this.clock.tick(10);
}
// Failure should occur on the third call, and not before.
test.ok(!this.spy.calledOnce, 'Fail event shouldn\'t have been emitted.');
this.backoff.backoff(err);
test.ok(this.spy.calledOnce, 'Fail event should have been emitted.');
test.equal(this.spy.getCall(0).args[0], err, 'Error should be passed');
test.done();
},
"calling backoff while a backoff is in progress should throw an error": function(test) {
this.backoffStrategy.next.returns(10);
var backoff = this.backoff;
backoff.backoff();
test.throws(function() {
backoff.backoff();
}, /in progress/);
test.done();
},
"backoff limit should be greater than 0": function(test) {
var backoff = this.backoff;
test.throws(function() {
backoff.failAfter(0);
}, /must be greater than 0/);
test.done();
},
"reset should cancel any backoff in progress": function(test) {
this.backoffStrategy.next.returns(10);
this.backoff.on('ready', this.spy);
this.backoff.backoff();
this.backoff.reset();
this.clock.tick(100); // 'ready' should not be emitted.
test.equals(this.spy.callCount, 0, 'Reset should have aborted the backoff.');
test.done();
},
"reset should reset the backoff strategy": function(test) {
this.backoff.reset();
test.ok(this.backoffStrategy.reset.calledOnce,
'The backoff strategy should have been resetted.');
test.done();
},
"backoff should be reset after fail": function(test) {
this.backoffStrategy.next.returns(10);
this.backoff.failAfter(1);
this.backoff.backoff();
this.clock.tick(10);
this.backoff.backoff();
test.ok(this.backoffStrategy.reset.calledOnce,
'Backoff should have been resetted after failure.');
test.done();
},
"the backoff number should increase from 0 to N - 1": function(test) {
this.backoffStrategy.next.returns(10);
this.backoff.on('backoff', this.spy);
var expectedNumbers = [0, 1, 2, 3, 4];
var actualNumbers = [];
for (var i = 0; i < expectedNumbers.length; i++) {
this.backoff.backoff();
this.clock.tick(10);
actualNumbers.push(this.spy.getCall(i).args[0]);
}
test.deepEqual(expectedNumbers, actualNumbers,
'Backoff number should increase from 0 to N - 1.');
test.done();
}
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var sinon = require('sinon');
var util = require('util');
var BackoffStrategy = require('../lib/strategy/strategy');
function SampleBackoffStrategy(options) {
BackoffStrategy.call(this, options);
}
util.inherits(SampleBackoffStrategy, BackoffStrategy);
SampleBackoffStrategy.prototype.next_ = function() {
return this.getInitialDelay();
};
SampleBackoffStrategy.prototype.reset_ = function() {};
exports["BackoffStrategy"] = {
setUp: function(callback) {
this.random = sinon.stub(Math, 'random');
callback();
},
tearDown: function(callback) {
this.random.restore();
callback();
},
"the randomisation factor should be between 0 and 1": function(test) {
test.throws(function() {
new BackoffStrategy({
randomisationFactor: -0.1
});
});
test.throws(function() {
new BackoffStrategy({
randomisationFactor: 1.1
});
});
test.doesNotThrow(function() {
new BackoffStrategy({
randomisationFactor: 0.5
});
});
test.done();
},
"the raw delay should be randomized based on the randomisation factor": function(test) {
var strategy = new SampleBackoffStrategy({
randomisationFactor: 0.5,
initialDelay: 1000
});
this.random.returns(0.5);
var backoffDelay = strategy.next();
test.equals(backoffDelay, 1000 + (1000 * 0.5 * 0.5));
test.done();
},
"the initial backoff delay should be greater than 0": function(test) {
test.throws(function() {
new BackoffStrategy({
initialDelay: -1
});
});
test.throws(function() {
new BackoffStrategy({
initialDelay: 0
});
});
test.doesNotThrow(function() {
new BackoffStrategy({
initialDelay: 1
});
});
test.done();
},
"the maximal backoff delay should be greater than 0": function(test) {
test.throws(function() {
new BackoffStrategy({
maxDelay: -1
});
});
test.throws(function() {
new BackoffStrategy({
maxDelay: 0
});
});
test.done();
},
"the maximal backoff delay should be greater than the initial backoff delay": function(test) {
test.throws(function() {
new BackoffStrategy({
initialDelay: 10,
maxDelay: 10
});
});
test.doesNotThrow(function() {
new BackoffStrategy({
initialDelay: 10,
maxDelay: 11
});
});
test.done();
}
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var sinon = require('sinon');
var ExponentialBackoffStrategy = require('../lib/strategy/exponential');
exports["ExponentialBackoffStrategy"] = {
setUp: function(callback) {
this.strategy = new ExponentialBackoffStrategy({
initialDelay: 10,
maxDelay: 1000
});
callback();
},
"backoff delays should follow an exponential sequence": function(test) {
// Exponential sequence: x[i] = x[i-1] * 2.
var expectedDelays = [10, 20, 40, 80, 160, 320, 640, 1000, 1000];
var actualDelays = [];
for (var i = 0; i < expectedDelays.length; i++) {
actualDelays.push(this.strategy.next());
}
test.deepEqual(expectedDelays, actualDelays,
'Generated delays should follow an exponential sequence.');
test.done();
},
"backoff delays should restart from the initial delay after reset": function(test) {
var strategy = new ExponentialBackoffStrategy({
initialDelay: 10,
maxDelay: 1000
});
strategy.next();
strategy.reset();
var backoffDelay = strategy.next();
test.equals(backoffDelay, 10,
'Strategy should return the initial delay after reset.');
test.done();
}
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var sinon = require('sinon');
var FibonacciBackoffStrategy = require('../lib/strategy/fibonacci');
exports["FibonacciBackoffStrategy"] = {
setUp: function(callback) {
this.strategy = new FibonacciBackoffStrategy({
initialDelay: 10,
maxDelay: 1000
});
callback();
},
"backoff delays should follow a Fibonacci sequence": function(test) {
// Fibonacci sequence: x[i] = x[i-1] + x[i-2].
var expectedDelays = [10, 10, 20, 30, 50, 80, 130, 210, 340, 550, 890, 1000];
var actualDelays = [];
for (var i = 0; i < expectedDelays.length; i++) {
actualDelays.push(this.strategy.next());
}
test.deepEqual(expectedDelays, actualDelays,
'Generated delays should follow a Fibonacci sequence.');
test.done();
},
"backoff delays should restart from the initial delay after reset": function(test) {
var strategy = new FibonacciBackoffStrategy({
initialDelay: 10,
maxDelay: 1000
});
strategy.next();
strategy.reset();
var backoffDelay = strategy.next();
test.equals(backoffDelay, 10,
'Strategy should return the initial delay after reset.');
test.done();
}
};
/*
* Copyright (c) 2012 Mathieu Turcotte
* Licensed under the MIT license.
*/
var assert = require('assert');
var events = require('events');
var sinon = require('sinon');
var util = require('util');
var FunctionCall = require('../lib/function_call');
function MockBackoff() {
events.EventEmitter.call(this);
this.reset = sinon.spy();
this.backoff = sinon.spy();
this.failAfter = sinon.spy();
}
util.inherits(MockBackoff, events.EventEmitter);
exports["FunctionCall"] = {
setUp: function(callback) {
this.wrappedFn = sinon.stub();
this.callback = sinon.stub();
this.backoff = new MockBackoff();
this.backoffFactory = sinon.stub();
this.backoffFactory.returns(this.backoff);
callback();
},
tearDown: function(callback) {
callback();
},
"constructor's first argument should be a function": function(test) {
test.throws(function() {
new FunctionCall(1, [], function() {});
}, /should be a function/);
test.done();
},
"constructor's last argument should be a function": function(test) {
test.throws(function() {
new FunctionCall(function() {}, [], 3);
}, /should be a function/);
test.done();
},
"isPending should return false once the call is started": function(test) {
this.wrappedFn.yields(new Error()).yields(null, 'Success!');
var call = new FunctionCall(this.wrappedFn, [], this.callback);
test.ok(call.isPending());
call.start(this.backoffFactory);
test.ok(!call.isPending());
this.backoff.emit('ready');
test.ok(!call.isPending());
test.done();
},
"isRunning should return true when call is in progress": function(test) {
this.wrappedFn.yields(new Error()).yields(null, 'Success!');
var call = new FunctionCall(this.wrappedFn, [], this.callback);
test.ok(!call.isRunning());
call.start(this.backoffFactory);
test.ok(call.isRunning());
this.backoff.emit('ready');
test.ok(!call.isRunning());
test.done();
},
"isCompleted should return true once the call completes": function(test) {
this.wrappedFn.yields(new Error()).yields(null, 'Success!');
var call = new FunctionCall(this.wrappedFn, [], this.callback);
test.ok(!call.isCompleted());
call.start(this.backoffFactory);
test.ok(!call.isCompleted());
this.backoff.emit('ready');
test.ok(call.isCompleted());
test.done();
},
"isAborted should return true once the call is aborted": function(test) {
this.wrappedFn.yields(new Error()).yields(null, 'Success!');
var call = new FunctionCall(this.wrappedFn, [], this.callback);
test.ok(!call.isAborted());
call.abort();
test.ok(call.isAborted());
test.done();
},
"setStrategy should overwrite the default strategy": function(test) {
var replacementStrategy = {};
var call = new FunctionCall(this.wrappedFn, [], this.callback);
call.setStrategy(replacementStrategy);
call.start(this.backoffFactory);
test.ok(this.backoffFactory.calledWith(replacementStrategy),
'User defined strategy should be used to instantiate ' +
'the backoff instance.');
test.done();
},
"setStrategy should throw if the call is in progress": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
call.start(this.backoffFactory);
test.throws(function() {
call.setStrategy({});
}, /in progress/);
test.done();
},
"failAfter should not be set by default": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
call.start(this.backoffFactory);
test.equal(0, this.backoff.failAfter.callCount);
test.done();
},
"failAfter should be used as the maximum number of backoffs": function(test) {
var failAfterValue = 99;
var call = new FunctionCall(this.wrappedFn, [], this.callback);
call.failAfter(failAfterValue);
call.start(this.backoffFactory);
test.ok(this.backoff.failAfter.calledWith(failAfterValue),
'User defined maximum number of backoffs shoud be ' +
'used to configure the backoff instance.');
test.done();
},
"failAfter should throw if the call is in progress": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
call.start(this.backoffFactory);
test.throws(function() {
call.failAfter(1234);
}, /in progress/);
test.done();
},
"start shouldn't allow overlapping invocation": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
var backoffFactory = this.backoffFactory;
call.start(backoffFactory);
test.throws(function() {
call.start(backoffFactory);
}, /already started/);
test.done();
},
"start shouldn't allow invocation of aborted call": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
var backoffFactory = this.backoffFactory;
call.abort();
test.throws(function() {
call.start(backoffFactory);
}, /aborted/);
test.done();
},
"call should forward its arguments to the wrapped function": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
call.start(this.backoffFactory);
test.ok(this.wrappedFn.calledWith(1, 2, 3));
test.done();
},
"call should complete when the wrapped function succeeds": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
this.wrappedFn.yields(new Error())
.yields(new Error())
.yields(new Error())
.yields(null, 'Success!');
call.start(this.backoffFactory);
for (var i = 0; i < 2; i++) {
this.backoff.emit('ready');
}
test.equals(this.callback.callCount, 0);
this.backoff.emit('ready');
test.ok(this.callback.calledWith(null, 'Success!'));
test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
test.done();
},
"call should fail when the backoff limit is reached": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
var error = new Error();
this.wrappedFn.yields(error);
call.start(this.backoffFactory);
for (var i = 0; i < 3; i++) {
this.backoff.emit('ready');
}
test.equals(this.callback.callCount, 0);
this.backoff.emit('fail');
test.ok(this.callback.calledWith(error));
test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
test.done();
},
"wrapped function's callback shouldn't be called after abort": function(test) {
var call = new FunctionCall(function(callback) {
call.abort(); // Abort in middle of wrapped function's execution.
callback(null, 'ok');
}, [], this.callback);
call.start(this.backoffFactory);
test.equals(this.callback.callCount, 0,
'Wrapped function\'s callback shouldn\'t be called after abort.');
test.done();
},
"getResults should return intermediary results": function(test) {
var call = new FunctionCall(this.wrappedFn, [], this.callback);
this.wrappedFn.yields(1);
call.start(this.backoffFactory);
for (var i = 2; i < 5; i++) {
this.wrappedFn.yields(i);
this.backoff.emit('ready');
}
this.wrappedFn.yields(null);
this.backoff.emit('ready');
test.deepEqual([[1], [2], [3], [4], [null]], call.getResults());
test.done();
},
"wrapped function's errors should be propagated": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
this.wrappedFn.throws(new Error());
test.throws(function() {
call.start(this.backoffFactory);
}, Error);
test.done();
},
"wrapped callback's errors should be propagated": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
this.wrappedFn.yields(null, 'Success!');
this.callback.throws(new Error());
test.throws(function() {
call.start(this.backoffFactory);
}, Error);
test.done();
},
"call event should be emitted when wrapped function gets called": function(test) {
this.wrappedFn.yields(1);
var callEventSpy = sinon.spy();
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
call.on('call', callEventSpy);
call.start(this.backoffFactory);
for (var i = 1; i < 5; i++) {
this.backoff.emit('ready');
}
test.equal(5, callEventSpy.callCount,
'The call event should have been emitted 5 times.');
test.deepEqual([1, 'two'], callEventSpy.getCall(0).args,
'The call event should carry function\'s args.');
test.done();
},
"callback event should be emitted when callback is called": function(test) {
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
var callbackSpy = sinon.spy();
call.on('callback', callbackSpy);
this.wrappedFn.yields('error');
call.start(this.backoffFactory);
this.wrappedFn.yields(null, 'done');
this.backoff.emit('ready');
test.equal(2, callbackSpy.callCount,
'Callback event should have been emitted 2 times.');
test.deepEqual(['error'], callbackSpy.firstCall.args,
'First callback event should carry first call\'s results.');
test.deepEqual([null, 'done'], callbackSpy.secondCall.args,
'Second callback event should carry second call\'s results.');
test.done();
},
"backoff event should be emitted on backoff start": function(test) {
var err = new Error('backoff event error');
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
var backoffSpy = sinon.spy();
call.on('backoff', backoffSpy);
this.wrappedFn.yields(err);
call.start(this.backoffFactory);
this.backoff.emit('backoff', 3, 1234, err);
test.ok(this.backoff.backoff.calledWith(err),
'The backoff instance should have been called with the error.');
test.equal(1, backoffSpy.callCount,
'Backoff event should have been emitted 1 time.');
test.deepEqual([3, 1234, err], backoffSpy.firstCall.args,
'Backoff event should carry the backoff number, delay and error.');
test.done();
}
};
Trent Mick <trentm@gmail.com> (http://trentm.com)
Mark Cavage <mcavage@gmail.com> (https://github.com/mcavage)
Dave Pacheco <dap@joyent.com> (https://github.com/davepacheco)
Michael Hart (https://github.com/mhart)
Isaac Schlueter (https://github.com/isaacs)
Rob Gulewich (https://github.com/rgulewich)
Bryan Cantrill (https://github.com/bcantrill)
#!/usr/bin/env node
// -*- mode: js -*-
//
// bunyan -- filter and pretty-print JSON logs, like Bunyan logs.
//
// See <https://github.com/trentm/node-bunyan>.
//
var VERSION = '0.22.1';
var p = console.log;
var util = require('util');
var pathlib = require('path');
var vm = require('vm');
var http = require('http');
var fs = require('fs');
var warn = console.warn;
var child_process = require('child_process'),
spawn = child_process.spawn,
exec = child_process.exec,
execFile = child_process.execFile;
var assert = require('assert');
var nodeSpawnSupportsStdio = (
Number(process.version.split('.')[0]) >= 0 ||
Number(process.version.split('.')[1]) >= 8);
//---- globals and constants
// Internal debug logging via `console.warn`.
var _DEBUG = false;
// Output modes.
var OM_LONG = 1;
var OM_JSON = 2;
var OM_INSPECT = 3;
var OM_SIMPLE = 4;
var OM_SHORT = 5;
var OM_BUNYAN = 6;
var OM_FROM_NAME = {
'long': OM_LONG,
'paul': OM_LONG, /* backward compat */
'json': OM_JSON,
'inspect': OM_INSPECT,
'simple': OM_SIMPLE,
'short': OM_SHORT,
'bunyan': OM_BUNYAN
};
// Levels
var TRACE = 10;
var DEBUG = 20;
var INFO = 30;
var WARN = 40;
var ERROR = 50;
var FATAL = 60;
var levelFromName = {
'trace': TRACE,
'debug': DEBUG,
'info': INFO,
'warn': WARN,
'error': ERROR,
'fatal': FATAL
};
var nameFromLevel = {};
var upperNameFromLevel = {};
var upperPaddedNameFromLevel = {};
Object.keys(levelFromName).forEach(function (name) {
var lvl = levelFromName[name];
nameFromLevel[lvl] = name;
upperNameFromLevel[lvl] = name.toUpperCase();
upperPaddedNameFromLevel[lvl] = (
name.length === 4 ? ' ' : '') + name.toUpperCase();
});
// The current raw input line being processed. Used for `uncaughtException`.
var currLine = null;
// Child dtrace process, if any. Used for signal-handling.
var child = null;
// Whether ANSI codes are being used. Used for signal-handling.
var usingAnsiCodes = false;
// Global ref to options used only by 'uncaughtException' handler.
var gOptsForUncaughtException;
// Pager child process, and output stream to which to write.
var pager = null;
var stdout = process.stdout;
//---- support functions
function getVersion() {
return VERSION;
}
var format = util.format;
if (!format) {
/* BEGIN JSSTYLED */
// If not node 0.6, then use its `util.format`:
// <https://github.com/joyent/node/blob/master/lib/util.js#L22>:
var inspect = util.inspect;
var formatRegExp = /%[sdj%]/g;
format = function format(f) {
if (typeof f !== 'string') {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function (x) {
if (i >= len)
return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j': return JSON.stringify(args[i++]);
case '%%': return '%';
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (x === null || typeof x !== 'object') {
str += ' ' + x;
} else {
str += ' ' + inspect(x);
}
}
return str;
};
/* END JSSTYLED */
}
function indent(s) {
return ' ' + s.split(/\r?\n/).join('\n ');
}
function objCopy(obj) {
if (obj === null) {
return null;
} else if (Array.isArray(obj)) {
return obj.slice();
} else {
var copy = {};
Object.keys(obj).forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
}
function printHelp() {
/* BEGIN JSSTYLED */
p('Usage:');
p(' bunyan [OPTIONS] [FILE ...]');
p(' ... | bunyan [OPTIONS]');
p(' bunyan [OPTIONS] -p PID');
p('');
p('Filter and pretty-print Bunyan log file content.');
p('');
p('General options:');
p(' -h, --help print this help info and exit');
p(' --version print version of this command and exit');
p('');
p('Runtime log snooping (via DTrace, only on supported platforms):');
p(' -p PID Process bunyan:log-* probes from the process');
p(' with the given PID. Can be used multiple times,');
p(' or specify all processes with "*", or a set of');
p(' processes whose command & args match a pattern');
p(' with "-p NAME".');
p('');
p('Filtering options:');
p(' -l, --level LEVEL');
p(' Only show messages at or above the specified level.');
p(' You can specify level *names* or numeric values.');
p(' (See "Log Levels" below.)');
p(' -c, --condition CONDITION');
p(' Run each log message through the condition and');
p(' only show those that return truish. E.g.:');
p(' -c \'this.pid == 123\'');
p(' -c \'this.level == DEBUG\'');
p(' -c \'this.msg.indexOf("boom") != -1\'');
p(' "CONDITION" must be legal JS code. `this` holds');
p(' the log record. The TRACE, DEBUG, ... FATAL values');
p(' are defined to help with comparing `this.level`.');
p(' --strict Suppress all but legal Bunyan JSON log lines. By default');
p(' non-JSON, and non-Bunyan lines are passed through.');
p('');
p('Output options:');
p(' --pager Pipe output into `less` (or $PAGER if set), if');
p(' stdout is a TTY. This overrides $BUNYAN_NO_PAGER.');
p(' Note: Paging is only supported on node >=0.8.');
p(' --no-pager Do not pipe output into a pager.');
p(' --color Colorize output. Defaults to try if output');
p(' stream is a TTY.');
p(' --no-color Force no coloring (e.g. terminal doesn\'t support it)');
p(' -o, --output MODE');
p(' Specify an output mode/format. One of');
p(' long: (the default) pretty');
p(' json: JSON output, 2-space indent');
p(' json-N: JSON output, N-space indent, e.g. "json-4"');
p(' bunyan: 0 indented JSON, bunyan\'s native format');
p(' inspect: node.js `util.inspect` output');
p(' short: like "long", but more concise');
p(' -j shortcut for `-o json`');
p('');
p('Log Levels:');
p(' Either numeric values or their associated strings are valid for the');
p(' -l|--level argument. However, -c|--condition scripts will see a numeric');
p(' "level" value, not a string.');
p('');
Object.keys(levelFromName).forEach(function (name) {
var n = name;
while (n.length < 6)
n += ' ';
p(' %s %d', n, levelFromName[name]);
});
p('');
p('Environment Variables:');
p(' BUNYAN_NO_COLOR Set to a non-empty value to force no output ');
p(' coloring. See "--no-color".');
p(' BUNYAN_NO_PAGER Disable piping output to a pager. ');
p(' See "--no-pager".');
p('');
p('See <https://github.com/trentm/node-bunyan> for more complete docs.');
p('Please report bugs to <https://github.com/trentm/node-bunyan/issues>.');
/* END JSSTYLED */
}
/*
* If the user specifies multiple input sources, we want to print out records
* from all sources in a single, chronologically ordered stream. To do this
* efficiently, we first assume that all records within each source are ordered
* already, so we need only keep track of the next record in each source and
* the time of the last record emitted. To avoid excess memory usage, we
* pause() streams that are ahead of others.
*
* 'streams' is an object indexed by source name (file name) which specifies:
*
* stream Actual stream object, so that we can pause and resume it.
*
* records Array of log records we've read, but not yet emitted. Each
* record includes 'line' (the raw line), 'rec' (the JSON
* record), and 'time' (the parsed time value).
*
* done Whether the stream has any more records to emit.
*/
var streams = {};
function gotRecord(file, line, rec, opts, stylize)
{
var time = new Date(rec.time);
streams[file]['records'].push({ line: line, rec: rec, time: time });
emitNextRecord(opts, stylize);
}
function filterRecord(rec, opts)
{
if (opts.level && rec.level < opts.level) {
return false;
}
if (opts.conditions) {
for (var i = 0; i < opts.conditions.length; i++) {
var pass = opts.conditions[i].runInNewContext(rec);
if (!pass)
return false;
}
}
return true;
}
function emitNextRecord(opts, stylize)
{
var ofile, ready, minfile, rec;
for (;;) {
/*
* Take a first pass through the input streams to see if we have a
* record from all of them. If not, we'll pause any streams for
* which we do already have a record (to avoid consuming excess
* memory) and then wait until we have records from the others
* before emitting the next record.
*
* As part of the same pass, we look for the earliest record
* we have not yet emitted.
*/
minfile = undefined;
ready = true;
for (ofile in streams) {
if (streams[ofile].stream === null ||
(!streams[ofile].done && streams[ofile].records.length === 0)) {
ready = false;
break;
}
if (streams[ofile].records.length > 0 &&
(minfile === undefined ||
streams[minfile].records[0].time >
streams[ofile].records[0].time)) {
minfile = ofile;
}
}
if (!ready || minfile === undefined) {
for (ofile in streams) {
if (!streams[ofile].stream || streams[ofile].done)
continue;
if (streams[ofile].records.length > 0) {
if (!streams[ofile].paused) {
streams[ofile].paused = true;
streams[ofile].stream.pause();
}
} else if (streams[ofile].paused) {
streams[ofile].paused = false;
streams[ofile].stream.resume();
}
}
return;
}
/*
* Emit the next record for 'minfile', and invoke ourselves again to
* make sure we emit as many records as we can right now.
*/
rec = streams[minfile].records.shift();
emitRecord(rec.rec, rec.line, opts, stylize);
}
}
/**
* Parse the command-line options and arguments into an object.
*
* {
* 'args': [...] // arguments
* 'help': true, // true if '-h' option given
* // etc.
* }
*
* @return {Object} The parsed options. `.args` is the argument list.
* @throws {Error} If there is an error parsing argv.
*/
function parseArgv(argv) {
var parsed = {
args: [],
help: false,
color: null,
paginate: null,
outputMode: OM_LONG,
jsonIndent: 2,
level: null,
conditions: null,
strict: false,
pids: null,
pidsType: null
};
// Turn '-iH' into '-i -H', except for argument-accepting options.
var args = argv.slice(2); // drop ['node', 'scriptname']
var newArgs = [];
var optTakesArg = {'d': true, 'o': true, 'c': true, 'l': true, 'p': true};
for (var i = 0; i < args.length; i++) {
if (args[i].charAt(0) === '-' && args[i].charAt(1) !== '-' &&
args[i].length > 2)
{
var splitOpts = args[i].slice(1).split('');
for (var j = 0; j < splitOpts.length; j++) {
newArgs.push('-' + splitOpts[j]);
if (optTakesArg[splitOpts[j]]) {
var optArg = splitOpts.slice(j+1).join('');
if (optArg.length) {
newArgs.push(optArg);
}
break;
}
}
} else {
newArgs.push(args[i]);
}
}
args = newArgs;
var condDefines = [];
Object.keys(upperNameFromLevel).forEach(function (lvl) {
condDefines.push(
format('Object.prototype.%s = %s;', upperNameFromLevel[lvl], lvl));
});
condDefines = condDefines.join('\n') + '\n';
var endOfOptions = false;
while (args.length > 0) {
var arg = args.shift();
switch (arg) {
case '--':
endOfOptions = true;
break;
case '-h': // display help and exit
case '--help':
parsed.help = true;
break;
case '--version':
parsed.version = true;
break;
case '--strict':
parsed.strict = true;
break;
case '--color':
parsed.color = true;
break;
case '--no-color':
parsed.color = false;
break;
case '--pager':
parsed.paginate = true;
break;
case '--no-pager':
parsed.paginate = false;
break;
case '-o':
case '--output':
var name = args.shift();
var idx = name.lastIndexOf('-');
if (idx !== -1) {
var indentation = Number(name.slice(idx+1));
if (! isNaN(indentation)) {
parsed.jsonIndent = indentation;
name = name.slice(0, idx);
}
}
parsed.outputMode = OM_FROM_NAME[name];
if (parsed.outputMode === undefined) {
throw new Error('unknown output mode: "'+name+'"');
}
break;
case '-j': // output with JSON.stringify
parsed.outputMode = OM_JSON;
break;
case '-p':
if (!parsed.pids) {
parsed.pids = [];
}
var pidArg = args.shift();
var pid = +(pidArg);
if (!isNaN(pid) || pidArg === '*') {
if (parsed.pidsType && parsed.pidsType !== 'num') {
throw new Error(format('cannot mix PID name and '
+ 'number arguments: "%s"', pidArg));
}
parsed.pidsType = 'num';
if (!parsed.pids) {
parsed.pids = [];
}
parsed.pids.push(isNaN(pid) ? pidArg : pid);
} else {
if (parsed.pidsType && parsed.pidsType !== 'name') {
throw new Error(format('cannot mix PID name and '
+ 'number arguments: "%s"', pidArg));
}
parsed.pidsType = 'name';
parsed.pids = pidArg;
}
break;
case '-l':
case '--level':
var levelArg = args.shift();
var level = +(levelArg);
if (isNaN(level)) {
level = +levelFromName[levelArg.toLowerCase()];
}
if (isNaN(level)) {
throw new Error('unknown level value: "'+levelArg+'"');
}
parsed.level = level;
break;
case '-c':
case '--condition':
var condition = args.shift();
parsed.conditions = parsed.conditions || [];
var scriptName = 'bunyan-condition-'+parsed.conditions.length;
var code = condDefines + condition;
try {
var script = vm.createScript(code, scriptName);
} catch (compileErr) {
throw new Error(format('illegal CONDITION code: %s\n'
+ ' CONDITION script:\n'
+ '%s\n'
+ ' Error:\n'
+ '%s',
compileErr, indent(code), indent(compileErr.stack)));
}
// Ensure this is a reasonably safe CONDITION.
try {
script.runInNewContext(minValidRecord);
} catch (condErr) {
throw new Error(format(
/* JSSTYLED */
'CONDITION code cannot safely filter a minimal Bunyan log record\n'
+ ' CONDITION script:\n'
+ '%s\n'
+ ' Minimal Bunyan log record:\n'
+ '%s\n'
+ ' Filter error:\n'
+ '%s',
indent(code),
indent(JSON.stringify(minValidRecord, null, 2)),
indent(condErr.stack)
));
}
parsed.conditions.push(script);
break;
default: // arguments
if (!endOfOptions && arg.length > 0 && arg[0] === '-') {
throw new Error('unknown option "'+arg+'"');
}
parsed.args.push(arg);
break;
}
}
//TODO: '--' handling and error on a first arg that looks like an option.
return parsed;
}
function isInteger(s) {
return (s.search(/^-?[0-9]+$/) == 0);
}
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
// Suggested colors (some are unreadable in common cases):
// - Good: cyan, yellow (limited use), grey, bold, green, magenta, red
// - Bad: blue (not visible on cmd.exe)
var colors = {
'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39]
};
function stylizeWithColor(str, color) {
if (!str)
return '';
var codes = colors[color];
if (codes) {
return '\033[' + codes[0] + 'm' + str +
'\033[' + codes[1] + 'm';
} else {
return str;
}
}
function stylizeWithoutColor(str, color) {
return str;
}
/**
* Is this a valid Bunyan log record.
*/
function isValidRecord(rec) {
if (rec.v == null ||
rec.level == null ||
rec.name == null ||
rec.hostname == null ||
rec.pid == null ||
rec.time == null ||
rec.msg == null) {
// Not valid Bunyan log.
return false;
} else {
return true;
}
}
var minValidRecord = {
v: 0, //TODO: get this from bunyan.LOG_VERSION
level: INFO,
name: 'name',
hostname: 'hostname',
pid: 123,
time: Date.now(),
msg: 'msg'
};
/**
* Parses the given log line and either emits it right away (for invalid
* records) or enqueues it for emitting later when it's the next line to show.
*/
function handleLogLine(file, line, opts, stylize) {
currLine = line; // intentionally global
// Emit non-JSON lines immediately.
var rec;
if (!line) {
if (!opts.strict) emit(line + '\n');
return;
} else if (line[0] !== '{') {
if (!opts.strict) emit(line + '\n'); // not JSON
return;
} else {
try {
rec = JSON.parse(line);
} catch (e) {
if (!opts.strict) emit(line + '\n');
return;
}
}
if (!isValidRecord(rec)) {
if (!opts.strict) emit(line + '\n');
return;
}
if (!filterRecord(rec, opts))
return;
if (file === null)
return emitRecord(rec, line, opts, stylize);
return gotRecord(file, line, rec, opts, stylize);
}
/**
* Print out a single result, considering input options.
*/
function emitRecord(rec, line, opts, stylize) {
var short = false;
switch (opts.outputMode) {
case OM_SHORT:
short = true;
/* jsl:fall-thru */
case OM_LONG:
// [time] LEVEL: name[/comp]/pid on hostname (src): msg* (extras...)
// msg*
// --
// long and multi-line extras
// ...
// If 'msg' is single-line, then it goes in the top line.
// If 'req', show the request.
// If 'res', show the response.
// If 'err' and 'err.stack' then show that.
if (!isValidRecord(rec)) {
return emit(line + '\n');
}
delete rec.v;
/*
* We assume the Date is formatted according to ISO8601, in which
* case we can safely chop off the date information.
*/
if (short && rec.time[10] == 'T') {
var time = rec.time.substr(11);
time = stylize(time, 'XXX');
} else {
var time = stylize('[' + rec.time + ']', 'XXX');
}
delete rec.time;
var nameStr = rec.name;
delete rec.name;
if (rec.component) {
nameStr += '/' + rec.component;
}
delete rec.component;
if (!short)
nameStr += '/' + rec.pid;
delete rec.pid;
var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level);
if (opts.color) {
var colorFromLevel = {
10: 'grey', // TRACE
20: 'grey', // DEBUG
30: 'cyan', // INFO
40: 'magenta', // WARN
50: 'red', // ERROR
60: 'inverse', // FATAL
};
level = stylize(level, colorFromLevel[rec.level]);
}
delete rec.level;
var src = '';
if (rec.src && rec.src.file) {
var s = rec.src;
if (s.func) {
src = format(' (%s:%d in %s)', s.file, s.line, s.func);
} else {
src = format(' (%s:%d)', s.file, s.line);
}
src = stylize(src, 'green');
}
delete rec.src;
var hostname = rec.hostname;
delete rec.hostname;
var extras = [];
var details = [];
if (rec.req_id) {
extras.push('req_id=' + rec.req_id);
}
delete rec.req_id;
var onelineMsg;
if (rec.msg.indexOf('\n') !== -1) {
onelineMsg = '';
details.push(indent(stylize(rec.msg, 'cyan')));
} else {
onelineMsg = ' ' + stylize(rec.msg, 'cyan');
}
delete rec.msg;
if (rec.req && typeof (rec.req) === 'object') {
var req = rec.req;
delete rec.req;
var headers = req.headers;
var s = format('%s %s HTTP/%s%s', req.method,
req.url,
req.httpVersion || '1.1',
(headers ?
'\n' + Object.keys(headers).map(function (h) {
return h + ': ' + headers[h];
}).join('\n') :
'')
);
delete req.url;
delete req.method;
delete req.httpVersion;
delete req.headers;
if (req.body) {
s += '\n\n' + (typeof (req.body) === 'object'
? JSON.stringify(req.body, null, 2) : req.body);
delete req.body;
}
if (req.trailers && Object.keys(req.trailers) > 0) {
s += '\n' + Object.keys(req.trailers).map(function (t) {
return t + ': ' + req.trailers[t];
}).join('\n');
}
delete req.trailers;
details.push(indent(s));
// E.g. for extra 'foo' field on 'req', add 'req.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'req.foo' key.
Object.keys(req).forEach(function (k) {
rec['req.' + k] = req[k];
})
}
if (rec.client_req && typeof (rec.client_req) === 'object') {
var client_req = rec.client_req;
delete rec.client_req;
var headers = client_req.headers;
var hostHeaderLine = '';
var s = '';
if (client_req.address) {
hostHeaderLine = 'Host: ' + client_req.address;
if (client_req.port)
hostHeaderLine += ':' + client_req.port;
hostHeaderLine += '\n';
}
delete client_req.headers;
delete client_req.address;
delete client_req.port;
s += format('%s %s HTTP/%s\n%s%s', client_req.method,
client_req.url,
client_req.httpVersion || '1.1',
hostHeaderLine,
(headers ?
Object.keys(headers).map(
function (h) {
return h + ': ' + headers[h];
}).join('\n') :
''));
delete client_req.method;
delete client_req.url;
delete client_req.httpVersion;
if (client_req.body) {
s += '\n\n' + (typeof (client_req.body) === 'object' ?
JSON.stringify(client_req.body, null, 2) :
client_req.body);
delete client_req.body;
}
// E.g. for extra 'foo' field on 'client_req', add
// 'client_req.foo' at top-level. This *does* have the potential
// to stomp on a literal 'client_req.foo' key.
Object.keys(client_req).forEach(function (k) {
rec['client_req.' + k] = client_req[k];
})
details.push(indent(s));
}
function _res(res) {
var s = '';
if (res.header) {
s += res.header.trimRight();
} else if (typeof(res.headers) === 'string') {
s += res.headers.trimRight();
} else if (res.headers) {
if (res.statusCode) {
s += format('HTTP/1.1 %s %s\n', res.statusCode,
http.STATUS_CODES[res.statusCode]);
}
var headers = res.headers;
s += Object.keys(headers).map(
function (h) { return h + ': ' + headers[h]; }).join('\n');
}
delete res.header;
delete res.headers;
delete res.statusCode;
if (res.body) {
s += '\n\n' + (typeof (res.body) === 'object'
? JSON.stringify(res.body, null, 2) : res.body);
delete res.body;
}
if (res.trailer) {
s += '\n' + res.trailer;
}
delete res.trailer;
if (s) {
details.push(indent(s));
}
// E.g. for extra 'foo' field on 'res', add 'res.foo' at
// top-level. This *does* have the potential to stomp on a
// literal 'res.foo' key.
Object.keys(res).forEach(function (k) {
rec['res.' + k] = res[k];
});
}
if (rec.res && typeof (rec.res) === 'object') {
_res(rec.res);
delete rec.res;
}
if (rec.client_res && typeof (rec.client_res) === 'object') {
_res(rec.client_res);
delete rec.res;
}
if (rec.err && rec.err.stack) {
details.push(indent(rec.err.stack));
delete rec.err;
}
var leftover = Object.keys(rec);
for (var i = 0; i < leftover.length; i++) {
var key = leftover[i];
var value = rec[key];
var stringified = false;
if (typeof (value) !== 'string') {
value = JSON.stringify(value, null, 2);
stringified = true;
}
if (value.indexOf('\n') !== -1 || value.length > 50) {
details.push(indent(key + ': ' + value));
} else if (!stringified && (value.indexOf(' ') != -1 ||
value.length === 0))
{
extras.push(key + '=' + JSON.stringify(value));
} else {
extras.push(key + '=' + value);
}
}
extras = stylize(
(extras.length ? ' (' + extras.join(', ') + ')' : ''), 'grey');
details = stylize(
(details.length ? details.join('\n --\n') + '\n' : ''), 'grey');
if (!short)
emit(format('%s %s: %s on %s%s:%s%s\n%s',
time,
level,
nameStr,
hostname || '<no-hostname>',
src,
onelineMsg,
extras,
details));
else
emit(format('%s %s %s:%s%s\n%s',
time,
level,
nameStr,
onelineMsg,
extras,
details));
break;
case OM_INSPECT:
emit(util.inspect(rec, false, Infinity, true) + '\n');
break;
case OM_BUNYAN:
emit(JSON.stringify(rec, null, 0) + '\n');
break;
case OM_JSON:
emit(JSON.stringify(rec, null, opts.jsonIndent) + '\n');
break;
case OM_SIMPLE:
/* JSSTYLED */
// <http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/SimpleLayout.html>
if (!isValidRecord(rec)) {
return emit(line + '\n');
}
emit(format('%s - %s\n',
upperNameFromLevel[rec.level] || 'LVL' + rec.level,
rec.msg));
break;
default:
throw new Error('unknown output mode: '+opts.outputMode);
}
}
var stdoutFlushed = true;
function emit(s) {
try {
stdoutFlushed = stdout.write(s);
} catch (e) {
// Handle any exceptions in stdout writing in `stdout.on('error', ...)`.
}
}
/**
* A hacked up version of 'process.exit' that will first drain stdout
* before exiting. *WARNING: This doesn't stop event processing.* IOW,
* callers have to be careful that code following this call isn't
* accidentally executed.
*
* In node v0.6 "process.stdout and process.stderr are blocking when they
* refer to regular files or TTY file descriptors." However, this hack might
* still be necessary in a shell pipeline.
*/
function drainStdoutAndExit(code) {
if (_DEBUG) warn('(drainStdoutAndExit(%d))', code);
stdout.on('drain', function () {
cleanupAndExit(code);
});
if (stdoutFlushed) {
cleanupAndExit(code);
}
}
/**
* Process all input from stdin.
*
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function ()`
*/
function processStdin(opts, stylize, callback) {
var leftover = ''; // Left-over partial line from last chunk.
var stdin = process.stdin;
stdin.resume();
stdin.setEncoding('utf8');
stdin.on('data', function (chunk) {
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(null, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(null, lines[i], opts, stylize);
}
});
stdin.on('end', function () {
if (leftover) {
handleLogLine(null, leftover, opts, stylize);
leftover = '';
}
callback();
});
}
/**
* Process bunyan:log-* probes from the given pid.
*
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function (code)`
*/
function processPids(opts, stylize, callback) {
var leftover = ''; // Left-over partial line from last chunk.
/**
* Get the PIDs to dtrace.
*
* @param cb {Function} `function (errCode, pids)`
*/
function getPids(cb) {
if (opts.pidsType === 'num') {
return cb(null, opts.pids);
}
if (process.platform === 'sunos') {
execFile('/bin/pgrep', ['-lf', opts.pids],
function (pidsErr, stdout, stderr) {
if (pidsErr) {
warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s',
opts.pids, pidsErr.message, stdout, stderr);
return cb(1);
}
var pids = stdout.trim().split('\n')
.map(function (line) {
return line.trim().split(/\s+/)[0]
})
.filter(function (pid) {
return Number(pid) !== process.pid
});
if (pids.length === 0) {
warn('bunyan: error: no matching PIDs found for "%s"',
opts.pids);
return cb(2);
}
cb(null, pids);
}
);
} else {
var regex = opts.pids;
if (regex && /[a-zA-Z0-9_]/.test(regex[0])) {
// 'foo' -> '[f]oo' trick to exclude the 'grep' PID from its
// own search.
regex = '[' + regex[0] + ']' + regex.slice(1);
}
exec(format('ps -A -o pid,command | grep \'%s\'', regex),
function (pidsErr, stdout, stderr) {
if (pidsErr) {
warn('bunyan: error getting PIDs for "%s": %s\n%s\n%s',
opts.pids, pidsErr.message, stdout, stderr);
return cb(1);
}
var pids = stdout.trim().split('\n')
.map(function (line) {
return line.trim().split(/\s+/)[0];
})
.filter(function (pid) {
return Number(pid) !== process.pid;
});
if (pids.length === 0) {
warn('bunyan: error: no matching PIDs found for "%s"',
opts.pids);
return cb(2);
}
cb(null, pids);
}
);
}
}
getPids(function (errCode, pids) {
if (errCode) {
return callback(errCode);
}
var probes = pids.map(function (pid) {
if (!opts.level)
return format('bunyan%s:::log-*', pid);
var rval = [], l;
for (l in levelFromName) {
if (levelFromName[l] >= opts.level)
rval.push(format('bunyan%s:::log-%s', pid, l));
}
if (rval.length != 0)
return rval.join(',');
warn('bunyan: error: level (%d) exceeds maximum logging level',
opts.level);
return drainStdoutAndExit(1);
}).join(',');
var argv = ['dtrace', '-Z', '-x', 'strsize=4k',
'-x', 'switchrate=10hz', '-qn',
format('%s{printf("%s", copyinstr(arg0))}', probes)];
//console.log('dtrace argv: %s', argv);
var dtrace = spawn(argv[0], argv.slice(1),
// Share the stderr handle to have error output come
// straight through. Only supported in v0.8+.
{stdio: ['pipe', 'pipe', process.stderr]});
dtrace.on('error', function (e) {
if (e.syscall === 'spawn' && e.errno === 'ENOENT') {
console.error('bunyan: error: could not spawn "dtrace" ' +
'("bunyan -p" is only supported on platforms with dtrace)');
} else {
console.error('bunyan: error: unexpected dtrace error: %s', e);
}
callback(1);
})
child = dtrace; // intentionally global
function finish(code) {
if (leftover) {
handleLogLine(null, leftover, opts, stylize);
leftover = '';
}
callback(code);
}
dtrace.stdout.setEncoding('utf8');
dtrace.stdout.on('data', function (chunk) {
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(null, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(null, lines[i], opts, stylize);
}
});
if (nodeSpawnSupportsStdio) {
dtrace.on('exit', finish);
} else {
// Fallback (for < v0.8) to pipe the dtrace process' stderr to
// this stderr. Wait for all of (1) process 'exit', (2) stderr
// 'end', and (2) stdout 'end' before returning to ensure all
// stderr is flushed (issue #54).
var returnCode = null;
var eventsRemaining = 3;
function countdownToFinish(code) {
returnCode = code;
eventsRemaining--;
if (eventsRemaining == 0) {
finish(returnCode);
}
}
dtrace.stderr.pipe(process.stderr);
dtrace.stderr.on('end', countdownToFinish);
dtrace.stderr.on('end', countdownToFinish);
dtrace.on('exit', countdownToFinish);
}
});
}
/**
* Process all input from the given log file.
*
* @param file {String} Log file path to process.
* @params opts {Object} Bunyan options object.
* @param stylize {Function} Output stylize function to use.
* @param callback {Function} `function ()`
*/
function processFile(file, opts, stylize, callback) {
var stream = fs.createReadStream(file);
if (/\.gz$/.test(file)) {
stream = stream.pipe(require('zlib').createGunzip());
}
// Manually decode streams - lazy load here as per node/lib/fs.js
var decoder = new (require('string_decoder').StringDecoder)('utf8');
streams[file].stream = stream;
stream.on('error', function (err) {
streams[file].done = true;
callback(err);
});
var leftover = ''; // Left-over partial line from last chunk.
stream.on('data', function (data) {
var chunk = decoder.write(data);
if (!chunk.length) {
return;
}
var lines = chunk.split(/\r\n|\n/);
var length = lines.length;
if (length === 1) {
leftover += lines[0];
return;
}
if (length > 1) {
handleLogLine(file, leftover + lines[0], opts, stylize);
}
leftover = lines.pop();
length -= 1;
for (var i = 1; i < length; i++) {
handleLogLine(file, lines[i], opts, stylize);
}
});
stream.on('end', function () {
streams[file].done = true;
if (leftover) {
handleLogLine(file, leftover, opts, stylize);
leftover = '';
} else {
emitNextRecord(opts, stylize);
}
callback();
});
}
/**
* From node async module.
*/
/* BEGIN JSSTYLED */
function asyncForEach(arr, iterator, callback) {
callback = callback || function () {};
if (!arr.length) {
return callback();
}
var completed = 0;
arr.forEach(function (x) {
iterator(x, function (err) {
if (err) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed === arr.length) {
callback();
}
}
});
});
};
/* END JSSTYLED */
/**
* Cleanup and exit properly.
*
* Warning: this doesn't stop processing, i.e. process exit might be delayed.
* It is up to the caller to ensure that no subsequent bunyan processing
* is done after calling this.
*
* @param code {Number} exit code.
* @param signal {String} Optional signal name, if this was exitting because
* of a signal.
*/
var cleanedUp = false;
function cleanupAndExit(code, signal) {
// Guard one call.
if (cleanedUp) {
return;
}
cleanedUp = true;
if (_DEBUG) warn('(bunyan: cleanupAndExit)');
// Clear possibly interrupted ANSI code (issue #59).
if (usingAnsiCodes) {
stdout.write('\033[0m');
}
// Kill possible dtrace child.
if (child) {
child.kill(signal);
}
if (pager) {
// Let pager know that output is done, then wait for pager to exit.
stdout.end();
pager.on('exit', function (pagerCode) {
if (_DEBUG)
warn('(bunyan: pager exit -> process.exit(%s))',
pagerCode || code);
process.exit(pagerCode || code);
});
} else {
if (_DEBUG) warn('(bunyan: process.exit(%s))', code);
process.exit(code);
}
}
//---- mainline
process.on('SIGINT', function () { cleanupAndExit(1, 'SIGINT'); });
process.on('SIGQUIT', function () { cleanupAndExit(1, 'SIGQUIT'); });
process.on('SIGTERM', function () { cleanupAndExit(1, 'SIGTERM'); });
process.on('SIGHUP', function () { cleanupAndExit(1, 'SIGHUP'); });
process.on('uncaughtException', function (err) {
function _indent(s) {
var lines = s.split(/\r?\n/);
for (var i = 0; i < lines.length; i++) {
lines[i] = '* ' + lines[i];
}
return lines.join('\n');
}
var title = encodeURIComponent(format(
'Bunyan %s crashed: %s', getVersion(), String(err)));
var e = console.error;
e('* * *');
e('* The Bunyan CLI crashed!');
e('*');
if (err.name === 'ReferenceError' && gOptsForUncaughtException.conditions) {
e('* A "ReferenceError" is often the result of given');
e('* `-c CONDITION` code that doesn\'t guard against undefined');
e('* values. If that is not the problem:');
e('*');
}
e('* Please report this issue and include the details below:');
e('*');
e('* https://github.com/trentm/node-bunyan/issues/new?title=%s', title);
e('*');
e('* * *');
e('* platform:', process.platform);
e('* node version:', process.version);
e('* bunyan version:', getVersion());
e('* argv: %j', process.argv);
e('* log line: %j', currLine);
e('* stack:');
e(_indent(err.stack));
e('* * *');
process.exit(1);
});
function main(argv) {
try {
var opts = parseArgv(argv);
} catch (e) {
warn('bunyan: error: %s', e.message);
return drainStdoutAndExit(1);
}
gOptsForUncaughtException = opts; // intentionally global
if (opts.help) {
printHelp();
return;
}
if (opts.version) {
console.log('bunyan ' + getVersion());
return;
}
if (opts.pid && opts.args.length > 0) {
warn('bunyan: error: can\'t use both "-p PID" (%s) and file (%s) args',
opts.pid, opts.args.join(' '));
return drainStdoutAndExit(1);
}
if (opts.color === null) {
if (process.env.BUNYAN_NO_COLOR &&
process.env.BUNYAN_NO_COLOR.length > 0) {
opts.color = false;
} else {
opts.color = process.stdout.isTTY;
}
}
usingAnsiCodes = opts.color; // intentionally global
var stylize = (opts.color ? stylizeWithColor : stylizeWithoutColor);
// Pager.
var nodeVer = process.versions.node.split('.').map(Number);
var paginate = (
process.stdout.isTTY &&
process.stdin.isTTY &&
!opts.pids && // Don't page if following process output.
opts.args.length > 0 && // Don't page if no file args to process.
process.platform !== 'win32' &&
(nodeVer[0] > 0 || nodeVer[1] >= 8) &&
(opts.paginate === true ||
(opts.paginate !== false &&
(!process.env.BUNYAN_NO_PAGER ||
process.env.BUNYAN_NO_PAGER.length === 0))));
if (paginate) {
var pagerCmd = process.env.PAGER || 'less';
/* JSSTYLED */
assert.ok(pagerCmd.indexOf('"') === -1 && pagerCmd.indexOf("'") === -1,
'cannot parse PAGER quotes yet');
var argv = pagerCmd.split(/\s+/g);
var env = objCopy(process.env);
if (env.LESS === undefined) {
// git's default is LESS=FRSX. I don't like the 'S' here because
// lines are *typically* wide with bunyan output and scrolling
// horizontally is a royal pain. Note a bug in Mac's `less -F`,
// such that SIGWINCH can kill it. If that rears too much then
// I'll remove 'F' from here.
env.LESS = 'FRX';
}
if (_DEBUG) warn('(pager: argv=%j, env.LESS=%j)', argv, env.LESS);
// `pager` and `stdout` intentionally global.
pager = spawn(argv[0], argv.slice(1),
// Share the stderr handle to have error output come
// straight through. Only supported in v0.8+.
{env: env, stdio: ['pipe', 1, 2]});
stdout = pager.stdin;
// Early termination of the pager: just stop.
pager.on('exit', function (pagerCode) {
if (_DEBUG) warn('(bunyan: pager exit)');
pager = null;
stdout.end()
stdout = process.stdout;
cleanupAndExit(pagerCode);
});
}
// Stdout error handling. (Couldn't setup until `stdout` was determined.)
stdout.on('error', function (err) {
if (_DEBUG) warn('(stdout error event: %s)', err);
if (err.code === 'EPIPE') {
drainStdoutAndExit(0);
} else if (err.toString() === 'Error: This socket is closed.') {
// Could get this if the pager closes its stdin, but hasn't
// exited yet.
drainStdoutAndExit(1);
} else {
warn(err);
drainStdoutAndExit(1);
}
});
var retval = 0;
if (opts.pids) {
processPids(opts, stylize, function (code) {
cleanupAndExit(code);
});
} else if (opts.args.length > 0) {
var files = opts.args;
files.forEach(function (file) {
streams[file] = { stream: null, records: [], done: false }
});
asyncForEach(files,
function (file, next) {
processFile(file, opts, stylize, function (err) {
if (err) {
warn('bunyan: %s', err.message);
retval += 1;
}
next();
});
},
function (err) {
if (err) {
warn('bunyan: unexpected error: %s', err.stack || err);
return drainStdoutAndExit(1);
}
cleanupAndExit(retval);
}
);
} else {
processStdin(opts, stylize, function () {
cleanupAndExit(retval);
});
}
}
if (require.main === module) {
// HACK guard for <https://github.com/trentm/json/issues/24>.
// We override the `process.stdout.end` guard that core node.js puts in
// place. The real fix is that `.end()` shouldn't be called on stdout
// in node core. Node v0.6.9 fixes that. Only guard for v0.6.0..v0.6.8.
var nodeVer = process.versions.node.split('.').map(Number);
if ([0, 6, 0] <= nodeVer && nodeVer <= [0, 6, 8]) {
var stdout = process.stdout;
stdout.end = stdout.destroy = stdout.destroySoon = function () {
/* pass */
};
}
main(process.argv);
}

bunyan Changelog

Known issues:

bunyan 0.22.1

  • #111 Fix a crash when attempting to use bunyan -p on a platform without dtrace.

  • #101 Fix a crash in bunyan rendering a record with unexpected "res.headers".

bunyan 0.22.0

  • #104 log.reopenFileStreams() convenience method to be used with external log rotation.

bunyan 0.21.4

  • #96 Fix bunyan to default to paging (with less) by default in node 0.10.0. The intention has always been to default to paging for node >=0.8.

bunyan 0.21.3

  • [issue #90] Fix bunyan -p '*' breakage in version 0.21.2.

bunyan 0.21.2

Note: Bad release. The switchrate change below broke bunyan -p '*' usage (see issue #90). Use 0.21.3 or later.

  • [issue #88] Should be able to efficiently combine "-l" with "-p *".

  • Avoid DTrace buffer filling up, e.g. like this:

      $ bunyan -p 42241 > /tmp/all.log
      dtrace: error on enabled probe ID 3 (ID 75795: bunyan42241:mod-87ea640:log-trace:log-trace): out of scratch space in action #1 at DIF offset 12
      dtrace: error on enabled probe ID 3 (ID 75795: bunyan42241:mod-87ea640:log-trace:log-trace): out of scratch space in action #1 at DIF offset 12
      dtrace: 138 drops on CPU 4
      ...
    

    From Bryan: "the DTrace buffer is filling up because the string size is so large... by increasing the switchrate, you're increasing the rate at which that buffer is emptied."

bunyan 0.21.1

  • [pull #83] Support rendering 'client_res' key in bunyan CLI (by github.com/mcavage).

bunyan 0.21.0

  • 'make check' clean, 4-space indenting. No functional change here, just lots of code change.
  • [issue #80, #82] Drop assert that broke using 'rotating-file' with a default period (by github.com/ricardograca).

bunyan 0.20.0

  • [Slight backward incompatibility] Fix serializer bug introduced in 0.18.3 (see below) to only apply serializers to log records when appropriate.

    This also makes a semantic change to custom serializers. Before this change a serializer function was called for a log record key when that value was truth-y. The semantic change is to call the serializer function as long as the value is not undefined. That means that a serializer function should handle falsey values such as false and null.

  • Update to latest 'mv' dep (required for rotating-file support) to support node v0.10.0.

bunyan 0.19.0

WARNING: This release includes a bug introduced in bunyan 0.18.3 (see below). Please upgrade to bunyan 0.20.0.

  • [Slight backward incompatibility] Change the default error serialization (a.k.a. bunyan.stdSerializers.err) to not serialize all additional attributes of the given error object. This is an open door to unsafe logging and logging should always be safe. With this change, error serialization will log these attributes: message, name, stack, code, signal. The latter two are added because some core node APIs include those fields (e.g. child_process.exec).

    Concrete examples where this has hurt have been the "domain" change necessitating 0.18.3 and a case where node-restify uses an error object as the response object. When logging the err and res in the same log statement (common for restify audit logging), the res.body would be JSON stringified as '[Circular]' as it had already been emitted for the err key. This results in a WTF with the bunyan CLI because the err.body is not rendered.

    If you need the old behaviour back you will need to do this:

      var bunyan = require('bunyan');
      var errSkips = {
          // Skip domain keys. `domain` especially can have huge objects that can
          // OOM your app when trying to JSON.stringify.
          domain: true,
          domain_emitter: true,
          domain_bound: true,
          domain_thrown: true
      };
      bunyan.stdSerializers.err = function err(err) {
         if (!err || !err.stack)
             return err;
         var obj = {
             message: err.message,
             name: err.name,
             stack: getFullErrorStack(err)
         }
         Object.keys(err).forEach(function (k) {
             if (err[k] !== undefined && !errSkips[k]) {
                 obj[k] = err[k];
             }
         });
         return obj;
       };
    
  • "long" and "bunyan" output formats for the CLI. bunyan -o long is the default format, the same as before, just called "long" now instead of the cheesy "paul" name. The "bunyan" output format is the same as "json-0", just with a more convenient name.

bunyan 0.18.3

WARNING: This release introduced a bug such that all serializers are applied to all log records even if the log record did not contain the key for that serializer. If a logger serializer function does not handle being given undefined, then you'll get warnings like this on stderr:

bunyan: ERROR: This should never happen. This is a bug in <https://github.com/trentm/node-bunyan> or in this application. Exception from "foo" Logger serializer: Error: ...
    at Object.bunyan.createLogger.serializers.foo (.../myapp.js:20:15)
    at Logger._applySerializers (.../lib/bunyan.js:644:46)
    at Array.forEach (native)
    at Logger._applySerializers (.../lib/bunyan.js:640:33)
    ...

and the following junk in written log records:

"foo":"(Error in Bunyan log "foo" serializer broke field. See stderr for details.)"

Please upgrade to bunyan 0.20.0.

  • Change the bunyan.stdSerializers.err serializer for errors to exclude the "domain*" keys. err.domain will include its assigned members which can arbitrarily large objects that are not intended for logging.

  • Make the "dtrace-provider" dependency optional. I hate to do this, but installing bunyan on Windows is made very difficult with this as a required dep. Even though "dtrace-provider" stubs out for non-dtrace-y platforms, without a compiler and Python around, node-gyp just falls over.

bunyan 0.18.2

  • [pull #67] Remove debugging prints in rotating-file support. (by github.com/chad3814).
  • Update to dtrace-provider@0.2.7.

bunyan 0.18.1

  • Get the bunyan CLI to not automatically page (i.e. pipe to less) if stdin isn't a TTY, or if following dtrace probe output (via -p PID), or if not given log file arguments.

bunyan 0.18.0

  • Automatic paging support in the bunyan CLI (similar to git log et al). IOW, bunyan will open your pager (by default less) and pipe rendered log output through it. A main benefit of this is getting colored logs with a pager without the pain. Before you had to explicit use --color to tell bunyan to color output when the output was not a TTY:

      bunyan foo.log --color | less -R        # before
      bunyan foo.log                          # now
    

    Disable with the --no-pager option or the BUNYAN_NO_PAGER=1 environment variable.

    Limitations: Only supported for node >=0.8. Windows is not supported (at least not yet).

  • Switch test suite to nodeunit (still using a node-tap'ish API via a helper).

bunyan 0.17.0

  • [issue #33] Log rotation support:

      var bunyan = require('bunyan');
      var log = bunyan.createLogger({
          name: 'myapp',
          streams: [{
              type: 'rotating-file',
              path: '/var/log/myapp.log',
              count: 7,
              period: 'daily'
          }]
      });
    
  • Tweak to CLI default pretty output: don't special case "latency" field. The special casing was perhaps nice, but less self-explanatory. Before:

      [2012-12-27T21:17:38.218Z]  INFO: audit/45769 on myserver: handled: 200 (15ms, audit=true, bar=baz)
        GET /foo
        ...
    

    After:

      [2012-12-27T21:17:38.218Z]  INFO: audit/45769 on myserver: handled: 200 (audit=true, bar=baz, latency=15)
        GET /foo
        ...
    
  • Exit CLI on EPIPE, otherwise we sit there useless processing a huge log file with, e.g. bunyan huge.log | head.

bunyan 0.16.8

  • Guards on -c CONDITION usage to attempt to be more user friendly. Bogus JS code will result in this:

      $ bunyan portal.log -c 'this.req.username==boo@foo'
      bunyan: error: illegal CONDITION code: SyntaxError: Unexpected token ILLEGAL
        CONDITION script:
          Object.prototype.TRACE = 10;
          Object.prototype.DEBUG = 20;
          Object.prototype.INFO = 30;
          Object.prototype.WARN = 40;
          Object.prototype.ERROR = 50;
          Object.prototype.FATAL = 60;
          this.req.username==boo@foo
        Error:
          SyntaxError: Unexpected token ILLEGAL
              at new Script (vm.js:32:12)
              at Function.Script.createScript (vm.js:48:10)
              at parseArgv (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:465:27)
              at main (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:1252:16)
              at Object.<anonymous> (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:1330:3)
              at Module._compile (module.js:449:26)
              at Object.Module._extensions..js (module.js:467:10)
              at Module.load (module.js:356:32)
              at Function.Module._load (module.js:312:12)
              at Module.runMain (module.js:492:10)
    

    And all CONDITION scripts will be run against a minimal valid Bunyan log record to ensure they properly guard against undefined values (at least as much as can reasonably be checked). For example:

      $ bunyan portal.log -c 'this.req.username=="bob"'
      bunyan: error: CONDITION code cannot safely filter a minimal Bunyan log record
        CONDITION script:
          Object.prototype.TRACE = 10;
          Object.prototype.DEBUG = 20;
          Object.prototype.INFO = 30;
          Object.prototype.WARN = 40;
          Object.prototype.ERROR = 50;
          Object.prototype.FATAL = 60;
          this.req.username=="bob"
        Minimal Bunyan log record:
          {
            "v": 0,
            "level": 30,
            "name": "name",
            "hostname": "hostname",
            "pid": 123,
            "time": 1355514346206,
            "msg": "msg"
          }
        Filter error:
          TypeError: Cannot read property 'username' of undefined
              at bunyan-condition-0:7:9
              at Script.Object.keys.forEach.(anonymous function) [as runInNewContext] (vm.js:41:22)
              at parseArgv (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:477:18)
              at main (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:1252:16)
              at Object.<anonymous> (/Users/trentm/tm/node-bunyan-0.x/bin/bunyan:1330:3)
              at Module._compile (module.js:449:26)
              at Object.Module._extensions..js (module.js:467:10)
              at Module.load (module.js:356:32)
              at Function.Module._load (module.js:312:12)
              at Module.runMain (module.js:492:10)
    

    A proper way to do that condition would be:

      $ bunyan portal.log -c 'this.req && this.req.username=="bob"'
    

bunyan 0.16.7

  • [issue #59] Clear a possibly interrupted ANSI color code on signal termination.

bunyan 0.16.6

  • [issue #56] Support bunyan -p NAME to dtrace all PIDs matching 'NAME' in their command and args (using ps -A -o pid,command | grep NAME or, on SunOS pgrep -lf NAME). E.g.:

      bunyan -p myappname
    

    This is useful for usage of node's cluster module where you'll have multiple worker processes.

bunyan 0.16.5

  • Allow bunyan -p '*' to capture bunyan dtrace probes from all processes.
  • issue #55: Add support for BUNYAN_NO_COLOR environment variable to turn off all output coloring. This is still overridden by the --color and --no-color options.

bunyan 0.16.4

  • issue #54: Ensure (again, see 0.16.2) that stderr from the dtrace child process (when using bunyan -p PID) gets through. There had been a race between exiting bunyan and the flushing of the dtrace process' stderr.

bunyan 0.16.3

bunyan 0.16.2

  • Ensure that stderr from the dtrace child process (when using bunyan -p PID) gets through. The pipe usage wasn't working on SmartOS. This is important to show the user if they need to 'sudo'.

bunyan 0.16.1

  • Ensure that a possible dtrace child process (with using bunyan -p PID) is terminated on signal termination of the bunyan CLI (at least for SIGINT, SIGQUIT, SIGTERM, SIGHUP).

bunyan 0.16.0

  • Add bunyan -p PID support. This is a convenience wrapper that effectively calls:

      dtrace -x strsize=4k -qn 'bunyan$PID:::log-*{printf("%s", copyinstr(arg0))}' | bunyan
    

bunyan 0.15.0

  • issue #48: Dtrace support! The elevator pitch is you can watch all logging from all Bunyan-using process with something like this:

      dtrace -x strsize=4k -qn 'bunyan*:::log-*{printf("%d: %s: %s", pid, probefunc, copyinstr(arg0))}'
    

    And this can include log levels below what the service is actually configured to log. E.g. if the service is only logging at INFO level and you need to see DEBUG log messages, with this you can. Obviously this only works on dtrace-y platforms: Illumos derivatives of SunOS (e.g. SmartOS, OmniOS), Mac, FreeBSD.

    Or get the bunyan CLI to render logs nicely:

      dtrace -x strsize=4k -qn 'bunyan*:::log-*{printf("%s", copyinstr(arg0))}' | bunyan
    

    See https://github.com/trentm/node-bunyan#dtrace-support for details. By Bryan Cantrill.

bunyan 0.14.6

  • Export bunyan.safeCycles(). This may be useful for custom type == "raw" streams that may do JSON stringification of log records themselves. Usage:

      var str = JSON.stringify(rec, bunyan.safeCycles());
    
  • [issue #49] Allow a log.child() to specify the level of inherited streams. For example:

      # Before
      var childLog = log.child({...});
      childLog.level('debug');
    
      # After
      var childLog = log.child({..., level: 'debug'});
    
  • Improve the Bunyan CLI crash message to make it easier to provide relevant details in a bug report.

bunyan 0.14.5

  • Fix a bug in the long-stack-trace error serialization added in 0.14.4. The symptom:

      bunyan@0.14.4: .../node_modules/bunyan/lib/bunyan.js:1002
        var ret = ex.stack || ex.toString();
                    ^
      TypeError: Cannot read property 'stack' of undefined
          at getFullErrorStack (.../node_modules/bunyan/lib/bunyan.js:1002:15)
          ...
    

bunyan 0.14.4

  • Bad release. Use 0.14.5 instead.

  • Improve error serialization to walk the chain of .cause() errors from the likes of WError or VError error classes from verror and restify v2.0. Example:

      [2012-10-11T00:30:21.871Z] ERROR: imgapi/99612 on 0525989e-2086-4270-b960-41dd661ebd7d: my-message
          ValidationFailedError: my-message; caused by TypeError: cause-error-message
              at Server.apiPing (/opt/smartdc/imgapi/lib/app.js:45:23)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.setupReq (/opt/smartdc/imgapi/lib/app.js:178:9)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.parseBody (/opt/smartdc/imgapi/node_modules/restify/lib/plugins/body_parser.js:15:33)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.parseQueryString (/opt/smartdc/imgapi/node_modules/restify/lib/plugins/query.js:40:25)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server._run (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:579:17)
              at Server._handle.log.trace.req (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:480:38)
          Caused by: TypeError: cause-error-message
              at Server.apiPing (/opt/smartdc/imgapi/lib/app.js:40:25)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.setupReq (/opt/smartdc/imgapi/lib/app.js:178:9)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.parseBody (/opt/smartdc/imgapi/node_modules/restify/lib/plugins/body_parser.js:15:33)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server.parseQueryString (/opt/smartdc/imgapi/node_modules/restify/lib/plugins/query.js:40:25)
              at next (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:550:50)
              at Server._run (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:579:17)
              at Server._handle.log.trace.req (/opt/smartdc/imgapi/node_modules/restify/lib/server.js:480:38)
    

bunyan 0.14.2

  • [issue #45] Fix bunyan CLI (default output mode) to not crash on a 'res' field that isn't a response object, but a string.

bunyan 0.14.1

  • [issue #44] Fix the default bunyan CLI output of a res.body that is an object instead of a string. See issue#38 for the same with req.body.

bunyan 0.14.0

  • [pull #41] Safe JSON.stringifying of emitted log records to avoid blowing up on circular objects (by Isaac Schlueter).

bunyan 0.13.5

  • [issue #39] Fix a bug with client_req handling in the default output of the bunyan CLI.

bunyan 0.13.4

  • [issue #38] Fix the default bunyan CLI output of a req.body that is an object instead of a string.

bunyan 0.13.3

  • Export bunyan.resolveLevel(NAME-OR-NUM) to resolve a level name or number to its log level number value:

      > bunyan.resolveLevel('INFO')
      30
      > bunyan.resolveLevel('debug')
      20
    

    A side-effect of this change is that the uppercase level name is now allowed in the logger constructor.

bunyan 0.13.2

  • [issue #35] Ensure that an accidental log.info(BUFFER), where BUFFER is a node.js Buffer object, doesn't blow up.

bunyan 0.13.1

  • [issue #34] Ensure req.body, res.body and other request/response fields are emitted by the bunyan CLI (mostly by Rob Gulewich).

bunyan 0.13.0

  • [issue #31] Re-instate defines for the (uppercase) log level names (TRACE, DEBUG, etc.) in bunyan -c "..." filtering condition code. E.g.:

      $ ... | bunyan -c 'level >= ERROR'
    

bunyan 0.12.0

  • [pull #32] bunyan -o short for more concise output (by Dave Pacheco). E.g.:

      22:56:52.856Z  INFO myservice: My message
    

    instead of:

      [2012-02-08T22:56:52.856Z]  INFO: myservice/123 on example.com: My message
    

bunyan 0.11.3

  • Add '--strict' option to bunyan CLI to suppress all but legal Bunyan JSON log lines. By default non-JSON, and non-Bunyan lines are passed through.

bunyan 0.11.2

  • [issue #30] Robust handling of 'req' field without a 'headers' subfield in bunyan CLI.
  • [issue #31] Pull the TRACE, DEBUG, et al defines from bunyan -c "..." filtering code. This was added in v0.11.1, but has a significant adverse affect.

bunyan 0.11.1

  • Bad release. The TRACE et al names are bleeding into the log records when using '-c'.

  • Add defines for the (uppercase) log level names (TRACE, DEBUG, etc.) in bunyan -c "..." filtering condition code. E.g.:

      $ ... | bunyan -c 'level >= ERROR'
    

bunyan 0.11.0

  • [pull #29] Add -l/--level for level filtering, and -c/--condition for arbitrary conditional filtering (by github.com/isaacs):

      $ ... | bunyan -l error   # filter out log records below error
      $ ... | bunyan -l 50      # numeric value works too
      $ ... | bunyan -c 'level===50'              # equiv with -c filtering
      $ ... | bunyan -c 'pid===123'               # filter on any field
      $ ... | bunyan -c 'pid===123' -c '_audit'   # multiple filters
    

bunyan 0.10.0

  • [pull #24] Support for gzip'ed log files in the bunyan CLI (by github.com/mhart):

      $ bunyan foo.log.gz
      ...
    

bunyan 0.9.0

  • [pull #16] Bullet proof the bunyan.stdSerializers (by github.com/rlidwka).

  • [pull #15] The bunyan CLI will now chronologically merge multiple log streams when it is given multiple file arguments. (by github.com/davepacheco)

      $ bunyan foo.log bar.log
      ... merged log records ...
    
  • [pull #15] A new bunyan.RingBuffer stream class that is useful for keeping the last N log messages in memory. This can be a fast way to keep recent, and thus hopefully relevant, log messages. (by @dapsays, github.com/davepacheco)

    Potential uses: Live debugging if a running process could inspect those messages. One could dump recent log messages at a finer log level than is typically logged on uncaughtException.

      var ringbuffer = new bunyan.RingBuffer({ limit: 100 });
      var log = new bunyan({
          name: 'foo',
          streams: [{
              type: 'raw',
              stream: ringbuffer,
              level: 'debug'
          }]
      });
    
      log.info('hello world');
      console.log(ringbuffer.records);
    
  • Add support for "raw" streams. This is a logging stream that is given raw log record objects instead of a JSON-stringified string.

      function Collector() {
          this.records = [];
      }
      Collector.prototype.write = function (rec) {
          this.records.push(rec);
      }
      var log = new Logger({
          name: 'mylog',
          streams: [{
              type: 'raw',
              stream: new Collector()
          }]
      });
    

    See "examples/raw-stream.js". I expect raw streams to be useful for piping Bunyan logging to separate services (e.g. http://www.loggly.com/, https://github.com/etsy/statsd) or to separate in-process handling.

  • Add test/corpus/*.log files (accidentally excluded) so the test suite actually works(!).

bunyan 0.8.0

  • [pull #21] Bunyan loggers now re-emit fs.createWriteStream error events. By github.com/EvanOxfeld. See "examples/handle-fs-error.js" and "test/error-event.js" for details.

      var log = new Logger({name: 'mylog', streams: [{path: FILENAME}]});
      log.on('error', function (err, stream) {
          // Handle error writing to or creating FILENAME.
      });
    
  • jsstyle'ing (via make check)

bunyan 0.7.0

  • [issue #12] Add bunyan.createLogger(OPTIONS) form, as is more typical in node.js APIs. This'll eventually become the preferred form.

bunyan 0.6.9

  • Change bunyan CLI default output to color "src" info red. Before the "src" information was uncolored. The "src" info is the filename, line number and function name resulting from using src: true in Logger creation. I.e., the (/Users/trentm/tm/node-bunyan/examples/hi.js:10) in:

      [2012-04-10T22:28:58.237Z]  INFO: myapp/39339 on banana.local (/Users/trentm/tm/node-bunyan/examples/hi.js:10): hi
    
  • Tweak bunyan CLI default output to still show an "err" field if it doesn't have a "stack" attribute.

bunyan 0.6.8

  • Fix bad bug in log.child({...}, true); where the added child fields would be added to the parent's fields. This bug only existed for the "fast child" path (that second true argument). A side-effect of fixing this is that the "fast child" path is only 5 times as fast as the regular log.child, instead of 10 times faster.

bunyan 0.6.7

  • [issue #6] Fix bleeding 'type' var to global namespace. (Thanks Mike!)

bunyan 0.6.6

  • Add support to the bunyan CLI taking log file path args, bunyan foo.log, in addition to the usual cat foo.log | bunyan.
  • Improve reliability of the default output formatting of the bunyan CLI. Before it could blow up processing log records missing some expected fields.

bunyan 0.6.5

  • ANSI coloring output from bunyan CLI tool (for the default output mode/style). Also add the '--color' option to force coloring if the output stream is not a TTY, e.g. cat my.log | bunyan --color | less -R. Use --no-color to disable coloring, e.g. if your terminal doesn't support ANSI codes.
  • Add 'level' field to log record before custom fields for that record. This just means that the raw record JSON will show the 'level' field earlier, which is a bit nicer for raw reading.

bunyan 0.6.4

  • [issue #5] Fix log.info() -> boolean to work properly. Previous all were returning false. Ditto all trace/debug/.../fatal methods.

bunyan 0.6.3

  • Allow an optional msg and arguments to the log.info(<Error> err) logging form. For example, before:

      log.debug(my_error_instance)            // good
      log.debug(my_error_instance, "boom!")   // wasn't allowed
    

    Now the latter is allowed if you want to expliciting set the log msg. Of course this applies to all the log.{trace|debug|info...}() methods.

  • bunyan cli output: clarify extra fields with quoting if empty or have spaces. E.g. 'cmd' and 'stderr' in the following:

      [2012-02-12T00:30:43.736Z] INFO: mo-docs/43194 on banana.local: buildDocs results (req_id=185edca2-2886-43dc-911c-fe41c09ec0f5, route=PutDocset, error=null, stderr="", cmd="make docs")
    

bunyan 0.6.2

bunyan 0.6.1

  • Internal: starting jsstyle usage.
  • Internal: add .npmignore. Previous packages had reams of bunyan crud in them.

bunyan 0.6.0

  • Add 'pid' automatic log record field.

bunyan 0.5.3

  • Add 'client_req' (HTTP client request) standard formatting in bunyan CLI default output.
  • Improve bunyan CLI default output to include all log record keys. Unknown keys are either included in the first line parenthetical (if short) or in the indented subsequent block (if long or multiline).

bunyan 0.5.2

  • [issue #3] More type checking of new Logger(...) and log.child(...) options.
  • Start a test suite.

bunyan 0.5.1

  • [issue #2] Add guard on JSON.stringifying of log records before emission. This will prevent log.info et al throwing on record fields that cannot be represented as JSON. An error will be printed on stderr and a clipped log record emitted with a 'bunyanMsg' key including error details. E.g.:

      bunyan: ERROR: could not stringify log record from /Users/trentm/tm/node-bunyan/examples/unstringifyable.js:12: TypeError: Converting circular structure to JSON
      {
        "name": "foo",
        "hostname": "banana.local",
        "bunyanMsg": "bunyan: ERROR: could not stringify log record from /Users/trentm/tm/node-bunyan/examples/unstringifyable.js:12: TypeError: Converting circular structure to JSON",
      ...
    

    Some timing shows this does effect log speed:

      $ node tools/timeguard.js     # before
      Time try/catch-guard on JSON.stringify:
       - log.info:  0.07365ms per iteration
      $ node tools/timeguard.js     # after
      Time try/catch-guard on JSON.stringify:
       - log.info:  0.07368ms per iteration
    

bunyan 0.5.0

  • Use 10/20/... instead of 1/2/... for level constant values. Ostensibly this allows for intermediary levels from the defined "trace/debug/..." set. However, that is discouraged. I'd need a strong user argument to add support for easily using alternative levels. Consider using a separate JSON field instead.

  • s/service/name/ for Logger name field. "service" is unnecessarily tied to usage for a service. No need to differ from log4j Logger "name".

  • Add log.level(...) and log.levels(...) API for changing logger stream levels.

  • Add TRACE|DEBUG|INFO|WARN|ERROR|FATAL level constants to exports.

  • Add log.info(err) special case for logging an Error instance. For example log.info(new TypeError("boom") will produce:

      ...
      "err": {
        "message": "boom",
        "name": "TypeError",
        "stack": "TypeError: boom\n    at Object.<anonymous> ..."
      },
      "msg": "boom",
      ...
    

bunyan 0.4.0

  • Add new Logger({src: true}) config option to have a 'src' attribute be automatically added to log records with the log call source info. Example:

      "src": {
        "file": "/Users/trentm/tm/node-bunyan/examples/src.js",
        "line": 20,
        "func": "Wuzzle.woos"
      },
    

bunyan 0.3.0

  • log.child(options[, simple]) Added simple boolean arg. Set true to assert that options only add fields (no config changes). Results in a 10x speed increase in child creation. See "tools/timechild.js". On my Mac, "fast child" creation takes about 0.001ms. IOW, if your app is dishing 10,000 req/s, then creating a log child for each request will take about 1% of the request time.
  • log.clone -> log.child to better reflect the relationship: streams and serializers are inherited. Streams can't be removed as part of the child creation. The child doesn't own the parent's streams (so can't close them).
  • Clean up Logger creation. The goal here was to ensure log.child usage is fast. TODO: measure that.
  • Add Logger.stdSerializers.err serializer which is necessary to get good Error object logging with node 0.6 (where core Error object properties are non-enumerable).

bunyan 0.2.0

  • Spec'ing core/recommended log record fields.
  • Add LOG_VERSION to exports.
  • Improvements to request/response serializations.

bunyan 0.1.0

First release.

.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "BUNYAN" "1" "November 2012" "" "bunyan manual"
.
.SH "NAME"
\fBbunyan\fR \- filter and pretty\-print Bunyan log file content
.
.SH "SYNOPSIS"
\fBbunyan\fR [OPTIONS]
.
.P
\&\.\.\. | \fBbunyan\fR [OPTIONS]
.
.P
\fBbunyan\fR [OPTIONS] \-p PID
.
.SH "DESCRIPTION"
"Bunyan" is \fBa simple and fast a JSON logging library\fR for node\.js services, a one\-JSON\-object\-per\-line log format, and \fBa \fBbunyan\fR CLI tool\fR for nicely viewing those logs\. This man page describes the latter\.
.
.SS "Pretty\-printing"
A bunyan log file is a stream of JSON objects, optionally interspersed with non\-JSON log lines\. The primary usage of bunyan(1) is to pretty print, for example:
.
.IP "" 4
.
.nf
$ bunyan foo\.log # or `cat foo\.log | bunyan
[2012\-02\-08T22:56:52\.856Z] INFO: myservice/123 on example\.com: My message
extra: multi
line
[2012\-02\-08T22:56:54\.856Z] ERROR: myservice/123 on example\.com: My message
\.\.\.
.
.fi
.
.IP "" 0
.
.P
By default the "long" output format is used\. Use the \fB\-o FORMAT\fR option to emit other formats\. E\.g\.:
.
.IP "" 4
.
.nf
$ bunyan foo\.log \-o short
22:56:52\.856Z INFO myservice: My message
extra: multi
line
22:56:54\.856Z ERROR myservice: My message
\.\.\.
.
.fi
.
.IP "" 0
.
.P
These will color the output if supported in your terminal\. See "OUTPUT FORMATS" below\.
.
.SS "Filtering"
The \fBbunyan\fR CLI can also be used to filter a bunyan log\. Use \fB\-l LEVEL\fR to filter by level:
.
.IP "" 4
.
.nf
$ bunyan foo\.log \-l error # show only \'error\' level records
[2012\-02\-08T22:56:54\.856Z] ERROR: myservice/123 on example\.com: My message
.
.fi
.
.IP "" 0
.
.P
Use \fB\-c COND\fR to filter on a JavaScript expression returning true on the record data\. In the COND code, \fBthis\fR refers to the record object:
.
.IP "" 4
.
.nf
$ bunyan foo\.log \-c `this\.three` # show records with the \'extra\' field
[2012\-02\-08T22:56:52\.856Z] INFO: myservice/123 on example\.com: My message
extra: multi
line
.
.fi
.
.IP "" 0
.
.SH "OPTIONS"
.
.TP
\fB\-h\fR, \fB\-\-help\fR
Print this help info and exit\.
.
.TP
\fB\-\-version\fR
Print version of this command and exit\.
.
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Don\'t warn if input isn\'t valid JSON\.
.
.P
Dtrace options (only on dtrace\-supporting platforms):
.
.TP
\fB\-p PID\fR, \fB\-p NAME\fR
Process bunyan:log\-* probes from the process with the given PID\. Can be used multiple times, or specify all processes with \'*\', or a set of processes whose command & args match a pattern with \'\-p NAME\'\.
.
.P
Filtering options:
.
.TP
\fB\-l\fR, \fB\-\-level LEVEL\fR
Only show messages at or above the specified level\. You can specify level \fInames\fR or numeric values\. (See \'Log Levels\' below\.)
.
.TP
\fB\-c COND\fR, \fB\-\-condition COND\fR
Run each log message through the condition and only show those that resolve to a truish value\. E\.g\. \fB\-c \'this\.pid == 123\'\fR\.
.
.TP
\fB\-\-strict\fR
Suppress all but legal Bunyan JSON log lines\. By default non\-JSON, and non\-Bunyan lines are passed through\.
.
.P
Output options:
.
.TP
\fB\-\-color\fR
Colorize output\. Defaults to try if output stream is a TTY\.
.
.TP
\fB\-\-no\-color\fR
Force no coloring (e\.g\. terminal doesn\'t support it)
.
.TP
\fB\-o FORMAT\fR, \fB\-\-output FORMAT\fR
Specify an output format\. One of \fBpaul\fR (the default), \fBshort\fR, \fBjson\fR, \fBjson\-N\fR, or \fBinspect\fR\.
.
.TP
\fB\-j\fR
Shortcut for \fB\-o json\fR\.
.
.SH "LOG LEVELS"
In Bunyan log records, then \fBlevel\fR field is a number\. For the \fB\-l|\-\-level\fR argument the level \fBnames\fR are supported as shortcuts\. In \fB\-c|\-\-condition\fR scripts, uppercase symbols like "DEBUG" are defined for convenience\.
.
.IP "" 4
.
.nf
Level Name Level Number Symbol in COND Scripts
trace 10 TRACE
debug 20 DEBUG
info 30 INFO
warn 40 WARN
error 50 ERROR
fatal 60 FATAL
.
.fi
.
.IP "" 0
.
.SH "OUTPUT FORMATS"
.
.nf
FORMAT NAME DESCRIPTION
paul (default) The default output\. Long form\. Colored and "pretty"\.
\'req\' and \'res\' and \'err\' fields are rendered specially
as an HTTP request, HTTP response and exception
stack trace, respectively\. Note: the "paul" name
is deprecated and will be changed to "long"\.
short Like the default output, but more concise\. Some
typically redundant fields are ellided\.
json JSON output, 2\-space indentation\.
json\-N JSON output, N\-space indentation, e\.g\. "json\-0"
inspect Node\.js `util\.inspect` output\.
.
.fi
.
.SH "DTRACE SUPPORT"
On systems that support DTrace (e\.g\., MacOS, FreeBSD, illumos derivatives like SmartOS and OmniOS), Bunyan will create a DTrace provider (\fBbunyan\fR) that makes available the following probes:
.
.IP "" 4
.
.nf
log\-trace
log\-debug
log\-info
log\-warn
log\-error
log\-fatal
.
.fi
.
.IP "" 0
.
.P
Each of these probes has a single argument: the string that would be written to the log\. Note that when a probe is enabled, it will fire whenever the corresponding function is called, even if the level of the log message is less than that of any stream\.
.
.P
See \fIhttps://github\.com/trentm/node\-bunyan#dtrace\-support\fR for more details and the \'\-p PID\' option above for convenience usage\.
.
.SH "ENVIRONMENT"
.
.TP
\fBBUNYAN_NO_COLOR\fR
Set to a non\-empty value to force no output coloring\. See \'\-\-no\-color\'\.
.
.SH "PROJECT & BUGS"
\fBbunyan\fR is written in JavaScript and requires node\.js (\fBnode\fR)\. The project lives at \fIhttps://github\.com/trentm/node\-bunyan\fR and is published to npm as "bunyan"\.
.
.IP "\(bu" 4
README, Install notes: \fIhttps://github\.com/trentm/node\-bunyan#readme\fR
.
.IP "\(bu" 4
Report bugs to \fIhttps://github\.com/trentm/node\-bunyan/issues\fR\.
.
.IP "\(bu" 4
See the full changelog at: \fIhttps://github\.com/trentm/node\-bunyan/blob/master/CHANGES\.md\fR
.
.IP "" 0
.
.SH "LICENSE"
MIT License (see \fIhttps://github\.com/trentm/node\-bunyan/blob/master/LICENSE\.txt\fR)
.
.SH "COPYRIGHT"
node\-bunyan is Copyright (c) 2012 Joyent, Inc\. Copyright (c) 2012 Trent Mick\. All rights reserved\.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' value='text/html;charset=utf8'>
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
<title>bunyan(1) - filter and pretty-print Bunyan log file content</title>
<style type='text/css' media='all'>
/* style: man */
body#manpage {margin:0}
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
.mp h2 {margin:10px 0 0 0}
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
.mp h3 {margin:0 0 0 4ex}
.mp dt {margin:0;clear:left}
.mp dd {margin:0 0 0 9ex}
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
.mp pre {margin-bottom:20px}
.mp pre+h2,.mp pre+h3 {margin-top:22px}
.mp h2+pre,.mp h3+pre {margin-top:5px}
.mp img {display:block;margin:auto}
.mp h1.man-title {display:none}
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
.mp h2 {font-size:16px;line-height:1.25}
.mp h1 {font-size:20px;line-height:2}
.mp {text-align:justify;background:#fff}
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
.mp u {text-decoration:underline}
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
.mp b.man-ref {font-weight:normal;color:#434241}
.mp pre {padding:0 4ex}
.mp pre code {font-weight:normal;color:#434241}
.mp h2+pre,h3+pre {padding-left:0}
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
ol.man-decor {width:100%}
ol.man-decor li.tl {text-align:left}
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
ol.man-decor li.tr {text-align:right;float:right}
</style>
<style type='text/css' media='all'>
/* style: toc */
.man-navigation {display:block !important;position:fixed;top:0;left:113ex;height:100%;width:100%;padding:48px 0 0 0;border-left:1px solid #dbdbdb;background:#eee}
.man-navigation a,.man-navigation a:hover,.man-navigation a:link,.man-navigation a:visited {display:block;margin:0;padding:5px 2px 5px 30px;color:#999;text-decoration:none}
.man-navigation a:hover {color:#111;text-decoration:underline}
</style>
</head>
<!--
The following styles are deprecated and will be removed at some point:
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
.man-navigation should be used instead.
-->
<body id='manpage'>
<div class='mp' id='man'>
<div class='man-navigation' style='display:none'>
<a href="#NAME">NAME</a>
<a href="#SYNOPSIS">SYNOPSIS</a>
<a href="#DESCRIPTION">DESCRIPTION</a>
<a href="#OPTIONS">OPTIONS</a>
<a href="#LOG-LEVELS">LOG LEVELS</a>
<a href="#OUTPUT-FORMATS">OUTPUT FORMATS</a>
<a href="#DTRACE-SUPPORT">DTRACE SUPPORT</a>
<a href="#ENVIRONMENT">ENVIRONMENT</a>
<a href="#PROJECT-BUGS">PROJECT &amp; BUGS</a>
<a href="#LICENSE">LICENSE</a>
<a href="#COPYRIGHT">COPYRIGHT</a>
</div>
<ol class='man-decor man-head man head'>
<li class='tl'>bunyan(1)</li>
<li class='tc'>bunyan manual</li>
<li class='tr'>bunyan(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>bunyan</code> - <span class="man-whatis">filter and pretty-print Bunyan log file content</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>bunyan</code> [OPTIONS]</p>
<p>... | <code>bunyan</code> [OPTIONS]</p>
<p><code>bunyan</code> [OPTIONS] -p PID</p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p>"Bunyan" is <strong>a simple and fast a JSON logging library</strong> for node.js services,
a one-JSON-object-per-line log format, and <strong>a <code>bunyan</code> CLI tool</strong> for nicely
viewing those logs. This man page describes the latter.</p>
<h3 id="Pretty-printing">Pretty-printing</h3>
<p>A bunyan log file is a stream of JSON objects, optionally interspersed with
non-JSON log lines. The primary usage of <a href="bunyan.1.html" class="man-ref">bunyan<span class="s">(1)</span></a> is to pretty print,
for example:</p>
<pre><code>$ bunyan foo.log # or `cat foo.log | bunyan
[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message
extra: multi
line
[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message
...
</code></pre>
<p>By default the "long" output format is used. Use the <code>-o FORMAT</code> option to
emit other formats. E.g.:</p>
<pre><code>$ bunyan foo.log -o short
22:56:52.856Z INFO myservice: My message
extra: multi
line
22:56:54.856Z ERROR myservice: My message
...
</code></pre>
<p>These will color the output if supported in your terminal.
See "OUTPUT FORMATS" below.</p>
<h3 id="Filtering">Filtering</h3>
<p>The <code>bunyan</code> CLI can also be used to filter a bunyan log. Use <code>-l LEVEL</code>
to filter by level:</p>
<pre><code>$ bunyan foo.log -l error # show only 'error' level records
[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message
</code></pre>
<p>Use <code>-c COND</code> to filter on a JavaScript expression returning true on the
record data. In the COND code, <code>this</code> refers to the record object:</p>
<pre><code>$ bunyan foo.log -c `this.three` # show records with the 'extra' field
[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message
extra: multi
line
</code></pre>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt><code>-h</code>, <code>--help</code></dt><dd><p>Print this help info and exit.</p></dd>
<dt><code>--version</code></dt><dd><p>Print version of this command and exit.</p></dd>
<dt><code>-q</code>, <code>--quiet</code></dt><dd><p>Don't warn if input isn't valid JSON.</p></dd>
</dl>
<p>Dtrace options (only on dtrace-supporting platforms):</p>
<dl>
<dt><code>-p PID</code>, <code>-p NAME</code></dt><dd>Process bunyan:log-* probes from the process with the given PID.
Can be used multiple times, or specify all processes with '*',
or a set of processes whose command &amp; args match a pattern with
'-p NAME'.</dd>
</dl>
<p>Filtering options:</p>
<dl>
<dt><code>-l</code>, <code>--level LEVEL</code></dt><dd><p>Only show messages at or above the specified level. You can specify level
<em>names</em> or numeric values. (See 'Log Levels' below.)</p></dd>
<dt><code>-c COND</code>, <code>--condition COND</code></dt><dd><p>Run each log message through the condition and only show those that
resolve to a truish value. E.g. <code>-c 'this.pid == 123'</code>.</p></dd>
<dt><code>--strict</code></dt><dd><p>Suppress all but legal Bunyan JSON log lines. By default non-JSON, and
non-Bunyan lines are passed through.</p></dd>
</dl>
<p>Output options:</p>
<dl>
<dt class="flush"><code>--color</code></dt><dd><p>Colorize output. Defaults to try if output stream is a TTY.</p></dd>
<dt><code>--no-color</code></dt><dd><p>Force no coloring (e.g. terminal doesn't support it)</p></dd>
<dt><code>-o FORMAT</code>, <code>--output FORMAT</code></dt><dd><p>Specify an output format. One of <code>paul</code> (the default), <code>short</code>, <code>json</code>,
<code>json-N</code>, or <code>inspect</code>.</p></dd>
<dt class="flush"><code>-j</code></dt><dd><p>Shortcut for <code>-o json</code>.</p></dd>
</dl>
<h2 id="LOG-LEVELS">LOG LEVELS</h2>
<p>In Bunyan log records, then <code>level</code> field is a number. For the <code>-l|--level</code>
argument the level <strong>names</strong> are supported as shortcuts. In <code>-c|--condition</code>
scripts, uppercase symbols like "DEBUG" are defined for convenience.</p>
<pre><code>Level Name Level Number Symbol in COND Scripts
trace 10 TRACE
debug 20 DEBUG
info 30 INFO
warn 40 WARN
error 50 ERROR
fatal 60 FATAL
</code></pre>
<h2 id="OUTPUT-FORMATS">OUTPUT FORMATS</h2>
<pre><code>FORMAT NAME DESCRIPTION
paul (default) The default output. Long form. Colored and "pretty".
'req' and 'res' and 'err' fields are rendered specially
as an HTTP request, HTTP response and exception
stack trace, respectively. Note: the "paul" name
is deprecated and will be changed to "long".
short Like the default output, but more concise. Some
typically redundant fields are ellided.
json JSON output, 2-space indentation.
json-N JSON output, N-space indentation, e.g. "json-0"
inspect Node.js `util.inspect` output.
</code></pre>
<h2 id="DTRACE-SUPPORT">DTRACE SUPPORT</h2>
<p>On systems that support DTrace (e.g., MacOS, FreeBSD, illumos derivatives
like SmartOS and OmniOS), Bunyan will create a DTrace provider (<code>bunyan</code>)
that makes available the following probes:</p>
<pre><code>log-trace
log-debug
log-info
log-warn
log-error
log-fatal
</code></pre>
<p>Each of these probes has a single argument: the string that would be
written to the log. Note that when a probe is enabled, it will
fire whenever the corresponding function is called, even if the level of
the log message is less than that of any stream.</p>
<p>See <a href="https://github.com/trentm/node-bunyan#dtrace-support" data-bare-link="true">https://github.com/trentm/node-bunyan#dtrace-support</a> for more details
and the '-p PID' option above for convenience usage.</p>
<h2 id="ENVIRONMENT">ENVIRONMENT</h2>
<dl>
<dt><code>BUNYAN_NO_COLOR</code></dt><dd>Set to a non-empty value to force no output coloring. See '--no-color'.</dd>
</dl>
<h2 id="PROJECT-BUGS">PROJECT &amp; BUGS</h2>
<p><code>bunyan</code> is written in JavaScript and requires node.js (<code>node</code>). The project
lives at <a href="https://github.com/trentm/node-bunyan" data-bare-link="true">https://github.com/trentm/node-bunyan</a> and is published to npm as
"bunyan".</p>
<ul>
<li>README, Install notes: <a href="https://github.com/trentm/node-bunyan#readme" data-bare-link="true">https://github.com/trentm/node-bunyan#readme</a></li>
<li>Report bugs to <a href="https://github.com/trentm/node-bunyan/issues" data-bare-link="true">https://github.com/trentm/node-bunyan/issues</a>.</li>
<li>See the full changelog at: <a href="https://github.com/trentm/node-bunyan/blob/master/CHANGES.md" data-bare-link="true">https://github.com/trentm/node-bunyan/blob/master/CHANGES.md</a></li>
</ul>
<h2 id="LICENSE">LICENSE</h2>
<p>MIT License (see <a href="https://github.com/trentm/node-bunyan/blob/master/LICENSE.txt" data-bare-link="true">https://github.com/trentm/node-bunyan/blob/master/LICENSE.txt</a>)</p>
<h2 id="COPYRIGHT">COPYRIGHT</h2>
<p>node-bunyan is Copyright (c) 2012 Joyent, Inc. Copyright (c) 2012 Trent Mick.
All rights reserved.</p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>November 2012</li>
<li class='tr'>bunyan(1)</li>
</ol>
</div>
<a href="https://github.com/trentm/node-bunyan"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a></body>
</html>

bunyan(1) -- filter and pretty-print Bunyan log file content

SYNOPSIS

bunyan [OPTIONS]

... | bunyan [OPTIONS]

bunyan [OPTIONS] -p PID

DESCRIPTION

"Bunyan" is a simple and fast a JSON logging library for node.js services, a one-JSON-object-per-line log format, and a bunyan CLI tool for nicely viewing those logs. This man page describes the latter.

Pretty-printing

A bunyan log file is a stream of JSON objects, optionally interspersed with non-JSON log lines. The primary usage of bunyan(1) is to pretty print, for example:

$ bunyan foo.log          # or `cat foo.log | bunyan
[2012-02-08T22:56:52.856Z]  INFO: myservice/123 on example.com: My message
    extra: multi
    line
[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message
...

By default the "long" output format is used. Use the -o FORMAT option to emit other formats. E.g.:

$ bunyan foo.log -o short
22:56:52.856Z  INFO myservice: My message
    extra: multi
    line
22:56:54.856Z ERROR myservice: My message
...

These will color the output if supported in your terminal. See "OUTPUT FORMATS" below.

Filtering

The bunyan CLI can also be used to filter a bunyan log. Use -l LEVEL to filter by level:

$ bunyan foo.log -l error       # show only 'error' level records
[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message

Use -c COND to filter on a JavaScript expression returning true on the record data. In the COND code, this refers to the record object:

$ bunyan foo.log -c `this.three`     # show records with the 'extra' field
[2012-02-08T22:56:52.856Z]  INFO: myservice/123 on example.com: My message
    extra: multi
    line

OPTIONS

  • -h, --help: Print this help info and exit.

  • --version: Print version of this command and exit.

  • -q, --quiet: Don't warn if input isn't valid JSON.

Dtrace options (only on dtrace-supporting platforms):

  • -p PID, -p NAME: Process bunyan:log-* probes from the process with the given PID. Can be used multiple times, or specify all processes with '*', or a set of processes whose command & args match a pattern with '-p NAME'.

Filtering options:

  • -l, --level LEVEL: Only show messages at or above the specified level. You can specify level names or numeric values. (See 'Log Levels' below.)

  • -c COND, --condition COND: Run each log message through the condition and only show those that resolve to a truish value. E.g. -c 'this.pid == 123'.

  • --strict: Suppress all but legal Bunyan JSON log lines. By default non-JSON, and non-Bunyan lines are passed through.

Output options:

  • --color: Colorize output. Defaults to try if output stream is a TTY.

  • --no-color: Force no coloring (e.g. terminal doesn't support it)

  • -o FORMAT, --output FORMAT: Specify an output format. One of long (the default), short, json, json-N, bunyan (the native bunyan 0-indent JSON output) or inspect.

  • -j: Shortcut for -o json.

LOG LEVELS

In Bunyan log records, then level field is a number. For the -l|--level argument the level names are supported as shortcuts. In -c|--condition scripts, uppercase symbols like "DEBUG" are defined for convenience.

Level Name      Level Number    Symbol in COND Scripts
trace           10              TRACE
debug           20              DEBUG
info            30              INFO
warn            40              WARN
error           50              ERROR
fatal           60              FATAL

OUTPUT FORMATS

FORMAT NAME         DESCRIPTION
long (default)      The default output. Long form. Colored and "pretty".
                    'req' and 'res' and 'err' fields are rendered specially
                    as an HTTP request, HTTP response and exception
                    stack trace, respectively. For backward compat, the
                    name "paul" also works for this.
short               Like the default output, but more concise. Some
                    typically redundant fields are ellided.
json                JSON output, 2-space indentation.
json-N              JSON output, N-space indentation, e.g. "json-4"
bunyan              Alias for "json-0", the Bunyan "native" format.
inspect             Node.js `util.inspect` output.

DTRACE SUPPORT

On systems that support DTrace (e.g., MacOS, FreeBSD, illumos derivatives like SmartOS and OmniOS), Bunyan will create a DTrace provider (bunyan) that makes available the following probes:

log-trace
log-debug
log-info
log-warn
log-error
log-fatal

Each of these probes has a single argument: the string that would be written to the log. Note that when a probe is enabled, it will fire whenever the corresponding function is called, even if the level of the log message is less than that of any stream.

See https://github.com/trentm/node-bunyan#dtrace-support for more details and the '-p PID' option above for convenience usage.

ENVIRONMENT

  • BUNYAN_NO_COLOR: Set to a non-empty value to force no output coloring. See '--no-color'.

PROJECT & BUGS

bunyan is written in JavaScript and requires node.js (node). The project lives at https://github.com/trentm/node-bunyan and is published to npm as "bunyan".

LICENSE

MIT License (see https://github.com/trentm/node-bunyan/blob/master/LICENSE.txt)

COPYRIGHT

node-bunyan is Copyright (c) 2012 Joyent, Inc. Copyright (c) 2012 Trent Mick. All rights reserved.

// Example logging an error:
var http = require('http');
var Logger = require('../lib/bunyan');
var util = require('util');
var log = new Logger({
name: 'myserver',
serializers: {
err: Logger.stdSerializers.err, // <--- use this
}
});
try {
throw new TypeError('boom');
} catch (err) {
log.warn({err: err}, 'operation went boom: %s', err) // <--- here
}
log.info(new TypeError('how about this?')) // <--- alternatively this
try {
throw 'boom string';
} catch (err) {
log.error(err)
}
/* BEGIN JSSTYLED */
/**
*
* $ node err.js | ../bin/bunyan -j
* {
* "name": "myserver",
* "hostname": "banana.local",
* "err": {
* "stack": "TypeError: boom\n at Object.<anonymous> (/Users/trentm/tm/node-bunyan/examples/err.js:15:9)\n at Module._compile (module.js:411:26)\n at Object..js (module.js:417:10)\n at Module.load (module.js:343:31)\n at Function._load (module.js:302:12)\n at Array.0 (module.js:430:10)\n at EventEmitter._tickCallback (node.js:126:26)",
* "name": "TypeError",
* "message": "boom"
* },
* "level": 4,
* "msg": "operation went boom: TypeError: boom",
* "time": "2012-02-02T04:42:53.206Z",
* "v": 0
* }
* $ node err.js | ../bin/bunyan
* [2012-02-02T05:02:39.412Z] WARN: myserver on banana.local: operation went boom: TypeError: boom
* TypeError: boom
* at Object.<anonymous> (/Users/trentm/tm/node-bunyan/examples/err.js:15:9)
* at Module._compile (module.js:411:26)
* at Object..js (module.js:417:10)
* at Module.load (module.js:343:31)
* at Function._load (module.js:302:12)
* at Array.0 (module.js:430:10)
* at EventEmitter._tickCallback (node.js:126:26)
*
*/
/* END JSSTYLED */
// Example handling an fs error for a Bunyan-created
// stream: we create a logger to a file that is read-only.
var fs = require('fs');
var path = require('path');
var Logger = require('../lib/bunyan');
var FILENAME = 'handle-fs-error.log';
var S_IWUSR = 00200; // mask for owner write permission in stat mode
console.warn('- Log file is "%s".', FILENAME);
if (!path.existsSync(FILENAME)) {
console.warn('- Touch log file.');
fs.writeFileSync(FILENAME, 'touch\n');
}
if (fs.statSync(FILENAME).mode & S_IWUSR) {
console.warn('- Make log file read-only.');
fs.chmodSync(FILENAME, 0444);
}
console.warn('- Create logger.')
var log = new Logger({name: 'handle-fs-error', streams: [{path: FILENAME}]});
log.on('error', function (err) {
console.warn('- The logger emitted an error:', err);
});
console.warn('- Call log.info(...).')
log.info('info log message');
console.warn('- Called log.info(...).')
setTimeout(function () {
console.warn('- Call log.warn(...).')
log.warn('warn log message');
console.warn('- Called log.warn(...).')
}, 1000);
var Logger = require('../lib/bunyan');
// Basic usage.
var log = new Logger({name: 'myapp', level: 'info', src: true});
// isInfoEnabled replacement
console.log('log.info() is:', log.info())
// `util.format`-based printf handling
log.info('hi');
log.info('hi', 'trent');
log.info('hi %s there', true);
// First arg as an object adds fields to the log record.
log.info({foo:'bar', multiline:'one\ntwo\nthree'}, 'hi %d', 1, 'two', 3);
// Shows `log.child(...)` to specialize a logger for a sub-component.
console.log('\n')
function Wuzzle(options) {
this.log = options.log;
this.log.info('creating a wuzzle')
}
Wuzzle.prototype.woos = function () {
this.log.warn('This wuzzle is woosey.')
}
var wuzzle = new Wuzzle({log: log.child({component: 'wuzzle'})});
wuzzle.woos();
log.info('done with the wuzzle')
// Play with setting levels.
//
// TODO: put this in a damn test suite
var Logger = require('../lib/bunyan'),
DEBUG = Logger.DEBUG,
INFO = Logger.INFO,
WARN = Logger.WARN;
var assert = require('assert');
// Basic usage.
var log = new Logger({
name: 'example-level',
streams: [
{
name: 'stdout',
stream: process.stdout,
level: 'debug'
},
{
name: 'stderr',
stream: process.stderr
}
]
});
assert.equal(log.level(), DEBUG);
assert.equal(log.levels()[0], DEBUG);
assert.equal(log.levels()[1], INFO);
assert.equal(log.levels(0), DEBUG);
assert.equal(log.levels(1), INFO);
assert.equal(log.levels('stdout'), DEBUG)
try {
log.levels('foo')
} catch (e) {
assert.ok(e.message.indexOf('name') !== -1)
}
log.trace('no one should see this')
log.debug('should see this once (on stdout)')
log.info('should see this twice')
log.levels('stdout', INFO)
log.debug('no one should see this either')
log.level('trace')
log.trace('should see this twice as 4th and 5th emitted log messages')
/*
* A long-running process that does some periodic logging. Use bunyan with
* it some of these ways:
*
* 1. Direct piping:
* node long-running.js | bunyan
* 2. Logging to file (e.g. if run via a service system like upstart or
* illumos' SMF that sends std output to a log file), then tail -f that
* log file.
* node long-running.js > long-running.log 2>&1
* tail -f long-running.log | bunyan
* 3. Dtrace to watch the logging. This has the bonus of being able to watch
* all log levels... even if not normally emitted.
* node long-running.js > long-running.log 2>&1
* bunyan -p $(head -1 long-running.log | json pid)
*
*/
var fs = require('fs');
var bunyan = require('../lib/bunyan');
function randint(n) {
return Math.floor(Math.random() * n);
}
function randchoice(array) {
return array[randint(array.length)];
}
//---- mainline
var words = fs.readFileSync(
__dirname + '/long-running.js', 'utf8').split(/\s+/);
var levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
var timeout;
var log = bunyan.createLogger({name: 'lr', level: 'debug'});
// We're logging to stdout. Let's exit gracefully on EPIPE. E.g. if piped
// to `head` which will close after N lines.
process.stdout.on('error', function (err) {
if (err.code === 'EPIPE') {
process.exit(0);
}
})
function logOne() {
var level = randchoice(levels);
var msg = [randchoice(words), randchoice(words)].join(' ');
var delay = randint(300);
//console.warn('long-running about to log.%s(..., "%s")', level, msg)
log[level]({'word': randchoice(words), 'delay': delay}, msg);
timeout = setTimeout(logOne, delay);
}
log.info('hi, this is the start');
timeout = setTimeout(logOne, 1000);
var Logger = require('../lib/bunyan');
log = new Logger({
name: 'amon',
streams: [
{
level: 'info',
stream: process.stdout,
},
{
level: 'error',
path: 'multi.log'
}
]
});
log.debug('hi nobody on debug');
log.info('hi stdout on info');
log.error('hi both on error');
log.fatal('hi both on fatal');
// Example of a "raw" stream in a Bunyan Logger. A raw stream is one to
// which log record *objects* are written instead of the JSON-serialized
// string.
var Logger = require('../lib/bunyan');
/**
* A raw Bunyan Logger stream object. It takes raw log records and writes
* them to stdout with an added "yo": "yo" field.
*/
function MyRawStream() {}
MyRawStream.prototype.write = function (rec) {
if (typeof (rec) !== 'object') {
console.error('error: raw stream got a non-object record: %j', rec)
} else {
rec.yo = 'yo';
process.stdout.write(JSON.stringify(rec) + '\n');
}
}
// A Logger using the raw stream.
var log = new Logger({
name: 'raw-example',
streams: [
{
level: 'info',
stream: new MyRawStream(),
type: 'raw'
},
]
});
log.info('hi raw stream');
log.info({foo: 'bar'}, 'added "foo" key');
/* Create a ring buffer that stores the last 100 records. */
var bunyan = require('..');
var ringbuffer = new bunyan.RingBuffer({ limit: 100 });
var log = new bunyan({
name: 'foo',
streams: [ {
type: 'raw',
stream: ringbuffer,
level: 'debug'
} ]
});
log.info('hello world');
console.log(ringbuffer.records);
// Example logging HTTP server request and response objects.
var http = require('http');
var Logger = require('../lib/bunyan');
var log = new Logger({
name: 'myserver',
serializers: {
req: Logger.stdSerializers.req,
res: Logger.stdSerializers.res
}
});
var server = http.createServer(function (req, res) {
log.info({req: req}, 'start request'); // <-- this is the guy we're testing
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
log.info({res: res}, 'done response'); // <-- this is the guy we're testing
});
server.listen(1337, '127.0.0.1', function () {
log.info('server listening');
var options = {
port: 1337,
hostname: '127.0.0.1',
path: '/path?q=1#anchor',
headers: {
'X-Hi': 'Mom'
}
};
var req = http.request(options);
req.on('response', function (res) {
res.on('end', function () {
process.exit();
})
});
req.write('hi from the client');
req.end();
});
/* BEGIN JSSTYLED */
/**
*
* $ node server.js
* {"service":"myserver","hostname":"banana.local","level":3,"msg":"server listening","time":"2012-02-02T05:32:13.257Z","v":0}
* {"service":"myserver","hostname":"banana.local","req":{"method":"GET","url":"/path?q=1#anchor","headers":{"x-hi":"Mom","connection":"close"}},"level":3,"msg":"start request","time":"2012-02-02T05:32:13.260Z","v":0}
* {"service":"myserver","hostname":"banana.local","res":{"statusCode":200,"_hasBody":true,"_header":"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n","_trailer":""},"level":3,"msg":"done response","time":"2012-02-02T05:32:13.261Z","v":0}
*
* $ node server.js | ../bin/bunyan
* [2012-02-02T05:32:16.006Z] INFO: myserver on banana.local: server listening
* [2012-02-02T05:32:16.010Z] INFO: myserver on banana.local: start request
* GET /path?q=1#anchor
* x-hi: Mom
* connection: close
* [2012-02-02T05:32:16.011Z] INFO: myserver on banana.local: done response
* HTTP/1.1 200 OK
* Content-Type: text/plain
* Connection: close
* Transfer-Encoding: chunked
* (body)
*
*/
/* END JSSTYLED */
// Show the usage of `src: true` config option to get log call source info in
// log records (the `src` field).
var Logger = require('../lib/bunyan');
var log = new Logger({name: 'src-example', src: true});
log.info('one');
log.info('two');
function doSomeFoo() {
log.info({foo:'bar'}, 'three');
}
doSomeFoo();
function Wuzzle(options) {
this.log = options.log;
this.log.info('creating a wuzzle')
}
Wuzzle.prototype.woos = function () {
this.log.warn('This wuzzle is woosey.')
}
var wuzzle = new Wuzzle({log: log.child({component: 'wuzzle'})});
wuzzle.woos();
log.info('done with the wuzzle')
// See how bunyan behaves with an un-stringify-able object.
var Logger = require('../lib/bunyan');
var log = new Logger({src: true, name: 'foo'});
// Make a circular object (cannot be JSON-ified).
var myobj = {
foo: 'bar'
};
myobj.myobj = myobj;
log.info({obj: myobj}, 'hi there'); // <--- here
/*
* Copyright (c) 2013 Trent Mick. All rights reserved.
*
* The bunyan logging library for node.js.
*/
var VERSION = '0.22.1';
// Bunyan log format version. This becomes the 'v' field on all log records.
// `0` is until I release a version '1.0.0' of node-bunyan. Thereafter,
// starting with `1`, this will be incremented if there is any backward
// incompatible change to the log record format. Details will be in
// 'CHANGES.md' (the change log).
var LOG_VERSION = 0;
var xxx = function xxx(s) { // internal dev/debug logging
var args = ['XX' + 'X: '+s].concat(
Array.prototype.slice.call(arguments, 1));
console.error.apply(this, args);
};
var xxx = function xxx() {}; // comment out to turn on debug logging
var os = require('os');
var fs = require('fs');
var util = require('util');
var assert = require('assert');
try {
var dtrace = require('dtrace-provider');
} catch (e) {
dtrace = null;
}
var EventEmitter = require('events').EventEmitter;
// The 'mv' module is required for rotating-file stream support.
try {
var mv = require('mv');
} catch (e) {
mv = null;
}
//---- Internal support stuff
function objCopy(obj) {
if (obj === null) {
return null;
} else if (Array.isArray(obj)) {
return obj.slice();
} else {
var copy = {};
Object.keys(obj).forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
}
var format = util.format;
if (!format) {
// If node < 0.6, then use its `util.format`:
// <https://github.com/joyent/node/blob/master/lib/util.js#L22>:
var inspect = util.inspect;
var formatRegExp = /%[sdj%]/g;
format = function format(f) {
if (typeof (f) !== 'string') {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function (x) {
if (i >= len)
return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j': return JSON.stringify(args[i++], safeCycles());
case '%%': return '%';
default:
return x;
}
});
for (var x = args[i]; i < len; x = args[++i]) {
if (x === null || typeof (x) !== 'object') {
str += ' ' + x;
} else {
str += ' ' + inspect(x);
}
}
return str;
};
}
/**
* Gather some caller info 3 stack levels up.
* See <http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi>.
*/
function getCaller3Info() {
var obj = {};
var saveLimit = Error.stackTraceLimit;
var savePrepare = Error.prepareStackTrace;
Error.stackTraceLimit = 3;
Error.captureStackTrace(this, getCaller3Info);
Error.prepareStackTrace = function (_, stack) {
var caller = stack[2];
obj.file = caller.getFileName();
obj.line = caller.getLineNumber();
var func = caller.getFunctionName();
if (func)
obj.func = func;
};
this.stack;
Error.stackTraceLimit = saveLimit;
Error.prepareStackTrace = savePrepare;
return obj;
}
/**
* Warn about an bunyan processing error.
*
* If file/line are given, this makes an attempt to warn on stderr only once.
*
* @param msg {String} Message with which to warn.
* @param file {String} Optional. File path relevant for the warning.
* @param line {String} Optional. Line number in `file` path relevant for
* the warning.
*/
function _warn(msg, file, line) {
assert.ok(msg);
var key;
if (file && line) {
key = file + ':' + line;
if (_warned[key]) {
return;
}
_warned[key] = true;
}
process.stderr.write(msg + '\n');
}
var _warned = {};
//---- Levels
var TRACE = 10;
var DEBUG = 20;
var INFO = 30;
var WARN = 40;
var ERROR = 50;
var FATAL = 60;
var levelFromName = {
'trace': TRACE,
'debug': DEBUG,
'info': INFO,
'warn': WARN,
'error': ERROR,
'fatal': FATAL
};
// Dtrace probes.
var dtp = undefined;
var probes = dtrace && {};
/**
* Resolve a level number, name (upper or lowercase) to a level number value.
*
* @api public
*/
function resolveLevel(nameOrNum) {
var level = (typeof (nameOrNum) === 'string'
? levelFromName[nameOrNum.toLowerCase()]
: nameOrNum);
if (! (TRACE <= level && level <= FATAL)) {
throw new Error('invalid level: ' + nameOrNum);
}
return level;
}
//---- Logger class
/**
* Create a Logger instance.
*
* @param options {Object} See documentation for full details. At minimum
* this must include a 'name' string key. Configuration keys:
* - `streams`: specify the logger output streams. This is an array of
* objects with these fields:
* - `type`: The stream type. See README.md for full details.
* Often this is implied by the other fields. Examples are
* 'file', 'stream' and "raw".
* - `level`: Defaults to 'info'.
* - `path` or `stream`: The specify the file path or writeable
* stream to which log records are written. E.g.
* `stream: process.stdout`.
* - `closeOnExit` (boolean): Optional. Default is true for a
* 'file' stream when `path` is given, false otherwise.
* See README.md for full details.
* - `level`: set the level for a single output stream (cannot be used
* with `streams`)
* - `stream`: the output stream for a logger with just one, e.g.
* `process.stdout` (cannot be used with `streams`)
* - `serializers`: object mapping log record field names to
* serializing functions. See README.md for details.
* - `src`: Boolean (default false). Set true to enable 'src' automatic
* field with log call source info.
* All other keys are log record fields.
*
* An alternative *internal* call signature is used for creating a child:
* new Logger(<parent logger>, <child options>[, <child opts are simple>]);
*
* @param _childSimple (Boolean) An assertion that the given `_childOptions`
* (a) only add fields (no config) and (b) no serialization handling is
* required for them. IOW, this is a fast path for frequent child
* creation.
*/
function Logger(options, _childOptions, _childSimple) {
xxx('Logger start:', options)
if (! this instanceof Logger) {
return new Logger(options, _childOptions);
}
// Input arg validation.
var parent;
if (_childOptions !== undefined) {
parent = options;
options = _childOptions;
if (! parent instanceof Logger) {
throw new TypeError(
'invalid Logger creation: do not pass a second arg');
}
}
if (!options) {
throw new TypeError('options (object) is required');
}
if (!parent) {
if (!options.name) {
throw new TypeError('options.name (string) is required');
}
} else {
if (options.name) {
throw new TypeError(
'invalid options.name: child cannot set logger name');
}
}
if (options.stream && options.streams) {
throw new TypeError('cannot mix "streams" and "stream" options');
}
if (options.streams && !Array.isArray(options.streams)) {
throw new TypeError('invalid options.streams: must be an array')
}
if (options.serializers && (typeof (options.serializers) !== 'object' ||
Array.isArray(options.serializers))) {
throw new TypeError('invalid options.serializers: must be an object')
}
EventEmitter.call(this);
// Fast path for simple child creation.
if (parent && _childSimple) {
// `_isSimpleChild` is a signal to stream close handling that this child
// owns none of its streams.
this._isSimpleChild = true;
this._level = parent._level;
this.streams = parent.streams;
this.serializers = parent.serializers;
this.src = parent.src;
var fields = this.fields = {};
var parentFieldNames = Object.keys(parent.fields);
for (var i = 0; i < parentFieldNames.length; i++) {
var name = parentFieldNames[i];
fields[name] = parent.fields[name];
}
var names = Object.keys(options);
for (var i = 0; i < names.length; i++) {
var name = names[i];
fields[name] = options[name];
}
return;
}
// Null values.
var self = this;
if (parent) {
this._level = parent._level;
this.streams = [];
for (var i = 0; i < parent.streams.length; i++) {
var s = objCopy(parent.streams[i]);
s.closeOnExit = false; // Don't own parent stream.
this.streams.push(s);
}
this.serializers = objCopy(parent.serializers);
this.src = parent.src;
this.fields = objCopy(parent.fields);
if (options.level) {
this.level(options.level);
}
} else {
this._level = Number.POSITIVE_INFINITY;
this.streams = [];
this.serializers = null;
this.src = false;
this.fields = {};
}
if (!dtp && dtrace) {
dtp = dtrace.createDTraceProvider('bunyan');
for (var level in levelFromName) {
var probe;
probes[levelFromName[level]] = probe =
dtp.addProbe('log-' + level, 'char *');
// Explicitly add a reference to dtp to prevent it from being GC'd
probe.dtp = dtp;
}
dtp.enable();
}
// Helpers
function addStream(s) {
s = objCopy(s);
// Implicit 'type' from other args.
var type = s.type;
if (!s.type) {
if (s.stream) {
s.type = 'stream';
} else if (s.path) {
s.type = 'file'
}
}
s.raw = (s.type === 'raw'); // PERF: Allow for faster check in `_emit`.
if (s.level) {
s.level = resolveLevel(s.level);
} else if (options.level) {
s.level = resolveLevel(options.level);
} else {
s.level = INFO;
}
if (s.level < self._level) {
self._level = s.level;
}
switch (s.type) {
case 'stream':
if (!s.closeOnExit) {
s.closeOnExit = false;
}
break;
case 'file':
if (!s.stream) {
s.stream = fs.createWriteStream(s.path,
{flags: 'a', encoding: 'utf8'});
s.stream.on('error', function (err) {
self.emit('error', err, s);
});
if (!s.closeOnExit) {
s.closeOnExit = true;
}
} else {
if (!s.closeOnExit) {
s.closeOnExit = false;
}
}
break;
case 'rotating-file':
assert.ok(!s.stream,
'"rotating-file" stream should not give a "stream"');
assert.ok(s.path);
assert.ok(mv, '"rotating-file" stream type is not supported: '
+ 'missing "mv" module');
s.stream = new RotatingFileStream(s);
if (!s.closeOnExit) {
s.closeOnExit = true;
}
break;
case 'raw':
if (!s.closeOnExit) {
s.closeOnExit = false;
}
break;
default:
throw new TypeError('unknown stream type "' + s.type + '"');
}
self.streams.push(s);
}
function addSerializers(serializers) {
if (!self.serializers) {
self.serializers = {};
}
Object.keys(serializers).forEach(function (field) {
var serializer = serializers[field];
if (typeof (serializer) !== 'function') {
throw new TypeError(format(
'invalid serializer for "%s" field: must be a function',
field));
} else {
self.serializers[field] = serializer;
}
});
}
// Handle *config* options (i.e. options that are not just plain data
// for log records).
if (options.stream) {
addStream({
type: 'stream',
stream: options.stream,
closeOnExit: false,
level: (options.level ? resolveLevel(options.level) : INFO)
});
} else if (options.streams) {
options.streams.forEach(addStream);
} else if (parent && options.level) {
this.level(options.level);
} else if (!parent) {
addStream({
type: 'stream',
stream: process.stdout,
closeOnExit: false,
level: (options.level ? resolveLevel(options.level) : INFO)
});
}
if (options.serializers) {
addSerializers(options.serializers);
}
if (options.src) {
this.src = true;
}
xxx('Logger: ', self)
// Fields.
// These are the default fields for log records (minus the attributes
// removed in this constructor). To allow storing raw log records
// (unrendered), `this.fields` must never be mutated. Create a copy for
// any changes.
var fields = objCopy(options);
delete fields.stream;
delete fields.level;
delete fields.streams;
delete fields.serializers;
delete fields.src;
if (this.serializers) {
this._applySerializers(fields);
}
if (!fields.hostname) {
fields.hostname = os.hostname();
}
if (!fields.pid) {
fields.pid = process.pid;
}
Object.keys(fields).forEach(function (k) {
self.fields[k] = fields[k];
});
}
util.inherits(Logger, EventEmitter);
/**
* Create a child logger, typically to add a few log record fields.
*
* This can be useful when passing a logger to a sub-component, e.g. a
* 'wuzzle' component of your service:
*
* var wuzzleLog = log.child({component: 'wuzzle'})
* var wuzzle = new Wuzzle({..., log: wuzzleLog})
*
* Then log records from the wuzzle code will have the same structure as
* the app log, *plus the component='wuzzle' field*.
*
* @param options {Object} Optional. Set of options to apply to the child.
* All of the same options for a new Logger apply here. Notes:
* - The parent's streams are inherited and cannot be removed in this
* call. Any given `streams` are *added* to the set inherited from
* the parent.
* - The parent's serializers are inherited, though can effectively be
* overwritten by using duplicate keys.
* - Can use `level` to set the level of the streams inherited from
* the parent. The level for the parent is NOT affected.
* @param simple {Boolean} Optional. Set to true to assert that `options`
* (a) only add fields (no config) and (b) no serialization handling is
* required for them. IOW, this is a fast path for frequent child
* creation. See 'tools/timechild.js' for numbers.
*/
Logger.prototype.child = function (options, simple) {
return new Logger(this, options || {}, simple);
}
/**
* A convenience method to reopen 'file' streams on a logger. This can be
* useful with external log rotation utilities that move and re-open log files
* (e.g. logrotate on Linux, logadm on SmartOS/Illumos). Those utilities
* typically have rotation options to copy-and-truncate the log file, but
* you may not want to use that. An alternative is to do this in your
* application:
*
* var log = bunyan.createLogger(...);
* ...
* process.on('SIGUSR2', function () {
* log.reopenFileStreams();
* });
* ...
*
* See <https://github.com/trentm/node-bunyan/issues/104>.
*/
Logger.prototype.reopenFileStreams = function () {
var self = this;
self.streams.forEach(function (s) {
if (s.type === 'file') {
if (s.stream) {
// Not sure if typically would want this, or more immediate
// `s.stream.destroy()`.
s.stream.end();
s.stream.destroySoon();
delete s.stream;
}
s.stream = fs.createWriteStream(s.path,
{flags: 'a', encoding: 'utf8'});
s.stream.on('error', function (err) {
self.emit('error', err, s);
});
}
});
};
/* BEGIN JSSTYLED */
/**
* Close this logger.
*
* This closes streams (that it owns, as per 'endOnClose' attributes on
* streams), etc. Typically you **don't** need to bother calling this.
Logger.prototype.close = function () {
if (this._closed) {
return;
}
if (!this._isSimpleChild) {
self.streams.forEach(function (s) {
if (s.endOnClose) {
xxx('closing stream s:', s);
s.stream.end();
s.endOnClose = false;
}
});
}
this._closed = true;
}
*/
/* END JSSTYLED */
/**
* Get/set the level of all streams on this logger.
*
* Get Usage:
* // Returns the current log level (lowest level of all its streams).
* log.level() -> INFO
*
* Set Usage:
* log.level(INFO) // set all streams to level INFO
* log.level('info') // can use 'info' et al aliases
*/
Logger.prototype.level = function level(value) {
if (value === undefined) {
return this._level;
}
var newLevel = resolveLevel(value);
var len = this.streams.length;
for (var i = 0; i < len; i++) {
this.streams[i].level = newLevel;
}
this._level = newLevel;
}
/**
* Get/set the level of a particular stream on this logger.
*
* Get Usage:
* // Returns an array of the levels of each stream.
* log.levels() -> [TRACE, INFO]
*
* // Returns a level of the identified stream.
* log.levels(0) -> TRACE // level of stream at index 0
* log.levels('foo') // level of stream with name 'foo'
*
* Set Usage:
* log.levels(0, INFO) // set level of stream 0 to INFO
* log.levels(0, 'info') // can use 'info' et al aliases
* log.levels('foo', WARN) // set stream named 'foo' to WARN
*
* Stream names: When streams are defined, they can optionally be given
* a name. For example,
* log = new Logger({
* streams: [
* {
* name: 'foo',
* path: '/var/log/my-service/foo.log'
* level: 'trace'
* },
* ...
*
* @param name {String|Number} The stream index or name.
* @param value {Number|String} The level value (INFO) or alias ('info').
* If not given, this is a 'get' operation.
* @throws {Error} If there is no stream with the given name.
*/
Logger.prototype.levels = function levels(name, value) {
if (name === undefined) {
assert.equal(value, undefined);
return this.streams.map(
function (s) { return s.level });
}
var stream;
if (typeof (name) === 'number') {
stream = this.streams[name];
if (stream === undefined) {
throw new Error('invalid stream index: ' + name);
}
} else {
var len = this.streams.length;
for (var i = 0; i < len; i++) {
var s = this.streams[i];
if (s.name === name) {
stream = s;
break;
}
}
if (!stream) {
throw new Error(format('no stream with name "%s"', name));
}
}
if (value === undefined) {
return stream.level;
} else {
var newLevel = resolveLevel(value);
stream.level = newLevel;
if (newLevel < this._level) {
this._level = newLevel;
}
}
}
/**
* Apply registered serializers to the appropriate keys in the given fields.
*
* Pre-condition: This is only called if there is at least one serializer.
*
* @param fields (Object) The log record fields.
* @param excludeFields (Object) Optional mapping of keys to `true` for
* keys to NOT apply a serializer.
*/
Logger.prototype._applySerializers = function (fields, excludeFields) {
var self = this;
xxx('_applySerializers: excludeFields', excludeFields);
// Check each serializer against these (presuming number of serializers
// is typically less than number of fields).
Object.keys(this.serializers).forEach(function (name) {
if (fields[name] === undefined ||
(excludeFields && excludeFields[name]))
{
return;
}
xxx('_applySerializers; apply to "%s" key', name)
try {
fields[name] = self.serializers[name](fields[name]);
} catch (err) {
_warn(format('bunyan: ERROR: This should never happen. '
+ 'This is a bug in <https://github.com/trentm/node-bunyan> '
+ 'or in this application. Exception from "%s" Logger '
+ 'serializer: %s',
name, err.stack || err));
fields[name] = format('(Error in Bunyan log "%s" serializer '
+ 'broke field. See stderr for details.)', name);
}
});
}
/**
* Emit a log record.
*
* @param rec {log record}
* @param noemit {Boolean} Optional. Set to true to skip emission
* and just return the JSON string.
*/
Logger.prototype._emit = function (rec, noemit) {
var i;
// Lazily determine if this Logger has non-'raw' streams. If there are
// any, then we need to stringify the log record.
if (this.haveNonRawStreams === undefined) {
this.haveNonRawStreams = false;
for (i = 0; i < this.streams.length; i++) {
if (!this.streams[i].raw) {
this.haveNonRawStreams = true;
break;
}
}
}
// Stringify the object. Attempt to warn/recover on error.
var str;
if (noemit || this.haveNonRawStreams) {
str = JSON.stringify(rec, safeCycles()) + '\n';
}
if (noemit)
return str;
var level = rec.level;
for (i = 0; i < this.streams.length; i++) {
var s = this.streams[i];
if (s.level <= level) {
xxx('writing log rec "%s" to "%s" stream (%d <= %d): %j',
rec.msg, s.type, s.level, level, rec);
s.stream.write(s.raw ? rec : str);
}
};
return str;
}
/**
* Build a log emitter function for level minLevel. I.e. this is the
* creator of `log.info`, `log.error`, etc.
*/
function mkLogEmitter(minLevel) {
return function () {
var log = this;
function mkRecord(args) {
var excludeFields;
if (args[0] instanceof Error) {
// `log.<level>(err, ...)`
fields = {err: errSerializer(args[0])};
excludeFields = {err: true};
if (args.length === 1) {
msgArgs = [fields.err.message];
} else {
msgArgs = Array.prototype.slice.call(args, 1);
}
} else if (typeof (args[0]) === 'string') {
// `log.<level>(msg, ...)`
fields = null;
msgArgs = Array.prototype.slice.call(args);
} else if (Buffer.isBuffer(args[0])) { // `log.<level>(buf, ...)`
// Almost certainly an error, show `inspect(buf)`. See bunyan
// issue #35.
fields = null;
msgArgs = Array.prototype.slice.call(args);
msgArgs[0] = util.inspect(msgArgs[0]);
} else { // `log.<level>(fields, msg, ...)`
fields = args[0];
msgArgs = Array.prototype.slice.call(args, 1);
}
// Build up the record object.
var rec = objCopy(log.fields);
var level = rec.level = minLevel;
var recFields = (fields ? objCopy(fields) : null);
if (recFields) {
if (log.serializers) {
log._applySerializers(recFields, excludeFields);
}
Object.keys(recFields).forEach(function (k) {
rec[k] = recFields[k];
});
}
rec.msg = format.apply(log, msgArgs);
if (!rec.time) {
rec.time = (new Date());
}
// Get call source info
if (log.src && !rec.src) {
rec.src = getCaller3Info()
}
rec.v = LOG_VERSION;
return rec;
};
var fields = null;
var msgArgs = arguments;
var str = null;
var rec = null;
if (arguments.length === 0) { // `log.<level>()`
return (this._level <= minLevel);
} else if (this._level > minLevel) {
/* pass through */
} else {
rec = mkRecord(msgArgs);
str = this._emit(rec);
}
probes && probes[minLevel].fire(function () {
return [ str ||
(rec && log._emit(rec, true)) ||
log._emit(mkRecord(msgArgs), true) ];
});
}
}
/**
* The functions below log a record at a specific level.
*
* Usages:
* log.<level>() -> boolean is-trace-enabled
* log.<level>(<Error> err, [<string> msg, ...])
* log.<level>(<string> msg, ...)
* log.<level>(<object> fields, <string> msg, ...)
*
* where <level> is the lowercase version of the log level. E.g.:
*
* log.info()
*
* @params fields {Object} Optional set of additional fields to log.
* @params msg {String} Log message. This can be followed by additional
* arguments that are handled like
* [util.format](http://nodejs.org/docs/latest/api/all.html#util.format).
*/
Logger.prototype.trace = mkLogEmitter(TRACE);
Logger.prototype.debug = mkLogEmitter(DEBUG);
Logger.prototype.info = mkLogEmitter(INFO);
Logger.prototype.warn = mkLogEmitter(WARN);
Logger.prototype.error = mkLogEmitter(ERROR);
Logger.prototype.fatal = mkLogEmitter(FATAL);
//---- Standard serializers
// A serializer is a function that serializes a JavaScript object to a
// JSON representation for logging. There is a standard set of presumed
// interesting objects in node.js-land.
Logger.stdSerializers = {};
// Serialize an HTTP request.
Logger.stdSerializers.req = function req(req) {
if (!req || !req.connection)
return req;
return {
method: req.method,
url: req.url,
headers: req.headers,
remoteAddress: req.connection.remoteAddress,
remotePort: req.connection.remotePort
};
// Trailers: Skipping for speed. If you need trailers in your app, then
// make a custom serializer.
//if (Object.keys(trailers).length > 0) {
// obj.trailers = req.trailers;
//}
};
// Serialize an HTTP response.
Logger.stdSerializers.res = function res(res) {
if (!res || !res.statusCode)
return res;
return {
statusCode: res.statusCode,
header: res._header
}
};
/*
* This function dumps long stack traces for exceptions having a cause()
* method. The error classes from
* [verror](https://github.com/davepacheco/node-verror) and
* [restify v2.0](https://github.com/mcavage/node-restify) are examples.
*
* Based on `dumpException` in
* https://github.com/davepacheco/node-extsprintf/blob/master/lib/extsprintf.js
*/
function getFullErrorStack(ex)
{
var ret = ex.stack || ex.toString();
if (ex.cause && typeof (ex.cause) === 'function') {
var cex = ex.cause();
if (cex) {
ret += '\nCaused by: ' + getFullErrorStack(cex);
}
}
return (ret);
}
// Serialize an Error object
// (Core error properties are enumerable in node 0.4, not in 0.6).
var errSerializer = Logger.stdSerializers.err = function err(err) {
if (!err || !err.stack)
return err;
var obj = {
message: err.message,
name: err.name,
stack: getFullErrorStack(err),
code: err.code,
signal: err.signal
}
return obj;
};
// A JSON stringifier that handles cycles safely.
// Usage: JSON.stringify(obj, safeCycles())
function safeCycles() {
var seen = [];
return function (key, val) {
if (!val || typeof (val) !== 'object') {
return val;
}
if (seen.indexOf(val) !== -1) {
return '[Circular]';
}
seen.push(val);
return val;
};
}
/**
* XXX
*/
if (mv) {
function RotatingFileStream(options) {
this.path = options.path;
this.stream = fs.createWriteStream(this.path,
{flags: 'a', encoding: 'utf8'});
this.count = (options.count == null ? 10 : options.count);
assert.ok(typeof (this.count) === 'number' && this.count >= 0);
// Parse `options.period`.
if (options.period) {
// <number><scope> where scope is:
// h hours (at the start of the hour)
// d days (at the start of the day, i.e. just after midnight)
// w weeks (at the start of Sunday)
// m months (on the first of the month)
// y years (at the start of Jan 1st)
// with special values 'hourly' (1h), 'daily' (1d), "weekly" (1w),
// 'monthly' (1m) and 'yearly' (1y)
var period = {
'hourly': '1h',
'daily': '1d',
'weekly': '1w',
'monthly': '1m',
'yearly': '1y'
}[options.period] || options.period;
var m = /^([1-9][0-9]*)([hdwmy]|ms)$/.exec(period);
if (!m) {
throw new Error(format('invalid period: "%s"', options.period));
}
this.periodNum = Number(m[1]);
this.periodScope = m[2];
} else {
this.periodNum = 1;
this.periodScope = 'd';
}
// TODO: template support for backup files
// template: <path to which to rotate>
// default is %P.%n
// '/var/log/archive/foo.log' -> foo.log.%n
// '/var/log/archive/foo.log.%n'
// codes:
// XXX support strftime codes (per node version of those)
// or whatever module. Pick non-colliding for extra
// codes
// %P `path` base value
// %n integer number of rotated log (1,2,3,...)
// %d datetime in YYYY-MM-DD_HH-MM-SS
// XXX what should default date format be?
// prior art? Want to avoid ':' in
// filenames (illegal on Windows for one).
this.rotQueue = [];
this.rotating = false;
this._setupNextRot();
}
util.inherits(RotatingFileStream, EventEmitter);
RotatingFileStream.prototype._setupNextRot = function () {
var self = this;
this.rotAt = this._nextRotTime();
this.timeout = setTimeout(
function () { self.rotate(); },
this.rotAt - Date.now());
}
RotatingFileStream.prototype._nextRotTime = function _nextRotTime(first) {
var _DEBUG = false;
if (_DEBUG)
console.log('-- _nextRotTime: %s%s', this.periodNum, this.periodScope);
var d = new Date();
if (_DEBUG) console.log(' now local: %s', d);
if (_DEBUG) console.log(' now utc: %s', d.toISOString());
var rotAt;
switch (this.periodScope) {
case 'ms':
// Hidden millisecond period for debugging.
if (this.rotAt) {
rotAt = this.rotAt + this.periodNum;
} else {
rotAt = Date.now() + this.periodNum;
}
break;
case 'h':
if (this.rotAt) {
rotAt = this.rotAt + this.periodNum * 60 * 60 * 1000;
} else {
// First time: top of the next hour.
rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(),
d.getUTCDate(), d.getUTCHours() + 1);
}
break;
case 'd':
if (this.rotAt) {
rotAt = this.rotAt + this.periodNum * 24 * 60 * 60 * 1000;
} else {
// First time: start of tomorrow (i.e. at the coming midnight) UTC.
rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(),
d.getUTCDate() + 1);
}
break;
case 'w':
// Currently, always on Sunday morning at 00:00:00 (UTC).
if (this.rotAt) {
rotAt = this.rotAt + this.periodNum * 7 * 24 * 60 * 60 * 1000;
} else {
// First time: this coming Sunday.
rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(),
d.getUTCDate() + (7 - d.getUTCDay()));
}
break;
case 'm':
if (this.rotAt) {
rotAt = Date.UTC(d.getUTCFullYear(),
d.getUTCMonth() + this.periodNum, 1);
} else {
// First time: the start of the next month.
rotAt = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
}
break;
case 'y':
if (this.rotAt) {
rotAt = Date.UTC(d.getUTCFullYear() + this.periodNum, 0, 1);
} else {
// First time: the start of the next year.
rotAt = Date.UTC(d.getUTCFullYear() + 1, 0, 1);
}
break;
default:
assert.fail(format('invalid period scope: "%s"', this.periodScope));
}
if (_DEBUG) {
console.log(' **rotAt**: %s (utc: %s)', rotAt,
new Date(rotAt).toUTCString());
var now = Date.now();
console.log(' now: %s (%sms == %smin == %sh to go)',
now,
rotAt - now,
(rotAt-now)/1000/60,
(rotAt-now)/1000/60/60);
}
return rotAt;
};
RotatingFileStream.prototype.rotate = function rotate() {
// XXX What about shutdown?
var self = this;
var _DEBUG = false;
if (_DEBUG) console.log('-- [%s] rotating %s', new Date(), self.path);
if (self.rotating) {
throw new TypeError('cannot start a rotation when already rotating');
}
self.rotating = true;
self.stream.end(); // XXX can do moves sync after this? test at high rate
function del() {
var toDel = self.path + '.' + String(n - 1);
if (n === 0) {
toDel = self.path;
}
n -= 1;
if (_DEBUG) console.log('rm %s', toDel);
fs.unlink(toDel, function (delErr) {
//XXX handle err other than not exists
moves();
});
}
function moves() {
if (self.count === 0 || n < 0) {
return finish();
}
var before = self.path;
var after = self.path + '.' + String(n);
if (n > 0) {
before += '.' + String(n - 1);
}
n -= 1;
fs.exists(before, function (exists) {
if (!exists) {
moves();
} else {
if (_DEBUG) console.log('mv %s %s', before, after);
mv(before, after, function (mvErr) {
if (mvErr) {
self.emit('error', mvErr);
finish(); // XXX finish here?
} else {
moves();
}
});
}
})
}
function finish() {
if (_DEBUG) console.log('open %s', self.path);
self.stream = fs.createWriteStream(self.path,
{flags: 'a', encoding: 'utf8'});
var q = self.rotQueue, len = q.length;
for (var i = 0; i < len; i++) {
self.stream.write(q[i]);
}
self.rotQueue = [];
self.rotating = false;
self.emit('drain');
self._setupNextRot();
}
var n = this.count;
del();
};
RotatingFileStream.prototype.write = function write(s) {
if (this.rotating) {
this.rotQueue.push(s);
return false;
} else {
return this.stream.write(s);
}
};
RotatingFileStream.prototype.end = function end(s) {
this.stream.end();
};
RotatingFileStream.prototype.destroy = function destroy(s) {
this.stream.destroy();
};
RotatingFileStream.prototype.destroySoon = function destroySoon(s) {
this.stream.destroySoon();
};
} /* if (mv) */
/**
* RingBuffer is a Writable Stream that just stores the last N records in
* memory.
*
* @param options {Object}, with the following fields:
*
* - limit: number of records to keep in memory
*/
function RingBuffer(options) {
this.limit = options && options.limit ? options.limit : 100;
this.writable = true;
this.records = [];
EventEmitter.call(this);
}
util.inherits(RingBuffer, EventEmitter);
RingBuffer.prototype.write = function (record) {
if (!this.writable)
throw (new Error('RingBuffer has been ended already'));
this.records.push(record);
if (this.records.length > this.limit)
this.records.shift();
return (true);
};
RingBuffer.prototype.end = function () {
if (arguments.length > 0)
this.write.apply(this, Array.prototype.slice.call(arguments));
this.writable = false;
};
RingBuffer.prototype.destroy = function () {
this.writable = false;
this.emit('close');
};
RingBuffer.prototype.destroySoon = function () {
this.destroy();
};
//---- Exports
module.exports = Logger;
module.exports.TRACE = TRACE;
module.exports.DEBUG = DEBUG;
module.exports.INFO = INFO;
module.exports.WARN = WARN;
module.exports.ERROR = ERROR;
module.exports.FATAL = FATAL;
module.exports.resolveLevel = resolveLevel;
module.exports.VERSION = VERSION;
module.exports.LOG_VERSION = LOG_VERSION;
module.exports.createLogger = function createLogger(options) {
return new Logger(options);
};
module.exports.RingBuffer = RingBuffer;
// Useful for custom `type == 'raw'` streams that may do JSON stringification
// of log records themselves. Usage:
// var str = JSON.stringify(rec, bunyan.safeCycles());
module.exports.safeCycles = safeCycles;
# This is the MIT license
Copyright (c) 2011-2012 Joyent Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#---- Tools
NODEUNIT := ./node_modules/.bin/nodeunit
SUDO := sudo
ifeq ($(shell uname -s),SunOS)
# On SunOS (e.g. SmartOS) we expect to run the test suite as the
# root user -- necessary to run dtrace. Therefore `pfexec` isn't
# necessary.
SUDO :=
endif
DTRACE_UP_IN_HERE=
ifeq ($(shell uname -s),SunOS)
DTRACE_UP_IN_HERE=1
endif
ifeq ($(shell uname -s),Darwin)
DTRACE_UP_IN_HERE=1
endif
NODEOPT ?= $(HOME)/opt
#---- Files
JSSTYLE_FILES := $(shell find lib test tools examples -name "*.js") bin/bunyan
# All test files *except* dtrace.test.js.
NON_DTRACE_TEST_FILES := $(shell ls -1 test/*.test.js | grep -v dtrace | xargs)
#---- Targets
all:
npm install
# Ensure all version-carrying files have the same version.
.PHONY: versioncheck
versioncheck:
[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -1 | awk '{print $$3}'` ]]
[[ `cat package.json | json version` == `grep '^var VERSION' bin/bunyan | awk -F"'" '{print $$2}'` ]]
[[ `cat package.json | json version` == `grep '^var VERSION' lib/bunyan.js | awk -F"'" '{print $$2}'` ]]
@echo Version check ok.
.PHONY: cutarelease
cutarelease: versioncheck
[[ `git status | tail -n1` == "nothing to commit (working directory clean)" ]]
./tools/cutarelease.py -p bunyan -f package.json -f lib/bunyan.js -f bin/bunyan
.PHONY: docs
docs:
@[[ `which ronn` ]] || (echo "No 'ronn' on your PATH. Install with 'gem install ronn'" && exit 2)
mkdir -p man/man1
ronn --style=toc --manual="bunyan manual" --date=$(shell git log -1 --pretty=format:%cd --date=short) --roff --html docs/bunyan.1.ronn
python -c 'import sys; h = open("docs/bunyan.1.html").read(); h = h.replace(".mp dt.flush {float:left;width:8ex}", ""); open("docs/bunyan.1.html", "w").write(h)'
python -c 'import sys; h = open("docs/bunyan.1.html").read(); h = h.replace("</body>", """<a href="https://github.com/trentm/node-bunyan"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a></body>"""); open("docs/bunyan.1.html", "w").write(h)'
@echo "# test with 'man ./docs/bunyan.1' and 'open ./docs/bunyan.1.html'"
.PHONY: publish
publish:
mkdir -p tmp
[[ -d tmp/bunyan-gh-pages ]] || git clone git@github.com:trentm/node-bunyan.git tmp/bunyan-gh-pages
cd tmp/bunyan-gh-pages && git checkout gh-pages && git pull --rebase origin gh-pages
cp docs/index.html tmp/bunyan-gh-pages/index.html
cp docs/bunyan.1.html tmp/bunyan-gh-pages/bunyan.1.html
(cd tmp/bunyan-gh-pages \
&& git commit -a -m "publish latest docs" \
&& git push origin gh-pages || true)
.PHONY: distclean
distclean:
rm -rf node_modules
#---- test
.PHONY: test
test: $(NODEUNIT)
test -z "$(DTRACE_UP_IN_HERE)" || test -n "$(SKIP_DTRACE)" || \
(node -e 'require("dtrace-provider").createDTraceProvider("isthisthingon")' && \
$(SUDO) $(NODEUNIT) test/dtrace.test.js)
$(NODEUNIT) $(NON_DTRACE_TEST_FILES)
# Test will all node supported versions (presumes install locations I use on
# my machine).
# Note: 'test08' is last so (if all is well) I end up with a binary
# dtrace-provider build for node 0.8 (my current version).
.PHONY: testall
testall: test06 test10 test08
.PHONY: test10
test10:
@echo "# Test node 0.10.x (with node `$(NODEOPT)/node-0.10/bin/node --version`)"
@$(NODEOPT)/node-0.10/bin/node --version
PATH="$(NODEOPT)/node-0.10/bin:$(PATH)" make distclean all test
.PHONY: test08
test08:
@echo "# Test node 0.8.x (with node `$(NODEOPT)/node-0.8/bin/node --version`)"
@$(NODEOPT)/node-0.8/bin/node --version
PATH="$(NODEOPT)/node-0.8/bin:$(PATH)" make distclean all test
.PHONY: test06
test06:
@echo "# Test node 0.6.x (with node `$(NODEOPT)/node-0.6/bin/node --version`)"
@$(NODEOPT)/node-0.6/bin/node --version
PATH="$(NODEOPT)/node-0.6/bin:$(PATH)" make distclean all test
#---- check
.PHONY: check-jsstyle
check-jsstyle: $(JSSTYLE_FILES)
./tools/jsstyle -o indent=4,doxygen,unparenthesized-return=0,blank-after-start-comment=0,leading-right-paren-ok=1 $(JSSTYLE_FILES)
.PHONY: check
check: check-jsstyle
@echo "Check ok."
.PHONY: prepush
prepush: check testall
@echo "Okay to push."
var fs;
fs = require('fs');
module.exports = function mv(source, dest, cb){
fs.rename(source, dest, function(err){
if (!err) return cb();
if (err.code !== 'EXDEV') return cb(err);
fs.stat(source, function (err, stats) {
if (err) return cb(err);
if (stats.isFile()) {
moveFileAcrossDevice(source, dest, cb);
} else if (stats.isDirectory()) {
moveDirAcrossDevice(source, dest, cb);
} else {
var err;
err = new Error("source must be file or directory");
err.code = 'NOTFILEORDIR';
cb(err);
}
});
});
}
function moveFileAcrossDevice(source, dest, cb) {
var ins, outs;
ins = fs.createReadStream(source);
outs = fs.createWriteStream(dest);
ins.once('error', function(err){
outs.removeAllListeners('error');
outs.removeAllListeners('close');
outs.destroy();
cb(err);
});
outs.once('error', function(err){
ins.removeAllListeners('error');
outs.removeAllListeners('close');
ins.destroy();
cb(err);
});
outs.once('close', function(){
fs.unlink(source, cb);
});
ins.pipe(outs);
}
// TODO: do this natively instead of shelling out to `mv`
function moveDirAcrossDevice(source, dest, cb) {
var child, stdout, stderr, err;
child = require('child_process').spawn('mv', [source, dest], {stdio: 'pipe'});
child.stderr.setEncoding('utf8');
child.stdout.setEncoding('utf8');
stderr = '';
stdout = '';
child.stderr.on('data', function(data) { stderr += data; });
child.stdout.on('data', function(data) { stdout += data; });
child.on('close', function(code) {
if (code === 0) {
cb();
} else {
err = new Error("mv had nonzero exit code");
err.code = 'RETCODE';
err.stdout = stdout;
err.stderr = stderr;
cb(err);
}
});
}
{
"name": "mv",
"version": "0.0.5",
"description": "fs.rename but works across devices. same as the unix utility 'mv'",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "git://github.com/superjoe30/node-mv.git"
},
"keywords": [
"mv",
"move",
"rename",
"device",
"recursive",
"folder"
],
"author": {
"name": "Andrew Kelley"
},
"license": "BSD",
"engines": {
"node": ">=0.8.0"
},
"devDependencies": {
"proxyquire": "~0.3.4",
"mocha": "~1.6.0"
},
"readme": "[![Build Status](https://secure.travis-ci.org/superjoe30/node-mv.png)](http://travis-ci.org/superjoe30/node-mv)\n\nUsage:\n------\n\n```javascript\nvar mv;\n\nmv = require('mv');\n\nmv('source/file', 'dest/file', function(err) {\n // done. it tried fs.rename first, and then falls back to\n // piping the source file to the dest file and then unlinking\n // the source file.\n});\n```\n",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/superjoe30/node-mv/issues"
},
"homepage": "https://github.com/superjoe30/node-mv",
"_id": "mv@0.0.5",
"dist": {
"shasum": "52ada8bf38ffab2380fbe48075d9b1cf47bc4f00"
},
"_from": "mv@0.0.5",
"_resolved": "https://registry.npmjs.org/mv/-/mv-0.0.5.tgz"
}

Build Status

Usage:

var mv;

mv = require('mv');

mv('source/file', 'dest/file', function(err) {
  // done. it tried fs.rename first, and then falls back to
  // piping the source file to the dest file and then unlinking
  // the source file.
});
var assert, proxyquire, fs;
assert = require('assert');
proxyquire = require('proxyquire');
fs = require('fs');
describe("mv", function() {
var mock_fs, mocked_mv;
// makes fs.rename return cross-device error.
mock_fs = {};
mock_fs.rename = function(src, dest, cb) {
setTimeout(function() {
var err;
err = new Error();
err.code = 'EXDEV';
cb(err);
}, 10);
};
it("should rename a file on the same device", function (done) {
var mv;
mv = proxyquire.resolve('../index', __dirname, {});
mv("test/a-file", "test/a-file-dest", function (err) {
assert.ifError(err);
fs.readFile("test/a-file-dest", 'utf8', function (err, contents) {
assert.ifError(err);
assert.strictEqual(contents, "sonic the hedgehog\n");
// move it back
mv("test/a-file-dest", "test/a-file", done);
});
});
});
it("should work across devices", function (done) {
var mv;
mv = proxyquire.resolve('../index', __dirname, {fs: mock_fs});
mv("test/a-file", "test/a-file-dest", function (err) {
assert.ifError(err);
fs.readFile("test/a-file-dest", 'utf8', function (err, contents) {
assert.ifError(err);
assert.strictEqual(contents, "sonic the hedgehog\n");
// move it back
mv("test/a-file-dest", "test/a-file", done);
});
});
});
it("should move folders", function (done) {
var mv;
mv = proxyquire.resolve('../index', __dirname, {});
mv("test/a-folder", "test/a-folder-dest", function (err) {
assert.ifError(err);
fs.readFile("test/a-folder-dest/another-file", 'utf8', function (err, contents) {
assert.ifError(err);
assert.strictEqual(contents, "tails\n");
// move it back
mv("test/a-folder-dest", "test/a-folder", done);
});
});
});
it("should move folders across devices", function (done) {
var mv;
mv = proxyquire.resolve('../index', __dirname, {fs: mock_fs});
mv("test/a-folder", "test/a-folder-dest", function (err) {
assert.ifError(err);
fs.readFile("test/a-folder-dest/another-folder/file3", 'utf8', function (err, contents) {
assert.ifError(err);
assert.strictEqual(contents, "knuckles\n");
// move it back
mv("test/a-folder-dest", "test/a-folder", done);
});
});
});
});
{
"name": "bunyan",
"version": "0.22.1",
"description": "a JSON Logger library for node.js services",
"author": {
"name": "Trent Mick",
"email": "trentm@gmail.com",
"url": "http://trentm.com"
},
"main": "./lib/bunyan.js",
"bin": {
"bunyan": "./bin/bunyan"
},
"repository": {
"type": "git",
"url": "git://github.com/trentm/node-bunyan.git"
},
"engines": [
"node >=0.6.0"
],
"keywords": [
"log",
"logging",
"log4j",
"json"
],
"dependencies": {
"mv": "0.0.5",
"dtrace-provider": "0.2.8"
},
"// comment": "'mv' required for RotatingFileStream",
"optionalDependencies": {
"mv": "0.0.5",
"dtrace-provider": "0.2.8"
},
"devDependencies": {
"nodeunit": "0.7.4",
"ben": "0.0.0",
"verror": "1.3.3"
},
"scripts": {
"test": "make test"
},
"contributors": [
{
"name": "Trent Mick",
"email": "trentm@gmail.com",
"url": "http://trentm.com"
},
{
"name": "Mark Cavage",
"email": "mcavage@gmail.com",
"url": "https://github.com/mcavage"
},
{
"name": "Dave Pacheco",
"email": "dap@joyent.com",
"url": "https://github.com/davepacheco"
},
{
"name": "Michael Hart",
"url": "https://github.com/mhart"
},
{
"name": "Isaac Schlueter",
"url": "https://github.com/isaacs"
},
{
"name": "Rob Gulewich",
"url": "https://github.com/rgulewich"
},
{
"name": "Bryan Cantrill",
"url": "https://github.com/bcantrill"
}
],
"readme": "Bunyan is **a simple and fast JSON logging library** for node.js services:\n\n var bunyan = require('bunyan');\n var log = bunyan.createLogger({name: \"myapp\"});\n log.info(\"hi\");\n\nand **a `bunyan` CLI tool** for nicely viewing those logs:\n\n![bunyan CLI screenshot](https://raw.github.com/trentm/node-bunyan/master/tools/screenshot1.png)\n\nManifesto: Server logs should be structured. JSON's a good format. Let's do\nthat. A log record is one line of `JSON.stringify`'d output. Let's also\nspecify some common names for the requisite and common fields for a log\nrecord (see below).\n\nAlso: log4j is way more than you need.\n\n\n# Current Status\n\nSolid core functionality is there. Joyent is using this for a number of\nproduction services. Bunyan supports node 0.6 and greater. Follow\n<a href=\"https://twitter.com/intent/user?screen_name=trentmick\" target=\"_blank\">@trentmick</a>\nfor updates to Bunyan.\n\nThere is an email discussion list\n[bunyan-logging@googlegroups.com](mailto:bunyan-logging@googlegroups.com),\nalso [as a forum in the\nbrowser](https://groups.google.com/forum/?fromgroups#!forum/bunyan-logging).\n\n\n# Installation\n\n npm install bunyan\n\n**Tip**: The `bunyan` CLI tool is written to be compatible (within reason) with\nall versions of Bunyan logs. Therefore you might want to `npm install -g bunyan`\nto get the bunyan CLI on your PATH, then use local bunyan installs for\nnode.js library usage of bunyan in your apps.\n\n\n# Features\n\n- elegant [log method API](#log-method-api)\n- extensible [streams](#streams) system for controlling where log records\n go (to a stream, to a file, [log file rotation](#stream-type-rotating-file),\n etc.)\n- [`bunyan` CLI](#cli-usage) for pretty-printing and filtering of Bunyan logs\n- simple include of log call source location (file, line, function) with\n [`src: true`](#src)\n- light-weight specialization of Logger instances with [`log.child`](#logchild)\n- custom rendering of logged objects with [\"serializers\"](#serializers)\n- [Runtime log snooping via Dtrace support](#dtrace-support)\n\n\n# Introduction\n\nLike most logging libraries you create a Logger instance and call methods\nnamed after the logging levels:\n\n $ cat hi.js\n var bunyan = require('bunyan');\n var log = bunyan.createLogger({name: 'myapp'});\n log.info('hi');\n log.warn({lang: 'fr'}, 'au revoir');\n\nAll loggers must provide a \"name\". This is somewhat akin to the log4j logger\n\"name\", but Bunyan doesn't do hierarchical logger names.\n\n**Bunyan log records are JSON.** A few fields are added automatically:\n\"pid\", \"hostname\", \"time\" and \"v\".\n\n $ node hi.js\n {\"name\":\"myapp\",\"hostname\":\"banana.local\",\"pid\":40161,\"level\":30,\"msg\":\"hi\",\"time\":\"2013-01-04T18:46:23.851Z\",\"v\":0}\n {\"name\":\"myapp\",\"hostname\":\"banana.local\",\"pid\":40161,\"level\":40,\"lang\":\"fr\",\"msg\":\"au revoir\",\"time\":\"2013-01-04T18:46:23.853Z\",\"v\":0}\n\n\n## Log Method API\n\nThe example above shows two different ways to call `log.info(...)`. The\nfull API is:\n\n log.info(); // Returns a boolean: is the \"info\" level enabled?\n // This is equivalent to `log.isInfoEnabled()` or\n // `log.isEnabledFor(INFO)` in log4j.\n\n log.info('hi'); // Log a simple string message.\n log.info('hi %s', bob, anotherVar); // Uses `util.format` for msg formatting.\n\n log.info({foo: 'bar'}, 'hi');\n // Adds \"foo\" field to log record. You can add any number\n // of additional fields here.\n\n log.info(err); // Special case to log an `Error` instance to the record.\n // This adds an \"err\" field with exception details\n // (including the stack) and sets \"msg\" to the exception\n // message.\n log.info(err, 'more on this: %s', more);\n // ... or you can specify the \"msg\".\n\nNote that this implies **you cannot pass any object as the first argument\nto log it**. IOW, `log.info(mywidget)` may not be what you expect. Instead\nof a string representation of `mywidget` that other logging libraries may\ngive you, Bunyan will try to JSON-ify your object. It is a Bunyan best\npractice to always give a field name to included objects, e.g.:\n\n log.info({widget: mywidget}, ...)\n\nThis will dove-tail with [Bunyan serializer support](#serializers), discussed\nlater.\n\nThe same goes for all of Bunyan's log levels: `log.trace`, `log.debug`,\n`log.info`, `log.warn`, and `log.fatal`. See the [levels section](#levels)\nbelow for details and suggestions.\n\n\n## CLI Usage\n\nBunyan log output is a stream of JSON objects. This is great for processing,\nbut not for reading directly. A **`bunyan` tool** is provided **for\npretty-printing bunyan logs** and for **filtering** (e.g.\n`| bunyan -c 'this.foo == \"bar\"'`). Using our example above:\n\n $ node hi.js | ./bin/bunyan\n [2013-01-04T19:01:18.241Z] INFO: myapp/40208 on banana.local: hi\n [2013-01-04T19:01:18.242Z] WARN: myapp/40208 on banana.local: au revoir (lang=fr)\n\nSee the screenshot above for an example of the default coloring of rendered\nlog output. That example also shows the nice formatting automatically done for\nsome well-known log record fields (e.g. `req` is formatted like an HTTP request,\n`res` like an HTTP response, `err` like an error stack trace).\n\nOne interesting feature is **filtering** of log content, which can be useful\nfor digging through large log files or for analysis. We can filter only\nrecords above a certain level:\n\n $ node hi.js | bunyan -l warn\n [2013-01-04T19:08:37.182Z] WARN: myapp/40353 on banana.local: au revoir (lang=fr)\n\nOr filter on the JSON fields in the records (e.g. only showing the French\nrecords in our contrived example):\n\n $ node hi.js | bunyan -c 'this.lang == \"fr\"'\n [2013-01-04T19:08:26.411Z] WARN: myapp/40342 on banana.local: au revoir (lang=fr)\n\nSee `bunyan --help` for other facilities.\n\n\n## Streams Introduction\n\nBy default, log output is to stdout and at the \"info\" level. Explicitly that\nlooks like:\n\n var log = bunyan.createLogger({\n name: 'myapp',\n stream: process.stdout,\n level: 'info'\n });\n\nThat is an abbreviated form for a single stream. **You can define multiple\nstreams at different levels**.\n\n var log = bunyan.createLogger({\n name: 'myapp',\n streams: [\n {\n level: 'info',\n stream: process.stdout, // log INFO and above to stdout\n },\n {\n level: 'error',\n path: '/var/log/myapp-error.log' // log ERROR and above to a file\n }\n ]\n });\n\nMore on streams in the [Streams section](#streams) below.\n\n\n## log.child\n\nBunyan has a concept of a child logger to **specialize a logger for a\nsub-component of your application**, i.e. to create a new logger with\nadditional bound fields that will be included in its log records. A child\nlogger is created with `log.child(...)`.\n\nIn the following example, logging on a \"Wuzzle\" instance's `this.log` will\nbe exactly as on the parent logger with the addition of the `widget_type`\nfield:\n\n var bunyan = require('bunyan');\n var log = bunyan.createLogger({name: 'myapp'});\n\n function Wuzzle(options) {\n this.log = options.log.child({widget_type: 'wuzzle'});\n this.log.info('creating a wuzzle')\n }\n Wuzzle.prototype.woos = function () {\n this.log.warn('This wuzzle is woosey.')\n }\n\n log.info('start');\n var wuzzle = new Wuzzle({log: log});\n wuzzle.woos();\n log.info('done');\n\nRunning that looks like (raw):\n\n $ node myapp.js\n {\"name\":\"myapp\",\"hostname\":\"myhost\",\"pid\":34572,\"level\":30,\"msg\":\"start\",\"time\":\"2013-01-04T07:47:25.814Z\",\"v\":0}\n {\"name\":\"myapp\",\"hostname\":\"myhost\",\"pid\":34572,\"widget_type\":\"wuzzle\",\"level\":30,\"msg\":\"creating a wuzzle\",\"time\":\"2013-01-04T07:47:25.815Z\",\"v\":0}\n {\"name\":\"myapp\",\"hostname\":\"myhost\",\"pid\":34572,\"widget_type\":\"wuzzle\",\"level\":40,\"msg\":\"This wuzzle is woosey.\",\"time\":\"2013-01-04T07:47:25.815Z\",\"v\":0}\n {\"name\":\"myapp\",\"hostname\":\"myhost\",\"pid\":34572,\"level\":30,\"msg\":\"done\",\"time\":\"2013-01-04T07:47:25.816Z\",\"v\":0}\n\nAnd with the `bunyan` CLI (using the \"short\" output mode):\n\n $ node myapp.js | bunyan -o short\n 07:46:42.707Z INFO myapp: start\n 07:46:42.709Z INFO myapp: creating a wuzzle (widget_type=wuzzle)\n 07:46:42.709Z WARN myapp: This wuzzle is woosey. (widget_type=wuzzle)\n 07:46:42.709Z INFO myapp: done\n\n\nA more practical example is in the\n[node-restify](https://github.com/mcavage/node-restify) web framework.\nRestify uses Bunyan for its logging. One feature of its integration, is that\nif `server.use(restify.requestLogger())` is used, each restify request handler\nincludes a `req.log` logger that is:\n\n log.child({req_id: <unique request id>}, true)\n\nApps using restify can then use `req.log` and have all such log records\ninclude the unique request id (as \"req\\_id\"). Handy.\n\n\n## Serializers\n\nBunyan has a concept of **\"serializers\" to produce a JSON-able object from a\nJavaScript object**, so you can easily do the following:\n\n log.info({req: <request object>}, 'something about handling this request');\n\nSerializers is a mapping of log record field name, \"req\" in this example, to\na serializer function. That looks like this:\n\n function reqSerializer(req) {\n return {\n method: req.method,\n url: req.url,\n headers: req.headers\n }\n }\n var log = bunyan.createLogger({\n name: 'myapp',\n serializers: {\n req: reqSerializer\n }\n });\n\nOr this:\n\n var log = bunyan.createLogger({\n name: 'myapp',\n serializers: {req: bunyan.stdSerializers.req}\n });\n\nbecause Buyan includes a small set of standard serializers. To use all the\nstandard serializers you can use:\n\n var log = bunyan.createLogger({\n ...\n serializers: bunyan.stdSerializers\n });\n\n**Note**: Your own serializers should never throw, otherwise you'll get an\nugly message on stderr from Bunyan (along with the traceback) and the field\nin your log record will be replaced with a short error message.\n\n\n## src\n\nThe **source file, line and function of the log call site** can be added to\nlog records by using the `src: true` config option:\n\n var log = bunyan.createLogger({src: true, ...});\n\nThis adds the call source info with the 'src' field, like this:\n\n {\n \"name\": \"src-example\",\n \"hostname\": \"banana.local\",\n \"pid\": 123,\n \"component\": \"wuzzle\",\n \"level\": 4,\n \"msg\": \"This wuzzle is woosey.\",\n \"time\": \"2012-02-06T04:19:35.605Z\",\n \"src\": {\n \"file\": \"/Users/trentm/tm/node-bunyan/examples/src.js\",\n \"line\": 20,\n \"func\": \"Wuzzle.woos\"\n },\n \"v\": 0\n }\n\n**WARNING: Determining the call source info is slow. Never use this option\nin production.**\n\n\n# Levels\n\nThe log levels in bunyan are as follows. The level descriptions are best\npractice *opinions*.\n\n- \"fatal\" (60): The service/app is going to stop or become unusable now.\n An operator should definitely look into this soon.\n- \"error\" (50): Fatal for a particular request, but the service/app continues\n servicing other requests. An operator should look at this soon(ish).\n- \"warn\" (40): A note on something that should probably be looked at by an\n operator eventually.\n- \"info\" (30): Detail on regular operation.\n- \"debug\" (20): Anything else, i.e. too verbose to be included in \"info\" level.\n- \"trace\" (10): Logging from external libraries used by your app or *very*\n detailed application logging.\n\nSuggestions: Use \"debug\" sparingly. Information that will be useful to debug\nerrors *post mortem* should usually be included in \"info\" messages if it's\ngenerally relevant or else with the corresponding \"error\" event. Don't rely\non spewing mostly irrelevant debug messages all the time and sifting through\nthem when an error occurs.\n\nIntegers are used for the actual level values (10 for \"trace\", ..., 60 for\n\"fatal\") and constants are defined for the (bunyan.TRACE ... bunyan.DEBUG).\nThe lowercase level names are aliases supported in the API.\n\nHere is the API for changing levels in an existing logger:\n\n log.level() -> INFO // gets current level (lowest level of all streams)\n\n log.level(INFO) // set all streams to level INFO\n log.level(\"info\") // set all streams to level INFO\n\n log.levels() -> [DEBUG, INFO] // get array of levels of all streams\n log.levels(0) -> DEBUG // get level of stream at index 0\n log.levels(\"foo\") // get level of stream with name \"foo\"\n\n log.levels(0, INFO) // set level of stream 0 to INFO\n log.levels(0, \"info\") // can use \"info\" et al aliases\n log.levels(\"foo\", WARN) // set stream named \"foo\" to WARN\n\n\n\n# Log Record Fields\n\nThis section will describe *rules* for the Bunyan log format: field names,\nfield meanings, required fields, etc. However, a Bunyan library doesn't\nstrictly enforce all these rules while records are being emitted. For example,\nBunyan will add a `time` field with the correct format to your log records,\nbut you can specify your own. It is the caller's responsibility to specify\nthe appropriate format.\n\nThe reason for the above leniency is because IMO logging a message should\nnever break your app. This leads to this rule of logging: **a thrown\nexception from `log.info(...)` or equivalent (other than for calling with the\nincorrect signature) is always a bug in Bunyan.**\n\n\nA typical Bunyan log record looks like this:\n\n {\"name\":\"myserver\",\"hostname\":\"banana.local\",\"pid\":123,\"req\":{\"method\":\"GET\",\"url\":\"/path?q=1#anchor\",\"headers\":{\"x-hi\":\"Mom\",\"connection\":\"close\"}},\"level\":3,\"msg\":\"start request\",\"time\":\"2012-02-03T19:02:46.178Z\",\"v\":0}\n\nPretty-printed:\n\n {\n \"name\": \"myserver\",\n \"hostname\": \"banana.local\",\n \"pid\": 123,\n \"req\": {\n \"method\": \"GET\",\n \"url\": \"/path?q=1#anchor\",\n \"headers\": {\n \"x-hi\": \"Mom\",\n \"connection\": \"close\"\n },\n \"remoteAddress\": \"120.0.0.1\",\n \"remotePort\": 51244\n },\n \"level\": 3,\n \"msg\": \"start request\",\n \"time\": \"2012-02-03T19:02:57.534Z\",\n \"v\": 0\n }\n\n\nCore fields:\n\n- `v`: Required. Integer. Added by Bunyan. Cannot be overriden.\n This is the Bunyan log format version (`require('bunyan').LOG_VERSION`).\n The log version is a single integer. `0` is until I release a version\n \"1.0.0\" of node-bunyan. Thereafter, starting with `1`, this will be\n incremented if there is any backward incompatible change to the log record\n format. Details will be in \"CHANGES.md\" (the change log).\n- `level`: Required. Integer. Added by Bunyan. Cannot be overriden.\n See the \"Levels\" section.\n- `name`: Required. String. Provided at Logger creation.\n You must specify a name for your logger when creating it. Typically this\n is the name of the service/app using Bunyan for logging.\n- `hostname`: Required. String. Provided or determined at Logger creation.\n You can specify your hostname at Logger creation or it will be retrieved\n vi `os.hostname()`.\n- `pid`: Required. Integer. Filled in automatically at Logger creation.\n- `time`: Required. String. Added by Bunyan. Can be overriden.\n The date and time of the event in [ISO 8601\n Extended Format](http://en.wikipedia.org/wiki/ISO_8601) format and in UTC,\n as from\n [`Date.toISOString()`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/toISOString).\n- `msg`: Required. String.\n Every `log.debug(...)` et al call must provide a log message.\n- `src`: Optional. Object giving log call source info. This is added\n automatically by Bunyan if the \"src: true\" config option is given to the\n Logger. Never use in production as this is really slow.\n\n\nGo ahead and add more fields, and nested ones are fine (and recommended) as\nwell. This is why we're using JSON. Some suggestions and best practices\nfollow (feedback from actual users welcome).\n\n\nRecommended/Best Practice Fields:\n\n- `err`: Object. A caught JS exception. Log that thing with `log.info(err)`\n to get:\n\n ...\n \"err\": {\n \"message\": \"boom\",\n \"name\": \"TypeError\",\n \"stack\": \"TypeError: boom\\n at Object.<anonymous> ...\"\n },\n \"msg\": \"boom\",\n ...\n\n Or use the `bunyan.stdSerializers.err` serializer in your Logger and\n do this `log.error({err: err}, \"oops\")`. See \"examples/err.js\".\n\n- `req_id`: String. A request identifier. Including this field in all logging\n tied to handling a particular request to your server is strongly suggested.\n This allows post analysis of logs to easily collate all related logging\n for a request. This really shines when you have a SOA with multiple services\n and you carry a single request ID from the top API down through all APIs\n (as [node-restify](https://github.com/mcavage/node-restify) facilitates\n with its 'Request-Id' header).\n\n- `req`: An HTTP server request. Bunyan provides `bunyan.stdSerializers.req`\n to serialize a request with a suggested set of keys. Example:\n\n {\n \"method\": \"GET\",\n \"url\": \"/path?q=1#anchor\",\n \"headers\": {\n \"x-hi\": \"Mom\",\n \"connection\": \"close\"\n },\n \"remoteAddress\": \"120.0.0.1\",\n \"remotePort\": 51244\n }\n\n- `res`: An HTTP server response. Bunyan provides `bunyan.stdSerializers.res`\n to serialize a response with a suggested set of keys. Example:\n\n {\n \"statusCode\": 200,\n \"header\": \"HTTP/1.1 200 OK\\r\\nContent-Type: text/plain\\r\\nConnection: keep-alive\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n\"\n }\n\n\nOther fields to consider:\n\n- `req.username`: Authenticated user (or for a 401, the user attempting to\n auth).\n- Some mechanism to calculate response latency. \"restify\" users will have\n a \"X-Response-Time\" header. A `latency` custom field would be fine.\n- `req.body`: If you know that request bodies are small (common in APIs,\n for example), then logging the request body is good.\n\n\n# Streams\n\nA \"stream\" is Bunyan's name for an output for log messages (the equivalent\nto a log4j Appender). Ultimately Bunyan uses a\n[Writable Stream](http://nodejs.org/docs/latest/api/all.html#writable_Stream)\ninterface, but there are some additional attributes used to create and\nmanage the stream. A Bunyan Logger instance has one or more streams.\nIn general streams are specified with the \"streams\" option:\n\n var bunyan = require('bunyan');\n var log = bunyan.createLogger({\n name: \"foo\",\n streams: [\n {\n stream: process.stderr,\n level: \"debug\"\n },\n ...\n ]\n });\n\nFor convenience, if there is only one stream, it can specified with the\n\"stream\" and \"level\" options (internally converted to a `Logger.streams`).\n\n var log = bunyan.createLogger({\n name: \"foo\",\n stream: process.stderr,\n level: \"debug\"\n });\n\nNote that \"file\" streams do not support this shortcut (partly for historical\nreasons and partly to not make it difficult to add a literal \"path\" field\non log records).\n\nIf neither \"streams\" nor \"stream\" are specified, the default is a stream of\ntype \"stream\" emitting to `process.stdout` at the \"info\" level.\n\n\n## stream errors\n\nBunyan re-emits error events from the created `WriteStream`. So you can\ndo this:\n\n var log = bunyan.createLogger({name: 'mylog', streams: [{path: LOG_PATH}]});\n log.on('error', function (err, stream) {\n // Handle stream write or create error here.\n });\n\n\n## stream type: `stream`\n\nA `type === 'stream'` is a plain ol' node.js [Writable\nStream](http://nodejs.org/docs/latest/api/all.html#writable_Stream). A\n\"stream\" (the writeable stream) field is required. E.g.: `process.stdout`,\n`process.stderr`.\n\n var log = bunyan.createLogger({\n name: 'foo',\n streams: [{\n stream: process.stderr\n // `type: 'stream'` is implied\n }]\n });\n\n<table>\n<tr>\n<th>Field</th>\n<th>Required?</th>\n<th>Default</th>\n<th>Description</th>\n</tr>\n<tr>\n<td>stream</td>\n<td>Yes</td>\n<td>-</td>\n<td>A \"Writable Stream\", e.g. a std handle or an open file write stream.</td>\n</tr>\n<tr>\n<td>type</td>\n<td>No</td>\n<td>n/a</td>\n<td>`type == 'stream'` is implied if the `stream` field is given.</td>\n</tr>\n<tr>\n<td>level</td>\n<td>No</td>\n<td>info</td>\n<td>The level at which logging to this stream is enabled. If not\nspecified it defaults to \"info\". If specified this can be one of the\nlevel strings (\"trace\", \"debug\", ...) or constants (`bunyan.TRACE`,\n`bunyan.DEBUG`, ...).</td>\n</tr>\n<tr>\n<td>name</td>\n<td>No</td>\n<td>-</td>\n<td>A name for this stream. This may be useful for usage of `log.level(NAME,\nLEVEL)`. See the [Levels section](#levels) for details. A stream \"name\" isn't\nused for anything else.</td>\n</tr>\n</table>\n\n\n## stream type: `file`\n\nA `type === 'file'` stream requires a \"path\" field. Bunyan will open this\nfile for appending. E.g.:\n\n var log = bunyan.createLogger({\n name: 'foo',\n streams: [{\n path: '/var/log/foo.log',\n // `type: 'file'` is implied\n }]\n });\n\n<table>\n<tr>\n<th>Field</th>\n<th>Required?</th>\n<th>Default</th>\n<th>Description</th>\n</tr>\n<tr>\n<td>path</td>\n<td>Yes</td>\n<td>-</td>\n<td>A file path to which to log.</td>\n</tr>\n<tr>\n<td>type</td>\n<td>No</td>\n<td>n/a</td>\n<td>`type == 'file'` is implied if the `path` field is given.</td>\n</tr>\n<tr>\n<td>level</td>\n<td>No</td>\n<td>info</td>\n<td>The level at which logging to this stream is enabled. If not\nspecified it defaults to \"info\". If specified this can be one of the\nlevel strings (\"trace\", \"debug\", ...) or constants (`bunyan.TRACE`,\n`bunyan.DEBUG`, ...).</td>\n</tr>\n<tr>\n<td>name</td>\n<td>No</td>\n<td>-</td>\n<td>A name for this stream. This may be useful for usage of `log.level(NAME,\nLEVEL)`. See the [Levels section](#levels) for details. A stream \"name\" isn't\nused for anything else.</td>\n</tr>\n</table>\n\n\n## stream type: `rotating-file`\n\nA `type === 'rotating-file'` is a file stream that handles file automatic\nrotation.\n\n var log = bunyan.createLogger({\n name: 'foo',\n streams: [{\n type: 'rotating-file',\n path: '/var/log/foo.log',\n period: '1d', // daily rotation\n count: 3 // keep 3 back copies\n }]\n });\n\nThis will rotate '/var/log/foo.log' every day (at midnight) to:\n\n /var/log/foo.log.0 # yesterday\n /var/log/foo.log.1 # 1 day ago\n /var/log/foo.log.2 # 2 days ago\n\n*Currently*, there is no support for providing a template for the rotated\nfiles, or for rotating when the log reaches a threshold size.\n\n<table>\n<tr>\n<th>Field</th>\n<th>Required?</th>\n<th>Default</th>\n<th>Description</th>\n</tr>\n<tr>\n<td>type</td>\n<td>Yes</td>\n<td>-</td>\n<td>\"rotating-file\"</td>\n</tr>\n<tr>\n<td>path</td>\n<td>Yes</td>\n<td>-</td>\n<td>A file path to which to log. Rotated files will be \"$path.0\",\n\"$path.1\", ...</td>\n</tr>\n<tr>\n<td>period</td>\n<td>No</td>\n<td>1d</td>\n<td>The period at which to rotate. This is a string of the format\n\"$number$scope\" where \"$scope\" is one of \"h\" (hours), \"d\" (days), \"w\" (weeks),\n\"m\" (months), \"y\" (years). Or one of the following names can be used\n\"hourly\" (means 1h), \"daily\" (1d), \"weekly\" (1w), \"monthly\" (1m),\n\"yearly\" (1y). Rotation is done at the start of the scope: top of the hour (h),\nmidnight (d), start of Sunday (w), start of the 1st of the month (m),\nstart of Jan 1st (y).</td>\n</tr>\n<tr>\n<td>count</td>\n<td>No</td>\n<td>10</td>\n<td>The number of rotated files to keep.</td>\n</tr>\n<tr>\n<td>level</td>\n<td>No</td>\n<td>info</td>\n<td>The level at which logging to this stream is enabled. If not\nspecified it defaults to \"info\". If specified this can be one of the\nlevel strings (\"trace\", \"debug\", ...) or constants (`bunyan.TRACE`,\n`bunyan.DEBUG`, ...).</td>\n</tr>\n<tr>\n<td>name</td>\n<td>No</td>\n<td>-</td>\n<td>A name for this stream. This may be useful for usage of `log.level(NAME,\nLEVEL)`. See the [Levels section](#levels) for details. A stream \"name\" isn't\nused for anything else.</td>\n</tr>\n</table>\n\n\n**Note on log rotation**: Often you may be using external log rotation utilities\nlike `logrotate` on Linux or `logadm` on SmartOS/Illumos. In those cases, unless\nyour are ensuring \"copy and truncate\" sematics (via `copytruncate` with\nlogrotate or `-c` with logadm) then the fd for your 'file' stream will change.\nYou can tell bunyan to reopen the file stream with code like this in your\napp:\n\n var log = bunyan.createLogger(...);\n ...\n process.on('SIGUSR2', function () {\n log.reopenFileStreams();\n });\n\nwhere you'd configure your log rotation to send SIGUSR2 (or some other signal)\nto your process. Any other mechanism to signal your app to run\n`log.reopenFileStreams()` would work as well.\n\n\n## stream type: `raw`\n\n- `raw`: Similar to a \"stream\" writeable stream, except that the write method\n is given raw log record *Object*s instead of a JSON-stringified string.\n This can be useful for hooking on further processing to all Bunyan logging:\n pushing to an external service, a RingBuffer (see below), etc.\n\n\n\n## `raw` + RingBuffer Stream\n\nBunyan comes with a special stream called a RingBuffer which keeps the last N\nrecords in memory and does *not* write the data anywhere else. One common\nstrategy is to log 'info' and higher to a normal log file but log all records\n(including 'trace') to a ringbuffer that you can access via a debugger, or your\nown HTTP interface, or a post-mortem facility like MDB or node-panic.\n\nTo use a RingBuffer:\n\n /* Create a ring buffer that stores the last 100 records. */\n var bunyan = require('bunyan');\n var ringbuffer = new bunyan.RingBuffer({ limit: 100 });\n var log = bunyan.createLogger({\n name: 'foo',\n streams: [\n {\n level: 'info',\n stream: process.stdout\n },\n {\n level: 'trace',\n type: 'raw', // use 'raw' to get raw log record objects\n stream: ringbuffer\n }\n ]\n });\n\n log.info('hello world');\n console.log(ringbuffer.records);\n\nThis example emits:\n\n [ { name: 'foo',\n hostname: '912d2b29',\n pid: 50346,\n level: 30,\n msg: 'hello world',\n time: '2012-06-19T21:34:19.906Z',\n v: 0 } ]\n\n\n## third-party streams\n\n- syslog:\n [mcavage/node-bunyan-syslog](https://github.com/mcavage/node-bunyan-syslog)\n provides support for directing bunyan logging to a syslog server.\n\n- TODO: eventually https://github.com/trentm/node-bunyan-winston\n\n\n\n# Runtime log snooping via DTrace\n\nOn systems that support DTrace (e.g., MacOS, FreeBSD, illumos derivatives\nlike SmartOS and OmniOS), Bunyan will create a DTrace provider (`bunyan`)\nthat makes available the following probes:\n\n log-trace\n log-debug\n log-info\n log-warn\n log-error\n log-fatal\n\nEach of these probes has a single argument: the string that would be\nwritten to the log. Note that when a probe is enabled, it will\nfire whenever the corresponding function is called, even if the level of\nthe log message is less than that of any stream.\n\n\n## DTrace examples\n\nTrace all log messages coming from any Bunyan module on the system.\n(The `-x strsize=4k` is to raise dtrace's default 256 byte buffer size\nbecause log messages are longer than typical dtrace probes.)\n\n dtrace -x strsize=4k -qn 'bunyan*:::log-*{printf(\"%d: %s: %s\", pid, probefunc, copyinstr(arg0))}'\n\nTrace all log messages coming from the \"wuzzle\" component:\n\n dtrace -x strsize=4k -qn 'bunyan*:::log-*/strstr(this->str = copyinstr(arg0), \"\\\"component\\\":\\\"wuzzle\\\"\") != NULL/{printf(\"%s\", this->str)}'\n\nAggregate debug messages from process 1234, by message:\n\n dtrace -x strsize=4k -n 'bunyan1234:::log-debug{@[copyinstr(arg0)] = count()}'\n\nHave the bunyan CLI pretty-print the traced logs:\n\n dtrace -x strsize=4k -qn 'bunyan1234:::log-*{printf(\"%s\", copyinstr(arg0))}' | bunyan\n\nA convenience handle has been made for this:\n\n bunyan -p 1234\n\n\nOn systems that support the\n[`jstack`](http://dtrace.org/blogs/dap/2012/04/25/profiling-node-js/) action\nvia a node.js helper, get a stack backtrace for any debug message that\nincludes the string \"danger!\":\n\n dtrace -x strsize=4k -qn 'log-debug/strstr(copyinstr(arg0), \"danger!\") != NULL/{printf(\"\\n%s\", copyinstr(arg0)); jstack()}'\n\nOutput of the above might be:\n\n {\"name\":\"foo\",\"hostname\":\"763bf293-d65c-42d5-872b-4abe25d5c4c7.local\",\"pid\":12747,\"level\":20,\"msg\":\"danger!\",\"time\":\"2012-10-30T18:28:57.115Z\",\"v\":0}\n\n node`0x87e2010\n DTraceProviderBindings.node`usdt_fire_probe+0x32\n DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d\n DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77\n << internal code >>\n (anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484\n << adaptor >>\n (anon) as doit at /root/my-prog.js position 360\n (anon) as list.ontimeout at timers.js position 4960\n << adaptor >>\n << internal >>\n << entry >>\n node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101\n node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb\n node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0\n node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f\n node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66\n node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63\n node`uv__run_timers+0x66\n node`uv__run+0x1b\n node`uv_run+0x17\n node`_ZN4node5StartEiPPc+0x1d0\n node`main+0x1b\n node`_start+0x83\n\n node`0x87e2010\n DTraceProviderBindings.node`usdt_fire_probe+0x32\n DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d\n DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77\n << internal code >>\n (anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484\n << adaptor >>\n (anon) as doit at /root/my-prog.js position 360\n (anon) as list.ontimeout at timers.js position 4960\n << adaptor >>\n << internal >>\n << entry >>\n node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101\n node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb\n node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0\n node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f\n node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66\n node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63\n node`uv__run_timers+0x66\n node`uv__run+0x1b\n node`uv_run+0x17\n node`_ZN4node5StartEiPPc+0x1d0\n node`main+0x1b\n node`_start+0x83\n\n\n# Versioning\n\nThe scheme I follow is most succintly described by the bootstrap guys\n[here](https://github.com/twitter/bootstrap#versioning).\n\ntl;dr: All versions are `<major>.<minor>.<patch>` which will be incremented for\nbreaking backward compat and major reworks, new features without breaking\nchange, and bug fixes, respectively.\n\n\n# License\n\nMIT. See \"LICENSE.txt\".\n\n\n# See Also\n\n- Bunyan syslog support: <https://github.com/mcavage/node-bunyan-syslog>.\n- Bunyan + Graylog2: <https://github.com/mhart/gelf-stream>.\n- An example of a Bunyan shim to the Winston logging system:\n <https://github.com/trentm/node-bunyan-winston>.\n- [Bunyan for Bash](https://github.com/trevoro/bash-bunyan).\n- TODO: `RequestCaptureStream` example from restify.\n",
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/trentm/node-bunyan/issues"
},
"homepage": "https://github.com/trentm/node-bunyan",
"_id": "bunyan@0.22.1",
"dist": {
"shasum": "ccabcc60e5c7876af61d1e7b57b26647212cf301"
},
"_from": "bunyan@0.22.1",
"_resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
}

Bunyan is a simple and fast JSON logging library for node.js services:

var bunyan = require('bunyan');
var log = bunyan.createLogger({name: "myapp"});
log.info("hi");

and a bunyan CLI tool for nicely viewing those logs:

bunyan CLI screenshot

Manifesto: Server logs should be structured. JSON's a good format. Let's do that. A log record is one line of JSON.stringify'd output. Let's also specify some common names for the requisite and common fields for a log record (see below).

Also: log4j is way more than you need.

Current Status

Solid core functionality is there. Joyent is using this for a number of production services. Bunyan supports node 0.6 and greater. Follow @trentmick for updates to Bunyan.

There is an email discussion list bunyan-logging@googlegroups.com, also as a forum in the browser.

Installation

npm install bunyan

Tip: The bunyan CLI tool is written to be compatible (within reason) with all versions of Bunyan logs. Therefore you might want to npm install -g bunyan to get the bunyan CLI on your PATH, then use local bunyan installs for node.js library usage of bunyan in your apps.

Features

Introduction

Like most logging libraries you create a Logger instance and call methods named after the logging levels:

$ cat hi.js
var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'myapp'});
log.info('hi');
log.warn({lang: 'fr'}, 'au revoir');

All loggers must provide a "name". This is somewhat akin to the log4j logger "name", but Bunyan doesn't do hierarchical logger names.

Bunyan log records are JSON. A few fields are added automatically: "pid", "hostname", "time" and "v".

$ node hi.js
{"name":"myapp","hostname":"banana.local","pid":40161,"level":30,"msg":"hi","time":"2013-01-04T18:46:23.851Z","v":0}
{"name":"myapp","hostname":"banana.local","pid":40161,"level":40,"lang":"fr","msg":"au revoir","time":"2013-01-04T18:46:23.853Z","v":0}

Log Method API

The example above shows two different ways to call log.info(...). The full API is:

log.info();     // Returns a boolean: is the "info" level enabled?
                // This is equivalent to `log.isInfoEnabled()` or
                // `log.isEnabledFor(INFO)` in log4j.

log.info('hi');                     // Log a simple string message.
log.info('hi %s', bob, anotherVar); // Uses `util.format` for msg formatting.

log.info({foo: 'bar'}, 'hi');
                // Adds "foo" field to log record. You can add any number
                // of additional fields here.

log.info(err);  // Special case to log an `Error` instance to the record.
                // This adds an "err" field with exception details
                // (including the stack) and sets "msg" to the exception
                // message.
log.info(err, 'more on this: %s', more);
                // ... or you can specify the "msg".

Note that this implies you cannot pass any object as the first argument to log it. IOW, log.info(mywidget) may not be what you expect. Instead of a string representation of mywidget that other logging libraries may give you, Bunyan will try to JSON-ify your object. It is a Bunyan best practice to always give a field name to included objects, e.g.:

log.info({widget: mywidget}, ...)

This will dove-tail with Bunyan serializer support, discussed later.

The same goes for all of Bunyan's log levels: log.trace, log.debug, log.info, log.warn, and log.fatal. See the levels section below for details and suggestions.

CLI Usage

Bunyan log output is a stream of JSON objects. This is great for processing, but not for reading directly. A bunyan tool is provided for pretty-printing bunyan logs and for filtering (e.g. | bunyan -c 'this.foo == "bar"'). Using our example above:

$ node hi.js | ./bin/bunyan
[2013-01-04T19:01:18.241Z]  INFO: myapp/40208 on banana.local: hi
[2013-01-04T19:01:18.242Z]  WARN: myapp/40208 on banana.local: au revoir (lang=fr)

See the screenshot above for an example of the default coloring of rendered log output. That example also shows the nice formatting automatically done for some well-known log record fields (e.g. req is formatted like an HTTP request, res like an HTTP response, err like an error stack trace).

One interesting feature is filtering of log content, which can be useful for digging through large log files or for analysis. We can filter only records above a certain level:

$ node hi.js | bunyan -l warn
[2013-01-04T19:08:37.182Z]  WARN: myapp/40353 on banana.local: au revoir (lang=fr)

Or filter on the JSON fields in the records (e.g. only showing the French records in our contrived example):

$ node hi.js | bunyan -c 'this.lang == "fr"'
[2013-01-04T19:08:26.411Z]  WARN: myapp/40342 on banana.local: au revoir (lang=fr)

See bunyan --help for other facilities.

Streams Introduction

By default, log output is to stdout and at the "info" level. Explicitly that looks like:

var log = bunyan.createLogger({
    name: 'myapp',
    stream: process.stdout,
    level: 'info'
});

That is an abbreviated form for a single stream. You can define multiple streams at different levels.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'info',
      stream: process.stdout,           // log INFO and above to stdout
    },
    {
      level: 'error',
      path: '/var/log/myapp-error.log'  // log ERROR and above to a file
    }
  ]
});

More on streams in the Streams section below.

log.child

Bunyan has a concept of a child logger to specialize a logger for a sub-component of your application, i.e. to create a new logger with additional bound fields that will be included in its log records. A child logger is created with log.child(...).

In the following example, logging on a "Wuzzle" instance's this.log will be exactly as on the parent logger with the addition of the widget_type field:

var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'myapp'});

function Wuzzle(options) {
    this.log = options.log.child({widget_type: 'wuzzle'});
    this.log.info('creating a wuzzle')
}
Wuzzle.prototype.woos = function () {
    this.log.warn('This wuzzle is woosey.')
}

log.info('start');
var wuzzle = new Wuzzle({log: log});
wuzzle.woos();
log.info('done');

Running that looks like (raw):

$ node myapp.js
{"name":"myapp","hostname":"myhost","pid":34572,"level":30,"msg":"start","time":"2013-01-04T07:47:25.814Z","v":0}
{"name":"myapp","hostname":"myhost","pid":34572,"widget_type":"wuzzle","level":30,"msg":"creating a wuzzle","time":"2013-01-04T07:47:25.815Z","v":0}
{"name":"myapp","hostname":"myhost","pid":34572,"widget_type":"wuzzle","level":40,"msg":"This wuzzle is woosey.","time":"2013-01-04T07:47:25.815Z","v":0}
{"name":"myapp","hostname":"myhost","pid":34572,"level":30,"msg":"done","time":"2013-01-04T07:47:25.816Z","v":0}

And with the bunyan CLI (using the "short" output mode):

$ node myapp.js  | bunyan -o short
07:46:42.707Z  INFO myapp: start
07:46:42.709Z  INFO myapp: creating a wuzzle (widget_type=wuzzle)
07:46:42.709Z  WARN myapp: This wuzzle is woosey. (widget_type=wuzzle)
07:46:42.709Z  INFO myapp: done

A more practical example is in the node-restify web framework. Restify uses Bunyan for its logging. One feature of its integration, is that if server.use(restify.requestLogger()) is used, each restify request handler includes a req.log logger that is:

log.child({req_id: <unique request id>}, true)

Apps using restify can then use req.log and have all such log records include the unique request id (as "req_id"). Handy.

Serializers

Bunyan has a concept of "serializers" to produce a JSON-able object from a JavaScript object, so you can easily do the following:

log.info({req: <request object>}, 'something about handling this request');

Serializers is a mapping of log record field name, "req" in this example, to a serializer function. That looks like this:

function reqSerializer(req) {
    return {
        method: req.method,
        url: req.url,
        headers: req.headers
    }
}
var log = bunyan.createLogger({
    name: 'myapp',
    serializers: {
        req: reqSerializer
    }
});

Or this:

var log = bunyan.createLogger({
    name: 'myapp',
    serializers: {req: bunyan.stdSerializers.req}
});

because Buyan includes a small set of standard serializers. To use all the standard serializers you can use:

var log = bunyan.createLogger({
  ...
  serializers: bunyan.stdSerializers
});

Note: Your own serializers should never throw, otherwise you'll get an ugly message on stderr from Bunyan (along with the traceback) and the field in your log record will be replaced with a short error message.

src

The source file, line and function of the log call site can be added to log records by using the src: true config option:

var log = bunyan.createLogger({src: true, ...});

This adds the call source info with the 'src' field, like this:

{
  "name": "src-example",
  "hostname": "banana.local",
  "pid": 123,
  "component": "wuzzle",
  "level": 4,
  "msg": "This wuzzle is woosey.",
  "time": "2012-02-06T04:19:35.605Z",
  "src": {
    "file": "/Users/trentm/tm/node-bunyan/examples/src.js",
    "line": 20,
    "func": "Wuzzle.woos"
  },
  "v": 0
}

WARNING: Determining the call source info is slow. Never use this option in production.

Levels

The log levels in bunyan are as follows. The level descriptions are best practice opinions.

  • "fatal" (60): The service/app is going to stop or become unusable now. An operator should definitely look into this soon.
  • "error" (50): Fatal for a particular request, but the service/app continues servicing other requests. An operator should look at this soon(ish).
  • "warn" (40): A note on something that should probably be looked at by an operator eventually.
  • "info" (30): Detail on regular operation.
  • "debug" (20): Anything else, i.e. too verbose to be included in "info" level.
  • "trace" (10): Logging from external libraries used by your app or very detailed application logging.

Suggestions: Use "debug" sparingly. Information that will be useful to debug errors post mortem should usually be included in "info" messages if it's generally relevant or else with the corresponding "error" event. Don't rely on spewing mostly irrelevant debug messages all the time and sifting through them when an error occurs.

Integers are used for the actual level values (10 for "trace", ..., 60 for "fatal") and constants are defined for the (bunyan.TRACE ... bunyan.DEBUG). The lowercase level names are aliases supported in the API.

Here is the API for changing levels in an existing logger:

log.level() -> INFO   // gets current level (lowest level of all streams)

log.level(INFO)       // set all streams to level INFO
log.level("info")     // set all streams to level INFO

log.levels() -> [DEBUG, INFO]   // get array of levels of all streams
log.levels(0) -> DEBUG          // get level of stream at index 0
log.levels("foo")               // get level of stream with name "foo"

log.levels(0, INFO)             // set level of stream 0 to INFO
log.levels(0, "info")           // can use "info" et al aliases
log.levels("foo", WARN)         // set stream named "foo" to WARN

Log Record Fields

This section will describe rules for the Bunyan log format: field names, field meanings, required fields, etc. However, a Bunyan library doesn't strictly enforce all these rules while records are being emitted. For example, Bunyan will add a time field with the correct format to your log records, but you can specify your own. It is the caller's responsibility to specify the appropriate format.

The reason for the above leniency is because IMO logging a message should never break your app. This leads to this rule of logging: a thrown exception from log.info(...) or equivalent (other than for calling with the incorrect signature) is always a bug in Bunyan.

A typical Bunyan log record looks like this:

{"name":"myserver","hostname":"banana.local","pid":123,"req":{"method":"GET","url":"/path?q=1#anchor","headers":{"x-hi":"Mom","connection":"close"}},"level":3,"msg":"start request","time":"2012-02-03T19:02:46.178Z","v":0}

Pretty-printed:

{
  "name": "myserver",
  "hostname": "banana.local",
  "pid": 123,
  "req": {
    "method": "GET",
    "url": "/path?q=1#anchor",
    "headers": {
      "x-hi": "Mom",
      "connection": "close"
    },
    "remoteAddress": "120.0.0.1",
    "remotePort": 51244
  },
  "level": 3,
  "msg": "start request",
  "time": "2012-02-03T19:02:57.534Z",
  "v": 0
}

Core fields:

  • v: Required. Integer. Added by Bunyan. Cannot be overriden. This is the Bunyan log format version (require('bunyan').LOG_VERSION). The log version is a single integer. 0 is until I release a version "1.0.0" of node-bunyan. Thereafter, starting with 1, this will be incremented if there is any backward incompatible change to the log record format. Details will be in "CHANGES.md" (the change log).
  • level: Required. Integer. Added by Bunyan. Cannot be overriden. See the "Levels" section.
  • name: Required. String. Provided at Logger creation. You must specify a name for your logger when creating it. Typically this is the name of the service/app using Bunyan for logging.
  • hostname: Required. String. Provided or determined at Logger creation. You can specify your hostname at Logger creation or it will be retrieved vi os.hostname().
  • pid: Required. Integer. Filled in automatically at Logger creation.
  • time: Required. String. Added by Bunyan. Can be overriden. The date and time of the event in ISO 8601 Extended Format format and in UTC, as from Date.toISOString().
  • msg: Required. String. Every log.debug(...) et al call must provide a log message.
  • src: Optional. Object giving log call source info. This is added automatically by Bunyan if the "src: true" config option is given to the Logger. Never use in production as this is really slow.

Go ahead and add more fields, and nested ones are fine (and recommended) as well. This is why we're using JSON. Some suggestions and best practices follow (feedback from actual users welcome).

Recommended/Best Practice Fields:

  • err: Object. A caught JS exception. Log that thing with log.info(err) to get:

      ...
      "err": {
        "message": "boom",
        "name": "TypeError",
        "stack": "TypeError: boom\n    at Object.<anonymous> ..."
      },
      "msg": "boom",
      ...
    

    Or use the bunyan.stdSerializers.err serializer in your Logger and do this log.error({err: err}, "oops"). See "examples/err.js".

  • req_id: String. A request identifier. Including this field in all logging tied to handling a particular request to your server is strongly suggested. This allows post analysis of logs to easily collate all related logging for a request. This really shines when you have a SOA with multiple services and you carry a single request ID from the top API down through all APIs (as node-restify facilitates with its 'Request-Id' header).

  • req: An HTTP server request. Bunyan provides bunyan.stdSerializers.req to serialize a request with a suggested set of keys. Example:

      {
        "method": "GET",
        "url": "/path?q=1#anchor",
        "headers": {
          "x-hi": "Mom",
          "connection": "close"
        },
        "remoteAddress": "120.0.0.1",
        "remotePort": 51244
      }
    
  • res: An HTTP server response. Bunyan provides bunyan.stdSerializers.res to serialize a response with a suggested set of keys. Example:

      {
        "statusCode": 200,
        "header": "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: keep-alive\r\nTransfer-Encoding: chunked\r\n\r\n"
      }
    

Other fields to consider:

  • req.username: Authenticated user (or for a 401, the user attempting to auth).
  • Some mechanism to calculate response latency. "restify" users will have a "X-Response-Time" header. A latency custom field would be fine.
  • req.body: If you know that request bodies are small (common in APIs, for example), then logging the request body is good.

Streams

A "stream" is Bunyan's name for an output for log messages (the equivalent to a log4j Appender). Ultimately Bunyan uses a Writable Stream interface, but there are some additional attributes used to create and manage the stream. A Bunyan Logger instance has one or more streams. In general streams are specified with the "streams" option:

var bunyan = require('bunyan');
var log = bunyan.createLogger({
    name: "foo",
    streams: [
        {
            stream: process.stderr,
            level: "debug"
        },
        ...
    ]
});

For convenience, if there is only one stream, it can specified with the "stream" and "level" options (internally converted to a Logger.streams).

var log = bunyan.createLogger({
    name: "foo",
    stream: process.stderr,
    level: "debug"
});

Note that "file" streams do not support this shortcut (partly for historical reasons and partly to not make it difficult to add a literal "path" field on log records).

If neither "streams" nor "stream" are specified, the default is a stream of type "stream" emitting to process.stdout at the "info" level.

stream errors

Bunyan re-emits error events from the created WriteStream. So you can do this:

var log = bunyan.createLogger({name: 'mylog', streams: [{path: LOG_PATH}]});
log.on('error', function (err, stream) {
    // Handle stream write or create error here.
});

stream type: stream

A type === 'stream' is a plain ol' node.js Writable Stream. A "stream" (the writeable stream) field is required. E.g.: process.stdout, process.stderr.

var log = bunyan.createLogger({
    name: 'foo',
    streams: [{
        stream: process.stderr
        // `type: 'stream'` is implied
    }]
});
Field Required? Default Description
stream Yes - A "Writable Stream", e.g. a std handle or an open file write stream.
type No n/a `type == 'stream'` is implied if the `stream` field is given.
level No info The level at which logging to this stream is enabled. If not specified it defaults to "info". If specified this can be one of the level strings ("trace", "debug", ...) or constants (`bunyan.TRACE`, `bunyan.DEBUG`, ...).
name No - A name for this stream. This may be useful for usage of `log.level(NAME, LEVEL)`. See the [Levels section](#levels) for details. A stream "name" isn't used for anything else.

stream type: file

A type === 'file' stream requires a "path" field. Bunyan will open this file for appending. E.g.:

var log = bunyan.createLogger({
    name: 'foo',
    streams: [{
        path: '/var/log/foo.log',
        // `type: 'file'` is implied
    }]
});
Field Required? Default Description
path Yes - A file path to which to log.
type No n/a `type == 'file'` is implied if the `path` field is given.
level No info The level at which logging to this stream is enabled. If not specified it defaults to "info". If specified this can be one of the level strings ("trace", "debug", ...) or constants (`bunyan.TRACE`, `bunyan.DEBUG`, ...).
name No - A name for this stream. This may be useful for usage of `log.level(NAME, LEVEL)`. See the [Levels section](#levels) for details. A stream "name" isn't used for anything else.

stream type: rotating-file

A type === 'rotating-file' is a file stream that handles file automatic rotation.

var log = bunyan.createLogger({
    name: 'foo',
    streams: [{
        type: 'rotating-file',
        path: '/var/log/foo.log',
        period: '1d',   // daily rotation
        count: 3        // keep 3 back copies
    }]
});

This will rotate '/var/log/foo.log' every day (at midnight) to:

/var/log/foo.log.0     # yesterday
/var/log/foo.log.1     # 1 day ago
/var/log/foo.log.2     # 2 days ago

Currently, there is no support for providing a template for the rotated files, or for rotating when the log reaches a threshold size.

Field Required? Default Description
type Yes - "rotating-file"
path Yes - A file path to which to log. Rotated files will be "$path.0", "$path.1", ...
period No 1d The period at which to rotate. This is a string of the format "$number$scope" where "$scope" is one of "h" (hours), "d" (days), "w" (weeks), "m" (months), "y" (years). Or one of the following names can be used "hourly" (means 1h), "daily" (1d), "weekly" (1w), "monthly" (1m), "yearly" (1y). Rotation is done at the start of the scope: top of the hour (h), midnight (d), start of Sunday (w), start of the 1st of the month (m), start of Jan 1st (y).
count No 10 The number of rotated files to keep.
level No info The level at which logging to this stream is enabled. If not specified it defaults to "info". If specified this can be one of the level strings ("trace", "debug", ...) or constants (`bunyan.TRACE`, `bunyan.DEBUG`, ...).
name No - A name for this stream. This may be useful for usage of `log.level(NAME, LEVEL)`. See the [Levels section](#levels) for details. A stream "name" isn't used for anything else.

Note on log rotation: Often you may be using external log rotation utilities like logrotate on Linux or logadm on SmartOS/Illumos. In those cases, unless your are ensuring "copy and truncate" sematics (via copytruncate with logrotate or -c with logadm) then the fd for your 'file' stream will change. You can tell bunyan to reopen the file stream with code like this in your app:

var log = bunyan.createLogger(...);
...
process.on('SIGUSR2', function () {
    log.reopenFileStreams();
});

where you'd configure your log rotation to send SIGUSR2 (or some other signal) to your process. Any other mechanism to signal your app to run log.reopenFileStreams() would work as well.

stream type: raw

  • raw: Similar to a "stream" writeable stream, except that the write method is given raw log record Objects instead of a JSON-stringified string. This can be useful for hooking on further processing to all Bunyan logging: pushing to an external service, a RingBuffer (see below), etc.

raw + RingBuffer Stream

Bunyan comes with a special stream called a RingBuffer which keeps the last N records in memory and does not write the data anywhere else. One common strategy is to log 'info' and higher to a normal log file but log all records (including 'trace') to a ringbuffer that you can access via a debugger, or your own HTTP interface, or a post-mortem facility like MDB or node-panic.

To use a RingBuffer:

/* Create a ring buffer that stores the last 100 records. */
var bunyan = require('bunyan');
var ringbuffer = new bunyan.RingBuffer({ limit: 100 });
var log = bunyan.createLogger({
    name: 'foo',
    streams: [
        {
            level: 'info',
            stream: process.stdout
        },
        {
            level: 'trace',
            type: 'raw',    // use 'raw' to get raw log record objects
            stream: ringbuffer
        }
    ]
});

log.info('hello world');
console.log(ringbuffer.records);

This example emits:

[ { name: 'foo',
    hostname: '912d2b29',
    pid: 50346,
    level: 30,
    msg: 'hello world',
    time: '2012-06-19T21:34:19.906Z',
    v: 0 } ]

third-party streams

Runtime log snooping via DTrace

On systems that support DTrace (e.g., MacOS, FreeBSD, illumos derivatives like SmartOS and OmniOS), Bunyan will create a DTrace provider (bunyan) that makes available the following probes:

log-trace
log-debug
log-info
log-warn
log-error
log-fatal

Each of these probes has a single argument: the string that would be written to the log. Note that when a probe is enabled, it will fire whenever the corresponding function is called, even if the level of the log message is less than that of any stream.

DTrace examples

Trace all log messages coming from any Bunyan module on the system. (The -x strsize=4k is to raise dtrace's default 256 byte buffer size because log messages are longer than typical dtrace probes.)

dtrace -x strsize=4k -qn 'bunyan*:::log-*{printf("%d: %s: %s", pid, probefunc, copyinstr(arg0))}'

Trace all log messages coming from the "wuzzle" component:

dtrace -x strsize=4k -qn 'bunyan*:::log-*/strstr(this->str = copyinstr(arg0), "\"component\":\"wuzzle\"") != NULL/{printf("%s", this->str)}'

Aggregate debug messages from process 1234, by message:

dtrace -x strsize=4k -n 'bunyan1234:::log-debug{@[copyinstr(arg0)] = count()}'

Have the bunyan CLI pretty-print the traced logs:

dtrace -x strsize=4k -qn 'bunyan1234:::log-*{printf("%s", copyinstr(arg0))}' | bunyan

A convenience handle has been made for this:

bunyan -p 1234

On systems that support the jstack action via a node.js helper, get a stack backtrace for any debug message that includes the string "danger!":

dtrace -x strsize=4k -qn 'log-debug/strstr(copyinstr(arg0), "danger!") != NULL/{printf("\n%s", copyinstr(arg0)); jstack()}'

Output of the above might be:

{"name":"foo","hostname":"763bf293-d65c-42d5-872b-4abe25d5c4c7.local","pid":12747,"level":20,"msg":"danger!","time":"2012-10-30T18:28:57.115Z","v":0}

          node`0x87e2010
          DTraceProviderBindings.node`usdt_fire_probe+0x32
          DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d
          DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77
          << internal code >>
          (anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484
          << adaptor >>
          (anon) as doit at /root/my-prog.js position 360
          (anon) as list.ontimeout at timers.js position 4960
          << adaptor >>
          << internal >>
          << entry >>
          node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101
          node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb
          node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0
          node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f
          node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66
          node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63
          node`uv__run_timers+0x66
          node`uv__run+0x1b
          node`uv_run+0x17
          node`_ZN4node5StartEiPPc+0x1d0
          node`main+0x1b
          node`_start+0x83

          node`0x87e2010
          DTraceProviderBindings.node`usdt_fire_probe+0x32
          DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d
          DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77
          << internal code >>
          (anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484
          << adaptor >>
          (anon) as doit at /root/my-prog.js position 360
          (anon) as list.ontimeout at timers.js position 4960
          << adaptor >>
          << internal >>
          << entry >>
          node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101
          node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb
          node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0
          node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f
          node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66
          node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63
          node`uv__run_timers+0x66
          node`uv__run+0x1b
          node`uv_run+0x17
          node`_ZN4node5StartEiPPc+0x1d0
          node`main+0x1b
          node`_start+0x83

Versioning

The scheme I follow is most succintly described by the bootstrap guys here.

tl;dr: All versions are <major>.<minor>.<patch> which will be incremented for breaking backward compat and major reworks, new features without breaking change, and bug fixes, respectively.

License

MIT. See "LICENSE.txt".

See Also

/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
* Copyright (c) 2012 Joyent Inc. All rights reserved.
*
* Test logging with (accidental) usage of buffers.
*/
var util = require('util'),
inspect = util.inspect,
format = util.format;
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
function Catcher() {
this.records = [];
}
Catcher.prototype.write = function (record) {
this.records.push(record);
}
var catcher = new Catcher();
var log = new bunyan.createLogger({
name: 'buffer.test',
streams: [
{
type: 'raw',
stream: catcher,
level: 'trace'
}
]
});
test('log.info(BUFFER)', function (t) {
var b = new Buffer('foo');
['trace',
'debug',
'info',
'warn',
'error',
'fatal'].forEach(function (lvl) {
log[lvl].call(log, b);
var rec = catcher.records[catcher.records.length - 1];
t.equal(rec.msg, inspect(b),
format('log.%s msg is inspect(BUFFER)', lvl));
t.ok(rec['0'] === undefined,
'no "0" array index key in record: ' + inspect(rec['0']));
t.ok(rec['parent'] === undefined,
'no "parent" array index key in record: ' + inspect(rec['parent']));
log[lvl].call(log, b, 'bar');
var rec = catcher.records[catcher.records.length - 1];
t.equal(rec.msg, inspect(b) + ' bar', format(
'log.%s(BUFFER, "bar") msg is inspect(BUFFER) + " bar"', lvl));
});
t.end();
});
//test('log.info({buf: BUFFER})', function (t) {
// var b = new Buffer('foo');
//
// // Really there isn't much Bunyan can do here. See
// // <https://github.com/joyent/node/issues/3905>. An unwelcome hack would
// // be to monkey-patch in Buffer.toJSON. Bletch.
// log.info({buf: b}, 'my message');
// var rec = catcher.records[catcher.records.length - 1];
//
// t.end();
//});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test some `<Logger>.child(...)` behaviour.
*/
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
function CapturingStream(recs) {
this.recs = recs || [];
}
CapturingStream.prototype.write = function (rec) {
this.recs.push(rec);
}
test('child can add stream', function (t) {
var dadStream = new CapturingStream();
var dad = bunyan.createLogger({
name: 'surname',
streams: [ {
type: 'raw',
stream: dadStream,
level: 'info'
} ]
});
var sonStream = new CapturingStream();
var son = dad.child({
component: 'son',
streams: [ {
type: 'raw',
stream: sonStream,
level: 'debug'
} ]
});
dad.info('info from dad');
dad.debug('debug from dad');
son.debug('debug from son');
var rec;
t.equal(dadStream.recs.length, 1);
rec = dadStream.recs[0];
t.equal(rec.msg, 'info from dad');
t.equal(sonStream.recs.length, 1);
rec = sonStream.recs[0];
t.equal(rec.msg, 'debug from son');
t.end();
});
test('child can set level of inherited streams', function (t) {
var dadStream = new CapturingStream();
var dad = bunyan.createLogger({
name: 'surname',
streams: [ {
type: 'raw',
stream: dadStream,
level: 'info'
} ]
});
// Intention here is that the inherited `dadStream` logs at 'debug' level
// for the son.
var son = dad.child({
component: 'son',
level: 'debug'
});
dad.info('info from dad');
dad.debug('debug from dad');
son.debug('debug from son');
var rec;
t.equal(dadStream.recs.length, 2);
rec = dadStream.recs[0];
t.equal(rec.msg, 'info from dad');
rec = dadStream.recs[1];
t.equal(rec.msg, 'debug from son');
t.end();
});
test('child can set level of inherited streams and add streams', function (t) {
var dadStream = new CapturingStream();
var dad = bunyan.createLogger({
name: 'surname',
streams: [ {
type: 'raw',
stream: dadStream,
level: 'info'
} ]
});
// Intention here is that the inherited `dadStream` logs at 'debug' level
// for the son.
var sonStream = new CapturingStream();
var son = dad.child({
component: 'son',
level: 'trace',
streams: [ {
type: 'raw',
stream: sonStream,
level: 'debug'
} ]
});
dad.info('info from dad');
dad.trace('trace from dad');
son.trace('trace from son');
son.debug('debug from son');
t.equal(dadStream.recs.length, 3);
t.equal(dadStream.recs[0].msg, 'info from dad');
t.equal(dadStream.recs[1].msg, 'trace from son');
t.equal(dadStream.recs[2].msg, 'debug from son');
t.equal(sonStream.recs.length, 1);
t.equal(sonStream.recs[0].msg, 'debug from son');
t.end();
});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test the `bunyan` CLI.
*/
var path = require('path');
var exec = require('child_process').exec;
var _ = require('util').format;
var debug = console.warn;
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
var BUNYAN = path.resolve(__dirname, '../bin/bunyan');
//child = exec('cat *.js bad_file | wc -l',
// function (error, stdout, stderr) {
// console.log('stdout: ' + stdout);
// console.log('stderr: ' + stderr);
// if (error !== null) {
// console.log('exec error: ' + error);
// }
//});
test('--version', function (t) {
var version = require('../package.json').version;
exec(BUNYAN + ' --version', function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout, 'bunyan ' + version + '\n');
t.end();
});
});
test('--help', function (t) {
exec(BUNYAN + ' --help', function (err, stdout, stderr) {
t.ifError(err)
t.ok(stdout.indexOf('General options:') !== -1);
t.end();
});
});
test('-h', function (t) {
exec(BUNYAN + ' -h', function (err, stdout, stderr) {
t.ifError(err)
t.ok(stdout.indexOf('General options:') !== -1);
t.end();
});
});
test('--bogus', function (t) {
exec(BUNYAN + ' --bogus', function (err, stdout, stderr) {
t.ok(err, 'should error out')
t.equal(err.code, 1, '... with exit code 1')
t.end();
});
});
test('simple.log', function (t) {
exec(_('%s %s/corpus/simple.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout,
'[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: '
+ 'My message\n');
t.end();
});
});
test('cat simple.log', function (t) {
exec(_('cat %s/corpus/simple.log | %s', __dirname, BUNYAN),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout,
/* JSSTYLED */
'[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n');
t.end();
}
);
});
test('simple.log with color', function (t) {
exec(_('%s --color %s/corpus/simple.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout,
/* JSSTYLED */
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: myservice/123 on example.com: \u001b[36mMy message\u001b[39m\n\u001b[0m');
t.end();
});
});
test('extrafield.log', function (t) {
exec(_('%s %s/corpus/extrafield.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout,
'[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: '
+ 'My message (extra=field)\n');
t.end();
});
});
test('extrafield.log with color', function (t) {
exec(_('%s --color %s/corpus/extrafield.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout,
'[2012-02-08T22:56:52.856Z] \u001b[36m INFO\u001b[39m: '
+ 'myservice/123 '
+ 'on example.com: \u001b[36mMy message\u001b[39m'
+ '\u001b[90m (extra=field)\u001b[39m\n\u001b[0m');
t.end();
});
});
test('bogus.log', function (t) {
exec(_('%s %s/corpus/bogus.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout, 'not a JSON line\n{"hi": "there"}\n');
t.end();
});
});
test('bogus.log -j', function (t) {
exec(_('%s -j %s/corpus/bogus.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err)
t.equal(stdout, 'not a JSON line\n{"hi": "there"}\n');
t.end();
});
});
test('all.log', function (t) {
exec(_('%s %s/corpus/all.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
// Just make sure don't blow up on this.
t.ifError(err)
t.end();
});
});
test('simple.log doesnotexist1.log doesnotexist2.log', function (t) {
exec(_('%s %s/corpus/simple.log doesnotexist1.log doesnotexist2.log',
BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ok(err)
t.equal(err.code, 2)
t.equal(stdout,
/* JSSTYLED */
'[2012-02-08T22:56:52.856Z] INFO: myservice/123 on example.com: My message\n');
// Note: node v0.6.10:
// ENOENT, no such file or directory 'asdf.log'
// but node v0.6.14:
// ENOENT, open 'asdf.log'
// Somewhat annoying change.
t.equal(stderr,
'bunyan: ENOENT, open \'doesnotexist1.log\'\nbunyan: ENOENT, '
+ 'open \'doesnotexist2.log\'\n');
t.end();
}
);
});
test('multiple logs', function (t) {
var cmd = _('%s %s/corpus/log1.log %s/corpus/log2.log',
BUNYAN, __dirname, __dirname);
exec(cmd, function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, [
/* BEGIN JSSTYLED */
'[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n',
/* END JSSTYLED */
].join(''));
t.end();
});
});
test('multiple logs, bunyan format', function (t) {
var cmd = _('%s -o bunyan %s/corpus/log1.log %s/corpus/log2.log',
BUNYAN, __dirname, __dirname);
exec(cmd, function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, [
/* BEGIN JSSTYLED */
'{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:57:55.586Z","v":0}',
'{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:58:55.586Z","v":0}',
'{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:01:49.339Z","v":0}',
'{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:47.404Z","v":0}',
'{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.339Z","v":0}',
'{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}',
'{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}',
'{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:57.404Z","v":0}',
'{"name":"agent2","pid":76156,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:08:01.105Z","v":0}',
''
/* END JSSTYLED */
].join('\n'));
t.end();
});
});
test('log1.log.gz', function (t) {
exec(_('%s %s/corpus/log1.log.gz', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, [
/* BEGIN JSSTYLED */
'[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
/* END JSSTYLED */
].join(''));
t.end();
});
});
test('mixed text and gzip logs', function (t) {
var cmd = _('%s %s/corpus/log1.log.gz %s/corpus/log2.log',
BUNYAN, __dirname, __dirname);
exec(cmd, function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, [
/* BEGIN JSSTYLED */
'[2012-05-08T16:57:55.586Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T16:58:55.586Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:01:49.339Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:02:47.404Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:02:49.339Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:49.404Z] INFO: agent1/73267 on headnode: message\n',
'[2012-05-08T17:02:57.404Z] INFO: agent2/73267 on headnode: message\n',
'[2012-05-08T17:08:01.105Z] INFO: agent2/76156 on headnode: message\n',
/* END JSSTYLED */
].join(''));
t.end();
});
});
test('--level 40', function (t) {
expect = [
/* BEGIN JSSTYLED */
'# levels\n',
'[2012-02-08T22:56:53.856Z] WARN: myservice/123 on example.com: My message\n',
'[2012-02-08T22:56:54.856Z] ERROR: myservice/123 on example.com: My message\n',
'[2012-02-08T22:56:55.856Z] LVL55: myservice/123 on example.com: My message\n',
'[2012-02-08T22:56:56.856Z] FATAL: myservice/123 on example.com: My message\n',
'\n',
'# extra fields\n',
'\n',
'# bogus\n',
'not a JSON line\n',
'{"hi": "there"}\n'
/* END JSSTYLED */
].join('');
exec(_('%s -l 40 %s/corpus/all.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
exec(_('%s --level 40 %s/corpus/all.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
t.end();
});
});
});
test('--condition "level === 10 && pid === 123"', function (t) {
var expect = [
'# levels\n',
/* JSSTYLED */
'[2012-02-08T22:56:50.856Z] TRACE: myservice/123 on example.com: My message\n',
'\n',
'# extra fields\n',
'\n',
'# bogus\n',
'not a JSON line\n',
'{"hi": "there"}\n'
].join('');
var cmd = _('%s -c "level === 10 && pid === 123" %s/corpus/all.log',
BUNYAN, __dirname);
exec(cmd, function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
var cmd = _(
'%s --condition "level === 10 && pid === 123" %s/corpus/all.log',
BUNYAN, __dirname);
exec(cmd, function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
t.end();
});
});
});
// multiple
// not sure if this is a bug or a feature. let's call it a feature!
test('multiple --conditions', function (t) {
var expect = [
'# levels\n',
/* JSSTYLED */
'[2012-02-08T22:56:53.856Z] WARN: myservice/1 on example.com: My message\n',
'\n',
'# extra fields\n',
'\n',
'# bogus\n',
'not a JSON line\n',
'{"hi": "there"}\n'
].join('');
exec(_('%s %s/corpus/all.log ' +
'-c "if (level === 40) pid = 1; true" ' +
'-c "pid === 1"', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
t.end();
});
});
// https://github.com/trentm/node-bunyan/issues/30
//
// One of the records in corpus/withreq.log has a 'req'
// field with no 'headers'. Ditto for the 'res' field.
test('robust req handling', function (t) {
var expect = [
/* BEGIN JSSTYLED */
'[2012-08-08T10:25:47.636Z] DEBUG: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: headAgentProbes respond (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, route=HeadAgentProbes, contentMD5=11FxOYiYfpMxmANj4kGJzg==)',
'[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)',
' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1',
' accept: application/json',
' content-type: application/json',
' host: 10.2.207.16',
' connection: keep-alive',
' --',
' HTTP/1.1 200 OK',
' content-md5: 11FxOYiYfpMxmANj4kGJzg==',
' access-control-allow-origin: *',
' access-control-allow-headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
' access-control-allow-methods: HEAD',
' access-control-expose-headers: X-Api-Version, X-Request-Id, X-Response-Time',
' connection: Keep-Alive',
' date: Wed, 08 Aug 2012 10:25:47 GMT',
' server: Amon Master/1.0.0',
' x-request-id: cce79d15-ffc2-487c-a4e4-e940bdaac31e',
' x-response-time: 3',
' --',
' route: {',
' "name": "HeadAgentProbes",',
' "version": false',
' }',
'[2012-08-08T10:25:47.637Z] INFO: amon-master/12859 on 9724a190-27b6-4fd8-830b-a574f839c67d: HeadAgentProbes handled: 200 (req_id=cce79d15-ffc2-487c-a4e4-e940bdaac31e, audit=true, remoteAddress=10.2.207.2, remotePort=50394, latency=3, secure=false, _audit=true, req.version=*)',
' HEAD /agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037 HTTP/1.1',
' --',
' route: {',
' "name": "HeadAgentProbes",',
' "version": false',
' }'
/* END JSSTYLED */
].join('\n') + '\n';
exec(_('%s %s/corpus/withreq.log', BUNYAN, __dirname),
function (err, stdout, stderr) {
t.ifError(err);
t.equal(stdout, expect);
t.end();
});
});
# levels
{"name":"myservice","pid":123,"hostname":"example.com","level":10,"msg":"My message","time":"2012-02-08T22:56:50.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":20,"msg":"My message","time":"2012-02-08T22:56:51.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":40,"msg":"My message","time":"2012-02-08T22:56:53.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":50,"msg":"My message","time":"2012-02-08T22:56:54.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":55,"msg":"My message","time":"2012-02-08T22:56:55.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":60,"msg":"My message","time":"2012-02-08T22:56:56.856Z","v":0}
# extra fields
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"one":"short","msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"two":"short with space","msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"three":"multi\nline","msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"four":"over 50 chars long long long long long long long long long","msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"five":{"a": "json object"},"msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"six":["a", "json", "array"],"msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
# bogus
not a JSON line
{"hi": "there"}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"extra":"field","msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:57:55.586Z","v":0}
{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.339Z","v":0}
{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}
{"name":"agent1","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:49.404Z","v":0}
���Olog1.logō;
�0�=ƫ�����c+�@*���`#�}���aN�!1��s� �F ������_8ļGn��o@�27����7T�[V(U����-YO��vpS3 �^�߷�P�{��'����/x>��
{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T16:58:55.586Z","v":0}
{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:01:49.339Z","v":0}
{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:47.404Z","v":0}
{"name":"agent2","pid":73267,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:02:57.404Z","v":0}
{"name":"agent2","pid":76156,"hostname":"headnode","level":30,"msg":"message","time":"2012-05-08T17:08:01.105Z","v":0}
{"name":"cnapi.get_existing_nics","job_uuid":"3499b13e-dbca-4331-b13a-f164c0da320a","hostname":"710c784f-6aa5-428c-9074-e046c3af884e","pid":24440,"level":30,"nic":"<unknown>","res":"error: Unknown nic \"020820d753e0\"","msg":"got existing: 02:08:20:d7:53:e0","time":"2012-10-10T16:14:07.610Z","v":0}
{"name":"myservice","pid":123,"hostname":"example.com","level":30,"msg":"My message","time":"2012-02-08T22:56:52.856Z","v":0}
{"name":"amon-master","hostname":"9724a190-27b6-4fd8-830b-a574f839c67d","pid":12859,"route":"HeadAgentProbes","req_id":"cce79d15-ffc2-487c-a4e4-e940bdaac31e","level":20,"contentMD5":"11FxOYiYfpMxmANj4kGJzg==","msg":"headAgentProbes respond","time":"2012-08-08T10:25:47.636Z","v":0}
{"name":"amon-master","hostname":"9724a190-27b6-4fd8-830b-a574f839c67d","pid":12859,"audit":true,"level":30,"remoteAddress":"10.2.207.2","remotePort":50394,"req_id":"cce79d15-ffc2-487c-a4e4-e940bdaac31e","req":{"method":"HEAD","url":"/agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037","headers":{"accept":"application/json","content-type":"application/json","host":"10.2.207.16","connection":"keep-alive"},"httpVersion":"1.1","trailers":{},"version":"*"},"res":{"statusCode":200,"headers":{"content-md5":"11FxOYiYfpMxmANj4kGJzg==","access-control-allow-origin":"*","access-control-allow-headers":"Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version","access-control-allow-methods":"HEAD","access-control-expose-headers":"X-Api-Version, X-Request-Id, X-Response-Time","connection":"Keep-Alive","date":"Wed, 08 Aug 2012 10:25:47 GMT","server":"Amon Master/1.0.0","x-request-id":"cce79d15-ffc2-487c-a4e4-e940bdaac31e","x-response-time":3},"trailer":false},"route":{"name":"HeadAgentProbes","version":false},"latency":3,"secure":false,"_audit":true,"msg":"HeadAgentProbes handled: 200","time":"2012-08-08T10:25:47.637Z","v":0}
{"name":"amon-master","hostname":"9724a190-27b6-4fd8-830b-a574f839c67d","pid":12859,"audit":true,"level":30,"remoteAddress":"10.2.207.2","remotePort":50394,"req_id":"cce79d15-ffc2-487c-a4e4-e940bdaac31e","req":{"method":"HEAD","url":"/agentprobes?agent=ccf92af9-0b24-46b6-ab60-65095fdd3037","httpVersion":"1.1","trailers":{},"version":"*"},"res":{"statusCode":200,"trailer":false},"route":{"name":"HeadAgentProbes","version":false},"latency":3,"secure":false,"_audit":true,"msg":"HeadAgentProbes handled: 200","time":"2012-08-08T10:25:47.637Z","v":0}
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test type checking on creation of the Logger.
*/
var bunyan = require('../lib/bunyan'),
Logger = bunyan;
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
test('ensure Logger creation options', function (t) {
t.throws(function () { new Logger(); },
'options (object) is required',
'no options should throw');
t.throws(function () { new Logger({}); },
'options.name (string) is required',
'no options.name should throw');
t.doesNotThrow(function () { new Logger({name: 'foo'}); },
'just options.name should be sufficient');
var options = {name: 'foo', stream: process.stdout, streams: []};
t.throws(function () { new Logger(options); },
'cannot use "stream" and "streams"');
// https://github.com/trentm/node-bunyan/issues/3
options = {name: 'foo', streams: {}};
t.throws(function () { new Logger(options); },
'invalid options.streams: must be an array',
'"streams" must be an array');
options = {name: 'foo', serializers: 'a string'};
t.throws(function () { new Logger(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be a string');
options = {name: 'foo', serializers: [1, 2, 3]};
t.throws(function () { new Logger(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be an array');
t.end();
});
test('ensure Logger creation options (createLogger)', function (t) {
t.throws(function () { bunyan.createLogger(); },
'options (object) is required',
'no options should throw');
t.throws(function () { bunyan.createLogger({}); },
'options.name (string) is required',
'no options.name should throw');
t.doesNotThrow(function () { bunyan.createLogger({name: 'foo'}); },
'just options.name should be sufficient');
var options = {name: 'foo', stream: process.stdout, streams: []};
t.throws(function () { bunyan.createLogger(options); },
'cannot use "stream" and "streams"');
// https://github.com/trentm/node-bunyan/issues/3
options = {name: 'foo', streams: {}};
t.throws(function () { bunyan.createLogger(options); },
'invalid options.streams: must be an array',
'"streams" must be an array');
options = {name: 'foo', serializers: 'a string'};
t.throws(function () { bunyan.createLogger(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be a string');
options = {name: 'foo', serializers: [1, 2, 3]};
t.throws(function () { bunyan.createLogger(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be an array');
t.end();
});
test('ensure Logger child() options', function (t) {
var log = new Logger({name: 'foo'});
t.doesNotThrow(function () { log.child(); },
'no options should be fine');
t.doesNotThrow(function () { log.child({}); },
'empty options should be fine too');
t.throws(function () { log.child({name: 'foo'}); },
'invalid options.name: child cannot set logger name',
'child cannot change name');
var options = {stream: process.stdout, streams: []};
t.throws(function () { log.child(options); },
'cannot use "stream" and "streams"');
// https://github.com/trentm/node-bunyan/issues/3
options = {streams: {}};
t.throws(function () { log.child(options); },
'invalid options.streams: must be an array',
'"streams" must be an array');
options = {serializers: 'a string'};
t.throws(function () { log.child(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be a string');
options = {serializers: [1, 2, 3]};
t.throws(function () { log.child(options); },
'invalid options.serializers: must be an object',
'"serializers" cannot be an array');
t.end();
});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Make sure cycles are safe.
*/
var Logger = require('../lib/bunyan.js');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
var Stream = require('stream').Stream;
var outstr = new Stream;
outstr.writable = true;
var output = [];
outstr.write = function (c) {
output.push(JSON.parse(c + ''));
};
outstr.end = function (c) {
if (c) this.write(c);
this.emit('end');
};
// these are lacking a few fields that will probably never match
var expect =
[
{
'name': 'blammo',
'level': 30,
'msg': 'bango { bang: \'boom\', KABOOM: [Circular] }',
'v': 0
},
{
'name': 'blammo',
'level': 30,
'msg': 'kaboom { bang: \'boom\', KABOOM: [Circular] }',
'v': 0
},
{
'name': 'blammo',
'level': 30,
'bang': 'boom',
'KABOOM': {
'bang': 'boom',
'KABOOM': '[Circular]'
},
'msg': '',
'v': 0
}
];
var log = new Logger({
name: 'blammo',
streams: [
{
type: 'stream',
level: 'info',
stream: outstr
}
]
});
test('cycles', function (t) {
outstr.on('end', function () {
output.forEach(function (o, i) {
// Drop variable parts for comparison.
delete o.hostname;
delete o.pid;
delete o.time;
// Hack object/dict comparison: JSONify.
t.equal(JSON.stringify(o), JSON.stringify(expect[i]),
'log item ' + i + ' matches');
});
t.end();
});
var obj = { bang: 'boom' };
obj.KABOOM = obj;
log.info('bango', obj);
log.info('kaboom', obj.KABOOM);
log.info(obj);
outstr.end();
t.ok('did not throw');
});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* If available, test dtrace support.
*/
var spawn = require('child_process').spawn;
var format = require('util').format;
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
// Determine if we can run the dtrace tests.
var dtracePlats = ['sunos', 'darwin', 'freebsd'];
var runDtraceTests = true;
try {
require('dtrace-provider')
} catch (e) {
console.log('# skip dtrace tests: no dtrace-provider module');
runDtraceTests = false;
}
if (!runDtraceTests) {
/* pass through */
} else if (dtracePlats.indexOf(process.platform) === -1) {
console.log('# skip dtrace tests: not on a platform with dtrace');
runDtraceTests = false;
} else if (process.env.SKIP_DTRACE) {
console.log('# skip dtrace tests: SKIP_DTRACE envvar set');
runDtraceTests = false;
} else if (process.getgid() !== 0) {
console.log('# skip dtrace tests: gid is not 0, run with `sudo`');
runDtraceTests = false;
}
if (runDtraceTests) {
test('basic dtrace', function (t) {
var argv = ['dtrace', '-Z', '-x', 'strsize=4k', '-qn',
'bunyan$target:::log-*{printf("%s", copyinstr(arg0))}',
'-c', format('node %s/log-some.js', __dirname)];
var dtrace = spawn(argv[0], argv.slice(1));
//console.error('ARGV: %j', argv);
var traces = [];
dtrace.stdout.on('data', function (data) {
//console.error('DTRACE STDOUT:', data.toString());
traces.push(data.toString());
});
dtrace.stderr.on('data', function (data) {
console.error('DTRACE STDERR:', data.toString());
});
dtrace.on('exit', function (code) {
t.notOk(code, 'dtrace exited cleanly');
traces = traces.join('').split('\n')
.filter(function (t) { return t.trim().length })
.map(function (t) { return JSON.parse(t) });
t.equal(traces.length, 2, 'got 2 log records');
if (traces.length) {
t.equal(traces[0].level, bunyan.DEBUG);
t.equal(traces[0].foo, 'bar');
t.equal(traces[1].level, bunyan.TRACE);
t.equal(traces[1].msg, 'hi at trace');
}
t.end();
});
});
} /* end of `if (runDtraceTests)` */
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test emission and handling of 'error' event in a logger with a 'path'
* stream.
*/
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
test('error event on log write', function (t) {
LOG_PATH = '/this/path/is/bogus.log'
var log = bunyan.createLogger(
{name: 'error-event', streams: [ {path: LOG_PATH} ]});
log.on('error', function (err, stream) {
t.ok(err, 'got err in error event: ' + err);
t.equal(err.code, 'ENOENT', 'error code is ENOENT');
t.ok(stream, 'got a stream argument');
t.equal(stream.path, LOG_PATH);
t.equal(stream.type, 'file');
t.end();
});
log.info('info log message');
});
// A helper script to log a few times. We attempt to NOT emit
// to stdout or stderr because this is used for dtrace testing
// and we don't want to mix output.
var bunyan = require('../lib/bunyan');
var log = bunyan.createLogger({
name: 'play',
serializers: bunyan.stdSerializers
});
log.debug({foo: 'bar'}, 'hi at debug')
log.trace('hi at trace')
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test the `log.trace(...)`, `log.debug(...)`, ..., `log.fatal(...)` API.
*/
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
var log1 = bunyan.createLogger({
name: 'log1',
streams: [
{
path: __dirname + '/log.test.log1.log',
level: 'info'
}
]
});
var log2 = bunyan.createLogger({
name: 'log2',
streams: [
{
path: __dirname + '/log.test.log2a.log',
level: 'error'
},
{
path: __dirname + '/log.test.log2b.log',
level: 'debug'
}
]
})
test('log.LEVEL() -> boolean', function (t) {
t.equal(log1.trace(), false, 'log1.trace() is false')
t.equal(log1.debug(), false)
t.equal(log1.info(), true)
t.equal(log1.warn(), true)
t.equal(log1.error(), true)
t.equal(log1.fatal(), true)
// Level is the *lowest* level of all streams.
t.equal(log2.trace(), false)
t.equal(log2.debug(), true)
t.equal(log2.info(), true)
t.equal(log2.warn(), true)
t.equal(log2.error(), true)
t.equal(log2.fatal(), true)
t.end();
});
//TODO:
// - rest of api
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test other parts of the exported API.
*/
var bunyan = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
test('bunyan.<LEVEL>s', function (t) {
t.ok(bunyan.TRACE, 'TRACE');
t.ok(bunyan.DEBUG, 'DEBUG');
t.ok(bunyan.INFO, 'INFO');
t.ok(bunyan.WARN, 'WARN');
t.ok(bunyan.ERROR, 'ERROR');
t.ok(bunyan.FATAL, 'FATAL');
t.end();
});
test('bunyan.resolveLevel()', function (t) {
t.equal(bunyan.resolveLevel('trace'), bunyan.TRACE, 'TRACE');
t.equal(bunyan.resolveLevel('TRACE'), bunyan.TRACE, 'TRACE');
t.equal(bunyan.resolveLevel('debug'), bunyan.DEBUG, 'DEBUG');
t.equal(bunyan.resolveLevel('DEBUG'), bunyan.DEBUG, 'DEBUG');
t.equal(bunyan.resolveLevel('info'), bunyan.INFO, 'INFO');
t.equal(bunyan.resolveLevel('INFO'), bunyan.INFO, 'INFO');
t.equal(bunyan.resolveLevel('warn'), bunyan.WARN, 'WARN');
t.equal(bunyan.resolveLevel('WARN'), bunyan.WARN, 'WARN');
t.equal(bunyan.resolveLevel('error'), bunyan.ERROR, 'ERROR');
t.equal(bunyan.resolveLevel('ERROR'), bunyan.ERROR, 'ERROR');
t.equal(bunyan.resolveLevel('fatal'), bunyan.FATAL, 'FATAL');
t.equal(bunyan.resolveLevel('FATAL'), bunyan.FATAL, 'FATAL');
t.end();
});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test `type: 'raw'` Logger streams.
*/
var format = require('util').format;
var Logger = require('../lib/bunyan');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
function CapturingStream(recs) {
this.recs = recs;
}
CapturingStream.prototype.write = function (rec) {
this.recs.push(rec);
}
test('raw stream', function (t) {
var recs = [];
var log = new Logger({
name: 'raw-stream-test',
streams: [
{
stream: new CapturingStream(recs),
type: 'raw'
}
]
});
log.info('first');
log.info({two: 'deux'}, 'second');
t.equal(recs.length, 2);
t.equal(typeof (recs[0]), 'object', 'first rec is an object');
t.equal(recs[1].two, 'deux', '"two" field made it through');
t.end();
});
test('raw streams and regular streams can mix', function (t) {
var rawRecs = [];
var nonRawRecs = [];
var log = new Logger({
name: 'raw-stream-test',
streams: [
{
stream: new CapturingStream(rawRecs),
type: 'raw'
},
{
stream: new CapturingStream(nonRawRecs)
}
]
});
log.info('first');
log.info({two: 'deux'}, 'second');
t.equal(rawRecs.length, 2);
t.equal(typeof (rawRecs[0]), 'object', 'first rawRec is an object');
t.equal(rawRecs[1].two, 'deux', '"two" field made it through');
t.equal(nonRawRecs.length, 2);
t.equal(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string');
t.end();
});
test('child adding a non-raw stream works', function (t) {
var parentRawRecs = [];
var rawRecs = [];
var nonRawRecs = [];
var logParent = new Logger({
name: 'raw-stream-test',
streams: [
{
stream: new CapturingStream(parentRawRecs),
type: 'raw'
}
]
});
var logChild = logParent.child({
child: true,
streams: [
{
stream: new CapturingStream(rawRecs),
type: 'raw'
},
{
stream: new CapturingStream(nonRawRecs)
}
]
});
logParent.info('first');
logChild.info({two: 'deux'}, 'second');
t.equal(rawRecs.length, 1,
format('rawRecs length should be 1 (is %d)', rawRecs.length));
t.equal(typeof (rawRecs[0]), 'object', 'rawRec entry is an object');
t.equal(rawRecs[0].two, 'deux', '"two" field made it through');
t.equal(nonRawRecs.length, 1);
t.equal(typeof (nonRawRecs[0]), 'string', 'first nonRawRec is a string');
t.end();
});
/*
* Test the RingBuffer output stream.
*/
var Logger = require('../lib/bunyan');
var ringbuffer = new Logger.RingBuffer({ 'limit': 5 });
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
var log1 = new Logger({
name: 'log1',
streams: [
{
stream: ringbuffer,
type: 'raw',
level: 'info'
}
]
});
test('ringbuffer', function (t) {
log1.info('hello');
log1.trace('there');
log1.error('android');
t.equal(ringbuffer.records.length, 2);
t.equal(ringbuffer.records[0]['msg'], 'hello');
t.equal(ringbuffer.records[1]['msg'], 'android');
log1.error('one');
log1.error('two');
log1.error('three');
t.equal(ringbuffer.records.length, 5);
log1.error('four');
t.equal(ringbuffer.records.length, 5);
t.equal(ringbuffer.records[0]['msg'], 'android');
t.equal(ringbuffer.records[1]['msg'], 'one');
t.equal(ringbuffer.records[2]['msg'], 'two');
t.equal(ringbuffer.records[3]['msg'], 'three');
t.equal(ringbuffer.records[4]['msg'], 'four');
t.end();
});
/*
* Copyright (c) 2012 Trent Mick. All rights reserved.
*
* Test the standard serializers in Bunyan.
*/
var http = require('http');
var bunyan = require('../lib/bunyan');
var verror = require('verror');
// node-tap API
if (require.cache[__dirname + '/tap4nodeunit.js'])
delete require.cache[__dirname + '/tap4nodeunit.js'];
var tap4nodeunit = require('./tap4nodeunit.js');
var after = tap4nodeunit.after;
var before = tap4nodeunit.before;
var test = tap4nodeunit.test;
function CapturingStream(recs) {
this.recs = recs;
}
CapturingStream.prototype.write = function (rec) {
this.recs.push(rec);
}
test('req serializer', function (t) {
var records = [];
var log = bunyan.createLogger({
name: 'serializer-test',
streams: [
{
stream: new CapturingStream(records),
type: 'raw'
}
],
serializers: {
req: bunyan.stdSerializers.req
}
});
// None of these should blow up.
var bogusReqs = [
undefined,
null,
{},
1,
'string',
[1, 2, 3],
{'foo':'bar'}
];
for (var i = 0; i < bogusReqs.length; i++) {
log.info({req: bogusReqs[i]}, 'hi');
t.equal(records[i].req, bogusReqs[i]);
}
// Get http request and response objects to play with and test.
var theReq, theRes;
var server = http.createServer(function (req, res) {
theReq = req;
theRes = res;
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
})
server.listen(8765, function () {
http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) {
res.resume();
log.info({req: theReq}, 'the request');
var lastRecord = records[records.length-1];
t.equal(lastRecord.req.method, 'GET');
t.equal(lastRecord.req.url, theReq.url);
t.equal(lastRecord.req.remoteAddress,
theReq.connection.remoteAddress);
t.equal(lastRecord.req.remotePort, theReq.connection.remotePort);
server.close();
t.end();
}).on('error', function (err) {
t.ok(false, 'error requesting to our test server: ' + err);
server.close();
t.end();
});
});
});
test('res serializer', function (t) {
var records = [];
var log = bunyan.createLogger({
name: 'serializer-test',
streams: [
{
stream: new CapturingStream(records),
type: 'raw'
}
],
serializers: {
res: bunyan.stdSerializers.res
}
});
// None of these should blow up.
var bogusRess = [
undefined,
null,
{},
1,
'string',
[1, 2, 3],
{'foo':'bar'}
];
for (var i = 0; i < bogusRess.length; i++) {
log.info({res: bogusRess[i]}, 'hi');
t.equal(records[i].res, bogusRess[i]);
}
// Get http request and response objects to play with and test.
var theReq, theRes;
var server = http.createServer(function (req, res) {
theReq = req;
theRes = res;
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
})
server.listen(8765, function () {
http.get({host: '127.0.0.1', port: 8765, path: '/'}, function (res) {
res.resume();
log.info({res: theRes}, 'the response');
var lastRecord = records[records.length-1];
t.equal(lastRecord.res.statusCode, theRes.statusCode);
t.equal(lastRecord.res.header, theRes._header);
server.close();
t.end();
}).on('error', function (err) {
t.ok(false, 'error requesting to our test server: ' + err);
server.close();
t.end();
});
});
});
test('err serializer', function (t) {
var records = [];
var log = bunyan.createLogger({
name: 'serializer-test',
streams: [
{
stream: new CapturingStream(records),
type: 'raw'
}
],
serializers: {
err: bunyan.stdSerializers.err
}
});
// None of these should blow up.
var bogusErrs = [
undefined,
null,
{},
1,
'string',
[1, 2, 3],
{'foo':'bar'}
];
for (var i = 0; i < bogusErrs.length; i++) {
log.info({err: bogusErrs[i]}, 'hi');
t.equal(records[i].err, bogusErrs[i]);
}
var theErr = new TypeError('blah');
log.info(theErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, theErr.message);
t.equal(lastRecord.err.name, theErr.name);
t.equal(lastRecord.err.stack, theErr.stack);
t.end();
});
test('err serializer: long stack', function (t) {
var records = [];
var log = bunyan.createLogger({
name: 'serializer-test',
streams: [ {
stream: new CapturingStream(records),
type: 'raw'
} ],
serializers: {
err: bunyan.stdSerializers.err
}
});
var topErr, midErr, bottomErr;
// Just a VError.
topErr = new verror.VError('top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message, 'Just a VError');
t.equal(lastRecord.err.name, topErr.name, 'Just a VError');
t.equal(lastRecord.err.stack, topErr.stack, 'Just a VError');
// Just a WError.
topErr = new verror.WError('top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message, 'Just a WError');
t.equal(lastRecord.err.name, topErr.name, 'Just a WError');
t.equal(lastRecord.err.stack, topErr.stack, 'Just a WError');
// WError <- TypeError
bottomErr = new TypeError('bottom err');
topErr = new verror.WError(bottomErr, 'top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message, 'WError <- TypeError');
t.equal(lastRecord.err.name, topErr.name, 'WError <- TypeError');
var expectedStack = topErr.stack + '\nCaused by: ' + bottomErr.stack;
t.equal(lastRecord.err.stack, expectedStack, 'WError <- TypeError');
// WError <- WError
bottomErr = new verror.WError('bottom err');
topErr = new verror.WError(bottomErr, 'top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message, 'WError <- WError');
t.equal(lastRecord.err.name, topErr.name, 'WError <- WError');
var expectedStack = topErr.stack + '\nCaused by: ' + bottomErr.stack;
t.equal(lastRecord.err.stack, expectedStack, 'WError <- WError');
// WError <- WError <- TypeError
bottomErr = new TypeError('bottom err');
midErr = new verror.WError(bottomErr, 'mid err');
topErr = new verror.WError(midErr, 'top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message,
'WError <- WError <- TypeError');
t.equal(lastRecord.err.name, topErr.name, 'WError <- WError <- TypeError');
var expectedStack = (topErr.stack
+ '\nCaused by: ' + midErr.stack
+ '\nCaused by: ' + bottomErr.stack);
t.equal(lastRecord.err.stack, expectedStack,
'WError <- WError <- TypeError');
// WError <- WError <- WError
bottomErr = new verror.WError('bottom err');
midErr = new verror.WError(bottomErr, 'mid err');
topErr = new verror.WError(midErr, 'top err');
log.info(topErr, 'the error');
var lastRecord = records[records.length-1];
t.equal(lastRecord.err.message, topErr.message,
'WError <- WError <- WError');
t.equal(lastRecord.err.name, topErr.name, 'WError <- WError <- WError');
var expectedStack = (topErr.stack
+ '\nCaused by: ' + midErr.stack
+ '\nCaused by: ' + bottomErr.stack);
t.equal(lastRecord.err.stack, expectedStack, 'WError <- WError <- WError');
t.end();
});
// Bunyan 0.18.3 introduced a bug where *all* serializers are applied
// even if the log record doesn't have the associated key. That means
// serializers that don't handle an `undefined` value will blow up.
test('do not apply serializers if no record key', function (t) {
var records = [];
var log = bunyan.createLogger({
name: 'serializer-test',
streams: [ {
stream: new CapturingStream(records),
type: 'raw'
} ],
serializers: {
err: bunyan.stdSerializers.err,
boom: function (value) {
throw new Error('boom');
}
}
});
log.info({foo: 'bar'}, 'record one');
log.info({err: new Error('record two err')}, 'record two');
t.equal(records[0].boom, undefined);
t.equal(records[0].foo, 'bar');
t.equal(records[1].boom, undefined);
t.equal(records[1].err.message, 'record two err');
t.end();
});
/*
* Copyright 2012 Mark Cavage. All rights reserved.
*
* Help nodeunit API feel like node-tap's.
*
* Usage:
* if (require.cache[__dirname + '/tap4nodeunit.js'])
* delete require.cache[__dirname + '/tap4nodeunit.js'];
* var tap4nodeunit = require('./tap4nodeunit.js');
* var after = tap4nodeunit.after;
* var before = tap4nodeunit.before;
* var test = tap4nodeunit.test;
*/
//---- Exports
module.exports = {
after: function after(teardown) {
module.parent.exports.tearDown = function _teardown(callback) {
try {
teardown.call(this, callback);
} catch (e) {
console.error('after:\n' + e.stack);
process.exit(1);
}
};
},
before: function before(setup) {
module.parent.exports.setUp = function _setup(callback) {
try {
setup.call(this, callback);
} catch (e) {
console.error('before:\n' + e.stack);
process.exit(1);
}
};
},
test: function test(name, tester) {
module.parent.exports[name] = function _(t) {
var _done = false;
t.end = function end() {
if (!_done) {
_done = true;
t.done();
}
};
t.notOk = function notOk(ok, message) {
return (t.ok(!ok, message));
};
t.error = t.ifError;
tester.call(this, t);
};
}
};
  • need .npmignore? tools dir, etc.
  • man page for the bunyan CLI (refer to it in the readme)
  • tail -f-like support
  • 1.0 with v: 1 in log records. Fwd/bwd compat in bunyan CLI

someday/maybe

  • full-on docs

  • better examples/

  • better coloring

  • "template" support for 'rotating-file' stream to get dated rolled files

  • "all" or "off" levels? log4j? logging.py? logging.py has NOTSET === 0. I think that is only needed/used for multi-level hierarchical effective level.

  • buffered writes to increase speed:

    • I'd start with a tools/timeoutput.js for some numbers to compare before/after. Sustained high output to a file.
    • perhaps this would be a "buffered: true" option on the stream object
    • then wrap the "stream" with a local class that handles the buffering
    • to finish this, need the 'log.close' and process.on('exit', ...) work that Trent has started.
  • "canWrite" handling for full streams. Need to buffer a la log4js

  • test file log with logadm rotation: does it handle that?

  • test suite:

    • test for a cloned logger double-stream.end() causing problems. Perhaps the "closeOnExit" for existing streams should be false for clones.
    • test that a log.clone(...) adding a new field matching a serializer works and that an existing field in the parent is not re-serialized.
  • split out bunyan cli to a "bunyan" or "bunyan-reader" or "node-bunyan-reader" as the basis for tools to consume bunyan logs. It can grow indep of node-bunyan for generating the logs. It would take a Bunyan log record object and be expected to emit it.

      node-bunyan-reader
          .createReadStream(path, [options]) ?
    
  • coloring bug: in less the indented extra info lines only have the first line colored. Do we need the ANSI char on each line? That'll be slower.

  • document "well-known" keys from bunyan CLI p.o.v.. Add "client_req".

  • More bunyan output formats and filtering features.

  • Think about a bunyan dashboard that supports organizing and viewing logs from multiple hosts and services.

  • doc the restify RequestCaptureStream usage of RingBuffer. Great example.

  • A vim plugin (a la http://vim.cybermirror.org/runtime/autoload/zip.vim ?) to allow browsing (read-only) a bunyan log in rendered form.

  • Some speed comparisons with others to get a feel for Bunyan's speed.

  • what about promoting 'latency' field and making that easier?

  • log.close to close streams and shutdown and this.closed process.on('exit', log.close) -> 'end' for the name

  • bunyan cli: more layouts (http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/EnhancedPatternLayout.html) Custom log formats (in config file? in '-f' arg) using printf or hogan.js or whatever. Dap wants field width control for lining up. Hogan.js is probably overkill for this.

  • loggly example using raw streams, hook.io?, whatever.

  • serializer support:

    • restify-server.js example -> restifyReq ? or have req detect that. That is nicer for the "use all standard ones". Does restify req have anything special?
    • differential HTTP client req/res with server req/res.
  • statsd stream? http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/ Think about it.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2009-2012 Trent Mick
"""cutarelease -- Cut a release of your project.
A script that will help cut a release for a git-based project that follows
a few conventions. It'll update your changelog (CHANGES.md), add a git
tag, push those changes, update your version to the next patch level release
and create a new changelog section for that new version.
Conventions:
- XXX
"""
__version_info__ = (1, 0, 7)
__version__ = '.'.join(map(str, __version_info__))
import sys
import os
from os.path import join, dirname, normpath, abspath, exists, basename, splitext
from glob import glob
from pprint import pprint
import re
import codecs
import logging
import optparse
import json
#---- globals and config
log = logging.getLogger("cutarelease")
class Error(Exception):
pass
#---- main functionality
def cutarelease(project_name, version_files, dry_run=False):
"""Cut a release.
@param project_name {str}
@param version_files {list} List of paths to files holding the version
info for this project.
If none are given it attempts to guess the version file:
package.json or VERSION.txt or VERSION or $project_name.py
or lib/$project_name.py or $project_name.js or lib/$project_name.js.
The version file can be in one of the following forms:
- A .py file, in which case the file is expect to have a top-level
global called "__version_info__" as follows. [1]
__version_info__ = (0, 7, 6)
Note that I typically follow that with the following to get a
string version attribute on my modules:
__version__ = '.'.join(map(str, __version_info__))
- A .js file, in which case the file is expected to have a top-level
global called "VERSION" as follows:
ver VERSION = "1.2.3";
- A "package.json" file, typical of a node.js npm-using project.
The package.json file must have a "version" field.
- TODO: A simple version file whose only content is a "1.2.3"-style version
string.
[1]: This is a convention I tend to follow in my projects.
Granted it might not be your cup of tea. I should add support for
just `__version__ = "1.2.3"`. I'm open to other suggestions too.
"""
dry_run_str = dry_run and " (dry-run)" or ""
if not version_files:
log.info("guessing version file")
candidates = [
"package.json",
"VERSION.txt",
"VERSION",
"%s.py" % project_name,
"lib/%s.py" % project_name,
"%s.js" % project_name,
"lib/%s.js" % project_name,
]
for candidate in candidates:
if exists(candidate):
version_files = [candidate]
break
else:
raise Error("could not find a version file: specify its path or "
"add one of the following to your project: '%s'"
% "', '".join(candidates))
log.info("using '%s' as version file", version_files[0])
parsed_version_files = [_parse_version_file(f) for f in version_files]
version_file_type, version_info = parsed_version_files[0]
version = _version_from_version_info(version_info)
# Confirm
if not dry_run:
answer = query_yes_no("* * *\n"
"Are you sure you want cut a %s release?\n"
"This will involved commits and a push." % version,
default="no")
print "* * *"
if answer != "yes":
log.info("user abort")
return
log.info("cutting a %s release%s", version, dry_run_str)
# Checks: Ensure there is a section in changes for this version.
changes_path = "CHANGES.md"
changes_txt, changes, nyr = parse_changelog(changes_path)
#pprint(changes)
top_ver = changes[0]["version"]
if top_ver != version:
raise Error("changelog '%s' top section says "
"version %r, expected version %r: aborting"
% (changes_path, top_ver, version))
top_verline = changes[0]["verline"]
if not top_verline.endswith(nyr):
answer = query_yes_no("\n* * *\n"
"The changelog '%s' top section doesn't have the expected\n"
"'%s' marker. Has this been released already?"
% (changes_path, nyr), default="yes")
print "* * *"
if answer != "no":
log.info("abort")
return
top_body = changes[0]["body"]
if top_body.strip() == "(nothing yet)":
raise Error("top section body is `(nothing yet)': it looks like "
"nothing has been added to this release")
# Commits to prepare release.
changes_txt_before = changes_txt
changes_txt = changes_txt.replace(" (not yet released)", "", 1)
if not dry_run and changes_txt != changes_txt_before:
log.info("prepare `%s' for release", changes_path)
f = codecs.open(changes_path, 'w', 'utf-8')
f.write(changes_txt)
f.close()
run('git commit %s -m "prepare for %s release"'
% (changes_path, version))
# Tag version and push.
curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t)
if not dry_run and version not in curr_tags:
log.info("tag the release")
run('git tag -a "%s" -m "version %s"' % (version, version))
run('git push --tags')
# Optionally release.
if exists("package.json"):
answer = query_yes_no("\n* * *\nPublish to npm?", default="yes")
print "* * *"
if answer == "yes":
if dry_run:
log.info("skipping npm publish (dry-run)")
else:
run('npm publish')
elif exists("setup.py"):
answer = query_yes_no("\n* * *\nPublish to pypi?", default="yes")
print "* * *"
if answer == "yes":
if dry_run:
log.info("skipping pypi publish (dry-run)")
else:
run("%spython setup.py sdist --formats zip upload"
% _setup_command_prefix())
# Commits to prepare for future dev and push.
# - update changelog file
next_version_info = _get_next_version_info(version_info)
next_version = _version_from_version_info(next_version_info)
log.info("prepare for future dev (version %s)", next_version)
marker = "## " + changes[0]["verline"]
if marker.endswith(nyr):
marker = marker[0:-len(nyr)]
if marker not in changes_txt:
raise Error("couldn't find `%s' marker in `%s' "
"content: can't prep for subsequent dev" % (marker, changes_path))
next_verline = "%s %s%s" % (marker.rsplit(None, 1)[0], next_version, nyr)
changes_txt = changes_txt.replace(marker + '\n',
"%s\n\n(nothing yet)\n\n\n%s\n" % (next_verline, marker))
if not dry_run:
f = codecs.open(changes_path, 'w', 'utf-8')
f.write(changes_txt)
f.close()
# - update version file
next_version_tuple = _tuple_from_version(next_version)
for i, ver_file in enumerate(version_files):
ver_content = codecs.open(ver_file, 'r', 'utf-8').read()
ver_file_type, ver_info = parsed_version_files[i]
if ver_file_type == "json":
marker = '"version": "%s"' % version
if marker not in ver_content:
raise Error("couldn't find `%s' version marker in `%s' "
"content: can't prep for subsequent dev" % (marker, ver_file))
ver_content = ver_content.replace(marker,
'"version": "%s"' % next_version)
elif ver_file_type == "javascript":
candidates = [
("single", "var VERSION = '%s';" % version),
("double", 'var VERSION = "%s";' % version),
]
for quote_type, marker in candidates:
if marker in ver_content:
break
else:
raise Error("couldn't find any candidate version marker in "
"`%s' content: can't prep for subsequent dev: %r"
% (ver_file, candidates))
if quote_type == "single":
ver_content = ver_content.replace(marker,
"var VERSION = '%s';" % next_version)
else:
ver_content = ver_content.replace(marker,
'var VERSION = "%s";' % next_version)
elif ver_file_type == "python":
marker = "__version_info__ = %r" % (version_info,)
if marker not in ver_content:
raise Error("couldn't find `%s' version marker in `%s' "
"content: can't prep for subsequent dev" % (marker, ver_file))
ver_content = ver_content.replace(marker,
"__version_info__ = %r" % (next_version_tuple,))
elif ver_file_type == "version":
ver_content = next_version
else:
raise Error("unknown ver_file_type: %r" % ver_file_type)
if not dry_run:
log.info("update version to '%s' in '%s'", next_version, ver_file)
f = codecs.open(ver_file, 'w', 'utf-8')
f.write(ver_content)
f.close()
if not dry_run:
run('git commit %s %s -m "prep for future dev"' % (
changes_path, ' '.join(version_files)))
run('git push')
#---- internal support routines
def _indent(s, indent=' '):
return indent + indent.join(s.splitlines(True))
def _tuple_from_version(version):
def _intify(s):
try:
return int(s)
except ValueError:
return s
return tuple(_intify(b) for b in version.split('.'))
def _get_next_version_info(version_info):
next = list(version_info[:])
next[-1] += 1
return tuple(next)
def _version_from_version_info(version_info):
v = str(version_info[0])
state_dot_join = True
for i in version_info[1:]:
if state_dot_join:
try:
int(i)
except ValueError:
state_dot_join = False
else:
pass
if state_dot_join:
v += "." + str(i)
else:
v += str(i)
return v
_version_re = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+)([abc](\d+)?)?)?$")
def _version_info_from_version(version):
m = _version_re.match(version)
if not m:
raise Error("could not convert '%s' version to version info" % version)
version_info = []
for g in m.groups():
if g is None:
break
try:
version_info.append(int(g))
except ValueError:
version_info.append(g)
return tuple(version_info)
def _parse_version_file(version_file):
"""Get version info from the given file. It can be any of:
Supported version file types (i.e. types of files from which we know
how to parse the version string/number -- often by some convention):
- json: use the "version" key
- javascript: look for a `var VERSION = "1.2.3";` or
`var VERSION = '1.2.3';`
- python: Python script/module with `__version_info__ = (1, 2, 3)`
- version: a VERSION.txt or VERSION file where the whole contents are
the version string
@param version_file {str} Can be a path or "type:path", where "type"
is one of the supported types.
"""
# Get version file *type*.
version_file_type = None
match = re.compile("^([a-z]+):(.*)$").search(version_file)
if match:
version_file = match.group(2)
version_file_type = match.group(1)
aliases = {
"js": "javascript"
}
if version_file_type in aliases:
version_file_type = aliases[version_file_type]
f = codecs.open(version_file, 'r', 'utf-8')
content = f.read()
f.close()
if not version_file_type:
# Guess the type.
base = basename(version_file)
ext = splitext(base)[1]
if ext == ".json":
version_file_type = "json"
elif ext == ".py":
version_file_type = "python"
elif ext == ".js":
version_file_type = "javascript"
elif content.startswith("#!"):
shebang = content.splitlines(False)[0]
shebang_bits = re.split(r'[/ \t]', shebang)
for name, typ in {"python": "python", "node": "javascript"}.items():
if name in shebang_bits:
version_file_type = typ
break
elif base in ("VERSION", "VERSION.txt"):
version_file_type = "version"
if not version_file_type:
raise RuntimeError("can't extract version from '%s': no idea "
"what type of file it it" % version_file)
if version_file_type == "json":
obj = json.loads(content)
version_info = _version_info_from_version(obj["version"])
elif version_file_type == "python":
m = re.search(r'^__version_info__ = (.*?)$', content, re.M)
version_info = eval(m.group(1))
elif version_file_type == "javascript":
m = re.search(r'^var VERSION = (\'|")(.*?)\1;$', content, re.M)
version_info = _version_info_from_version(m.group(2))
elif version_file_type == "version":
version_info = _version_info_from_version(content.strip())
else:
raise RuntimeError("unexpected version_file_type: %r"
% version_file_type)
return version_file_type, version_info
def parse_changelog(changes_path):
"""Parse the given changelog path and return `(content, parsed, nyr)`
where `nyr` is the ' (not yet released)' marker and `parsed` looks like:
[{'body': u'\n(nothing yet)\n\n',
'verline': u'restify 1.0.1 (not yet released)',
'version': u'1.0.1'}, # version is parsed out for top section only
{'body': u'...',
'verline': u'1.0.0'},
{'body': u'...',
'verline': u'1.0.0-rc2'},
{'body': u'...',
'verline': u'1.0.0-rc1'}]
A changelog (CHANGES.md) is expected to look like this:
# $project Changelog
## $next_version (not yet released)
...
## $version1
...
## $version2
... and so on
The version lines are enforced as follows:
- The top entry should have a " (not yet released)" suffix. "Should"
because recovery from half-cutarelease failures is supported.
- A version string must be extractable from there, but it tries to
be loose (though strict "X.Y.Z" versioning is preferred). Allowed
## 1.0.0
## my project 1.0.1
## foo 1.2.3-rc2
Basically, (a) the " (not yet released)" is stripped, (b) the
last token is the version, and (c) that version must start with
a digit (sanity check).
"""
if not exists(changes_path):
raise Error("changelog file '%s' not found" % changes_path)
content = codecs.open(changes_path, 'r', 'utf-8').read()
parser = re.compile(
r'^##\s*(?P<verline>[^\n]*?)\s*$(?P<body>.*?)(?=^##|\Z)',
re.M | re.S)
sections = parser.findall(content)
# Sanity checks on changelog format.
if not sections:
template = "## 1.0.0 (not yet released)\n\n(nothing yet)\n"
raise Error("changelog '%s' must have at least one section, "
"suggestion:\n\n%s" % (changes_path, _indent(template)))
first_section_verline = sections[0][0]
nyr = ' (not yet released)'
#if not first_section_verline.endswith(nyr):
# eg = "## %s%s" % (first_section_verline, nyr)
# raise Error("changelog '%s' top section must end with %r, "
# "naive e.g.: '%s'" % (changes_path, nyr, eg))
items = []
for i, section in enumerate(sections):
item = {
"verline": section[0],
"body": section[1]
}
if i == 0:
# We only bother to pull out 'version' for the top section.
verline = section[0]
if verline.endswith(nyr):
verline = verline[0:-len(nyr)]
version = verline.split()[-1]
try:
int(version[0])
except ValueError:
msg = ''
if version.endswith(')'):
msg = " (cutarelease is picky about the trailing %r " \
"on the top version line. Perhaps you misspelled " \
"that?)" % nyr
raise Error("changelog '%s' top section version '%s' is "
"invalid: first char isn't a number%s"
% (changes_path, version, msg))
item["version"] = version
items.append(item)
return content, items, nyr
## {{{ http://code.activestate.com/recipes/577058/ (r2)
def query_yes_no(question, default="yes"):
"""Ask a yes/no question via raw_input() and return their answer.
"question" is a string that is presented to the user.
"default" is the presumed answer if the user just hits <Enter>.
It must be "yes" (the default), "no" or None (meaning
an answer is required of the user).
The "answer" return value is one of "yes" or "no".
"""
valid = {"yes":"yes", "y":"yes", "ye":"yes",
"no":"no", "n":"no"}
if default == None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
elif default == "no":
prompt = " [y/N] "
else:
raise ValueError("invalid default answer: '%s'" % default)
while 1:
sys.stdout.write(question + prompt)
choice = raw_input().lower()
if default is not None and choice == '':
return default
elif choice in valid.keys():
return valid[choice]
else:
sys.stdout.write("Please respond with 'yes' or 'no' "\
"(or 'y' or 'n').\n")
## end of http://code.activestate.com/recipes/577058/ }}}
def _capture_stdout(argv):
import subprocess
p = subprocess.Popen(argv, stdout=subprocess.PIPE)
return p.communicate()[0]
class _NoReflowFormatter(optparse.IndentedHelpFormatter):
"""An optparse formatter that does NOT reflow the description."""
def format_description(self, description):
return description or ""
def run(cmd):
"""Run the given command.
Raises OSError is the command returns a non-zero exit status.
"""
log.debug("running '%s'", cmd)
fixed_cmd = cmd
if sys.platform == "win32" and cmd.count('"') > 2:
fixed_cmd = '"' + cmd + '"'
retval = os.system(fixed_cmd)
if hasattr(os, "WEXITSTATUS"):
status = os.WEXITSTATUS(retval)
else:
status = retval
if status:
raise OSError(status, "error running '%s'" % cmd)
def _setup_command_prefix():
prefix = ""
if sys.platform == "darwin":
# http://forums.macosxhints.com/archive/index.php/t-43243.html
# This is an Apple customization to `tar` to avoid creating
# '._foo' files for extended-attributes for archived files.
prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 "
return prefix
#---- mainline
def main(argv):
logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s")
log.setLevel(logging.INFO)
# Parse options.
parser = optparse.OptionParser(prog="cutarelease", usage='',
version="%prog " + __version__, description=__doc__,
formatter=_NoReflowFormatter())
parser.add_option("-v", "--verbose", dest="log_level",
action="store_const", const=logging.DEBUG,
help="more verbose output")
parser.add_option("-q", "--quiet", dest="log_level",
action="store_const", const=logging.WARNING,
help="quieter output (just warnings and errors)")
parser.set_default("log_level", logging.INFO)
parser.add_option("--test", action="store_true",
help="run self-test and exit (use 'eol.py -v --test' for verbose test output)")
parser.add_option("-p", "--project-name", metavar="NAME",
help='the name of this project (default is the base dir name)',
default=basename(os.getcwd()))
parser.add_option("-f", "--version-file", metavar="[TYPE:]PATH",
action='append', dest="version_files",
help='The path to the project file holding the version info. Can be '
'specified multiple times if more than one file should be updated '
'with new version info. If excluded, it will be guessed.')
parser.add_option("-n", "--dry-run", action="store_true",
help='Do a dry-run', default=False)
opts, args = parser.parse_args()
log.setLevel(opts.log_level)
cutarelease(opts.project_name, opts.version_files, dry_run=opts.dry_run)
## {{{ http://code.activestate.com/recipes/577258/ (r5+)
if __name__ == "__main__":
try:
retval = main(sys.argv)
except KeyboardInterrupt:
sys.exit(1)
except SystemExit:
raise
except:
import traceback, logging
if not log.handlers and not logging.root.handlers:
logging.basicConfig()
skip_it = False
exc_info = sys.exc_info()
if hasattr(exc_info[0], "__name__"):
exc_class, exc, tb = exc_info
if isinstance(exc, IOError) and exc.args[0] == 32:
# Skip 'IOError: [Errno 32] Broken pipe': often a cancelling of `less`.
skip_it = True
if not skip_it:
tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3]
log.error("%s (%s:%s in %s)", exc_info[1], tb_path,
tb_lineno, tb_func)
else: # string exception
log.error(exc_info[0])
if not skip_it:
if log.isEnabledFor(logging.DEBUG):
traceback.print_exception(*exc_info)
sys.exit(1)
else:
sys.exit(retval)
## end of http://code.activestate.com/recipes/577258/ }}}
#!/usr/bin/env perl
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2008 Sun Microsystems, Inc. All rights reserved.
# Use is subject to license terms.
#
# Copyright 2011 Joyent, Inc. All rights reserved.
#
# jsstyle - check for some common stylistic errors.
#
# jsstyle is a sort of "lint" for Javascript coding style. This tool is
# derived from the cstyle tool, used to check for the style used in the
# Solaris kernel, sometimes known as "Bill Joy Normal Form".
#
# There's a lot this can't check for, like proper indentation of code
# blocks. There's also a lot more this could check for.
#
# A note to the non perl literate:
#
# perl regular expressions are pretty much like egrep
# regular expressions, with the following special symbols
#
# \s any space character
# \S any non-space character
# \w any "word" character [a-zA-Z0-9_]
# \W any non-word character
# \d a digit [0-9]
# \D a non-digit
# \b word boundary (between \w and \W)
# \B non-word boundary
#
require 5.0;
use IO::File;
use Getopt::Std;
use strict;
my $usage =
"Usage: jsstyle [-h?vcC] [-t <num>] [-f <path>] [-o <config>] file ...
Check your JavaScript file for style.
See <https://github.com/davepacheco/jsstyle> for details on config options.
Report bugs to <https://github.com/davepacheco/jsstyle/issues>.
Options:
-h print this help and exit
-v verbose
-c check continuation indentation inside functions
-t specify tab width for line length calculation
-C don't check anything in header block comments
-f PATH
path to a jsstyle config file
-o OPTION1,OPTION2
set config options, e.g. '-o doxygen,indent=2'
";
my %opts;
if (!getopts("ch?o:t:f:vC", \%opts)) {
print $usage;
exit 2;
}
if (defined($opts{'h'}) || defined($opts{'?'})) {
print $usage;
exit;
}
my $check_continuation = $opts{'c'};
my $verbose = $opts{'v'};
my $ignore_hdr_comment = $opts{'C'};
my $tab_width = $opts{'t'};
# By default, tabs are 8 characters wide
if (! defined($opts{'t'})) {
$tab_width = 8;
}
# Load config
my %config = (
indent => "tab",
doxygen => 0, # doxygen comments: /** ... */
splint => 0, # splint comments. Needed?
"unparenthesized-return" => 1,
"literal-string-quote" => "single", # 'single' or 'double'
"blank-after-start-comment" => 1,
"continuation-at-front" => 0,
"leading-right-paren-ok" => 0,
"strict-indent" => 0
);
sub add_config_var ($$) {
my ($scope, $str) = @_;
if ($str !~ /^([\w-]+)(?:\s*=\s*(.*?))?$/) {
die "$scope: invalid option: '$str'";
}
my $name = $1;
my $value = ($2 eq '' ? 1 : $2);
#print "scope: '$scope', str: '$str', name: '$name', value: '$value'\n";
# Validate config var.
if ($name eq "indent") {
# A number of spaces or "tab".
if ($value !~ /^\d+$/ && $value ne "tab") {
die "$scope: invalid '$name': must be a number (of ".
"spaces) or 'tab'";
}
} elsif ($name eq "doxygen" || # boolean vars
$name eq "splint" ||
$name eq "unparenthesized-return" ||
$name eq "continuation-at-front" ||
$name eq "leading-right-paren-ok" ||
$name eq "leading-comma-ok" ||
$name eq "uncuddled-else-ok" ||
$name eq "whitespace-after-left-paren-ok" ||
$name eq "strict-indent" ||
$name eq "blank-after-start-comment") {
if ($value != 1 && $value != 0) {
die "$scope: invalid '$name': don't give a value";
}
} elsif ($name eq "literal-string-quote") {
if ($value !~ /single|double/) {
die "$scope: invalid '$name': must be 'single' ".
"or 'double'";
}
} else {
die "$scope: unknown config var: $name";
}
$config{$name} = $value;
}
if (defined($opts{'f'})) {
my $path = $opts{'f'};
my $fh = new IO::File $path, "r";
if (!defined($fh)) {
die "cannot open config path '$path'";
}
my $line = 0;
while (<$fh>) {
$line++;
s/^\s*//; # drop leading space
s/\s*$//; # drop trailing space
next if ! $_; # skip empty line
next if /^#/; # skip comments
add_config_var "$path:$line", $_;
}
}
if (defined($opts{'o'})) {
for my $x (split /,/, $opts{'o'}) {
add_config_var "'-o' option", $x;
}
}
my ($filename, $line, $prev); # shared globals
my $fmt;
my $hdr_comment_start;
if ($verbose) {
$fmt = "%s: %d: %s\n%s\n";
} else {
$fmt = "%s: %d: %s\n";
}
if ($config{"doxygen"}) {
# doxygen comments look like "/*!" or "/**"; allow them.
$hdr_comment_start = qr/^\s*\/\*[\!\*]?$/;
} else {
$hdr_comment_start = qr/^\s*\/\*$/;
}
# Note, following must be in single quotes so that \s and \w work right.
my $lint_re = qr/\/\*(?:
jsl:\w+?|ARGSUSED[0-9]*|NOTREACHED|LINTLIBRARY|VARARGS[0-9]*|
CONSTCOND|CONSTANTCOND|CONSTANTCONDITION|EMPTY|
FALLTHRU|FALLTHROUGH|LINTED.*?|PRINTFLIKE[0-9]*|
PROTOLIB[0-9]*|SCANFLIKE[0-9]*|JSSTYLED.*?
)\*\//x;
my $splint_re = qr/\/\*@.*?@\*\//x;
my $err_stat = 0; # exit status
if ($#ARGV >= 0) {
foreach my $arg (@ARGV) {
my $fh = new IO::File $arg, "r";
if (!defined($fh)) {
printf "%s: cannot open\n", $arg;
} else {
&jsstyle($arg, $fh);
close $fh;
}
}
} else {
&jsstyle("<stdin>", *STDIN);
}
exit $err_stat;
my $no_errs = 0; # set for JSSTYLED-protected lines
sub err($) {
my ($error) = @_;
unless ($no_errs) {
printf $fmt, $filename, $., $error, $line;
$err_stat = 1;
}
}
sub err_prefix($$) {
my ($prevline, $error) = @_;
my $out = $prevline."\n".$line;
unless ($no_errs) {
printf $fmt, $filename, $., $error, $out;
$err_stat = 1;
}
}
sub err_prev($) {
my ($error) = @_;
unless ($no_errs) {
printf $fmt, $filename, $. - 1, $error, $prev;
$err_stat = 1;
}
}
sub jsstyle($$) {
my ($fn, $filehandle) = @_;
$filename = $fn; # share it globally
my $in_cpp = 0;
my $next_in_cpp = 0;
my $in_comment = 0;
my $in_header_comment = 0;
my $comment_done = 0;
my $in_function = 0;
my $in_function_header = 0;
my $in_declaration = 0;
my $note_level = 0;
my $nextok = 0;
my $nocheck = 0;
my $in_string = 0;
my ($okmsg, $comment_prefix);
$line = '';
$prev = '';
reset_indent();
line: while (<$filehandle>) {
s/\r?\n$//; # strip return and newline
# save the original line, then remove all text from within
# double or single quotes, we do not want to check such text.
$line = $_;
#
# C allows strings to be continued with a backslash at the end of
# the line. We translate that into a quoted string on the previous
# line followed by an initial quote on the next line.
#
# (we assume that no-one will use backslash-continuation with character
# constants)
#
$_ = '"' . $_ if ($in_string && !$nocheck && !$in_comment);
#
# normal strings and characters
#
s/'([^\\']|\\.)*'/\'\'/g;
s/"([^\\"]|\\.)*"/\"\"/g;
#
# detect string continuation
#
if ($nocheck || $in_comment) {
$in_string = 0;
} else {
#
# Now that all full strings are replaced with "", we check
# for unfinished strings continuing onto the next line.
#
$in_string =
(s/([^"](?:"")*)"([^\\"]|\\.)*\\$/$1""/ ||
s/^("")*"([^\\"]|\\.)*\\$/""/);
}
#
# figure out if we are in a cpp directive
#
$in_cpp = $next_in_cpp || /^\s*#/; # continued or started
$next_in_cpp = $in_cpp && /\\$/; # only if continued
# strip off trailing backslashes, which appear in long macros
s/\s*\\$//;
# an /* END JSSTYLED */ comment ends a no-check block.
if ($nocheck) {
if (/\/\* *END *JSSTYLED *\*\//) {
$nocheck = 0;
} else {
reset_indent();
next line;
}
}
# a /*JSSTYLED*/ comment indicates that the next line is ok.
if ($nextok) {
if ($okmsg) {
err($okmsg);
}
$nextok = 0;
$okmsg = 0;
if (/\/\* *JSSTYLED.*\*\//) {
/^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
$okmsg = $1;
$nextok = 1;
}
$no_errs = 1;
} elsif ($no_errs) {
$no_errs = 0;
}
# check length of line.
# first, a quick check to see if there is any chance of being too long.
if ((($line =~ tr/\t/\t/) * ($tab_width - 1)) + length($line) > 80) {
# yes, there is a chance.
# replace tabs with spaces and check again.
my $eline = $line;
1 while $eline =~
s/\t+/' ' x
(length($&) * $tab_width - length($`) % $tab_width)/e;
if (length($eline) > 80) {
err("line > 80 characters");
}
}
# ignore NOTE(...) annotations (assumes NOTE is on lines by itself).
if ($note_level || /\b_?NOTE\s*\(/) { # if in NOTE or this is NOTE
s/[^()]//g; # eliminate all non-parens
$note_level += s/\(//g - length; # update paren nest level
next;
}
# a /* BEGIN JSSTYLED */ comment starts a no-check block.
if (/\/\* *BEGIN *JSSTYLED *\*\//) {
$nocheck = 1;
}
# a /*JSSTYLED*/ comment indicates that the next line is ok.
if (/\/\* *JSSTYLED.*\*\//) {
/^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
$okmsg = $1;
$nextok = 1;
}
if (/\/\/ *JSSTYLED/) {
/^.*\/\/ *JSSTYLED *(.*)$/;
$okmsg = $1;
$nextok = 1;
}
# universal checks; apply to everything
if (/\t +\t/) {
err("spaces between tabs");
}
if (/ \t+ /) {
err("tabs between spaces");
}
if (/\s$/) {
err("space or tab at end of line");
}
if (/[^ \t(]\/\*/ && !/\w\(\/\*.*\*\/\);/) {
err("comment preceded by non-blank");
}
# is this the beginning or ending of a function?
# (not if "struct foo\n{\n")
if (/^{$/ && $prev =~ /\)\s*(const\s*)?(\/\*.*\*\/\s*)?\\?$/) {
$in_function = 1;
$in_declaration = 1;
$in_function_header = 0;
$prev = $line;
next line;
}
if (/^}\s*(\/\*.*\*\/\s*)*$/) {
if ($prev =~ /^\s*return\s*;/) {
err_prev("unneeded return at end of function");
}
$in_function = 0;
reset_indent(); # we don't check between functions
$prev = $line;
next line;
}
if (/^\w*\($/) {
$in_function_header = 1;
}
# a blank line terminates the declarations within a function.
# XXX - but still a problem in sub-blocks.
if ($in_declaration && /^$/) {
$in_declaration = 0;
}
if ($comment_done) {
$in_comment = 0;
$in_header_comment = 0;
$comment_done = 0;
}
# does this looks like the start of a block comment?
if (/$hdr_comment_start/) {
if ($config{"indent"} eq "tab") {
if (!/^\t*\/\*/) {
err("block comment not indented by tabs");
}
} elsif (!/^ *\/\*/) {
err("block comment not indented by spaces");
}
$in_comment = 1;
/^(\s*)\//;
$comment_prefix = $1;
if ($comment_prefix eq "") {
$in_header_comment = 1;
}
$prev = $line;
next line;
}
# are we still in the block comment?
if ($in_comment) {
if (/^$comment_prefix \*\/$/) {
$comment_done = 1;
} elsif (/\*\//) {
$comment_done = 1;
err("improper block comment close")
unless ($ignore_hdr_comment && $in_header_comment);
} elsif (!/^$comment_prefix \*[ \t]/ &&
!/^$comment_prefix \*$/) {
err("improper block comment")
unless ($ignore_hdr_comment && $in_header_comment);
}
}
if ($in_header_comment && $ignore_hdr_comment) {
$prev = $line;
next line;
}
# check for errors that might occur in comments and in code.
# allow spaces to be used to draw pictures in header comments.
#if (/[^ ] / && !/".* .*"/ && !$in_header_comment) {
# err("spaces instead of tabs");
#}
#if (/^ / && !/^ \*[ \t\/]/ && !/^ \*$/ &&
# (!/^ \w/ || $in_function != 0)) {
# err("indent by spaces instead of tabs");
#}
if ($config{"indent"} eq "tab") {
if (/^ {2,}/ && !/^ [^ ]/) {
err("indent by spaces instead of tabs");
}
} elsif (/^\t/) {
err("indent by tabs instead of spaces")
} elsif (/^( +)/ && !$in_comment) {
my $indent = $1;
if (length($indent) < $config{"indent"}) {
err("indent of " . length($indent) .
" space(s) instead of " . $config{"indent"});
} elsif ($config{"strict-indent"} &&
length($indent) % $config{"indent"} != 0) {
err("indent is " . length($indent) .
" not a multiple of " . $config{'indent'} . " spaces");
}
}
if (/^\t+ [^ \t\*]/ || /^\t+ \S/ || /^\t+ \S/) {
err("continuation line not indented by 4 spaces");
}
# A multi-line block comment must not have content on the first line.
if (/^\s*\/\*./ && !/^\s*\/\*.*\*\// && !/$hdr_comment_start/) {
err("improper first line of block comment");
}
if ($in_comment) { # still in comment, don't do further checks
$prev = $line;
next line;
}
if ((/[^(]\/\*\S/ || /^\/\*\S/) &&
!(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
err("missing blank after open comment");
}
if (/\S\*\/[^)]|\S\*\/$/ &&
!(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
err("missing blank before close comment");
}
if ($config{"blank-after-start-comment"} && /(?<!\w:)\/\/\S/) { # C++ comments
err("missing blank after start comment");
}
# check for unterminated single line comments, but allow them when
# they are used to comment out the argument list of a function
# declaration.
if (/\S.*\/\*/ && !/\S.*\/\*.*\*\// && !/\(\/\*/) {
err("unterminated single line comment");
}
if (/^(#else|#endif|#include)(.*)$/) {
$prev = $line;
next line;
}
#
# delete any comments and check everything else. Note that
# ".*?" is a non-greedy match, so that we don't get confused by
# multiple comments on the same line.
#
s/\/\*.*?\*\///g;
s/\/\/.*$//; # C++ comments
# delete any trailing whitespace; we have already checked for that.
s/\s*$//;
# following checks do not apply to text in comments.
my $quote = $config{"literal-string-quote"};
if ($quote eq "single") {
if (/"/) {
err("literal string using double-quote instead of single");
}
} elsif ($quote eq "double") {
if (/'/) {
err("literal string using single-quote instead of double");
}
}
if (/[^=!<>\s][!<>=]=/ || /[^<>!=][!<>=]==?[^\s,=]/ ||
(/[^->]>[^,=>\s]/ && !/[^->]>$/) ||
(/[^<]<[^,=<\s]/ && !/[^<]<$/) ||
/[^<\s]<[^<]/ || /[^->\s]>[^>]/) {
err("missing space around relational operator");
}
if (/\S>>=/ || /\S<<=/ || />>=\S/ || /<<=\S/ || /\S[-+*\/&|^%]=/ ||
(/[^-+*\/&|^%!<>=\s]=[^=]/ && !/[^-+*\/&|^%!<>=\s]=$/) ||
(/[^!<>=]=[^=\s]/ && !/[^!<>=]=$/)) {
# XXX - should only check this for C++ code
# XXX - there are probably other forms that should be allowed
if (!/\soperator=/) {
err("missing space around assignment operator");
}
}
if (/[,;]\S/ && !/\bfor \(;;\)/ &&
# Allow a comma in a regex quantifier.
!/\/.*?\{\d+,?\d*\}.*?\//) {
err("comma or semicolon followed by non-blank");
}
# check for commas preceded by blanks
if ((!$config{"leading-comma-ok"} && /^\s*,/) || (!/^\s*,/ && /\s,/)) {
err("comma preceded by blank");
}
# check for semicolons preceded by blanks
# allow "for" statements to have empty "while" clauses
if (/\s;/ && !/^[\t]+;$/ && !/^\s*for \([^;]*; ;[^;]*\)/) {
err("semicolon preceded by blank");
}
if (!$config{"continuation-at-front"} && /^\s*(&&|\|\|)/) {
err("improper boolean continuation");
} elsif ($config{"continuation-at-front"} && /(&&|\|\||\+)$/) {
err("improper continuation");
}
if (/\S *(&&|\|\|)/ || /(&&|\|\|) *\S/) {
err("more than one space around boolean operator");
}
# We allow methods which look like obj.delete() but not keywords without
# spaces ala: delete(obj)
if (/(?<!\.)\b(delete|typeof|instanceof|throw|with|catch|new|function|in|for|if|while|switch|return|case)\(/) {
err("missing space between keyword and paren");
}
if (/(\b(catch|for|if|with|while|switch|return)\b.*){2,}/) {
# multiple "case" and "sizeof" allowed
err("more than one keyword on line");
}
if (/\b(delete|typeof|instanceOf|with|throw|catch|new|function|in|for|if|while|switch|return|case)\s\s+\(/ &&
!/^#if\s+\(/) {
err("extra space between keyword and paren");
}
# try to detect "func (x)" but not "if (x)" or
# "#define foo (x)" or "int (*func)();"
if (/\w\s\(/) {
my $s = $_;
# strip off all keywords on the line
s/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\s\(/XXX(/g;
s/#elif\s\(/XXX(/g;
s/^#define\s+\w+\s+\(/XXX(/;
# do not match things like "void (*f)();"
# or "typedef void (func_t)();"
s/\w\s\(+\*/XXX(*/g;
s/\b(void)\s+\(+/XXX(/og;
if (/\w\s\(/) {
err("extra space between function name and left paren");
}
$_ = $s;
}
if ($config{"unparenthesized-return"} &&
/^\s*return\W[^;]*;/ && !/^\s*return\s*\(.*\);/) {
err("unparenthesized return expression");
}
if (/\btypeof\b/ && !/\btypeof\s*\(.*\)/) {
err("unparenthesized typeof expression");
}
if (!$config{"whitespace-after-left-paren-ok"} && /\(\s/) {
err("whitespace after left paren");
}
# allow "for" statements to have empty "continue" clauses
if (/\s\)/ && !/^\s*for \([^;]*;[^;]*; \)/) {
if ($config{"leading-right-paren-ok"} && /^\s+\)/) {
# this is allowed
} else {
err("whitespace before right paren");
}
}
if (/^\s*\(void\)[^ ]/) {
err("missing space after (void) cast");
}
if (/\S{/ && !/({|\(){/ &&
# Allow a brace in a regex quantifier.
!/\/.*?\{\d+,?\d*\}.*?\//) {
err("missing space before left brace");
}
if ($in_function && /^\s+{/ &&
($prev =~ /\)\s*$/ || $prev =~ /\bstruct\s+\w+$/)) {
err("left brace starting a line");
}
if (/}(else|while)/) {
err("missing space after right brace");
}
if (/}\s\s+(else|while)/) {
err("extra space after right brace");
}
if (/^\s+#/) {
err("preprocessor statement not in column 1");
}
if (/^#\s/) {
err("blank after preprocessor #");
}
#
# We completely ignore, for purposes of indentation:
# * lines outside of functions
# * preprocessor lines
#
if ($check_continuation && $in_function && !$in_cpp) {
process_indent($_);
}
if (/^\s*else\W/) {
if (!$config{"uncuddled-else-ok"} && $prev =~ /^\s*}$/) {
err_prefix($prev,
"else and right brace should be on same line");
}
}
$prev = $line;
}
if ($prev eq "") {
err("last line in file is blank");
}
}
#
# Continuation-line checking
#
# The rest of this file contains the code for the continuation checking
# engine. It's a pretty simple state machine which tracks the expression
# depth (unmatched '('s and '['s).
#
# Keep in mind that the argument to process_indent() has already been heavily
# processed; all comments have been replaced by control-A, and the contents of
# strings and character constants have been elided.
#
my $cont_in; # currently inside of a continuation
my $cont_off; # skipping an initializer or definition
my $cont_noerr; # suppress cascading errors
my $cont_start; # the line being continued
my $cont_base; # the base indentation
my $cont_first; # this is the first line of a statement
my $cont_multiseg; # this continuation has multiple segments
my $cont_special; # this is a C statement (if, for, etc.)
my $cont_macro; # this is a macro
my $cont_case; # this is a multi-line case
my @cont_paren; # the stack of unmatched ( and [s we've seen
sub
reset_indent()
{
$cont_in = 0;
$cont_off = 0;
}
sub
delabel($)
{
#
# replace labels with tabs. Note that there may be multiple
# labels on a line.
#
local $_ = $_[0];
while (/^(\t*)( *(?:(?:\w+\s*)|(?:case\b[^:]*)): *)(.*)$/) {
my ($pre_tabs, $label, $rest) = ($1, $2, $3);
$_ = $pre_tabs;
while ($label =~ s/^([^\t]*)(\t+)//) {
$_ .= "\t" x (length($2) + length($1) / 8);
}
$_ .= ("\t" x (length($label) / 8)).$rest;
}
return ($_);
}
sub
process_indent($)
{
require strict;
local $_ = $_[0]; # preserve the global $_
s///g; # No comments
s/\s+$//; # Strip trailing whitespace
return if (/^$/); # skip empty lines
# regexps used below; keywords taking (), macros, and continued cases
my $special = '(?:(?:\}\s*)?else\s+)?(?:if|for|while|switch)\b';
my $macro = '[A-Z_][A-Z_0-9]*\(';
my $case = 'case\b[^:]*$';
# skip over enumerations, array definitions, initializers, etc.
if ($cont_off <= 0 && !/^\s*$special/ &&
(/(?:(?:\b(?:enum|struct|union)\s*[^\{]*)|(?:\s+=\s*)){/ ||
(/^\s*{/ && $prev =~ /=\s*(?:\/\*.*\*\/\s*)*$/))) {
$cont_in = 0;
$cont_off = tr/{/{/ - tr/}/}/;
return;
}
if ($cont_off) {
$cont_off += tr/{/{/ - tr/}/}/;
return;
}
if (!$cont_in) {
$cont_start = $line;
if (/^\t* /) {
err("non-continuation indented 4 spaces");
$cont_noerr = 1; # stop reporting
}
$_ = delabel($_); # replace labels with tabs
# check if the statement is complete
return if (/^\s*\}?$/);
return if (/^\s*\}?\s*else\s*\{?$/);
return if (/^\s*do\s*\{?$/);
return if (/{$/);
return if (/}[,;]?$/);
# Allow macros on their own lines
return if (/^\s*[A-Z_][A-Z_0-9]*$/);
# cases we don't deal with, generally non-kosher
if (/{/) {
err("stuff after {");
return;
}
# Get the base line, and set up the state machine
/^(\t*)/;
$cont_base = $1;
$cont_in = 1;
@cont_paren = ();
$cont_first = 1;
$cont_multiseg = 0;
# certain things need special processing
$cont_special = /^\s*$special/? 1 : 0;
$cont_macro = /^\s*$macro/? 1 : 0;
$cont_case = /^\s*$case/? 1 : 0;
} else {
$cont_first = 0;
# Strings may be pulled back to an earlier (half-)tabstop
unless ($cont_noerr || /^$cont_base / ||
(/^\t*(?: )?(?:gettext\()?\"/ && !/^$cont_base\t/)) {
err_prefix($cont_start,
"continuation should be indented 4 spaces");
}
}
my $rest = $_; # keeps the remainder of the line
#
# The split matches 0 characters, so that each 'special' character
# is processed separately. Parens and brackets are pushed and
# popped off the @cont_paren stack. For normal processing, we wait
# until a ; or { terminates the statement. "special" processing
# (if/for/while/switch) is allowed to stop when the stack empties,
# as is macro processing. Case statements are terminated with a :
# and an empty paren stack.
#
foreach $_ (split /[^\(\)\[\]\{\}\;\:]*/) {
next if (length($_) == 0);
# rest contains the remainder of the line
my $rxp = "[^\Q$_\E]*\Q$_\E";
$rest =~ s/^$rxp//;
if (/\(/ || /\[/) {
push @cont_paren, $_;
} elsif (/\)/ || /\]/) {
my $cur = $_;
tr/\)\]/\(\[/;
my $old = (pop @cont_paren);
if (!defined($old)) {
err("unexpected '$cur'");
$cont_in = 0;
last;
} elsif ($old ne $_) {
err("'$cur' mismatched with '$old'");
$cont_in = 0;
last;
}
#
# If the stack is now empty, do special processing
# for if/for/while/switch and macro statements.
#
next if (@cont_paren != 0);
if ($cont_special) {
if ($rest =~ /^\s*{?$/) {
$cont_in = 0;
last;
}
if ($rest =~ /^\s*;$/) {
err("empty if/for/while body ".
"not on its own line");
$cont_in = 0;
last;
}
if (!$cont_first && $cont_multiseg == 1) {
err_prefix($cont_start,
"multiple statements continued ".
"over multiple lines");
$cont_multiseg = 2;
} elsif ($cont_multiseg == 0) {
$cont_multiseg = 1;
}
# We've finished this section, start
# processing the next.
goto section_ended;
}
if ($cont_macro) {
if ($rest =~ /^$/) {
$cont_in = 0;
last;
}
}
} elsif (/\;/) {
if ($cont_case) {
err("unexpected ;");
} elsif (!$cont_special) {
err("unexpected ;") if (@cont_paren != 0);
if (!$cont_first && $cont_multiseg == 1) {
err_prefix($cont_start,
"multiple statements continued ".
"over multiple lines");
$cont_multiseg = 2;
} elsif ($cont_multiseg == 0) {
$cont_multiseg = 1;
}
if ($rest =~ /^$/) {
$cont_in = 0;
last;
}
if ($rest =~ /^\s*special/) {
err("if/for/while/switch not started ".
"on its own line");
}
goto section_ended;
}
} elsif (/\{/) {
err("{ while in parens/brackets") if (@cont_paren != 0);
err("stuff after {") if ($rest =~ /[^\s}]/);
$cont_in = 0;
last;
} elsif (/\}/) {
err("} while in parens/brackets") if (@cont_paren != 0);
if (!$cont_special && $rest !~ /^\s*(while|else)\b/) {
if ($rest =~ /^$/) {
err("unexpected }");
} else {
err("stuff after }");
}
$cont_in = 0;
last;
}
} elsif (/\:/ && $cont_case && @cont_paren == 0) {
err("stuff after multi-line case") if ($rest !~ /$^/);
$cont_in = 0;
last;
}
next;
section_ended:
# End of a statement or if/while/for loop. Reset
# cont_special and cont_macro based on the rest of the
# line.
$cont_special = ($rest =~ /^\s*$special/)? 1 : 0;
$cont_macro = ($rest =~ /^\s*$macro/)? 1 : 0;
$cont_case = 0;
next;
}
$cont_noerr = 0 if (!$cont_in);
}
�PNG

IHDR~�%� IDATx��yxTE���v��NBv1���!�����a܀qFpˆ3�8.�� �g�#*2,��, @H ���$�=���N/��;+ݷ;MX�z��G�ϭS眪�[9�n�jٲ�S���B� X�R F"�H$�D�F@�J�
�BQ�Q���@�P�¼ TU*v� �H$�D"�\&B� �G�Tj�Bh��R�B�J�!����vJ$�D"�H.�[��
��B��U*UB��T����P�D"�H$�e�R�V�J�eB�5B�@#D�F�R�"����D"�H$��C�J��"D�
�G=��Ӫ�m�D"�H$�����D|||�j�����j�j�������F�A�V�R�P�TX,�f3f��u��a�1��:���#�D"�H$���Ì�s�I���J��*z�z���>W�2��1%�fc
h�@~7�>����%�et�'gӺW ��ږ+����J�q#��u��@j�EZ��&J}��i.5��\�uH(�W���QG���[����NfFǧ��G� �m�W����U�f��z �Ӧ�����i�eˊe��q
�^#y⹩�U?a�-k���J���{�:�pQ�Y���'�ePN/��<�h��Ŀ��x��|���Y�F����a,/=u�
��ˎ�fٿ�t��N��a�s��#ԍޤ�{� >Y��M�s��tx� FŸڹ�L򆥬ڜ��W�Gxn�D�^���:ƻq\�ٳj ���@Ou��� ����]�g��բ5i;d*���Y��ɧ(�X̚�F����
tNg��;]�t��Uݷ���d|�E��Q�����Z���h:�^g֑�0#`�o�q�
Kn>�V��.��SWOU��t�穱78f���'��b|�W�%�E�ۿg�.�p_�Si)�<�-M�1�7���t��� ��u\ ��Xٓy� mna|pӾ�$�ʊl�g��#ԋ#=B���-���GO���n����:�C9�W��.���W-���RC2 �1�D��G�������Pª H��L̸Q�y��7O#��<��� �� f��/�=���DQ�
��� �̈ �Y��RFO���s�={6�U���q���)Liӗ)�-���W�g�{d_���� �ÔC!<4i]�+ۚ��/y��=�{Һ�`��Յ�3���l�pYv*�T� &N~�2o���T���󹥔��RZ]���f�ܹ�[�o�X�Z��?�����6��ZRA��u��tM.��Q���:� FJזSY Pc�ta'��ࢽ|ŷ俘k�V�>/�4�xY��ؘHy�u��(=�б�����Jn���4��d��+��7�U������
|�]�\c�7���r�Z��d�ʰ6|��W8��R��c�����[�
�X_q�/_����W.[�|˲��.[�<O8�B,�1���I^�Q�m᎗��uBT�(�ٙ�_
@<�e��{D������C�?�N%���%�"��VV�2��b:,T��GM��)�~���� V�X�g�z1)f��p��|���T�y��6���X���u6%mioA�E��v�CC��}�����t��X��`臢��D��X�Hq�E?R�"�_�YZ�XI��"�ֳ�F!L�"5n�H�zX$������ Er�>�u�j+�tL$�ŕB�OIq�DR�>��dB��q�"yyE]�QcL������IcEll�H��w��X�B�s��//Y%�M�KLz�b��������6��0E,���&�U����݈T1o�?����/'�-bcK6�&g&�a���u�M��9�I���?��ju�X=�:[��9�E/�����4i���0YL�:ULJ�,6׎��zϭ�X����F�Si)�.g��x;T� *�ԕ��y���t}9g�v�~�:S�_2K$ &z
@,9T��N��)�T��.n�XB�ub��#"n�1<9Ul.5�
������
���".񀘜�#�,�dB��KĒc�u:�U��SSuQ���GR� ǎ��'
lcšf�mz����"����C)bڑ�b��YW� ���L㎤�����ou Ɖ�L�;�&�牗%���Œ���U��i�뜗l�9?�ܭ�9���P�K�'����4թ��.0�����Ÿ��bܡ�B��O,)�BXı�SbZ�a����|vp��w�^q��Aq��1���!rrrą DYY�����A�L&a6���b555B�� �N'>��c�l�
�˖�-[��в�˷|�l٪�nF��B���b o�صX��.ޘ�w�˜���_�w�I|���ψs�S ��R�q����߲廟�,�\9�j1��F���;�m��Q�y�]�SCay}����5�I�e�r��ݯ��`�l9p�U�&p��=PQg-��ʢ#l�tnm���v*�H��>�m '�"==��*�zS?z��6��O��%�� ��Ъ���=Lax�l@��R�/�ىVX)�Sՠ4XMV�L�!�F�LV���h+%o`�뒔�R9FO�7?��<��Ý��;�T買���i���n���^���Y�- �y������?��c�j���&�"�?�;#��K��y����H���J�m�Lގ��gcX��"n)\���L�[��U�va�\?��x1�Zm$!v;�P�Ň���=z�^Щ�`�2��F�.��}�� �|�����*�#��c労�ࣧ�|�gy����R����KvaŒL�͜�CÇ7�M�NE��t*�i��c�����L+���h�.�P�Qv,TU1)�$ %j^�
%�2���T*�,�<�r��.jx�}C�dR�>�.t*Q��)gx����.�f�dE���A��u��H ��?��!�  ���� @E��0&��+�X���-&���9�CiPO�����a�+;��י�ۆ�s��\�L1�|R� >m��Tjw%�Yz�8�kH��� A ���I-��? ����qLH?I��U��-���G��B�y}�B��O�*�o�@�'���s"��wR�]� ��L�B�t������رc����"-��e}���m߁�6���#�v� �t^�նL@�_�Mbz��y�m�&.�7)��#1��i�ט
ľ�Ev���+W���������4��B!t{�0U�rߜw�Ng8өQ!>��ȇ��D��h:�Fц��� Q�b�Hz�X�SN���}"������"ib�0 !~y�g�41OXk��YaB��BqԞի���� !��a�B��%�����L�Ȟ�8�納��d�:o���g���b�dU��I^��y擻D�wR�2T�KkK�/���a�=��Md�ߵ�+w�QS[j�e/��n��c&!�3�e�d��g�L�b��=���!Lb孈%G�������*m�B�TGNe���i�(�S�u���m\ʼn�)v�S[��y}J�)�t��f��j�Y�M�'>+�g^,ebf�>1�\��(VJ�ψ��^��WܚV�(+,8.�����N���O ?U�B�5�bx�>��/6�L姅Ob�8fq�BX���bI��쒒�^>��A�ܞI�����<<q�XYjV�S�Z����*�r�>1.�Bx�ڌ_֥:]��1���B�x@l�M_Z�Ÿ��_=&��sE\m\��y�빮3~T��B��l��6_Ϲ�N�Έ�3�߇=CO� _�N��3ӯ_?bcc��׏ �f���[#�^h����Tfz���O(꼢�9"�~���L�NV�{+ư�ܯC�5��8|0�����R��B��������pxܽ���9� @�@6��R������yL;6d�ܳ��t*�c+�!�@i�_!u�8>?V�I����I0�CO��چ�A d�Q���� ��+Uh� @m.��j4w�b6���@Y���|�;������`�U� ���h�G�����gr�FN1���T*��U�ݜ�1�h2c2���/��5�gɰ~A�t����Ƿ)��[��-c���:�OL�>�Av��4��ۂ_�����́��,��_���րIo[�9���m�*�޷…�K[@�0�����-a�x�?��d�����4��d���i%��S��B�І���ܫϕ�t��@�NJ�l�����ݷ��|��O��08$�(������-�����*,�������->��ڏJ:��� ,�;ǁ�b�)�*�i���m��ju�XIV/o��\��
�V������C쟼� A�joc�z��T��t3%���^�~��$g�w�~�'��y�j�������&~�)���w/�\���ޒm�g K���jJ? �ͯA����,���U��Nt�}�>��K�����~���m�.��� fE����}��������K��x1vYaY��W%[ �?�5�������y��IPa�Ogu��JxTW��7���l��=;�%�I?C�%is�:��Ұ|��n�Bo���o���d�O]�G�����b
ת��>'
)�`� 0/�0|�#w&��j�J�n�M�
�t %�ބ����j b�h�z1�+�<� /m������E����&�(�ð�O4���|��Mg�u���o����a����8GJ���)T�xy�+���c�/�~�M����31�t�����t���ͥ-`���_t�\>� ���4�/�9�J���sgL7�r��%��)�Z��LF o��*ù�ln�����S��N���c�~^��6�n�GA|?�n���.�Ɔ�K�7���hP�+�gBg�B��"�H 0FUɜ��8�Nr�-���Z���x¡$��7ف� |��KJv6 ����X�!l:���Z���P���Z�>�;��k��>C|g5)��+K�L��g�38���0��r`7;v� 9�"�%����[�?�����'x�O��Б~���{9]%Y9p��mݝ�N%�� �1��^0��5d���M&n��n��
��T��~����lX�~�O������` R����������}���|���fmn
T� :2�3���'?����O"�ۺ�r���=@���xhe��:]�%���|2S�1g�C���[ ���Lz|3��w����ck��k-[���F��7�z�k�mRW������6n�!��=1�`���{M��������m���MX�K�u�F�1T�pS�?�y����`~r�ȮC&��y��.�g�Ge�Qxn��~�-��#��B�������ԄAx۳:�]���yl�L��:�Ν���fֳ5��p ���d���]i�fW�|�\�H��Ҥ�R��q�L�z�8�J��J�Vj?��.�W|��(٩�� ��S�OI�;}��c%0(����Yy�1 &�0��o�%΋;�u �KU�ԓ��6�ٓ��,���#�����NǗ@Ϡּݎ'#B�6���/x/7�T��̲2���Pf!����2
��*ٕ��H�)�� �BG�Q�wi� �Ԯ~�w�cm�_eUTPd�b˩6�Q����~Z?T�"���\�=y��w�&7i4T�x�x&{�*Pj�+M������p@���
��j�^&N,�n_/S�\�rJ:�pZ������kw�}��[p��W���Fko�|o(�'�o���x���n�������/6�k���D�+��="���1ψ�i������-3=�yI�h��*�����<�Me���J���վ�����5��T��O��X���'R�6^gU�7���nQ�8�Od�Ԯ�������
�k��o !�w�Iq�����_#�ݝS!ֿp{]<�N�X}��8�����N��H`�8l"m��Ǿ&4m��b9�K���M���L[D�u}666���5[�M�-<hkC7l)=ָ�m��^���`��K�Tꟊ�HA��XqF����rNc��~�$v/}�N����El��E}N�Sҩh���!
�����}Bk�瓘,�!�Q�NJ��3k��L��ܴ:}��}b^�ۤ��b(��: ����;Ӿ&͹6JK�6�7\��Pf_�m�o~Y�ڵ��]��s��ܨ��b��څt��R!�''9��:f���ݧו���&���ե����oܑ����n��\�5~��˗�* �"D=��3N��:���LL�m�~8f ��h��k��(�g���o�s���������Q
�����V����NG:�f�jx���A� �-��rm1�Z�w��8��F�p;*Pjw�2:��{33����`�vdK� �|,�4�jzh.0Gso?�-��F��m0�G~ f4_40go���V����.��N粟)�#c�3Z�\�,�J}�l�a�y���?W:��'c�`�*|]d}��J�ɂ���e�_z�an��pt����fξ����>.�~�(�`�`hTj���d-o�%X��CR&wt�gV �0�k�o�j�O <iw�ńY卯�"f� 3j}�c�g�
!
TP��|!D�G}pƻ�S�^K�?���.�\@�o�ռ�*�w%l�������`��%3��h|�z~FR�I������!�{�]u�Ҵ#q����'72�o��7 �-ܯ��e0�g��c\�p�.�} �{8�ѵ��6�����Lz9���Li�c�����
g�V�KW���ҩ�'<+�Ӊ��x��2�ȈV�Te<{�$ >��*a���B�E��u*V�����`�s�*��FS���o���F�|2�$sǞ+E3Ǩ7��Z��b��<�W��@�@p���V�dTn�'�]W��E���l��8ٿ�Bd�W���} ��`�c � �D���)� �MwΪ��ZZENM 7D��s$���� _E<�S��Kam��J�~�hfd������u�&j�g5}�Jr=/����fH$�=Q�ofڕ�E��N�@��+]��+�'�H$�D"��ȉ�D"�H$� �U���<Ϊ��^I�R�K�w��k�󷎌YjH/)s�?Vs1T���l�\_*�H$��8Wu�y�(C
�\_x�Xٓ�Ɔ
�w�ʊl�gd0&���j�:�Ș5�TΣ�O���e�96 +;�
�礜�I$ɍ��{mƐ���j��ʹ6W�m`AJa9�0�����HV���rs�- �:�Ș5�KE�n�a؁�.��ɪ���v���D"�H~���ޡ@`��wp'��z>�W3��GN.�U��Dw?�nR2��>�b"i���_�x+#����^j$><�� �,KK�s��/*��w��5`)��sX�k�8-��/�5��/țNc^n!ߛ-וp���/�
PF�]keK�Q�VP�NW���$�:}�����T���G ���20��>�̆b����s�$�����6�� �lFޠ�5�ҙ�s�՟A����-i'H��s�Ng�Xa��6��K%��Jg�����\߶&����bV��周S1:<�B\��O��/:s��/��6�<ƥ���ך8�n�\�t�ϮP���'�l�+���BJn3���������ѣ�<.0��m�Sm�D"�����E�V��h�6l�ݴY�V��兗�W��B�V+V��'N`�X�TP��J���zͤ�T����?
&��$՝�HVkKU�ݎ�&����erT���im���X�y.�4/]��J�(�Rɤ�#|^)��rƜΡ4(�������ɋ��a�
$���Ȑ`�����@E��0&��+�XM�㻝��&v�3ڝ��:���3�d�ga�t3W��JO�Vx�Ur}}ɾmx*@07/�3 UULJ?IB����BI���o9���U�ǙV��+��$x]����0X�p�8�*5<��7�s��=c�]u% � ٩iEji#��1�~��k3����
��E}��'����Ӷ�Nۨ5�uG��$�׼{�D"�H.�f$�|x��~<��04��Gϓ���b�ia����������
���^�)��6Q�/y|����0X DS���*��1�����ʎ�`��ˤL�TY�ՁX���M��t�?��+�z�N ���Ӯ�\x5����;��wex5w{W�ϓU<u�?E�dU�`�-��RY���}� ��᜻� Q�Oi�4�1T3ìbu�>�VA[-Y�Z�� IDATYw�"ã�)�Q!4�� ��֋Q�p�g��-z�1#Iǔ[�p��;��k3G:;�Y`H�Ǻ�(_�_W�G�de�`�M��(��� J8�3q�|�+�H$7$���YI�>����O���F��eIsu5{�_��V�ȰK��VVaQ���&����#(������jd�#`pj�M�e�� �\�j-��¼s(+fљb�Ÿ֗��`���Mt�B+  �� ��}r�PLf[�퉓�ݷ��|�,�u����"⒓�=p�ϋ/:��6.&���amU[�Cð�ޘ�Mcy�b�L��~�Rg3c�N��\�S]��E���Q��H$ɍ��?}>w�W���=������gݫ�ϏAX9~��p��7A��������<���3�3���V�VNGc��/�zyc�h��y�#�����~��Ĺ��)��cT��9]���$��R7Q�Ŕ�j:� #ܣ ��
T���R���`h�wk���8_A���6�CC;��h�+J�p�4Od��#�6��Z�ƥAU&+\�c�1SN��[����'l:�z�F �n���D"��X�=�0�l���4*�E�q<���V� �j���f��`aN����W�]N�������'ϓmг'3�YVOG������rsH���,+#�d[{�_YIvU�@A����J��yyq3����� <��CZ��3���/��A�y%�OF��mn��*��1�d�f�Y�o�A@V��2���2�xx�]p��Q&+0(����Yy�1 &�0��~)$�ʀ�0w�RU&�qQ��Ŭ�\N?�$֊���Y��r�MgT��mԀJ]?�� �W"�HnT�����gM@)��R�����A^��[16F�e2�] =�p><���S<�q!�8t��D�w�H{=q���r�Wn�ɴP5Xl7]m���=��2����.[pK?������L�@��0��ҏY!
:ա<�֗!�g�
��*�Mf)�H��+��E�|�i����Q1,�����32��ѹ�8��@�ÂS|X�ae�8F���9j�N��˻-ݢ���-��Z��J��䝬�v��k�~�����X�}�������3���(�3xk��Z~�fn�i:=m�Z |~��ׁX9�H$�����_ D���}��O��f�����R��2�
�‘^M���ł��V*M�=��Ċ�"Ш���b��Ǿ; =�0��i8:�VzxT3g�Q��F�+����Qȟ�bX�bb���n��j���� ۛ�M}��h��Kڡ%㢄�]��g�k��l��W�x��Ie�?J��j�� d|�D"�������Z�???�Z-Z����]�ju�v.��ٌ�lfݺukLBQ��T� !
�}�Ԩ=���p>�VJ�L^�m�����ĎnV6���tBᅃ%����h�JUƳGO��ù�V��) ��q��ł��;�;�>+��P�ޜ�g� ��ӲqQ�U�.�f��ˎ�R}��mTV�A��j��[N�$��������Α�l�����?�m�sV����*rjj�) �m�#l[�ճs<E��6'���奰��huU����*fW�_Q�[��2�5�����Dr�s��Z=���q�+�N�j}3�Z;�i���?]�+�Ǻ��o�J1���b��m�����
�D"�\\�EY�D"�H$��9�H$�D"�A�?�D"�H$���xe����_r��vW_#U5����_���},�l�Ҿ�����R�-M�� �t�0#z�د�vcy`@[0�}��]@���w?:x��v�!���+�L�jj �{ �e$�D"��3~��LRR!x{Z|�%i)L��v�ss�p�F��L�Na��(��vO�O��Y=� 6�̥ClO8��� ^b{��2th����{��7[>�=vM"�H$��6�q��F�ȉLy�]�o �Z�
�] �%ob����F��_�!\�$K�)6|���gK� ��ȱ�2"�� ��Z���Y�h=&���07=������L!������Ly&�H%[�gX��gz��Jʺ�I��c�Ɍ��� �����?gDZ||û0j�$�t Q�eY_,ag��"T�ٻ\��XidП_bH{דmGm���ʩ��ؖ:��c_�����]kIB�#s�dH�
n���a��R�To�tFx���4��[J��i<�����D"�\U�㌟���B�F=:�����_ %�O�a�|>�����Std#���,3���W��l�MH���a���Ξir��F����*�s�����n VO~`F�����Bʍ�����3X��3�zgp�*v��?N�q!+g�k��M�7�=�(q�S�]4��R%[���O||w�����ML�xں�G��62� ���U�O�����._[|>��������Pwd�0?H?�Eu�a6cLfcS�D"�H$7<�uJ�[+��Ǽx���P�Y�1��V@�H�� �w�v�WaNe�Gws�0��>��A��]�y���{�Q�D)��i��<�Cx7w"4����B�?��O�0b�MT��ݹ-�]���9�8�=#98syFb:8��⻛m�jf=LjN�J�s ٓ\����Nm�Αt‚��0�u#��)s�F]4Qݖ��&����{��Z �;^�_�n�g��~�O�tJ$�D"q��z�g2����O�F3�!���7�����=�wĜU ����Ne����{z��8�a!W:��;���X�Tq�{_"�p.]�#гkۺ뵀�lV�Y z6/x��uRE[l0&���t gm�0� j?�1_�y�O �P�n���y��Pۜfs���H$�D���� $8� �?_�4�|P��S2��@Tg)�`@_�hx��N@W��UX�9��P�;��-f3��_��ґ�l`�� mm�4_��S[j�ʫ-eJ(��_F<|7�,��^4Dt��C���~Ȣ��r��J <�NE�t6�d�D"�HnH��5~�Y�d�f���AF�y� �%�r�d��RY�z'�E�l\�N�w��΂]K���l�}������3�̑5�]�9��Β�}F�li.��}��޴���0_�B^f���-u�
�l�"r32(�[ݪW���{�f�o��� RY�j�E$�[��r5�{hp�<��:%�D"�����yZ!s ��b��~��7���|g2u�Hf=r�k�1/��݀�/�@7-`V� �����n�Z1v�l��/e���ǯ��]C�T��hKtyk��i�٩̿/ӧ�˻K��`�V�4�� ���ܖZ[G>1����a��:�=��J�Cgm�l�F�pϣٹ: 4hۏb�#��`�F�:��c�#/׷�:�4>�D"�Hl��/_���H
�D���D]kî,�z#Z�f=��������b4bF�V۴F�mQ�h4���l��c4��h��<�ny,��f����]"�H$���������j���C�բ�j�V��hP�ըT*T*��ٌ�lfݺukLBQ��T� !
nд�ח9*�������ɍ�(�U�ӹ-6{O�j���O"�H$���u��O"�H$�D�2ȉ�D"�H$� ���I$�D"�� ȉ�D"�H$� ���I�O,d��Qt���Mݸ���W�R�\$+-�ݍ�W��`7|� ����D"� s���� R6~Bi߉�������)��HE�N�F�jZ3�� ���E��D9��Y�b�;����WŢ�]�b[&�tk m��_�[Y�t�� V>����J��H�w��9�\�=���^�
w�Ɇ4|jϽ��m��7 ,صa#{�.�����GL��{�a�m�zLdҐ(�d���'��ƿob��<i�D"��,r��6&No;�)�O����u�g�ʻ0`��`ܻ�_�����n� ��nD��cN�^�ͤ��sx &�^^�v��*���lK��ym���G׸�����&�*/��d*38�k""h(��z�񍿳�B8���FΎX1�,�Φ���C�3l��*��� $�{2���^��W|��;����K$���ȉ_2v�c��S�/�ދa&0��/�k��3�L*��].�`�42��/1���ɓ1���Ύc��waԄI �صz��K���9�Ǎ�ɿ�C�ZAf>��e?�{LWR�}Mz��Mf�ΨK�)6|���gK� ��ȱ�2"�u�9�#'2��Ɠ���/Y��<��� C���<�����N���= YI[����+k�Ȕg�T+�g�����r�ȟ~�Mf҈(�k�~* #2 ������9�ϨW�~��j1��F�o�v.t:��2��k�&-h�h�@��A:V���O�$1=� /N���;c�v.�F�ɳx >��m1�|FY�?�d��G���sF�}�Y��U9<xO'�Y_�킆�9�3��7 ���&���##��g����@�O���,���k���������%�r2�Σ%�H$�k��0�y(�.�������EK�0��9������=��ݛ�~�m�j�\���^�$o�{�QⴧX�hI�JN�`â�|�߇�'����F>�!ׅ���3X��3�zgp�*v��?N���e1;�p߄n�(V��rMuUT�7���t���t�}�$��_��E@��E�<c����G�Y�-`������ȑwXYH�ѕ�vfG���f�nx��rqOo�v���ޑ9�٪��p*�WXα�gA�ɿ#��@I�3ߵ��M���O|�H
N6�������&�#}� �ҥMP���Gv�k�`ω_�,=�N�w l�٤��J<B��u���b�|�|�� �9r!�rw{o,E�୯�,�R�/�/�}8�=;qIbPIV�/w���*�_�i;�D"�4 ���^��(�b�~�L�����9�NX�����cD7��f��Ͷr5 ��cD' � ���I.`�� �
�ɼ�$�N����* LAf���sxqt{0Frp�
2��\��3�1���� ��J���d��s�z$
o������Z-�g�c�?���W����g0�W3h� W�1�/6���I޶��1� ���=����A�ӯ�?��Q*�SM�!��i�7���U�������"Fs%�k�x:�H�-��|��Gz�_P���Q��?4��BE��#��#�NA.)�i��<�Cx��5�� ��]�>�CB0���L����}�%n��L�7����x�zV��]G2&�'��T���cq�;2��ܛvq2�"1�q�D"��(r�W����V�t��Fߚ�f@ 0&�w�f1�nۛ<��oUDT� 0��-��qֈ9�jQ��̩��]��i���p.�{z�%��a!���
�Oa��>`4�j�:a����,��n2���׹Y�Tq�{7�?lv��]�@p��:W����?gq剟����ѩ��%2g:]��IiLS�[1�\� f���$<1�Q1����;�Mc5�P���&�T`<�3����0��ڷXγ�ӓ�[�1���4T�P��z5t�</��E�'_���nG��jW-&�H$O��Z�~f����=������^��Q�K���j�lE#̶[��9����E3h4�@����A5� ��j�j0�����78U�Y
�J5�
/D=!��i�/�����@�����*�ϐ���h�zh�9S���
�j[���-��T� �tQw�sW��"��� 5 s0�p�S�w7�T����UT{U鈠ncy��1eg��ؼb � �j����TG���&��0c��i��-�Bf��ٲ}*��`\�w��2�>��H<�K��ƚ���'�@�$,f��N"�H$�C��c�* �ϗ��GY��Cʛ^�+8��[2����Ƞ@��ǚt"��7� ������Y[v���2Gt��He�����q%; ^�=�s�5�Y�d�f���AF�y���|��N҃Y _瞐b���,#�ߙ;#�Y�ߥ����,��wQ`t�(��F�+$q�r�P1vD�z���@Qn.�Te����Rjt�?�І� 3����i[�����m�Z����ᴎ�ӢrR�R�*(Eэ�[n��(ص�ٯ�f�WY�vvHW�~� ���d���/h��{m�О��9��<� �s'���{�** q�R���wwwT�.���JZ�Q� *�E'IMM��+T�գ�>N9��F�Ǽ�D��Ȍ�m�Q�ﵟ ��`'�5.��_S��Gm���������$��L�~/�.�ʂ�[�_F2}�\��m7�`o�M�$���%���#���y�]Ǽ$�l��x��0�h��-,xk���m�_Ⱦ�l����O�ɿ���8�~��U{Y2}cg�F��R6��o�ّ��5D�?5�'7/f�f[]c��ɝu� '��|��o��^�a1�A��cRO�����Hn�f�W��<�N 2pNu�h[W�% ,
��m��=�L��s�j�G����ޫA�O�@���yr�}��t+o�b��ט���.�{��b„��v�1L�w����??�Ny�A�9��x����A��ʊ�0�����]A[;����?{]&r�Ҷ1�D"�����_ D���}��(W�X�z�h�j�mb�h4��h?�u��h4
z[ z����[[����h��ćK�3����t}�&��SK<����noE�'��y�r�:��h4��:yΫO��?�w�IQm�F�u��hD�P���Z��m�4Nb�>�(����f2~�
FD�\�H$��0�������j���C�բ�j�������F�Z�F�R�R��X,��f�f3�֭�XcB��"_Q 3~MPk]M&�Mޜ��j��[>]�v2��Y�� ߔ��I<����?s'���:����� �ܶ��kZN��)/.M#n@
��RNwn��8����r�vx3�r-9�ӥ�e��L��?GN�$��
!'~�+�6�������ua1��5�5E��I���w��o�W������A�����iч1 �������D"�9�\��5����t�s W鐸K�f��h�\��1�>�u���H$��6�^�D"�H$�9�H$�D"�A��z��J�'�s7�^$+-ߛ�t�Ӎ%d�Φ
oB�ѭ}赶�W���]?qFg�d�Nw��o����o JsOSb���rs���X��盝���f��� o�;H�W��m"�_��W�҂ v�ц*&>?�����DN���B��O(�;�����c�7m�@���oa�}�3�S�O�t�G�k�]�%�3��`�.D��|�љ�W�W�Rxk�G��?
�=,z��P}�?���cTz�:�3��v�����m������
��u_ M�r������7l?W�\���x�fc)��,>Nnyk:����f�h���=Vl��n��u�卣�� �y$�;��|.Ҟ��x/�j�2}��nن��D& q�0g�,�t���{g�i�}���}bk1�8{r+o��fo�$��֑?�1qz�AL1jF���O�c�=�ra�O�^���9K�V�/�?���0�U�7�%^����dl��\��ׅ3��������m|���.��>��]|���=��C���픇��}���<�MO���nj�"_�+�f̓=km��<���X������CY��Ek3i����]^_j��c�n^�����5�;>n��g��Ul;Y@H�E�*S*�Og��%��v����ڳ��Jyu�d"��#x�s�^�1�x��6ڗHn0䝥 �ֱe�)���Ű �͗�5Kٙc�|�.}0V�����]�`�Z�P���7������wX���N�C �O��[vs�|+��7�I#:��k�&-h�h�@���ј���?gDZ||û0j�$�t ��#+i+_��B~e �Q���(��X���Rw��j����&}�1sꃁ]�Wa�+��M�ߙCx�h���=�W��m�9���i����Ԟ�k)=ņ�ֳ�l ��]9�QFĴ�V/��c����k�+�8n2�tF�$�걮�d�m
�v;����$��c�c��)w]r$�%��I̜�@�ڹ΂��l� �'s?�2:2��|}��Sg0�sq]}'50�e IDAT���cE���a\9ᰍ���mkWmvX�"�߮�Z �� ���y����7�E1�ng�m��<��k��i�g���#K�jҷd�J�� d��'(���KN�vZ�B����%�gޜI�?04��_Y˾���'AP�D�wKd� ^9�w�H~�� >��@�t|��g£�Jk-%��Cd�x��G�$�wob��Ӷ���|>B{}k7�Uwd�0?H?�ElG����?
†0������\h�ߏ��x�{GRp�g�j�Z�����|���}�>J��k�#�~�l�w X��{�����w�qQVi��3� (/�H`����J��o��h)mi���om�մR[7-݇��\-Q��v�g3�4��,E1�7PDDByf���3 ��=j�����G�>�u��:/�\sι�:�q<*.SjP7���9;���,�7�0$��w��ݷ^c��K����ճ��7�����^��'��Qr�zZ�°�Z�8ϱx�"�Ϸ����<� 6��<�&��!⿠2l0�;W������p��K�u���$��e{f �������ӱ�f��=��uV��x�����x��ٔKG�|��6�/����A{u���6x �/��D�qO7Z[֜v��d�!��K���ӗ����7.��!Y�B�>m8����)�o��,��ك9+c���r�����+/����#�F���� �Z�x
�"��u��%�O��:Z5=�(�l��`#?Ŭ'?��%3�����Y�85WŅS��Ev��#U�f���#g0}X0�(œ��`�%�^�o�H��%���!�v�*���C�Uي�o,d��Bz�/��ċx��3��Z� �
6��r10�၌�1���K�υ�����f���߆��P����0�4� %v� ��Il�RIQE&�ԇ �%'�8��N��'�7���������Б��>, �����|���=��˺y�Щ���b%x�ѱsw�={;���evޮ�%��u�h,�?ߝ��i��2�����0Y�6��#���o�@�;+�oK?3}��z�o��M���*(@A���C���3ydQ��� ��%x�L�o�kuE�U���7��R��c?���i���w6c[���f9�.�������QO����^RQ�W=nL�k�uś���V.�{@~�0��c�\5�L��c�&3��w_3��.�#����d��m @Xh�be ������Lo����w�7I�����Lf@��]���T@�\|:E�7r�'/�~�{7������x�A���=�f��Z� ���[�(W)3�g�7,�.d"y�.uQv��dZ?Yʺm��j��~�/_�rȵ� }u�㶳`�.���x���d56��d|��i2���鉧��
Z�3��4C�� ��-��F�"ۖ�ym��K�(ō�֮7�5�����<�FS�N�e"z�i�FЊ�|@oαt���_�9�E�O��o���O߁�N6�~�~v��CA@1���4���r��~C~��H|�)���6��M�1ޝ���-j�����ֆhi���9��5�+�h�U����02Y����Z�����A�k�2Y��E �U0��u����N�sy�ixc�'��D�E���d}���X��(�Ю�#��^�5��?�����\,+����\��� �-_� m��v'˺�8��N#��r8�9�ؽy)�+���C�d�D��2�廬N ��r�jKh�Ԁ���lYhzl>J?��&��:�m����v&�C-���i/�%���lt6�5LXG�4���Lv]ɥAm�Iv�R���}9��y3�i Y�IM=O떍n���.��5~V �W�v�Ru� q+)m|�V���o�*.&/+�B]�����҈_����BR6,!�T��/�Gc}�_�Φ����k���Bo�S�q�� ?��-��XeSE��4.�ޑ�~&��]ʎ� �'%i/���|2>dm�LJ >d��e5�\䰑��Vl���w�Gw$v��es���rH��5{s���9HcEB2�%�ۼ�d� �t���9�dY7�z(%-�9�%h�:�P;��O�旙��R�rv.��Iٰ����z���(,̦ �V�Ka^%uTK3�`�д"��|� 6ׇZl���4 ���½�̜=�دsn�ٱ�H|��v�JJ���o�T<�lT���FF�
�餥��Xh'?y9=�9g��8J҆�L�ۉ��d�Kt��垢"��4�@P�� �bT��lJx�d�/4�.᮪-��#q1�4�c� �nDŌ�~f���|x|3��/�ky���A�@O�'.ev"@+�O���36��q���)��Q�6�l�^�g��<�[0b�Lt��I\1�:mԁ���=�Uއ�>@m�n�48.7�J6J�^~r>X����6���A&Ο��e+ؙ������g<��w(3^�Ă��M�\�=�mKY,A�����?�M��e�Td��p���PT��ޣަ��}�_f������:%� s� L��u�\���E�l� Y,�p��?ش�X��)3�A[jnl>�<�CI����?��z#p�kgr>�� i� 1�9l/*7<���Ce�<�Y��o���;� ������O3��+��_�6V�7܏�f~����>���4�� z�mF l�㢔�އ�
�R�
���˗�'�?�#��득�]��l�aB�Fco�~3� �J������t&4��}��9�1ޝ���66�5,[~ز�l0`��&��`�i��r�C>?{>�j4�0�D�i\�ft:wwe�|�DYۦ� o�&/����\=X��F�8��ef��&�4�1g�
B'����-��'ߖ��k���`ڼ�B�/e����̓oŇ�>C����ҦN3�4��l�\�<��k�{?&v}6�bV0��n��] p���\]]�h4�����h�h4����V�Q�T(�J

��ٌ�d�d2�a� ׍�$I�
(DA�$I�bįJ��/*e3�%���dn֧����t�ݞ����0���<Rd�A(e����`O��r����9�h�%�����(k9�7&�k����/�7��H�\=��!G���~�M�i�2�.nk��m�im�P�º�g�,L�8�6o�0�)��2{�',�˛���A�v���3�Q{��)�ح[�4yng�W�����ل��%�>��"� q�K�՜�RM�S/�O(�*��t��SPf�h4�R��q˻M+~����� ]�rǽ�
��(�FxӦ�1�O�8f�kMq�ǝ1���3��_7�GG0<*����}����@ � �S��^�@ ��� �@ �'41���ֵ|��g�^��˽�XD � 0�����|��g|��74i���ާ�̣i����~ _�D��:�N��kߦ����!m�
6���:�7�*r2�(�wo��F~����1�� ������)$�������2��L��{�j_F4�u�26�>�{����� ���z�����aFt�M�m��A~T��=�Ȣ�.L|wAJ�9���^��]�X��10TWC�����he|7];��e���R�#�b�/ӹ� ��|�v�ϖ�f�O� ���_}��py}�8�)�Չ�(��z�7&<C��V��Y���K5�V/���F��Q�9���C!�;>Z��,)h4h ����a�q���d-b힡|�Yne�}M�3��ₗ� ��PX��Ρ��.䣜k��2��+`f_�Y�[1� �rΑ�����Lu�;�L�0W���E��pwmɋ�;0�����bb��S4p�:���Zꇄ=�ˏ�u�>��LZ�����$���+ʙ���|r'��%�Ks�<c:E2���SJnPþ�3�y�Q^�l M��6�+杷�cV���)'Nl6� l^��2�:���ңw�}nᙬ;K|�"ml��KѬ��%�KY�:���R�Q���`��� �g������u\�*�G�'z�QŸ�B��M���
��4��z�O0u޿��B�Ldu�!�����`�� ���j'_�o��C�EfΜ��#e�f�—�����`�F3ÆE������[��t�Y��ěǎ%D��a��Q`k�������ֽ��\�N{�v���r�F���)S.I��]杵1�l�Υ�JJJ(���wf23&�]y�h�F�hu +W�O2h�1%%��M�X���P��弜�K1fү����`��m�ܫ���ӌ�RM7oЕ0<�(�*Ց�>F��1������$ΛsW�ȶO�~$���rP�i���۝��͝`hշ��� �w���֩��c�K9_s �����m�E��U�����'n}6�#g�|�ם��\���TR Mx�+ؗ�% f�CJ�-�A�
-�v�O�^�|�{˖/_�l��˖/?�l��|�.eҒG��)�)%?�T{���w%*,���̕ }q��Ư$@��W��T�O��yi�O?I�# Z|�)�r��'�,-y��ۥjy5�ǿZ�?I�
�U'�7���~����H���D� ?�6Jcß�V�-r���ғ
��#V����(��٘���VJ%N�� �uʷ K�[��H�i�sH Z)]n�R��H���OڑӘ�Hϥ�V���ȣ䬤I9 iRHK��K�d�V��H��\��wV�M�.��4)�ĊZ�eҴ��:�g���8SG 1J'?�&�!EDDH�c�&%��j�oG����R��~���G�a�ߥ��祄�,���Ǥ%��Hғc�"m<�Ո4)v�ߥ���#�=@��x^Z�xZ2�3� [,���o섷�i��,�y�.K_�Ζb_���Nή-:����Ǝ+E�=N�0a�46z��X�?j�Q��j$[4h�r�H&�V��m���R�iªϤر�,rߞ�!g��ԟT�-m\2C�~�I)::L�%G��i�?9��vZ���WLפأ'�o+���e���ϦkR�� )�J�{4U�~�'i~ni=�ri��S�s�������+�%I2K'��H��$u?xHz��i)�D/I�I�6�4��1�{���c���'�qG�I��5u:ҏK��M��9�ρv}0I��'��%eҷgOH�SI��.J�֢0V_���<Zg��b�7���\�i�)���r����2)��qi��CR����q���^>��5i���R��gI�~� J�'�zo�4m�i}r�d�M��ci����9�����?�V��,}�� ���IGw}&M�6Gښ~M��'�w^{M���ӥ�_�(���YI����OK���H��/����������&��W3����KӦM���`��������{��&}��6�ӏߖ�M�#}�+ۮ����>,�<yR��ʒ.^�(I׮]�*++%�^/�F�d2If�Y�~�����r�O>��-_Q�t���e˖Y�|�֥˖��m/wt�v�]Wy��] Y{����T�����@QA����kF� �V�nO���c� �?ҟ��߲uǏd_snqB���d�0u7���n�� u?�}���˥7F�.�_�ڴ=,;���n-xD1㧲��!V��I^`���:k���Tg���Hܜ�S[:� Mk8}h/����W�ԛ��ϼ�?���[�6��R�5� �� )[��e ���GI��vWM7���� �m���z�œqmH�U� �Ցmt�m߃{�Q�_��*�Nt���P��ļ���f'�y���1��f���)^�S�Z�?�cyR��>�.�T��'cH�3\<���\bj�T~�h�-��<�ǘz>�Ek�x��"/��6���*�� a|���hϵ�Lt�v#c�+"�1l�D�@pd�8�!� �AφSj>�0� ~�p��f��G2i��+w��Ck^�#�z��[�^b��V9�����"��0zj6�M�ɋ���M�NY��t��i��}��H�^�I��o����R����Qm^k)����
�&��)^�r�h�v�np柉�����a^Ѓ<N�3�I3���Ç(�^ ��b�����>z���\�D�K/f�`���!烙��J�f�}U���H��睋`.�cgy�J���Q�ج�V9�>��3Y�m���̦�SL�P1)(�y�|y����S����1�i.T�&��!⿠2l0�;W������s5g��q��:[d���se�Y�'�Y�����M�t��'�Ha���� �-��
0s��i6�����.���7���y`8��ًH>ߒgGG�'ش�o�ȳ��
���
}ҿ���>�K�%���ג�� ��$\ �o��û�1dț���$<����?d8�H'���'�.�V��hD��Cth�Z&��Kg�9�O>��={��$����:o�?9n�=(�Y ��?����@���3��]Z��U�N@��urr����G�q,����`ږC<��F��tP�#ý��:�=m�e}�3v�ŎN�r��Ě�Xc�>u �G���/�����i#�~��]�\�ƈL=U��
_\���Ҹ@�3hX>�m��'��r3]���_r�g"�vT�udO^^� /& ���G�rVG_�n+�z^?��ȟR?�SE��YM�u���{�ӿu��u�|�M&/O�h���s�~%��l�G���x,�]�ٷ%<�
{j���o1�1-�G̖�y&��Oym7|���Qaj���O�Y�|��#��b`bԋt�DłI��3*���N��1:b���`՘��)ۏdҚ�Wn�䨐��{�E���zt{�����K���N��M �L�|5my�<m���}�F I��S^�NhF_��Q������ cuG/�-���XT�S3j�|ۣ��@�+�dY%\y�HK��� ��<q���&��35T�|�����J_Q�T�����奀�re����^�2�9Ɩ/{h�jԾ\x4�������M�*v������i�UX��!P��?�U��6u:��]��z]���ҭ�/�5.D9뀩�tok�������a�`���d��4�K|� ?K��xwB˦#d�t�f���kj�����M���Z�q���ЁF����y_@}4�mW*)9���a��i���r}��ٻ�Q���@��L <J��8�z���`���V��۹T�6CI�:�of=����ː!o2��#Hk&�Vĕ[� P}�"##��� "2�џg9�O-CI�l����H�iLs���O�꼣��"`$��d�"��n����#�߸G�}��}�����O�v��@�_I�a;����=�p�� �x�!�j ��H�?���dS��9;�aO�,�����$!IF2g���9֝�I-".z, Zɢ��pQԘ��d��ī9\��� 5 �x���]9�%������������kjnf���b���(
�jn�9 �&���t��'i�5�_3�'#=1�[V����ӫ.& �Ե�r�[�].�V� ׏�ۂ�7�(`ޚO9�v��w����
�:�:�W��Z6)U�������: ��h�me�g ����W���^��Gri��+�Rrؓ�/k��� �u/�9��#���t�&�f����l�C�V�j@��
d�k�i���8H�C�>q� �`��`��Ϻ�V�Lg_M?���A��2Y \6����OYj���/��7��ч#�"����-��-�y�U�j=�]�ju*�t���ʋ���XL���h�`ݕ*�|0�Q���o!4����gGcj�1�h���Sc��V� k�Iwc$�HO\J�K�~�%ѓ?�q �A�wxϺ��!�V���0�.d"y �K]jC�����-v֖�o�_���U�-�,Dž�w�ޒ ���%JX=>�#qt����[m��FiG'�G�����/mf'������r!� W�&Y����}x�%}���[������߫�b�k��5x��٢���5�I��#����$Zq��{��괠£U+|B9��T�y�
�4]�\�9.ߴ�}�r�R_��P��< ��w"3O %����o-
˗���]Ɉ�:�@U11z�.7��������=4�P�pK5����ݥgO� ��+�k�v١`;������T%��}<Y �^�׏��t���ɯ����t�H IDAT<Лl
��n?���T� Zq�w��&-l)'/�m�2Z��Ua,/������r־R�u>;���_Ze���X��/��D�nJ��>��>�L�ṋԃCl�ɕ�\� �j���� $6��ON�3m�&�k쥩��F#��l� ��X�v�Dm�W��m�;�-��/�a�>�}ώ<�H�(55 ��k�=J壑�����H��R>�&QχF�ZpA��*��zud���؊8%�Nj�}Z�
&��ޜ��m�fepԉ�N���T�Sdg;�z�^]0e������i� L�|�¹<1��\����޹y'�?��(����@Qv��u����\�l�(3����Kq{?�yf�����}�7?�ݻws4�
�G��V�?���̂\v|�*��0i��@9��q��~�^��9�9t�eݝ�N9����<� .0샵d���^�/>���'���T��y���?�)� �"^��܃ �./n{ ?�_�����᫝�p��$J��Y�PNvf6�da�k��#u�ѐNٙ����։W�9��Q�d�[P@v�Nf�{x���z`*���?&B�!���o�>Keߩ=��d���YRI9�^z�̲
*=�=Z�g���d�MW:���EQ������U�#+�&K��릛lב}�<��NE~
�zc�R�Ёc�����-��o�_�z��]7�}�q��@�~��o�f3!� ��Q��'�V@��͜�0���� 0�99���-\KZA�ii�VX����cʬ�B ȵO��G���[܉z�C������'�/�� 7m3û��T~r���t�M4����B;`�5
*�x��E2�%{9e�\3�ؑ����z�qC��/�{-�gҋ�d�����
]1��f�M���yI��Ⱦv�\���g+���s�(�d$����E6?G�󡖫6dB}-ϥ ����ؗ�Ɍ��<�c9ee�+�z�Eg������ϗɬ����E?O��|�V�iZ�Dv~��{kQ��2��P\x���?��� � m`KO��4V$$SXṞͫHֻ0�OǺ �rv6%%�]��T��?���Y�W]L�I˟1��^��${�>�'V�����;K�.�}�<��Ư����(gFW9���m�ɏ�����G����>=»C���w+���Ш�����ә�Ś���cI�i&���@���en��zo�����L8G��O��5�����z���Yq����s��s�d3>d�� �O:��+�'��;�S�M�`|�_6�#?�`�t:�������Ի �������\�%�5=���������*�>�(]��O*7�y3#� ��{/�k=:p<PO��|���\� ���z{�٬#���Ʒ>c��`��^��S�B�1c�ycJK�t�
MF��E���Y�b�Jgb�;�on�7��&ڢ}��Л����'���'-�c�W�,ݏO�$��Gk�5� �T��VjG�x���6<2����Xr��)�j�[�LMj���o����V���9�+���`뾺��˺�L����bw��Ο�ՙ��i��^~��!�S��V���}e+^o�e��s|u^l����@`e�V�Xҝ(w� ;�\��3��}�)))����W k[�06'��0w7��dzn��2��Uf�NX�LY�P$SZ�e�C:^:[H��� $4x�=[����}�<��m�P���x���y�x��%f� �<�j4�N�$�� ��}h%U���� F*g� #ҩ���J��@N�i�{�jh��}�<Ĉ��X�s%1;!�K(�g�V�P�����:Ujm�k�?5�e��)}�2�K,X���T˵ޣ���N0{�5Г�K��Њ�>���/�&]�|���$�$^}r���ˉ���62��BiYL�+�Qyx܁7J���% 7��͘_�?0�����v����TME�7�M;m�4������vCی���ho��[��tf��_�S�Nz޵{��Pa�pS+��#G���poF�������6�k[�d~M��>d�(b����-�5Quy��z@�,?�z*��
L��PwM¹��1��o��L�9�c���׃#앵\[2��1�<��\��#�rm�Y}�lĄ��^d.����<ީ'3ڨ�5Yf21)�hm�_6u6��YB�P�jt��l7���g'|���
�7��F�دU�����R�~��ŐƜi+(��g���i1�A� �/�q��΀�ݝ�s4c0�,�;Ȑ�����+�7�a�WWW�j5*�
�RiY�P`6�1�L�L&6l؀�Q�$�P�((�$��Yϐ�ͥ,PC�?�ET��;O��z�ߴr�� [~i��y��آrC�L[:U*-*��=<��8��͕��~�G�S��?�P�~5\�����/2��g3��]۲�/� `&��?�s�|�9���f��C���jKN��00��䧟��b#�'ڙ\?r�ǚ��G�+k��t'ڙ#�rm�Y}Ei#��� ��
�&�rN�YpAk'�L9��'|���=9����M �jф3er_f��cys�s���[؝D����7앿`ڐ&����f�`�#z�W�8JJ��@�n���+^O�fIT����N��3���+�~<E��?��k6?�~���[ߑ�T����`�!�����}�ҹ�/|B���^�>��1�5Łw���]K[���<���(�U��`ʌ��� �&z>�Ğ���f7�������{��F��/��ݽ����O �@p�"?�@ ��:�3��ҥ�i�n=�@ ;��<�E�ݴ��Eh������ky��l2r>}��@�^��@ �̯<�W�������<�GF`�LbEL�Me��0w���� ����@ ئi#~�|��[��h}C�=����-i�\�}�)��߈�>���S
9I�,R=k.Q�j������lO)$l��@o��W�)��K6�ո_� V�@ �{�&~�l�3���]5~ W�}��X�>������U�lϋdԘ1�U�Wn=J�Oڙ�P����D��G�?�$ b@�s�aV|�5���Je�|�����N�|��(�@ ����ϐ�;K�D�x�!�*�l��7��h!��zRV�/?:v�N�g�rEY�f�M�*W��S�IH� �C�r� ϬӴ�7{Iϭ"<�ٓB�@ �q:�3�- ��Ib�U~ez������p�v��� �_OT����?���3n�!7͈Bq���&�Gt!�oHj|������@ �p~��d9�tԬE j]C��v��r�g��[9��S�޼��KZ�!A���r��ޓ>&�Cp�A���½�"Ri����:� C���@ Xq:�s�)|�e7_�Ok����RZw�����C��i�׉��ڑ|Έ p{�� _��4�A�Ѡ4�ŏ�L�O�zP��2��`܀ ��垢"��4�@ Asq~�ϽS�<�G��X��zџ) c�T��u�����{��tqP�q��F�{8S&?�G+ju�1h�\���_�Wʎ�C
y��ċ�@ ͦI۹�=˼��b0M�(^8sV��l0`�:�����3o�H :*w���
�����JFO�N�@ ��4�������&|�$m�
�g�'v}6�#g1�_aCN �@�,���m2�������G�ڦ�@ �湫?�O7FD��V�@ ���g�
�@ ~)D�'�@p�� ���Ȣ�>���\�KJ�V�%��X �@ �%~����,��ql?S�k[rg1_`��y|�)������5�@ ~���/w�E�B ����|u� &/�p�y�@ �[�.���n�]�(���G@�O�����v��q'���3z<��G����غ� ������“�G3��7Α��G��؆o�,s��s�5�#J�\r�M_l����>�����-����h �l[���' ��5z�E'�.�m�EJfz�7����GmXj�Kba�N:����=}����,L�@ �9w�ToΎ,H��n �8�)�.ts���%!Cܹ���W��Г}$א^�3�PRYO����s�H���ʰ���\I��Z�.����H�ڞ1����I�ӳi��4��y�\���yv��kΰ>.��K~I�bٞق�c�0�A���l�G���ӗ����M�_�u$)�6b�O �m���3d�e�E�{�y��`Ȑ�4������a�`�'e�
�� �wjA��X��"�������:���ח;\+��gQ�}�K�w%��yRd1�_{ y[�Y�$z� VAd+.���}G �=ԓ�b%x�ѱsw�={��J��7��w�n����|�c?��-)�2M#�@ ����MT��w��l�B��}�F� P��c��oI���x���[I�x�_�F���^@�7I��T�W�����mg��]� "���D����\ن��m���TJ�-��]����JF�@ �wo�g���:� }L&��Eu[��H|�)���6��M�1ޝ�Ʊ���
�q�u+����i�O�/�35k�Z�Pe�
@ �g��[9��S�޼��KZ�!a��h�K�i����YƚP&�[Mqj<1 �(|5g��@ d�{��w�����k��#����II�K���� �W�v�Ru� q+)u";��,��Ώ��1�1�o7T�*�A���ٲ��Uf0UQ���I����z���4~��x��z� ��3s�Lb��iRx�j \⊮Ib�@ 6��G�Z0b�Lt��I\1�:�ځ��X��Ԫ��~�G1��A6%�O2�E�p��kW�/��;�������ty!�7��`ʔg�(~ b�Y%�0Oe_'�����G�M�z�}*7<ݛV��Z�&I �@ �F�|���$�$^}r��mX}�&Th4NM�Zet��8-��] ��`Ʋ�+��z��8�_,�kߺ)Z��6l��*���t��`@�i�j=]��L���� �@p������+�7774 �WWW�j5*�
�R�B�@�P`6�1�L�L&6l؀�Q�$�P�((�$��71���L�˸7I�u@' �5�?�{�'�������tn�Gc'xsdcS�>����Š���� ���H�@ �f��~ <��;%�}'s(.-�U��Lӏ�޿�Q�����$'�Z���!���@~�� ���a�~m3�� a��!���@ ��޷z�@ �� �@ �'ܓ������� ��f�sΒS\� -U�ddQbs�@�4�u�;�@ �� ���l">~ �%��d�"���t%����DZ�LU�Қ����s{7���9sX�z#i��[�)����� ��m-*�@zz�qS7��/��˴riM�y[�l@)�����h�!3�1qd��?�@ p����UP��ڻ����p��/�.<9z4;Y�l1䓴e+�_��" �V���m�ֱ�dZ��F�uN�];��yI,��I�q3x���C? 9I�,R=k.Q�j������lO)$l�8�W ��{r�Ͻ�K�\C'�;Q��0�`k���#����b��ф���x� W��N,�����I���z��J�<g.�S�<;f �5gXKj�#99;-��<}�N����A�1 P���0K?���+�M(=�@ �*�ň� Z5=�(�l��`#?Ŭ'?v�"Y߂����0{s0� �,�i���Y�$z� VAd+.���}G ��׮�3�I~���٩�U*W��S�IH� �C�r�&��@ �{��,�3��c�\5�L֒�⡵^45���f�[�%.x�ĺ�
����rN�lCD�6N߮�6�P!!B����]H������@ �{��+�+����St�m�د#��c�;s�%�d����
��X��0 F�ZĠ�5T�@e "��Ѿ��������өs��:�4��!� ("���� WIK/�]Dg|�z�@ ��4��?{*��?�� �VRjM�x�����,�Y���`{��\�{P7�o���b�LU��cҸ��9C��xfΞI��9N��ӭ/~�������6��(xz@��{��Ί5K���N�@ ��Ո�&8�Q]�)�}��� (���J���I& �ϚM�8t�7��S��ރ)S���m,��f�ɟ) c��s����NV�{8S&?�G+jmqc������Ưc�g`���M�w�@�[G�|���$�$^}r��m؝�l�aB�Fs�\�٠är�F�l��`T7�u$'o��F�T) :*w�FS�f�Ӷ��{B_�a���E �����\]]�h4�����h�h4����V�Q�T(�J

��ٌ�d�d2�a� ׍�$I�
(DA�$I��Ո_-JM����4v4Grr4=�P�qw��j�W%n���ޯ0I}�@ �wܗ���J��X���9C��@ ~���>B�q��q@�@ � ��[��@ �3"��@ �O�'?syЙ��%���ΝU�ddQbp��$�,YYYdeeQ�����:�����'�@p�{2�+;����5䖜������ug���c�*��������#..���5%�2sl�H��;����tڿ�`.b|8g Wo$��x�:�@ �Uܓ��ֻ-�E�HOo3nj;�h�xj�#�-1�V��5����9��0��[x����N��4�l~�o�OΣ}d��$V�đ��E��@ �*z��F�|��le���5<;א϶u��}��oQ��2��w]6��l�iIYO�E�(���(�p�Pa��?���@�|~rv���X����f�|O��c�Ibg���Ys�
TC&�lf{J!ab�?�@ ���?�N/�re �4�D�[Ü��)W��N,�����I���zR�l�3��j�3��3���%��Qn��4�w�IϞ� x�ѽkW�#{Ҷ��A~rv��g�������w�g�����#H� �1f�_Pq��)y�@ w���6��#~��v��o��b����9�|C��,U=� �� ���XȾ������/�3Ӈ%3�����Y�tR9!WH���v�t���o2��"��:;u�J�
Tr*u# �!t(�[�a�Q�PV ��G�v�0�H7]��? Z<��?M7����m� �$���2�2��1F�����Xζ�N�lCD�6N߮�6�P!!B����]H������@ ����+�3-*�e��s7�Ι,��F�ZĠ�5T�@���S�D ��rj���l�;r�?ڷ�I�K�i�����~��k �����rH���z҆����.�3>��7�@ ~�ܓk����
���Y
�v3g���4��n��l���*3��(��Ǥq���QX�Mp� �¼<J ��h%�o��b�(������� �{�9{&�_�8u�O������S\H��x�������J�;+�,!~�s:�@ �}(�y�A�G��{���U��C�xѮ�(�پ�����DoJ��y��<���D�@�7ߐ�g'�v�a�� t2ߚ >�������d�}z��������� �#��cϞ=�;pE��t��k�#cgkǃ�����9��������`�~DI��-I{�r2���=Բ�o��f�|���Z7��@ 8K^^J��J�Z�F�R�R�P*�(�J\\\pqqA�P�P(�$���jjjHKKC�,�T@%
*�J����ߓ��x������w�A�I��Δ��`Th��`y93� �J�R霜#;�0 (5��Ja�P��7�"6S������B ��/�@ �qRRRpuuE�����F�A�����Z*�ʺ��l6c2�0�L|���k$I��B��@����k����q`�M�&g�vI{r�씣�A����MW���+q{��~�I"���7�}� �F��X���9C��@ �D�'p�R�����@ ~k�Wo�
�@ �ψ�O ��>� ���dd\@g6S�s����ۜC9Y�n�Z'�<#�dIDAT��� �@ ��'��3���_Cn�9>Y���;.�� tg���c�*'n6sl�Hʑ?�)����E�ݰ���a�ꍤo]�@ ���{2��z���<��m�M}��a�h��jgn6rv�a
M�`C��n��l~�o�OΣ}d��$V�đ��E��@ �*z�j#�9&���)��W��k~M��S��K²�:<��������D>;��C:Ze�Iڲ�}�/�FW��\3Y{7�u�.���ۅ'G�f`'-)k�I�h�|�E�* <��C>�֭c����!D���N���ϊ!/��q;�<n���qX>��$v���5��@5��`R�f��&�������s��+W��I�NԼ5�y!ؒ`��l�9Η^�~��t��jr�"!� r��g�7}Ď<#p� �IJ)��Ȩ�� �����#����b��ф���x� ��w�IϞ� x�ѽkW�{�e��q�{�;y�B�L1�P��2�r�a�C��(K�2��`�`�Z
5��P�[�񲸄�L�y��RfA.e�b�@RYd&(7g"��1�B�n��ϾH��t������&�>��y�Ϳ�s��dO���.��+W��2����c�u�ͅ������_��G�Q�g�'_�u��9�m�2}dA�G����4^���+/"""}YL��]���Ḯ��X�������8X��}R���� �I��,?��b�KW��0%���+�L��V��G&A���.��b��/��z���Y�432]0e�^����g��f��e�Z�´���V}�+���ʭ���AV��ρV��"""���V�׉���c�S<�d��k� Jh���C�c�BQ��o�����h �x���� t�kSz�[CZ�Շ@�/ �aL�vu_K+����f-f��qT��m��rX��b+� ��s�O����.+�� ��@+���}�������y?���({��O���1��|�7z��Z���>��������)ma%���/L � HI
�ߴ{2�K�.z�G�5T|�@r^��������w'�!E'����D�~�ǯKN���U'�z������ ���344\�b�z*1̞����x(��$�Se�\��Z��$'&�t�ϔn���ql���GNy�T�:��9�;c<i��o�q�)�&jΝ#`9�/��E,v9����~���p����"�`�9�kuN���⍿�������'�V��1�G��@�n9. >���x���u,/¬'�gj���'s�q�:�����J�.Vf>s�d[�/)Ҳ2�� �WW@���p�����xt�$�,y���v��`g{�t��-`lz��Õ8�$w�?�;�%�f�r��X�]��}�mF��Ax;����w��¬_��9ҁ@:6#�zjш�F�������Vf+QL֢���=:�I�7p��:���i�߬����p^W�����v7^O�~?N����16�q����>���-
��%���/"""w\EE���X�Ebb"�eaY�������r�p:�c0�  6m�D0d۶m{ x0��m�[+~���� �m���EgI_���(k��eRhu����g7��:�pb��7|{����}#s~�B%}"""Q+&�NYC���3�H�1�u��<��9�Yz�>C��UλȟW��蓜���"""�/����aJ�DDDDbD�L� �9v�s��A��Or���z�?�ADDD�~��՟�FQ�F��~��֬���<�!���C���[6��ʕ�}u+U��[�SDDD�F�L��� ��F299Hb\�=�r��Pǎ_��˫�{����P\Pȱ������D��ˈn���I��;����r���{���|�qt2�coɫ��!��l-?K��������� X{�m��r�o_��z/y��1#{h}�g���T?�H��KL��7� �.��W�am�n�<���'��x �����Ŝ��� ba�vUx�w󉈈Ą~��G?Ɔ m�G����k%u�X���u㘻`������$>_HNJ��N~���㐐͜9ϟJw�wǰ⁔��~���Ϯ㳄1���4Ζob[�'��("dw}����쪞��y�Hl�Pݐ�ڜ?��j_=�O� +�;{������q�Q��v��x��-^m�kΝ;�w�o<��_&~]�W�cw��9˞fF� � ���k��CN^X6��z�#�q�����e �˲�?��3Rxr�/��L����_�����?6��>�N�ƨ1�L���\2g-fa�YRǏ ���\�h�VJJ� +�Y��@�鱭���D��� �Wn��S�_�׶��t�bJ�}kH��I�7$2��%3V�Jn� 4uYvr�F��?fܵźa�H r��]� yO���]�Y�$d0��g��7�0&� {V�9DI d�Z�������� ��v1��h�|�XG��M���yI�_��������)e��% ������h�w`w��E�I�g��YxO�l�o)-� k_`l��d4�60 %)��|���.���%� �P�a�y:�MDD$V�˧z���O���e�m
B���s�X___7�;+�{�t���r<�^>��
�>���C�uTU~�iO-V�h&|�:�{#��"�?����O�U?e�TҀ��8��P�zG0̜~O�#���H���?�$�,y���v��`g��,Y[@����٫�*s���/X�y �+۾˙� ���@�}��^���_��}�qa������'tg�d�L^.�:�Dr�bj����
�~���lHF�،x�E7>����~��eݮ[�A���XnwX�^���� .��U��m�Vg/��f?�^�)"""߮��
���,���D,�²,��㉋���r�t:1�`�! lٲ��V۶m���m{bků�� SO���a.�ulu _Ƕ7�
�&���S{�DDDDb�?���ODDD$F(��}:��V�˖e�D:��ק?_��#��RT����ؑGDDD$����o�ԟ�↕LĦ���pDDDD�Z�N�� $5��.N���DA�����H� """բ �@�ԿW̖w�Q��t@""""Q)
?���t��圬F:������)���Ư�`DDDD�VT���3|���t""""Q-:?��H """ݢ �k��g�5�q����D�>���S���o5OwG:��֧?��Ⱥ�'<2�~�SM���j}:�K���O�t""""�C����A�����H����������� �����;��m�V�����a̜�#��F�v5p�����&5!ұ�i��\�������.5R��dkك�n�{/�e���ЁɌ�3a�E������!�:��xw�)*Z��8�m�9�ᡠ��� �t`""""rkB!�1���5S�r8�F�q�l;ހ����qEDDD��m��0\ �\r�v�+2���.c�.۶m�CǕH*""""��ءK`j�m_˜z��a7�q��1Nc��ƶ}�TDDDDn�m�m��1�.c�\�m_��ĄB!ۘ+�m_q�h�t�""""rkBv�1�Fc�F��h��%��g��EIEND�B`�
# building pycairo (needed for graphite)
wget http://cairographics.org/releases/pycairo-1.10.0.tar.bz2
hack pycairo wscript:
#ctx.check_python_version((3,1,0))
brew install cairo
LDFLAGS -L/usr/local/Cellar/cairo/1.10.2/lib
CPPFLAGS -I/usr/local/Cellar/cairo/1.10.2/include
PKG_CONFIG_PATH=/usr/local/Cellar/cairo/1.10.2/lib/pkgconfig ./waf configure
FAIL so far.
#!/usr/bin/env node
/*
* Time `log.child(...)`.
*
* Getting 0.011ms on my Mac. For about 1000 req/s that means that the
* `log.child` would be about 1% of the time handling that request.
* Could do better. I.e. consider a hackish fast path.
*
* ...
*
* Added: `log.fastchild({...}, true)`. Use the `true` to assert that
* the given options are just new fields (and no serializers).
* Result: Another order of magnitude.
*/
var ben = require('ben'); // npm install ben
var Logger = require('../lib/bunyan');
var log = new Logger({
name: 'svc',
streams: [
{
path: __dirname + '/timechild.log'
},
{
stream: process.stdout
}
],
serializers: {
err: Logger.stdSerializers.err
}
});
console.log('Time `log.child`:');
var ms = ben(1e5, function () {
var child = log.child();
});
console.log(' - adding no fields: %dms per iteration', ms);
var ms = ben(1e5, function () {
var child = log.child({a:1});
});
console.log(' - adding one field: %dms per iteration', ms);
var ms = ben(1e5, function () {
var child = log.child({a:1, b:2});
});
console.log(' - adding two fields: %dms per iteration', ms);
function fooSerializer(obj) {
return {bar: obj.bar};
}
var ms = ben(1e5, function () {
var child = log.child({
a: 1,
serializers: {foo: fooSerializer}
});
});
console.log(' - adding serializer and one field: %dms per iteration', ms);
var ms = ben(1e5, function () {
var child = log.child({
a: 1,
streams: [ {stream: process.stderr} ]
});
});
console.log(' - adding a (stderr) stream and one field: %dms per iteration',
ms);
var ms = ben(1e6, function () {
var child = log.child({}, true);
});
console.log(' - [fast] adding no fields: %dms per iteration', ms);
var ms = ben(1e6, function () {
var child = log.child({a:1}, true);
});
console.log(' - [fast] adding one field: %dms per iteration', ms);
var ms = ben(1e6, function () {
var child = log.child({a:1, b:2}, true);
});
console.log(' - [fast] adding two fields: %dms per iteration', ms);
#!/usr/bin/env node
/*
* Time logging with/without a try/catch-guard on the JSON.stringify.
*/
console.log('Time try/catch-guard on JSON.stringify:');
var ben = require('ben'); // npm install ben
var Logger = require('../lib/bunyan');
var records = [];
function Collector() {
}
Collector.prototype.write = function (s) {
//records.push(s);
}
var collector = new Collector();
var log = new Logger({
name: 'timeguard',
src: true,
stream: collector
});
var ms = ben(1e5, function () {
log.info('hi');
});
console.log(' - log.info: %dms per iteration', ms);
#!/usr/bin/env node
/*
* Time 'src' fields (getting log call source info). This is expensive.
*/
console.log('Time adding "src" field with call source info:');
var ben = require('ben'); // npm install ben
var Logger = require('../lib/bunyan');
var records = [];
function Collector() {
}
Collector.prototype.write = function (s) {
//records.push(s);
}
var collector = new Collector();
var logwith = new Logger({
name: 'with-src',
src: true,
stream: collector
});
var ms = ben(1e5, function () {
logwith.info('hi');
});
console.log(' - log.info with src: %dms per iteration', ms);
var logwithout = new Logger({
name: 'without-src',
stream: collector
});
var ms = ben(1e5, function () {
logwithout.info('hi');
});
console.log(' - log.info without src: %dms per iteration', ms);
language: node_js
node_js:
# - 0.7
# - 0.8
- 0.9
- "0.10"
# - "0.11"
language layout title date comments sharing footer navigation github
en
page
Changes in latest versions
2012-10-24 16:24:28 UTC
false
false
false
csv

version 0.2.6

  • Print headers if no records
  • Add function to.prototype.array
  • TSV sample
  • Test 0, null and undefined values in from.prototype.array

version 0.2.5

  • Print doc generation path
  • Update doc generation with latest each dependency
  • Remove buffer related test

version 0.2.4

  • Speed improvement to transformation

version 0.2.3

  • Fix stdout samples
  • Install instruction
  • Honor the pipe end option

version 0.2.2

  • Function from.stream now use a "pipe" implementation
  • Add setEncoding to the generator to respect the readable stream API

version 0.2.1

  • Line number in parsing error
  • Asynchronous transformation
  • Have from stream send pause/resume advisories from csv to stream
  • Column property to column name if defined option column defined as an object
  • Pass options in the from and to functions
  • Function to.string receives the number of written records
  • Skip UTF BOM from first data event on UTF-8 decoded stream
  • Fix from array with the column options
  • Travis support
  • More doc about columns and transformation
  • Update and improve samples
  • Backward compatibility width Node.js < 0.8.x

version 0.2.0

  • Add to and from convenient functions
  • Documentation generation
  • New generator function and class
  • Full stream support
  • Externalize the parse, stringify and transform functionnalities
  • Rename the data event to record
  • Test coverage
  • Isolate state into its own file
  • Isolate default options into their own file; Implement to.options
  • Convert lib to coffee
  • Rename from* and to* functions to from.* and to.*
language layout title date comments sharing footer navigation github
en
page
Columns ordering and filtering
2012-10-24 13:17:49 UTC
false
false
false
csv

Columns are defined in the csv.options.from and csv.options.to. Columns names may be provided or discovered in the first line with the read options columns. Most users will define columns as an array of property names. If defined as an array, the order must match the one of the input source. If set to true, the fields are expected to be present in the first line of the input source. For greater flexibility in parallel with the csv.options.to.header option, it is possible to define the "columns" options as an object where keys are the property names and values are the display name.

You can define a different order and even different columns in the read options and in the write options. If columns is not defined in the write options, it will default to the one present in the read options.

When working with fields, the transform method and the data events receive their row parameter as an object instead of an array where the keys are the field names.

// node samples/column.js
var csv = require('csv');

csv()
.from.path(__dirname+'/columns.in', {
  columns: true
})
.to.stream(process.stdout, {
  columns: ['id', 'name']
})
.transform(function(row){
  row.name = row.firstname + ' ' + row.lastname
  return row;
});

// Print sth like:
// 82,Zbigniew Preisner
// 94,Serge Gainsbourg

Columns as true:

var data = 'field1,field2\nval1,val2';
csv()
.from(data, {columns: true})
.to(function(data){
  data.should.eql('val1,val3');
});

Columns as an array:

var data = 'field1,field2,field3\nval1,val2,val3';
csv()
.from(data, {columns: true})
.to(function(data){
  data.should.eql('val1,val3');
}, {columns: ['field1', 'field3']});

Columns as an object with header option:

var data = 'field1,field2,field3\nval1,val2,val3';
csv()
.from(data, {columns: true})
.to(function(data){
  data.should.eql('column1,column3\nval1,val3');
}, {columns: {field1: 'column1', field3: 'column3'}, header: true});
Software License Agreement (BSD License)
========================================
Copyright (c) 2011, SARL Adaltas.
All rights reserved.
Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of SARL Adaltas nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of the SARL Adaltas.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
REPORTER = dot
build:
@./node_modules/.bin/coffee -b -o lib src/*.coffee
doc: build
@./node_modules/.bin/coffee src/doc.coffee $(CSV_DOC)
test: build
@NODE_ENV=test ./node_modules/.bin/mocha --compilers coffee:coffee-script \
--reporter $(REPORTER)
coverage: build
@jscoverage --no-highlight lib lib-cov
@CSV_COV=1 $(MAKE) test REPORTER=html-cov > doc/coverage.html
@rm -rf lib-cov
.PHONY: test

Build Status

     _   _           _        _____  _______      __
    | \ | |         | |      / ____|/ ____\ \    / /
    |  \| | ___   __| | ___ | |    | (___  \ \  / / 
    | . ` |/ _ \ / _` |/ _ \| |     \___ \  \ \/ /  
    | |\  | (_) | (_| |  __/| |____ ____) |  \  /   
    |_| \_|\___/ \__,_|\___| \_____|_____/    \/     New BSD License

This project provides CSV parsing and has been tested and used on large input files. It provides every option you would expect from an advanced CSV parser and stringifier.

The full documentation of the CSV parser is available here.

Usage

Installation command is npm install csv.

Quick example

// node samples/string.js
csv()
.from.string(
  '#Welcome\n"1","2","3","4"\n"a","b","c","d"',
  {comment: '#'} )
.to.array( function(data){
  console.log(data)
} );
// [ [ '1', '2', '3', '4' ], [ 'a', 'b', 'c', 'd' ] ]

Advanced example

// node samples/sample.js

var fs = require('fs');
var csv = require('csv');

// opts is optional
var opts = ;

csv()
.from.path(__dirname+'/sample.in', { delimiter: ',', escape: '"' })
.to.stream(fs.createWriteStream(__dirname+'/sample.out'))
.transform( function(row){
  row.unshift(row.pop());
  return row;
})
.on('record', function(row,index){
  console.log('#'+index+' '+JSON.stringify(row));
})
.on('close', function(count){
  // when writing to a file, use the 'close' event
  // the 'end' event may fire before the file has been written
  console.log('Number of lines: '+count);
})
.on('error', function(error){
  console.log(error.message);
});
// Output:
// #0 ["2000-01-01","20322051544","1979.0","8.8017226E7","ABC","45"]
// #1 ["2050-11-27","28392898392","1974.0","8.8392926E7","DEF","23"]
// Number of lines: 2

Migration

This README covers the current version 0.2.x of the node csv parser. The documentation for the previous version (0.1.0) is available here.

The functions 'from*' and 'to*' are now rewritten as 'from.' and 'to.'. The 'data' event is now the 'record' event. The 'data' now receives a stringified version of the 'record' event.

Development

Tests are executed with mocha. To install it, simple run npm install, it will install mocha and its dependencies in your project "node_modules" directory.

To run the tests:

npm test

The tests run against the CoffeeScript source files.

To generate the JavaScript files:

make build

The test suite is run online with Travis against Node.js version 0.6, 0.7, 0.8 and 0.9.

Contributors

Related projects

restify

Build Status Gitter chat Dependency Status devDependency Status

restify is a smallish framework, similar to express for building REST APIs. For full details, see http://mcavage.github.com/node-restify.

Usage

Server

var restify = require('restify');

var server = restify.createServer({
  name: 'myapp',
  version: '1.0.0'
});
server.use(restify.acceptParser(server.acceptable));
server.use(restify.queryParser());
server.use(restify.bodyParser());

server.get('/echo/:name', function (req, res, next) {
  res.send(req.params);
  return next();
});

server.listen(8080, function () {
  console.log('%s listening at %s', server.name, server.url);
});

Client

var assert = require('assert');
var restify = require('restify');

var client = restify.createJsonClient({
  url: 'http://localhost:8080',
  version: '~1.0'
});

client.get('/echo/mark', function (err, req, res, obj) {
  assert.ifError(err);
  console.log('Server returned: %j', obj);
});

Installation

$ npm install restify

License

The MIT License (MIT) Copyright (c) 2012 Mark Cavage

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Bugs

See https://github.com/mcavage/node-restify/issues.

Mailing list

See the Google group .

This file has been truncated, but you can view the full file.
<!DOCTYPE html><html><head><title>Coverage</title><script>
headings = [];
onload = function(){
headings = document.querySelectorAll('h2');
};
onscroll = function(e){
var heading = find(window.scrollY);
if (!heading) return;
var links = document.querySelectorAll('#menu a')
, link;
for (var i = 0, len = links.length; i < len; ++i) {
link = links[i];
link.className = link.getAttribute('href') == '#' + heading.id
? 'active'
: '';
}
};
function find(y) {
var i = headings.length
, heading;
while (i--) {
heading = headings[i];
if (y > heading.offsetTop) {
return heading;
}
}
}
</script><style>
body {
font: 14px/1.6 "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: 0;
color: #2C2C2C;
border-top: 2px solid #ddd;
}
#coverage {
padding: 60px;
}
h1 a {
color: inherit;
font-weight: inherit;
}
h1 a:hover {
text-decoration: none;
}
.onload h1 {
opacity: 1;
}
h2 {
width: 80%;
margin-top: 80px;
margin-bottom: 0;
font-weight: 100;
letter-spacing: 1px;
border-bottom: 1px solid #eee;
}
a {
color: #8A6343;
font-weight: bold;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul {
margin-top: 20px;
padding: 0 15px;
width: 100%;
}
ul li {
float: left;
width: 40%;
margin-top: 5px;
margin-right: 60px;
list-style: none;
border-bottom: 1px solid #eee;
padding: 5px 0;
font-size: 12px;
}
ul::after {
content: '.';
height: 0;
display: block;
visibility: hidden;
clear: both;
}
code {
font: 12px monaco, monospace;
}
pre {
margin: 30px;
padding: 30px;
border: 1px solid #eee;
border-bottom-color: #ddd;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-webkit-box-shadow: inset 0 0 10px #eee;
-moz-box-shadow: inset 0 0 10px #eee;
overflow-x: auto;
}
img {
margin: 30px;
padding: 1px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-webkit-box-shadow: 0 3px 10px #dedede, 0 1px 5px #888;
-moz-box-shadow: 0 3px 10px #dedede, 0 1px 5px #888;
max-width: 100%;
}
footer {
background: #eee;
width: 100%;
padding: 50px 0;
text-align: right;
border-top: 1px solid #ddd;
}
footer span {
display: block;
margin-right: 30px;
color: #888;
font-size: 12px;
}
#menu {
position: fixed;
font-size: 12px;
overflow-y: auto;
top: 0;
right: 0;
margin: 0;
height: 100%;
padding: 15px 0;
text-align: right;
border-left: 1px solid #eee;
-moz-box-shadow: 0 0 2px #888
, inset 5px 0 20px rgba(0,0,0,.5)
, inset 5px 0 3px rgba(0,0,0,.3);
-webkit-box-shadow: 0 0 2px #888
, inset 5px 0 20px rgba(0,0,0,.5)
, inset 5px 0 3px rgba(0,0,0,.3);
-webkit-font-smoothing: antialiased;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGYAAABmCAMAAAAOARRQAAABelBMVEUjJSU6OzshIyM5OjoqKy02NjgsLS01NTYjJCUzNTUgISMlJSc0NTUvMDA6PDwlJyg1NjYoKis2NjYrLS02ODkpKyw0NDYrLC04ODovLzA4Ojo0NDUtLy86OjwjIyU4OTosLS82ODgtLS8hIyQvMTEnKCooKSsrKy0qLCwkJSUnKCkrLCwpKiwwMjIxMzMqLC0tLS0pKissLC00NTYwMDIwMTQpKysoKSovMDEtLzA2OTkxMzUrKywvLy8qKyszNTY5OzsqKiw6OjswMDExNDUoKiozNDUvMDIyNDY1Njg2Njk5OTozMzU0NjY4ODkiIyUiIyQ4OTkuMDEmKCowMjQwMTErLS4qKywwMTMhIiMpKiopKy0tLjAkJScxNDQvLzExNDYyNDQmKCk5OTslJig5OjskJSYxMzQrLS8gISIwMTIoKCk1NTUlJSUnJygwMDA4ODgiIiMhISI8PDw6Ojo5OTkpKSojIyQ7OzsyMjIpKSssLCw6Ozw1NjlrfLakAAAg2UlEQVR42jR6i3ea6rYvPgANIAhVXh8WvkQlioUiFlFcBtAmoiRNdzxqu9p0J7vrdK29zuPeex77nnvO/35n1r1ndHRktI0jTOacv/l7lCBK5UqVpOha/YxmWK7BC4TQFKVXrbYsnimqxuuMVlOQ0XltWjUdCwRJ1M+tC1KudOs9q6+da2adUewG0SC0SwELfHtgDds93VEuydEbl3QMWeNoYkR7b/0x1ZRobGI3mLwzAhePqTAwhg6aogjNsGy7/jwQ4rkdqe7CWLxF8k9LfMVFyRS7VJqtkrW8Vt/bkR8FZJao16ipknbC3Yw2lM7laO6HBEOadEZ2tpf65c4v8e3u7FyU6qbiNNyCuzXZ6pawgnwgmrpTT/Q7w2EZmiIJ0dzWDI7mhQ80IfRnMu2kzA5r5r1pIFoia+/d93HRYp1GV8TbrkWoU/+jdI0Ff6yGwTjT1Hn8J+8m1rKpGiYPuNiHnMtNMIv+zpsk84MYTNW1/+DpwXLvckdOCMYowVNPREe0QlM8xRHXXFhcNDzupwsSmb5pH+0t0RP2Qk+QtI7F1Qm6JRC6ZPBtPq/dq/kH+jxtCljn9TIpW6rQIgmSVyj6lPICIw4N/taka41PFUInth0je9+jO6Kt1G4/a7V2LEgG02B0pHVuCZrgltSKMuIl5SyufUv9mYuQi+mFgzbBEtFo2g+Dh4sSTrLNu8JPh00sQydpb00tqXBvqRN7Q7kqzcnIxCGnvZt/WmJacoOEO6Dcn8Qre03pOCSQxbMOXUuDNx9SxuLz4W1I18gvjViQ67zV0rxdWL8Te/TQkuo8STS41DR48W7L6YP2uWIqiUV8rd6Gbf/rnegKZeG8TpAM6afhGze9JAOxbLjsnUXEbrZ9vLYd7MT32cPF5mKKxmjy7huaoD9n62GOxni3iIJwv0IzZAZjdZkUtolCNLVfYZNaquFjGszVVf+J0vrz4CawoKdHnOzb0NMH7CDBOybfYNJ4rfeMyFNjkFYVTzMFs87rnPGXLUOeNKRVc0LnU7/UIgelzsy3CMuth0YfvnY0wsD3vODUL3eJcKqHQpm8yM3XZQWJxO6Un9iYloyyLpOwN2obHy6W6gbpcb44XmyC+mg+itAcaprGcrwZCqMj/GmtKn0zPvpTz/Cv1dw21XwP3cRupg3H3MF/S71eTKj1YrdwKdc2Mw0fRmb2sFf8lW3aU6JbIZSEPqvXvjM7G/aApyXlXeqKfMq0g/Su3rUGJPSPrtGElgknrZM3xUXqsAP6zMCNVn5u8aJnSNpJv2uru7t2jfRziW2+GuhqfldUNbPk71olwo+46ePUo1U3WKk/e5YK07F/wGRgcpODmQnIlVeHCWBE4puBi2jq28UKpqiN1/4UOrGz59TNYrrQHtd+11sG40BGD+pXdelNqGOg4NXe8W4eacJV/NS9/2Umtym6WQqveqR9xdCMElpxnbkalM4Vf9uaEcWZaKdyibEIjWKxJZPN95niCL3GiaXyssIrHxoLkqkzLCXULN46/f2h3tQJgyip+Tk9EAjJ9aJshq7t8X45aowSKspMSvPf7r9R8yxNptIaHS5ozuEm6luPDApugyNP8OaqiQ4BjaequXA54SLC83eHIY2r+CZp4409Xqw8Aa2oI7XkCrQi+in0w5AqF/kLNrcUz+qkl/lAobY1jSnx5OJNhyXIz3qfNFlXc0TKaglNwdWkWYt9QQ1Kr6W8zue21iNrdJk+N5oCr2O9nEtWKC7IS5J/zdDEYrmnAYfg6agCy+qcgz7ZofeDc4PbUWSvkshWuAc7OjiUyLkj+RAtdlwXJcjxdpkTTHDhK8lBCi8+JtvDVL1W6elmOM++YS0LuSlaP1oUvAeiW3cFnvTr8EbTz1tsSMYdGeZe40sRWu5uAfj7q+ZoKv2FNQ0p5XY1lmlcigHZqTPpabufEVrNuNPi165w3uCVQJHyJqmSJ7ZHnguqwtCmwViIJijj04ba2JNYtB+yORf5gg1/9t9iw4vUpeqiunSAbf+IBdj/b+iG2qrHvuNP0Vd/+ThVZT/lrvHYjjgDbbyxaqgHNM2uhxa1GW3UedZYhMMwM4mQhltouK+IV4NdbIQNM+8Yv311RZk9kT4tiYR4LkyFcuPpdcjuhUuFqBAWRZa11lcZ3gEBlXywsNhrt+plISZP5DlsV9l4EgY6J3yZPTUcMrgaWAT3oI79eSbGEbcJpr6BD8kyDiVt+G0/hXosQN4NFXKlfWIfsIs0BHODVok1/IGnKFHJYIquh8Xo+2+bkQNTGgWmN/fZ0Y33LSj6lr1GyV7mWIKg7ZTRZPGuhF/zjRNcQ1UPtSYgnWQxSs0yrVhwNDcdGMNSNe2JT3WuzbAM3HykyAajS3Uphf6STKEqxLas9EnmnhA/lyj9Uj+JoY7SVgVmGLl46Rm2u98sbkap2lzAdKBG4r6LgulQOSSjQv1GWdQ0jtDUK/mAaqM1Uqjpu4k3Rvfvxv7YTxLSK+wN3E5jVIzmF23uZ7hiH/sVP49D7tvoKp4S8b1LuvRlivVB/algbhcFITYVXvDpLzpDfplR2uD5V4XJFxpjmIpLc9Y5sB2TpBRix7Bme6GZIq+06v3XzNeTcA4obQIKxrnT4C2JpOqD92dbmSX8MGazly5EsZVMvSU1f4RZwyu8iQXbVdeLlZrjuTT1jrY1uk5c7iZ7RsvhhluqAkq4JpVQAg7RJFtSu+xgJ8Pv6O1j5DkLxT8mkbfyRW5DrQmG7hiDIjCgBsADbjuof6YHLGeV6a5Q1Smx9joUXPpdaaDx97A/Wq00oJkdR7ZYuQRfS533JtxO1erduqWOYIt3wh0wpbLuCNIYkwxbswbikCUu2CDCS+Q+7rgVtfRcm+SOcdKPRlZ/rE7wNVUEE39KTS5uvUKN1PUnkloPkyzhyGQ8qkouEjJ3H/VXdqG6asSRiw3ecMlBvDDt8dDhBHXMwZ2Cajzjr7/76T+IavqPYvz6r7//E/3X3+N//h/0QozbjPgPiir69P/8X3/9F/yv8b/827/++98WItPu5/Hvwd8YPf5bp/2/lX/T/+Of/0MJ/lYTa+L/Ef+d9vN/3/2T6P/+jyTzu/evf6U7vxN7B6pJkRtAF6jUr8I+P8RsP/ptGhfqFk+pQ/DgAy6NJtRYJdXmp4gK7WLqLKJ+MaKhGjOojvL+SnIWrkpy0SLHDe4QuyNzaEA15mLMCcmE8Em+4HdOihW4/ZWuppJEmzeAwcDtv7MuLc9y2V5atvxXNe3S4DUMt5/Qy2LM9kSYKiVWBuKlfp4nxTntpuW03JbIlkiRvBXmT23g1I2OYe6IizUHPIq6zm6mbfsbteKmi/sg9J+ocQBMctGFO7iljo8TPN+z3jxw4do+ZwfqoR9dkNTKHyM305GpTkfhcHexVkPVGEbUOjuo9f0UMPHBFlGEx0SLvJvVRKTwW7PSew5oPme+E42+frJa9cGt2njS3dK5kIif2eYbhuSEQXEqMVfUjhGIuin0G0/W5ezJyJQy3SpMLai4M0JUWb5u1k9tny5bd1pPwYBpQuDCXZl62xg4CdVEAtflXHs6JKmP/pH6mOl796Lgopj0o8d5kKh00hxG3OSdEE/QBo9Hgr8JJqAeLDwJohG5j/DGh61Rc/+tf22/8kEnxHNCEjo0ElvvGfESZkqmz2BDcKV1H1buSkhkdg7p1IMGs2s17nYjpblrWuE2K9WEO/hcRp5e9oOF/QBmOaDtgil+oaU6szPrdwW65fOB0KUTsVUn7LFU7J8e6cxJIl9+FHw5MQMzuQJ+4oxMH3iW/5GK+hWuG0T+gTLs+fAjdtUd58TmIUq04EeyRCYCjkldow234aIgR5bqwrtZosZ+6YEqAmDqatJ9lWasz4IquKALPtd92hGI3Z2BdzzZue+REl1Om4DIWD+RrtUTOJLI+S0jHowXXdAxsGLSd40zYNuEUlOGhrwL6c7tcOtUOvpJCP7QBQS19H+GvZn05ewjlVLz+IGKoC9TyfQjLMBNmXCuqqtTdOSukZW48B0HqgSTCBrBnlFvF4CG2Su7yFzqmJFURK3UmTT3ru050r0ptUpMilYnBJWfl2Bv6kPlUuE1kxxpdzui9AubsR2N2boVSu81OulAwBqoSr1LZ0LLYOomyZHmjqnXlP72s8LnDouEJjtodBvdHaG1jMySYO7crWd90MpCRyCG14vb5IE7Arupw/y/RcCm/Tm3zK6zYj8PYNaGldiUfkB/LHWcmf2lVM+mwyU27a0qq2tscrQ/vzBjN26DnntIrOyGizzXK35yKQdYnUABkyN4saz3WD/viF+eCcsXnIajdWYJWaYHRstIis9CS+tqnFGmz2j5uzfr3Z4prqgK4XOT/PyftvjZqIm8lhkfxJ7Ol3CJF1piYBGAG8wtAk56Drw1YwmOpcz+NdfkSpSLplRXLXHL0Rquj6YW/gabqgK7Dgr6NwtH0B/AN7XrN+MVJ6AmXmUuqmQulrNNYPmH0RoDogydOKLo/QbfYNARSQQKISRCzRXU+q9WWJFL3LZW6u34CkeG97xC0NNGaJ0bvK6SnZS3zPskr5EtuCgjMWR5o2x5BqhKmDWJPRe7JMEOyRb5uUKlHaGVtq5ivSOaSliSXp9SQm2qk8MRJh10MAp9QQ2H5t59J8rjiwSZtoIfMGjlLPVNdYl/LBR0AO6WLGDmkLkIPRE45Y9MftdAK/yNu1Hn6tzOQTesgQ+8fSzB19wO91vCnO23vOWQdwJ63SJrYjdfKFW6W281PKs2k8iT9ai1cgJ4sa3xqdvmtxR8/+D1B8AKc2u+6JftryRhMWSQtoSBgIyyQGyxcnELuAasXN12oSriU4RMz1DD6RL0TSV+om7i1Yt+jEE/jnawM8cX/UhN4nkiv/w9eALrzNhXuQfOzFL0Fi6SjF7/4Qn8rLYBoa85cvgAnkCEBP+HPbEnquVXCZsMS/yzYw2Vru60P/+nJPYKkzZFjmbykzUoEqV836T5q3fP/L383dF82tx18/AZgZczMAgyeWYKmSZIqtHL+e+O4ZRcq9VI3g/qPeCoiK4pcgEqdbS0S/Be54sbVQOuJVPNBblIghzeasNu7h/g+Sz1IdhI5lCwq1nUb3Ji4OCIcqQZqtqJ5w7rXrg/DA9IgVmEGhDgGecEwnCTHffXcXs0V3OCEVzYDKS1vp/oX+ng+6XVU86UjA6FMO2RXOOOrqY1GgPvrAk9HV/BXtCu5RuwF8qgdGDLsBcui4E33ymdBip1X8uKyhIWT8qNRDsXz+gvO9UiEC0d8RG4Tf2x8H4slljgHtCBcxHLTWOYJm5H/fCPCzOgf9qgOUxTRZ0Pc6ha5yLuLVT9ntvIa6gacE99mCovdUumTQdRP4RPsS9129eEe2uSvvGh0bV4Y3QPPhPZMqhZWSMa5R0Hc1SGO4IVOQc0FrirlibTVfKRrYkD8kz3b+X65/QkUNaZdrdl3mCap0Hf3YcCw/LiouJYNbqz88UqeDYv93yO7vvXtgl4XCyAO4ODkY6W+83+LZU//p3/zXNGGrUKClCiOnL27iJZbNWDF02XXAOeFlB7IaADoMH1Yqr+UP9biyZDEa/iJt4MDeIz6GKTdLVBfWGVtRN4fdT2rgReX8UXwF2zOrradm4J0nyTgdPnai3RvzpZvCKDUqjOwD/QA6EDaMCLewX6QWYVnHY1sx1bd8ovYnPm1ZvPH+rE20lWjOCnZ66/xDt0QAl15FjfBcZp+i9OU0RNPQ0t3x2pSNWo8eiYudwsnuP1Hq6iH1LJCJynkYsfgJ0p3pF6SoQk2l+jqE8CPk+ziGJRSKjs+W5AO185umPdkYzlK4wl7TC9NxyyDP7ZoyYVoXiuS6SjnInlLWrwz1i8bGTKXX0AVQWkSfIlglW3zRJRJ8bg5VgE6ZEnqNu9B++0GNQvDQJvFize4ESNKBJP+8vA3LM4AX5SIBq08Mob+7QMTCZx4nwP/64+4BnlZC+8WtlP/CXw6t1PwMwkJ3jhP1FiXLhDF/3I6FGUzO2DSi9ABxKyyL9paZxSEz40ZCPQToDAJu1959k7QdbVxgB4icsu2s4zsTPJhcEDo+N1GX4zSk/wriRh8AqwL62972i9HJHd1ydaLXVzvKvOfGGw5RVcUVMiKXFH4APdkQU/dc5BX0YfKTNZYXCW9mb8bc8mufoQP6BbdQmT99ZjoYfr/go4TgQX9IDgztim7wyFeGMfbNaeqj8Dzs38pgcqwSv2hbqB3oSGKWKy+sesY7p57wAHldqE6NDudk/W7s/zjrK4rZFlFvaGxnSZdHbc1y47qDN6xkoK8O3bfr2j41dlJZ71rB4dlDqapPFa8N6xBrprUdtenUCHwxKNhw1uuTBh+9uU45k4REpQABN2bAO9DSLqoIL26gNroWgup5pUMxHUNSq4Gyz47vBPvilpo5f9OYI2ddAqTqmnxXERxQJ3UK8fHbVE9HagHi3+tqNRoNsArdmAxHA5LwtQo9ZAaNKUTljnokljo2x8scqVpEEIPc01fPCdHOCg0DeWBz8D5TVAAfx8aRH5X2ZYNI3ebKDZdeJ+oBDAxmRqJ30Eh2/DaeAy5diVNMpEDmXiPDsGTzBLXy8eVDdJoIafgx/gxMyQi454QrW56nCyeELgSuNNEmYkflF+t3CZQOVRWjKhIuCclmQSlAXT3+4JGG75B4t/5hQ+ldMP4LsAW6z3XmU6IJJwpnGVnsgUZhoY1fZlwTR8wSU7xRejf2uCx9Z5trVTRRJP9KnEb134dEieil6eCOGWgboI7xsqsqM99jfJLTePjygKlH2CVxxsse9QRzTBFjD/Kjqitr/CCTBt/SJ6nLxz7cKP9pFqBpp0lN5y+adKNsZjrPuroemZauH9aTTFD3EKHW8S55XBLFQAt1jgxTQCTwxmx/JyfsZDN1RroN3VaxpSenpIX7K+ZbL8VdlQDcI4Cbzg3QJLa9yVqNxUelu+EtxLVqeekaAvSJkO6sSVqbUajxqhKshNpvZqoeApF0k/0P0ikkwUcbdwc4A1ejN7Oo0O15kG7hTMoK3hZRBCX7YYeLW0wvcXx/18n/u37yLgzBYVBUvORGli+sfRcX/74uD6P4hq+7xu54TlWJLFzT63uwUDwuEDdOjJQqx7JV+ZjaEAPi7t0MMrR4Q8Rkf18uxD6RK0RKh0hL8YU+DeL97i4pa5ZSyAfXKwZRS/8gXcxdZXm62RBDj8U3sN8x95b5PpPs/mCBKYvpaA50pN5Ct/499AFTtwQ5vgeSh+NHrKIi4NVpwM/XzRaNfJD856lPE6M21zWPguFsH7jbLVyEDfRmt4VwrhCJ5VTYmcSPfGgO5clfN+vbaDZ7sakU5+2vZ2WCDY031NxJarVytfDDVtiafcTGO2rJ/taoL3zChN2qmjxofczTOYQPPVQPh0JVtYgdUQINcSiNEEy58UdYXX1MpWUCEBx7LbcGtAm8XWRQTVOaoV3ySri4RShhs/B/0m4jX6OAwXOvcA09bNSG4czEGv/Wey6V/jbTCNTW6awXdNTcA1GsPe1E9fZdGl7R0vyoVpIdJtfC6d32NNErrvq/R+d65VG+YOwRXppXxOCYyGNSf1K3x6VxAW/vtz4EC1SgCOSPdN62sLsoIzuDfg8GwZAbquVO8HIuFP/ToVoeUB7nnwMF35a1wK1tI6fkrqFKhQdeJpwyls0pIy8AZde3/6LUUbFaYJthyUJSU/kqDXTLQElnn0Jr4B2RVghNrmNmoEn7pXIeshPguXVsvwoTdmClq49JJU3LWhHyWTrJL9bRP6VKv3tZoA/th77p5Jw++OEENvyvWy/pNeExiDUVQaXIRGh8xySZTI36yueFaSXo1uJY0RnXYgEOoWWOJHeaVuX/bGNhHsh2yinznl/++NJcE9j6fBPRcBdq9hb8awNw8U7Bl6GM7x69EDOIIbX/npZ++amlHR9L/35mE/2Ss4gb0xCcY4VyTFLRE796vHysLAamqcyO+aFQyJIDBNslbH2/MrAvZiSEIedc/cqjmv4fbda2pXbv+F5a2szSsdkm9noiNURXt8edUhGUF6fSZWd1IJaXKFwD+49R6eCXD4Bkef7j9tRtNMVgW8BhRz/Qpy1TmeYk0doyjZoJSbePOReVHgkFsCFuQJ+Lgc4BxeAsK/cOiNDRmdNw0ctYhn/nQ498dYI5znzGLoJi1rav7Cn88rL3wLePVtDK5gl77Tki3gHEsIAQ2+IKgarj7Y8W1IQzV5V9N+0TjLqbg68WfKcOmBCOj3JkwJhVIkwDhc+JorXuZEPMEh0vvH3x7iqf+VAwXgd4diZiaJD1zHL9Snx6Wfg4IugreyhabQkcir+y5XgDtdx3Avs7lkeeCBwDvZoTUCXx5QrZkcEqWfYEiEYRs/EphmRALSNGR1Iclgdr5VFoELpzF4++f35w3/j0t5ucW3n2ch4PQCLuUXupsPRR7UA5FjSKrMtPcKAZJfagO4lGE7FH3YKMjorpK0ZxAv+i2JkJhtAMWWWFej4RhPR/cJ3DxwocCvXDi4SGZU4cu+K32XndiFWgopAl+0GApcwf1XvymJcFs39jExIBO4yUjU9MExBLQYc9H+W7+IgdESPRpciT+rKZPebVtaVq+1GYO/5xTAL3HASjNTGIgMvdjWbgc7JvdE1zIFpuC0U9ESiZyzBixzxWxj4Kwh8My34q+FK3KNLtmsA1qyrmKSNQOXCPUZd+ONelBTvFoUI/CYsqa/RhtKiyMf2CgSFqEPk59Y3uqnlZ8gFpswfSYyko23yVZYxzKGxGm49Zqxg1l8oz5Ra9XaRwHkuxepmgyhm0SoNy2KlbcEqK+9QqS9PNx9Ihm9U7gsR55SSJ1FBDNnkuWKxIZ0SDpXuOGwZdoUbOMDPHP4vBAgz2VlSEJAHZGJVbYIg7l/FO5KfIVvxC8pPPxMGcNMoevFDeStt2iqztE10n2TA4dgJH76YS9HDhKHD3iCx6ieFX84BAI3QQnngh76f5ruPQVbr5qZmck/5UjDc26lfrOvUBWy0Ogl8bCoOkMOns81TnC3cuUS9KW8+9A+fe3XYZOFUPG1u5epSSmDLw0s5s2F0W30ANeo+zJkJQz9SPZgzwYpEoktofhGVfmLOAB20boCbW1QWq/NpET/hnMecw/uSyAH4NJc3ECOU4nnkK1fj3S/i5dwb3R7k00AqQQUwt7Ie1qV0aY/VQX0J8hLPy7eBNXMHYZYDNxHZ2Qh6AuXJxq+AeRec/Q+JLhZV6hpXwQEzw7bf5v9uUf2vpq3qlhmy0IIGTkwYdCfSAFmqbdo+3XvDTDjFJde0mbeQLcn2n31xaAqJ0ixO/CLsT4I4G4DoncVTgRGNBtsCcjISWT+oeXZ4Iedw/8OsJI1aPnNKLX/60VvcZb94uasRxCkqlPQ11u1Sa2hHvB80WQENxVyzjns0/PiEByyil21Te6oisk3mNCEMrhouCFO3yEZTHHOCMy9eb/4Tmi8cVf3Lf7P53SY2hX3PSN033As3ETIMLHWumWEO9JXHA2y2SIBlIPpLGG2qvNsCIlIr+B1SWAqRKm2w6Blf7U+zCSBwJrfHG5i8J5Gax/cVonMlon7aHJX/gSvucIncRP93XCqkv7D8IFKFsLiBgHqUpXhE3pYjEcV1dk/JD9zFVCfEaQIVX8Jmfz7IIofcBKQ4OaG+C3xC2veX9CD+iAFXDNaGg9eTVxvkbJRJlW4Nk9Wk13kn696jWppRDe/8pDrYMO9ZyxZ98ReKSz9kWKLLyk2zCZgAniCkLJVX3n1M9DYbomyahWiv/KixRIV9hj/oFz87I+HLznbPTjpa+D+bZQnMuRsljTpv90vQUt/pK7jCFnA30B/jtroSF2/m/gpWn1aQs5WeA6ghzF8SdqWI20fghdSeDOCSCmLgTkfaGgGDmw7nHFkRzGtag57IHS2na06I+gzEphXo1w/Zx2BM/jKL2nZoFjHggtFQjYi8nSVRSXIE58RPbBObXk7uuIL9+rs/5Zo7suJInEUxgsiZZAWS25iBtpEiZeBgDtghEoAE0sjcayNq85M4tbu/LF5h51335PsGzQ09O875+vUS89lkWMyNOFoip2PuyWyMP/iU2XIZdfCCJNDjebDoBLQdpy7QQZC7s9c0wjHJervQNDu2jWzBW5MSAJMr7bP+Iv92BkS/GGgzjEn7MF1IRKFwwzbjbS4/slGOmhx9cZrFu7HSEefojNv3r0UaKfKOWzXsq1zEugbzlMDFsacRJJI/iJlK3vtkZ+PLZIVMFlKA32wbq2Kd5T0uCLZ1CPkAfCdzkz2EYscjDcZq2AWfziN2covN4kXE1lQXPPLTNM1xx3tbiepcO/t3SWm4w87qfh99SL0ZnY+LKFPLPeXVM2mIIoVWt+9Nk0I7nY4O79iGYqxZ8RVz289an6NVdJWnSKZvJQCAuHNiVaDxPAFoH392t9wot5t0/qmU95eEWNbU2udUW5sN9JVqcYlvAIfLeYC33oUzzxZgSktsv21mA7Uly1FA5VnoJFh6N244Wmv3YJGFv/TCPryaw+ZORlpZjQdq/2DYXr3EZskfed0G61P09ipTKmlTQ1067Rg5+PAk5FlQ9e0SWbGf2B/08kqymOTMVOznsALHHNFH4LFRKl2F/NOiYFl9khNHnSu9Ak5sq26Ynl/i2fdTle29Y1ugqmR5Yj4YT9pvslFyYCbw0mNFr5rVQm1LvkG27QMq9ph3t8fmn6r6SQ4oSbr5tz+J1kIawGzDxb6VYOvvWhobDTXfBeNv3b4aNm5XUinsCGqG2q/45m3+LoCOsddFceYhRx1Tsss9PLdPfJdErFMjYd3gddjiP0+XQjcRadZP6bwNLySvunFf20Czy6JqdEW2a96KxdYdOryBv1BjbuUq2yCHeh+6sk7fGmmPi50pe/1l5TyPe5oHW9oPnhPswLyf2TFDdCyYlhwBCstv5C1HwlW7xWoGT9XZt4qVj5WryLPLLD6h/5cMLEjWzgCeAIKNsLak92aBqBsHl4AJwl2N4jfvbSkBExGimv0nFvv09uDScQbjx+w4kPQjgjlW+g9ws9VEJvI2k8N6XxVu0uIwovgTFdunG24gBtaDi+y1YLQwZ8mwbip5fVlO3k0n0AEr/ETbtu8Vjkm+nNSiEb7X/3fMjBL5A8PdgG+/FnbexbFFExmEfetXAnisEKy5z44WVPpQZjSy/jzeGn4yDRsFGqhh87QPaDBWhlo37IFbe/C0xynS91d2tP/AJoJS0sVF6iwAAAAAElFTkSuQmCC");
}
#logo {
position: fixed;
bottom: 10px;
right: 10px;
background: rgba(255,255,255,.1);
font-size: 11px;
display: block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
-webkit-box-shadow: 0 0 3px rgba(0,0,0,.2);
-moz-box-shadow: 0 0 3px rgba(0,0,0,.2);
color: inherit;
}
#menu li a {
display: block;
color: white;
padding: 0 35px 0 25px;
-webkit-transition: background 300ms;
-moz-transition: background 300ms;
}
#menu li {
position: relative;
list-style: none;
}
#menu a:hover,
#menu a.active {
text-decoration: none;
background: rgba(255,255,255,.1);
}
#menu li:hover .cov {
opacity: 1;
}
#menu li .dirname {
opacity: .60;
padding-right: 2px;
}
#menu li .basename {
opacity: 1;
}
#menu .cov {
background: rgba(0,0,0,.4);
position: absolute;
top: 0;
right: 8px;
font-size: 9px;
opacity: .6;
text-align: left;
width: 17px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
padding: 2px 3px;
text-align: center;
}
#stats:nth-child(2n) {
display: inline-block;
margin-top: 15px;
border: 1px solid #eee;
padding: 10px;
-webkit-box-shadow: inset 0 0 2px #eee;
-moz-box-shadow: inset 0 0 2px #eee;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
}
#stats div {
float: left;
padding: 0 5px;
}
#stats::after {
display: block;
content: '';
clear: both;
}
#stats .sloc::after {
content: ' SLOC';
color: #b6b6b6;
}
#stats .percentage::after {
content: ' coverage';
color: #b6b6b6;
}
#stats .hits,
#stats .misses {
display: none;
}
.high {
color: #00d4b4;
}
.medium {
color: #e87d0d;
}
.low {
color: #d4081a;
}
.terrible {
color: #d4081a;
font-weight: bold;
}
table {
width: 80%;
margin-top: 10px;
border-collapse: collapse;
border: 1px solid #cbcbcb;
color: #363636;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
}
table thead {
display: none;
}
table td.line,
table td.hits {
width: 20px;
background: #eaeaea;
text-align: center;
font-size: 11px;
padding: 0 10px;
color: #949494;
}
table td.hits {
width: 10px;
padding: 2px 5px;
color: rgba(0,0,0,.2);
background: #f0f0f0;
}
tr.miss td.line,
tr.miss td.hits {
background: #e6c3c7;
}
tr.miss td {
background: #f8d5d8;
}
td.source {
padding-left: 15px;
line-height: 15px;
white-space: pre;
font: 12px monaco, monospace;
}
code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }
</style></head><body><div id="coverage"><h1 id="overview">Coverage</h1><div id="menu"><li><a href="#overview">overview</a></li><li><span class="cov high">97</span><a href="#csv.js"><span class="basename">csv.js</span></a></li><li><span class="cov high">100</span><a href="#state.js"><span class="basename">state.js</span></a></li><li><span class="cov high">100</span><a href="#options.js"><span class="basename">options.js</span></a></li><li><span class="cov high">91</span><a href="#from.js"><span class="basename">from.js</span></a></li><li><span class="cov high">100</span><a href="#utils.js"><span class="basename">utils.js</span></a></li><li><span class="cov high">86</span><a href="#to.js"><span class="basename">to.js</span></a></li><li><span class="cov high">98</span><a href="#stringifier.js"><span class="basename">stringifier.js</span></a></li><li><span class="cov high">96</span><a href="#parser.js"><span class="basename">parser.js</span></a></li><li><span class="cov high">98</span><a href="#transformer.js"><span class="basename">transformer.js</span></a></li><li><span class="cov high">97</span><a href="#generator.js"><span class="basename">generator.js</span></a></li><a id="logo" href="http://visionmedia.github.com/mocha/">m</a></div><div id="stats" class="high"><div class="percentage">95%</div><div class="sloc">486</div><div class="hits">464</div><div class="misses">22</div></div><div id="files"><div class="file"><h2 id="csv.js">csv.js</h2><div id="stats" class="high"><div class="percentage">97%</div><div class="sloc">68</div><div class="hits">66</div><div class="misses">2</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr><td class="line">2</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">3</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">4</td><td class="hits"></td><td class="source">Node CSV</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source">========</td></tr><tr><td class="line">6</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source">This project provides CSV parsing and has been tested and used </td></tr><tr><td class="line">8</td><td class="hits"></td><td class="source">on a large input file (over 2Gb).</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">10</td><td class="hits"></td><td class="source">* Follow the NodeJs streaming API</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source">* Async and event based</td></tr><tr><td class="line">12</td><td class="hits"></td><td class="source">* Support delimiters, quotes and escape characters</td></tr><tr><td class="line">13</td><td class="hits"></td><td class="source">* Line breaks discovery: detected in source and reported to destination</td></tr><tr><td class="line">14</td><td class="hits"></td><td class="source">* Data transformation</td></tr><tr><td class="line">15</td><td class="hits"></td><td class="source">* Support for large datasets</td></tr><tr><td class="line">16</td><td class="hits"></td><td class="source">* Complete test coverage as sample and inspiration</td></tr><tr><td class="line">17</td><td class="hits"></td><td class="source">* no external dependencies</td></tr><tr><td class="line">18</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">19</td><td class="hits"></td><td class="source">Important, this documentation cover the current version of the node </td></tr><tr><td class="line">20</td><td class="hits"></td><td class="source">csv parser. The documentation for the current version 0.1.0 is </td></tr><tr><td class="line">21</td><td class="hits"></td><td class="source">available [here](https://github.com/wdavidw/node-csv-parser/tree/v0.1).</td></tr><tr><td class="line">22</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">23</td><td class="hits"></td><td class="source">Quick example</td></tr><tr><td class="line">24</td><td class="hits"></td><td class="source">-------------</td></tr><tr><td class="line">25</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">26</td><td class="hits"></td><td class="source"> // node samples/string.js</td></tr><tr><td class="line">27</td><td class="hits"></td><td class="source"> var csv = require('csv');</td></tr><tr><td class="line">28</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">29</td><td class="hits"></td><td class="source"> .from( '&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;\n&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;' )</td></tr><tr><td class="line">30</td><td class="hits"></td><td class="source"> .to( console.log )</td></tr><tr><td class="line">31</td><td class="hits"></td><td class="source"> // Output:</td></tr><tr><td class="line">32</td><td class="hits"></td><td class="source"> // 1,2,3,4</td></tr><tr><td class="line">33</td><td class="hits"></td><td class="source"> // a,b,c,d</td></tr><tr><td class="line">34</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">35</td><td class="hits"></td><td class="source">Advanced example</td></tr><tr><td class="line">36</td><td class="hits"></td><td class="source">----------------</td></tr><tr><td class="line">37</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">38</td><td class="hits"></td><td class="source">The following example illustrates 4 usages of the library:</td></tr><tr><td class="line">39</td><td class="hits"></td><td class="source">1. Plug a readable stream by defining a file path</td></tr><tr><td class="line">40</td><td class="hits"></td><td class="source">2. Direct output to a file path</td></tr><tr><td class="line">41</td><td class="hits"></td><td class="source">3. Transform the data (optional)</td></tr><tr><td class="line">42</td><td class="hits"></td><td class="source">4. Listen to events (optional)</td></tr><tr><td class="line">43</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">44</td><td class="hits"></td><td class="source"> // node samples/sample.js</td></tr><tr><td class="line">45</td><td class="hits"></td><td class="source"> var csv = require('csv');</td></tr><tr><td class="line">46</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">47</td><td class="hits"></td><td class="source"> .from.stream(fs.createReadStream(__dirname+'/sample.in')</td></tr><tr><td class="line">48</td><td class="hits"></td><td class="source"> .to.path(__dirname+'/sample.out')</td></tr><tr><td class="line">49</td><td class="hits"></td><td class="source"> .transform( function(data){</td></tr><tr><td class="line">50</td><td class="hits"></td><td class="source"> data.unshift(data.pop());</td></tr><tr><td class="line">51</td><td class="hits"></td><td class="source"> return data;</td></tr><tr><td class="line">52</td><td class="hits"></td><td class="source"> })</td></tr><tr><td class="line">53</td><td class="hits"></td><td class="source"> .on('record', function(data,index){</td></tr><tr><td class="line">54</td><td class="hits"></td><td class="source"> console.log('#'+index+' '+JSON.stringify(data));</td></tr><tr><td class="line">55</td><td class="hits"></td><td class="source"> })</td></tr><tr><td class="line">56</td><td class="hits"></td><td class="source"> .on('end', function(count){</td></tr><tr><td class="line">57</td><td class="hits"></td><td class="source"> console.log('Number of lines: '+count);</td></tr><tr><td class="line">58</td><td class="hits"></td><td class="source"> })</td></tr><tr><td class="line">59</td><td class="hits"></td><td class="source"> .on('error', function(error){</td></tr><tr><td class="line">60</td><td class="hits"></td><td class="source"> console.log(error.message);</td></tr><tr><td class="line">61</td><td class="hits"></td><td class="source"> });</td></tr><tr><td class="line">62</td><td class="hits"></td><td class="source"> // Output:</td></tr><tr><td class="line">63</td><td class="hits"></td><td class="source"> // #0 [&quot;2000-01-01&quot;,&quot;20322051544&quot;,&quot;1979.0&quot;,&quot;8.8017226E7&quot;,&quot;ABC&quot;,&quot;45&quot;]</td></tr><tr><td class="line">64</td><td class="hits"></td><td class="source"> // #1 [&quot;2050-11-27&quot;,&quot;28392898392&quot;,&quot;1974.0&quot;,&quot;8.8392926E7&quot;,&quot;DEF&quot;,&quot;23&quot;]</td></tr><tr><td class="line">65</td><td class="hits"></td><td class="source"> // Number of lines: 2</td></tr><tr><td class="line">66</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">67</td><td class="hits"></td><td class="source">Pipe example</td></tr><tr><td class="line">68</td><td class="hits"></td><td class="source">------------</td></tr><tr><td class="line">69</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">70</td><td class="hits"></td><td class="source">The module follow a Stream architecture. At it's core, the parser and </td></tr><tr><td class="line">71</td><td class="hits"></td><td class="source">the stringifier utilities provide a [Stream Writer][writable_stream] </td></tr><tr><td class="line">72</td><td class="hits"></td><td class="source">and a [Stream Reader][readable_stream] implementation available in the CSV API.</td></tr><tr><td class="line">73</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">74</td><td class="hits"></td><td class="source"> |-----------| |---------|---------| |---------|</td></tr><tr><td class="line">75</td><td class="hits"></td><td class="source"> | | | | | | |</td></tr><tr><td class="line">76</td><td class="hits"></td><td class="source"> | | | CSV | | |</td></tr><tr><td class="line">77</td><td class="hits"></td><td class="source"> | | | | | | |</td></tr><tr><td class="line">78</td><td class="hits"></td><td class="source"> | Stream | | Writer | Reader | | Stream |</td></tr><tr><td class="line">79</td><td class="hits"></td><td class="source"> | Reader |.pipe(| API | API |).pipe(| Writer |)</td></tr><tr><td class="line">80</td><td class="hits"></td><td class="source"> | | | | | | |</td></tr><tr><td class="line">81</td><td class="hits"></td><td class="source"> | | | | | | |</td></tr><tr><td class="line">82</td><td class="hits"></td><td class="source"> |-----------| |---------|---------| |---------|</td></tr><tr><td class="line">83</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">84</td><td class="hits"></td><td class="source">Here's a quick example:</td></tr><tr><td class="line">85</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">86</td><td class="hits"></td><td class="source"> in = fs.createReadStream('./in')</td></tr><tr><td class="line">87</td><td class="hits"></td><td class="source"> out = fs.createWriteStream('./out')</td></tr><tr><td class="line">88</td><td class="hits"></td><td class="source"> in.pipe(csv()).pipe(out)</td></tr><tr><td class="line">89</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">90</td><td class="hits"></td><td class="source">Installing</td></tr><tr><td class="line">91</td><td class="hits"></td><td class="source">----------</td></tr><tr><td class="line">92</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">93</td><td class="hits"></td><td class="source">Via [npm](http://github.com/isaacs/npm):</td></tr><tr><td class="line">94</td><td class="hits"></td><td class="source">```bash</td></tr><tr><td class="line">95</td><td class="hits"></td><td class="source">npm install csv</td></tr><tr><td class="line">96</td><td class="hits"></td><td class="source">```</td></tr><tr><td class="line">97</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">98</td><td class="hits"></td><td class="source">Via git (or downloaded tarball):</td></tr><tr><td class="line">99</td><td class="hits"></td><td class="source">```bash</td></tr><tr><td class="line">100</td><td class="hits"></td><td class="source">git clone http://github.com/wdavidw/node-csv-parser.git</td></tr><tr><td class="line">101</td><td class="hits"></td><td class="source">```</td></tr><tr><td class="line">102</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">103</td><td class="hits"></td><td class="source">Events</td></tr><tr><td class="line">104</td><td class="hits"></td><td class="source">------</td></tr><tr><td class="line">105</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">106</td><td class="hits"></td><td class="source">The library extends Node [EventEmitter][event] class and emit all</td></tr><tr><td class="line">107</td><td class="hits"></td><td class="source">the events of the Writable and Readable [Stream API][stream]. Additionally, the useful &quot;records&quot; event </td></tr><tr><td class="line">108</td><td class="hits"></td><td class="source">is emitted.</td></tr><tr><td class="line">109</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">110</td><td class="hits"></td><td class="source">* *record* </td></tr><tr><td class="line">111</td><td class="hits"></td><td class="source"> Emitted by the stringifier when a new row is parsed and transformed. The data is </td></tr><tr><td class="line">112</td><td class="hits"></td><td class="source"> the value returned by the user `transform` callback if any. Note however that the event won't </td></tr><tr><td class="line">113</td><td class="hits"></td><td class="source"> be called if transform return `null` since the record is skipped.</td></tr><tr><td class="line">114</td><td class="hits"></td><td class="source"> The callback provides two arguments. `data` is the CSV line being processed (an array or an object)</td></tr><tr><td class="line">115</td><td class="hits"></td><td class="source"> and `index` is the index number of the line starting at zero</td></tr><tr><td class="line">116</td><td class="hits"></td><td class="source">* *data* </td></tr><tr><td class="line">117</td><td class="hits"></td><td class="source"> Emitted by the stringifier on each line once the data has been transformed and stringified.</td></tr><tr><td class="line">118</td><td class="hits"></td><td class="source">* *drain* </td></tr><tr><td class="line">119</td><td class="hits"></td><td class="source">* *end* </td></tr><tr><td class="line">120</td><td class="hits"></td><td class="source"> Emitted when the CSV content has been parsed.</td></tr><tr><td class="line">121</td><td class="hits"></td><td class="source">* *close* </td></tr><tr><td class="line">122</td><td class="hits"></td><td class="source"> Emitted when the underlying resource has been closed. For example, when writting to a file with `csv().to.path()`, the event will be called once the writing process is complete and the file closed.</td></tr><tr><td class="line">123</td><td class="hits"></td><td class="source">* *error* </td></tr><tr><td class="line">124</td><td class="hits"></td><td class="source"> Thrown whenever an error occured.</td></tr><tr><td class="line">125</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">126</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">127</td><td class="hits">1</td><td class="source">var CSV, from, options, parser, state, stream, stringifier, to, transformer;</td></tr><tr><td class="line">128</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">129</td><td class="hits">1</td><td class="source">stream = require('stream');</td></tr><tr><td class="line">130</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">131</td><td class="hits">1</td><td class="source">state = require('./state');</td></tr><tr><td class="line">132</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">133</td><td class="hits">1</td><td class="source">options = require('./options');</td></tr><tr><td class="line">134</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">135</td><td class="hits">1</td><td class="source">from = require('./from');</td></tr><tr><td class="line">136</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">137</td><td class="hits">1</td><td class="source">to = require('./to');</td></tr><tr><td class="line">138</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">139</td><td class="hits">1</td><td class="source">stringifier = require('./stringifier');</td></tr><tr><td class="line">140</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">141</td><td class="hits">1</td><td class="source">parser = require('./parser');</td></tr><tr><td class="line">142</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">143</td><td class="hits">1</td><td class="source">transformer = require('./transformer');</td></tr><tr><td class="line">144</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">145</td><td class="hits">1</td><td class="source">CSV = function() {</td></tr><tr class="hit"> <td class="line">146</td><td class="hits">78</td><td class="source"> this.paused = false;</td></tr><tr class="hit"> <td class="line">147</td><td class="hits">78</td><td class="source"> this.readable = true;</td></tr><tr class="hit"> <td class="line">148</td><td class="hits">78</td><td class="source"> this.writable = true;</td></tr><tr class="hit"> <td class="line">149</td><td class="hits">78</td><td class="source"> this.state = state();</td></tr><tr class="hit"> <td class="line">150</td><td class="hits">78</td><td class="source"> this.options = options();</td></tr><tr class="hit"> <td class="line">151</td><td class="hits">78</td><td class="source"> this.from = from(this);</td></tr><tr class="hit"> <td class="line">152</td><td class="hits">78</td><td class="source"> this.to = to(this);</td></tr><tr class="hit"> <td class="line">153</td><td class="hits">78</td><td class="source"> this.parser = parser(this);</td></tr><tr class="hit"> <td class="line">154</td><td class="hits">78</td><td class="source"> this.parser.on('row', (function(row) {</td></tr><tr class="hit"> <td class="line">155</td><td class="hits">27450</td><td class="source"> return this.transformer.transform(row);</td></tr><tr><td class="line">156</td><td class="hits"></td><td class="source"> }).bind(this));</td></tr><tr class="hit"> <td class="line">157</td><td class="hits">78</td><td class="source"> this.parser.on('end', (function() {</td></tr><tr class="hit"> <td class="line">158</td><td class="hits">73</td><td class="source"> return this.transformer.end();</td></tr><tr><td class="line">159</td><td class="hits"></td><td class="source"> }).bind(this));</td></tr><tr class="hit"> <td class="line">160</td><td class="hits">78</td><td class="source"> this.parser.on('error', (function(e) {</td></tr><tr class="hit"> <td class="line">161</td><td class="hits">3</td><td class="source"> return this.error(e);</td></tr><tr><td class="line">162</td><td class="hits"></td><td class="source"> }).bind(this));</td></tr><tr class="hit"> <td class="line">163</td><td class="hits">78</td><td class="source"> this.stringifier = stringifier(this);</td></tr><tr class="hit"> <td class="line">164</td><td class="hits">78</td><td class="source"> this.transformer = transformer(this);</td></tr><tr class="hit"> <td class="line">165</td><td class="hits">78</td><td class="source"> this.transformer.on('end', (function() {</td></tr><tr class="hit"> <td class="line">166</td><td class="hits">73</td><td class="source"> return this.emit('end', this.state.count);</td></tr><tr><td class="line">167</td><td class="hits"></td><td class="source"> }).bind(this));</td></tr><tr class="hit"> <td class="line">168</td><td class="hits">78</td><td class="source"> return this;</td></tr><tr><td class="line">169</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">170</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">171</td><td class="hits">1</td><td class="source">CSV.prototype.__proto__ = stream.prototype;</td></tr><tr><td class="line">172</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">173</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">174</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">175</td><td class="hits"></td><td class="source">`pause()`</td></tr><tr><td class="line">176</td><td class="hits"></td><td class="source">---------</td></tr><tr><td class="line">177</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">178</td><td class="hits"></td><td class="source">Implementation of the Readable Stream API, requesting that no further data </td></tr><tr><td class="line">179</td><td class="hits"></td><td class="source">be sent until resume() is called.</td></tr><tr><td class="line">180</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">181</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">182</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">183</td><td class="hits">1</td><td class="source">CSV.prototype.pause = function() {</td></tr><tr class="hit"> <td class="line">184</td><td class="hits">19463</td><td class="source"> return this.paused = true;</td></tr><tr><td class="line">185</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">186</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">187</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">188</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">189</td><td class="hits"></td><td class="source">`resume()`</td></tr><tr><td class="line">190</td><td class="hits"></td><td class="source">----------</td></tr><tr><td class="line">191</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">192</td><td class="hits"></td><td class="source">Implementation of the Readable Stream API, resuming the incoming 'data' </td></tr><tr><td class="line">193</td><td class="hits"></td><td class="source">events after a pause().</td></tr><tr><td class="line">194</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">195</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">196</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">197</td><td class="hits">1</td><td class="source">CSV.prototype.resume = function() {</td></tr><tr class="hit"> <td class="line">198</td><td class="hits">15346</td><td class="source"> this.paused = false;</td></tr><tr class="hit"> <td class="line">199</td><td class="hits">15346</td><td class="source"> return this.emit('drain');</td></tr><tr><td class="line">200</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">201</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">202</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">203</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">204</td><td class="hits"></td><td class="source">`write(data, [preserve])`</td></tr><tr><td class="line">205</td><td class="hits"></td><td class="source">-------------------------</td></tr><tr><td class="line">206</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">207</td><td class="hits"></td><td class="source">Implementation of the Writable Stream API with a larger signature. Data</td></tr><tr><td class="line">208</td><td class="hits"></td><td class="source">may be a string, a buffer, an array or an object.</td></tr><tr><td class="line">209</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">210</td><td class="hits"></td><td class="source">If data is a string or a buffer, it could span multiple lines. If data </td></tr><tr><td class="line">211</td><td class="hits"></td><td class="source">is an object or an array, it must represent a single line.</td></tr><tr><td class="line">212</td><td class="hits"></td><td class="source">Preserve is for line which are not considered as CSV data.</td></tr><tr><td class="line">213</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">214</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">215</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">216</td><td class="hits">1</td><td class="source">CSV.prototype.write = function(data, preserve) {</td></tr><tr class="hit"> <td class="line">217</td><td class="hits">29461</td><td class="source"> var csv;</td></tr><tr class="hit"> <td class="line">218</td><td class="hits">29461</td><td class="source"> if (!this.writable) {</td></tr><tr class="miss"> <td class="line">219</td><td class="hits">0</td><td class="source"> return false;</td></tr><tr><td class="line">220</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">221</td><td class="hits">29461</td><td class="source"> if (data instanceof Buffer) {</td></tr><tr class="hit"> <td class="line">222</td><td class="hits">26286</td><td class="source"> data = data.toString();</td></tr><tr><td class="line">223</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">224</td><td class="hits">29461</td><td class="source"> if (typeof data === 'string' &amp;&amp; !preserve) {</td></tr><tr class="hit"> <td class="line">225</td><td class="hits">26420</td><td class="source"> this.parser.parse(data);</td></tr><tr class="hit"> <td class="line">226</td><td class="hits">3041</td><td class="source"> } else if (Array.isArray(data) &amp;&amp; !this.state.transforming) {</td></tr><tr class="hit"> <td class="line">227</td><td class="hits">2020</td><td class="source"> csv = this;</td></tr><tr class="hit"> <td class="line">228</td><td class="hits">2020</td><td class="source"> this.transformer.transform(data);</td></tr><tr><td class="line">229</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">230</td><td class="hits">1021</td><td class="source"> if (preserve || this.state.transforming) {</td></tr><tr class="hit"> <td class="line">231</td><td class="hits">9</td><td class="source"> this.stringifier.write(data, preserve);</td></tr><tr><td class="line">232</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">233</td><td class="hits">1012</td><td class="source"> this.transformer.transform(data);</td></tr><tr><td class="line">234</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">235</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">236</td><td class="hits">29461</td><td class="source"> return !this.paused;</td></tr><tr><td class="line">237</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">238</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">239</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">240</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">241</td><td class="hits"></td><td class="source">`end()`</td></tr><tr><td class="line">242</td><td class="hits"></td><td class="source">-------</td></tr><tr><td class="line">243</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">244</td><td class="hits"></td><td class="source">Terminate the parsing. Call this method when no more csv data is </td></tr><tr><td class="line">245</td><td class="hits"></td><td class="source">to be parsed. It implement the StreamWriter API by setting the `writable` </td></tr><tr><td class="line">246</td><td class="hits"></td><td class="source">property to &quot;false&quot; and emitting the `end` event.</td></tr><tr><td class="line">247</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">248</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">249</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">250</td><td class="hits">1</td><td class="source">CSV.prototype.end = function() {</td></tr><tr class="hit"> <td class="line">251</td><td class="hits">75</td><td class="source"> if (!this.writable) {</td></tr><tr class="hit"> <td class="line">252</td><td class="hits">1</td><td class="source"> return;</td></tr><tr><td class="line">253</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">254</td><td class="hits">74</td><td class="source"> this.readable = false;</td></tr><tr class="hit"> <td class="line">255</td><td class="hits">74</td><td class="source"> this.writable = false;</td></tr><tr class="hit"> <td class="line">256</td><td class="hits">74</td><td class="source"> return this.parser.end();</td></tr><tr><td class="line">257</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">258</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">259</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">260</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">261</td><td class="hits"></td><td class="source">`transform(callback)`</td></tr><tr><td class="line">262</td><td class="hits"></td><td class="source">---------------------</td></tr><tr><td class="line">263</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">264</td><td class="hits"></td><td class="source">Register the transformer callback. The callback is a user provided </td></tr><tr><td class="line">265</td><td class="hits"></td><td class="source">function call on each line to filter, enrich or modify the </td></tr><tr><td class="line">266</td><td class="hits"></td><td class="source">dataset. More information in the &quot;transforming data&quot; section.</td></tr><tr><td class="line">267</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">268</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">269</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">270</td><td class="hits">1</td><td class="source">CSV.prototype.transform = function(callback) {</td></tr><tr class="hit"> <td class="line">271</td><td class="hits">31</td><td class="source"> this.transformer.callback = callback;</td></tr><tr class="hit"> <td class="line">272</td><td class="hits">31</td><td class="source"> return this;</td></tr><tr><td class="line">273</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">274</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">275</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">276</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">277</td><td class="hits"></td><td class="source">`error(error)`</td></tr><tr><td class="line">278</td><td class="hits"></td><td class="source">--------------</td></tr><tr><td class="line">279</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">280</td><td class="hits"></td><td class="source">Unified mechanism to handle error, emit the error and mark the </td></tr><tr><td class="line">281</td><td class="hits"></td><td class="source">stream as non readable and non writable.</td></tr><tr><td class="line">282</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">283</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">284</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">285</td><td class="hits">1</td><td class="source">CSV.prototype.error = function(e) {</td></tr><tr class="hit"> <td class="line">286</td><td class="hits">5</td><td class="source"> this.readable = false;</td></tr><tr class="hit"> <td class="line">287</td><td class="hits">5</td><td class="source"> this.writable = false;</td></tr><tr class="hit"> <td class="line">288</td><td class="hits">5</td><td class="source"> this.emit('error', e);</td></tr><tr class="hit"> <td class="line">289</td><td class="hits">5</td><td class="source"> if (this.readStream) {</td></tr><tr class="miss"> <td class="line">290</td><td class="hits">0</td><td class="source"> this.readStream.destroy();</td></tr><tr><td class="line">291</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">292</td><td class="hits">5</td><td class="source"> return this;</td></tr><tr><td class="line">293</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">294</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">295</td><td class="hits">1</td><td class="source">module.exports = function() {</td></tr><tr class="hit"> <td class="line">296</td><td class="hits">78</td><td class="source"> return new CSV;</td></tr><tr><td class="line">297</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">298</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">299</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">300</td><td class="hits"></td><td class="source">[event]: http://nodejs.org/api/events.html</td></tr><tr><td class="line">301</td><td class="hits"></td><td class="source">[stream]: http://nodejs.org/api/stream.html</td></tr><tr><td class="line">302</td><td class="hits"></td><td class="source">[writable_stream]: http://nodejs.org/api/stream.html#stream_writable_stream</td></tr><tr><td class="line">303</td><td class="hits"></td><td class="source">[readable_stream]: http://nodejs.org/api/stream.html#stream_readable_stream</td></tr><tr><td class="line">304</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">305</td><td class="hits"></td><td class="source"> </td></tr></tbody></table></div><div class="file"><h2 id="state.js">state.js</h2><div id="stats" class="high"><div class="percentage">100%</div><div class="sloc">2</div><div class="hits">2</div><div class="misses">0</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr><td class="line">2</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">3</td><td class="hits">1</td><td class="source">module.exports = function() {</td></tr><tr class="hit"> <td class="line">4</td><td class="hits">78</td><td class="source"> return {</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source"> count: 0,</td></tr><tr><td class="line">6</td><td class="hits"></td><td class="source"> field: '',</td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source"> line: [],</td></tr><tr><td class="line">8</td><td class="hits"></td><td class="source"> lastC: '',</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source"> countWriten: 0,</td></tr><tr><td class="line">10</td><td class="hits"></td><td class="source"> transforming: 0</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">12</td><td class="hits"></td><td class="source">};</td></tr></tbody></table></div><div class="file"><h2 id="options.js">options.js</h2><div id="stats" class="high"><div class="percentage">100%</div><div class="sloc">2</div><div class="hits">2</div><div class="misses">0</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr><td class="line">2</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">3</td><td class="hits"></td><td class="source">Input and output options</td></tr><tr><td class="line">4</td><td class="hits"></td><td class="source">========================</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">6</td><td class="hits"></td><td class="source">The `options` property provide access to the `from` and `to` object used to store options. This</td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source">property is for internal usage and could be considered private. It is recommanded to use</td></tr><tr><td class="line">8</td><td class="hits"></td><td class="source">the `from.options()` and `to.options()` to access those objects.</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">10</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">11</td><td class="hits">1</td><td class="source">module.exports = function() {</td></tr><tr class="hit"> <td class="line">12</td><td class="hits">78</td><td class="source"> return {</td></tr><tr><td class="line">13</td><td class="hits"></td><td class="source"> from: {</td></tr><tr><td class="line">14</td><td class="hits"></td><td class="source"> delimiter: ',',</td></tr><tr><td class="line">15</td><td class="hits"></td><td class="source"> quote: '&quot;',</td></tr><tr><td class="line">16</td><td class="hits"></td><td class="source"> escape: '&quot;',</td></tr><tr><td class="line">17</td><td class="hits"></td><td class="source"> columns: null,</td></tr><tr><td class="line">18</td><td class="hits"></td><td class="source"> flags: 'r',</td></tr><tr><td class="line">19</td><td class="hits"></td><td class="source"> encoding: 'utf8',</td></tr><tr><td class="line">20</td><td class="hits"></td><td class="source"> trim: false,</td></tr><tr><td class="line">21</td><td class="hits"></td><td class="source"> ltrim: false,</td></tr><tr><td class="line">22</td><td class="hits"></td><td class="source"> rtrim: false</td></tr><tr><td class="line">23</td><td class="hits"></td><td class="source"> },</td></tr><tr><td class="line">24</td><td class="hits"></td><td class="source"> to: {</td></tr><tr><td class="line">25</td><td class="hits"></td><td class="source"> delimiter: null,</td></tr><tr><td class="line">26</td><td class="hits"></td><td class="source"> quote: null,</td></tr><tr><td class="line">27</td><td class="hits"></td><td class="source"> quoted: false,</td></tr><tr><td class="line">28</td><td class="hits"></td><td class="source"> escape: null,</td></tr><tr><td class="line">29</td><td class="hits"></td><td class="source"> columns: null,</td></tr><tr><td class="line">30</td><td class="hits"></td><td class="source"> header: false,</td></tr><tr><td class="line">31</td><td class="hits"></td><td class="source"> lineBreaks: null,</td></tr><tr><td class="line">32</td><td class="hits"></td><td class="source"> flags: 'w',</td></tr><tr><td class="line">33</td><td class="hits"></td><td class="source"> encoding: 'utf8',</td></tr><tr><td class="line">34</td><td class="hits"></td><td class="source"> newColumns: false,</td></tr><tr><td class="line">35</td><td class="hits"></td><td class="source"> end: true</td></tr><tr><td class="line">36</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">37</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">38</td><td class="hits"></td><td class="source">};</td></tr></tbody></table></div><div class="file"><h2 id="from.js">from.js</h2><div id="stats" class="high"><div class="percentage">91%</div><div class="sloc">60</div><div class="hits">55</div><div class="misses">5</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr class="hit"> <td class="line">2</td><td class="hits">1</td><td class="source">var Stream, fs, path, utils, _ref;</td></tr><tr><td class="line">3</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">4</td><td class="hits">1</td><td class="source">fs = require('fs');</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">6</td><td class="hits">1</td><td class="source">path = require('path');</td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">8</td><td class="hits">1</td><td class="source">if ((_ref = fs.exists) == null) {</td></tr><tr class="miss"> <td class="line">9</td><td class="hits">0</td><td class="source"> fs.exists = path.exists;</td></tr><tr><td class="line">10</td><td class="hits"></td><td class="source">}</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">12</td><td class="hits">1</td><td class="source">utils = require('./utils');</td></tr><tr><td class="line">13</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">14</td><td class="hits">1</td><td class="source">Stream = require('stream');</td></tr><tr><td class="line">15</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">16</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">17</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">18</td><td class="hits"></td><td class="source">Reading data from a source</td></tr><tr><td class="line">19</td><td class="hits"></td><td class="source">==========================</td></tr><tr><td class="line">20</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">21</td><td class="hits"></td><td class="source">The `csv().from` property provides functions to read from an external </td></tr><tr><td class="line">22</td><td class="hits"></td><td class="source">source and write to a CSV instance. The source may be a string, a file, </td></tr><tr><td class="line">23</td><td class="hits"></td><td class="source">a buffer or a readable stream. </td></tr><tr><td class="line">24</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">25</td><td class="hits"></td><td class="source">You may call the `from` function or one of its sub function. For example, </td></tr><tr><td class="line">26</td><td class="hits"></td><td class="source">here are two identical ways to read from a file:</td></tr><tr><td class="line">27</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">28</td><td class="hits"></td><td class="source"> csv.from('/tmp/data.csv').on('data', console.log);</td></tr><tr><td class="line">29</td><td class="hits"></td><td class="source"> csv.from.path('/tmp/data.csv').on('data', console.log);</td></tr><tr><td class="line">30</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">31</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">32</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">33</td><td class="hits">1</td><td class="source">module.exports = function(csv) {</td></tr><tr><td class="line">34</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">35</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">36</td><td class="hits"></td><td class="source"> `from(mixed)`</td></tr><tr><td class="line">37</td><td class="hits"></td><td class="source"> -------------</td></tr><tr><td class="line">38</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">39</td><td class="hits"></td><td class="source"> Read from any sort of source. It should be considered as a convenient function which </td></tr><tr><td class="line">40</td><td class="hits"></td><td class="source"> will discover the nature of the data source to parse. </td></tr><tr><td class="line">41</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">42</td><td class="hits"></td><td class="source"> If it is a string, then if check if it match an existing file path and read the file content, </td></tr><tr><td class="line">43</td><td class="hits"></td><td class="source"> otherwise, it treat the string as csv data. If it is an instance of stream, it consider the</td></tr><tr><td class="line">44</td><td class="hits"></td><td class="source"> object to be an input stream. If is an array, then for each line should correspond a record.</td></tr><tr><td class="line">45</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">46</td><td class="hits"></td><td class="source"> Here's some examples on how to use this function:</td></tr><tr><td class="line">47</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">48</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">49</td><td class="hits"></td><td class="source"> .from('&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;\n&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;')</td></tr><tr><td class="line">50</td><td class="hits"></td><td class="source"> .on('end', function(){ console.log('done') })</td></tr><tr><td class="line">51</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">52</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">53</td><td class="hits"></td><td class="source"> .from('./path/to/file.csv')</td></tr><tr><td class="line">54</td><td class="hits"></td><td class="source"> .on('end', function(){ console.log('done') })</td></tr><tr><td class="line">55</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">56</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">57</td><td class="hits"></td><td class="source"> .from(fs.createReadStream('./path/to/file.csv'))</td></tr><tr><td class="line">58</td><td class="hits"></td><td class="source"> .on('end', function(){ console.log('done') })</td></tr><tr><td class="line">59</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">60</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">61</td><td class="hits"></td><td class="source"> .from(['&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;,&quot;5&quot;',['1','2','3','4','5']])</td></tr><tr><td class="line">62</td><td class="hits"></td><td class="source"> .on('end', function(){ console.log('done') })</td></tr><tr><td class="line">63</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">64</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">65</td><td class="hits">78</td><td class="source"> var from;</td></tr><tr class="hit"> <td class="line">66</td><td class="hits">78</td><td class="source"> from = function(mixed, options) {</td></tr><tr class="hit"> <td class="line">67</td><td class="hits">13</td><td class="source"> var error;</td></tr><tr class="hit"> <td class="line">68</td><td class="hits">13</td><td class="source"> error = false;</td></tr><tr class="hit"> <td class="line">69</td><td class="hits">13</td><td class="source"> switch (typeof mixed) {</td></tr><tr><td class="line">70</td><td class="hits"></td><td class="source"> case 'string':</td></tr><tr class="hit"> <td class="line">71</td><td class="hits">9</td><td class="source"> fs.exists(mixed, function(exists) {</td></tr><tr class="hit"> <td class="line">72</td><td class="hits">9</td><td class="source"> if (exists) {</td></tr><tr class="hit"> <td class="line">73</td><td class="hits">1</td><td class="source"> return from.path(mixed, options);</td></tr><tr><td class="line">74</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">75</td><td class="hits">8</td><td class="source"> return from.string(mixed, options);</td></tr><tr><td class="line">76</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">77</td><td class="hits"></td><td class="source"> });</td></tr><tr class="hit"> <td class="line">78</td><td class="hits">9</td><td class="source"> break;</td></tr><tr><td class="line">79</td><td class="hits"></td><td class="source"> case 'object':</td></tr><tr class="hit"> <td class="line">80</td><td class="hits">4</td><td class="source"> if (Array.isArray(mixed)) {</td></tr><tr class="hit"> <td class="line">81</td><td class="hits">3</td><td class="source"> from.array(mixed, options);</td></tr><tr><td class="line">82</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">83</td><td class="hits">1</td><td class="source"> if (mixed instanceof Stream) {</td></tr><tr class="hit"> <td class="line">84</td><td class="hits">1</td><td class="source"> from.stream(mixed, options);</td></tr><tr><td class="line">85</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="miss"> <td class="line">86</td><td class="hits">0</td><td class="source"> error = true;</td></tr><tr><td class="line">87</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">88</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">89</td><td class="hits">4</td><td class="source"> break;</td></tr><tr><td class="line">90</td><td class="hits"></td><td class="source"> default:</td></tr><tr class="miss"> <td class="line">91</td><td class="hits">0</td><td class="source"> error = true;</td></tr><tr><td class="line">92</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">93</td><td class="hits">13</td><td class="source"> if (error) {</td></tr><tr class="miss"> <td class="line">94</td><td class="hits">0</td><td class="source"> csv.error(new Error(&quot;Invalid mixed argument in from&quot;));</td></tr><tr><td class="line">95</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">96</td><td class="hits">13</td><td class="source"> return csv;</td></tr><tr><td class="line">97</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">98</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">99</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">100</td><td class="hits"></td><td class="source"> `from.options([options])`</td></tr><tr><td class="line">101</td><td class="hits"></td><td class="source"> -------------------------</td></tr><tr><td class="line">102</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">103</td><td class="hits"></td><td class="source"> Update and retrieve options relative to the input source. Return </td></tr><tr><td class="line">104</td><td class="hits"></td><td class="source"> the options as an object if no argument is provided.</td></tr><tr><td class="line">105</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">106</td><td class="hits"></td><td class="source"> * `delimiter` Set the field delimiter, one character only, defaults to comma.</td></tr><tr><td class="line">107</td><td class="hits"></td><td class="source"> * `quote` Set the field delimiter, one character only, defaults to double quotes.</td></tr><tr><td class="line">108</td><td class="hits"></td><td class="source"> * `escape` Set the field delimiter, one character only, defaults to double quotes.</td></tr><tr><td class="line">109</td><td class="hits"></td><td class="source"> * `columns` List of fields or true if autodiscovered in the first CSV line, default to null. Impact the `transform` argument and the `data` event by providing an object instead of an array, order matters, see the transform and the columns sections for more details.</td></tr><tr><td class="line">110</td><td class="hits"></td><td class="source"> * `flags` Used to read a file stream, default to the r charactere.</td></tr><tr><td class="line">111</td><td class="hits"></td><td class="source"> * `encoding` Encoding of the read stream, defaults to 'utf8', applied when a readable stream is created.</td></tr><tr><td class="line">112</td><td class="hits"></td><td class="source"> * `trim` If true, ignore whitespace immediately around the delimiter, defaults to false.</td></tr><tr><td class="line">113</td><td class="hits"></td><td class="source"> * `ltrim` If true, ignore whitespace immediately following the delimiter (i.e. left-trim all fields), defaults to false.</td></tr><tr><td class="line">114</td><td class="hits"></td><td class="source"> * `rtrim` If true, ignore whitespace immediately preceding the delimiter (i.e. right-trim all fields), defaults to false.</td></tr><tr><td class="line">115</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">116</td><td class="hits"></td><td class="source"> Additionnaly, in case you are working with stream, you can pass all </td></tr><tr><td class="line">117</td><td class="hits"></td><td class="source"> the options accepted by the `stream.pipe` function.</td></tr><tr><td class="line">118</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">119</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">120</td><td class="hits">78</td><td class="source"> from.options = function(options) {</td></tr><tr class="hit"> <td class="line">121</td><td class="hits">191</td><td class="source"> if (options != null) {</td></tr><tr class="hit"> <td class="line">122</td><td class="hits">24</td><td class="source"> utils.merge(csv.options.from, options);</td></tr><tr class="hit"> <td class="line">123</td><td class="hits">24</td><td class="source"> return csv;</td></tr><tr><td class="line">124</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">125</td><td class="hits">167</td><td class="source"> return csv.options.from;</td></tr><tr><td class="line">126</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">127</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">128</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">129</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">130</td><td class="hits"></td><td class="source"> `from.array(data, [options])`</td></tr><tr><td class="line">131</td><td class="hits"></td><td class="source"> ------------------------------</td></tr><tr><td class="line">132</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">133</td><td class="hits"></td><td class="source"> Read from an array. Take an array as first argument and optionally </td></tr><tr><td class="line">134</td><td class="hits"></td><td class="source"> some options as a second argument. Each element of the array </td></tr><tr><td class="line">135</td><td class="hits"></td><td class="source"> represents a csv record. Those elements may be a string, a buffer, an </td></tr><tr><td class="line">136</td><td class="hits"></td><td class="source"> array or an object.</td></tr><tr><td class="line">137</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">138</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">139</td><td class="hits">78</td><td class="source"> from.array = function(data, options) {</td></tr><tr class="hit"> <td class="line">140</td><td class="hits">8</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">141</td><td class="hits">8</td><td class="source"> process.nextTick(function() {</td></tr><tr class="hit"> <td class="line">142</td><td class="hits">8</td><td class="source"> var record, _i, _len;</td></tr><tr class="hit"> <td class="line">143</td><td class="hits">8</td><td class="source"> for (_i = 0, _len = data.length; _i &lt; _len; _i++) {</td></tr><tr class="hit"> <td class="line">144</td><td class="hits">17</td><td class="source"> record = data[_i];</td></tr><tr class="hit"> <td class="line">145</td><td class="hits">17</td><td class="source"> csv.write(record);</td></tr><tr><td class="line">146</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">147</td><td class="hits">8</td><td class="source"> return csv.end();</td></tr><tr><td class="line">148</td><td class="hits"></td><td class="source"> });</td></tr><tr class="hit"> <td class="line">149</td><td class="hits">8</td><td class="source"> return csv;</td></tr><tr><td class="line">150</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">151</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">152</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">153</td><td class="hits"></td><td class="source"> `from.string(data, [options])`</td></tr><tr><td class="line">154</td><td class="hits"></td><td class="source"> -------------------------------</td></tr><tr><td class="line">155</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">156</td><td class="hits"></td><td class="source"> Read from a string or a buffer. Take a string as first argument and </td></tr><tr><td class="line">157</td><td class="hits"></td><td class="source"> optionally an object of options as a second argument. The string </td></tr><tr><td class="line">158</td><td class="hits"></td><td class="source"> must be the complete csv data, look at the streaming alternative if your </td></tr><tr><td class="line">159</td><td class="hits"></td><td class="source"> CSV is large.</td></tr><tr><td class="line">160</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">161</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">162</td><td class="hits"></td><td class="source"> .from( '&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;\n&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;' )</td></tr><tr><td class="line">163</td><td class="hits"></td><td class="source"> .to( function(data){} )</td></tr><tr><td class="line">164</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">165</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">166</td><td class="hits">78</td><td class="source"> from.string = function(data, options) {</td></tr><tr class="hit"> <td class="line">167</td><td class="hits">17</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">168</td><td class="hits">17</td><td class="source"> process.nextTick(function() {</td></tr><tr class="hit"> <td class="line">169</td><td class="hits">17</td><td class="source"> csv.write(data);</td></tr><tr class="hit"> <td class="line">170</td><td class="hits">17</td><td class="source"> return csv.end();</td></tr><tr><td class="line">171</td><td class="hits"></td><td class="source"> });</td></tr><tr class="hit"> <td class="line">172</td><td class="hits">17</td><td class="source"> return csv;</td></tr><tr><td class="line">173</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">174</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">175</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">176</td><td class="hits"></td><td class="source"> `from.path(path, [options])`</td></tr><tr><td class="line">177</td><td class="hits"></td><td class="source"> ----------------------------</td></tr><tr><td class="line">178</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">179</td><td class="hits"></td><td class="source"> Read from a file path. Take a file path as first argument and optionally an object </td></tr><tr><td class="line">180</td><td class="hits"></td><td class="source"> of options as a second argument.</td></tr><tr><td class="line">181</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">182</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">183</td><td class="hits">78</td><td class="source"> from.path = function(path, options) {</td></tr><tr class="hit"> <td class="line">184</td><td class="hits">40</td><td class="source"> var stream;</td></tr><tr class="hit"> <td class="line">185</td><td class="hits">40</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">186</td><td class="hits">40</td><td class="source"> stream = fs.createReadStream(path, csv.from.options());</td></tr><tr class="hit"> <td class="line">187</td><td class="hits">40</td><td class="source"> return csv.from.stream(stream);</td></tr><tr><td class="line">188</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">189</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">190</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">191</td><td class="hits"></td><td class="source"> `from.stream(stream, [options])`</td></tr><tr><td class="line">192</td><td class="hits"></td><td class="source"> --------------------------------</td></tr><tr><td class="line">193</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">194</td><td class="hits"></td><td class="source"> Read from a stream. Take a readable stream as first argument and optionally </td></tr><tr><td class="line">195</td><td class="hits"></td><td class="source"> an object of options as a second argument.</td></tr><tr><td class="line">196</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">197</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">198</td><td class="hits">78</td><td class="source"> from.stream = function(stream, options) {</td></tr><tr class="hit"> <td class="line">199</td><td class="hits">43</td><td class="source"> if (options) {</td></tr><tr class="miss"> <td class="line">200</td><td class="hits">0</td><td class="source"> this.options(options);</td></tr><tr><td class="line">201</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">202</td><td class="hits">43</td><td class="source"> stream.setEncoding(csv.from.options().encoding);</td></tr><tr class="hit"> <td class="line">203</td><td class="hits">43</td><td class="source"> stream.pipe(csv, csv.from.options());</td></tr><tr class="hit"> <td class="line">204</td><td class="hits">43</td><td class="source"> return csv;</td></tr><tr><td class="line">205</td><td class="hits"></td><td class="source"> };</td></tr><tr class="hit"> <td class="line">206</td><td class="hits">78</td><td class="source"> return from;</td></tr><tr><td class="line">207</td><td class="hits"></td><td class="source">};</td></tr></tbody></table></div><div class="file"><h2 id="utils.js">utils.js</h2><div id="stats" class="high"><div class="percentage">100%</div><div class="sloc">6</div><div class="hits">6</div><div class="misses">0</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr><td class="line">2</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">3</td><td class="hits">1</td><td class="source">module.exports = {</td></tr><tr><td class="line">4</td><td class="hits"></td><td class="source"> merge: function(obj1, obj2) {</td></tr><tr class="hit"> <td class="line">5</td><td class="hits">94</td><td class="source"> var key, r;</td></tr><tr class="hit"> <td class="line">6</td><td class="hits">94</td><td class="source"> r = obj1 || {};</td></tr><tr class="hit"> <td class="line">7</td><td class="hits">94</td><td class="source"> for (key in obj2) {</td></tr><tr class="hit"> <td class="line">8</td><td class="hits">618</td><td class="source"> r[key] = obj2[key];</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">10</td><td class="hits">94</td><td class="source"> return r;</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">12</td><td class="hits"></td><td class="source">};</td></tr></tbody></table></div><div class="file"><h2 id="to.js">to.js</h2><div id="stats" class="high"><div class="percentage">86%</div><div class="sloc">67</div><div class="hits">58</div><div class="misses">9</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr class="hit"> <td class="line">2</td><td class="hits">1</td><td class="source">var Stream, fs, utils;</td></tr><tr><td class="line">3</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">4</td><td class="hits">1</td><td class="source">fs = require('fs');</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">6</td><td class="hits">1</td><td class="source">Stream = require('stream');</td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">8</td><td class="hits">1</td><td class="source">utils = require('./utils');</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">10</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">12</td><td class="hits"></td><td class="source">Writing data to a destination</td></tr><tr><td class="line">13</td><td class="hits"></td><td class="source">=============================</td></tr><tr><td class="line">14</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">15</td><td class="hits"></td><td class="source">The `csv().to` property provides functions to read from a CSV instance and</td></tr><tr><td class="line">16</td><td class="hits"></td><td class="source">to write to an external destination. The destination may be a stream, a file</td></tr><tr><td class="line">17</td><td class="hits"></td><td class="source">or a callback. </td></tr><tr><td class="line">18</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">19</td><td class="hits"></td><td class="source">You may call the `to` function or one of its sub function. For example, </td></tr><tr><td class="line">20</td><td class="hits"></td><td class="source">here are two identical ways to write to a file:</td></tr><tr><td class="line">21</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">22</td><td class="hits"></td><td class="source"> csv.from(data).to('/tmp/data.csv');</td></tr><tr><td class="line">23</td><td class="hits"></td><td class="source"> csv.from(data).to.path('/tmp/data.csv');</td></tr><tr><td class="line">24</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">25</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">26</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">27</td><td class="hits">1</td><td class="source">module.exports = function(csv) {</td></tr><tr><td class="line">28</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">29</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">30</td><td class="hits"></td><td class="source"> `to(mixed)`</td></tr><tr><td class="line">31</td><td class="hits"></td><td class="source"> -----------</td></tr><tr><td class="line">32</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">33</td><td class="hits"></td><td class="source"> Write from any sort of destination. It should be considered as a convenient function </td></tr><tr><td class="line">34</td><td class="hits"></td><td class="source"> which will discover the nature of the destination where to write the CSV data. </td></tr><tr><td class="line">35</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">36</td><td class="hits"></td><td class="source"> If is an function, then the csv will be provided as the first argument </td></tr><tr><td class="line">37</td><td class="hits"></td><td class="source"> of the callback. If it is a string, then it is expected to be a </td></tr><tr><td class="line">38</td><td class="hits"></td><td class="source"> file path. If it is an instance of stream, it consider the object to be an </td></tr><tr><td class="line">39</td><td class="hits"></td><td class="source"> output stream. </td></tr><tr><td class="line">40</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">41</td><td class="hits"></td><td class="source"> Here's some examples on how to use this function:</td></tr><tr><td class="line">42</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">43</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">44</td><td class="hits"></td><td class="source"> .from('&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;,&quot;5&quot;')</td></tr><tr><td class="line">45</td><td class="hits"></td><td class="source"> .to(function(data){ console.log(data) })</td></tr><tr><td class="line">46</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">47</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">48</td><td class="hits"></td><td class="source"> .from('&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;,&quot;5&quot;')</td></tr><tr><td class="line">49</td><td class="hits"></td><td class="source"> .to('./path/to/file.csv')</td></tr><tr><td class="line">50</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">51</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">52</td><td class="hits"></td><td class="source"> .from('&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;,&quot;5&quot;')</td></tr><tr><td class="line">53</td><td class="hits"></td><td class="source"> .to(fs.createWriteStream('./path/to/file.csv'))</td></tr><tr><td class="line">54</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">55</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">56</td><td class="hits">78</td><td class="source"> var to;</td></tr><tr class="hit"> <td class="line">57</td><td class="hits">78</td><td class="source"> to = function(mixed, options) {</td></tr><tr class="hit"> <td class="line">58</td><td class="hits">13</td><td class="source"> var error;</td></tr><tr class="hit"> <td class="line">59</td><td class="hits">13</td><td class="source"> error = false;</td></tr><tr class="hit"> <td class="line">60</td><td class="hits">13</td><td class="source"> switch (typeof mixed) {</td></tr><tr><td class="line">61</td><td class="hits"></td><td class="source"> case 'string':</td></tr><tr class="hit"> <td class="line">62</td><td class="hits">1</td><td class="source"> to.path(mixed, options);</td></tr><tr class="hit"> <td class="line">63</td><td class="hits">1</td><td class="source"> break;</td></tr><tr><td class="line">64</td><td class="hits"></td><td class="source"> case 'object':</td></tr><tr class="miss"> <td class="line">65</td><td class="hits">0</td><td class="source"> if (mixed instanceof Stream) {</td></tr><tr class="miss"> <td class="line">66</td><td class="hits">0</td><td class="source"> to.stream(mixed, options);</td></tr><tr><td class="line">67</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="miss"> <td class="line">68</td><td class="hits">0</td><td class="source"> error = true;</td></tr><tr><td class="line">69</td><td class="hits"></td><td class="source"> }</td></tr><tr class="miss"> <td class="line">70</td><td class="hits">0</td><td class="source"> break;</td></tr><tr><td class="line">71</td><td class="hits"></td><td class="source"> case 'function':</td></tr><tr class="hit"> <td class="line">72</td><td class="hits">12</td><td class="source"> to.string(mixed, options);</td></tr><tr class="hit"> <td class="line">73</td><td class="hits">12</td><td class="source"> break;</td></tr><tr><td class="line">74</td><td class="hits"></td><td class="source"> default:</td></tr><tr class="miss"> <td class="line">75</td><td class="hits">0</td><td class="source"> error = true;</td></tr><tr><td class="line">76</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">77</td><td class="hits">13</td><td class="source"> if (error) {</td></tr><tr class="miss"> <td class="line">78</td><td class="hits">0</td><td class="source"> csv.error(new Error(&quot;Invalid mixed argument in from&quot;));</td></tr><tr><td class="line">79</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">80</td><td class="hits">13</td><td class="source"> return csv;</td></tr><tr><td class="line">81</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">82</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">83</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">84</td><td class="hits"></td><td class="source"> `to.options([options])`</td></tr><tr><td class="line">85</td><td class="hits"></td><td class="source"> -----------------------</td></tr><tr><td class="line">86</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">87</td><td class="hits"></td><td class="source"> Update and retrieve options relative to the output. Return the options </td></tr><tr><td class="line">88</td><td class="hits"></td><td class="source"> as an object if no argument is provided.</td></tr><tr><td class="line">89</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">90</td><td class="hits"></td><td class="source"> * `delimiter` Set the field delimiter, one character only, defaults to `options.from.delimiter` which is a comma.</td></tr><tr><td class="line">91</td><td class="hits"></td><td class="source"> * `quote` Defaults to the quote read option.</td></tr><tr><td class="line">92</td><td class="hits"></td><td class="source"> * `quoted` Boolean, default to false, quote all the fields even if not required.</td></tr><tr><td class="line">93</td><td class="hits"></td><td class="source"> * `escape` Defaults to the escape read option.</td></tr><tr><td class="line">94</td><td class="hits"></td><td class="source"> * `columns` List of fields, applied when `transform` returns an object, order matters, see the transform and the columns sections below.</td></tr><tr><td class="line">95</td><td class="hits"></td><td class="source"> * `header` Display the column names on the first line if the columns option is provided.</td></tr><tr><td class="line">96</td><td class="hits"></td><td class="source"> * `lineBreaks` String used to delimit record rows or a special value; special values are 'auto', 'unix', 'mac', 'windows', 'unicode'; defaults to 'auto' (discovered in source or 'unix' if no source is specified).</td></tr><tr><td class="line">97</td><td class="hits"></td><td class="source"> * `flags` Defaults to 'w', 'w' to create or overwrite an file, 'a' to append to a file. Applied when using the `toPath` method.</td></tr><tr><td class="line">98</td><td class="hits"></td><td class="source"> * `newColumns` If the `columns` option is not specified (which means columns will be taken from the reader options, will automatically append new columns if they are added during `transform()`.</td></tr><tr><td class="line">99</td><td class="hits"></td><td class="source"> * `end` Prevent calling `end` on the destination, so that destination is no longer writable, similar to passing `{end: false}` option in `stream.pipe()`.</td></tr><tr><td class="line">100</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">101</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">102</td><td class="hits">78</td><td class="source"> to.options = function(options) {</td></tr><tr class="hit"> <td class="line">103</td><td class="hits">116</td><td class="source"> if (options != null) {</td></tr><tr class="hit"> <td class="line">104</td><td class="hits">19</td><td class="source"> utils.merge(csv.options.to, options);</td></tr><tr class="hit"> <td class="line">105</td><td class="hits">19</td><td class="source"> return csv;</td></tr><tr><td class="line">106</td><td class="hits"></td><td class="source"> } else {</td></tr><tr class="hit"> <td class="line">107</td><td class="hits">97</td><td class="source"> return csv.options.to;</td></tr><tr><td class="line">108</td><td class="hits"></td><td class="source"> }</td></tr><tr><td class="line">109</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">110</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">111</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">112</td><td class="hits"></td><td class="source"> `to.string(callback, [options])`</td></tr><tr><td class="line">113</td><td class="hits"></td><td class="source"> ------------------------------</td></tr><tr><td class="line">114</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">115</td><td class="hits"></td><td class="source"> Provide the output string to a callback.</td></tr><tr><td class="line">116</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">117</td><td class="hits"></td><td class="source"> csv()</td></tr><tr><td class="line">118</td><td class="hits"></td><td class="source"> .from( '&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;\n&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;' )</td></tr><tr><td class="line">119</td><td class="hits"></td><td class="source"> .to( function(data, count){} )</td></tr><tr><td class="line">120</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">121</td><td class="hits"></td><td class="source"> Callback is called with 2 arguments:</td></tr><tr><td class="line">122</td><td class="hits"></td><td class="source"> * data Stringify CSV string</td></tr><tr><td class="line">123</td><td class="hits"></td><td class="source"> * count Number of stringified records</td></tr><tr><td class="line">124</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">125</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">126</td><td class="hits">78</td><td class="source"> to.string = function(callback, options) {</td></tr><tr class="hit"> <td class="line">127</td><td class="hits">13</td><td class="source"> var data, stream;</td></tr><tr class="hit"> <td class="line">128</td><td class="hits">13</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">129</td><td class="hits">13</td><td class="source"> data = '';</td></tr><tr class="hit"> <td class="line">130</td><td class="hits">13</td><td class="source"> stream = new Stream;</td></tr><tr class="hit"> <td class="line">131</td><td class="hits">13</td><td class="source"> stream.writable = true;</td></tr><tr class="hit"> <td class="line">132</td><td class="hits">13</td><td class="source"> stream.write = function(d) {</td></tr><tr class="hit"> <td class="line">133</td><td class="hits">26</td><td class="source"> data += d;</td></tr><tr class="hit"> <td class="line">134</td><td class="hits">26</td><td class="source"> return true;</td></tr><tr><td class="line">135</td><td class="hits"></td><td class="source"> };</td></tr><tr class="hit"> <td class="line">136</td><td class="hits">13</td><td class="source"> stream.end = function() {</td></tr><tr class="hit"> <td class="line">137</td><td class="hits">13</td><td class="source"> return callback(data, csv.state.countWriten);</td></tr><tr><td class="line">138</td><td class="hits"></td><td class="source"> };</td></tr><tr class="hit"> <td class="line">139</td><td class="hits">13</td><td class="source"> csv.pipe(stream);</td></tr><tr class="hit"> <td class="line">140</td><td class="hits">13</td><td class="source"> return csv;</td></tr><tr><td class="line">141</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">142</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">143</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">144</td><td class="hits"></td><td class="source"> `to.stream(stream, [options])`</td></tr><tr><td class="line">145</td><td class="hits"></td><td class="source"> ------------------------------</td></tr><tr><td class="line">146</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">147</td><td class="hits"></td><td class="source"> Write to a stream. Take a writable stream as first argument and </td></tr><tr><td class="line">148</td><td class="hits"></td><td class="source"> optionally an object of options as a second argument.</td></tr><tr><td class="line">149</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">150</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">151</td><td class="hits">78</td><td class="source"> to.stream = function(stream, options) {</td></tr><tr class="hit"> <td class="line">152</td><td class="hits">52</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">153</td><td class="hits">52</td><td class="source"> switch (csv.options.to.lineBreaks) {</td></tr><tr><td class="line">154</td><td class="hits"></td><td class="source"> case 'auto':</td></tr><tr class="miss"> <td class="line">155</td><td class="hits">0</td><td class="source"> csv.options.to.lineBreaks = null;</td></tr><tr class="miss"> <td class="line">156</td><td class="hits">0</td><td class="source"> break;</td></tr><tr><td class="line">157</td><td class="hits"></td><td class="source"> case 'unix':</td></tr><tr class="hit"> <td class="line">158</td><td class="hits">2</td><td class="source"> csv.options.to.lineBreaks = &quot;\n&quot;;</td></tr><tr class="hit"> <td class="line">159</td><td class="hits">2</td><td class="source"> break;</td></tr><tr><td class="line">160</td><td class="hits"></td><td class="source"> case 'mac':</td></tr><tr class="hit"> <td class="line">161</td><td class="hits">1</td><td class="source"> csv.options.to.lineBreaks = &quot;\r&quot;;</td></tr><tr class="hit"> <td class="line">162</td><td class="hits">1</td><td class="source"> break;</td></tr><tr><td class="line">163</td><td class="hits"></td><td class="source"> case 'windows':</td></tr><tr class="hit"> <td class="line">164</td><td class="hits">1</td><td class="source"> csv.options.to.lineBreaks = &quot;\r\n&quot;;</td></tr><tr class="hit"> <td class="line">165</td><td class="hits">1</td><td class="source"> break;</td></tr><tr><td class="line">166</td><td class="hits"></td><td class="source"> case 'unicode':</td></tr><tr class="hit"> <td class="line">167</td><td class="hits">1</td><td class="source"> csv.options.to.lineBreaks = &quot;\u2028&quot;;</td></tr><tr><td class="line">168</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">169</td><td class="hits">52</td><td class="source"> csv.pipe(stream);</td></tr><tr class="hit"> <td class="line">170</td><td class="hits">52</td><td class="source"> stream.on('error', function(e) {</td></tr><tr class="miss"> <td class="line">171</td><td class="hits">0</td><td class="source"> return csv.error(e);</td></tr><tr><td class="line">172</td><td class="hits"></td><td class="source"> });</td></tr><tr class="hit"> <td class="line">173</td><td class="hits">52</td><td class="source"> stream.on('close', function() {</td></tr><tr class="hit"> <td class="line">174</td><td class="hits">47</td><td class="source"> return csv.emit('close', csv.state.count);</td></tr><tr><td class="line">175</td><td class="hits"></td><td class="source"> });</td></tr><tr class="hit"> <td class="line">176</td><td class="hits">52</td><td class="source"> return csv;</td></tr><tr><td class="line">177</td><td class="hits"></td><td class="source"> };</td></tr><tr><td class="line">178</td><td class="hits"></td><td class="source"> /*</td></tr><tr><td class="line">179</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">180</td><td class="hits"></td><td class="source"> `to.path(path, [options])`</td></tr><tr><td class="line">181</td><td class="hits"></td><td class="source"> --------------------------</td></tr><tr><td class="line">182</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">183</td><td class="hits"></td><td class="source"> Write to a path. Take a file path as first argument and optionally an object of </td></tr><tr><td class="line">184</td><td class="hits"></td><td class="source"> options as a second argument. The `close` event is sent after the file is written. </td></tr><tr><td class="line">185</td><td class="hits"></td><td class="source"> Relying on the `end` event is incorrect because it is sent when parsing is done </td></tr><tr><td class="line">186</td><td class="hits"></td><td class="source"> but before the file is written.</td></tr><tr><td class="line">187</td><td class="hits"></td><td class="source"> */</td></tr><tr><td class="line">188</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">189</td><td class="hits">78</td><td class="source"> to.path = function(path, options) {</td></tr><tr class="hit"> <td class="line">190</td><td class="hits">51</td><td class="source"> var stream;</td></tr><tr class="hit"> <td class="line">191</td><td class="hits">51</td><td class="source"> this.options(options);</td></tr><tr class="hit"> <td class="line">192</td><td class="hits">51</td><td class="source"> options = utils.merge({}, csv.options.to);</td></tr><tr class="hit"> <td class="line">193</td><td class="hits">51</td><td class="source"> delete options.end;</td></tr><tr class="hit"> <td class="line">194</td><td class="hits">51</td><td class="source"> stream = fs.createWriteStream(path, options);</td></tr><tr class="hit"> <td class="line">195</td><td class="hits">51</td><td class="source"> csv.to.stream(stream, null);</td></tr><tr class="hit"> <td class="line">196</td><td class="hits">51</td><td class="source"> return csv;</td></tr><tr><td class="line">197</td><td class="hits"></td><td class="source"> };</td></tr><tr class="hit"> <td class="line">198</td><td class="hits">78</td><td class="source"> return to;</td></tr><tr><td class="line">199</td><td class="hits"></td><td class="source">};</td></tr></tbody></table></div><div class="file"><h2 id="stringifier.js">stringifier.js</h2><div id="stats" class="high"><div class="percentage">98%</div><div class="sloc">67</div><div class="hits">66</div><div class="misses">1</div></div><table id="source"><thead><tr><th>Line</th><th>Hits</th><th>Source</th></tr></thead><tbody><tr><td class="line">1</td><td class="hits"></td><td class="source">// Generated by CoffeeScript 1.3.3</td></tr><tr><td class="line">2</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">3</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">4</td><td class="hits"></td><td class="source">Stringifier</td></tr><tr><td class="line">5</td><td class="hits"></td><td class="source">===========</td></tr><tr><td class="line">6</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">7</td><td class="hits"></td><td class="source">Convert an array or an object into a CSV line.</td></tr><tr><td class="line">8</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">9</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">10</td><td class="hits">1</td><td class="source">var Stringifier;</td></tr><tr><td class="line">11</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">12</td><td class="hits">1</td><td class="source">Stringifier = function(csv) {</td></tr><tr class="hit"> <td class="line">13</td><td class="hits">78</td><td class="source"> this.csv = csv;</td></tr><tr class="hit"> <td class="line">14</td><td class="hits">78</td><td class="source"> return this;</td></tr><tr><td class="line">15</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">16</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">17</td><td class="hits"></td><td class="source">/*</td></tr><tr><td class="line">18</td><td class="hits"></td><td class="source">Write a line to the written stream. Line may be an object, an array or a string</td></tr><tr><td class="line">19</td><td class="hits"></td><td class="source">The `preserve` argument is for line which are not considered as CSV data.</td></tr><tr><td class="line">20</td><td class="hits"></td><td class="source">*/</td></tr><tr><td class="line">21</td><td class="hits"></td><td class="source"> </td></tr><tr><td class="line">22</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">23</td><td class="hits">1</td><td class="source">Stringifier.prototype.write = function(line, preserve) {</td></tr><tr class="hit"> <td class="line">24</td><td class="hits">30491</td><td class="source"> if (typeof line === 'undefined' || line === null) {</td></tr><tr class="hit"> <td class="line">25</td><td class="hits">9</td><td class="source"> return;</td></tr><tr><td class="line">26</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">27</td><td class="hits">30482</td><td class="source"> if (!preserve) {</td></tr><tr class="hit"> <td class="line">28</td><td class="hits">30475</td><td class="source"> try {</td></tr><tr class="hit"> <td class="line">29</td><td class="hits">30475</td><td class="source"> this.csv.emit('record', line, this.csv.state.count - 1);</td></tr><tr><td class="line">30</td><td class="hits"></td><td class="source"> } catch (e) {</td></tr><tr class="hit"> <td class="line">31</td><td class="hits">1</td><td class="source"> return this.csv.error(e);</td></tr><tr><td class="line">32</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">33</td><td class="hits">30474</td><td class="source"> line = this.csv.stringifier.stringify(line);</td></tr><tr><td class="line">34</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">35</td><td class="hits">30481</td><td class="source"> this.csv.emit('data', line);</td></tr><tr class="hit"> <td class="line">36</td><td class="hits">30481</td><td class="source"> if (!preserve) {</td></tr><tr class="hit"> <td class="line">37</td><td class="hits">30474</td><td class="source"> this.csv.state.countWriten++;</td></tr><tr><td class="line">38</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">39</td><td class="hits">30481</td><td class="source"> return true;</td></tr><tr><td class="line">40</td><td class="hits"></td><td class="source">};</td></tr><tr><td class="line">41</td><td class="hits"></td><td class="source"> </td></tr><tr class="hit"> <td class="line">42</td><td class="hits">1</td><td class="source">Stringifier.prototype.stringify = function(line) {</td></tr><tr class="hit"> <td class="line">43</td><td class="hits">30474</td><td class="source"> var column, columns, containsLinebreak, containsQuote, containsdelimiter, delimiter, escape, field, i, newLine, quote, regexp, _i, _j, _line, _ref, _ref1;</td></tr><tr class="hit"> <td class="line">44</td><td class="hits">30474</td><td class="source"> columns = this.csv.options.to.columns || this.csv.options.from.columns;</td></tr><tr class="hit"> <td class="line">45</td><td class="hits">30474</td><td class="source"> if (typeof columns === 'object' &amp;&amp; columns !== null &amp;&amp; !Array.isArray(columns)) {</td></tr><tr class="hit"> <td class="line">46</td><td class="hits">8</td><td class="source"> columns = Object.keys(columns);</td></tr><tr><td class="line">47</td><td class="hits"></td><td class="source"> }</td></tr><tr class="hit"> <td class="line">48</td><td class="hits">30474</td><td class="source"> delimiter = this.csv.options.to.delimiter || this.csv.options.from.delimiter;</td></tr><tr class="hit"> <td class="line">49</td><td class="hits">30474</td><td class="source"> quote = this.csv.options.to.quote || this.csv.options.from.quote;</td></tr><tr class="hit"> <td class="line">50</td><td class="hits">30474</td><td class="source"> escape = this.csv.options.to.escape || this.csv.options.from.escape;</td></tr><tr class="hit"> <td class="line">51</td><td class="hits">30474</td><td class="source"> if (typeof line === 'object') {</td></tr><tr class="hit"> <td class="line">52</td><td class="hits">29469</td
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

@Illvili
Copy link

Illvili commented Mar 6, 2014

额 这么多文件为什么不创建repository

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment