varanus
v2.0.1
Published
Tool for sending performance monitoring metrics
Downloads
5
Readme
Varanus
Monitor utility for NodeJS apps
What does it do?
A simple utility for monitoring function run times and flushing them out to another system in batches.
Varanus is agnostic with where the monitor logs are sent. They can be sent to an SQL database, Elasticsearch, InfluxDB, Prometheus, or even just your regular log files.
Varanus provides easy to use wrapper functions to be as transparent as possible in your code.
Monitored functions have low overhead. In testing, monitoring functions adds about 0.0006ms overhead.
Monitored functions can be given various log levels. Thus, you can log more information during testing without adversely affecting production performance. If the log level is turned off, expect only about 0.00008ms overhead per function call.
Failures to flush are handled gracefully. If flushing throws an exception or returns a rejected promise, the records that would have been sent are re-collected and sent again on the next flush cycle.
Varanus does not have to be initialized before anything is logged. Varanus will simply keep collecting records until you've successfully initialized it. Your app can start logging metrics immediately on startup.
Example
Initialization
// require('varanus') returns an initialization function that returns a new instance
var varanus = require('varanus')({
flushInterval: 30000,
logLevel: 'debug',
flush: function(logs) {
// Is a non-empty array of logs
records.forEach(function(record) {
console.log('Performance: ', {
service: record.service, // Name of the monitor
fnName: record.fnName, // Name of the function being monitored
time: record.time, // Number of milliseconds it took to run that function
level: record.level, // Log level for this record
created: record.created, // Date when the function was first executed
params: record.params // Any extra data passed to logTime() will be here. Will never be null/undefined
});
});
}
});
Sample Service
var monitor = varanus.newMonitor(__filename);
...
exports.getById = monitor.info(function getById(id) {
return dao.getById(id); // Works when the function returns a promise
});
exports.getByName = monitor.debug(function getByName(name, callback) {
// Also works with callback functions, if it's the last argument and follows (err, result) style
return dao.getByName(name, callback);
});
exports.transform = monitor.trace(function transform(records) {
// And of course works with synchronous functions
for (var i = 0; i < records.length; i += 1) {
...
});
return ...
});
exports.foo = function() {
// Alternatively, instead of wrapping a function, you can directly call
// monitor.logTime(logLevel, fnName, startMillis, endMillis)
var start = Date.now();
...
monitor.logTime('info', 'foo', start, Date.now());
}
Log Levels
Just like other logging frameworks like Bunyan, Winston, etc, Varanus supports log levels. These are used primarily to control how much is logged in various environments. For instance, during testing, it's probably worthwhile monitoring just about everything. However, in production, it may be wiser to only monitor key functions, in order to cut down on overhead spent logging.
When a log level is set, monitors set below that level will be ignored. For instance, if Varnus is set to level debug
, then all logs from functions monitored at the trace
level will be thrown away. There will still be a slight overhead when calling trace
functions, but it will be considerably less than if trace
were enabled.
Unlike textual logging frameworks, there are no warn
, error
, or fatal
log levels.
Performance
In testing, monitoring synchronous/promise functions seems to add roughly 600ns overhead. That means you'd have to call that function over 1,500 times for the performance to degrade by just 1ms. If your function being monitored normally takes 1ms to run, then monitoring it adds an overhead of about 0.06%.
If the monitor is set below the log level threshold (IE: it's trace
when the level is set to info
), then the overhead drops to about 80ns. That means the function can be called about 12,500 times for the performance to be degraded by 1ms.
For a comparison, simply doing console.log('foo')
takes more than 6,000ns per call.
Note that traditional Node callback-style functions require some addition processing to intercept, and will roughly double that overhead. It's still pretty trivial, however.
Testing is done right now by running the rather rudimentary perfTest.js
file via npm run perf
. It's far from perfect, but it allows for some tuning.
API
Varanus Initialization
The following functions are available on the initialization function:
flush
Required Function to call to flush a batch of records. This will usually format and send them to an external system, such as Elasticsearch, Mongo, etc. A non-empty array of records will be the only argument. See the example above to to see the available fields. May return a promise, which, if rejected, will result in the records being re-attempted in the next flush. (The ability to use a traditional Node callback function is in the works.)flushInterval
Optional - Integer - Number of milliseconds between flushing records. Defaults to60000
(One minute).maxRecords
Optional - Integer - Maximum number of records to gather in memory before automatically flushing, regardless offlushInterval
. Optional - Integer - Set to a number <= 1 to flush on every record. Defaults toInfinity
.level
Optional -'off'
|'trace'
|'debug'
|'info'
- The log level for Varanus. Defaults toinfo
.captureErrors
Optional - Boolean - Whether or not to capture errors that occur in monitored functions. Iftrue
, then if a monitored function threw/rejected, the error will be inparams.err
passed to the record inflush()
. Defaults totrue
.log
Optional - Bunyan-like Logger - Custom logger to use. Must support at leastwarn()
, anderror()
functions, in the style of Bunyan logs. Logging is a no-op by default.
Varanus Instance Functions
newMonitor(string)
Creates and returns a new Monitor (see below) which can monitor the execution times of functions and log them. The only argument is the monitor name. It is recommended that you pass in__filename
as the monitor name. Varanus will automatically strip the root path (process.cwd()
) from the filename. Assuming the app is always launched from the same folder, this will yield consistent results across multiple deployments.flush()
Will force a a call to the flush function defined during initialization. Note that if there are no records in memory, this will not do anything.setLogLevel(string)
Sets the log level for Varnus. May be one ofoff
|trace
|debug
|info
.logEnabled(string)
Returnstrue
if the given log level is enabled. For instance, if the log level isdebug
, thenlogEnabled('info')
will returntrue
, whilelogEnabled('trace')
will returnfalse
.
Monitors
Monitors are created by calling newMonitor(string)
on a Varanaus instance. They are used to wrap functions to be monitored, and can manually log times if necessary.
var monitor = varanus.newMonitor(__filename);
Wrapping functions
The easiest way to use Varanus is to just wrap your functions and let Varanus do the work.
Monitors are functions that take in one or two arguments. A single named function can be passed as the only argument. If the function is anonymous, then you should provide a name for the function as the first argument, and the second argument is the function.
monitor.debug(function foo() { ... });
// OR
monitor.debug('foo', function() { ... });
The wrapped functions can be:
- Promise-based, where the function returns a Promise
- Traditional Node callback-style, where the last argument is a function with the signature
function(err, result)
- Synchronous, where it either returns a non-Promise value, or nothing at all
There are methods on monitor
for each log level:
monitor.trace(function foo() { ... });
monitor.debug(function foo() { ... });
monitor.info(function foo() { ... });
Lastly, there is a raw logTime(string, string, int, int, obj)
function for cases where wrapping a function is not feasible. This is a low-level function and should generally be avoided.
One caveat with wrapping a function is that the returned function will not have the correct (or any) function name. If the function name is required, then using logTime(string, string, int, int, obj)
may be required.
Note! When using logTime
, Varanus has no knowledge of whether an error has occurred, so even if captureErrors
was set to true during initialization, you'll have to manually include any error in the params object. (See the example below.)
logTime(logLevel, fnName, startMillis, endMillis, params)
Logs times directly.logLevel
Required - String - The log level. If the Varanus log level is set above the threshold, this record is essentially thrown away immediately. See above section on log levels for possible values.fnName
Required - String - The function namestartMillis
Required - Integer - The Unix milliseconds value (usually fromDate.now()
) of when the function was startedendMillis
Required - Integer - Unix milliseconds value of when the function completedparams
Optional - Object - An extra parameters object to hold whatever data you may want to be included in the record when passed toflush()
.
function foo(cb) {
var start = Date.now();
database.find({id: 42}, function(err, result) {
monitor.logTime('info', 'foo', start, Date.now(), {err: err});
cb(err, result);
});
}