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", {