1031 lines
31 KiB
Lua
1031 lines
31 KiB
Lua
--- @module Prometheus
|
|
--
|
|
-- vim: ts=2:sw=2:sts=2:expandtab:textwidth=80
|
|
-- This module uses a single dictionary shared between Nginx workers to keep
|
|
-- all metrics. Each metric is stored as a separate entry in that dictionary.
|
|
--
|
|
-- In addition, each worker process has a separate set of counters within
|
|
-- its lua runtime that are used to track increments to count metrics, and
|
|
-- are regularly flushed into the main shared dictionary. This is a performance
|
|
-- optimization that allows counters to be incremented without locking the
|
|
-- shared dictionary. It also means that counter increments are "eventually
|
|
-- consistent"; it can take up to a single counter sync interval (which
|
|
-- defaults to 1 second) for counter values to be visible for collection.
|
|
--
|
|
-- Prometheus requires that (a) all samples for a given metric are presented
|
|
-- as one uninterrupted group, and (b) buckets of a histogram appear in
|
|
-- increasing numerical order. We satisfy that by carefully constructing full
|
|
-- metric names (i.e. metric name along with all labels) so that they meet
|
|
-- those requirements while being sorted alphabetically. In particular:
|
|
--
|
|
-- * all labels for a given metric are presented in reproducible order (the one
|
|
-- used when labels were declared). "le" label for histogram metrics always
|
|
-- goes last;
|
|
-- * bucket boundaries (which are exposed as values of the "le" label) are
|
|
-- stored as floating point numbers with leading and trailing zeroes,
|
|
-- and those zeros would be removed just before we expose the metrics;
|
|
-- * internally "+Inf" bucket is stored as "Inf" (to make it appear after
|
|
-- all numeric buckets), and gets replaced by "+Inf" just before we
|
|
-- expose the metrics.
|
|
--
|
|
-- For example, if you define your bucket boundaries as {0.00005, 10, 1000}
|
|
-- then we will keep the following samples for a metric `m1` with label
|
|
-- `site` set to `site1`:
|
|
--
|
|
-- m1_bucket{site="site1",le="0000.00005"}
|
|
-- m1_bucket{site="site1",le="0010.00000"}
|
|
-- m1_bucket{site="site1",le="1000.00000"}
|
|
-- m1_bucket{site="site1",le="Inf"}
|
|
-- m1_count{site="site1"}
|
|
-- m1_sum{site="site1"}
|
|
--
|
|
-- And when exposing the metrics, their names will be changed to:
|
|
--
|
|
-- m1_bucket{site="site1",le="0.00005"}
|
|
-- m1_bucket{site="site1",le="10"}
|
|
-- m1_bucket{site="site1",le="1000"}
|
|
-- m1_bucket{site="site1",le="+Inf"}
|
|
-- m1_count{site="site1"}
|
|
-- m1_sum{site="site1"}
|
|
--
|
|
-- You can find the latest version and documentation at
|
|
-- https://github.com/knyar/nginx-lua-prometheus
|
|
-- Released under MIT license.
|
|
|
|
-- This library provides per-worker counters used to store counter metric
|
|
-- increments. Copied from https://github.com/Kong/lua-resty-counter
|
|
local buffer = require("string.buffer")
|
|
local resty_counter_lib = require("prometheus_resty_counter")
|
|
local ngx = ngx
|
|
local ngx_log = ngx.log
|
|
local ngx_sleep = ngx.sleep
|
|
local ngx_re_match = ngx.re.match
|
|
local ngx_re_gsub = ngx.re.gsub
|
|
local error = error
|
|
local type = type
|
|
local pairs = pairs
|
|
local tostring = tostring
|
|
local tonumber = tonumber
|
|
local table_sort = table.sort
|
|
local tb_new = require("table.new")
|
|
local yield = require("kong.tools.yield").yield
|
|
|
|
|
|
local Prometheus = {}
|
|
local mt = { __index = Prometheus }
|
|
|
|
local TYPE_COUNTER = 0x1
|
|
local TYPE_GAUGE = 0x2
|
|
local TYPE_HISTOGRAM = 0x4
|
|
local TYPE_LITERAL = {
|
|
[TYPE_COUNTER] = "counter",
|
|
[TYPE_GAUGE] = "gauge",
|
|
[TYPE_HISTOGRAM] = "histogram",
|
|
}
|
|
|
|
|
|
-- Default metric name size for string.buffer.new()
|
|
local NAME_BUFFER_SIZE_HINT = 256
|
|
|
|
-- Default metric data size for string.buffer.new()
|
|
local DATA_BUFFER_SIZE_HINT = 4096
|
|
|
|
-- Default name for error metric incremented by this library.
|
|
local DEFAULT_ERROR_METRIC_NAME = "nginx_metric_errors_total"
|
|
|
|
-- Default value for per-worker counter sync interval (seconds).
|
|
local DEFAULT_SYNC_INTERVAL = 1
|
|
|
|
-- Default set of latency buckets, 5ms to 10s:
|
|
local DEFAULT_BUCKETS = {0.005, 0.01, 0.02, 0.03, 0.05, 0.075, 0.1, 0.2, 0.3,
|
|
0.4, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 10}
|
|
|
|
local METRICS_KEY_REGEX = [[(.*[,{]le=")(.*)(".*)]]
|
|
local METRIC_NAME_REGEX = [[^[a-z_:][a-z0-9_:]*$]]
|
|
local LABEL_NAME_REGEX = [[^[a-z_][a-z0-9_]*$]]
|
|
|
|
-- Accepted range of byte values for tailing bytes of utf8 strings.
|
|
-- This is defined outside of the validate_utf8_string function as a const
|
|
-- variable to avoid creating and destroying table frequently.
|
|
-- Values in this table (and in validate_utf8_string) are from table 3-7 of
|
|
-- www.unicode.org/versions/Unicode6.2.0/UnicodeStandard-6.2.pdf
|
|
local accept_range = {
|
|
{lo = 0x80, hi = 0xBF},
|
|
{lo = 0xA0, hi = 0xBF},
|
|
{lo = 0x80, hi = 0x9F},
|
|
{lo = 0x90, hi = 0xBF},
|
|
{lo = 0x80, hi = 0x8F}
|
|
}
|
|
|
|
-- Validate utf8 string for label values.
|
|
--
|
|
-- Args:
|
|
-- str: string
|
|
--
|
|
-- Returns:
|
|
-- (bool) whether the input string is a valid utf8 string.
|
|
-- (number) position of the first invalid byte.
|
|
local function validate_utf8_string(str)
|
|
local i, n = 1, #str
|
|
local first, byte, left_size, range_idx
|
|
while i <= n do
|
|
first = string.byte(str, i)
|
|
if first >= 0x80 then
|
|
range_idx = 1
|
|
if first >= 0xC2 and first <= 0xDF then -- 2 bytes
|
|
left_size = 1
|
|
elseif first >= 0xE0 and first <= 0xEF then -- 3 bytes
|
|
left_size = 2
|
|
if first == 0xE0 then
|
|
range_idx = 2
|
|
elseif first == 0xED then
|
|
range_idx = 3
|
|
end
|
|
elseif first >= 0xF0 and first <= 0xF4 then -- 4 bytes
|
|
left_size = 3
|
|
if first == 0xF0 then
|
|
range_idx = 4
|
|
elseif first == 0xF4 then
|
|
range_idx = 5
|
|
end
|
|
else
|
|
return false, i
|
|
end
|
|
|
|
if i + left_size > n then
|
|
return false, i
|
|
end
|
|
|
|
for j = 1, left_size do
|
|
byte = string.byte(str, i + j)
|
|
if byte < accept_range[range_idx].lo or byte > accept_range[range_idx].hi then
|
|
return false, i
|
|
end
|
|
range_idx = 1
|
|
end
|
|
i = i + left_size
|
|
end
|
|
i = i + 1
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Generate full metric name that includes all labels.
|
|
--
|
|
-- Args:
|
|
-- name: string
|
|
-- label_names: (array) a list of label keys.
|
|
-- label_values: (array) a list of label values.
|
|
--
|
|
-- Returns:
|
|
-- (string) full metric name.
|
|
local function full_metric_name(name, label_names, label_values)
|
|
if not label_names then
|
|
return name
|
|
end
|
|
|
|
local slash, double_slash, reg_slash = [[\]], [[\\]], [[\\]]
|
|
local quote, slash_quote, reg_quote = [["]], [[\"]], [["]]
|
|
|
|
local buf = buffer.new(NAME_BUFFER_SIZE_HINT)
|
|
|
|
-- format "name{k1=v1,k2=v2}"
|
|
buf:put(name):put("{")
|
|
|
|
for idx = 1, #label_names do
|
|
local key = label_names[idx]
|
|
local label_value = label_values[idx]
|
|
|
|
-- we only check string value for '\\' and '"'
|
|
if type(label_value) == "string" then
|
|
local valid, pos = validate_utf8_string(label_value)
|
|
|
|
if not valid then
|
|
label_value = string.sub(label_value, 1, pos - 1)
|
|
end
|
|
|
|
if string.find(label_value, slash, 1, true) then
|
|
label_value = ngx_re_gsub(label_value, reg_slash, double_slash, "jo")
|
|
end
|
|
|
|
if string.find(label_value, quote, 1, true) then
|
|
label_value = ngx_re_gsub(label_value, reg_quote, slash_quote, "jo")
|
|
end
|
|
end
|
|
|
|
-- add a comma to seperate k=v
|
|
if idx > 1 then
|
|
buf:put(",")
|
|
end
|
|
|
|
buf:putf('%s="%s"', key, tostring(label_value))
|
|
end
|
|
|
|
buf:put("}") -- close the bracket
|
|
|
|
-- update the size hint
|
|
if NAME_BUFFER_SIZE_HINT < #buf then
|
|
NAME_BUFFER_SIZE_HINT = #buf
|
|
end
|
|
|
|
local metric = buf:get()
|
|
|
|
buf:free() -- free buffer space ASAP
|
|
|
|
return metric
|
|
end
|
|
|
|
-- Extract short metric name from the full one.
|
|
--
|
|
-- This function is only used by Prometheus:metric_data.
|
|
--
|
|
-- Args:
|
|
-- full_name: (string) full metric name that can include labels.
|
|
--
|
|
-- Returns:
|
|
-- (string) short metric name with no labels. For a `*_bucket` metric of
|
|
-- histogram the _bucket suffix will be removed.
|
|
local function short_metric_name(full_name)
|
|
local labels_start, _ = full_name:find("{", 1, true)
|
|
if not labels_start then
|
|
return full_name
|
|
end
|
|
-- Try to detect if this is a histogram metric. We only check for the
|
|
-- `_bucket` suffix here, since it alphabetically goes before other
|
|
-- histogram suffixes (`_count` and `_sum`).
|
|
local suffix_idx, _ = full_name:find("_bucket{", 1, true)
|
|
if suffix_idx and full_name:find("le=", labels_start + 1, true) then
|
|
-- this is a histogram metric
|
|
return full_name:sub(1, suffix_idx - 1)
|
|
end
|
|
-- this is not a histogram metric
|
|
return full_name:sub(1, labels_start - 1)
|
|
end
|
|
|
|
-- Check metric name and label names for correctness.
|
|
--
|
|
-- Regular expressions to validate metric and label names are
|
|
-- documented in https://prometheus.io/docs/concepts/data_model/
|
|
--
|
|
-- Args:
|
|
-- metric_name: (string) metric name.
|
|
-- label_names: label names (array of strings).
|
|
--
|
|
-- Returns:
|
|
-- Either an error string, or nil of no errors were found.
|
|
local function check_metric_and_label_names(metric_name, label_names)
|
|
if not ngx_re_match(metric_name, METRIC_NAME_REGEX, "ijo") then
|
|
return "Metric name '" .. metric_name .. "' is invalid"
|
|
end
|
|
if not label_names then
|
|
return
|
|
end
|
|
|
|
for i = 1, #label_names do
|
|
local label_name = label_names[i]
|
|
if label_name == "le" then
|
|
return "Invalid label name 'le' in " .. metric_name
|
|
end
|
|
if not ngx_re_match(label_name, LABEL_NAME_REGEX, "ijo") then
|
|
return "Metric '" .. metric_name .. "' label name '" .. label_name ..
|
|
"' is invalid"
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Construct bucket format for a list of buckets.
|
|
--
|
|
-- This receives a list of buckets and returns a sprintf template that should
|
|
-- be used for bucket boundaries to make them come in increasing order when
|
|
-- sorted alphabetically.
|
|
--
|
|
-- To re-phrase, this is where we detect how many leading and trailing zeros we
|
|
-- need.
|
|
--
|
|
-- Args:
|
|
-- buckets: a list of buckets
|
|
--
|
|
-- Returns:
|
|
-- (string) a sprintf template.
|
|
local function construct_bucket_format(buckets)
|
|
local max_order = 1
|
|
local max_precision = 1
|
|
|
|
for i = 1, #buckets do
|
|
local bucket = buckets[i]
|
|
assert(type(bucket) == "number", "bucket boundaries should be numeric")
|
|
|
|
-- floating point number with all trailing zeros removed
|
|
local as_string = ngx_re_gsub(string.format("%f", bucket), "0*$", "", "jo")
|
|
|
|
local dot_idx = as_string:find(".", 1, true)
|
|
max_order = math.max(max_order, dot_idx - 1)
|
|
max_precision = math.max(max_precision, #as_string - dot_idx)
|
|
end
|
|
|
|
return "%0" .. (max_order + max_precision + 1) .. "." .. max_precision .. "f"
|
|
end
|
|
|
|
-- Format bucket format when exposing metrics.
|
|
--
|
|
-- This function removes leading and trailing zeroes from `le` label values.
|
|
--
|
|
-- Args:
|
|
-- key: the metric key
|
|
--
|
|
-- Returns:
|
|
-- (string) the formatted key
|
|
local function fix_histogram_bucket_labels(key)
|
|
local match, err = ngx_re_match(key, METRICS_KEY_REGEX, "jo")
|
|
if err then
|
|
ngx_log(ngx.ERR, "failed to match regex: ", err)
|
|
return
|
|
end
|
|
|
|
if not match then
|
|
return key
|
|
end
|
|
|
|
if match[2] == "Inf" then
|
|
return match[1] .. "+Inf" .. match[3]
|
|
else
|
|
return match[1] .. tostring(tonumber(match[2])) .. match[3]
|
|
end
|
|
end
|
|
|
|
-- Return a full metric name for a given metric+label combination.
|
|
--
|
|
-- This function calculates a full metric name (or, in case of a histogram
|
|
-- metric, several metric names) for a given combination of label values. It
|
|
-- stores the result in a tree of tables used as a cache (self.lookup) and
|
|
-- uses that cache to return results faster.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- label_values: a list of label values.
|
|
--
|
|
-- Returns:
|
|
-- - If `self` is a counter or a gauge: full metric name as a string.
|
|
-- - If `self` is a histogram metric: a list of strings:
|
|
-- [0]: full name of the _count histogram metric;
|
|
-- [1]: full name of the _sum histogram metric;
|
|
-- [...]: full names of each _bucket metrics.
|
|
local function lookup_or_create(self, label_values)
|
|
-- If one of the `label_values` is nil, #label_values will return the number
|
|
-- of non-nil labels in the beginning of the list. This will make us return an
|
|
-- error here as well.
|
|
local cnt = label_values and #label_values or 0
|
|
-- specially, if first element is nil, # will treat it as "non-empty"
|
|
if cnt ~= self.label_count or (self.label_count > 0 and label_values[1] == nil) then
|
|
return nil, string.format("metric '%s' has inconsistent labels count, expected %d, got %d",
|
|
self.name, self.label_count, cnt)
|
|
end
|
|
local t = self.lookup
|
|
if label_values then
|
|
-- Don't use ipairs here to avoid inner loop generates trace first
|
|
-- Otherwise the inner for loop below is likely to get JIT compiled before
|
|
-- the outer loop which include `lookup_or_create`, in this case the trace
|
|
-- for outer loop will be aborted. By not using ipairs, we will be able to
|
|
-- compile longer traces as possible.
|
|
local label
|
|
for i=1, self.label_count do
|
|
label = label_values[i]
|
|
if not t[label] then
|
|
t[label] = {}
|
|
end
|
|
t = t[label]
|
|
end
|
|
end
|
|
|
|
local LEAF_KEY = mt -- key used to store full metric names in leaf tables.
|
|
local full_name = t[LEAF_KEY]
|
|
if full_name then
|
|
return full_name
|
|
end
|
|
|
|
if self.typ == TYPE_HISTOGRAM then
|
|
-- Pass empty metric name to full_metric_name to just get the formatted
|
|
-- labels ({key1="value1",key2="value2",...}).
|
|
local labels = full_metric_name("", self.label_names, label_values)
|
|
full_name = {
|
|
self.name .. "_count" .. labels,
|
|
self.name .. "_sum" .. labels,
|
|
}
|
|
|
|
local bucket_pref
|
|
if self.label_count > 0 then
|
|
-- strip last }
|
|
bucket_pref = self.name .. "_bucket" .. string.sub(labels, 1, -2) .. ","
|
|
else
|
|
bucket_pref = self.name .. "_bucket{"
|
|
end
|
|
|
|
for i = 1, #self.buckets do
|
|
local buc = self.buckets[i]
|
|
full_name[i+2] = string.format("%sle=\"%s\"}", bucket_pref, self.bucket_format:format(buc))
|
|
end
|
|
-- Last bucket. Note, that the label value is "Inf" rather than "+Inf"
|
|
-- required by Prometheus. This is necessary for this bucket to be the last
|
|
-- one when all metrics are lexicographically sorted. "Inf" will get replaced
|
|
-- by "+Inf" in Prometheus:metric_data().
|
|
full_name[self.bucket_count+3] = string.format("%sle=\"Inf\"}", bucket_pref)
|
|
else
|
|
full_name = full_metric_name(self.name, self.label_names, label_values)
|
|
end
|
|
t[LEAF_KEY] = full_name
|
|
return full_name
|
|
end
|
|
|
|
-- Increment a gauge metric.
|
|
--
|
|
-- Gauges are incremented in the dictionary directly to provide strong ordering
|
|
-- of inc() and set() operations.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- value: numeric value to increment by. Can be negative.
|
|
-- label_values: a list of label values, in the same order as label keys.
|
|
local function inc_gauge(self, value, label_values)
|
|
local k, err, _
|
|
k, err = lookup_or_create(self, label_values)
|
|
if err then
|
|
self._log_error(err)
|
|
return
|
|
end
|
|
|
|
if self.local_storage then
|
|
local v = (self._local_dict[k] or 0) + value
|
|
self._local_dict[k] = v
|
|
return
|
|
end
|
|
|
|
_, err, _ = self._dict:incr(k, value, 0)
|
|
if err then
|
|
self._log_error_kv(k, value, err)
|
|
end
|
|
end
|
|
|
|
local ERR_MSG_COUNTER_NOT_INITIALIZED = "counter not initialized! " ..
|
|
"Have you called Prometheus:init() from the " ..
|
|
"init_worker_by_lua_block nginx phase?"
|
|
|
|
-- Increment a counter metric.
|
|
--
|
|
-- Counters are incremented in the per-worker counter, which will eventually get
|
|
-- flushed into the global shared dictionary.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- value: numeric value to increment by. Can be negative.
|
|
-- label_values: a list of label values, in the same order as label keys.
|
|
local function inc_counter(self, value, label_values)
|
|
-- counter is not allowed to decrease
|
|
if value and value < 0 then
|
|
self._log_error_kv(self.name, value, "Value should not be negative")
|
|
return
|
|
end
|
|
|
|
local k, err
|
|
k, err = lookup_or_create(self, label_values)
|
|
if err then
|
|
self._log_error(err)
|
|
return
|
|
end
|
|
|
|
local c = self._counter
|
|
if not c then
|
|
c = self.parent._counter
|
|
if not c then
|
|
self._log_error(ERR_MSG_COUNTER_NOT_INITIALIZED)
|
|
return
|
|
end
|
|
self._counter = c
|
|
end
|
|
c:incr(k, value)
|
|
end
|
|
|
|
-- Delete a counter or a gauge metric.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- label_values: a list of label values, in the same order as label keys.
|
|
local function del(self, label_values)
|
|
local k, _, err
|
|
k, err = lookup_or_create(self, label_values)
|
|
if err then
|
|
self._log_error(err)
|
|
return
|
|
end
|
|
|
|
-- `del` might be called immediately after a configuration change that stops a
|
|
-- given metric from being used, so we cannot guarantee that other workers
|
|
-- don't have unflushed counter values for a metric that is about to be
|
|
-- deleted. We wait for `sync_interval` here to ensure that those values are
|
|
-- synced (and deleted from worker-local counters) before a given metric is
|
|
-- removed.
|
|
-- Gauge metrics don't use per-worker counters, so for gauges we don't need to
|
|
-- wait for the counter to sync.
|
|
if self.typ ~= TYPE_GAUGE then
|
|
ngx_log(ngx.INFO, "waiting ", self.parent.sync_interval, "s for counter to sync")
|
|
ngx_sleep(self.parent.sync_interval)
|
|
end
|
|
|
|
if self.local_storage then
|
|
self._local_dict[k] = nil
|
|
return
|
|
end
|
|
|
|
_, err = self._dict:delete(k)
|
|
if err then
|
|
self._log_error("Error deleting key: ".. k .. ": " .. err)
|
|
end
|
|
end
|
|
|
|
-- Set the value of a gauge metric.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- value: numeric value.
|
|
-- label_values: a list of label values, in the same order as label keys.
|
|
local function set(self, value, label_values)
|
|
if not value then
|
|
self._log_error("No value passed for " .. self.name)
|
|
return
|
|
end
|
|
|
|
local k, _, err
|
|
k, err = lookup_or_create(self, label_values)
|
|
if err then
|
|
self._log_error(err)
|
|
return
|
|
end
|
|
|
|
if self.local_storage then
|
|
self._local_dict[k] = value
|
|
return
|
|
end
|
|
|
|
_, err = self._dict:safe_set(k, value)
|
|
if err then
|
|
self._log_error_kv(k, value, err)
|
|
end
|
|
end
|
|
|
|
-- Record a given value in a histogram.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
-- value: numeric value to record. Should be defined.
|
|
-- label_values: a list of label values, in the same order as label keys.
|
|
local function observe(self, value, label_values)
|
|
if not value then
|
|
self._log_error("No value passed for " .. self.name)
|
|
return
|
|
end
|
|
|
|
local keys, err = lookup_or_create(self, label_values)
|
|
if err then
|
|
self._log_error(err)
|
|
return
|
|
end
|
|
|
|
local c = self._counter
|
|
if not c then
|
|
c = self.parent._counter
|
|
if not c then
|
|
self._log_error(ERR_MSG_COUNTER_NOT_INITIALIZED)
|
|
return
|
|
end
|
|
self._counter = c
|
|
end
|
|
|
|
-- _count metric.
|
|
c:incr(keys[1], 1)
|
|
|
|
-- _sum metric.
|
|
c:incr(keys[2], value)
|
|
|
|
local seen = false
|
|
-- check in reverse order, otherwise we will always
|
|
-- need to traverse the whole table.
|
|
for i=self.bucket_count, 1, -1 do
|
|
if value <= self.buckets[i] then
|
|
c:incr(keys[2+i], 1)
|
|
seen = true
|
|
elseif seen then
|
|
break
|
|
end
|
|
end
|
|
-- the last bucket (le="Inf").
|
|
c:incr(keys[self.bucket_count+3], 1)
|
|
end
|
|
|
|
-- Delete all metrics for a given gauge, counter or a histogram.
|
|
--
|
|
-- This is like `del`, but will delete all time series for all previously
|
|
-- recorded label values.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
local function reset(self)
|
|
-- Wait for other worker threads to sync their counters before removing the
|
|
-- metric (please see `del` for a more detailed comment).
|
|
-- Gauge metrics don't use per-worker counters, so for gauges we don't need to
|
|
-- wait for the counter to sync.
|
|
if self.typ ~= TYPE_GAUGE then
|
|
ngx_log(ngx.INFO, "waiting ", self.parent.sync_interval, "s for counter to sync")
|
|
ngx_sleep(self.parent.sync_interval)
|
|
end
|
|
|
|
local keys = self._dict:get_keys(0)
|
|
local name_prefixes = {}
|
|
local name_prefix_length_base = #self.name
|
|
if self.typ == TYPE_HISTOGRAM then
|
|
if self.label_count == 0 then
|
|
name_prefixes[self.name .. "_count"] = name_prefix_length_base + 6
|
|
name_prefixes[self.name .. "_sum"] = name_prefix_length_base + 4
|
|
else
|
|
name_prefixes[self.name .. "_count{"] = name_prefix_length_base + 7
|
|
name_prefixes[self.name .. "_sum{"] = name_prefix_length_base + 5
|
|
end
|
|
name_prefixes[self.name .. "_bucket{"] = name_prefix_length_base + 8
|
|
else
|
|
name_prefixes[self.name .. "{"] = name_prefix_length_base + 1
|
|
end
|
|
|
|
for i = 1, #keys do
|
|
local key = keys[i]
|
|
local value, err = self._dict:get(key)
|
|
if value then
|
|
-- For a metric to be deleted its name should either match exactly, or
|
|
-- have a prefix listed in `name_prefixes` (which ensures deletion of
|
|
-- metrics with label values).
|
|
local remove = key == self.name
|
|
if not remove then
|
|
for name_prefix, name_prefix_length in pairs(name_prefixes) do
|
|
if name_prefix == string.sub(key, 1, name_prefix_length) then
|
|
remove = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if remove then
|
|
local _, err = self._dict:safe_set(key, nil)
|
|
if err then
|
|
self._log_error("Error resetting '", key, "': ", err)
|
|
end
|
|
end
|
|
else
|
|
self._log_error("Error getting '", key, "': ", err)
|
|
end
|
|
end
|
|
|
|
-- Clean up the full metric name lookup table as well.
|
|
self.lookup = {}
|
|
end
|
|
|
|
-- Delete all metrics for a given gauge, counter or a histogram.
|
|
-- Similar to `reset`, but is used for local_metrics thus simplified
|
|
--
|
|
-- This is like `del`, but will delete all time series for all previously
|
|
-- recorded label values.
|
|
--
|
|
-- Args:
|
|
-- self: a `metric` object, created by register().
|
|
local function reset_local(self)
|
|
local name_prefix = self.name .. "{"
|
|
local name_prefix_length = #name_prefix
|
|
for key, _ in pairs(self._local_dict) do
|
|
if string.sub(key, 1, name_prefix_length) == name_prefix then
|
|
self._local_dict[key] = nil
|
|
end
|
|
end
|
|
|
|
-- Clean up the full metric name lookup table as well.
|
|
self.lookup = {}
|
|
end
|
|
|
|
-- Initialize the module.
|
|
--
|
|
-- This should be called once from the `init_by_lua` section in nginx
|
|
-- configuration.
|
|
--
|
|
-- Args:
|
|
-- dict_name: (string) name of the nginx shared dictionary which will be
|
|
-- used to store all metrics
|
|
-- prefix: (optional string) if supplied, prefix is added to all
|
|
-- metric names on output
|
|
--
|
|
-- Returns:
|
|
-- an object that should be used to register metrics.
|
|
function Prometheus.init(dict_name, options_or_prefix)
|
|
local phase = ngx.get_phase()
|
|
if phase ~= 'init' and phase ~= 'init_worker' and
|
|
phase ~= 'timer' then
|
|
error('Prometheus.init can only be called from ' ..
|
|
'init_by_lua_block, init_worker_by_lua_block or timer' , 2)
|
|
end
|
|
|
|
local self = setmetatable({}, mt)
|
|
dict_name = dict_name or "prometheus_metrics"
|
|
self.dict_name = dict_name
|
|
self.dict = ngx.shared[dict_name]
|
|
if self.dict == nil then
|
|
error("Dictionary '" .. dict_name .. "' does not seem to exist. " ..
|
|
"Please define the dictionary using `lua_shared_dict`.", 2)
|
|
end
|
|
|
|
if type(options_or_prefix) == "table" then
|
|
self.prefix = options_or_prefix.prefix or ''
|
|
self.error_metric_name = options_or_prefix.error_metric_name or
|
|
DEFAULT_ERROR_METRIC_NAME
|
|
self.sync_interval = options_or_prefix.sync_interval or
|
|
DEFAULT_SYNC_INTERVAL
|
|
else
|
|
self.prefix = options_or_prefix or ''
|
|
self.error_metric_name = DEFAULT_ERROR_METRIC_NAME
|
|
self.sync_interval = DEFAULT_SYNC_INTERVAL
|
|
end
|
|
|
|
self.registry = {}
|
|
|
|
self.local_metrics = {}
|
|
|
|
self.initialized = true
|
|
|
|
self:counter(self.error_metric_name, "Number of nginx-lua-prometheus errors")
|
|
self.dict:set(self.error_metric_name, 0)
|
|
|
|
if phase == 'init_worker' then
|
|
self:init_worker(self.sync_interval)
|
|
end
|
|
return self
|
|
end
|
|
|
|
-- Initialize the worker counter.
|
|
--
|
|
-- This can call this function from the `init_worker_by_lua` if you are calling
|
|
-- Prometheus.init() from `init_by_lua`, but this is deprecated. Instead, just
|
|
-- call Prometheus.init() from `init_worker_by_lua_block` and pass sync_interval
|
|
-- as part of the `options` argument if you need.
|
|
--
|
|
-- Args:
|
|
-- sync_interval: per-worker counter sync interval (in seconds).
|
|
function Prometheus:init_worker(sync_interval)
|
|
if ngx.get_phase() ~= 'init_worker' then
|
|
error('Prometheus:init_worker can only be called in ' ..
|
|
'init_worker_by_lua_block', 2)
|
|
end
|
|
if self._counter then
|
|
ngx_log(ngx.WARN, 'init_worker() has been called twice. ' ..
|
|
'Please do not explicitly call init_worker. ' ..
|
|
'Instead, call Prometheus:init() in the init_worker_by_lua_block')
|
|
return
|
|
end
|
|
self.sync_interval = sync_interval or DEFAULT_SYNC_INTERVAL
|
|
local counter_instance, err = resty_counter_lib.new(
|
|
self.dict_name, self.sync_interval)
|
|
if err then
|
|
error(err, 2)
|
|
end
|
|
self._counter = counter_instance
|
|
end
|
|
|
|
-- Register a new metric.
|
|
--
|
|
-- Args:
|
|
-- self: a Prometheus object.
|
|
-- name: (string) name of the metric. Required.
|
|
-- help: (string) description of the metric. Will be used for the HELP
|
|
-- comment on the metrics page. Optional.
|
|
-- label_names: array of strings, defining a list of metrics. Optional.
|
|
-- buckets: array if numbers, defining bucket boundaries. Only used for
|
|
-- histogram metrics.
|
|
-- typ: metric type (one of the TYPE_* constants).
|
|
--
|
|
-- Returns:
|
|
-- a new metric object.
|
|
local function register(self, name, help, label_names, buckets, typ, local_storage)
|
|
if not self.initialized then
|
|
ngx_log(ngx.ERR, "Prometheus module has not been initialized")
|
|
return
|
|
end
|
|
|
|
local err = check_metric_and_label_names(name, label_names)
|
|
if err then
|
|
self:log_error(err)
|
|
return
|
|
end
|
|
|
|
local name_maybe_historgram = name
|
|
|
|
if string.find(name_maybe_historgram, "_bucket", 1, true) then
|
|
name_maybe_historgram = ngx_re_gsub(name_maybe_historgram, "_bucket$", "", "jo")
|
|
end
|
|
if string.find(name_maybe_historgram, "_count", 1, true) then
|
|
name_maybe_historgram = ngx_re_gsub(name_maybe_historgram, "_count$", "", "jo")
|
|
end
|
|
if string.find(name_maybe_historgram, "_sum", 1, true) then
|
|
name_maybe_historgram = ngx_re_gsub(name_maybe_historgram, "_sum$", "", "jo")
|
|
end
|
|
|
|
if (typ ~= TYPE_HISTOGRAM and (
|
|
self.registry[name] or self.registry[name_maybe_historgram]
|
|
)) or
|
|
(typ == TYPE_HISTOGRAM and (
|
|
self.registry[name] or
|
|
self.registry[name .. "_count"] or
|
|
self.registry[name .. "_sum"] or self.registry[name .. "_bucket"]
|
|
)) then
|
|
|
|
self:log_error("Duplicate metric " .. name)
|
|
return
|
|
end
|
|
|
|
if typ ~= TYPE_GAUGE and local_storage then
|
|
ngx_log(ngx.ERR, "Cannot use local_storage metrics for non Gauge type")
|
|
return
|
|
end
|
|
|
|
local metric = {
|
|
name = name,
|
|
help = help,
|
|
typ = typ,
|
|
label_names = label_names,
|
|
label_count = label_names and #label_names or 0,
|
|
-- Lookup is a tree of lua tables that contain label values, with leaf
|
|
-- tables containing full metric names. For example, given a metric
|
|
-- `http_count` and labels `host` and `status`, it might contain the
|
|
-- following values:
|
|
-- ['me.com']['200'][LEAF_KEY] = 'http_count{host="me.com",status="200"}'
|
|
-- ['me.com']['500'][LEAF_KEY] = 'http_count{host="me.com",status="500"}'
|
|
-- ['my.net']['200'][LEAF_KEY] = 'http_count{host="my.net",status="200"}'
|
|
-- ['my.net']['500'][LEAF_KEY] = 'http_count{host="my.net",status="500"}'
|
|
lookup = {},
|
|
parent = self,
|
|
-- Store a reference for logging functions for faster lookup.
|
|
_log_error = function(...) self:log_error(...) end,
|
|
_log_error_kv = function(...) self:log_error_kv(...) end,
|
|
_dict = self.dict,
|
|
_local_dict = self.local_metrics,
|
|
local_storage = local_storage,
|
|
reset = local_storage and reset_local or reset,
|
|
}
|
|
if typ < TYPE_HISTOGRAM then
|
|
if typ == TYPE_GAUGE then
|
|
metric.set = set
|
|
metric.inc = inc_gauge
|
|
else
|
|
metric.inc = inc_counter
|
|
end
|
|
metric.del = del
|
|
else
|
|
metric.observe = observe
|
|
metric.buckets = buckets or DEFAULT_BUCKETS
|
|
metric.bucket_count = #metric.buckets
|
|
metric.bucket_format = construct_bucket_format(metric.buckets)
|
|
end
|
|
|
|
self.registry[name] = metric
|
|
return metric
|
|
end
|
|
|
|
|
|
-- Public function to register a counter.
|
|
function Prometheus:counter(name, help, label_names)
|
|
return register(self, name, help, label_names, nil, TYPE_COUNTER)
|
|
end
|
|
|
|
Prometheus.LOCAL_STORAGE = true
|
|
-- Public function to register a gauge.
|
|
function Prometheus:gauge(name, help, label_names, local_storage)
|
|
return register(self, name, help, label_names, nil, TYPE_GAUGE, local_storage)
|
|
end
|
|
|
|
|
|
-- Public function to register a histogram.
|
|
function Prometheus:histogram(name, help, label_names, buckets)
|
|
return register(self, name, help, label_names, buckets, TYPE_HISTOGRAM)
|
|
end
|
|
|
|
-- Prometheus compatible metric data as an array of strings.
|
|
--
|
|
-- Returns:
|
|
-- Array of strings with all metrics in a text format compatible with
|
|
-- Prometheus.
|
|
function Prometheus:metric_data(write_fn, local_only)
|
|
if not self.initialized then
|
|
ngx_log(ngx.ERR, "Prometheus module has not been initialized")
|
|
return
|
|
end
|
|
write_fn = write_fn or ngx.print
|
|
|
|
-- Force a manual sync of counter local state (mostly to make tests work).
|
|
self._counter:sync()
|
|
|
|
local keys
|
|
if local_only then
|
|
keys = {}
|
|
|
|
else
|
|
keys = self.dict:get_keys(0)
|
|
end
|
|
|
|
local count = #keys
|
|
for k, v in pairs(self.local_metrics) do
|
|
keys[count+1] = k
|
|
count = count + 1
|
|
end
|
|
-- Prometheus server expects buckets of a histogram to appear in increasing
|
|
-- numerical order of their label values.
|
|
table_sort(keys)
|
|
|
|
local seen_metrics = tb_new(0, count)
|
|
|
|
-- the output is an integral string, not an array any more
|
|
local output = buffer.new(DATA_BUFFER_SIZE_HINT)
|
|
local output_count = 0
|
|
|
|
local function buffered_print(fmt, ...)
|
|
if fmt then
|
|
output_count = output_count + 1
|
|
output:putf(fmt, ...)
|
|
end
|
|
|
|
if output_count >= 100 or not fmt then
|
|
write_fn(output:get()) -- consume the whole buffer
|
|
output_count = 0
|
|
end
|
|
end
|
|
|
|
for i = 1, count do
|
|
yield()
|
|
|
|
local key = keys[i]
|
|
|
|
local value, err
|
|
local is_local_metrics = true
|
|
value = self.local_metrics[key]
|
|
if (not value) and (not local_only) then
|
|
is_local_metrics = false
|
|
value, err = self.dict:get(key)
|
|
end
|
|
|
|
if not value then
|
|
self:log_error("Error getting '", key, "': ", err)
|
|
goto continue
|
|
end
|
|
|
|
local short_name = short_metric_name(key)
|
|
if not seen_metrics[short_name] then
|
|
local m = self.registry[short_name]
|
|
if m then
|
|
if m.help then
|
|
buffered_print("# HELP %s%s %s\n",
|
|
self.prefix, short_name, m.help)
|
|
end
|
|
if m.typ then
|
|
buffered_print("# TYPE %s%s %s\n",
|
|
self.prefix, short_name, TYPE_LITERAL[m.typ])
|
|
end
|
|
end
|
|
seen_metrics[short_name] = true
|
|
end
|
|
if not is_local_metrics then -- local metrics is always a gauge
|
|
key = fix_histogram_bucket_labels(key)
|
|
end
|
|
buffered_print("%s%s %s\n", self.prefix, key, value)
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
buffered_print(nil)
|
|
|
|
output:free()
|
|
end
|
|
|
|
-- Present all metrics in a text format compatible with Prometheus.
|
|
--
|
|
-- This function should be used to expose the metrics on a separate HTTP page.
|
|
-- It will get the metrics from the dictionary, sort them, and expose them
|
|
-- aling with TYPE and HELP comments.
|
|
function Prometheus:collect()
|
|
ngx.header["Content-Type"] = "text/plain"
|
|
self:metric_data()
|
|
end
|
|
|
|
-- Log an error, incrementing the error counter.
|
|
function Prometheus:log_error(...)
|
|
ngx_log(ngx.ERR, ...)
|
|
self.dict:incr(self.error_metric_name, 1, 0)
|
|
end
|
|
|
|
-- Log an error that happened while setting up a dictionary key.
|
|
function Prometheus:log_error_kv(key, value, err)
|
|
self:log_error(
|
|
"Error while setting '", key, "' to '", value, "': '", err, "'")
|
|
end
|
|
|
|
return Prometheus
|