os-monitor
Version:
simple OS monitoring for Node.js
751 lines (739 loc) • 22.1 kB
JavaScript
const assert = require('node:assert'),
fs = require('node:fs'),
stream = require('node:stream'),
mock = require('mock-os'),
sinon = require('sinon'),
_ = require('underscore'),
semver = require('semver');
const monitor = require('../os-monitor'),
{ version } = require('../package.json');
function delay(n) {
return new Promise((resolve, reject) => setTimeout(resolve, n));
}
let tester;
if(semver.lt(process.version, '18.15.0')) {
throw 'Node.js v18.15.0 or later is required to run tests';
}
beforeEach(() => {
mock({
cpus: [{}, {}],
loadavg: [2, 2, 2],
freemem: 1000000,
totalmem: 100000000,
uptime: 1000000,
});
tester = new monitor.Monitor();
});
afterEach(() => {
tester.destroy();
mock.restore();
});
describe('Monitor class', function() {
it('should have default instance', async () => {
assert.ok(monitor instanceof monitor.Monitor);
});
it('should create new instances', async () => {
assert.ok(tester instanceof monitor.Monitor);
});
});
describe('factory method', function() {
it('should create new instances without options', async () => {
assert.ok(tester.createMonitor() instanceof monitor.Monitor);
});
it('should create new instances with options', async () => {
const monitorWithOptions = tester.createMonitor({delay: 10000});
assert.ok(monitorWithOptions instanceof monitor.Monitor);
assert.strictEqual(monitorWithOptions.config().delay, 10000);
});
});
describe('API signature', function() {
it('should have valid API signature', async () => {
// API signature
assert.ok(tester._read, "internal ._read() method expected");
assert.ok(tester.version, "version property expected");
assert.ok(tester.sendEvent, ".sendEvent() method expected");
assert.ok(tester.start, ".start() method expected");
assert.ok(tester.stop, ".stop() method expected");
assert.ok(tester.reset, ".reset() method expected");
assert.ok(tester.destroy, ".destroy() method expected");
assert.ok(tester.config, ".config() method expected");
assert.ok(tester.isRunning, ".isRunning() method expected");
assert.ok(tester.throttle, ".throttle() method expected");
assert.ok(tester.unthrottle, ".unthrottle() method expected");
assert.ok(tester.when, ".when() method expected");
assert.ok(tester.seconds, ".seconds() method expected");
assert.ok(tester.minutes, ".minutes() method expected");
assert.ok(tester.hours, ".hours() method expected");
assert.ok(tester.days, ".days() method expected");
assert.ok(tester.blocks, ".blocks() method expected");
assert.ok(tester.Monitor, "Monitor class expected");
assert.ok(tester.os, "os object reference expected");
assert.ok(tester.constants, "constants object expected");
assert.ok(tester.createMonitor, "factory method expected");
});
it('should have readonly version', async () => {
let version = tester.version;
tester.version = '0.0.0';
assert.strictEqual(tester.version, version);
});
it('should have version from package.json', async () => {
assert.strictEqual(tester.version, version);
});
});
describe('event emitter', function() {
it('should emit start event', (done) => {
let config = Object.assign({}, tester.constants.defaults, {uptime: 1234567, immediate: true});
tester.on('start', event => {
assert.strictEqual(event.type, tester.constants.events.START);
assert.strictEqual(tester.isRunning(), true);
assert.deepStrictEqual(tester.config(), config);
done();
});
tester.start(config);
});
it('should emit stop event', (done) => {
tester.start({
immediate: true,
});
tester.on('stop', event => {
assert.strictEqual(event.type, tester.constants.events.STOP);
assert.strictEqual(tester.isRunning(), false);
done();
});
tester.stop();
});
it('should emit config event', (done) => {
tester.on('config', event => {
assert.strictEqual(event.type, tester.constants.events.CONFIG);
assert.deepStrictEqual(event.options, {stream: true, immediate: true});
let config = Object.assign({}, tester.constants.defaults, {stream: true, immediate: true});
assert.deepStrictEqual(tester.config(), config);
done();
});
tester.config({stream: true, immediate: true});
});
it('should emit reset event', (done) => {
tester.on('reset', event => {
assert.strictEqual(event.type, tester.constants.events.RESET);
done();
});
tester.reset();
});
it('should emit destroy event', (done) => {
tester.on('destroy', event => {
assert.strictEqual(event.type, tester.constants.events.DESTROY);
done();
});
tester.destroy();
});
it('should emit monitor event', (done) => {
tester.on('monitor', event => {
assert.strictEqual(event.type, tester.constants.events.MONITOR);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
assert.ok(!event.diskfree);
done();
});
tester.start({
immediate: true
});
});
it('should emit monitor event(with diskfree config)', (done) => {
const stub = sinon.stub(fs.promises, 'statfs').callsFake(async (path) => {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
});
tester.on('monitor', event => {
assert.strictEqual(event.type, tester.constants.events.MONITOR);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
assert.ok(event.diskfree);
assert.strictEqual(event.diskfree['/'], 61058895);
stub.restore();
done();
});
tester.start({
immediate: true,
diskfree: {
'/': 123
}
});
});
it('should not emit monitor event', (done) => {
tester.on('monitor', event => {
done('should not emit monitor event');
});
tester.start({
immediate: true,
silent: true,
});
setImmediate(done);
});
describe('loadavg', function() {
it('should emit loadavg1 event', (done) => {
tester.start({
critical1: 1,
immediate: true,
});
tester.on('loadavg1', event => {
assert.strictEqual(event.type, tester.constants.events.LOADAVG1);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit loadavg1 event', (done) => {
tester.on('loadavg1', event => {
done('should not emit loadavg1 event');
});
tester.start({
critical1: 3,
immediate: true,
});
setImmediate(done);
});
it('should emit loadavg5 event', (done) => {
tester.start({
critical5: 1,
immediate: true,
});
tester.on('loadavg5', event => {
assert.strictEqual(event.type, tester.constants.events.LOADAVG5);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit loadavg5 event', (done) => {
tester.on('loadavg5', event => {
done('should not emit loadavg5 event');
});
tester.start({
critical5: 3,
immediate: true,
});
setImmediate(done);
});
it('should emit loadavg15 event', (done) => {
tester.start({
critical15: 1,
immediate: true,
});
tester.on('loadavg15', event => {
assert.strictEqual(event.type, tester.constants.events.LOADAVG15);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit loadavg15 event', (done) => {
tester.on('loadavg15', event => {
done('should not emit loadavg15 event');
});
tester.start({
critical15: 3,
immediate: true,
});
setImmediate(done);
});
});
describe('freemem', function() {
it('should emit freemem event', (done) => {
tester.start({
freemem: 10000000,
immediate: true,
});
tester.on('freemem', event => {
assert.strictEqual(event.type, tester.constants.events.FREEMEM);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit freemem event', (done) => {
tester.on('freemem', event => {
done('should not emit freemem event');
});
tester.start({
freemem: 99999,
immediate: true,
});
setImmediate(done);
});
it('should emit freemem event(% config)', (done) => {
tester.start({
freemem: 0.99,
immediate: true,
});
tester.on('freemem', event => {
assert.strictEqual(event.type, tester.constants.events.FREEMEM);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit freemem event(% config)', (done) => {
tester.on('freemem', event => {
done('should not emit freemem event(% config)');
});
tester.start({
freemem: 0.01,
immediate: true,
});
setImmediate(done);
});
});
it('should emit uptime event', (done) => {
tester.start({
uptime: 10000,
immediate: true,
});
tester.on('uptime', event => {
assert.strictEqual(event.type, tester.constants.events.UPTIME);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.timestamp);
done();
});
});
it('should not emit uptime event', (done) => {
tester.on('uptime', event => {
done('should not emit uptime event');
});
tester.start({
uptime: 9999999999,
immediate: true,
});
setImmediate(done);
});
describe('diskfree', function() {
it('should emit diskfree event', (done) => {
const stub = sinon.stub(fs.promises, 'statfs').callsFake(async (path) => {
if(path === '/path1') {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
} else if(path === '/path2') {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 41058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
} else {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
}
});
tester.start({
diskfree: {'/path1': 71058895, '/path2': 1000},
immediate: true,
});
tester.on('diskfree', event => {
assert.strictEqual(event.type, tester.constants.events.DISKFREE);
assert.ok(event.loadavg);
assert.ok(event.freemem);
assert.ok(event.totalmem);
assert.ok(event.uptime);
assert.ok(event.diskfree);
assert.strictEqual(event.diskfree['/path1'], 61058895);
assert.strictEqual(event.diskfree['/path2'], 41058895);
assert.ok(event.timestamp);
assert.ok(fs.promises.statfs.calledTwice);
stub.restore();
done();
});
});
it('should not emit diskfree event', (done) => {
const stub = sinon.stub(fs.promises, 'statfs').callsFake(async (path) => {
if(path === '/path1') {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
} else if(path === '/path2') {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
} else {
return {
type: 1397114950,
bsize: 4096,
blocks: 121938943,
bfree: 61058895,
bavail: 61058895,
files: 999,
ffree: 1000000,
};
}
});
tester.on('diskfree', event => {
done('should not emit diskfree event');
});
tester.start({
diskfree: {'/path2': 1000},
immediate: true,
});
setImmediate(() => {
assert.ok(fs.promises.statfs.calledOnce);
stub.restore();
done();
});
});
it('should emit error event when fs.promises.statfs() throws', (done) => {
const stub = sinon.stub(fs.promises, 'statfs').callsFake(async () => {
throw new Error('fs.promises.statfs() failed');
});
tester.on('diskfree', event => {
done('should not emit diskfree event');
});
tester.start({
diskfree: {'/path1': 1000},
immediate: true,
});
tester.on('error', event => {
assert.ok(fs.promises.statfs.calledOnce);
stub.restore();
done();
});
});
it('should throw if fs.statfs does not exist', async () => {
const statfs = fs.statfs;
fs.statfs = undefined;
try {
assert.throws(() => {
tester.start({
diskfree: {'/path1': 1000},
});
});
} catch(err) {
fs.statfs = statfs;
throw err;
}
fs.statfs = statfs;
});
});
});
describe('readable stream', function() {
it('should read data', async () => {
tester.pause();
tester.start({
stream: true,
immediate: true,
});
assert.ok(tester.read());
});
it('should emit data event', (done) => {
tester.config({stream: true});
tester.setEncoding('utf8');
tester.resume();
process.nextTick(() => {
tester.once('data', event => {
let eventObj = JSON.parse(event);
assert.strictEqual(eventObj.type, tester.constants.events.START);
assert.ok(eventObj.timestamp);
done();
});
tester.start();
});
});
it('should pipe on writable stream', (done) => {
let writable = new stream.PassThrough();
writable.once('data', () => done());
tester.start({
stream: true,
immediate: true,
}).pipe(writable);
});
it('should stop buffering if full and not flowing', async () => {
tester.config({
stream: true,
immediate: true,
silent: false,
});
tester.push(Buffer.alloc(200000));
tester.start();
await delay(1);
assert.strictEqual(tester.push('hello'), false);
assert.strictEqual(tester._monitorState.streamBuffering, false);
tester.read();
await delay(1);
assert.strictEqual(tester.push('hello'), true);
assert.strictEqual(tester._monitorState.streamBuffering, true);
tester.pause();
tester.push(Buffer.alloc(200000));
tester.start();
await delay(1);
assert.strictEqual(tester.push('hello'), false);
assert.strictEqual(tester._monitorState.streamBuffering, false);
tester.resume();
tester.push(Buffer.alloc(200000));
tester.start();
await delay(1);
assert.strictEqual(tester.push('hello'), true);
assert.strictEqual(tester._monitorState.streamBuffering, true);
});
it('should emit close event when destroyed', (done) => {
tester.on('close', event => {
done();
});
tester.destroy();
});
});
describe('cycles', function() {
it('should execute multiple cycles', (done) => {
let cycles = 0;
tester.start({delay: 3});
tester.on('monitor', () => {
cycles++;
});
tester.on('stop', () => {
assert.ok(cycles > 2);
done();
});
setTimeout(() => tester.stop(), 50);
});
});
describe('.throttle()', function() {
it('should register throttled handler', (done) => {
let cycles = 0;
tester.throttle('monitor', event => {
if(event.type !== tester.constants.events.MONITOR) {
done(`event.type should be === '${tester.constants.events.MONITOR}'`);
}
cycles++;
}, 30);
tester.on('stop', event => {
assert.ok(cycles < 3);
done();
});
tester.start({delay: 5});
setTimeout(() => {
tester.stop();
}, 50);
});
it('should fail if handler is not a function', async () => {
assert.throws(() => {
tester.throttle('monitor', 123);
});
});
});
describe('.unthrottle()', function() {
it('should remove throttled handler', (done) => {
let cycles = 0;
let handler = (event) => {
cycles++;
}
tester.throttle('monitor', _.noop, 50);
tester.throttle('freemem', _.noop, 50);
tester.throttle('monitor', handler, 50);
tester.throttle('freemem', handler, 50);
tester.unthrottle('monitor', handler);
assert.strictEqual(tester.listenerCount(tester.constants.events.MONITOR), 1);
assert.strictEqual(tester.listenerCount(tester.constants.events.FREEMEM), 2);
tester.on('stop', event => {
assert.strictEqual(cycles, 0);
done();
});
tester.start({delay: 2});
setTimeout(() => {
tester.stop();
}, 15);
});
});
describe('.when()', function() {
it('should return a promise that resolves', async () => {
tester.start({delay: 10});
let event = await tester.when('monitor');
assert.strictEqual(event.type, 'monitor');
assert.ok(tester.when('monitor') instanceof Promise);
});
});
describe('.reset()', function() {
it('should reset config to defaults', async () => {
tester.config({
delay: 1,
freemem: 0,
uptime: 0,
critical1: 0,
critical5: 0,
critical15: 0,
immediate: true,
silent: true,
stream: true,
});
tester.reset();
assert.deepStrictEqual(tester.config(), tester.constants.defaults);
});
});
describe('.destroy()', function() {
it('should permanently stop and throw error', async () => {
tester.destroy();
assert.throws(() => {
tester.start();
});
});
it('should permanently stop and emit error event', (done) => {
tester.on('error', err => {
done();
});
tester.destroy();
tester.start();
});
});
describe('convenience methods', function() {
describe('.seconds(), .minutes(), .hours(), .days()', function() {
it('should convert seconds into ms', async () => {
assert.strictEqual(tester.seconds(1), 1000);
assert.strictEqual(tester.seconds(5), 5000);
});
it('should convert minutes into ms', async () => {
assert.strictEqual(tester.minutes(1), 60000);
assert.strictEqual(tester.minutes(5), 300000);
});
it('should convert hours into ms', async () => {
assert.strictEqual(tester.hours(1), 3600000);
assert.strictEqual(tester.hours(10), 36000000);
});
it('should convert days into ms', async () => {
assert.strictEqual(tester.days(1), 86400000);
assert.strictEqual(tester.days(2), 172800000);
});
it('should fail if not a number', async () => {
assert.throws(() => {
tester._sanitizeNumber('not a number');
});
assert.throws(() => {
tester.seconds('not a number');
});
assert.throws(() => {
tester.minutes('not a number');
});
assert.throws(() => {
tester.hours('not a number');
});
assert.throws(() => {
tester.days('not a number');
});
});
it('should fail if < 0', async () => {
assert.throws(() => {
tester._sanitizeNumber(-1);
});
assert.throws(() => {
tester.seconds(-1);
});
assert.throws(() => {
tester.minutes(-1);
});
assert.throws(() => {
tester.hours(-1);
});
assert.throws(() => {
tester.days(-1);
});
});
it('should fail if > 2147483648', async () => {
assert.throws(() => {
tester._sanitizeNumber(2147483649);
});
assert.throws(() => {
tester.seconds(2147483649);
});
assert.throws(() => {
tester.minutes(2147483649);
});
assert.throws(() => {
tester.hours(2147483649);
});
assert.throws(() => {
tester.days(2147483649);
});
});
});
describe('.blocks()', function() {
it('should convert bytes into blocks', async () => {
assert.strictEqual(tester.blocks(2048, 2048), 1);
});
it('should convert bytes into blocks using default blockSize', async () => {
assert.strictEqual(tester.blocks(2048), 2048);
});
});
});
describe('chainable methods', function() {
it('should support chainable methods', async () => {
tester.start()
.reset()
.stop()
.throttle('start', _.noop)
.unthrottle('start', _.noop)
.sendEvent('start')
.destroy();
});
});
describe('.isRunning()', function() {
it('should check if monitor is running', async () => {
assert.strictEqual(tester.isRunning(), false);
tester.start();
assert.strictEqual(tester.isRunning(), true);
tester.stop();
assert.strictEqual(tester.isRunning(), false);
});
});