UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

206 lines (170 loc) 6.11 kB
'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() }) files.forEach(function (file) { 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 (let 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