Install prettier and eslint

This commit is contained in:
Eric Mc Sween 2020-09-11 14:18:22 -04:00
parent c530422f1e
commit 971a768c4e
14 changed files with 1102 additions and 804 deletions

View file

@ -0,0 +1,55 @@
{
"extends": [
"standard",
"prettier",
"prettier/standard"
],
"parserOptions": {
"ecmaVersion": 2018
},
"plugins": [
"mocha",
"chai-expect",
"chai-friendly"
],
"env": {
"node": true
},
"rules": {
// Swap the no-unused-expressions rule with a more chai-friendly one
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": "error"
},
"overrides": [
{
// Test specific rules
"files": ["test/**/*.js"],
"env": {
"mocha": true
},
"globals": {
"expect": true
},
"rules": {
// mocha-specific rules
"mocha/handle-done-callback": "error",
"mocha/no-exclusive-tests": "error",
"mocha/no-global-tests": "error",
"mocha/no-identical-title": "error",
"mocha/no-nested-tests": "error",
"mocha/no-pending-tests": "error",
"mocha/no-skipped-tests": "error",
"mocha/no-mocha-arrows": "error",
// chai-specific rules
"chai-expect/missing-assertion": "error",
"chai-expect/terminating-properties": "error",
// prefer-arrow-callback applies to all callbacks, not just ones in mocha tests.
// we don't enforce this at the top-level - just in tests to manage `this` scope
// based on mocha's context mechanism
"mocha/prefer-arrow-callback": "error"
}
}
]
}

View file

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View file

@ -4,27 +4,31 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
let EventLoopMonitor; module.exports = {
module.exports = (EventLoopMonitor = { monitor(logger, interval, logThreshold) {
monitor(logger, interval, log_threshold) { if (interval == null) {
if (interval == null) { interval = 1000; } interval = 1000
if (log_threshold == null) { log_threshold = 100; } }
const Metrics = require("./index"); if (logThreshold == null) {
// check for logger on startup to avoid exceptions later if undefined logThreshold = 100
if ((logger == null)) { throw new Error("logger is undefined"); } }
// monitor delay in setInterval to detect event loop blocking const Metrics = require('./index')
let previous = Date.now(); // check for logger on startup to avoid exceptions later if undefined
const intervalId = setInterval(function() { if (logger == null) {
const now = Date.now(); throw new Error('logger is undefined')
const offset = now - previous - interval; }
if (offset > log_threshold) { // monitor delay in setInterval to detect event loop blocking
logger.warn({offset}, "slow event loop"); let previous = Date.now()
} const intervalId = setInterval(function() {
previous = now; const now = Date.now()
return Metrics.timing("event-loop-millsec", offset); const offset = now - previous - interval
} if (offset > logThreshold) {
, interval); logger.warn({ offset }, 'slow event loop')
}
return Metrics.registerDestructor(() => clearInterval(intervalId)); previous = now
} return Metrics.timing('event-loop-millsec', offset)
}); }, interval)
return Metrics.registerDestructor(() => clearInterval(intervalId))
}
}

View file

