--- @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