UNPKG

os-monitor

Version:
412 lines (343 loc) 11.1 kB
// OS Monitoring for Node.js // Copyright (c) 2012-2025 lfortin // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const os = require('node:os'), fs = require('node:fs'), stream = require('node:stream'), throttle = require('lodash.throttle'), { version } = require('./package.json'), critical: number = os.cpus().length; // eslint-disable-next-line one-var const EventTypes = { MONITOR: "monitor", UPTIME: "uptime", FREEMEM: "freemem", DISKFREE: "diskfree", LOADAVG1: "loadavg1", LOADAVG5: "loadavg5", LOADAVG15: "loadavg15", START: "start", STOP: "stop", CONFIG: "config", RESET: "reset", DESTROY: "destroy" } as const; export class Monitor extends stream.Readable { constructor() { super({ highWaterMark: 102400, emitClose: true }); } public get version(): string { return version; } public get constants(): MonitorConstants { return { events: EventTypes, defaults: { delay : 3000, critical1 : critical, critical5 : critical, critical15: critical, freemem : 0, uptime : 0, diskfree : {}, silent : false, stream : false, immediate : false } }; } // expose main Monitor class public Monitor: typeof Monitor = Monitor; // expose OS module public os = os; private _monitorState: MonitorState = { running: false, ended: false, streamBuffering: true, interval: undefined, config: Monitor.prototype.constants.defaults, throttled: [] }; // readable stream implementation requirement private _read(): void { this._monitorState.streamBuffering = true; } public sendEvent(event: EventType, obj: Partial<InfoObject> = {}): Monitor { const eventObject: EventObject = {...obj, type: event, timestamp: Math.floor(Date.now() / 1000)}; // for EventEmitter this.emit(event, eventObject); // for readable Stream if(this._readableState.flowing) { this._monitorState.streamBuffering = true; } if(this.config().stream && this._monitorState.streamBuffering) { const prettyJSON = JSON.stringify(eventObject, null, 2); if(!this.push(`${os.EOL}${prettyJSON}`) && !this._readableState.flowing) { this._monitorState.streamBuffering = false; } } return this; } private _createInfoObject(): InfoObject { return { loadavg : os.loadavg(), uptime : os.uptime(), freemem : os.freemem(), totalmem : os.totalmem() }; } private async _cycle(): Promise<void> { const info: InfoObject = this._createInfoObject(), config = this.config(); if(config.diskfree && Object.keys(config.diskfree).length) { const deferreds: Array<Promise<void>> = []; for(const path in config.diskfree) { const deferredStats: Promise<StatFs> = fs.promises.statfs(path), deferred: Promise<void> = deferredStats.then(stats => { info.diskfree = Object.assign(info.diskfree || {}, {[path]: stats.bfree}); }, (err: unknown) => { this.emit('error', err); }); deferreds.push(deferred); } await Promise.all(deferreds); for(const path in config.diskfree) { const dfConfig: number = config.diskfree[path]; if(info.diskfree && info.diskfree[path] < dfConfig) { this.sendEvent(this.constants.events.DISKFREE, info); } } } this._sendEvents(info); } private _sendEvents(info: InfoObject): void { const config = this.config(), freemem = (config.freemem < 1) ? config.freemem * info.totalmem : config.freemem; if(!config.silent) { this.sendEvent(this.constants.events.MONITOR, info); } if(info.loadavg[0] > config.critical1) { this.sendEvent(this.constants.events.LOADAVG1, info); } if(info.loadavg[1] > config.critical5) { this.sendEvent(this.constants.events.LOADAVG5, info); } if(info.loadavg[2] > config.critical15) { this.sendEvent(this.constants.events.LOADAVG15, info); } if(info.freemem < freemem) { this.sendEvent(this.constants.events.FREEMEM, info); } if(Number(config.uptime) && info.uptime > Number(config.uptime)) { this.sendEvent(this.constants.events.UPTIME, info); } } public start(options?: Partial<ConfigObject>): Monitor { if(this._isEnded()) { this.emit('error', new Error("monitor has been ended by .destroy() method")); return this; } if(options) { this._validateConfig(options); } this.stop() .config(options); if(this.config().immediate) { process.nextTick(() => this._cycle()); } this._monitorState.interval = setInterval(() => this._cycle(), this.config().delay); this._monitorState.running = true; this.sendEvent(this.constants.events.START); return this; } public stop(): Monitor { clearInterval(this._monitorState.interval); if(this.isRunning()) { this._monitorState.running = false; this.sendEvent(this.constants.events.STOP); } return this; } public reset(): Monitor { this.sendEvent(this.constants.events.RESET); this[this.isRunning() ? 'start' : 'config'](this.constants.defaults); return this; } public destroy(err?: unknown): Monitor { if(!this._isEnded()) { this.sendEvent(this.constants.events.DESTROY); this.stop(); stream.Readable.prototype.destroy.apply(this, [err]); this._monitorState.ended = true; } return this; } public config(options?: Partial<ConfigObject>): ConfigObject { if(options !== null && typeof options === 'object') { this._validateConfig(options); Object.assign(this._monitorState.config, options); this.sendEvent(this.constants.events.CONFIG, { options: { ...options } }); } return this._monitorState.config; } private _validateConfig(options: Partial<ConfigObject>): void { if(options.diskfree && Object.keys(options.diskfree).length && !fs.statfs) { throw new Error("diskfree not supported"); } } public isRunning(): boolean { return !!this._monitorState.running; } private _isEnded(): boolean { return !!this._monitorState.ended; } public throttle(event: EventType, handler: EventHandler, wait: number): Monitor { if(typeof handler !== 'function') { throw new Error("Handler must be a function"); } const _handler: EventHandler = (eventObject: EventObject) => { if(this.isRunning()) { handler.apply(this, [eventObject]); } }, throttledFn: EventHandler = throttle(_handler, wait || this.config().throttle); this._monitorState.throttled.push({event: event, originalFn: handler, throttledFn: throttledFn}); return this.on(event, throttledFn); } public unthrottle(event: EventType, handler: EventHandler): Monitor { const {throttled} = this._monitorState; for(let i = throttled.length - 1; i >= 0; i--) { const pair = throttled[i]; if(pair.event === event && pair.originalFn === handler) { this.removeListener(event, pair.throttledFn); throttled.splice(i, 1); } } return this; } public when(event: EventType): Promise<EventObject> { return new Promise((resolve) => { this.once(event, (eventObj: EventObject) => resolve(eventObj)); }); } /* * convenience methods */ private _sanitizeNumber(n: number): number { if(!Number.isInteger(n)) { throw new Error("Integer expected"); } if(!n || n < 0) { throw new Error("Number must be greater than 0"); } // Math.pow(2, 31); if(n >= 2147483648) { throw new Error("Number must be smaller than 2147483648"); } return n; } public seconds(n: number): number { return this._sanitizeNumber(n * 1000); } public minutes(n: number): number { return this._sanitizeNumber(n * this.seconds(60)); } public hours(n: number): number { return this._sanitizeNumber(n * this.minutes(60)); } public days(n: number): number { return this._sanitizeNumber(n * this.hours(24)); } public blocks(bytes: number, blockSize = 1): number { return Math.ceil(bytes / blockSize); } public createMonitor(options?: Partial<ConfigObject>): Monitor { const monitor = new Monitor(); monitor.config(options); return monitor; } } module.exports = new Monitor(); export type EventType = typeof EventTypes[keyof typeof EventTypes]; export interface StatFs { type : number; bsize : number; blocks: number; bfree : number; bavail: number; files : number; ffree : number; } export interface DiskfreeConfig { [key: string]: number; } export interface ConfigObject { delay : number; critical1 : number; critical5 : number; critical15: number; freemem : number; uptime : number; silent : boolean; stream : boolean; immediate : boolean; diskfree : DiskfreeConfig; throttle? : number; } interface MonitorState { running: boolean; ended: boolean; streamBuffering: boolean; interval?: NodeJS.Timeout; config: ConfigObject; throttled: Array<{ event: EventType; originalFn: EventHandler; throttledFn: EventHandler; }>; } export interface MonitorConstants { events: { [key: string]: EventType; }; defaults: ConfigObject; } export interface DiskfreeInfo { [key: string]: number; } export interface InfoObject { loadavg : Array<number>; uptime : number; freemem : number; totalmem : number; diskfree?: DiskfreeInfo; options? : Partial<ConfigObject>; } export interface EventObject extends Partial<InfoObject> { type : EventType; timestamp : number; } export interface EventHandler { (event: EventObject): void; }