@ -5,73 +5,93 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const os = require("os"); const yn = require('yn')
const yn = require("yn");
const STACKDRIVER_LOGGING = yn(process.env['STACKDRIVER_LOGGING']); const STACKDRIVER_LOGGING = yn(process.env.STACKDRIVER_LOGGING)
module.exports.monitor = logger => (function(req, res, next) { module.exports.monitor = logger =>
const Metrics = require("./index"); function(req, res, next) {
const startTime = process.hrtime(); const Metrics = require('./index')
const { const startTime = process.hrtime()
end const { end } = res
} = res;
res.end = function() { res.end = function() {
let info; let info
end.apply(this, arguments); end.apply(this, arguments)
const responseTime = process.hrtime(startTime); const responseTime = process.hrtime(startTime)
const responseTimeMs = Math.round((responseTime[0] * 1000) + (responseTime[1] / 1000000)); const responseTimeMs = Math.round(
const requestSize = parseInt(req.headers["content-length"], 10); responseTime[0] * 1000 + responseTime[1] / 1000000
if ((req.route != null ? req.route.path : undefined) != null) { )
const routePath = req.route.path.toString().replace(/\//g, '_').replace(/\:/g, '').slice(1); const requestSize = parseInt(req.headers['content-length'], 10)
Metrics.timing("http_request", responseTimeMs, null, {method:req.method, status_code: res.statusCode, path:routePath}); if ((req.route != null ? req.route.path : undefined) != null) {
if (requestSize) { const routePath = req.route.path
Metrics.summary("http_request_size_bytes", requestSize, {method:req.method, status_code: res.statusCode, path:routePath}); .toString()
} .replace(/\//g, '_')
.replace(/:/g, '')
.slice(1)
Metrics.timing('http_request', responseTimeMs, null, {
method: req.method,
status_code: res.statusCode,
path: routePath
})
if (requestSize) {
Metrics.summary('http_request_size_bytes', requestSize, {
method: req.method,
status_code: res.statusCode,
path: routePath
})
} }
const remoteIp = req.ip || __guard__(req.socket != null ? req.socket.socket : undefined, x => x.remoteAddress) || (req.socket != null ? req.socket.remoteAddress : undefined); }
const reqUrl = req.originalUrl || req.url; const remoteIp =
const referrer = req.headers['referer'] || req.headers['referrer']; req.ip ||
if (STACKDRIVER_LOGGING) { __guard__(
info = { req.socket != null ? req.socket.socket : undefined,
httpRequest: { x => x.remoteAddress
requestMethod: req.method, ) ||
requestUrl: reqUrl, (req.socket != null ? req.socket.remoteAddress : undefined)
requestSize, const reqUrl = req.originalUrl || req.url
status: res.statusCode, const referrer = req.headers.referer || req.headers.referrer
responseSize: res.getHeader("content-length"), if (STACKDRIVER_LOGGING) {
userAgent: req.headers["user-agent"], info = {
remoteIp, httpRequest: {
referer: referrer, requestMethod: req.method,
latency: { requestUrl: reqUrl,
seconds: responseTime[0], requestSize,
nanos: responseTime[1] status: res.statusCode,
}, responseSize: res.getHeader('content-length'),
protocol: req.protocol userAgent: req.headers['user-agent'],
} remoteIp,
}; referer: referrer,
} else { latency: {
info = { seconds: responseTime[0],
req: { nanos: responseTime[1]
url: reqUrl, },
method: req.method, protocol: req.protocol
referrer, }
"remote-addr": remoteIp,
"user-agent": req.headers["user-agent"],
"content-length": req.headers["content-length"]
},
res: {
"content-length": res.getHeader("content-length"),
statusCode: res.statusCode
},
"response-time": responseTimeMs
};
} }
return logger.info(info, "%s %s", req.method, reqUrl); } else {
}; info = {
return next(); req: {
}); url: reqUrl,
method: req.method,
referrer,
'remote-addr': remoteIp,
'user-agent': req.headers['user-agent'],
'content-length': req.headers['content-length']
},
res: {
'content-length': res.getHeader('content-length'),
statusCode: res.statusCode
},
'response-time': responseTimeMs
}
}
return logger.info(info, '%s %s', req.method, reqUrl)
}
return next()
}
function __guard__(value, transform) { function __guard__(value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; return typeof value !== 'undefined' && value !== null
} ? transform(value)
: undefined
}

View file

@ -5,187 +5,229 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
let Metrics; let Metrics
console.log("using prometheus"); console.log('using prometheus')
const ExpressCompression = require('compression'); const ExpressCompression = require('compression')
const prom = require('./prom_wrapper'); const prom = require('./prom_wrapper')
const { const { collectDefaultMetrics } = prom
collectDefaultMetrics
} = prom;
let appname = "unknown"; let appname = 'unknown'
const hostname = require('os').hostname(); const hostname = require('os').hostname()
const destructors = []; const destructors = []
require("./uv_threadpool_size"); require('./uv_threadpool_size')
module.exports = (Metrics = { module.exports = Metrics = {
register: prom.registry, register: prom.registry,
initialize(_name, opts) { initialize(_name, opts) {
if (opts == null) { opts = {}; } if (opts == null) {
appname = _name; opts = {}
collectDefaultMetrics({ timeout: 5000, prefix: Metrics.buildPromKey()}); }
if (opts.ttlInMinutes) { appname = _name
prom.ttlInMinutes = opts.ttlInMinutes; collectDefaultMetrics({ timeout: 5000, prefix: Metrics.buildPromKey() })
} if (opts.ttlInMinutes) {
prom.ttlInMinutes = opts.ttlInMinutes
}
console.log(`ENABLE_TRACE_AGENT set to ${process.env['ENABLE_TRACE_AGENT']}`); console.log(
if (process.env['ENABLE_TRACE_AGENT'] === "true") { `ENABLE_TRACE_AGENT set to ${process.env.ENABLE_TRACE_AGENT}`
console.log("starting google trace agent"); )
const traceAgent = require('@google-cloud/trace-agent'); if (process.env.ENABLE_TRACE_AGENT === 'true') {
console.log('starting google trace agent')
const traceAgent = require('@google-cloud/trace-agent')
const traceOpts = const traceOpts = { ignoreUrls: [/^\/status/, /^\/health_check/] }
{ignoreUrls: [/^\/status/, /^\/health_check/]}; traceAgent.start(traceOpts)
traceAgent.start(traceOpts); }
}
console.log(`ENABLE_DEBUG_AGENT set to ${process.env['ENABLE_DEBUG_AGENT']}`); console.log(
if (process.env['ENABLE_DEBUG_AGENT'] === "true") { `ENABLE_DEBUG_AGENT set to ${process.env.ENABLE_DEBUG_AGENT}`
console.log("starting google debug agent"); )
const debugAgent = require('@google-cloud/debug-agent'); if (process.env.ENABLE_DEBUG_AGENT === 'true') {
debugAgent.start({ console.log('starting google debug agent')
allowExpressions: true, const debugAgent = require('@google-cloud/debug-agent')
serviceContext: { debugAgent.start({
service: appname, allowExpressions: true,
version: process.env['BUILD_VERSION'] serviceContext: {
} service: appname,
}); version: process.env.BUILD_VERSION
} }
})
}
console.log(`ENABLE_PROFILE_AGENT set to ${process.env['ENABLE_PROFILE_AGENT']}`); console.log(
if (process.env['ENABLE_PROFILE_AGENT'] === "true") { `ENABLE_PROFILE_AGENT set to ${process.env.ENABLE_PROFILE_AGENT}`
console.log("starting google profile agent"); )
const profiler = require('@google-cloud/profiler'); if (process.env.ENABLE_PROFILE_AGENT === 'true') {
profiler.start({ console.log('starting google profile agent')
serviceContext: { const profiler = require('@google-cloud/profiler')
service: appname, profiler.start({
version: process.env['BUILD_VERSION'] serviceContext: {
} service: appname,
}); version: process.env.BUILD_VERSION
} }
})
}
return Metrics.inc("process_startup"); return Metrics.inc('process_startup')
}, },
registerDestructor(func) { registerDestructor(func) {
return destructors.push(func); return destructors.push(func)
}, },
injectMetricsRoute(app) { injectMetricsRoute(app) {
return app.get('/metrics', ExpressCompression({ return app.get(
level: parseInt(process.env.METRICS_COMPRESSION_LEVEL || '1', 10) '/metrics',
}), function (req, res) { ExpressCompression({
res.set('Content-Type', prom.registry.contentType); level: parseInt(process.env.METRICS_COMPRESSION_LEVEL || '1', 10)
return res.end(prom.registry.metrics()); }),
}); function(req, res) {
}, res.set('Content-Type', prom.registry.contentType)
return res.end(prom.registry.metrics())
}
)
},
buildPromKey(key){ buildPromKey(key) {
if (key == null) { key = ""; } if (key == null) {
return key.replace(/[^a-zA-Z0-9]/g, "_"); key = ''
}, }
return key.replace(/[^a-zA-Z0-9]/g, '_')
},
sanitizeValue(value) { sanitizeValue(value) {
return parseFloat(value); return parseFloat(value)
}, },
set(key, value, sampleRate){ set(key, value, sampleRate) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
return console.log("counts are not currently supported"); sampleRate = 1
}, }
return console.log('counts are not currently supported')
},
inc(key, sampleRate, opts){ inc(key, sampleRate, opts) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
if (opts == null) { opts = {}; } sampleRate = 1
key = Metrics.buildPromKey(key); }
opts.app = appname; if (opts == null) {
opts.host = hostname; opts = {}
prom.metric('counter', key).inc(opts); }
if (process.env['DEBUG_METRICS']) { key = Metrics.buildPromKey(key)
return console.log("doing inc", key, opts); opts.app = appname
} opts.host = hostname
}, prom.metric('counter', key).inc(opts)
if (process.env.DEBUG_METRICS) {
return console.log('doing inc', key, opts)
}
},
count(key, count, sampleRate, opts){ count(key, count, sampleRate, opts) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
if (opts == null) { opts = {}; } sampleRate = 1
key = Metrics.buildPromKey(key); }
opts.app = appname; if (opts == null) {
opts.host = hostname; opts = {}
prom.metric('counter', key).inc(opts, count); }
if (process.env['DEBUG_METRICS']) { key = Metrics.buildPromKey(key)
return console.log("doing count/inc", key, opts); opts.app = appname
} opts.host = hostname
}, prom.metric('counter', key).inc(opts, count)
if (process.env.DEBUG_METRICS) {
return console.log('doing count/inc', key, opts)
}
},
summary(key, value, opts){ summary(key, value, opts) {
if (opts == null) { opts = {}; } if (opts == null) {
key = Metrics.buildPromKey(key); opts = {}
opts.app = appname; }
opts.host = hostname; key = Metrics.buildPromKey(key)
prom.metric('summary', key).observe(opts, value); opts.app = appname
if (process.env['DEBUG_METRICS']) { opts.host = hostname
return console.log("doing summary", key, value, opts); prom.metric('summary', key).observe(opts, value)
} if (process.env.DEBUG_METRICS) {
}, return console.log('doing summary', key, value, opts)
}
},
timing(key, timeSpan, sampleRate, opts){ timing(key, timeSpan, sampleRate, opts) {
if (opts == null) { opts = {}; } if (opts == null) {
key = Metrics.buildPromKey("timer_" + key); opts = {}
opts.app = appname; }
opts.host = hostname; key = Metrics.buildPromKey('timer_' + key)
prom.metric('summary', key).observe(opts, timeSpan); opts.app = appname
if (process.env['DEBUG_METRICS']) { opts.host = hostname
return console.log("doing timing", key, opts); prom.metric('summary', key).observe(opts, timeSpan)
} if (process.env.DEBUG_METRICS) {
}, return console.log('doing timing', key, opts)
}
},
Timer : class { Timer: class {
constructor(key, sampleRate, opts){ constructor(key, sampleRate, opts) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
this.start = new Date(); sampleRate = 1
key = Metrics.buildPromKey(key); }
this.key = key; this.start = new Date()
this.sampleRate = sampleRate; key = Metrics.buildPromKey(key)
this.opts = opts; this.key = key
} this.sampleRate = sampleRate
this.opts = opts
}
done() { done() {
const timeSpan = new Date - this.start; const timeSpan = new Date() - this.start
Metrics.timing(this.key, timeSpan, this.sampleRate, this.opts); Metrics.timing(this.key, timeSpan, this.sampleRate, this.opts)
return timeSpan; return timeSpan
} }
}, },
gauge(key, value, sampleRate, opts){ gauge(key, value, sampleRate, opts) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
key = Metrics.buildPromKey(key); sampleRate = 1
prom.metric('gauge', key).set({app: appname, host: hostname, status: (opts != null ? opts.status : undefined)}, this.sanitizeValue(value)); }
if (process.env['DEBUG_METRICS']) { key = Metrics.buildPromKey(key)
return console.log("doing gauge", key, opts); prom.metric('gauge', key).set(
} {
}, app: appname,
host: hostname,
status: opts != null ? opts.status : undefined
},
this.sanitizeValue(value)
)
if (process.env.DEBUG_METRICS) {
return console.log('doing gauge', key, opts)
}
},
globalGauge(key, value, sampleRate, opts){ globalGauge(key, value, sampleRate, opts) {
if (sampleRate == null) { sampleRate = 1; } if (sampleRate == null) {
key = Metrics.buildPromKey(key); sampleRate = 1
return prom.metric('gauge', key).set({app: appname, status: (opts != null ? opts.status : undefined)},this.sanitizeValue(value)); }
}, key = Metrics.buildPromKey(key)
return prom
.metric('gauge', key)
.set(
{ app: appname, status: opts != null ? opts.status : undefined },
this.sanitizeValue(value)
)
},
mongodb: require("./mongodb"), mongodb: require('./mongodb'),
http: require("./http"), http: require('./http'),
open_sockets: require("./open_sockets"), open_sockets: require('./open_sockets'),
event_loop: require("./event_loop"), event_loop: require('./event_loop'),
memory: require("./memory"), memory: require('./memory'),
timeAsyncMethod: require('./timeAsyncMethod'), timeAsyncMethod: require('./timeAsyncMethod'),
close() { close() {
return Array.from(destructors).map((func) => return Array.from(destructors).map(func => func())
func()); }
} }
});

