comparison core/statsmanager.lua @ 11523:5f15ab7c6ae5

Statistics: Rewrite statistics backends to use OpenMetrics The metric subsystem of Prosody has had some shortcomings from the perspective of the current state-of-the-art in metric observability. The OpenMetrics standard [0] is a formalization of the data model (and serialization format) of the well-known and widely-used Prometheus [1] software stack. The previous stats subsystem of Prosody did not map well to that format (see e.g. [2] and [3]); the key reason is that it was trying to do too much math on its own ([2]) while lacking first-class support for "families" of metrics ([3]) and structured metric metadata (despite the `extra` argument to metrics, there was no standard way of representing common things like "tags" or "labels"). Even though OpenMetrics has grown from the Prometheus world of monitoring, it maps well to other popular monitoring stacks such as: - InfluxDB (labels can be mapped to tags and fields as necessary) - Carbon/Graphite (labels can be attached to the metric name with dot-separation) - StatsD (see graphite when assuming that graphite is used as backend, which is the default) The util.statsd module has been ported to use the OpenMetrics model as a proof of concept. An implementation which exposes the util.statistics backend data as Prometheus metrics is ready for publishing in prosody-modules (most likely as mod_openmetrics_prometheus to avoid breaking existing 0.11 deployments). At the same time, the previous measure()-based API had one major advantage: It is really simple and easy to use without requiring lots of knowledge about OpenMetrics or similar concepts. For that reason as well as compatibility with existing code, it is preserved and may even be extended in the future. However, code relying on the `stats-updated` event as well as `get_stats` from `statsmanager` will break because the data model has changed completely; in case of `stats-updated`, the code will simply not run (as the event was renamed in order to avoid conflicts); the `get_stats` function has been removed completely (so it will cause a traceback when it is attempted to be used). Note that the measure_*_event methods have been removed from the module API. I was unable to find any uses or documentation and thus deemed they should not be ported. Re-implementation is possible when necessary. [0]: https://openmetrics.io/ [1]: https://prometheus.io/ [2]: #959 [3]: #960
author Jonas Schäfer <jonas@wielicki.name>
date Sun, 18 Apr 2021 11:47:41 +0200
parents 10d13e0554f9
children 76d32b2ca5eb
comparison
equal deleted inserted replaced
11522:5bd38d9197e1 11523:5f15ab7c6ae5
1 1
2 local config = require "core.configmanager"; 2 local config = require "core.configmanager";
3 local log = require "util.logger".init("stats"); 3 local log = require "util.logger".init("stats");
4 local timer = require "util.timer"; 4 local timer = require "util.timer";
5 local fire_event = prosody.events.fire_event; 5 local fire_event = prosody.events.fire_event;
6 local array = require "util.array";
7 local timed = require "util.openmetrics".timed;
6 8
7 local stats_interval_config = config.get("*", "statistics_interval"); 9 local stats_interval_config = config.get("*", "statistics_interval");
8 local stats_interval = tonumber(stats_interval_config); 10 local stats_interval = tonumber(stats_interval_config);
9 if stats_interval_config and not stats_interval and stats_interval_config ~= "manual" then 11 if stats_interval_config and not stats_interval and stats_interval_config ~= "manual" then
10 log("error", "Invalid 'statistics_interval' setting, statistics will be disabled"); 12 log("error", "Invalid 'statistics_interval' setting, statistics will be disabled");
55 57
56 if stats == nil then 58 if stats == nil then
57 log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err); 59 log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err);
58 end 60 end
59 61
60 local measure, collect; 62 local measure, collect, metric, cork, uncork;
61 local latest_stats = {};
62 local changed_stats = {};
63 local stats_extra = {};
64 63
65 if stats then 64 if stats then
66 function measure(type, name, conf) 65 function metric(type_, name, unit, description, labels, extra)
67 local f = assert(stats[type], "unknown stat type: "..type); 66 local registry = stats.metric_registry
68 return f(name, conf); 67 local f = assert(registry[type_], "unknown metric family type: "..type_);
68 return f(registry, name, unit or "", description or "", labels, extra);
69 end
70
71 local function new_legacy_metric(stat_type, name, unit, description, fixed_label_key, fixed_label_value, extra)
72 local label_keys = array()
73 local conf = extra or {}
74 if fixed_label_key then
75 label_keys:push(fixed_label_key)
76 end
77 unit = unit or ""
78 local mf = metric(stat_type, "prosody_" .. name, unit, description, label_keys, conf);
79 if fixed_label_key then
80 mf = mf:with_partial_label(fixed_label_value)
81 end
82 return mf:with_labels()
83 end
84
85 local function unwrap_legacy_extra(extra, type_, name, unit)
86 local description = extra and extra.description or "Legacy "..type_.." metric "..name
87 unit = extra and extra.unit or unit
88 return description, unit
89 end
90
91 -- These wrappers provide the pre-OpenMetrics interface of statsmanager
92 -- and moduleapi (module:measure).
93 local legacy_metric_wrappers = {
94 amount = function(name, fixed_label_key, fixed_label_value, extra)
95 local initial = 0
96 if type(extra) == "number" then
97 initial = extra
98 else
99 initial = extra and extra.initial or initial
100 end
101 local description, unit = unwrap_legacy_extra(extra, "amount", name)
102
103 local m = new_legacy_metric("gauge", name, unit, description, fixed_label_key, fixed_label_value)
104 m:set(initial or 0)
105 return function(v)
106 m:set(v)
107 end
108 end;
109
110 counter = function(name, fixed_label_key, fixed_label_value, extra)
111 if type(extra) == "number" then
112 -- previous versions of the API allowed passing an initial
113 -- value here; we do not allow that anymore, it is not a thing
114 -- which makes sense with counters
115 extra = nil
116 end
117
118 local description, unit = unwrap_legacy_extra(extra, "counter", name)
119
120 local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value)
121 m:set(0)
122 return function(v)
123 m:add(v)
124 end
125 end;
126
127 rate = function(name, fixed_label_key, fixed_label_value, extra)
128 if type(extra) == "number" then
129 -- previous versions of the API allowed passing an initial
130 -- value here; we do not allow that anymore, it is not a thing
131 -- which makes sense with counters
132 extra = nil
133 end
134
135 local description, unit = unwrap_legacy_extra(extra, "counter", name)
136
137 local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value)
138 m:set(0)
139 return function()
140 m:add(1)
141 end
142 end;
143
144 times = function(name, fixed_label_key, fixed_label_value, extra)
145 local conf = {}
146 if extra and extra.buckets then
147 conf.buckets = extra.buckets
148 else
149 conf.buckets = { 0.001, 0.01, 0.1, 1.0, 10.0, 100.0 }
150 end
151 local description, _ = unwrap_legacy_extra(extra, "times", name)
152
153 local m = new_legacy_metric("histogram", name, "seconds", description, fixed_label_key, fixed_label_value, conf)
154 return function()
155 return timed(m)
156 end
157 end;
158
159 sizes = function(name, fixed_label_key, fixed_label_value, extra)
160 local conf = {}
161 if extra and extra.buckets then
162 conf.buckets = extra.buckets
163 else
164 conf.buckets = { 1024, 4096, 32768, 131072, 1048576, 4194304, 33554432, 134217728, 1073741824 }
165 end
166 local description, _ = unwrap_legacy_extra(extra, "sizes", name)
167
168 local m = new_legacy_metric("histogram", name, "bytes", description, fixed_label_key, fixed_label_value, conf)
169 return function(v)
170 m:sample(v)
171 end
172 end;
173
174 distribution = function(name, fixed_label_key, fixed_label_value, extra)
175 if type(extra) == "string" then
176 -- compat with previous API
177 extra = { unit = extra }
178 end
179 local description, unit = unwrap_legacy_extra(extra, "distribution", name, "")
180 local m = new_legacy_metric("summary", name, unit, description, fixed_label_key, fixed_label_value)
181 return function(v)
182 m:sample(v)
183 end
184 end;
185 };
186
187 -- Argument order switched here to support the legacy statsmanager.measure
188 -- interface.
189 function measure(stat_type, name, extra, fixed_label_key, fixed_label_value)
190 local wrapper = assert(legacy_metric_wrappers[stat_type], "unknown legacy metric type "..stat_type)
191 return wrapper(name, fixed_label_key, fixed_label_value, extra)
192 end
193
194 if stats.cork then
195 function cork()
196 return stats:cork()
197 end
198
199 function uncork()
200 return stats:uncork()
201 end
202 else
203 function cork() end
204 function uncork() end
69 end 205 end
70 206
71 if stats_interval or stats_interval_config == "manual" then 207 if stats_interval or stats_interval_config == "manual" then
72 208
73 local mark_collection_start = measure("times", "stats.collection"); 209 local mark_collection_start = measure("times", "stats.collection");
74 local mark_processing_start = measure("times", "stats.processing"); 210 local mark_processing_start = measure("times", "stats.processing");
75 211
76 function collect() 212 function collect()
77 local mark_collection_done = mark_collection_start(); 213 local mark_collection_done = mark_collection_start();
78 fire_event("stats-update"); 214 fire_event("stats-update");
215 -- ensure that the backend is uncorked, in case it got stuck at
216 -- some point, to avoid infinite resource use
217 uncork()
79 mark_collection_done(); 218 mark_collection_done();
80 219 local manual_result = nil
81 if stats.get_stats then 220
221 if stats.metric_registry then
222 -- only if supported by the backend, we fire the event which
223 -- provides the current metric values
82 local mark_processing_done = mark_processing_start(); 224 local mark_processing_done = mark_processing_start();
83 changed_stats, stats_extra = {}, {}; 225 local metric_registry = stats.metric_registry;
84 for stat_name, getter in pairs(stats.get_stats()) do 226 fire_event("openmetrics-updated", { metric_registry = metric_registry })
85 -- luacheck: ignore 211/type
86 local type, value, extra = getter();
87 local old_value = latest_stats[stat_name];
88 latest_stats[stat_name] = value;
89 if value ~= old_value then
90 changed_stats[stat_name] = value;
91 end
92 if extra then
93 stats_extra[stat_name] = extra;
94 end
95 end
96 fire_event("stats-updated", { stats = latest_stats, changed_stats = changed_stats, stats_extra = stats_extra });
97 mark_processing_done(); 227 mark_processing_done();
98 end 228 manual_result = metric_registry;
99 return stats_interval; 229 end
230
231 return stats_interval, manual_result;
100 end 232 end
101 if stats_interval then 233 if stats_interval then
102 log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval); 234 log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval);
103 timer.add_task(stats_interval, collect); 235 timer.add_task(stats_interval, collect);
104 prosody.events.add_handler("server-started", function () collect() end, -1); 236 prosody.events.add_handler("server-started", function () collect() end, -1);
110 log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name); 242 log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name);
111 end 243 end
112 else 244 else
113 log("debug", "Statistics disabled"); 245 log("debug", "Statistics disabled");
114 function measure() return measure; end 246 function measure() return measure; end
247
248 local dummy_mt = {}
249 function dummy_mt.__newindex()
250 end
251 function dummy_mt:__index()
252 return self
253 end
254 function dummy_mt:__call()
255 return self
256 end
257 local dummy = {}
258 setmetatable(dummy, dummy_mt)
259
260 function metric() return dummy; end
261 function cork() end
262 function uncork() end
115 end 263 end
116 264
117 local exported_collect = nil; 265 local exported_collect = nil;
118 if stats_interval_config == "manual" then 266 if stats_interval_config == "manual" then
119 exported_collect = collect; 267 exported_collect = collect;
120 end 268 end
121 269
122 return { 270 return {
123 collect = exported_collect; 271 collect = exported_collect;
124 measure = measure; 272 measure = measure;
125 get_stats = function () 273 cork = cork;
126 return latest_stats, changed_stats, stats_extra; 274 uncork = uncork;
127 end; 275 metric = metric;
128 get = function (name) 276 get_metric_registry = function ()
129 return latest_stats[name], stats_extra[name]; 277 return stats and stats.metric_registry or nil
130 end; 278 end;
131 }; 279 };