UNPKG

node-red-contrib-prib-functions

Version:
322 lines (311 loc) 12.5 kB
const logger = new (require("node-red-contrib-logger"))("Monitor System") logger.sendInfo("Copyright 2020 Jaroslav Peter Prib"); const os = require('node:os'); const v8 = require('node:v8'); const { memoryUsage, resourceUsage } = require('node:process') const fs = require('fs') const path = require("path"); const svgObject = require("./svgObjects.js"); const resourceLabels={ userCPUTime: "User Time (ms)", systemCPUTime: "System Time (ms)", maxRSS: "Max RSS (kb)", // sharedMemorySize": "Shared Memeory", //<integer> maps to ru_ixrss but is not supported by any platform." // unsharedDataSize <integer> maps to ru_idrss but is not supported by any platform. // unsharedStackSize <integer> maps to ru_isrss but is not supported by any platform. minorPageFault: "Minor Page Faults", majorPageFault: "Major Page Faults", // swappedOut: // <integer> maps to ru_nswap but is not supported by any platform. fsRead: "Reads", fsWrite: "Writes", //ipcSent <integer> maps to ru_msgsnd but is not supported by any platform. //ipcReceived <integer> maps to ru_msgrcv but is not supported by any platform. //signalsCount <integer> maps to ru_nsignals but is not supported by any platform. // voluntaryContextSwitches: "Voluntary Context Switches" // This field is not supported on Windows. // involuntaryContextSwitches: "Involuntary Context Switches" // This field is not supported on Window } const chartLine=new svgObject.Chart({type:"line", labels:["RSS: Resident Set Size","Heap Total","Heap Used","External","Array Buffer","Free Memory","Total Memory"]}) const getPoints=(samples,metrics,metric,...properties)=>{ const points=[],size=Math.min(samples,metrics.length) for(let i=0; i<size ; i++){ const m=metrics[i][metric] points.push(properties.map(p=>m[p])) } return points } const getDiffPoints=(samples,metrics,metric,...properties)=>{ const points=getPoints(samples+1,metrics,metric,...properties) return Array.from({ length: points.length-1}, (_, i) =>{ const next=points[i+1] return points[i].map((c,j)=>c-next[j]) }) } const properties=Object.keys(resourceLabels) const labels=properties.map(c=>resourceLabels[c]) const chartLineResource=new svgObject.Chart({type:"line",labels:labels,normaliseDimension:"column", getData:()=>getDiffPoints(11,metrics,"resourceUsage",...properties) }) let svgHTMLCache const { performance, PerformanceObserver, } = require("node:perf_hooks"); const { action, children } = require("./defs.js"); const obs = new PerformanceObserver(list => { const entry = list.getEntries()[0] }) /* https://nodejs.org/api/perf_hooks.html PerformanceEntry { name: 'gc', entryType: 'gc', startTime: 2820.567669, duration: 1.315709, kind: 1 } */ process.resourceUsage() const systemMetrics=()=>{ return { freeMemory: os.freemem(), heapCodeStatistics:v8.getHeapCodeStatistics(), heapSpaceStatistics:v8.getHeapSpaceStatistics(), heapStatistics:v8.getHeapStatistics(), memoryUsage:memoryUsage(), loadAvg: os.loadavg(), timestamp: Date.now(), resourceUsage: resourceUsage(), totalMemory: os.totalmem(), uptime: os.uptime() }; } const metricLabel={ freeMemory: "Free Memory", heapCodeStatistics:"Heap Code", heapSpaceStatistics:"Heap Space", heapStatistics:"Heap", memoryUsage:"Memory Usage", loadAvg: "Load avg", resourceUsage: "Resource Usage", totalMemory: "Total Memory" } const svcTextLine=(metric,value)=>{ return {action:"text",id:metric,height:10 ,children:[metricLabel[metric]+": "+(typeof value== "object"?JSON.stringify(value):value)]} } const svcTextLineUpdate=(metric,value)=>{ return {action:"update",id:metric,children:[metricLabel[metric]+": "+(typeof value== "object"?JSON.stringify(value):value)]} } const mappingMemory=["freeMemory","memoryUsage.","totalMemory",] const getPointsMemory=()=>{ const points=[],size=Math.min(11,metrics.length) for(let i=0; i<size ; i++){ const mi=metrics[i] const m=mi.memoryUsage points.push([m.rss,m.heapTotal,m.heapUsed,m.external,m.arrayBuffers,mi.freeMemory,mi.totalMemory]) } return points } //const gapAngle=100 //const dialAngleArea=360-gapAngle const memoryDial=new svgObject.DialGauge({title:"% used memory"}) const metricFunction={ chartMemoryUsage: ()=>({action:"g",id:"chartMemoryUsage",height:100,children:[chartLine.setData(getPointsMemory()).get()]}), freeMemory: svcTextLine, gaugeMemory: ()=>{ const currentState=metrics[0] memoryDial.setMax(currentState.totalMemory) return memoryDial.get(currentState.totalMemory-currentState.memoryUsage) }, heapCodeStatistics:svcTextLine, heapSpaceStatistics:svcTextLine, heapStatistics:svcTextLine, memoryUsage:svcTextLine, loadAvg:svcTextLine, resourceUsage:svcTextLine, totalMemory:svcTextLine, chartLineResource:()=>({action:"g",id:"chartLineResource",height:100,children:[chartLineResource.get()]}) } const metricFunctionUpdate={ chartMemoryUsage: ()=>chartLine.setData(getPointsMemory()).getUpdate(), freeMemory: svcTextLineUpdate, gaugeMemory: ()=>{ const currentState=metrics[0] return memoryDial.setMax(currentState.totalMemory).getUpdate(currentState.totalMemory-currentState.freeMemory) }, heapCodeStatistics:svcTextLineUpdate, heapSpaceStatistics:svcTextLineUpdate, heapStatistics:svcTextLineUpdate, memoryUsage:svcTextLineUpdate, loadAvg:svcTextLineUpdate, resourceUsage:svcTextLineUpdate, totalMemory:svcTextLineUpdate, chartLineResource:()=>chartLineResource.getUpdate() } let metrics=[] const nodes=[] function getJson(){ return nodes.reduce((p,node)=>{ p[node.id]=metrics.length?node.getSendData(metrics[0]):null return p; },{}) } function updateJson(){ return nodes.reduce((p,node)=>{ try{ if(metrics[0]) p[node.id]=node.getSendDataUpdate(metrics[0]) } catch (ex) { logger.active&&logger.sendErrorAndStackDump("failed for "+node.id,ex) } return p; },{}) } let runtimeTimer function runtimeStop() { if(runtimeTimer) { clearTimeout(runtimeTimer) runtimeTimer=null } nodes.forEach(node=>{ node.status({fill:"red",shape:"ring",text:"Stopped"}) }) } const reset=()=>{ metrics=[]; nodes.forEach(node=>node.status({fill:"yellow",shape:"ring",text:"Metrics Reset"})) } const runtimeStart=(node)=>{ logger.active&&logger.send({ label: 'start system monitoring'}) if(runtimeTimer) clearTimeout(runtimeTimer) runtimeTimer=setInterval(function(){checkLoop();},1000); nodes.forEach(node=>{ node.status({fill:"green",shape:"ring",text:"Collecting"}) }) } function checkLoop() { logger.active&&logger.send({ label: 'checkloop collect state'}) const currentState=systemMetrics(); metrics.unshift(currentState); } module.exports = function (RED) { function nodeFunction(n) { RED.nodes.createNode(this, n); let node=Object.assign(this,n); if(runtimeTimer) node.status({fill:"green",shape:"ring",text:"Collecting"}) else node.status({fill:"yellow",shape:"ring",text:"Initial state"}); const selectedMetrics=node.metrics.split(',') const sendData=selectedMetrics.reduce((p,c,i)=>{ p.push("metricFunction."+c+"('"+c+"',metricSample."+c+")") return p },[]).join(",") const sendDataUpdate=selectedMetrics.reduce((p,c,i)=>{ p.push("metricFunctionUpdate."+c+"('"+c+"',metricSample."+c+")") return p },[]).join(",") try{ logger.active&&logger.info({label:"sendData",function:sendData}) node.getSendData=eval("(metricSample)=>svgObject.calculatePositionVertically(["+sendData+"],60)") const evalText="(metricSample)=>["+sendDataUpdate+"]" logger.active&&logger.debug({ label: 'eval', node:{id: node.id, name: node.name},eval:evalText}) node.getSendDataUpdate=eval(evalText) } catch(ex) { node.getSendData=()=>{return [{action:"text",x:0 ,y:60 ,children:["failed: "+ex.message]}]} logger.active&&logger.error({ label: 'eval', node:{id: node.id, name: node.name},error:ex.message,display:display,sendData:sendData,stack:ex.stack }) node.status({fill:"red",shape:"ring",text:"error: "+ex.message}); return } node.on('close', function (removed, done) { nodes=[] done() }) nodes.push(node) if(!runtimeTimer) { runtimeStart(node) } } const url='/' + logger.label.replace(" ","") + '/' RED.httpAdmin.get(url + ':id/:action/', RED.auth.needsPermission(logger.label + '.write'), function (req, res) { const node = RED.nodes.getNode(req.params.id); if (node && node.type===logger.label) { try { switch (req.params.action) { case "getBase": res.status(200).json(node.getBaseSVG()) break case "Start": runtimeStart() node.warn("Request to start monitor system"); break case "Stop": runtimeStop() node.warn("Request to stop monitor system"); break case "observeGC": obs.observe({ entryTypes: ['gc'] }); node.warn("Request to observe gc"); break case "observeDisconnect": obs.disconnect(); node.warn("Request to observe disconnect") break case "trace gc on": v8.setFlagsFromString('--trace-gc') node.warn("Request to --trace-gc"); break case "trace gc off": v8.setFlagsFromString('--notrace-gc') node.warn("Request to --notrace-gc"); break default: throw Error("invalid action "+req.params.action) } res.sendStatus(200); } catch(ex) { var reason1='Internal Server Error, monitor system failed '+ex.toString(); logger.error(reason1); res.status(500).send(reason1); logger.active&&logger.error(ex.stack); } } else { var reason2="request to reset monitor flow failed for id:" +req.params.id; node.error(reason1); res.status(404).send(reason2); } }); RED.httpAdmin.get(url + ':action/', RED.auth.needsPermission( logger.label + '.read'), function (req, res) { try { logger.active&&logger.info({ label: ' httpAdmin '+req.params.action}) switch (req.params.action) { case "svgHTML": if(svgHTMLCache) { res.type('json').status(200).send(svgHTMLCache) } else { fs.readFile(path.join(__dirname,"svgHTML.js"), "utf8", (err, data) => { if(err) { logger.error("svgHTML get error: "+err) res.status(500).send("NOT FOUND") } else { svgHTMLCache=data res.type('json').status(200).send(data); } }) } break case "getDataSVG": res.status(200).json(getJson()) break; case "getDataUpdateSVG": res.status(200).json(updateJson()) break; default: throw Error("invalid action "+req.params.action) } } catch(ex) { var reason1='Internal Server Error, monitor system failed '+ex.toString(); logger.error(reason1); res.status(500).send(reason1); logger.active&&logger.error(ex.stack); } }); RED.nodes.registerType(logger.label,nodeFunction); };