Skip to content

Commit af5a3d4

Browse files
committed
issue #48: add USDT DTrace support via node-dtrace-provider
1 parent 432eec6 commit af5a3d4

File tree

3 files changed

+161
-24
lines changed

3 files changed

+161
-24
lines changed

README.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,99 @@ This example emits:
517517
v: 0 } ]
518518

519519

520+
# DTrace support
521+
522+
On systems that support DTrace (e.g., MacOS, FreeBSD, illumos derivatives
523+
like SmartOS and OmniOS), Bunyan will create a DTrace provider (`bunyan`)
524+
that makes available the following probes:
525+
526+
log-trace
527+
log-debug
528+
log-info
529+
log-warn
530+
log-error
531+
log-fatal
532+
533+
Each of these probes has a single arugment: the string that would be
534+
written to the log. Note that when a probe is enabled, it will
535+
fire whenever the corresponding function is called, even if the level of
536+
the log message is less than that of any stream.
537+
538+
## DTrace examples
539+
540+
Trace all log messages coming from any Bunyan module on the system:
541+
542+
dtrace -qn 'bunyan*:::log-*{printf("%d: %s: %s", pid, probefunc, copyinstr(arg0))}'
543+
544+
Trace all log messages coming from the "wuzzle" component:
545+
546+
dtrace -qn 'bunyan*:::log-*/strstr(this->str = copyinstr(arg0), "\"component\":\"wuzzle\"") != NULL/{printf("%s", this->str)}'
547+
548+
Aggregate debug messages from process 1234, by message:
549+
550+
dtrace -n 'bunyan1234:::log-debug{@[copyinstr(arg0)] = count()}'
551+
552+
On systems that support the `jstack` action via a node.js helper, get
553+
a stack backtrace for any debug message that includes the string
554+
"danger!":
555+
556+
dtrace -qn 'log-debug/strstr(copyinstr(arg0), "danger!") != NULL/{printf("\n%s", copyinstr(arg0)); jstack()}'
557+
558+
Output of the above might be:
559+
560+
{"name":"foo","hostname":"763bf293-d65c-42d5-872b-4abe25d5c4c7.local","pid":12747,"level":20,"msg":"danger!","time":"2012-10-30T18:28:57.115Z","v":0}
561+
562+
node`0x87e2010
563+
DTraceProviderBindings.node`usdt_fire_probe+0x32
564+
DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d
565+
DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77
566+
<< internal code >>
567+
(anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484
568+
<< adaptor >>
569+
(anon) as doit at /root/my-prog.js position 360
570+
(anon) as list.ontimeout at timers.js position 4960
571+
<< adaptor >>
572+
<< internal >>
573+
<< entry >>
574+
node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101
575+
node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb
576+
node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0
577+
node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f
578+
node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66
579+
node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63
580+
node`uv__run_timers+0x66
581+
node`uv__run+0x1b
582+
node`uv_run+0x17
583+
node`_ZN4node5StartEiPPc+0x1d0
584+
node`main+0x1b
585+
node`_start+0x83
586+
587+
node`0x87e2010
588+
DTraceProviderBindings.node`usdt_fire_probe+0x32
589+
DTraceProviderBindings.node`_ZN4node11DTraceProbe5_fireEN2v85LocalINS1_5ValueEEE+0x32d
590+
DTraceProviderBindings.node`_ZN4node11DTraceProbe4FireERKN2v89ArgumentsE+0x77
591+
<< internal code >>
592+
(anon) as (anon) at /root/node-bunyan/lib/bunyan.js position 40484
593+
<< adaptor >>
594+
(anon) as doit at /root/my-prog.js position 360
595+
(anon) as list.ontimeout at timers.js position 4960
596+
<< adaptor >>
597+
<< internal >>
598+
<< entry >>
599+
node`_ZN2v88internalL6InvokeEbNS0_6HandleINS0_10JSFunctionEEENS1_INS0_6ObjectEEEiPS5_Pb+0x101
600+
node`_ZN2v88internal9Execution4CallENS0_6HandleINS0_6ObjectEEES4_iPS4_Pbb+0xcb
601+
node`_ZN2v88Function4CallENS_6HandleINS_6ObjectEEEiPNS1_INS_5ValueEEE+0xf0
602+
node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_8FunctionEEEiPNS1_INS0_5ValueEEE+0x11f
603+
node`_ZN4node12MakeCallbackEN2v86HandleINS0_6ObjectEEENS1_INS0_6StringEEEiPNS1_INS0_5ValueEEE+0x66
604+
node`_ZN4node9TimerWrap9OnTimeoutEP10uv_timer_si+0x63
605+
node`uv__run_timers+0x66
606+
node`uv__run+0x1b
607+
node`uv_run+0x17
608+
node`_ZN4node5StartEiPPc+0x1d0
609+
node`main+0x1b
610+
node`_start+0x83
611+
612+
520613
# Versioning
521614

522615
The scheme I follow is most succintly described by the bootstrap guys
@@ -527,7 +620,6 @@ breaking backward compat and major reworks, new features without breaking
527620
change, and bug fixes, respectively.
528621

529622

530-
531623
# License
532624

533625
MIT.

lib/bunyan.js

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var os = require('os');
2525
var fs = require('fs');
2626
var util = require('util');
2727
var assert = require('assert');
28+
var dtrace = require('dtrace-provider');
2829
var EventEmitter = require('events').EventEmitter;
2930

3031

@@ -156,6 +157,8 @@ var levelFromName = {
156157
'fatal': FATAL
157158
};
158159

160+
var dtp = undefined;
161+
var probes = {};
159162

160163
/**
161164
* Resolve a level number, name (upper or lowercase) to a level number value.
@@ -299,6 +302,22 @@ function Logger(options, _childOptions, _childSimple) {
299302
this.fields = {};
300303
}
301304

305+
if (!dtp) {
306+
dtp = dtrace.createDTraceProvider('bunyan');
307+
308+
for (var level in levelFromName) {
309+
var probe;
310+
311+
probes[levelFromName[level]] = probe =
312+
dtp.addProbe('log-' + level, 'char *');
313+
314+
// Explicitly add a reference to dtp to prevent it from being GC'd
315+
probe.dtp = dtp;
316+
}
317+
318+
dtp.enable();
319+
}
320+
302321
// Helpers
303322
function addStream(s) {
304323
s = objCopy(s);
@@ -641,8 +660,9 @@ Logger.prototype._mkRecord = function (fields, level, msgArgs) {
641660
* Emit a log record.
642661
*
643662
* @param rec {log record}
663+
* @param noemit {Boolean} Optional. Set to true to only return the string.
644664
*/
645-
Logger.prototype._emit = function (rec) {
665+
Logger.prototype._emit = function (rec, noemit) {
646666
var i;
647667

648668
var obj = objCopy(rec[0]);
@@ -685,6 +705,9 @@ Logger.prototype._emit = function (rec) {
685705
str = JSON.stringify(obj, safeCycles()) + '\n';
686706
}
687707

708+
if (noemit)
709+
return str;
710+
688711
for (i = 0; i < this.streams.length; i++) {
689712
var s = this.streams[i];
690713
if (s.level <= level) {
@@ -693,6 +716,8 @@ Logger.prototype._emit = function (rec) {
693716
s.stream.write(s.raw ? obj : str);
694717
}
695718
};
719+
720+
return str;
696721
}
697722

698723

@@ -702,33 +727,50 @@ Logger.prototype._emit = function (rec) {
702727
*/
703728
function mkLogEmitter(minLevel) {
704729
return function () {
730+
var log = this;
731+
732+
var mkRecord = function (args) {
733+
if (args[0] instanceof Error) {
734+
// `log.<level>(err, ...)`
735+
fields = {err: errSerializer(args[0])};
736+
if (args.length === 1) {
737+
msgArgs = [fields.err.message];
738+
} else {
739+
msgArgs = Array.prototype.slice.call(args, 1);
740+
}
741+
} else if (typeof (args[0]) === 'string') { // `log.<level>(msg, ...)`
742+
fields = null;
743+
msgArgs = Array.prototype.slice.call(args);
744+
} else if (Buffer.isBuffer(args[0])) { // `log.<level>(buf, ...)`
745+
// Almost certainly an error, show `inspect(buf)`. See bunyan issue #35.
746+
fields = null;
747+
msgArgs = Array.prototype.slice.call(args);
748+
msgArgs[0] = util.inspect(msgArgs[0]);
749+
} else { // `log.<level>(fields, msg, ...)`
750+
fields = args[0];
751+
msgArgs = Array.prototype.slice.call(args, 1);
752+
}
753+
return log._mkRecord(fields, minLevel, msgArgs);
754+
};
755+
705756
var fields = null, msgArgs = null;
706757
if (arguments.length === 0) { // `log.<level>()`
707758
return (this._level <= minLevel);
708759
} else if (this._level > minLevel) {
709-
return;
710-
} else if (arguments[0] instanceof Error) {
711-
// `log.<level>(err, ...)`
712-
fields = {err: errSerializer(arguments[0])};
713-
if (arguments.length === 1) {
714-
msgArgs = [fields.err.message];
715-
} else {
716-
msgArgs = Array.prototype.slice.call(arguments, 1);
717-
}
718-
} else if (typeof (arguments[0]) === 'string') { // `log.<level>(msg, ...)`
719-
fields = null;
720-
msgArgs = Array.prototype.slice.call(arguments);
721-
} else if (Buffer.isBuffer(arguments[0])) { // `log.<level>(buf, ...)`
722-
// Almost certainly an error, show `inspect(buf)`. See bunyan issue #35.
723-
fields = null;
724-
msgArgs = Array.prototype.slice.call(arguments);
725-
msgArgs[0] = util.inspect(msgArgs[0]);
726-
} else { // `log.<level>(fields, msg, ...)`
727-
fields = arguments[0];
728-
msgArgs = Array.prototype.slice.call(arguments, 1);
760+
/*
761+
* Even if our level is such that we will not emit the record, we want
762+
* to fire the DTrace probe associated with the level -- but we only
763+
* assemble the string representation if the probe is in fact enabled.
764+
*/
765+
msgArgs = arguments;
766+
767+
probes[minLevel].fire(function () {
768+
return [ log._emit(mkRecord(msgArgs), true) ];
769+
});
770+
} else {
771+
var str = this._emit(mkRecord(arguments));
772+
probes[minLevel].fire(function () { return [ str ]; });
729773
}
730-
var rec = this._mkRecord(fields, minLevel, msgArgs);
731-
this._emit(rec);
732774
}
733775
}
734776

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"engines": ["node >=0.6.0"],
1616
"keywords": ["log", "logging", "log4j", "json"],
1717

18+
"dependencies": {
19+
"dtrace-provider": "0.2.2"
20+
},
1821
"devDependencies": {
1922
"tap": "0.2.0",
2023
"ben": "0.0.0",

0 commit comments

Comments
 (0)