View file

@ -9,95 +9,105 @@
// adjust the period between gc()'s to reach a target of the gc saving // adjust the period between gc()'s to reach a target of the gc saving
// 4 megabytes each time. // 4 megabytes each time.
let MemoryMonitor; let MemoryMonitor
const oneMinute = 60 * 1000; const oneMinute = 60 * 1000
const oneMegaByte = 1024 * 1024; const oneMegaByte = 1024 * 1024
let CpuTimeBucket = 100; // current cpu time allowance in milliseconds let CpuTimeBucket = 100 // current cpu time allowance in milliseconds
const CpuTimeBucketMax = 100; // maximum amount of cpu time allowed in bucket const CpuTimeBucketMax = 100 // maximum amount of cpu time allowed in bucket
const CpuTimeBucketRate = 10; // add this many milliseconds per minute const CpuTimeBucketRate = 10 // add this many milliseconds per minute
let gcInterval = 1; // how many minutes between gc (parameter is dynamically adjusted) let gcInterval = 1 // how many minutes between gc (parameter is dynamically adjusted)
let countSinceLastGc = 0; // how many minutes since last gc let countSinceLastGc = 0 // how many minutes since last gc
const MemoryChunkSize = 4; // how many megabytes we need to free to consider gc worth doing const MemoryChunkSize = 4 // how many megabytes we need to free to consider gc worth doing
const readyToGc = function() { const readyToGc = function() {
// update allowed cpu time // update allowed cpu time
CpuTimeBucket = CpuTimeBucket + CpuTimeBucketRate; CpuTimeBucket = CpuTimeBucket + CpuTimeBucketRate
CpuTimeBucket = CpuTimeBucket < CpuTimeBucketMax ? CpuTimeBucket : CpuTimeBucketMax; CpuTimeBucket =
// update counts since last gc CpuTimeBucket < CpuTimeBucketMax ? CpuTimeBucket : CpuTimeBucketMax
countSinceLastGc = countSinceLastGc + 1; // update counts since last gc
// check there is enough time since last gc and we have enough cpu countSinceLastGc = countSinceLastGc + 1
return (countSinceLastGc > gcInterval) && (CpuTimeBucket > 0); // check there is enough time since last gc and we have enough cpu
}; return countSinceLastGc > gcInterval && CpuTimeBucket > 0
}
const executeAndTime = function(fn) { const executeAndTime = function(fn) {
// time the execution of fn() and subtract from cpu allowance // time the execution of fn() and subtract from cpu allowance
const t0 = process.hrtime(); const t0 = process.hrtime()
fn(); fn()
const dt = process.hrtime(t0); const dt = process.hrtime(t0)
const timeTaken = (dt[0] + (dt[1]*1e-9)) * 1e3; // in milliseconds const timeTaken = (dt[0] + dt[1] * 1e-9) * 1e3 // in milliseconds
CpuTimeBucket -= Math.ceil(timeTaken); CpuTimeBucket -= Math.ceil(timeTaken)
return timeTaken; return timeTaken
}; }
const inMegaBytes = function(obj) { const inMegaBytes = function(obj) {
// convert process.memoryUsage hash {rss,heapTotal,heapFreed} into megabytes // convert process.memoryUsage hash {rss,heapTotal,heapFreed} into megabytes
const result = {}; const result = {}
for (let k in obj) { for (const k in obj) {
const v = obj[k]; const v = obj[k]
result[k] = (v / oneMegaByte).toFixed(2); result[k] = (v / oneMegaByte).toFixed(2)
} }
return result; return result
}; }
const updateMemoryStats = function(oldMem, newMem) { const updateMemoryStats = function(oldMem, newMem) {
countSinceLastGc = 0; countSinceLastGc = 0
const delta = {}; const delta = {}
for (let k in newMem) { for (const k in newMem) {
delta[k] = (newMem[k] - oldMem[k]).toFixed(2); delta[k] = (newMem[k] - oldMem[k]).toFixed(2)
} }
// take the max of all memory measures // take the max of all memory measures
const savedMemory = Math.max(-delta.rss, -delta.heapTotal, -delta.heapUsed); const savedMemory = Math.max(-delta.rss, -delta.heapTotal, -delta.heapUsed)
delta.megabytesFreed = savedMemory; delta.megabytesFreed = savedMemory
// did it do any good? // did it do any good?
if (savedMemory < MemoryChunkSize) { if (savedMemory < MemoryChunkSize) {
gcInterval = gcInterval + 1; // no, so wait longer next time gcInterval = gcInterval + 1 // no, so wait longer next time
} else { } else {
gcInterval = Math.max(gcInterval - 1, 1); // yes, wait less time gcInterval = Math.max(gcInterval - 1, 1) // yes, wait less time
} }
return delta; return delta
}; }
module.exports = (MemoryMonitor = { module.exports = MemoryMonitor = {
monitor(logger) { monitor(logger) {
const interval = setInterval(() => MemoryMonitor.Check(logger) const interval = setInterval(() => MemoryMonitor.Check(logger), oneMinute)
, oneMinute); const Metrics = require('./index')
const Metrics = require("./index"); return Metrics.registerDestructor(() => clearInterval(interval))
return Metrics.registerDestructor(() => clearInterval(interval)); },
},
Check(logger) { Check(logger) {
let mem; let mem
const Metrics = require("./index"); const Metrics = require('./index')
const memBeforeGc = (mem = inMegaBytes(process.memoryUsage())); const memBeforeGc = (mem = inMegaBytes(process.memoryUsage()))
Metrics.gauge("memory.rss", mem.rss); Metrics.gauge('memory.rss', mem.rss)
Metrics.gauge("memory.heaptotal", mem.heapTotal); Metrics.gauge('memory.heaptotal', mem.heapTotal)
Metrics.gauge("memory.heapused", mem.heapUsed); Metrics.gauge('memory.heapused', mem.heapUsed)
Metrics.gauge("memory.gc-interval", gcInterval); Metrics.gauge('memory.gc-interval', gcInterval)
//Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket) // Metrics.gauge("memory.cpu-time-bucket", CpuTimeBucket)
logger.log(mem, "process.memoryUsage()"); logger.log(mem, 'process.memoryUsage()')
if ((global.gc != null) && readyToGc()) { if (global.gc != null && readyToGc()) {
const gcTime = (executeAndTime(global.gc)).toFixed(2); const gcTime = executeAndTime(global.gc).toFixed(2)
const memAfterGc = inMegaBytes(process.memoryUsage()); const memAfterGc = inMegaBytes(process.memoryUsage())
const deltaMem = updateMemoryStats(memBeforeGc, memAfterGc); const deltaMem = updateMemoryStats(memBeforeGc, memAfterGc)
logger.log({gcTime, memBeforeGc, memAfterGc, deltaMem, gcInterval, CpuTimeBucket}, "global.gc() forced"); logger.log(
//Metrics.timing("memory.gc-time", gcTime) {
Metrics.gauge("memory.gc-rss-freed", -deltaMem.rss); gcTime,
Metrics.gauge("memory.gc-heaptotal-freed", -deltaMem.heapTotal); memBeforeGc,
return Metrics.gauge("memory.gc-heapused-freed", -deltaMem.heapUsed); memAfterGc,
} deltaMem,
} gcInterval,
}); CpuTimeBucket
},
'global.gc() forced'
)
// Metrics.timing("memory.gc-time", gcTime)
Metrics.gauge('memory.gc-rss-freed', -deltaMem.rss)
Metrics.gauge('memory.gc-heaptotal-freed', -deltaMem.heapTotal)
return Metrics.gauge('memory.gc-heapused-freed', -deltaMem.heapUsed)
}
}
}

