UNPKG

loadmill-monitor

Version:

Performance monitoring for Node.js

196 lines (153 loc) 4.54 kB
const WebSocket = require('ws'); const querystring = require('querystring'); const numCpus = require('os').cpus().length; const Sampler = require('./sampler'); const reopenDelay = 5000; const minReportingDelay = 5000; const monitorClientVersion = '1.0.0'; class Monitor { constructor (options) { if (!options) { throw Error("Missing or invalid options object"); } this.apiToken = options.apiToken; if (!isNonEmptyString(this.apiToken)) { throw Error("Missing or invalid apiToken"); } this.envName = processName(options.envName, 'envName', 'default_env'); this.appName = processName(options.appName, 'appName', 'default_app'); this.destinationHost = options._destinationHost || 'monitor.loadmill.com'; this.sampler = new Sampler(); } listen() { if (this.ws) { return; } const queryPart = querystring.stringify({ t: this.apiToken, e: this.envName, a: this.appName, v: monitorClientVersion, p: `Node ${process.version}` }); this.ws = new WebSocket( `wss://${this.destinationHost}/monitor/metrics/?${queryPart}`//, {rejectUnauthorized: false} ); this.ws.on('unexpected-response', this._handleUnexpectedResponse.bind(this)); this._bindHandler('open', this._onOpen); this._bindHandler('error', this._onError); this._bindHandler('close', this._onClose); this._bindHandler('message', this._onMessage); } _handleUnexpectedResponse(req, res) { this._onClose({ code: res.statusCode === 401 ? 3000 : 1000, reason: "Server response was " + res.statusCode + " " + res.statusMessage }); } _bindHandler(event, handler) { this.ws['on' + event] = handler.bind(this); } _onOpen() { if (this.ws) { this.ws._socket.unref(); } } _onError(event) { if (this.ws) { console.error("Socket error (loadmill monitoring):", event && event.error || 'Error unknown'); this.ws.close(); } } _onClose(event) { if (event && event.code === 3000) { console.error('Terminating Loadmill monitoring:', event.reason || 'Reason unknown'); this.sampler.stopSampling(); } else { this.ws = null; setTimeout(this.listen.bind(this), reopenDelay) .unref(); } } _onMessage(event) { const msg = JSON.parse(event.data); switch (msg.type) { case 'start_monitor': return this._updateSampler(msg); case 'stop_monitor': return this._stopSampler(msg); default: break; } } _updateSampler(msg) { const stopTime = Date.now() + msg.duration; this.sampler.sampleUntil(stopTime, this._onSample.bind(this)); } _stopSampler() { this.sampler.stopSampling(); } _onSample() { if (this.ws && this.ws.readyState === WebSocket.OPEN && Date.now() - this.sampler.lastPopTime >= minReportingDelay) { const samplesReport = prepareSamples(this.sampler.popSamples()); if (samplesReport.t.length > 0) { this.ws.send(JSON.stringify(samplesReport)); } } } } module.exports = Monitor; function processName(value, key, defaultValue) { const resolved = isNonEmptyString(value) ? value : defaultValue; if (!isValidName(resolved)) { throw Error(`Invalid ${key}: [${value}]`); } return resolved; } function isNonEmptyString(value) { return value && typeof value === 'string'; } function prepareSamples(samples) { const t = [], tc = [], uc = [], hm = [], uhm = [], rss = []; samples.forEach(function (sample) { const duration = sample.duration; // Sample times must be strictly increasing: if (duration > 0) { t.push(sample.timestamp); const memoryUsage = sample.memoryUsage; rss.push(memoryUsage.rss); hm.push(memoryUsage.heapTotal); uhm.push(memoryUsage.heapUsed); const cpuUsage = sample.cpuUsage; const userCpu = getCpuPercentage(cpuUsage.user, duration); uc.push(userCpu); const systemCpu = getCpuPercentage(cpuUsage.system, duration); tc.push(systemCpu + userCpu); } }); return { t, tc, uc, hm, uhm, rss, type: "samples" }; } function getCpuPercentage(cpuTimeMicros, clockTime) { const totalPercentage = clockTime <= 0 ? 0 : 100 * (microToMilli(cpuTimeMicros) / clockTime); return totalPercentage / numCpus; } function microToMilli(microSeconds) { return microSeconds / 1000; } function isValidName(name) { return !/.*([^\-_a-zA-Z0-9]).*/.exec(name); }