From 24b8c0febb9367c8c60f56cfd60ce296b4da7cbb Mon Sep 17 00:00:00 2001 From: Ivano Picco Date: Sat, 7 Mar 2015 23:39:50 +0000 Subject: [PATCH 1/5] New Lua plugin --- index.lua | 98 ++++++++++++++++++++++++++++++ modules/tools.lua | 152 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 index.lua create mode 100644 modules/tools.lua diff --git a/index.lua b/index.lua new file mode 100644 index 0000000..f4370ee --- /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 = 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 + 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 + local source = _parameters.source --default hostname + currentValues[source]={}; + previousValues[source]={}; + timer.setInterval(_parameters.pollInterval,poll,source) +end + diff --git a/modules/tools.lua b/modules/tools.lua new file mode 100644 index 0000000..4edfab2 --- /dev/null +++ b/modules/tools.lua @@ -0,0 +1,152 @@ +-- +-- 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 powershell +-- Configuration parameters: +-- processName: process name pattern (as per `ps -o comm,args` on Unix ) +-- 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" + + if (isWindows) then + cb ("OS not supported yet") + return + end + + local opts = {"-e", "-o","pid,ppid,time,rss,comm,args" } + + if (isLinux and cfg.reconcile == "uptime") then + opts[#opts+1] = "--sort=lstart" + end + + local psHandler = function ( err, stdout, stderr ) + if (err or #stderr>0) then + --print errors to stderr + 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,' ') + local 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," "), + } + 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("ps" , opts , { ["COLUMNS"] = 4096 }, psHandler ) + +end + + +-- +-- Export. +-- +return tools \ No newline at end of file From 9d9a303925a12212cbe1f8eb5bcec7ab37e9e574 Mon Sep 17 00:00:00 2001 From: Ivano Picco Date: Sun, 8 Mar 2015 21:08:47 +0000 Subject: [PATCH 2/5] Reset values on error --- index.lua | 3 +++ modules/tools.lua | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/index.lua b/index.lua index f4370ee..4fc788c 100644 --- a/index.lua +++ b/index.lua @@ -70,6 +70,9 @@ function poll(cfg) tools.findProcStat(cfg, function (err,proc) if (err) then + --reset previous metrics + currentValues[cfg.processName]={}; + previousValues[cfg.processName]={}; utils.debug(err) return end diff --git a/modules/tools.lua b/modules/tools.lua index 4edfab2..243a75d 100644 --- a/modules/tools.lua +++ b/modules/tools.lua @@ -63,7 +63,7 @@ end -- -- Cross platform process stats by name pattern --- it uses `ps` on *nix and powershell +-- it uses `ps` on *nix and `tasklist` on Windows -- Configuration parameters: -- processName: process name pattern (as per `ps -o comm,args` on Unix ) -- reconcile: reconcile technique if multiple found, can be: From 86aa493fdc39574bbcdebb5e11d8ec18f0115cd3 Mon Sep 17 00:00:00 2001 From: Ivano Picco Date: Mon, 9 Mar 2015 16:37:22 +0000 Subject: [PATCH 3/5] Update readme and plugin metadata --- README.md | 30 +++++++++++++++++++++++++++++- plugin.json | 4 +++- 2 files changed, 32 insertions(+), 2 deletions(-) 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/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"}, From b46c021eb87e7837acca3d8a943e2b6d7b18e29c Mon Sep 17 00:00:00 2001 From: Ivano Picco Date: Mon, 9 Mar 2015 16:40:09 +0000 Subject: [PATCH 4/5] Windows support --- index.lua | 7 ++---- modules/tools.lua | 60 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/index.lua b/index.lua index 4fc788c..708e0a8 100644 --- a/index.lua +++ b/index.lua @@ -48,7 +48,7 @@ function diffTimeValues(source,name) --convert cur value into timestamp local t = tools.split(_cur,"-") --days local days = (#t>1) and table.remove(t,1) or 0 - local time = tools.split(t[1],":") -- hours, minutes , seconds + 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 @@ -93,9 +93,6 @@ if (#_parameters.items >0 ) then timer.setInterval(_parameters.pollInterval,poll,item) end else - local source = _parameters.source --default hostname - currentValues[source]={}; - previousValues[source]={}; - timer.setInterval(_parameters.pollInterval,poll,source) + utils.debug("No configuration found") end diff --git a/modules/tools.lua b/modules/tools.lua index 243a75d..87faff7 100644 --- a/modules/tools.lua +++ b/modules/tools.lua @@ -65,7 +65,7 @@ end -- Cross platform process stats by name pattern -- it uses `ps` on *nix and `tasklist` on Windows -- Configuration parameters: --- processName: process name pattern (as per `ps -o comm,args` on Unix ) +-- 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 @@ -83,15 +83,27 @@ tools.findProcStat = function (cfg, cb) 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 - cb ("OS not supported yet") - return - end + cmd = "tasklist" + opts = {"/v", "/fo","csv" } + env = {} + sep = "," + + else --*nix - local opts = {"-e", "-o","pid,ppid,time,rss,comm,args" } + 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 = " " - if (isLinux and cfg.reconcile == "uptime") then - opts[#opts+1] = "--sort=lstart" end local psHandler = function ( err, stdout, stderr ) @@ -107,15 +119,29 @@ tools.findProcStat = function (cfg, cb) stdout:gsub("[^\r\n]+", function(line) if (found) then return end - local _proc = tools.split(line,' ') - local 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," "), - } + 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("^\"*(.-)\"*$", "%1") + 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 + 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 @@ -141,7 +167,7 @@ tools.findProcStat = function (cfg, cb) cb("Process "..cfg.processName.." not found") end - childProcess.execFile("ps" , opts , { ["COLUMNS"] = 4096 }, psHandler ) + childProcess.execFile(cmd , opts , env , psHandler ) end From d62855b163159da9685942712d760a1692f34357 Mon Sep 17 00:00:00 2001 From: Ivano Picco Date: Wed, 11 Mar 2015 20:00:52 +0000 Subject: [PATCH 5/5] Update tools --- modules/tools.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/tools.lua b/modules/tools.lua index 87faff7..b46d65c 100644 --- a/modules/tools.lua +++ b/modules/tools.lua @@ -108,7 +108,6 @@ tools.findProcStat = function (cfg, cb) local psHandler = function ( err, stdout, stderr ) if (err or #stderr>0) then - --print errors to stderr cb(err or stderr) return end @@ -127,7 +126,7 @@ tools.findProcStat = function (cfg, cb) proc = {} proc.comm = _proc[1]:gsub("^\"*(.-)\"*$", "%1") --trim enclosing " proc.pid = _proc[2]:gsub("^\"*(.-)\"*$", "%1") - proc.rss = _proc[5]: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 @@ -142,6 +141,8 @@ tools.findProcStat = function (cfg, cb) } 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