View file

@ -5,124 +5,196 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
module.exports = { module.exports = {
monitor(mongodb_require_path, logger) { monitor(mongodbRequirePath, logger) {
let mongodb, mongodbCore
try {
// for the v1 driver the methods to wrap are in the mongodb
// module in lib/mongodb/db.js
mongodb = require(mongodbRequirePath)
} catch (error) {}
let mongodb, mongodbCore; try {
try { // for the v2 driver the relevant methods are in the mongodb-core
// for the v1 driver the methods to wrap are in the mongodb // module in lib/topologies/{server,replset,mongos}.js
// module in lib/mongodb/db.js const v2Path = mongodbRequirePath.replace(/\/mongodb$/, '/mongodb-core')
mongodb = require(`${mongodb_require_path}`); mongodbCore = require(v2Path)
} catch (error) {} } catch (error1) {}
try { const Metrics = require('./index')
// for the v2 driver the relevant methods are in the mongodb-core
// module in lib/topologies/{server,replset,mongos}.js
const v2_path = mongodb_require_path.replace(/\/mongodb$/, '/mongodb-core');
mongodbCore = require(v2_path);
} catch (error1) {}
const Metrics = require("./index"); const monitorMethod = function(base, method, type) {
let _method
if (base == null) {
return
}
if ((_method = base[method]) == null) {
return
}
const arglen = _method.length
const monitorMethod = function(base, method, type) { const mongoDriverV1Wrapper = function(dbCommand, options, callback) {
let _method; let query
if (base == null) { return; } if (typeof callback === 'undefined') {
if ((_method = base[method]) == null) { return; } callback = options
const arglen = _method.length; options = {}
}
const mongo_driver_v1_wrapper = function(db_command, options, callback) { const collection = dbCommand.collectionName
let query; if (collection.match(/\$cmd$/)) {
if (typeof callback === 'undefined') { // Ignore noisy command methods like authenticating, ismaster and ping
callback = options; return _method.call(this, dbCommand, options, callback)
options = {}; }
}
const collection = db_command.collectionName; if (dbCommand.query != null) {
if (collection.match(/\$cmd$/)) { query = Object.keys(dbCommand.query)
// Ignore noisy command methods like authenticating, ismaster and ping .sort()
return _method.call(this, db_command, options, callback); .join('_')
} }
let key = `mongo-requests.${collection}.${type}`; const timer = new Metrics.Timer('mongo', { collection, query })
if (db_command.query != null) { const start = new Date()
query = Object.keys(db_command.query).sort().join("_"); return _method.call(this, dbCommand, options, function() {
key += "." + query; timer.done()
} logger.log(
{
query: dbCommand.query,
query_type: type,
collection,
'response-time': new Date() - start
},
'mongo request'
)
return callback.apply(this, arguments)
})
}
const timer = new Metrics.Timer("mongo", {collection, query}); const mongoDriverV2Wrapper = function(ns, ops, options, callback) {
const start = new Date(); let query
return _method.call(this, db_command, options, function() { if (typeof callback === 'undefined') {
timer.done(); callback = options
const time = new Date() - start; options = {}
logger.log({ }
query: db_command.query,
query_type: type,
collection,
"response-time": new Date() - start
},
"mongo request");
return callback.apply(this, arguments);
});
};
const mongo_driver_v2_wrapper = function(ns, ops, options, callback) { if (ns.match(/\$cmd$/)) {
let query; // Ignore noisy command methods like authenticating, ismaster and ping
if (typeof callback === 'undefined') { return _method.call(this, ns, ops, options, callback)
callback = options; }
options = {};
}
if (ns.match(/\$cmd$/)) { let key = `mongo-requests.${ns}.${type}`
// Ignore noisy command methods like authenticating, ismaster and ping if (ops[0].q != null) {
return _method.call(this, ns, ops, options, callback); // ops[0].q
} query = Object.keys(ops[0].q)
.sort()
.join('_')
key += '.' + query
}
let key = `mongo-requests.${ns}.${type}`; const timer = new Metrics.Timer(key)
if (ops[0].q != null) { // ops[0].q const start = new Date()
query = Object.keys(ops[0].q).sort().join("_"); return _method.call(this, ns, ops, options, function() {
key += "." + query; timer.done()
} logger.log(
{
query: ops[0].q,
query_type: type,
collection: ns,
'response-time': new Date() - start
},
'mongo request'
)
return callback.apply(this, arguments)
})
}
const timer = new Metrics.Timer(key); if (arglen === 3) {
const start = new Date(); return (base[method] = mongoDriverV1Wrapper)
return _method.call(this, ns, ops, options, function() { } else if (arglen === 4) {
timer.done(); return (base[method] = mongoDriverV2Wrapper)
const time = new Date() - start; }
logger.log({ }
query: ops[0].q,
query_type: type,
collection: ns,
"response-time": new Date() - start
},
"mongo request");
return callback.apply(this, arguments);
});
};
if (arglen === 3) { monitorMethod(
return base[method] = mongo_driver_v1_wrapper; mongodb != null ? mongodb.Db.prototype : undefined,
} else if (arglen === 4) { '_executeQueryCommand',
return base[method] = mongo_driver_v2_wrapper; 'query'
} )
}; monitorMethod(
mongodb != null ? mongodb.Db.prototype : undefined,
'_executeRemoveCommand',
'remove'
)
monitorMethod(
mongodb != null ? mongodb.Db.prototype : undefined,
'_executeInsertCommand',
'insert'
)
monitorMethod(
mongodb != null ? mongodb.Db.prototype : undefined,
'_executeUpdateCommand',
'update'
)
monitorMethod(mongodb != null ? mongodb.Db.prototype : undefined, "_executeQueryCommand", "query"); monitorMethod(
monitorMethod(mongodb != null ? mongodb.Db.prototype : undefined, "_executeRemoveCommand", "remove"); mongodbCore != null ? mongodbCore.Server.prototype : undefined,
monitorMethod(mongodb != null ? mongodb.Db.prototype : undefined, "_executeInsertCommand", "insert"); 'command',
monitorMethod(mongodb != null ? mongodb.Db.prototype : undefined, "_executeUpdateCommand", "update"); 'command'
)
monitorMethod(
mongodbCore != null ? mongodbCore.Server.prototype : undefined,
'remove',
'remove'
)
monitorMethod(
mongodbCore != null ? mongodbCore.Server.prototype : undefined,
'insert',
'insert'
)
monitorMethod(
mongodbCore != null ? mongodbCore.Server.prototype : undefined,
'update',
'update'
)
monitorMethod(mongodbCore != null ? mongodbCore.Server.prototype : undefined, "command", "command"); monitorMethod(
monitorMethod(mongodbCore != null ? mongodbCore.Server.prototype : undefined, "remove", "remove"); mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined,
monitorMethod(mongodbCore != null ? mongodbCore.Server.prototype : undefined, "insert", "insert"); 'command',
monitorMethod(mongodbCore != null ? mongodbCore.Server.prototype : undefined, "update", "update"); 'command'
)
monitorMethod(
mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined,
'remove',
'remove'
)
monitorMethod(
mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined,
'insert',
'insert'
)
monitorMethod(
mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined,
'update',
'update'
)
monitorMethod(mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined, "command", "command"); monitorMethod(
monitorMethod(mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined, "remove", "remove"); mongodbCore != null ? mongodbCore.Mongos.prototype : undefined,
monitorMethod(mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined, "insert", "insert"); 'command',
monitorMethod(mongodbCore != null ? mongodbCore.ReplSet.prototype : undefined, "update", "update"); 'command'
)
monitorMethod(mongodbCore != null ? mongodbCore.Mongos.prototype : undefined, "command", "command"); monitorMethod(
monitorMethod(mongodbCore != null ? mongodbCore.Mongos.prototype : undefined, "remove", "remove"); mongodbCore != null ? mongodbCore.Mongos.prototype : undefined,
monitorMethod(mongodbCore != null ? mongodbCore.Mongos.prototype : undefined, "insert", "insert"); 'remove',
return monitorMethod(mongodbCore != null ? mongodbCore.Mongos.prototype : undefined, "update", "update"); 'remove'
} )
}; monitorMethod(
mongodbCore != null ? mongodbCore.Mongos.prototype : undefined,
'insert',
'insert'
)
return monitorMethod(
mongodbCore != null ? mongodbCore.Mongos.prototype : undefined,
'update',
'update'
)
}
}

