Mercurial > prosody-modules
comparison mod_prometheus/mod_prometheus.lua @ 4555:1e70538e4641
mod_prometheus: Port to new OpenMetrics based statistics module
| author | Jonas Schäfer <jonas@wielicki.name> |
|---|---|
| date | Wed, 28 Apr 2021 08:22:47 +0200 |
| parents | 64fa2dd34d43 |
| children | bac3dae031ee |
comparison
equal
deleted
inserted
replaced
| 4554:025cf93acfe9 | 4555:1e70538e4641 |
|---|---|
| 12 local t_insert = table.insert; | 12 local t_insert = table.insert; |
| 13 local t_concat = table.concat; | 13 local t_concat = table.concat; |
| 14 local socket = require "socket"; | 14 local socket = require "socket"; |
| 15 local statsman = require "core.statsmanager"; | 15 local statsman = require "core.statsmanager"; |
| 16 local get_stats = statsman.get_stats; | 16 local get_stats = statsman.get_stats; |
| 17 local get_metric_registry = statsman.get_metric_registry; | |
| 18 local collect = statsman.collect; | |
| 17 | 19 |
| 18 local function escape(text) | 20 local function escape(text) |
| 19 return text:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n"); | 21 return text:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n"); |
| 20 end | 22 end |
| 21 | 23 |
| 22 local function escape_name(name) | 24 local function escape_name(name) |
| 23 return name:gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); | 25 return name:gsub("/", "__"):gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); |
| 24 end | 26 end |
| 25 | 27 |
| 26 local function get_timestamp() | 28 local function get_timestamp() |
| 27 -- Using LuaSocket for that because os.time() only has second precision. | 29 -- Using LuaSocket for that because os.time() only has second precision. |
| 28 return math.floor(socket.gettime() * 1000); | 30 return math.floor(socket.gettime() * 1000); |
| 29 end | 31 end |
| 30 | 32 |
| 31 local function repr_help(metric, docstring) | 33 local function repr_help(metric, docstring) |
| 32 docstring = docstring:gsub("\\", "\\\\"):gsub("\n", "\\n"); | 34 docstring = docstring:gsub("\\", "\\\\"):gsub("\n", "\\n"); |
| 33 return "# HELP "..escape_name(metric).." "..docstring.."\n"; | 35 return "# HELP "..escape_name(metric).." "..docstring.."\n"; |
| 36 end | |
| 37 | |
| 38 local function repr_unit(metric, unit) | |
| 39 if not unit then | |
| 40 unit = "" | |
| 41 else | |
| 42 unit = unit:gsub("\\", "\\\\"):gsub("\n", "\\n"); | |
| 43 end | |
| 44 return "# UNIT "..escape_name(metric).." "..unit.."\n"; | |
| 34 end | 45 end |
| 35 | 46 |
| 36 -- local allowed_types = { counter = true, gauge = true, histogram = true, summary = true, untyped = true }; | 47 -- local allowed_types = { counter = true, gauge = true, histogram = true, summary = true, untyped = true }; |
| 37 -- local allowed_types = { "counter", "gauge", "histogram", "summary", "untyped" }; | 48 -- local allowed_types = { "counter", "gauge", "histogram", "summary", "untyped" }; |
| 38 local function repr_type(metric, type_) | 49 local function repr_type(metric, type_) |
| 44 | 55 |
| 45 local function repr_label(key, value) | 56 local function repr_label(key, value) |
| 46 return key.."=\""..escape(value).."\""; | 57 return key.."=\""..escape(value).."\""; |
| 47 end | 58 end |
| 48 | 59 |
| 49 local function repr_labels(labels) | 60 local function repr_labels(labelkeys, labelvalues, extra_labels) |
| 50 local values = {} | 61 local values = {} |
| 51 for key, value in pairs(labels) do | 62 if labelkeys then |
| 52 t_insert(values, repr_label(escape_name(key), escape(value))); | 63 for i, key in ipairs(labelkeys) do |
| 64 local value = labelvalues[i] | |
| 65 t_insert(values, repr_label(escape_name(key), escape(value))); | |
| 66 end | |
| 67 end | |
| 68 if extra_labels then | |
| 69 for key, value in pairs(extra_labels) do | |
| 70 t_insert(values, repr_label(escape_name(key), escape(value))); | |
| 71 end | |
| 53 end | 72 end |
| 54 if #values == 0 then | 73 if #values == 0 then |
| 55 return ""; | 74 return ""; |
| 56 end | 75 end |
| 57 return "{"..t_concat(values, ",").."}"; | 76 return "{"..t_concat(values, ",").."}"; |
| 58 end | 77 end |
| 59 | 78 |
| 60 local function repr_sample(metric, labels, value, timestamp) | 79 local function repr_sample(metric, labelkeys, labelvalues, extra_labels, value) |
| 61 return escape_name(metric)..repr_labels(labels).." "..value.." "..timestamp.."\n"; | 80 return escape_name(metric)..repr_labels(labelkeys, labelvalues, extra_labels).." "..string.format("%.17g", value).."\n"; |
| 62 end | 81 end |
| 63 | 82 |
| 64 local allowed_extras = { min = true, max = true, average = true }; | 83 local get_metrics; |
| 65 local function insert_extras(data, key, name, timestamp, extra) | 84 if statsman.get_metric_registry then |
| 66 if not extra then | 85 module:log("debug", "detected OpenMetrics statsmanager") |
| 67 return false; | 86 -- Prosody 0.12+ with OpenMetrics |
| 68 end | 87 function get_metrics(event) |
| 69 local has_extra = false; | 88 local response = event.response; |
| 70 for extra_name in pairs(allowed_extras) do | 89 response.headers.content_type = "application/openmetrics-text; version=0.0.4"; |
| 71 if extra[extra_name] then | 90 |
| 91 if collect then | |
| 92 -- Ensure to get up-to-date samples when running in manual mode | |
| 93 collect() | |
| 94 end | |
| 95 | |
| 96 local registry = get_metric_registry() | |
| 97 if registry == nil then | |
| 98 response.headers.content_type = "text/plain; charset=utf-8" | |
| 99 response.status_code = 404 | |
| 100 return "No statistics provider configured\n" | |
| 101 end | |
| 102 local answer = {}; | |
| 103 for metric_family_name, metric_family in pairs(registry:get_metric_families()) do | |
| 104 t_insert(answer, repr_help(metric_family_name, metric_family.description)) | |
| 105 t_insert(answer, repr_unit(metric_family_name, metric_family.unit)) | |
| 106 t_insert(answer, repr_type(metric_family_name, metric_family.type_)) | |
| 107 for labelset, metric in metric_family:iter_metrics() do | |
| 108 for suffix, extra_labels, value in metric:iter_samples() do | |
| 109 t_insert(answer, repr_sample(metric_family_name..suffix, metric_family.label_keys, labelset, extra_labels, value)) | |
| 110 end | |
| 111 end | |
| 112 end | |
| 113 t_insert(answer, "# EOF\n") | |
| 114 return t_concat(answer, ""); | |
| 115 end | |
| 116 else | |
| 117 module:log("debug", "detected pre-OpenMetrics statsmanager") | |
| 118 -- Pre-OpenMetrics | |
| 119 | |
| 120 local allowed_extras = { min = true, max = true, average = true }; | |
| 121 local function insert_extras(data, key, name, timestamp, extra) | |
| 122 if not extra then | |
| 123 return false; | |
| 124 end | |
| 125 local has_extra = false; | |
| 126 for extra_name in pairs(allowed_extras) do | |
| 127 if extra[extra_name] then | |
| 128 local field = { | |
| 129 value = extra[extra_name], | |
| 130 labels = { | |
| 131 ["type"] = name, | |
| 132 field = extra_name, | |
| 133 }, | |
| 134 typ = "gauge"; | |
| 135 timestamp = timestamp, | |
| 136 }; | |
| 137 t_insert(data[key], field); | |
| 138 has_extra = true; | |
| 139 end | |
| 140 end | |
| 141 return has_extra; | |
| 142 end | |
| 143 | |
| 144 local function parse_stats() | |
| 145 local timestamp = tostring(get_timestamp()); | |
| 146 local data = {}; | |
| 147 local stats, changed_only, extras = get_stats(); | |
| 148 for stat, value in pairs(stats) do | |
| 149 -- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value)); | |
| 150 local extra = extras[stat]; | |
| 151 local host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$"); | |
| 152 if host == nil then | |
| 153 sect, name, typ = stat:match("^([^.]+)%.(.+):(%a+)$"); | |
| 154 elseif host == "*" then | |
| 155 host = nil; | |
| 156 end | |
| 157 if sect:find("^mod_measure_.") then | |
| 158 sect = sect:sub(13); | |
| 159 elseif sect:find("^mod_statistics_.") then | |
| 160 sect = sect:sub(16); | |
| 161 end | |
| 162 | |
| 163 local key = escape_name("prosody_"..sect); | |
| 72 local field = { | 164 local field = { |
| 73 value = extra[extra_name], | 165 value = value, |
| 74 labels = { | 166 labels = { ["type"] = name}, |
| 75 ["type"] = name, | 167 -- TODO: Use the other types where it makes sense. |
| 76 field = extra_name, | 168 typ = (typ == "rate" and "counter" or "gauge"), |
| 77 }, | |
| 78 typ = "gauge"; | |
| 79 timestamp = timestamp, | 169 timestamp = timestamp, |
| 80 }; | 170 }; |
| 81 t_insert(data[key], field); | 171 if host then |
| 82 has_extra = true; | 172 field.labels.host = host; |
| 83 end | 173 end |
| 84 end | 174 if data[key] == nil then |
| 85 return has_extra; | 175 data[key] = {}; |
| 86 end | 176 end |
| 87 | 177 if not insert_extras(data, key, name, timestamp, extra) then |
| 88 local function parse_stats() | 178 t_insert(data[key], field); |
| 89 local timestamp = tostring(get_timestamp()); | 179 end |
| 90 local data = {}; | 180 end |
| 91 local stats, changed_only, extras = get_stats(); | 181 return data; |
| 92 for stat, value in pairs(stats) do | 182 end |
| 93 -- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value)); | 183 |
| 94 local extra = extras[stat]; | 184 function get_metrics(event) |
| 95 local host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$"); | 185 local response = event.response; |
| 96 if host == nil then | 186 response.headers.content_type = "text/plain; version=0.0.4"; |
| 97 sect, name, typ = stat:match("^([^.]+)%.(.+):(%a+)$"); | 187 if statsman.collect then |
| 98 elseif host == "*" then | 188 statsman.collect() |
| 99 host = nil; | 189 end |
| 100 end | 190 |
| 101 if sect:find("^mod_measure_.") then | 191 local answer = {}; |
| 102 sect = sect:sub(13); | 192 for key, fields in pairs(parse_stats()) do |
| 103 elseif sect:find("^mod_statistics_.") then | 193 t_insert(answer, repr_help(key, "")); |
| 104 sect = sect:sub(16); | 194 t_insert(answer, repr_type(key, fields[1].typ)); |
| 105 end | 195 for _, field in pairs(fields) do |
| 106 | 196 t_insert(answer, repr_sample(key, nil, nil, field.labels, field.value, field.timestamp)); |
| 107 local key = escape_name("prosody_"..sect); | 197 end |
| 108 local field = { | 198 end |
| 109 value = value, | 199 return t_concat(answer, ""); |
| 110 labels = { ["type"] = name}, | 200 end |
| 111 -- TODO: Use the other types where it makes sense. | |
| 112 typ = (typ == "rate" and "counter" or "gauge"), | |
| 113 timestamp = timestamp, | |
| 114 }; | |
| 115 if host then | |
| 116 field.labels.host = host; | |
| 117 end | |
| 118 if data[key] == nil then | |
| 119 data[key] = {}; | |
| 120 end | |
| 121 if not insert_extras(data, key, name, timestamp, extra) then | |
| 122 t_insert(data[key], field); | |
| 123 end | |
| 124 end | |
| 125 return data; | |
| 126 end | |
| 127 | |
| 128 local function get_metrics(event) | |
| 129 local response = event.response; | |
| 130 response.headers.content_type = "text/plain; version=0.0.4"; | |
| 131 if statsman.collect then | |
| 132 statsman.collect() | |
| 133 end | |
| 134 | |
| 135 local answer = {}; | |
| 136 for key, fields in pairs(parse_stats()) do | |
| 137 t_insert(answer, repr_help(key, "TODO: add a description here.")); | |
| 138 t_insert(answer, repr_type(key, fields[1].typ)); | |
| 139 for _, field in pairs(fields) do | |
| 140 t_insert(answer, repr_sample(key, field.labels, field.value, field.timestamp)); | |
| 141 end | |
| 142 end | |
| 143 return t_concat(answer, ""); | |
| 144 end | 201 end |
| 145 | 202 |
| 146 function module.add_host(module) | 203 function module.add_host(module) |
| 147 module:depends "http"; | 204 module:depends "http"; |
| 148 module:provides("http", { | 205 module:provides("http", { |
