Software /
code /
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 |
parent | 4544:64fa2dd34d43 |
child | 4595: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", { |