View file

@ -5,44 +5,50 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
let OpenSocketsMonitor; let OpenSocketsMonitor
const URL = require("url"); const { URL } = require('url')
const seconds = 1000; const seconds = 1000
// In Node 0.10 the default is 5, which means only 5 open connections at one. // In Node 0.10 the default is 5, which means only 5 open connections at one.
// Node 0.12 has a default of Infinity. Make sure we have no limit set, // Node 0.12 has a default of Infinity. Make sure we have no limit set,
// regardless of Node version. // regardless of Node version.
require("http").globalAgent.maxSockets = Infinity; require('http').globalAgent.maxSockets = Infinity
require("https").globalAgent.maxSockets = Infinity; require('https').globalAgent.maxSockets = Infinity
module.exports = (OpenSocketsMonitor = { module.exports = OpenSocketsMonitor = {
monitor(logger) { monitor(logger) {
const interval = setInterval(() => OpenSocketsMonitor.gaugeOpenSockets() const interval = setInterval(
, 5 * seconds); () => OpenSocketsMonitor.gaugeOpenSockets(),
const Metrics = require("./index"); 5 * seconds
return Metrics.registerDestructor(() => clearInterval(interval)); )
}, const Metrics = require('./index')
return Metrics.registerDestructor(() => clearInterval(interval))
},
gaugeOpenSockets() { gaugeOpenSockets() {
let agents, hostname, url; let agents, hostname, url
const Metrics = require("./index"); const Metrics = require('./index')
const object = require('http').globalAgent.sockets; const object = require('http').globalAgent.sockets
for (url in object) { for (url in object) {
agents = object[url]; agents = object[url]
url = URL.parse(`http://${url}`); url = new URL(`http://${url}`)
hostname = url.hostname != null ? url.hostname.replace(/\./g, "_") : undefined; hostname =
Metrics.gauge(`open_connections.http.${hostname}`, agents.length); url.hostname != null ? url.hostname.replace(/\./g, '_') : undefined
} Metrics.gauge(`open_connections.http.${hostname}`, agents.length)
return (() => { }
const result = []; return (() => {
const object1 = require('https').globalAgent.sockets; const result = []
for (url in object1) { const object1 = require('https').globalAgent.sockets
agents = object1[url]; for (url in object1) {
url = URL.parse(`https://${url}`); agents = object1[url]
hostname = url.hostname != null ? url.hostname.replace(/\./g, "_") : undefined; url = new URL(`https://${url}`)
result.push(Metrics.gauge(`open_connections.https.${hostname}`, agents.length)); hostname =
} url.hostname != null ? url.hostname.replace(/\./g, '_') : undefined
return result; result.push(
})(); Metrics.gauge(`open_connections.https.${hostname}`, agents.length)
} )
}); }
return result
})()
}
}

View file

@ -18,11 +18,25 @@
"devDependencies": { "devDependencies": {
"bunyan": "^1.0.0", "bunyan": "^1.0.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-chai-expect": "^2.2.0",
"eslint-plugin-chai-friendly": "^0.6.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"mocha": "^8.0.1", "mocha": "^8.0.1",
"prettier-eslint-cli": "^5.0.0",
"sandboxed-module": "^2.0.4", "sandboxed-module": "^2.0.4",
"sinon": "^9.0.2" "sinon": "^9.0.2"
}, },
"scripts": { "scripts": {
"test": "mocha --reporter spec --recursive --exit --grep=$MOCHA_GREP test/unit" "lint": "eslint --max-warnings 0 .",
"test": "mocha --reporter spec --recursive --exit --grep=$MOCHA_GREP test/unit",
"format": "prettier-eslint $PWD'/**/*.js' --list-different",
"format:fix": "prettier-eslint $PWD'/**/*.js' --write"
} }
} }

