diff options
Diffstat (limited to 'util/statsd.lua')
-rw-r--r-- | util/statsd.lua | 297 |
1 files changed, 241 insertions, 56 deletions
diff --git a/util/statsd.lua b/util/statsd.lua index 67481c36..25e03e38 100644 --- a/util/statsd.lua +++ b/util/statsd.lua @@ -1,82 +1,267 @@ local socket = require "socket"; +local time = require "util.time".now; +local array = require "util.array"; +local t_concat = table.concat; -local time = require "util.time".now +local new_metric_registry = require "util.openmetrics".new_metric_registry; +local render_histogram_le = require "util.openmetrics".render_histogram_le; -local function new(config) - if not config or not config.statsd_server then - return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics"; +-- BEGIN of Metric implementations + +-- Gauges +local gauge_metric_mt = {} +gauge_metric_mt.__index = gauge_metric_mt + +local function new_gauge_metric(full_name, impl) + local metric = { + _full_name = full_name; + _impl = impl; + value = 0; + } + setmetatable(metric, gauge_metric_mt) + return metric +end + +function gauge_metric_mt:set(value) + self.value = value + self._impl:push_gauge(self._full_name, value) +end + +function gauge_metric_mt:add(delta) + self.value = self.value + delta + self._impl:push_gauge(self._full_name, self.value) +end + +function gauge_metric_mt:reset() + self.value = 0 + self._impl:push_gauge(self._full_name, 0) +end + +function gauge_metric_mt.iter_samples() + -- statsd backend does not support iteration. + return function() + return nil end +end - local sock = socket.udp(); - sock:setpeername(config.statsd_server, config.statsd_port or 8125); +-- Counters +local counter_metric_mt = {} +counter_metric_mt.__index = counter_metric_mt - local prefix = (config.prefix or "prosody").."."; +local function new_counter_metric(full_name, impl) + local metric = { + _full_name = full_name, + _impl = impl, + value = 0, + } + setmetatable(metric, counter_metric_mt) + return metric +end + +function counter_metric_mt:set(value) + local delta = value - self.value + self.value = value + self._impl:push_counter_delta(self._full_name, delta) +end - local function send_metric(s) - return sock:send(prefix..s); +function counter_metric_mt:add(value) + self.value = (self.value or 0) + value + self._impl:push_counter_delta(self._full_name, value) +end + +function counter_metric_mt.iter_samples() + -- statsd backend does not support iteration. + return function() + return nil + end +end + +function counter_metric_mt:reset() + self.value = 0 +end + +-- Histograms +local histogram_metric_mt = {} +histogram_metric_mt.__index = histogram_metric_mt + +local function new_histogram_metric(buckets, full_name, impl) + -- NOTE: even though the more or less proprietrary dogstatsd has its own + -- histogram implementation, we push the individual buckets in this statsd + -- backend for both consistency and compatibility across statsd + -- implementations. + local metric = { + _sum_name = full_name..".sum", + _count_name = full_name..".count", + _impl = impl, + _created = time(), + _sum = 0, + _count = 0, + } + -- the order of buckets matters unfortunately, so we cannot directly use + -- the thresholds as table keys + for i, threshold in ipairs(buckets) do + local threshold_s = render_histogram_le(threshold) + metric[i] = { + threshold = threshold, + threshold_s = threshold_s, + count = 0, + _full_name = full_name..".bucket."..(threshold_s:gsub("%.", "_")), + } end + setmetatable(metric, histogram_metric_mt) + return metric +end - local function send_gauge(name, amount, relative) - local s_amount = tostring(amount); - if relative and amount > 0 then - s_amount = "+"..s_amount; +function histogram_metric_mt:sample(value) + -- According to the I-D, values must be part of all buckets + for i, bucket in pairs(self) do + if "number" == type(i) and bucket.threshold > value then + bucket.count = bucket.count + 1 + self._impl:push_counter_delta(bucket._full_name, 1) end - return send_metric(name..":"..s_amount.."|g"); end + self._sum = self._sum + value + self._count = self._count + 1 + self._impl:push_gauge(self._sum_name, self._sum) + self._impl:push_counter_delta(self._count_name, 1) +end - local function send_counter(name, amount) - return send_metric(name..":"..tostring(amount).."|c"); +function histogram_metric_mt.iter_samples() + -- statsd backend does not support iteration. + return function() + return nil end +end - local function send_duration(name, duration) - return send_metric(name..":"..tostring(duration).."|ms"); +function histogram_metric_mt:reset() + self._created = time() + self._count = 0 + self._sum = 0 + for i, bucket in pairs(self) do + if "number" == type(i) then + bucket.count = 0 + end end + self._impl:push_gauge(self._sum_name, self._sum) +end + +-- Summaries +local summary_metric_mt = {} +summary_metric_mt.__index = summary_metric_mt + +local function new_summary_metric(full_name, impl) + local metric = { + _sum_name = full_name..".sum", + _count_name = full_name..".count", + _impl = impl, + } + setmetatable(metric, summary_metric_mt) + return metric +end + +function summary_metric_mt:sample(value) + self._impl:push_counter_delta(self._sum_name, value) + self._impl:push_counter_delta(self._count_name, 1) +end - local function send_histogram_sample(name, sample) - return send_metric(name..":"..tostring(sample).."|h"); +function summary_metric_mt.iter_samples() + -- statsd backend does not support iteration. + return function() + return nil end +end - local methods; - methods = { - amount = function (name, initial) - if initial then - send_gauge(name, initial); - end - return function (new_v) send_gauge(name, new_v); end - end; - counter = function (name, initial) --luacheck: ignore 212/initial - return function (delta) - send_gauge(name, delta, true); - end; - end; - rate = function (name) - return function () - send_counter(name, 1); - end; +function summary_metric_mt.reset() +end + +-- BEGIN of statsd client implementation + +local statsd_mt = {} +statsd_mt.__index = statsd_mt + +function statsd_mt:cork() + self.corked = true + self.cork_buffer = self.cork_buffer or {} +end + +function statsd_mt:uncork() + self.corked = false + self:_flush_cork_buffer() +end + +function statsd_mt:_flush_cork_buffer() + local buffer = self.cork_buffer + for metric_name, value in pairs(buffer) do + self:_send_gauge(metric_name, value) + buffer[metric_name] = nil + end +end + +function statsd_mt:push_gauge(metric_name, value) + if self.corked then + self.cork_buffer[metric_name] = value + else + self:_send_gauge(metric_name, value) + end +end + +function statsd_mt:_send_gauge(metric_name, value) + self:_send(self.prefix..metric_name..":"..tostring(value).."|g") +end + +function statsd_mt:push_counter_delta(metric_name, delta) + self:_send(self.prefix..metric_name..":"..tostring(delta).."|c") +end + +function statsd_mt:_send(s) + return self.sock:send(s) +end + +-- END of statsd client implementation + +local function build_metric_name(family_name, labels) + local parts = array { family_name } + if labels then + parts:append(labels) + end + return t_concat(parts, "/"):gsub("%.", "_"):gsub("/", ".") +end + +local function new(config) + if not config or not config.statsd_server then + return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics"; + end + + local sock = socket.udp(); + sock:setpeername(config.statsd_server, config.statsd_port or 8125); + + local prefix = (config.prefix or "prosody").."."; + + local impl = { + metric_registry = nil; + sock = sock; + prefix = prefix; + }; + setmetatable(impl, statsd_mt) + + local backend = { + gauge = function(family_name, labels) + return new_gauge_metric(build_metric_name(family_name, labels), impl) end; - distribution = function (name, unit, type) --luacheck: ignore 212/unit 212/type - return function (value) - send_histogram_sample(name, value); - end; + counter = function(family_name, labels) + return new_counter_metric(build_metric_name(family_name, labels), impl) end; - sizes = function (name) - name = name.."_size"; - return function (value) - send_histogram_sample(name, value); - end; + histogram = function(buckets, family_name, labels) + return new_histogram_metric(buckets, build_metric_name(family_name, labels), impl) end; - times = function (name) - return function () - local start_time = time(); - return function () - local end_time = time(); - local duration = end_time - start_time; - send_duration(name, duration*1000); - end - end; + summary = function(family_name, labels, extra) + return new_summary_metric(build_metric_name(family_name, labels), impl, extra) end; }; - return methods; + + impl.metric_registry = new_metric_registry(backend); + + return impl; end return { |