diff --git a/README.md b/README.md
index 2fa67ea..221cc57 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,22 @@
-Boundary Memcached Plugin
--------------------------
+# Boundary Memcached Plugin
Collects metrics from a memcached instance. See video [walkthrough](https://help.boundary.com/hc/articles/201816101).
-### Prerequisites
+## Prerequisites
+
+### Supported OS
| OS | Linux | Windows | SmartOS | OS X |
|:----------|:-----:|:-------:|:-------:|:----:|
| Supported | v | v | v | v |
+#### Boundary Meter Versions V4.0 Or Later
+
+- To install new meter go to Settings->Installation or [see instructons|https://help.boundary.com/hc/en-us/sections/200634331-Installation].
+- To upgrade the meter to the latest version - [see instructons|https://help.boundary.com/hc/en-us/articles/201573102-Upgrading-the-Boundary-Meter].
+
+#### For Boundary Meter less than V4.0
+
| Runtime | node.js | Python | Java |
|:---------|:-------:|:------:|:----:|
| Required | + | | |
@@ -16,9 +24,12 @@ Collects metrics from a memcached instance. See video [walkthrough](https://help
- [How to install node.js?](https://help.boundary.com/hc/articles/202360701)
### Plugin Setup
+
None
-#### Plugin Configuration Fields
+### Plugin Configuration Fields
+
+#### For All Versions
|Field Name|Description |
|:---------|:----------------------------------------------------------|
@@ -27,6 +38,9 @@ None
|Host |The MEMCACHED hostname. |
### Metrics Collected
+
+#### For All Versions
+
|Metric Name |Description |
|:--------------------|:---------------------------------|
|Memcached Allocated |Percent of available memory used |
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..ea5c40e
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,105 @@
+local timer = require('timer')
+local boundary = require('boundary')
+local io = require('io')
+local net = require('net')
+
+
+local __pgk = "BOUNDARY MEMCACHED"
+local client
+local _previous = {}
+local host = "127.0.0.1"
+local port = 11211
+local pollInterval = 1000
+
+
+if (boundary.param ~= nil) then
+ pollInterval = boundary.param.pollInterval or pollInterval
+ host = boundary.param.host or host
+ port = boundary.param.port or port
+ source = (type(boundary.param.source) == 'string' and boundary.param.source:gsub('%s+', '') ~= '' and boundary.param.source) or
+ io.popen("uname -n"):read('*line')
+end
+
+
+function berror(err)
+ if err then print(string.format("%s ERROR: %s", __pgk, tostring(err))) return err end
+end
+
+
+
+function diff(a, b)
+ if a == nil or b == nil then return 0 end
+ return math.max(a - b, 0)
+end
+
+
+-- accumulate a value and return the difference from the previous value
+function accumulate(key, newValue)
+ local oldValue = _previous[key] or newValue
+ local difference = diff(newValue, oldValue)
+ _previous[key] = newValue
+ return difference
+end
+
+-- init client
+function init()
+ if client == nil then
+ client = net.createConnection(port, host, function (err)
+ if berror(err) then end
+ end)
+ end
+ client:on("error", function(err)
+ berror(err)
+ end)
+ client:on("data", function(data)
+
+ local d = {}
+ for _, v in pairs(split(data, "\r\n")) do
+ local s = v:gsub("STAT ", "")
+ local t = {}
+ for w in s:gmatch("%S+") do
+ table.insert(t, w)
+ end
+ d[t[1]] = tonumber(t[2])
+ end
+
+ print(string.format('MEMCACHED_ALLOCATED %d %s', d.bytes / d.limit_maxbytes, source))
+ print(string.format('MEMCACHED_CONNECTIONS %d %s', d.curr_connections, source))
+ print(string.format('MEMCACHED_HITS %d %s', accumulate('get_hits', d.get_hits), source))
+ print(string.format('MEMCACHED_MISSES %d %s', accumulate('get_misses', d.get_misses), source))
+ print(string.format('MEMCACHED_ITEMS %d %s', d.curr_items, source))
+ print(string.format('MEMCACHED_REQUESTS %d %s', accumulate('cmd_get', d.cmd_get) + accumulate('cmd_set', d.cmd_set), source))
+ print(string.format('MEMCACHED_NETWORK_IN %d %s', accumulate('bytes_read', d.bytes_read), source))
+ print(string.format('MEMCACHED_NETWORK_OUT %d %s', accumulate('bytes_written', d.bytes_written), source))
+ end)
+
+end
+
+
+-- get the natural difference between a and b
+function diff(a, b)
+ if not a or not b then return 0 end
+ return math.max(a - b, 0)
+end
+
+function split(str, delim)
+ local res = {}
+ local pattern = string.format("([^%s]+)%s()", delim, delim)
+ while (true) do
+ line, pos = str:match(pattern, pos)
+ if line == nil then break end
+ table.insert(res, line)
+ end
+ return res
+end
+
+print("_bevent:MEMCACHED plugin up : version 1.0|t:info|tags:memcached, lua, plugin")
+init()
+
+timer.setInterval(pollInterval, function ()
+ client:write("stats " .. "\r\n")
+end)
+
+
+
+
diff --git a/modules/_url.lua b/modules/_url.lua
new file mode 100644
index 0000000..d417f07
--- /dev/null
+++ b/modules/_url.lua
@@ -0,0 +1,443 @@
+-- neturl.lua - a robust url parser and builder
+--
+-- Bertrand Mansion, 2011-2013; License MIT
+-- @module neturl
+-- @alias M
+
+local M = {}
+M.version = "0.9.0"
+
+--- url options
+-- separator is set to `&` by default but could be anything like `&` or `;`
+-- @todo Add an option to limit the size of the argument table
+M.options = {
+ separator = '&'
+}
+
+--- list of known and common scheme ports
+-- as documented in IANA URI scheme list
+M.services = {
+ acap = 674,
+ cap = 1026,
+ dict = 2628,
+ ftp = 21,
+ gopher = 70,
+ http = 80,
+ https = 443,
+ iax = 4569,
+ icap = 1344,
+ imap = 143,
+ ipp = 631,
+ ldap = 389,
+ mtqp = 1038,
+ mupdate = 3905,
+ news = 2009,
+ nfs = 2049,
+ nntp = 119,
+ rtsp = 554,
+ sip = 5060,
+ snmp = 161,
+ telnet = 23,
+ tftp = 69,
+ vemmi = 575,
+ afs = 1483,
+ jms = 5673,
+ rsync = 873,
+ prospero = 191,
+ videotex = 516
+}
+
+local legal = {
+ ["-"] = true, ["_"] = true, ["."] = true, ["!"] = true,
+ ["~"] = true, ["*"] = true, ["'"] = true, ["("] = true,
+ [")"] = true, [":"] = true, ["@"] = true, ["&"] = true,
+ ["="] = true, ["+"] = true, ["$"] = true, [","] = true,
+ [";"] = true -- can be used for parameters in path
+}
+
+local function decode(str)
+ local str = str:gsub('+', ' ')
+ return (str:gsub("%%(%x%x)", function(c)
+ return string.char(tonumber(c, 16))
+ end))
+end
+
+local function encode(str)
+ return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v)
+ return string.upper(string.format("%%%02x", string.byte(v)))
+ end))
+end
+
+-- for query values, prefer + instead of %20 for spaces
+local function encodeValue(str)
+ local str = encode(str)
+ return str:gsub('%%20', '+')
+end
+
+local function encodeSegment(s)
+ local legalEncode = function(c)
+ if legal[c] then
+ return c
+ end
+ return encode(c)
+ end
+ return s:gsub('([^a-zA-Z0-9])', legalEncode)
+end
+
+--- builds the url
+-- @return a string representing the built url
+function M:build()
+ local url = ''
+ if self.path then
+ local path = self.path
+ path:gsub("([^/]+)", function (s) return encodeSegment(s) end)
+ url = url .. tostring(path)
+ end
+ if self.query then
+ local qstring = tostring(self.query)
+ if qstring ~= "" then
+ url = url .. '?' .. qstring
+ end
+ end
+ if self.host then
+ local authority = self.host
+ if self.port and self.scheme and M.services[self.scheme] ~= self.port then
+ authority = authority .. ':' .. self.port
+ end
+ local userinfo
+ if self.user and self.user ~= "" then
+ userinfo = self.user
+ if self.password then
+ userinfo = userinfo .. ':' .. self.password
+ end
+ end
+ if userinfo and userinfo ~= "" then
+ authority = userinfo .. '@' .. authority
+ end
+ if authority then
+ if url ~= "" then
+ url = '//' .. authority .. '/' .. url:gsub('^/+', '')
+ else
+ url = '//' .. authority
+ end
+ end
+ end
+ if self.scheme then
+ url = self.scheme .. ':' .. url
+ end
+ if self.fragment then
+ url = url .. '#' .. self.fragment
+ end
+ return url
+end
+
+--- builds the querystring
+-- @param tab The key/value parameters
+-- @param sep The separator to use (optional)
+-- @param key The parent key if the value is multi-dimensional (optional)
+-- @return a string representing the built querystring
+function M.buildQuery(tab, sep, key)
+ local query = {}
+ if not sep then
+ sep = M.options.separator or '&'
+ end
+ local keys = {}
+ for k in pairs(tab) do
+ keys[#keys+1] = k
+ end
+ table.sort(keys)
+ for _,name in ipairs(keys) do
+ local value = tab[name]
+ name = encode(tostring(name))
+ if key then
+ name = string.format('%s[%s]', tostring(key), tostring(name))
+ end
+ if type(value) == 'table' then
+ query[#query+1] = M.buildQuery(value, sep, name)
+ else
+ local value = encodeValue(tostring(value))
+ if value ~= "" then
+ query[#query+1] = string.format('%s=%s', name, value)
+ else
+ query[#query+1] = name
+ end
+ end
+ end
+ return table.concat(query, sep)
+end
+
+--- Parses the querystring to a table
+-- This function can parse multidimensional pairs and is mostly compatible
+-- with PHP usage of brackets in key names like ?param[key]=value
+-- @param str The querystring to parse
+-- @param sep The separator between key/value pairs, defaults to `&`
+-- @todo limit the max number of parameters with M.options.max_parameters
+-- @return a table representing the query key/value pairs
+function M.parseQuery(str, sep)
+ if not sep then
+ sep = M.options.separator or '&'
+ end
+
+ local values = {}
+ for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
+ local key = decode(key)
+ local keys = {}
+ key = key:gsub('%[([^%]]*)%]', function(v)
+ -- extract keys between balanced brackets
+ if string.find(v, "^-?%d+$") then
+ v = tonumber(v)
+ else
+ v = decode(v)
+ end
+ table.insert(keys, v)
+ return "="
+ end)
+ key = key:gsub('=+.*$', "")
+ key = key:gsub('%s', "_") -- remove spaces in parameter name
+ val = val:gsub('^=+', "")
+
+ if not values[key] then
+ values[key] = {}
+ end
+ if #keys > 0 and type(values[key]) ~= 'table' then
+ values[key] = {}
+ elseif #keys == 0 and type(values[key]) == 'table' then
+ values[key] = decode(val)
+ end
+
+ local t = values[key]
+ for i,k in ipairs(keys) do
+ if type(t) ~= 'table' then
+ t = {}
+ end
+ if k == "" then
+ k = #t+1
+ end
+ if not t[k] then
+ t[k] = {}
+ end
+ if i == #keys then
+ t[k] = decode(val)
+ end
+ t = t[k]
+ end
+ end
+ setmetatable(values, { __tostring = M.buildQuery })
+ return values
+end
+
+--- set the url query
+-- @param query Can be a string to parse or a table of key/value pairs
+-- @return a table representing the query key/value pairs
+function M:setQuery(query)
+ local query = query
+ if type(query) == 'table' then
+ query = M.buildQuery(query)
+ end
+ self.query = M.parseQuery(query)
+ return query
+end
+
+--- set the authority part of the url
+-- The authority is parsed to find the user, password, port and host if available.
+-- @param authority The string representing the authority
+-- @return a string with what remains after the authority was parsed
+function M:setAuthority(authority)
+ self.authority = authority
+ self.port = nil
+ self.host = nil
+ self.userinfo = nil
+ self.user = nil
+ self.password = nil
+
+ authority = authority:gsub('^([^@]*)@', function(v)
+ self.userinfo = v
+ return ''
+ end)
+ authority = authority:gsub("^%[[^%]]+%]", function(v)
+ -- ipv6
+ self.host = v
+ return ''
+ end)
+ authority = authority:gsub(':([^:]*)$', function(v)
+ self.port = tonumber(v)
+ return ''
+ end)
+ if authority ~= '' and not self.host then
+ self.host = authority:lower()
+ end
+ if self.userinfo then
+ local userinfo = self.userinfo
+ userinfo = userinfo:gsub(':([^:]*)$', function(v)
+ self.password = v
+ return ''
+ end)
+ self.user = userinfo
+ end
+ return authority
+end
+
+--- Parse the url into the designated parts.
+-- Depending on the url, the following parts can be available:
+-- scheme, userinfo, user, password, authority, host, port, path,
+-- query, fragment
+-- @param url Url string
+-- @return a table with the different parts and a few other functions
+function M.parse(url)
+ local comp = {}
+ M.setAuthority(comp, "")
+ M.setQuery(comp, "")
+
+ local url = tostring(url or '')
+ url = url:gsub('#(.*)$', function(v)
+ comp.fragment = v
+ return ''
+ end)
+ url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
+ comp.scheme = v:lower()
+ return ''
+ end)
+ url = url:gsub('%?(.*)', function(v)
+ M.setQuery(comp, v)
+ return ''
+ end)
+ url = url:gsub('^//([^/]*)', function(v)
+ M.setAuthority(comp, v)
+ return ''
+ end)
+ comp.path = decode(url)
+
+ setmetatable(comp, {
+ __index = M,
+ __tostring = M.build}
+ )
+ return comp
+end
+
+--- removes dots and slashes in urls when possible
+-- This function will also remove multiple slashes
+-- @param path The string representing the path to clean
+-- @return a string of the path without unnecessary dots and segments
+function M.removeDotSegments(path)
+ local fields = {}
+ if string.len(path) == 0 then
+ return ""
+ end
+ local startslash = false
+ local endslash = false
+ if string.sub(path, 1, 1) == "/" then
+ startslash = true
+ end
+ if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then
+ endslash = true
+ end
+
+ path:gsub('[^/]+', function(c) table.insert(fields, c) end)
+
+ local new = {}
+ local j = 0
+
+ for i,c in ipairs(fields) do
+ if c == '..' then
+ if j > 0 then
+ j = j - 1
+ end
+ elseif c ~= "." then
+ j = j + 1
+ new[j] = c
+ end
+ end
+ local ret = ""
+ if #new > 0 and j > 0 then
+ ret = table.concat(new, '/', 1, j)
+ else
+ ret = ""
+ end
+ if startslash then
+ ret = '/'..ret
+ end
+ if endslash then
+ ret = ret..'/'
+ end
+ return ret
+end
+
+local function absolutePath(base_path, relative_path)
+ if string.sub(relative_path, 1, 1) == "/" then
+ return '/' .. string.gsub(relative_path, '^[%./]+', '')
+ end
+ local path = base_path
+ if relative_path ~= "" then
+ path = '/'..path:gsub("[^/]*$", "")
+ end
+ path = path .. relative_path
+ path = path:gsub("([^/]*%./)", function (s)
+ if s ~= "./" then return s else return "" end
+ end)
+ path = string.gsub(path, "/%.$", "/")
+ local reduced
+ while reduced ~= path do
+ reduced = path
+ path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
+ if s ~= "../../" then return "" else return s end
+ end)
+ end
+ path = string.gsub(path, "([^/]*/%.%.?)$", function (s)
+ if s ~= "../.." then return "" else return s end
+ end)
+ local reduced
+ while reduced ~= path do
+ reduced = path
+ path = string.gsub(reduced, '^/?%.%./', '')
+ end
+ return '/' .. path
+end
+
+--- builds a new url by using the one given as parameter and resolving paths
+-- @param other A string or a table representing a url
+-- @return a new url table
+function M:resolve(other)
+ if type(self) == "string" then
+ self = M.parse(self)
+ end
+ if type(other) == "string" then
+ other = M.parse(other)
+ end
+ if other.scheme then
+ return other
+ else
+ other.scheme = self.scheme
+ if not other.authority or other.authority == "" then
+ other:setAuthority(self.authority)
+ if not other.path or other.path == "" then
+ other.path = self.path
+ local query = other.query
+ if not query or not next(query) then
+ other.query = self.query
+ end
+ else
+ other.path = absolutePath(self.path, other.path)
+ end
+ end
+ return other
+ end
+end
+
+--- normalize a url path following some common normalization rules
+-- described on The URL normalization page of Wikipedia
+-- @return the normalized path
+function M:normalize()
+ if type(self) == 'string' then
+ self = M.parse(self)
+ end
+ if self.path then
+ local path = self.path
+ path = absolutePath(path, "")
+ -- normalize multiple slashes
+ path = string.gsub(path, "//+", "/")
+ self.path = path
+ end
+ return self
+end
+
+return M
\ No newline at end of file
diff --git a/plugin.json b/plugin.json
index f8a9684..5d7598e 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,31 +1,49 @@
{
- "description" : "Displays important MEMCACHED metrics",
- "icon" : "icon.png",
- "command" : "node index.js",
- "postExtract" : "npm install",
- "ignore" : "node_modules",
- "metrics" : ["MEMCACHED_ALLOCATED", "MEMCACHED_CONNECTIONS", "MEMCACHED_HITS", "MEMCACHED_MISSES", "MEMCACHED_ITEMS",
- "MEMCACHED_REQUESTS", "MEMCACHED_NETWORK_IN", "MEMCACHED_NETWORK_OUT"],
- "dashboards" : [{ "name" : "memcached", "layout" : "d-w=3&d-h=2&d-pad=5&d-bg=none&d-g-MEMCACHED_ALLOCATED=0-0-1-1&d-g-MEMCACHED_CONNECTIONS=1-0-1-1&d-g-MEMCACHED_HITS=0-1-1-1-t&d-g-MEMCACHED_MISSES=0-1-1-1-b&d-g-MEMCACHED_ITEMS=1-1-1-1&d-g-MEMCACHED_REQUESTS=2-0-1-1&d-g-MEMCACHED_NETWORK_IN=2-1-1-1-t&d-g-MEMCACHED_NETWORK_OUT=2-1-1-1-b"}],
- "paramSchema" : [
- {
- "title" : "Source",
- "name" : "source",
- "description" : "The source to display in the legend for the MEMCACHED data.",
- "type" : "string"
- },
- {
- "title" : "Port",
- "name" : "port",
- "description" : "The MEMCACHED port.",
- "type" : "integer",
- "default" : 11211
- },
- {
- "title" : "Host",
- "name" : "host",
- "description" : "The MEMCACHED hostname.",
- "type" : "string"
- }
- ]
-}
\ No newline at end of file
+ "description" : "Displays important MEMCACHED metrics",
+ "icon" : "icon.png",
+ "command" : "node index.js",
+ "postExtract" : "npm install",
+ "command_lua" : "boundary-meter init.lua",
+ "postExtract_lua" : "",
+ "ignore" : "node_modules",
+
+ "metrics" : [
+ "MEMCACHED_ALLOCATED",
+ "MEMCACHED_CONNECTIONS",
+ "MEMCACHED_HITS",
+ "MEMCACHED_MISSES",
+ "MEMCACHED_ITEMS",
+ "MEMCACHED_REQUESTS",
+ "MEMCACHED_NETWORK_IN",
+ "MEMCACHED_NETWORK_OUT"
+ ],
+
+ "dashboards" : [
+ {
+ "name" : "memcached",
+ "layout" : "d-w=3&d-h=2&d-pad=5&d-bg=none&d-g-MEMCACHED_ALLOCATED=0-0-1-1&d-g-MEMCACHED_CONNECTIONS=1-0-1-1&d-g-MEMCACHED_HITS=0-1-1-1-t&d-g-MEMCACHED_MISSES=0-1-1-1-b&d-g-MEMCACHED_ITEMS=1-1-1-1&d-g-MEMCACHED_REQUESTS=2-0-1-1&d-g-MEMCACHED_NETWORK_IN=2-1-1-1-t&d-g-MEMCACHED_NETWORK_OUT=2-1-1-1-b"
+ }
+ ],
+
+ "paramSchema" : [
+ {
+ "title" : "Source",
+ "name" : "source",
+ "description" : "The source to display in the legend for the MEMCACHED data.",
+ "type" : "string"
+ },
+ {
+ "title" : "Port",
+ "name" : "port",
+ "description" : "The MEMCACHED port.",
+ "type" : "integer",
+ "default" : 11211
+ },
+ {
+ "title" : "Host",
+ "name" : "host",
+ "description" : "The MEMCACHED hostname.",
+ "type" : "string"
+ }
+ ]
+}