View file

@ -5,144 +5,162 @@
* DS205: Consider reworking code to avoid use of IIFEs * DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const prom = require('prom-client'); const prom = require('prom-client')
const registry = require('prom-client').register; const registry = require('prom-client').register
const metrics = new Map(); const metrics = new Map()
const optsKey = function(opts) { const optsKey = function(opts) {
let keys = Object.keys(opts); let keys = Object.keys(opts)
if (keys.length === 0) { return ''; } if (keys.length === 0) {
return ''
}
keys = keys.sort(); keys = keys.sort()
let hash = ''; let hash = ''
for (let key of Array.from(keys)) { for (const key of Array.from(keys)) {
if (hash.length) { hash += ","; } if (hash.length) {
hash += `${key}:${opts[key]}`; hash += ','
} }
hash += `${key}:${opts[key]}`
}
return hash; return hash
}; }
const extendOpts = function(opts, labelNames) { const extendOpts = function(opts, labelNames) {
for (let label of Array.from(labelNames)) { for (const label of Array.from(labelNames)) {
if (!opts[label]) { opts[label] = ''; } if (!opts[label]) {
} opts[label] = ''
return opts; }
}; }
return opts
}
const optsAsArgs = function(opts, labelNames) { const optsAsArgs = function(opts, labelNames) {
const args = []; const args = []
for (let label of Array.from(labelNames)) { for (const label of Array.from(labelNames)) {
args.push(opts[label] || ''); args.push(opts[label] || '')
} }
return args; return args
}; }
const PromWrapper = { const PromWrapper = {
ttlInMinutes: 0, ttlInMinutes: 0,
registry, registry,
metric(type, name) { metric(type, name) {
return metrics.get(name) || new MetricWrapper(type, name); return metrics.get(name) || new MetricWrapper(type, name)
}, },
collectDefaultMetrics: prom.collectDefaultMetrics
};
collectDefaultMetrics: prom.collectDefaultMetrics
}
class MetricWrapper { class MetricWrapper {
constructor(type, name) { constructor(type, name) {
metrics.set(name, this); metrics.set(name, this)
this.name = name; this.name = name
this.instances = new Map(); this.instances = new Map()
this.lastAccess = new Date(); this.lastAccess = new Date()
this.metric = (() => { switch (type) { this.metric = (() => {
case "counter": switch (type) {
return new prom.Counter({ case 'counter':
name, return new prom.Counter({
help: name, name,
labelNames: ['app','host','status','method', 'path'] help: name,
}); labelNames: ['app', 'host', 'status', 'method', 'path']
case "summary": })
return new prom.Summary({ case 'summary':
name, return new prom.Summary({
help: name, name,
maxAgeSeconds: 60, help: name,
ageBuckets: 10, maxAgeSeconds: 60,
labelNames: ['app', 'host', 'path', 'status_code', 'method', 'collection', 'query'] ageBuckets: 10,
}); labelNames: [
case "gauge": 'app',
return new prom.Gauge({ 'host',
name, 'path',
help: name, 'status_code',
labelNames: ['app','host', 'status'] 'method',
}); 'collection',
} })(); 'query'
} ]
})
case 'gauge':
return new prom.Gauge({
name,
help: name,
labelNames: ['app', 'host', 'status']
})
}
})()
}
inc(opts, value) { inc(opts, value) {
return this._execMethod('inc', opts, value); return this._execMethod('inc', opts, value)
} }
observe(opts, value) { observe(opts, value) {
return this._execMethod('observe', opts, value); return this._execMethod('observe', opts, value)
} }
set(opts, value) { set(opts, value) {
return this._execMethod('set', opts, value); return this._execMethod('set', opts, value)
} }
sweep() { sweep() {
const thresh = new Date(Date.now() - (1000 * 60 * PromWrapper.ttlInMinutes)); const thresh = new Date(Date.now() - 1000 * 60 * PromWrapper.ttlInMinutes)
this.instances.forEach((instance, key) => { this.instances.forEach((instance, key) => {
if (thresh > instance.time) { if (thresh > instance.time) {
if (process.env['DEBUG_METRICS']) { if (process.env.DEBUG_METRICS) {
console.log("Sweeping stale metric instance", this.name, {opts: instance.opts}, key); console.log(
} 'Sweeping stale metric instance',
return this.metric.remove(...Array.from(optsAsArgs(instance.opts, this.metric.labelNames) || [])); this.name,
} { opts: instance.opts },
}); key
)
}
return this.metric.remove(
...Array.from(optsAsArgs(instance.opts, this.metric.labelNames) || [])
)
}
})
if (thresh > this.lastAccess) { if (thresh > this.lastAccess) {
if (process.env['DEBUG_METRICS']) { if (process.env.DEBUG_METRICS) {
console.log("Sweeping stale metric", this.name, thresh, this.lastAccess); console.log('Sweeping stale metric', this.name, thresh, this.lastAccess)
} }
metrics.delete(this.name); metrics.delete(this.name)
return registry.removeSingleMetric(this.name); return registry.removeSingleMetric(this.name)
} }
} }
_execMethod(method, opts, value) { _execMethod(method, opts, value) {
opts = extendOpts(opts, this.metric.labelNames); opts = extendOpts(opts, this.metric.labelNames)
const key = optsKey(opts); const key = optsKey(opts)
if (key !== '') { this.instances.set(key, { time: new Date(), opts }); } if (key !== '') {
this.lastAccess = new Date(); this.instances.set(key, { time: new Date(), opts })
return this.metric[method](opts, value); }
} this.lastAccess = new Date()
return this.metric[method](opts, value)
}
} }
if (!PromWrapper.sweepRegistered) { if (!PromWrapper.sweepRegistered) {
if (process.env['DEBUG_METRICS']) { if (process.env.DEBUG_METRICS) {
console.log("Registering sweep method"); console.log('Registering sweep method')
} }
PromWrapper.sweepRegistered = true; PromWrapper.sweepRegistered = true
setInterval( setInterval(function() {
function() { if (PromWrapper.ttlInMinutes) {
if (PromWrapper.ttlInMinutes) { if (process.env.DEBUG_METRICS) {
if (process.env['DEBUG_METRICS']) { console.log('Sweeping metrics')
console.log("Sweeping metrics"); }
} return metrics.forEach((metric, key) => {
return metrics.forEach((metric, key) => { return metric.sweep()
return metric.sweep(); })
}); }
} }, 60000)
},
60000);
} }
module.exports = PromWrapper
module.exports = PromWrapper;

View file

