lib/metrics/platforms/linux/stats.js (123 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; const fs = require('fs'); const afterAll = require('after-all-results'); const whitespace = /\s+/; class Stats { constructor(opts) { opts = opts || {}; this.files = { processFile: opts.processFile || '/proc/self/stat', memoryFile: opts.memoryFile || '/proc/meminfo', cpuFile: opts.cpuFile || '/proc/stat', }; this.previous = { cpuTotal: 0, cpuUsage: 0, memTotal: 0, memAvailable: 0, utime: 0, stime: 0, vsize: 0, rss: 0, }; this.stats = { 'system.cpu.total.norm.pct': 0, 'system.memory.actual.free': 0, 'system.memory.total': 0, 'system.process.cpu.total.norm.pct': 0, 'system.process.cpu.system.norm.pct': 0, 'system.process.cpu.user.norm.pct': 0, 'system.process.memory.size': 0, 'system.process.memory.rss.bytes': 0, }; this.inProgress = false; this.timer = null; // Do initial load const files = [ this.files.processFile, this.files.memoryFile, this.files.cpuFile, ]; try { const datas = files.map(readFileSync); this.previous = this.readStats(datas); this.update(datas); } catch (err) {} } toJSON() { return this.stats; } collect(cb) { if (this.inProgress) { if (cb) process.nextTick(cb); return; } this.inProgress = true; const files = [ this.files.processFile, this.files.memoryFile, this.files.cpuFile, ]; const next = afterAll((err, files) => { if (!err) this.update(files); if (cb) cb(); }); for (const file of files) { fs.readFile(file, next()); } } readStats([processFile, memoryFile, cpuFile]) { // CPU data // // Example of line we're trying to parse: // cpu 13978 30 2511 9257 2248 0 102 0 0 0 const cpuLine = firstLineOfBufferAsString(cpuFile); const cpuTimes = cpuLine.split(whitespace); let cpuTotal = 0; for (let i = 1; i < cpuTimes.length; i++) { cpuTotal += Number(cpuTimes[i]); } // We're off-by-one in relation to the expected index, because we include // the `cpu` label at the beginning of the line const idle = Number(cpuTimes[4]); const iowait = Number(cpuTimes[5]); const cpuUsage = cpuTotal - idle - iowait; // Memory data let memAvailable = 0; let memTotal = 0; let matches = 0; for (const line of memoryFile.toString().split('\n')) { if (/^MemAvailable:/.test(line)) { memAvailable = parseInt(line.split(whitespace)[1], 10) * 1024; matches++; } else if (/^MemTotal:/.test(line)) { memTotal = parseInt(line.split(whitespace)[1], 10) * 1024; matches++; } if (matches === 2) break; } // Process data // // Example of line we're trying to parse: // // 44 (node /app/node_) R 1 44 44 0 -1 4210688 7948 0 0 0 109 21 0 0 20 0 10 0 133652 954462208 12906 18446744073709551615 4194304 32940036 140735797366336 0 0 0 0 4096 16898 0 0 0 17 0 0 0 0 0 0 35037200 35143856 41115648 140735797369050 140735797369131 140735797369131 140735797370852 0 // // We can't just split on whitespace as the 2nd field might contain // whitespace. However, the parentheses will always be there, so we can // ignore everything from before the `)` to get rid of the whitespace // problem. // // For details about each field, see: // http://man7.org/linux/man-pages/man5/proc.5.html const processLine = firstLineOfBufferAsString(processFile); const processData = processLine .slice(processLine.lastIndexOf(')')) .split(whitespace); // all fields are referenced by their index, but are off by one because // we're dropping the first field from the line due to the whitespace // problem described above const utime = parseInt(processData[12], 10); // position in file: 14 const stime = parseInt(processData[13], 10); // position in file: 15 const vsize = parseInt(processData[21], 10); // position in file: 23 return { cpuUsage, cpuTotal, memTotal, memAvailable, utime, stime, vsize, rss: process.memoryUsage().rss, // TODO: Calculate using field 24 (rss) * PAGE_SIZE }; } update(files) { const prev = this.previous; const next = this.readStats(files); const stats = this.stats; const cpuTotal = next.cpuTotal - prev.cpuTotal; const cpuUsage = next.cpuUsage - prev.cpuUsage; const utime = next.utime - prev.utime; const stime = next.stime - prev.stime; stats['system.cpu.total.norm.pct'] = cpuUsage / cpuTotal || 0; stats['system.memory.actual.free'] = next.memAvailable; stats['system.memory.total'] = next.memTotal; // We use Math.min to guard against an edge case where /proc/self/stat // reported more clock ticks than /proc/stat, in which case it looks like // the process spent more CPU time than was used by the system. In that // case we just assume it was a 100% CPU. // // This might happen because we don't read the process file at the same // time as the system file. In between the two reads, the process will // spend some time on the CPU and hence the two reads are not 100% synced // up. const cpuProcessPercent = Math.min((utime + stime) / cpuTotal || 0, 1); const cpuProcessUserPercent = Math.min(utime / cpuTotal || 0, 1); const cpuProcessSystemPercent = Math.min(stime / cpuTotal || 0, 1); stats['system.process.cpu.total.norm.pct'] = cpuProcessPercent; stats['system.process.cpu.user.norm.pct'] = cpuProcessUserPercent; stats['system.process.cpu.system.norm.pct'] = cpuProcessSystemPercent; stats['system.process.memory.size'] = next.vsize; stats['system.process.memory.rss.bytes'] = next.rss; this.previous = next; this.inProgress = false; } } function firstLineOfBufferAsString(buff) { const newline = buff.indexOf('\n'); return buff.toString('utf8', 0, newline === -1 ? buff.length : newline); } function readFileSync(file) { return fs.readFileSync(file); } module.exports = Stats;