Mercurial > prosody-hg
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 }; |