@ -3,45 +3,42 @@
* DS102: Remove unnecessary code created because of implicit returns * DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const chai = require('chai'); const chai = require('chai')
const should = chai.should(); const { expect } = chai
const { const path = require('path')
expect const modulePath = path.join(__dirname, '../../../event_loop.js')
} = chai; const SandboxedModule = require('sandboxed-module')
const path = require('path'); const sinon = require('sinon')
const modulePath = path.join(__dirname, '../../../event_loop.js');
const SandboxedModule = require('sandboxed-module');
const sinon = require("sinon");
describe('event_loop', function() { describe('event_loop', function() {
before(function() {
this.metrics = {
timing: sinon.stub(),
registerDestructor: sinon.stub()
}
this.logger = {
warn: sinon.stub()
}
return (this.event_loop = SandboxedModule.require(modulePath, {
requires: {
'./index': this.metrics
}
}))
})
before(function() { describe('with a logger provided', function() {
this.metrics = { before(function() {
timing: sinon.stub(), return this.event_loop.monitor(this.logger)
registerDestructor: sinon.stub() })
};
this.logger = {
warn: sinon.stub()
};
return this.event_loop = SandboxedModule.require(modulePath, { requires: {
'./index': this.metrics
}
}
);
});
describe('with a logger provided', function() { return it('should register a destructor with metrics', function() {
before(function() { return expect(this.metrics.registerDestructor.called).to.equal(true)
return this.event_loop.monitor(this.logger); })
}); })
return it('should register a destructor with metrics', function() {
return this.metrics.registerDestructor.called.should.equal(true);
});
});
return describe('without a logger provided', () => it('should throw an exception', function() {
return expect(this.event_loop.monitor).to.throw('logger is undefined');
}));
});
return describe('without a logger provided', function() {
return it('should throw an exception', function() {
return expect(this.event_loop.monitor).to.throw('logger is undefined')
})
})
})

View file

@ -4,160 +4,198 @@
* DS207: Consider shorter variations of null checks * DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
const chai = require('chai'); const chai = require('chai')
const should = chai.should(); const { expect } = chai
const { const path = require('path')
expect const modulePath = path.join(__dirname, '../../../timeAsyncMethod.js')
} = chai; const SandboxedModule = require('sandboxed-module')
const path = require('path'); const sinon = require('sinon')
const modulePath = path.join(__dirname, '../../../timeAsyncMethod.js');
const SandboxedModule = require('sandboxed-module');
const sinon = require("sinon");
describe('timeAsyncMethod', function() { describe('timeAsyncMethod', function() {
beforeEach(function() {
this.Timer = { done: sinon.stub() }
this.TimerConstructor = sinon.stub().returns(this.Timer)
this.metrics = {
Timer: this.TimerConstructor,
inc: sinon.stub()
}
this.timeAsyncMethod = SandboxedModule.require(modulePath, {
requires: {
'./index': this.metrics
}
})
beforeEach(function() { return (this.testObject = {
this.Timer = {done: sinon.stub()}; nextNumber(n, callback) {
this.TimerConstructor = sinon.stub().returns(this.Timer); return setTimeout(() => callback(null, n + 1), 100)
this.metrics = { }
Timer: this.TimerConstructor, })
inc: sinon.stub() })
};
this.timeAsyncMethod = SandboxedModule.require(modulePath, { requires: {
'./index': this.metrics
}
}
);
return this.testObject = { it('should have the testObject behave correctly before wrapping', function(done) {
nextNumber(n, callback) { return this.testObject.nextNumber(2, (err, result) => {
if (callback == null) { callback = function(err, result){}; } expect(err).to.not.exist
return setTimeout( expect(result).to.equal(3)
() => callback(null, n+1) return done()
, 100 })
); })
}
};});
it('should have the testObject behave correctly before wrapping', function(done) { it('should wrap method without error', function(done) {
return this.testObject.nextNumber(2, function(err, result) { this.timeAsyncMethod(
expect(err).to.not.exist; this.testObject,
expect(result).to.equal(3); 'nextNumber',
return done(); 'someContext.TestObject'
}); )
}); return done()
})
it('should wrap method without error', function(done) { it('should transparently wrap method invocation in timer', function(done) {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject'); this.timeAsyncMethod(
return done(); this.testObject,
}); 'nextNumber',
'someContext.TestObject'
)
return this.testObject.nextNumber(2, (err, result) => {
expect(err).to.not.exist
expect(result).to.equal(3)
expect(this.TimerConstructor.callCount).to.equal(1)
expect(this.Timer.done.callCount).to.equal(1)
return done()
})
})
it('should transparently wrap method invocation in timer', function(done) { it('should increment success count', function(done) {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject'); this.metrics.inc = sinon.stub()
return this.testObject.nextNumber(2, (err, result) => { this.timeAsyncMethod(
expect(err).to.not.exist; this.testObject,
expect(result).to.equal(3); 'nextNumber',
expect(this.TimerConstructor.callCount).to.equal(1); 'someContext.TestObject'
expect(this.Timer.done.callCount).to.equal(1); )
return done(); return this.testObject.nextNumber(2, (err, result) => {
}); if (err) {
}); return done(err)
}
expect(this.metrics.inc.callCount).to.equal(1)
expect(
this.metrics.inc.calledWith('someContext_result', 1, {
method: 'TestObject_nextNumber',
status: 'success'
})
).to.equal(true)
return done()
})
})
it('should increment success count', function(done) { describe('when base method produces an error', function() {
this.metrics.inc = sinon.stub(); beforeEach(function() {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject'); this.metrics.inc = sinon.stub()
return this.testObject.nextNumber(2, (err, result) => { return (this.testObject.nextNumber = function(n, callback) {
expect(this.metrics.inc.callCount).to.equal(1); return setTimeout(() => callback(new Error('woops')), 100)
expect(this.metrics.inc.calledWith('someContext_result', 1, { method: 'TestObject_nextNumber', status: 'success'})).to.equal(true); })
return done(); })
});
});
describe('when base method produces an error', function() { it('should propagate the error transparently', function(done) {
beforeEach(function() { this.timeAsyncMethod(
this.metrics.inc = sinon.stub(); this.testObject,
return this.testObject.nextNumber = function(n, callback) { 'nextNumber',
if (callback == null) { callback = function(err, result){}; } 'someContext.TestObject'
return setTimeout( )
() => callback(new Error('woops')) return this.testObject.nextNumber(2, (err, result) => {
, 100 expect(err).to.exist
); expect(err).to.be.instanceof(Error)
}; expect(result).to.not.exist
}); return done()
})
})
it('should propagate the error transparently', function(done) { return it('should increment failure count', function(done) {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject'); this.timeAsyncMethod(
return this.testObject.nextNumber(2, (err, result) => { this.testObject,
expect(err).to.exist; 'nextNumber',
expect(err).to.be.instanceof(Error); 'someContext.TestObject'
expect(result).to.not.exist; )
return done(); return this.testObject.nextNumber(2, (err, result) => {
}); expect(err).to.exist
}); expect(this.metrics.inc.callCount).to.equal(1)
expect(
this.metrics.inc.calledWith('someContext_result', 1, {
method: 'TestObject_nextNumber',
status: 'failed'
})
).to.equal(true)
return done()
})
})
})
return it('should increment failure count', function(done) { describe('when a logger is supplied', function() {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject'); beforeEach(function() {
return this.testObject.nextNumber(2, (err, result) => { return (this.logger = { log: sinon.stub() })
expect(this.metrics.inc.callCount).to.equal(1); })
expect(this.metrics.inc.calledWith('someContext_result', 1, { method: 'TestObject_nextNumber', status: 'failed'})).to.equal(true);
return done();
});
});
});
describe('when a logger is supplied', function() { return it('should also call logger.log', function(done) {
beforeEach(function() { this.timeAsyncMethod(
return this.logger = {log: sinon.stub()};}); this.testObject,
'nextNumber',
'someContext.TestObject',
this.logger
)
return this.testObject.nextNumber(2, (err, result) => {
expect(err).to.not.exist
expect(result).to.equal(3)
expect(this.TimerConstructor.callCount).to.equal(1)
expect(this.Timer.done.callCount).to.equal(1)
expect(this.logger.log.callCount).to.equal(1)
return done()
})
})
})
return it('should also call logger.log', function(done) { describe('when the wrapper cannot be applied', function() {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject', this.logger); beforeEach(function() {})
return this.testObject.nextNumber(2, (err, result) => {
expect(err).to.not.exist;
expect(result).to.equal(3);
expect(this.TimerConstructor.callCount).to.equal(1);
expect(this.Timer.done.callCount).to.equal(1);
expect(this.logger.log.callCount).to.equal(1);
return done();
});
});
});
describe('when the wrapper cannot be applied', function() { return it('should raise an error', function() {
beforeEach(function() {}); const badWrap = () => {
return this.timeAsyncMethod(
this.testObject,
'DEFINITELY_NOT_A_REAL_METHOD',
'someContext.TestObject'
)
}
return expect(badWrap).to.throw(
/^.*expected object property 'DEFINITELY_NOT_A_REAL_METHOD' to be a function.*$/
)
})
})
return it('should raise an error', function() { return describe('when the wrapped function is not using a callback', function() {
const badWrap = () => { beforeEach(function() {
return this.timeAsyncMethod(this.testObject, 'DEFINITELY_NOT_A_REAL_METHOD', 'someContext.TestObject'); this.realMethod = sinon.stub().returns(42)
}; return (this.testObject.nextNumber = this.realMethod)
return expect(badWrap).to.throw( })
/^.*expected object property 'DEFINITELY_NOT_A_REAL_METHOD' to be a function.*$/
);
});
});
return describe('when the wrapped function is not using a callback', function() {
beforeEach(function() {
this.realMethod = sinon.stub().returns(42);
return this.testObject.nextNumber = this.realMethod;
});
it('should not throw an error', function() {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject');
const badCall = () => {
return this.testObject.nextNumber(2);
};
return expect(badCall).to.not.throw(Error);
});
return it('should call the underlying method', function() {
this.timeAsyncMethod(this.testObject, 'nextNumber', 'someContext.TestObject');
const result = this.testObject.nextNumber(12);
expect(this.realMethod.callCount).to.equal(1);
expect(this.realMethod.calledWith(12)).to.equal(true);
return expect(result).to.equal(42);
});
});
});
it('should not throw an error', function() {
this.timeAsyncMethod(
this.testObject,
'nextNumber',
'someContext.TestObject'
)
const badCall = () => {
return this.testObject.nextNumber(2)
}
return expect(badCall).to.not.throw(Error)
})
return it('should call the underlying method', function() {
this.timeAsyncMethod(
this.testObject,
'nextNumber',
'someContext.TestObject'
)
const result = this.testObject.nextNumber(12)
expect(this.realMethod.callCount).to.equal(1)
expect(this.realMethod.calledWith(12)).to.equal(true)
return expect(result).to.equal(42)
})
})
})

