loadmill-monitor
Version:
Performance monitoring for Node.js
196 lines (153 loc) • 4.54 kB
JavaScript
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);
}