diff --git a/README.md b/README.md index 800d1f6..e367209 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,27 @@ Boundary Process CPU Plugin --------------------------- -Displays CPU usage (%) for specific processes. Uses regular expressions to specify a process name, process full path, and/or the process current working directory. As above, currently only works for Linux based systems that support procfs (i.e. have a /proc directory). **Note**: to monitor processes with elevated priviledges requires running the meter as root, which is not recommended. +Displays CPU usage (%) for specific processes. + +#### For Boundary Meter V4.0 +Uses lua pattern to specify a process name. + +#### For Boundary Meter less than V4.0 +Uses regular expressions to specify a process name, process full path, and/or the process current working directory. As above, currently only works for Linux based systems that support procfs (i.e. have a /proc directory). **Note**: to monitor processes with elevated priviledges requires running the meter as root, which is not recommended. ### Prerequisites +#### For Boundary Meter V4.0 +| OS | Linux | Windows | SmartOS | OS X | +|:----------|:-----:|:-------:|:-------:|:----:| +| Supported | v | v | v | v | + + +| Runtime | node.js | Python | Java | +|:---------|:-------:|:------:|:----:| +| Required | | | | + +#### For Boundary Meter less than V4.0 | OS | Linux | Windows | SmartOS | OS X | |:----------|:-----:|:-------:|:-------:|:----:| | Supported | v | - | - | - | @@ -21,6 +38,16 @@ None #### Plugin Configuration Fields +#### For Boundary Meter V4.0 +|Field Name |Description | +|:----------------|:------------------------------------------------------------| +|PollInterval |Interval to query the process | +|Items |Array of items to watch | +|Item Source |The source to display in the legend for the CPU data. | +|Item ProcessName |Pattern to match the name of the process | +|Item Reconcile |How to reconcile in the case that multiple processes match. Set to 'first' (default) to use the first matching process, 'parent' (*nix only) to choose the parent process (useful if process is forked), or 'uptime' (linux only) to pick the process that has been running the longest. | + +#### For Boundary Meter less than V4.0 |Field Name |Description | |:-----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |Source |The source to display in the legend for the CPU data. | @@ -31,6 +58,7 @@ None ### Metrics Collected +#### For All Versions |Metric Name|Description | |:----------|:-------------------------------| |CPU Process|Process specific CPU utilization| diff --git a/index.lua b/index.lua new file mode 100644 index 0000000..708e0a8 --- /dev/null +++ b/index.lua @@ -0,0 +1,98 @@ +-- [boundary.com] Process CPU Lua Plugin +-- [author] Ivano Picco + +-- Common requires. +local utils = require('utils') +local timer = require('timer') +local fs = require('fs') +local json = require('json') +local os = require ('os') +local tools = require ('tools') + +local success, boundary = pcall(require,'boundary') +if (not success) then + boundary = nil +end + +-- Business requires. +local string = require ("string") +local childProcess = require ('childprocess') +local table = require ('table') + +local osType = string.lower(os.type()) +local isWindows = osType == 'win32' +local isLinux = osType == 'linux' + +-- Default parameters. +local pollInterval = 15000 +local source = nil + +-- Configuration. +local _parameters = (boundary and boundary.param ) or json.parse(fs.readFileSync('param.json')) or {} + +_parameters.pollInterval = + (_parameters.pollInterval and tonumber(_parameters.pollInterval)>0 and tonumber(_parameters.pollInterval)) or + pollInterval; + +_parameters.source = + (type(_parameters.source) == 'string' and _parameters.source:gsub('%s+', '') ~= '' and _parameters.source ~= nil and _parameters.source) or + os.hostname() + +-- Back-trail. +local previousValues={} +local currentValues={} + +-- Get difference between current and previous time value (format: [dd-]hh:mm:ss). +function diffTimeValues(source,name) + local _cur = currentValues[source][name] or 0 + --convert cur value into timestamp + local t = tools.split(_cur,"-") --days + local days = (#t>1) and table.remove(t,1) or 0 + local time = isWindows and tools.split(t[1],".") or tools.split(t[1],":") -- hours, minutes , seconds + local cur = (days*24*60*60) + (time[1]*60*60) + (time[2]*60) + time[3] + + local last = previousValues[source][name] or cur or 0 + previousValues[source][name] = cur + + return (tonumber(cur) - tonumber(last)) +end + +-- print results +function outputs(cfg) + + utils.print('CPU_PROCESS',(diffTimeValues(cfg.processName, 'time')*1000*100)/_parameters.pollInterval, cfg.source) + +end + +-- Get current values. +function poll(cfg) + --get stat + tools.findProcStat(cfg, + function (err,proc) + if (err) then + --reset previous metrics + currentValues[cfg.processName]={}; + previousValues[cfg.processName]={}; + utils.debug(err) + return + end + + currentValues[cfg.processName] = proc + outputs(cfg) + end) + +end + +-- Ready, go. +if (#_parameters.items >0 ) then + for _,item in ipairs(_parameters.items) do + item.source = item.source or _parameters.source --default hostname + currentValues[item.processName]={}; + previousValues[item.processName]={}; + poll(item) + timer.setInterval(_parameters.pollInterval,poll,item) + end +else + utils.debug("No configuration found") +end + diff --git a/modules/tools.lua b/modules/tools.lua new file mode 100644 index 0000000..b46d65c --- /dev/null +++ b/modules/tools.lua @@ -0,0 +1,179 @@ +-- +-- Module. +-- +local tools = {} + + +-- Requires. +local string = require('string') +local childProcess = require ('childprocess') +local os = require ('os') +local table = require ('table') + +-- +-- Limit a given number x between two boundaries. +-- Either min or max can be nil, to fence on one side only. +-- +tools.fence = function(x, min, max) + return (min and x < min and min) or (max and x > max and max) or x +end + +-- +-- Encode data in Base64 format. +-- +tools.base64 = function(data) + local _lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + return ((data:gsub('.', function(x) + local r, b = '', x:byte() + for i = 8, 1, -1 do + r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and '1' or '0') + end + return r + end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x) + if #x < 6 then + return '' + end + local c = 0 + for i = 1, 6 do + c = c + (x:sub(i, i) == '1' and 2 ^ (6 - i) or 0) + end + return _lookup:sub(c + 1, c + 1) + end) .. ({ + '', + '==', + '=' + })[#data % 3 + 1]) +end + + +-- +-- Split a string into a table +-- +tools.split = function (inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} ; local i=1 + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + t[i] = str + i = i + 1 + end + return t +end + +-- +-- Cross platform process stats by name pattern +-- it uses `ps` on *nix and `tasklist` on Windows +-- Configuration parameters: +-- processName: process name pattern ( matching values from `ps -o comm,args` on *nix or command name on Windows) +-- reconcile: reconcile technique if multiple found, can be: +-- "first" : use the first one found, default +-- "parent" : use the one that is parent of others +-- "uptime" : (linux only) use the one that is started first +-- callback result is a table with pid (process id), and optionally ppid (parent process id), +-- time (total cpu time), rss (resident set size), comm (process name), args (command and arguments) +-- +local psStat= {} +tools.findProcStat = function (cfg, cb) + + local osType = string.lower(os.type()) + local isWindows = osType == 'win32' + local isLinux = osType == 'linux' + + cfg = cfg or {} + cfg.reconcile = cfg.reconcile or "first" + + local cmd --ps on *nix, tasklist on Windows + local opts --command options + local env --environment variables + local sep --field separator + + if (isWindows) then + cmd = "tasklist" + opts = {"/v", "/fo","csv" } + env = {} + sep = "," + + else --*nix + + cmd = "ps" + opts = {"-e", "-o","pid,ppid,time,rss,comm,args" } + if (isLinux and cfg.reconcile == "uptime") then + opts[#opts+1] = "--sort=lstart" + end + env = { ["COLUMNS"] = 4096 } + sep = " " + + end + + local psHandler = function ( err, stdout, stderr ) + if (err or #stderr>0) then + cb(err or stderr) + return + end + + local parents = {} + local found = false; + -- call func with each word in a string + stdout:gsub("[^\r\n]+", function(line) + if (found) then return end + + local _proc = tools.split(line,sep) + local proc + + if (isWindows) then + --csv format + proc = {} + proc.comm = _proc[1]:gsub("^\"*(.-)\"*$", "%1") --trim enclosing " + proc.pid = _proc[2]:gsub("^\"*(.-)\"*$", "%1") + proc.rss = _proc[5]:gsub("^\"*(%d-)[^%d]?(%d-)[^%d]?(%d-)[^%d]?(%d-) K\"*$", "%1%2%3%4") --remove trailing ' K' and thousands separator + proc.time = _proc[8]:gsub("^\"*(.-)\"*$", "%1") + proc.ppid = -1 -- tasklist doesn't support parent pid + proc.args = "" --tasklist doesn't show arguments + else + proc = { + ["pid"] = table.remove(_proc,1), + ["ppid"] = table.remove(_proc,1), + ["time"] = table.remove(_proc,1), + ["rss"] = table.remove(_proc,1), + ["comm"] = table.remove(_proc,1), + ["args"] = table.concat(_proc," "), + } + end + + proc.rss=(tonumber(proc.rss) or 0 ) --convert to number + + if (string.match(proc.comm, cfg.processName) ~= nil or string.match(proc.args, cfg.processName) ~= nil) then + if (cfg.reconcile == "first" or cfg.reconcile == "uptime") then + found=true + cb(nil,proc) + return + else --parent + parents[proc.pid]=proc --pid hashing, easy navigation + end + end + end) + + if (found) then return end + + if (cfg.reconcile == "parent" and next(parents) ~=nil) then + local _,proc = next(parents) --get the first matched process + while (parents[proc.ppid] ~= nil) do --follow parents + proc=parents[proc.ppid] + end + cb(nil,proc) + return + end + + cb("Process "..cfg.processName.." not found") + end + + childProcess.execFile(cmd , opts , env , psHandler ) + +end + + +-- +-- Export. +-- +return tools \ No newline at end of file diff --git a/plugin.json b/plugin.json index 34c53d8..4250097 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,9 @@ { - "description" : "Displays CPU use for a single process", + "description" : "Displays CPU use for processes", "command" : "node index.js", "postExtract" : "npm install", + "command_lua" : "boundary-meter index.lua", + "postExtract_lua" : "", "ignore" : "node_modules", "metrics" : ["CPU_PROCESS"], "paramArray" : { "itemTitle" : ["source"], "schemaTitle" : "Process"},