View file

@ -8,59 +8,77 @@
*/ */
module.exports = function(obj, methodName, prefix, logger) { module.exports = function(obj, methodName, prefix, logger) {
let modifedMethodName; let modifedMethodName
const metrics = require('./index'); const metrics = require('./index')
if (typeof obj[methodName] !== 'function') { if (typeof obj[methodName] !== 'function') {
throw new Error(`[Metrics] expected object property '${methodName}' to be a function`); throw new Error(
} `[Metrics] expected object property '${methodName}' to be a function`
)
}
const key = `${prefix}.${methodName}`; const key = `${prefix}.${methodName}`
const realMethod = obj[methodName]; const realMethod = obj[methodName]
const splitPrefix = prefix.split("."); const splitPrefix = prefix.split('.')
const startPrefix = splitPrefix[0]; const startPrefix = splitPrefix[0]
if (splitPrefix[1] != null) { if (splitPrefix[1] != null) {
modifedMethodName = `${splitPrefix[1]}_${methodName}`; modifedMethodName = `${splitPrefix[1]}_${methodName}`
} else { } else {
modifedMethodName = methodName; modifedMethodName = methodName
} }
return obj[methodName] = function(...originalArgs) { return (obj[methodName] = function(...originalArgs) {
const adjustedLength = Math.max(originalArgs.length, 1)
const firstArgs = originalArgs.slice(0, adjustedLength - 1)
const callback = originalArgs[adjustedLength - 1]
const adjustedLength = Math.max(originalArgs.length, 1), firstArgs = originalArgs.slice(0, adjustedLength - 1), callback = originalArgs[adjustedLength - 1]; if (callback == null || typeof callback !== 'function') {
if (logger != null) {
logger.log(
`[Metrics] expected wrapped method '${methodName}' to be invoked with a callback`
)
}
return realMethod.apply(this, originalArgs)
}
if ((callback == null) || (typeof callback !== 'function')) { const timer = new metrics.Timer(startPrefix, 1, {
if (logger != null) { method: modifedMethodName
logger.log(`[Metrics] expected wrapped method '${methodName}' to be invoked with a callback`); })
}
return realMethod.apply(this, originalArgs);
}
const timer = new metrics.Timer(startPrefix, 1, {method: modifedMethodName}); return realMethod.call(this, ...Array.from(firstArgs), function(
...callbackArgs
return realMethod.call(this, ...Array.from(firstArgs), function(...callbackArgs) { ) {
const elapsedTime = timer.done(); const elapsedTime = timer.done()
const possibleError = callbackArgs[0]; const possibleError = callbackArgs[0]
if (possibleError != null) { if (possibleError != null) {
metrics.inc(`${startPrefix}_result`, 1, {status:"failed", method: modifedMethodName}); metrics.inc(`${startPrefix}_result`, 1, {
} else { status: 'failed',
metrics.inc(`${startPrefix}_result`, 1, {status:"success", method: modifedMethodName}); method: modifedMethodName
} })
if (logger != null) { } else {
const loggableArgs = {}; metrics.inc(`${startPrefix}_result`, 1, {
try { status: 'success',
for (let idx = 0; idx < firstArgs.length; idx++) { method: modifedMethodName
const arg = firstArgs[idx]; })
if (arg.toString().match(/^[0-9a-f]{24}$/)) { }
loggableArgs[`${idx}`] = arg; if (logger != null) {
} const loggableArgs = {}
} try {
} catch (error) {} for (let idx = 0; idx < firstArgs.length; idx++) {
logger.log({key, args: loggableArgs, elapsedTime}, "[Metrics] timed async method call"); const arg = firstArgs[idx]
} if (arg.toString().match(/^[0-9a-f]{24}$/)) {
return callback.apply(this, callbackArgs); loggableArgs[`${idx}`] = arg
}); }
}; }
}; } catch (error) {}
logger.log(
{ key, args: loggableArgs, elapsedTime },
'[Metrics] timed async method call'
)
}
return callback.apply(this, callbackArgs)
})
})
}

View file

@ -1,4 +1,4 @@
if (!process.env.UV_THREADPOOL_SIZE) { if (!process.env.UV_THREADPOOL_SIZE) {
process.env.UV_THREADPOOL_SIZE=16; process.env.UV_THREADPOOL_SIZE = 16
console.log(`Set UV_THREADPOOL_SIZE=${process.env.UV_THREADPOOL_SIZE}`); console.log(`Set UV_THREADPOOL_SIZE=${process.env.UV_THREADPOOL_SIZE}`)
} }