From 2469ba80c6f359812252bf282f5ba5d5a7017e8e Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Fri, 28 Feb 2025 13:03:13 +0300 Subject: [PATCH 1/9] poc: Rust collectors histogram --- .gitignore | 1 + Makefile | 3 + benchmarks/histogram_bench.lua | 65 +++++ metrics-rs/Cargo.lock | 395 +++++++++++++++++++++++++++ metrics-rs/Cargo.toml | 12 + metrics-rs/src/lib.rs | 218 +++++++++++++++ metrics/api.lua | 11 + metrics/collectors/histogram_vec.lua | 50 ++++ metrics/init.lua | 1 + 9 files changed, 756 insertions(+) create mode 100644 benchmarks/histogram_bench.lua create mode 100644 metrics-rs/Cargo.lock create mode 100644 metrics-rs/Cargo.toml create mode 100644 metrics-rs/src/lib.rs create mode 100644 metrics/collectors/histogram_vec.lua diff --git a/.gitignore b/.gitignore index 21c7b86c..f518071e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ doc/locale/en/ build.luarocks packpack +metrics-rs/target diff --git a/Makefile b/Makefile index 798b783e..eaf70014 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ endif rpm: OS=el DIST=7 packpack/packpack +.rocks/lib/tarantool/metrics_rs.dylib: + cd metrics-rs && cargo build --release && cp target/release/libmetrics_rs.dylib ../$@ + .rocks: metrics-scm-1.rockspec metrics/*.lua metrics/*/*.lua $(TTCTL) rocks make $(TTCTL) rocks install luatest 1.0.1 diff --git a/benchmarks/histogram_bench.lua b/benchmarks/histogram_bench.lua new file mode 100644 index 00000000..38184c28 --- /dev/null +++ b/benchmarks/histogram_bench.lua @@ -0,0 +1,65 @@ +local metrics = require 'override.metrics' +local luahist = assert(metrics.histogram, 'no histogram') +local rusthist = assert(metrics.histogram_vec, 'no histogram_vec') + +local M = {} + +do + local lh = luahist('no_labels', 'histogram') + local rh = rusthist('rust_no_labels', 'histogram') + local random = math.random + + ---@param b luabench.B + function M.bench_no_labels_001_observe(b) + b:run("histogram:observe", function(sb) + for _ = 1, sb.N do lh:observe(random()) end + end) + b:run("histogram_vec:observe", function(sb) + for _ = 1, sb.N do rh:observe(random()) end + end) + end + + ---@param b luabench.B + function M.bench_no_labels_002_collect(b) + b:run("histogram:collect", function(sb) + for _ = 1, sb.N do lh:collect() end + end) + b:run("histogram_vec:collect", function(sb) + for _ = 1, sb.N do rh:collect() end + end) + end +end + +do + local lh = luahist('one_label', 'histogram') + local rh = rusthist('rust_one_label', 'histogram', {'status'}) + local random = math.random + + ---@param b luabench.B + function M.bench_one_label_001_observe(b) + b:run("histogram:observe", function(sb) + for _ = 1, sb.N do + local x = random() + lh:observe(x, {status = x < 0.99 and 'ok' or 'fail'}) + end + end) + b:run("histogram_vec:observe", function(sb) + for _ = 1, sb.N do + local x = random() + rh:observe(x, {status = x < 0.99 and 'ok' or 'fail'}) + end + end) + end + + ---@param b luabench.B + function M.bench_one_label_002_collect(b) + b:run("histogram:collect", function(sb) + for _ = 1, sb.N do lh:collect() end + end) + b:run("histogram_vec:collect", function(sb) + for _ = 1, sb.N do rh:collect() end + end) + end +end + +return M diff --git a/metrics-rs/Cargo.lock b/metrics-rs/Cargo.lock new file mode 100644 index 00000000..cb080129 --- /dev/null +++ b/metrics-rs/Cargo.lock @@ -0,0 +1,395 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" + +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "metrics_rs" +version = "0.1.0" +dependencies = [ + "mlua", + "prometheus", + "serde", +] + +[[package]] +name = "mlua" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b" +dependencies = [ + "bstr", + "either", + "erased-serde", + "mlua-sys", + "mlua_derive", + "num-traits", + "parking_lot", + "rustc-hash", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-sys" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594" +dependencies = [ + "cc", + "cfg-if", + "pkg-config", +] + +[[package]] +name = "mlua_derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870d71c172fcf491c6b5fb4c04160619a2ee3e5a42a1402269c66bcbf1dd4deb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/metrics-rs/Cargo.toml b/metrics-rs/Cargo.toml new file mode 100644 index 00000000..db76e62b --- /dev/null +++ b/metrics-rs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "metrics_rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +mlua = { version = "0.10.3", features = ["luajit", "module", "serialize"] } +prometheus = "0.13.4" +serde = { version = "1.0.218", features = ["derive"] } + +[lib] +crate-type = ["cdylib"] diff --git a/metrics-rs/src/lib.rs b/metrics-rs/src/lib.rs new file mode 100644 index 00000000..349e1253 --- /dev/null +++ b/metrics-rs/src/lib.rs @@ -0,0 +1,218 @@ +use std::collections::HashMap; + +use mlua::prelude::*; +use prometheus::core::Collector; +use prometheus::proto::{self, MetricType}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug)] +pub struct LuaHistogramVec { + pub histogram_vec: prometheus::HistogramVec, + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LuaHistogramOpts { + pub name: String, + pub help: String, + pub buckets: Option>, +} + +fn sample(lua: &Lua, output: LuaTable, + name: &str, + name_postfix: Option<&str>, + mc: &proto::Metric, + additional_label: Option<(&str, &str)>, + global_label: &Option>, + now: f64, + value: f64 +) -> LuaResult { + let mut name = name.to_string(); + if let Some(name_postfix) = name_postfix { + name.push_str(name_postfix); + } + + let label_pairs = lua.create_table_with_capacity(0, + mc.get_label().len() + +global_label.as_ref() + .and_then(|x| Some(x.len())).unwrap_or_default() + )?; + for lp in mc.get_label() { + label_pairs.set(lp.get_name().to_string(), lp.get_value().to_string())?; + } + if let Some(globals) = global_label { + for (k,v) in globals.into_iter() { + label_pairs.set(k.clone(), v.clone())?; + } + } + if let Some(additional_label) = additional_label { + label_pairs.set(additional_label.0.to_string(), additional_label.1.to_string())?; + } + + let rec = lua.create_table_with_capacity(0, 4)?; + rec.set("metric_name", name)?; + rec.set("value", value)?; + rec.set("timestamp", now)?; + rec.set("label_pairs", label_pairs)?; + output.push(rec)?; + + Ok(output) +} + +impl LuaHistogramVec { + pub fn new(opts: LuaHistogramOpts, label_names: LuaTable) -> LuaResult { + let name = opts.name.clone(); + let mut hopts = prometheus::HistogramOpts::new(opts.name, opts.help); + if let Some(buckets) = opts.buckets { + hopts = hopts.buckets(buckets); + } + + let label_names: Vec = label_names.pairs() + .map(|kv| -> LuaResult { + let (_,v):(LuaValue, LuaString) = kv?; + Ok(v.to_string_lossy()) + }) + .collect::>>()?; + + let label_names = label_names.iter().map(|s| &**s).collect::>(); + + let hist = prometheus::HistogramVec::new(hopts, &label_names) + .map_err(mlua::Error::external)?; + + Ok(Self { histogram_vec: hist, name }) + } + + pub fn observe(&self, value: f64, label_values: Option>) -> LuaResult<()> { + if let Some(lv) = label_values { + let label_values = lv.iter().map(|s| &**s).collect::>(); + + self.histogram_vec.get_metric_with_label_values(&label_values) + .map_err(mlua::Error::external)? + .observe(value); + } else { + self.histogram_vec.get_metric_with_label_values(&[]) + .map_err(mlua::Error::external)? + .observe(value); + } + + Ok(()) + } + + pub fn collect(&self, lua: &Lua, now: f64, global_labels: &Option>) -> LuaResult { + let mfs = self.histogram_vec.collect(); + + let mut result = lua.create_table_with_capacity(mfs.len(), 0)?; + + for mf in mfs.iter() { + if mf.get_metric().is_empty() { + continue; + } + if mf.get_name().is_empty() { + continue + } + + if mf.get_field_type() != MetricType::HISTOGRAM { + continue; + } + let metric_name = mf.get_name(); + for m in mf.get_metric() { + let h = m.get_histogram(); + + let mut inf_seen = false; + for b in h.get_bucket() { + let upper_bound = b.get_upper_bound(); + result = sample(lua, result, + metric_name, + Some("_bucket"), + m, + Some(("le", &upper_bound.to_string())), + global_labels, + now, + b.get_cumulative_count() as f64, + )?; + + if upper_bound.is_sign_positive() && upper_bound.is_infinite() { + inf_seen = true; + } + } + + if !inf_seen { + result = sample(lua, result, + metric_name, + Some("_bucket"), + m, + Some(("le", "+Inf")), + global_labels, + now, + h.get_sample_count() as f64, + )?; + } + + result = sample(lua, result, + metric_name, + Some("_sum"), + m, + None, + global_labels, + now, + h.get_sample_sum(), + )?; + + result = sample(lua, result, + metric_name, + Some("_count"), + m, + None, + global_labels, + now, + h.get_sample_count() as f64, + )?; + } + } + + Ok(result) + } +} + +impl LuaUserData for LuaHistogramVec { + fn add_methods>(methods: &mut M) { + let tostring = |lua: &Lua, this: &LuaHistogramVec, ()| -> LuaResult { + let msg = format!("HistogramVec<{}>", this.name); + let s = lua.create_string(msg)?; + Ok(s) + }; + methods.add_meta_method("__serialize", tostring); + methods.add_meta_method("__tostring", tostring); + + methods.add_method("observe", |_, this: &Self, (value, label_values):(f64, Option>)| { + this.observe(value, label_values) + }); + + methods.add_method("collect", |lua: &Lua, this: &Self, (now, global_labels): (f64, Option>)| { + this.collect(lua, now, &global_labels) + }) + } +} + +pub fn new_collectors(lua: &Lua) -> LuaResult { + let r = lua.create_table()?; + + let new_histogram_vec = lua.create_function(|lua: &Lua, (opts, names):(LuaValue, LuaTable)| { + let opts: LuaHistogramOpts = lua.from_value(opts)?; + LuaHistogramVec::new(opts, names) + .map_err(LuaError::external) + })?; + + r.set("new_histogram_vec", new_histogram_vec)?; + Ok(r) +} + +#[mlua::lua_module] +pub fn metrics_rs(lua: &Lua) -> LuaResult { + let r = lua.create_table()?; + + let collectors = new_collectors(lua)?; + r.set("collectors", collectors)?; + + Ok(r) +} diff --git a/metrics/api.lua b/metrics/api.lua index d36f1c9a..d40ca3cf 100644 --- a/metrics/api.lua +++ b/metrics/api.lua @@ -8,6 +8,7 @@ local Counter = require('metrics.collectors.counter') local Gauge = require('metrics.collectors.gauge') local Histogram = require('metrics.collectors.histogram') local Summary = require('metrics.collectors.summary') +local HistogramVec = require('metrics.collectors.histogram_vec') local registry = rawget(_G, '__metrics_registry') if not registry then @@ -86,6 +87,15 @@ local function histogram(name, help, buckets, metainfo) return registry:find_or_create(Histogram, name, help, buckets, metainfo) end +local function histogram_vec(name, help, label_names, buckets, metainfo) + checks('string', 'string', '?table', '?table', '?table') + if buckets ~= nil and not Histogram.check_buckets(buckets) then + error('Invalid value for buckets') + end + + return registry:find_or_create(HistogramVec, name, help, label_names, buckets, metainfo) +end + local function summary(name, help, objectives, params, metainfo) checks('string', '?string', '?table', { age_buckets_count = '?number', @@ -132,6 +142,7 @@ return { counter = counter, gauge = gauge, histogram = histogram, + histogram_vec = histogram_vec, summary = summary, collect = collect, diff --git a/metrics/collectors/histogram_vec.lua b/metrics/collectors/histogram_vec.lua new file mode 100644 index 00000000..d49ddeec --- /dev/null +++ b/metrics/collectors/histogram_vec.lua @@ -0,0 +1,50 @@ +local Shared = require('metrics.collectors.shared') +local collectors = require('metrics_rs').collectors +local fiber = require 'fiber' + +local HistogramVec = Shared:new_class('histogram', {}) + +---@param label_names string[] +function HistogramVec:new(name, help, label_names, buckets, metainfo) + metainfo = table.copy(metainfo) or {} + local obj = Shared.new(self, name, help, metainfo) + + obj._label_names = label_names or {} + obj.histogram_vec = collectors.new_histogram_vec({ + name = name, + help = help, + buckets = buckets, -- can be nil + }, obj._label_names) + + return obj +end + +local function to_values(label_pairs, keys) + local n = #keys + local values = table.new(n, 0) + for i, name in ipairs(keys) do + values[i] = label_pairs[name] + end + return values +end + +function HistogramVec:observe(value, label_pairs) + local label_values + if type(label_pairs) == 'table' then + label_values = to_values(label_pairs, self._label_names) + end + self.histogram_vec:observe(value, label_values) +end + +function HistogramVec:remove(label_pairs) + assert(label_pairs, 'label pairs is a required parameter') + local label_values = to_values(label_pairs, self._label_names) + self.histogram_vec:remove(label_values) +end + +function HistogramVec:collect() + local global_labels = self:append_global_labels({}) + return self.histogram_vec:collect(fiber.time(), global_labels) +end + +return HistogramVec diff --git a/metrics/init.lua b/metrics/init.lua index 9adb6662..425dafae 100644 --- a/metrics/init.lua +++ b/metrics/init.lua @@ -16,6 +16,7 @@ return setmetatable({ counter = api.counter, gauge = api.gauge, histogram = api.histogram, + histogram_vec = api.histogram_vec, summary = api.summary, INF = const.INF, From 8d94cf0422fa7a004b4f1d41fd5e572decb97504 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 16:03:13 +0300 Subject: [PATCH 2/9] perf: optimizations --- metrics-rs/src/histogram_vec.rs | 210 ++++++++++++++++++++++++ metrics-rs/src/lib.rs | 229 ++++----------------------- metrics/api.lua | 5 + metrics/collectors/histogram_vec.lua | 22 ++- metrics/init.lua | 1 + metrics/plugins/prometheus.lua | 26 +-- metrics/registry.lua | 6 + 7 files changed, 288 insertions(+), 211 deletions(-) create mode 100644 metrics-rs/src/histogram_vec.rs diff --git a/metrics-rs/src/histogram_vec.rs b/metrics-rs/src/histogram_vec.rs new file mode 100644 index 00000000..40f2dc91 --- /dev/null +++ b/metrics-rs/src/histogram_vec.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; + +use mlua::prelude::*; +use prometheus::core::Collector; +use prometheus::proto; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct LuaHistogramOpts { + pub name: String, + pub help: String, + pub buckets: Option>, +} + +#[derive(Debug)] +pub struct LuaHistogramVec { + pub histogram_vec: Box, + pub name: String, +} + +impl LuaHistogramVec { + pub fn new(opts: LuaHistogramOpts, label_names: LuaTable) -> LuaResult { + let name = opts.name.clone(); + let mut hopts = prometheus::HistogramOpts::new(opts.name, opts.help); + if let Some(buckets) = opts.buckets { + hopts = hopts.buckets(buckets); + } + + let label_names: Vec = label_names.pairs() + .map(|kv| -> LuaResult { + let (_,v):(LuaValue, LuaString) = kv?; + Ok(v.to_string_lossy()) + }) + .collect::>>()?; + + let label_names = label_names.iter().map(|s| &**s).collect::>(); + + let hist = Box::new(prometheus::HistogramVec::new(hopts, &label_names) + .map_err(mlua::Error::external)? + ); + + prometheus::register(hist.clone()) + .map_err(mlua::Error::external)?; + + Ok(Self { histogram_vec: hist, name }) + } + + pub fn observe(&self, value: f64, label_values: Option>) -> LuaResult<()> { + if let Some(lv) = label_values { + let label_values = lv.iter().map(|s| &**s).collect::>(); + + self.histogram_vec + .get_metric_with_label_values(&label_values) + .map_err(mlua::Error::external)? + .observe(value); + } else { + self.histogram_vec.get_metric_with_label_values(&[]) + .map_err(mlua::Error::external)? + .observe(value); + } + + Ok(()) + } + + pub fn remove(&mut self, lv: Vec) -> LuaResult<()> { + let label_values = lv.iter().map(|s| &**s).collect::>(); + + self.histogram_vec.remove_label_values(&label_values) + .map_err(mlua::Error::external) + } + + pub fn collect_str(&self, lua: &Lua) -> LuaResult { + let mfs = self.histogram_vec.collect(); + + let result = prometheus::TextEncoder::new() + .encode_to_string(&mfs) + .map_err(mlua::Error::external)?; + + lua.create_string(result) + } + + pub fn collect(&self, lua: &Lua, global_values: Option>) -> LuaResult { + let result = lua.create_table()?; + let mfs = self.histogram_vec.collect(); + + let pairs: Option> = global_values.map(|ref hmap| + hmap + .iter() + .map(|(k, v)| { + let mut label = proto::LabelPair::default(); + label.set_name(k.to_string()); + label.set_value(v.to_string()); + label + }) + .collect() + ); + + for mut mf in mfs.into_iter() { + if mf.get_metric().is_empty() { + continue; + } + + let metric_name = mf.get_name(); + let lbucket_name = lua.create_string(metric_name.to_string() + "_bucket")?; + let lsum_name = lua.create_string(metric_name.to_string() + "_sum")?; + let lcount_name = lua.create_string(metric_name.to_string() + "_count")?; + + // append global labels if needed + if let Some(pairs) = &pairs { + for metric in mf.mut_metric().iter_mut() { + let mut labels: Vec<_> = metric.take_label().into(); + labels.append(&mut pairs.clone()); + metric.set_label(labels.into()); + } + } + + for metric in mf.get_metric().iter() { + let time = metric.get_timestamp_ms() * 1000; + + // we convert labels into pair of lua strings + let mut labels: Vec<_> = Vec::with_capacity(metric.get_label().len()); + + for pair in metric.get_label().iter() { + let k = pair.get_name(); + let v = pair.get_value(); + + let k = lua.create_string(&k)?; + let v = lua.create_string(&v)?; + + labels.push((k,v)); + } + + let h = metric.get_histogram(); + + for b in h.get_bucket() { + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("metric_name", &lbucket_name)?; + lmetric.set("value", b.get_cumulative_count())?; + lmetric.set("timestamp", time)?; + + // upper_bound should be used as label + let upper_bound = b.get_upper_bound(); + + let blabels = lua.create_table()?; + for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + blabels.set("le", upper_bound)?; + + lmetric.set("labels", blabels)?; + result.raw_push(lmetric)?; + } + + let blabels = lua.create_table()?; + for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("value", h.get_sample_count())?; + lmetric.set("metric_name", &lcount_name)?; + lmetric.set("timestamp", time)?; + lmetric.set("labels", &blabels)?; // blabels are shared between _count and _sum + result.raw_push(lmetric)?; + + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("metric_name", &lsum_name)?; + lmetric.set("timestamp", time)?; + lmetric.set("value", h.get_sample_sum())?; + lmetric.set("labels", &blabels)?; // blabels are shared between _count and _sum + result.raw_push(lmetric)?; + } + } + + Ok(result) + } +} + +impl Drop for LuaHistogramVec { + fn drop(&mut self) { + let r = prometheus::unregister(self.histogram_vec.clone()); + if let Some(err) = r.err() { + eprintln!("Failed to unregister LuaHistogramVec: {}: {}", self.name, err); + } + } +} + +impl LuaUserData for LuaHistogramVec { + fn add_methods>(methods: &mut M) { + let tostring = |lua: &Lua, this: &LuaHistogramVec, ()| -> LuaResult { + let msg = format!("HistogramVec<{}>", this.name); + let s = lua.create_string(msg)?; + Ok(s) + }; + methods.add_meta_method("__serialize", tostring); + methods.add_meta_method("__tostring", tostring); + + methods.add_method("collect_str", |lua: &Lua, this: &Self, ():()| { + this.collect_str(lua) + }); + + methods.add_method("collect", |lua: &Lua, this: &Self, global_values: Option>| { + this.collect(lua, global_values) + }); + + methods.add_method_mut("remove", |_, this: &mut Self, label_values: _| { + this.remove(label_values) + }); + + methods.add_method("observe", |_, this: &Self, (value, label_values):(_, _)| { + this.observe(value, label_values) + }); + } +} diff --git a/metrics-rs/src/lib.rs b/metrics-rs/src/lib.rs index 349e1253..dcb05726 100644 --- a/metrics-rs/src/lib.rs +++ b/metrics-rs/src/lib.rs @@ -1,218 +1,55 @@ use std::collections::HashMap; - use mlua::prelude::*; -use prometheus::core::Collector; -use prometheus::proto::{self, MetricType}; -use serde::{Deserialize, Serialize}; -#[derive(Debug)] -pub struct LuaHistogramVec { - pub histogram_vec: prometheus::HistogramVec, - pub name: String, -} +mod histogram_vec; +use histogram_vec::{LuaHistogramVec, LuaHistogramOpts}; -#[derive(Debug, Serialize, Deserialize)] -pub struct LuaHistogramOpts { - pub name: String, - pub help: String, - pub buckets: Option>, +// creates new HistogramVec with given label_names +fn new_histogram_vec(lua: &Lua, (opts, names):(LuaValue, LuaTable)) -> LuaResult { + let opts: LuaHistogramOpts = lua.from_value(opts)?; + LuaHistogramVec::new(opts, names) + .map_err(LuaError::external) } -fn sample(lua: &Lua, output: LuaTable, - name: &str, - name_postfix: Option<&str>, - mc: &proto::Metric, - additional_label: Option<(&str, &str)>, - global_label: &Option>, - now: f64, - value: f64 -) -> LuaResult { - let mut name = name.to_string(); - if let Some(name_postfix) = name_postfix { - name.push_str(name_postfix); - } - - let label_pairs = lua.create_table_with_capacity(0, - mc.get_label().len() - +global_label.as_ref() - .and_then(|x| Some(x.len())).unwrap_or_default() - )?; - for lp in mc.get_label() { - label_pairs.set(lp.get_name().to_string(), lp.get_value().to_string())?; - } - if let Some(globals) = global_label { - for (k,v) in globals.into_iter() { - label_pairs.set(k.clone(), v.clone())?; - } - } - if let Some(additional_label) = additional_label { - label_pairs.set(additional_label.0.to_string(), additional_label.1.to_string())?; - } - - let rec = lua.create_table_with_capacity(0, 4)?; - rec.set("metric_name", name)?; - rec.set("value", value)?; - rec.set("timestamp", now)?; - rec.set("label_pairs", label_pairs)?; - output.push(rec)?; - - Ok(output) -} - -impl LuaHistogramVec { - pub fn new(opts: LuaHistogramOpts, label_names: LuaTable) -> LuaResult { - let name = opts.name.clone(); - let mut hopts = prometheus::HistogramOpts::new(opts.name, opts.help); - if let Some(buckets) = opts.buckets { - hopts = hopts.buckets(buckets); - } - - let label_names: Vec = label_names.pairs() - .map(|kv| -> LuaResult { - let (_,v):(LuaValue, LuaString) = kv?; - Ok(v.to_string_lossy()) - }) - .collect::>>()?; - - let label_names = label_names.iter().map(|s| &**s).collect::>(); - - let hist = prometheus::HistogramVec::new(hopts, &label_names) - .map_err(mlua::Error::external)?; - - Ok(Self { histogram_vec: hist, name }) - } - - pub fn observe(&self, value: f64, label_values: Option>) -> LuaResult<()> { - if let Some(lv) = label_values { - let label_values = lv.iter().map(|s| &**s).collect::>(); - - self.histogram_vec.get_metric_with_label_values(&label_values) - .map_err(mlua::Error::external)? - .observe(value); - } else { - self.histogram_vec.get_metric_with_label_values(&[]) - .map_err(mlua::Error::external)? - .observe(value); - } - - Ok(()) - } - - pub fn collect(&self, lua: &Lua, now: f64, global_labels: &Option>) -> LuaResult { - let mfs = self.histogram_vec.collect(); - - let mut result = lua.create_table_with_capacity(mfs.len(), 0)?; - - for mf in mfs.iter() { - if mf.get_metric().is_empty() { - continue; - } - if mf.get_name().is_empty() { - continue - } - - if mf.get_field_type() != MetricType::HISTOGRAM { - continue; - } - let metric_name = mf.get_name(); - for m in mf.get_metric() { - let h = m.get_histogram(); - - let mut inf_seen = false; - for b in h.get_bucket() { - let upper_bound = b.get_upper_bound(); - result = sample(lua, result, - metric_name, - Some("_bucket"), - m, - Some(("le", &upper_bound.to_string())), - global_labels, - now, - b.get_cumulative_count() as f64, - )?; - - if upper_bound.is_sign_positive() && upper_bound.is_infinite() { - inf_seen = true; - } - } - - if !inf_seen { - result = sample(lua, result, - metric_name, - Some("_bucket"), - m, - Some(("le", "+Inf")), - global_labels, - now, - h.get_sample_count() as f64, - )?; - } - - result = sample(lua, result, - metric_name, - Some("_sum"), - m, - None, - global_labels, - now, - h.get_sample_sum(), - )?; +// gathers all metrics registered in default prometheus::Registry +fn gather(lua: &Lua, global_labels: Option>) -> LuaResult { + let mfs = prometheus::gather(); + + // if some global_labels given, add them to all metrics + if let Some(ref hmap) = global_labels { + let pairs: Vec = hmap + .iter() + .map(|(k, v)| { + let mut label = prometheus::proto::LabelPair::default(); + label.set_name(k.to_string()); + label.set_value(v.to_string()); + label + }) + .collect(); - result = sample(lua, result, - metric_name, - Some("_count"), - m, - None, - global_labels, - now, - h.get_sample_count() as f64, - )?; + for mut m in mfs.clone().into_iter() { + for metric in m.mut_metric().iter_mut() { + let mut labels: Vec<_> = metric.take_label().into(); + labels.append(&mut pairs.clone()); + metric.set_label(labels.into()); } } - - Ok(result) } -} - -impl LuaUserData for LuaHistogramVec { - fn add_methods>(methods: &mut M) { - let tostring = |lua: &Lua, this: &LuaHistogramVec, ()| -> LuaResult { - let msg = format!("HistogramVec<{}>", this.name); - let s = lua.create_string(msg)?; - Ok(s) - }; - methods.add_meta_method("__serialize", tostring); - methods.add_meta_method("__tostring", tostring); - methods.add_method("observe", |_, this: &Self, (value, label_values):(f64, Option>)| { - this.observe(value, label_values) - }); + let result = prometheus::TextEncoder::new() + .encode_to_string(&mfs) + .map_err(mlua::Error::external)?; - methods.add_method("collect", |lua: &Lua, this: &Self, (now, global_labels): (f64, Option>)| { - this.collect(lua, now, &global_labels) - }) - } + lua.create_string(result) } -pub fn new_collectors(lua: &Lua) -> LuaResult { - let r = lua.create_table()?; - - let new_histogram_vec = lua.create_function(|lua: &Lua, (opts, names):(LuaValue, LuaTable)| { - let opts: LuaHistogramOpts = lua.from_value(opts)?; - LuaHistogramVec::new(opts, names) - .map_err(LuaError::external) - })?; - - r.set("new_histogram_vec", new_histogram_vec)?; - Ok(r) -} #[mlua::lua_module] pub fn metrics_rs(lua: &Lua) -> LuaResult { let r = lua.create_table()?; - let collectors = new_collectors(lua)?; - r.set("collectors", collectors)?; + r.set("new_histogram_vec", lua.create_function(new_histogram_vec)?)?; + r.set("gather", lua.create_function(gather)?)?; Ok(r) } diff --git a/metrics/api.lua b/metrics/api.lua index d40ca3cf..0933575f 100644 --- a/metrics/api.lua +++ b/metrics/api.lua @@ -135,6 +135,10 @@ local function set_global_labels(label_pairs) registry:set_labels(label_pairs) end +local function get_global_labels() + return registry:get_labels() +end + return { registry = registry, collectors = collectors, @@ -151,4 +155,5 @@ return { unregister_callback = unregister_callback, invoke_callbacks = invoke_callbacks, set_global_labels = set_global_labels, + get_global_labels = get_global_labels, } diff --git a/metrics/collectors/histogram_vec.lua b/metrics/collectors/histogram_vec.lua index d49ddeec..e7b8c665 100644 --- a/metrics/collectors/histogram_vec.lua +++ b/metrics/collectors/histogram_vec.lua @@ -1,6 +1,5 @@ local Shared = require('metrics.collectors.shared') -local collectors = require('metrics_rs').collectors -local fiber = require 'fiber' +local new_histogram_vec = require('metrics_rs').new_histogram_vec local HistogramVec = Shared:new_class('histogram', {}) @@ -10,12 +9,16 @@ function HistogramVec:new(name, help, label_names, buckets, metainfo) local obj = Shared.new(self, name, help, metainfo) obj._label_names = label_names or {} - obj.histogram_vec = collectors.new_histogram_vec({ + obj.inner = new_histogram_vec({ name = name, help = help, buckets = buckets, -- can be nil }, obj._label_names) + obj._observe = obj.inner.observe + obj._collect = obj.inner.collect + obj._collect_str = obj.inner.collect_str + return obj end @@ -33,18 +36,25 @@ function HistogramVec:observe(value, label_pairs) if type(label_pairs) == 'table' then label_values = to_values(label_pairs, self._label_names) end - self.histogram_vec:observe(value, label_values) + -- self._observe(self.inner, value, label_values) + self.inner:observe(value, label_values) end function HistogramVec:remove(label_pairs) assert(label_pairs, 'label pairs is a required parameter') local label_values = to_values(label_pairs, self._label_names) - self.histogram_vec:remove(label_values) + self.inner:remove(label_values) +end + +function HistogramVec:collect_str() + local global_labels = self:append_global_labels({}) + return self.inner:collect_str(global_labels) end +-- Slow collect function HistogramVec:collect() local global_labels = self:append_global_labels({}) - return self.histogram_vec:collect(fiber.time(), global_labels) + return self.inner:collect(global_labels) end return HistogramVec diff --git a/metrics/init.lua b/metrics/init.lua index 425dafae..f8da6388 100644 --- a/metrics/init.lua +++ b/metrics/init.lua @@ -28,6 +28,7 @@ return setmetatable({ unregister_callback = api.unregister_callback, invoke_callbacks = api.invoke_callbacks, set_global_labels = api.set_global_labels, + get_global_labels = api.get_global_labels, enable_default_metrics = tarantool.enable, cfg = cfg.cfg, http_middleware = http_middleware, diff --git a/metrics/plugins/prometheus.lua b/metrics/plugins/prometheus.lua index 97c62c72..2f9e8df6 100644 --- a/metrics/plugins/prometheus.lua +++ b/metrics/plugins/prometheus.lua @@ -1,6 +1,8 @@ local metrics = require('metrics') require('checks') +local metrics_rs = require('metrics_rs') + local prometheus = {} local function escape(str) @@ -52,17 +54,23 @@ local function collect_and_serialize() metrics.invoke_callbacks() local parts = {} for _, c in pairs(metrics.collectors()) do - table.insert(parts, string.format("# HELP %s %s", c.name, c.help)) - table.insert(parts, string.format("# TYPE %s %s", c.name, c.kind)) - for _, obs in ipairs(c:collect()) do - local s = string.format('%s%s %s', - serialize_name(obs.metric_name), - serialize_label_pairs(obs.label_pairs), - serialize_value(obs.value) - ) - table.insert(parts, s) + if not c.collect_str then + table.insert(parts, string.format("# HELP %s %s", c.name, c.help)) + table.insert(parts, string.format("# TYPE %s %s", c.name, c.kind)) + for _, obs in ipairs(c:collect()) do + local s = string.format('%s%s %s', + serialize_name(obs.metric_name), + serialize_label_pairs(obs.label_pairs), + serialize_value(obs.value) + ) + table.insert(parts, s) + end end end + local text = metrics_rs.gather(metrics.get_global_labels()) + if text ~= "" then + table.insert(parts, text) + end return table.concat(parts, '\n') .. '\n' end diff --git a/metrics/registry.lua b/metrics/registry.lua index c36584a0..f339eb62 100644 --- a/metrics/registry.lua +++ b/metrics/registry.lua @@ -12,6 +12,7 @@ function Registry:clear() self.collectors = {} self.callbacks = {} self.label_pairs = {} + collectgarbage('collect') end function Registry:find(kind, name) @@ -40,6 +41,7 @@ end function Registry:unregister(collector) self.collectors[collector.name .. collector.kind] = nil + collectgarbage('collect') end function Registry:invoke_callbacks() @@ -70,4 +72,8 @@ function Registry:set_labels(label_pairs) self.label_pairs = table.copy(label_pairs) end +function Registry:get_labels() + return table.copy(self.label_pairs) +end + return Registry From 34a2848524641171dffa1a3818d2e2c7dc4825b4 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 17:37:13 +0300 Subject: [PATCH 3/9] test: pass LUA_CPATH as well as LUA_PATH --- test/helper.lua | 3 +++ test/tarantool3_helpers/server.lua | 3 +++ test/utils.lua | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/helper.lua b/test/helper.lua index 8371c2bc..f4fea5d5 100644 --- a/test/helper.lua +++ b/test/helper.lua @@ -14,6 +14,9 @@ package.loaded['test.utils'].LUA_PATH = root .. '/?.lua;' .. root .. '/.rocks/share/tarantool/?.lua;' .. root .. '/.rocks/share/tarantool/?/init.lua' +package.loaded['test.utils'].LUA_CPATH = root .. '/.rocks/lib/tarantool/?.so;' .. + root .. '/.rocks/lib/tarantool/?.dylib' + local t = require('luatest') local ok, cartridge_helpers = pcall(require, 'cartridge.test-helpers') if not ok then diff --git a/test/tarantool3_helpers/server.lua b/test/tarantool3_helpers/server.lua index 82dd551a..d14c2a89 100644 --- a/test/tarantool3_helpers/server.lua +++ b/test/tarantool3_helpers/server.lua @@ -185,6 +185,9 @@ function Server:initialize() if self.env['LUA_PATH'] == nil then self.env['LUA_PATH'] = utils.LUA_PATH end + if self.env['LUA_CPATH'] == nil then + self.env['LUA_CPATH'] = utils.LUA_CPATH + end getmetatable(getmetatable(self)).initialize(self) end diff --git a/test/utils.lua b/test/utils.lua index 0f8253e2..c5ec38d9 100644 --- a/test/utils.lua +++ b/test/utils.lua @@ -11,7 +11,8 @@ function utils.create_server(g) g.server = t.Server:new({ alias = 'myserver', env = { - LUA_PATH = utils.LUA_PATH + LUA_PATH = utils.LUA_PATH, + LUA_CPATH = utils.LUA_CPATH, } }) g.server:start{wait_until_ready = true} @@ -116,7 +117,8 @@ function utils.is_tarantool_3_config_supported() end -- Empty by default. Empty LUA_PATH satisfies built-in package tests. --- For tarantool/metrics, LUA_PATH is set up through test.helper +-- For tarantool/metrics, LUA_PATH, LUA_CPATH is set up through test.helper utils.LUA_PATH = nil +utils.LUA_CPATH = nil return utils From 6b2edc030b802eb28f2995ddbc249a3def02c611 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 20:12:44 +0300 Subject: [PATCH 4/9] cmake: build metrics-rs as subdirectory --- CMakeLists.txt | 6 + Makefile | 3 - metrics-rs/CMakeLists.txt | 23 +++ metrics-rs/Cargo.toml | 7 + metrics-rs/src/histogram_vec.rs | 69 ++++++-- metrics-rs/src/lib.rs | 31 +--- metrics-rs/src/registry.rs | 221 +++++++++++++++++++++++++ metrics-scm-1.rockspec | 1 + metrics/api.lua | 3 + metrics/collectors/histogram_vec.lua | 26 ++- metrics/plugins/prometheus.lua | 4 +- metrics/rs.lua | 25 +++ test/collectors/histogram_vec_test.lua | 116 +++++++++++++ 13 files changed, 482 insertions(+), 53 deletions(-) create mode 100644 metrics-rs/CMakeLists.txt create mode 100644 metrics-rs/src/registry.rs create mode 100644 metrics/rs.lua create mode 100644 test/collectors/histogram_vec_test.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 97908658..19eb0dc4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 2.6 FATAL_ERROR) project(metrics NONE) +if(NOT DEFINED TARANTOOL_INSTALL_LIBDIR) + set(TARANTOOL_INSTALL_LIBDIR "${PROJECT_SOURCE_DIR}/.rocks/lib/tarantool") +endif() + +add_subdirectory(metrics-rs) + ## Install #################################################################### ############################################################################### diff --git a/Makefile b/Makefile index eaf70014..798b783e 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,6 @@ endif rpm: OS=el DIST=7 packpack/packpack -.rocks/lib/tarantool/metrics_rs.dylib: - cd metrics-rs && cargo build --release && cp target/release/libmetrics_rs.dylib ../$@ - .rocks: metrics-scm-1.rockspec metrics/*.lua metrics/*/*.lua $(TTCTL) rocks make $(TTCTL) rocks install luatest 1.0.1 diff --git a/metrics-rs/CMakeLists.txt b/metrics-rs/CMakeLists.txt new file mode 100644 index 00000000..e90c69ff --- /dev/null +++ b/metrics-rs/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 2.6 FATAL_ERROR) +project(metrics-rs NONE) + +if (NOT DEFINED CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif() +if (NOT DEFINED TARANTOOL_INSTALL_LIBDIR) + message(SEND_ERROR "TARANTOOL_INSTALL_LIBDIR is not defined") +endif() + +set(RUST_BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/target/${CMAKE_BUILD_TYPE}) + +find_program(CARGO cargo) +add_custom_target(metrics-rs ALL + COMMAND ${CARGO} build --profile "${CMAKE_BUILD_TYPE}" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +install( + FILES ${RUST_BUILD_DIR}/libmetrics_rs${CMAKE_SHARED_LIBRARY_SUFFIX} + RENAME metrics_rs${CMAKE_SHARED_LIBRARY_SUFFIX} + DESTINATION ${TARANTOOL_INSTALL_LIBDIR} +) diff --git a/metrics-rs/Cargo.toml b/metrics-rs/Cargo.toml index db76e62b..2626af48 100644 --- a/metrics-rs/Cargo.toml +++ b/metrics-rs/Cargo.toml @@ -10,3 +10,10 @@ serde = { version = "1.0.218", features = ["derive"] } [lib] crate-type = ["cdylib"] + +[profile.RelWithDebInfo] +inherits = "release" +debug = true + +[profile.Release] +inherits = "release" diff --git a/metrics-rs/src/histogram_vec.rs b/metrics-rs/src/histogram_vec.rs index 40f2dc91..b31c977d 100644 --- a/metrics-rs/src/histogram_vec.rs +++ b/metrics-rs/src/histogram_vec.rs @@ -5,6 +5,8 @@ use prometheus::core::Collector; use prometheus::proto; use serde::{Deserialize, Serialize}; +use crate::registry; + #[derive(Debug, Serialize, Deserialize)] pub struct LuaHistogramOpts { pub name: String, @@ -39,7 +41,7 @@ impl LuaHistogramVec { .map_err(mlua::Error::external)? ); - prometheus::register(hist.clone()) + registry::register(hist.clone()) .map_err(mlua::Error::external)?; Ok(Self { histogram_vec: hist, name }) @@ -132,6 +134,27 @@ impl LuaHistogramVec { let h = metric.get_histogram(); + let blabels = lua.create_table()?; + for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + + // firstly collect count + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("value", h.get_sample_count())?; + lmetric.set("metric_name", &lcount_name)?; + lmetric.set("timestamp", time)?; + lmetric.set("label_pairs", &blabels)?; // blabels are shared between _count and _sum + result.raw_push(lmetric)?; + + // secondly collect sum + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("metric_name", &lsum_name)?; + lmetric.set("timestamp", time)?; + lmetric.set("value", h.get_sample_sum())?; + lmetric.set("label_pairs", &blabels)?; // blabels are shared between _count and _sum + result.raw_push(lmetric)?; + + // lastly collect buckets + let mut inf_seen = false; for b in h.get_bucket() { let lmetric = lua.create_table_with_capacity(0, 4)?; lmetric.set("metric_name", &lbucket_name)?; @@ -145,26 +168,27 @@ impl LuaHistogramVec { for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } blabels.set("le", upper_bound)?; - lmetric.set("labels", blabels)?; + lmetric.set("label_pairs", blabels)?; result.raw_push(lmetric)?; + + if b.get_upper_bound().is_infinite() { + inf_seen = true; + } } - let blabels = lua.create_table()?; - for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + if !inf_seen { + let lmetric = lua.create_table_with_capacity(0, 4)?; + lmetric.set("metric_name", &lbucket_name)?; + lmetric.set("value", h.get_sample_count())?; + lmetric.set("timestamp", time)?; - let lmetric = lua.create_table_with_capacity(0, 4)?; - lmetric.set("value", h.get_sample_count())?; - lmetric.set("metric_name", &lcount_name)?; - lmetric.set("timestamp", time)?; - lmetric.set("labels", &blabels)?; // blabels are shared between _count and _sum - result.raw_push(lmetric)?; + let blabels = lua.create_table()?; + for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + blabels.set("le", "+Inf")?; - let lmetric = lua.create_table_with_capacity(0, 4)?; - lmetric.set("metric_name", &lsum_name)?; - lmetric.set("timestamp", time)?; - lmetric.set("value", h.get_sample_sum())?; - lmetric.set("labels", &blabels)?; // blabels are shared between _count and _sum - result.raw_push(lmetric)?; + lmetric.set("label_pairs", blabels)?; + result.raw_push(lmetric)?; + } } } @@ -174,9 +198,10 @@ impl LuaHistogramVec { impl Drop for LuaHistogramVec { fn drop(&mut self) { - let r = prometheus::unregister(self.histogram_vec.clone()); + let histogram_vec = self.histogram_vec.clone(); + let r = registry::unregister(histogram_vec); if let Some(err) = r.err() { - eprintln!("Failed to unregister LuaHistogramVec: {}: {}", self.name, err); + eprintln!("[ERR] Failed to unregister LuaHistogramVec: {}: {}", self.name, err); } } } @@ -206,5 +231,13 @@ impl LuaUserData for LuaHistogramVec { methods.add_method("observe", |_, this: &Self, (value, label_values):(_, _)| { this.observe(value, label_values) }); + + methods.add_method("unregister", |_, this: &Self, ():()| { + let r = registry::unregister(this.histogram_vec.clone()); + if let Some(err) = r.err() { + return Err(mlua::Error::external(err)); + } + Ok(()) + }) } } diff --git a/metrics-rs/src/lib.rs b/metrics-rs/src/lib.rs index dcb05726..2287f7a0 100644 --- a/metrics-rs/src/lib.rs +++ b/metrics-rs/src/lib.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use mlua::prelude::*; +mod registry; mod histogram_vec; use histogram_vec::{LuaHistogramVec, LuaHistogramOpts}; @@ -12,29 +13,8 @@ fn new_histogram_vec(lua: &Lua, (opts, names):(LuaValue, LuaTable)) -> LuaResult } // gathers all metrics registered in default prometheus::Registry -fn gather(lua: &Lua, global_labels: Option>) -> LuaResult { - let mfs = prometheus::gather(); - - // if some global_labels given, add them to all metrics - if let Some(ref hmap) = global_labels { - let pairs: Vec = hmap - .iter() - .map(|(k, v)| { - let mut label = prometheus::proto::LabelPair::default(); - label.set_name(k.to_string()); - label.set_value(v.to_string()); - label - }) - .collect(); - - for mut m in mfs.clone().into_iter() { - for metric in m.mut_metric().iter_mut() { - let mut labels: Vec<_> = metric.take_label().into(); - labels.append(&mut pairs.clone()); - metric.set_label(labels.into()); - } - } - } +fn gather(lua: &Lua, ():()) -> LuaResult { + let mfs = registry::gather(); let result = prometheus::TextEncoder::new() .encode_to_string(&mfs) @@ -43,6 +23,10 @@ fn gather(lua: &Lua, global_labels: Option>) -> LuaResul lua.create_string(result) } +fn set_labels(_: &Lua, global_labels: HashMap) -> LuaResult<()> { + registry::set_labels(global_labels); + Ok(()) +} #[mlua::lua_module] pub fn metrics_rs(lua: &Lua) -> LuaResult { @@ -50,6 +34,7 @@ pub fn metrics_rs(lua: &Lua) -> LuaResult { r.set("new_histogram_vec", lua.create_function(new_histogram_vec)?)?; r.set("gather", lua.create_function(gather)?)?; + r.set("set_labels", lua.create_function(set_labels)?)?; Ok(r) } diff --git a/metrics-rs/src/registry.rs b/metrics-rs/src/registry.rs new file mode 100644 index 00000000..6f95e078 --- /dev/null +++ b/metrics-rs/src/registry.rs @@ -0,0 +1,221 @@ +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::hash_map::Entry as HEntry; +use std::collections::btree_map::Entry as BEntry; +use prometheus::core::Collector; +use mlua::prelude::*; +use prometheus::proto; + + +/// A struct for registering Prometheus collectors, collecting their metrics, and gathering +/// them into `MetricFamilies` for exposition. +#[derive(Default)] +pub struct Registry { + pub collectors_by_id: HashMap>, + pub dim_hashes_by_name: HashMap, + pub desc_ids: HashSet, + /// Optional common labels for all registered collectors. + pub labels: Option>, +} + +impl Registry { + pub fn set_labels(&mut self, labels: HashMap) { + self.labels = Some(labels); + } + pub fn register(&mut self, c: Box) -> LuaResult<()> { + let mut desc_id_set = HashSet::new(); + let mut collector_id: u64 = 0; + + for desc in c.desc() { + // Is the desc_id unique? + // (In other words: Is the fqName + constLabel combination unique?) + if self.desc_ids.contains(&desc.id) { + return Err(mlua::Error::external(prometheus::Error::AlreadyReg)); + } + + if let Some(hash) = self.dim_hashes_by_name.get(&desc.fq_name) { + if *hash != desc.dim_hash { + return Err(mlua::Error::external(format!( + "a previously registered descriptor with the \ + same fully-qualified name as {:?} has \ + different label names or a different help \ + string", + desc + ))); + } + } + + self.dim_hashes_by_name + .insert(desc.fq_name.clone(), desc.dim_hash); + + // If it is not a duplicate desc in this collector, add it to + // the collector_id. + if desc_id_set.insert(desc.id) { + // The set did not have this value present, true is returned. + collector_id = collector_id.wrapping_add(desc.id); + } else { + // The set did have this value present, false is returned. + // + return Err(mlua::Error::external(format!( + "a duplicate descriptor within the same \ + collector the same fully-qualified name: {:?}", + desc.fq_name + ))); + } + } + + match self.collectors_by_id.entry(collector_id) { + HEntry::Vacant(vc) => { + self.desc_ids.extend(desc_id_set); + vc.insert(c); + Ok(()) + } + HEntry::Occupied(_) => Err(mlua::Error::external(prometheus::Error::AlreadyReg)), + } + } + + pub fn unregister(&mut self, c: Box) -> LuaResult<()> { + let mut id_set = Vec::new(); + let mut collector_id: u64 = 0; + for desc in c.desc() { + if !id_set.iter().any(|id| *id == desc.id) { + id_set.push(desc.id); + collector_id = collector_id.wrapping_add(desc.id); + } + let _ = self.dim_hashes_by_name.remove(&desc.fq_name); + } + + if self.collectors_by_id.remove(&collector_id).is_none() { + return Err(mlua::Error::external(format!( + "collector {:?} is not registered", + c.desc() + ))); + } + + for id in id_set { + self.desc_ids.remove(&id); + } + + // dim_hashes_by_name is left untouched as those must be consistent + // throughout the lifetime of a program. + Ok(()) + } + + pub fn gather(&self) -> Vec { + let mut mf_by_name = BTreeMap::new(); + + for c in self.collectors_by_id.values() { + let mfs = c.collect(); + for mut mf in mfs { + // Prune empty MetricFamilies. + if mf.get_metric().is_empty() { + continue; + } + + let name = mf.get_name().to_owned(); + match mf_by_name.entry(name) { + BEntry::Vacant(entry) => { + entry.insert(mf); + } + BEntry::Occupied(mut entry) => { + let existent_mf = entry.get_mut(); + let existent_metrics = existent_mf.mut_metric(); + + for metric in mf.take_metric().into_iter() { + existent_metrics.push(metric); + } + } + } + } + } + + // Now that MetricFamilies are all set, sort their Metrics + // lexicographically by their label values. + for mf in mf_by_name.values_mut() { + mf.mut_metric().sort_by(|m1, m2| { + let lps1 = m1.get_label(); + let lps2 = m2.get_label(); + + if lps1.len() != lps2.len() { + // This should not happen. The metrics are + // inconsistent. However, we have to deal with the fact, as + // people might use custom collectors or metric family injection + // to create inconsistent metrics. So let's simply compare the + // number of labels in this case. That will still yield + // reproducible sorting. + return lps1.len().cmp(&lps2.len()); + } + + for (lp1, lp2) in lps1.iter().zip(lps2.iter()) { + if lp1.get_value() != lp2.get_value() { + return lp1.get_value().cmp(lp2.get_value()); + } + } + + // We should never arrive here. Multiple metrics with the same + // label set in the same scrape will lead to undefined ingestion + // behavior. However, as above, we have to provide stable sorting + // here, even for inconsistent metrics. So sort equal metrics + // by their timestamp, with missing timestamps (implying "now") + // coming last. + m1.get_timestamp_ms().cmp(&m2.get_timestamp_ms()) + }); + } + + // Write out MetricFamilies sorted by their name. + mf_by_name + .into_values() + .map(|mut m| { + // Add registry common labels, if any. + if let Some(ref hmap) = self.labels { + let pairs: Vec = hmap + .iter() + .map(|(k, v)| { + let mut label = proto::LabelPair::default(); + label.set_name(k.to_string()); + label.set_value(v.to_string()); + label + }) + .collect(); + + for metric in m.mut_metric().iter_mut() { + let mut labels: Vec<_> = metric.take_label().into(); + labels.append(&mut pairs.clone()); + metric.set_label(labels.into()); + } + } + m + }) + .collect() + } +} + +thread_local! { + static DEFAULT_REGISTRY: RefCell = RefCell::new(Registry::default()); +} + +/// Registers a new [`Collector`] to be included in metrics collection. It +/// returns an error if the descriptors provided by the [`Collector`] are invalid or +/// if they - in combination with descriptors of already registered Collectors - +/// do not fulfill the consistency and uniqueness criteria described in the +/// [`Desc`](crate::core::Desc) documentation. +pub fn register(c: Box) -> LuaResult<()> { + DEFAULT_REGISTRY.with_borrow_mut(|r| r.register(c)) +} + +/// Unregisters the [`Collector`] that equals the [`Collector`] passed in as +/// an argument. (Two Collectors are considered equal if their Describe method +/// yields the same set of descriptors.) The function returns an error if a +/// [`Collector`] was not registered. +pub fn unregister(c: Box) -> LuaResult<()> { + DEFAULT_REGISTRY.with_borrow_mut(|r| r.unregister(c)) +} + +/// Return all `MetricFamily` of `DEFAULT_REGISTRY`. +pub fn gather() -> Vec { + DEFAULT_REGISTRY.with_borrow(|r| r.gather()) +} + +pub fn set_labels(labels: HashMap) { + DEFAULT_REGISTRY.with_borrow_mut(|r| r.set_labels(labels)) +} diff --git a/metrics-scm-1.rockspec b/metrics-scm-1.rockspec index e7f6ef7a..8bb30f40 100644 --- a/metrics-scm-1.rockspec +++ b/metrics-scm-1.rockspec @@ -22,6 +22,7 @@ build = { type = 'cmake', variables = { TARANTOOL_INSTALL_LUADIR = '$(LUADIR)', + TARANTOOL_INSTALL_LIBDIR = '$(LIBDIR)', }, } diff --git a/metrics/api.lua b/metrics/api.lua index 0933575f..f69184ef 100644 --- a/metrics/api.lua +++ b/metrics/api.lua @@ -10,6 +10,8 @@ local Histogram = require('metrics.collectors.histogram') local Summary = require('metrics.collectors.summary') local HistogramVec = require('metrics.collectors.histogram_vec') +local rs = require('metrics.rs') + local registry = rawget(_G, '__metrics_registry') if not registry then registry = Registry.new() @@ -133,6 +135,7 @@ local function set_global_labels(label_pairs) end registry:set_labels(label_pairs) + rs.set_labels(label_pairs) end local function get_global_labels() diff --git a/metrics/collectors/histogram_vec.lua b/metrics/collectors/histogram_vec.lua index e7b8c665..8844ccfb 100644 --- a/metrics/collectors/histogram_vec.lua +++ b/metrics/collectors/histogram_vec.lua @@ -1,7 +1,6 @@ local Shared = require('metrics.collectors.shared') -local new_histogram_vec = require('metrics_rs').new_histogram_vec - local HistogramVec = Shared:new_class('histogram', {}) +local rs = require('metrics.rs') ---@param label_names string[] function HistogramVec:new(name, help, label_names, buckets, metainfo) @@ -9,7 +8,7 @@ function HistogramVec:new(name, help, label_names, buckets, metainfo) local obj = Shared.new(self, name, help, metainfo) obj._label_names = label_names or {} - obj.inner = new_histogram_vec({ + obj.inner = rs.new_histogram_vec({ name = name, help = help, buckets = buckets, -- can be nil @@ -32,6 +31,13 @@ local function to_values(label_pairs, keys) end function HistogramVec:observe(value, label_pairs) + if value ~= nil then + value = tonumber(value) + if type(value) ~= 'number' then + error("Histogram observation should be a number") + end + end + local label_values if type(label_pairs) == 'table' then label_values = to_values(label_pairs, self._label_names) @@ -47,14 +53,20 @@ function HistogramVec:remove(label_pairs) end function HistogramVec:collect_str() - local global_labels = self:append_global_labels({}) - return self.inner:collect_str(global_labels) + return self.inner:collect_str() end -- Slow collect function HistogramVec:collect() - local global_labels = self:append_global_labels({}) - return self.inner:collect(global_labels) + return self.inner:collect() +end + +function HistogramVec:unregister() + if self.registry then + self.registry:unregister(self) + end + self.inner:unregister() + self.inner = nil end return HistogramVec diff --git a/metrics/plugins/prometheus.lua b/metrics/plugins/prometheus.lua index 2f9e8df6..14e3361d 100644 --- a/metrics/plugins/prometheus.lua +++ b/metrics/plugins/prometheus.lua @@ -1,7 +1,7 @@ local metrics = require('metrics') require('checks') -local metrics_rs = require('metrics_rs') +local rs = require('metrics.rs') local prometheus = {} @@ -67,7 +67,7 @@ local function collect_and_serialize() end end end - local text = metrics_rs.gather(metrics.get_global_labels()) + local text = rs.gather(metrics.get_global_labels()) if text ~= "" then table.insert(parts, text) end diff --git a/metrics/rs.lua b/metrics/rs.lua new file mode 100644 index 00000000..68d8da98 --- /dev/null +++ b/metrics/rs.lua @@ -0,0 +1,25 @@ +--- Bridge between Lua and Rust +--- We do some conversions here, for speed-ups +local M = {} +local checks = require('checks') +local rust = require('metrics_rs') + +function M.set_labels(label_pairs) + checks('table') + label_pairs = table.copy(label_pairs) + for k, v in pairs(label_pairs) do + label_pairs[k] = tostring(v) + end + rust.set_labels(label_pairs) +end + +function M.gather() + return rust.gather() +end + +function M.new_histogram_vec(opts, label_names) + checks('table', '?table') + return rust.new_histogram_vec(opts, label_names) +end + +return M diff --git a/test/collectors/histogram_vec_test.lua b/test/collectors/histogram_vec_test.lua new file mode 100644 index 00000000..77d01826 --- /dev/null +++ b/test/collectors/histogram_vec_test.lua @@ -0,0 +1,116 @@ +local t = require('luatest') +local g = t.group('histogram_vec') + +local metrics = require('metrics') +local HistogramVec = require('metrics.collectors.histogram_vec') +local utils = require('test.utils') + +g.before_all(utils.create_server) +g.after_all(utils.drop_server) + +g.before_each(metrics.clear) + +g.test_unsorted_buckets_error = function() + t.assert_error_msg_contains('Invalid value for buckets', metrics.histogram_vec, 'latency', 'help', nil, {0.9, 0.5}) +end + +g.test_remove_metric_by_label = function() + local h = HistogramVec:new('hist', 'some histogram', {'label'}, {2, 4}) + + h:observe(3, {label = '1'}) + h:observe(5, {label = '2'}) + + utils.assert_observations(h:collect(), + { + {'hist_count', 1, {label = '1'}}, + {'hist_sum', 3, {label = '1'}}, + {'hist_bucket', 0, {label = '1', le = 2}}, + {'hist_bucket', 1, {label = '1', le = 4}}, + {'hist_bucket', 1, {label = '1', le = '+Inf'}}, + {'hist_count', 1, {label = '2'}}, + {'hist_sum', 5, {label = '2'}}, + {'hist_bucket', 0, {label = '2', le = 4}}, + {'hist_bucket', 0, {label = '2', le = 2}}, + {'hist_bucket', 1, {label = '2', le = '+Inf'}}, + } + ) + + h:remove({label = '1'}) + utils.assert_observations(h:collect(), + { + {'hist_count', 1, {label = '2'}}, + {'hist_sum', 5, {label = '2'}}, + {'hist_bucket', 0, {label = '2', le = 2}}, + {'hist_bucket', 0, {label = '2', le = 4}}, + {'hist_bucket', 1, {label = '2', le = '+Inf'}}, + } + ) +end + +g.test_histogram = function() + t.assert_error_msg_contains("bad argument #1 to histogram_vec (string expected, got nil)", function() + metrics.histogram_vec() + end) + + t.assert_error_msg_contains("bad argument #1 to histogram_vec (string expected, got number)", function() + metrics.histogram_vec(2) + end) + + local h = metrics.histogram_vec('hist', 'some histogram', {'foo'}, {2, 4}) + + h:observe(3, {foo = ''}) + h:observe(5, {foo = ''}) + + local collectors = metrics.collectors() + t.assert_equals(utils.len(collectors), 1, 'histogram seen as only 1 collector') + + local observations = metrics.collect() + local obs_sum = utils.find_obs('hist_sum', {foo=''}, observations) + local obs_count = utils.find_obs('hist_count', {foo=''}, observations) + local obs_bucket_2 = utils.find_obs('hist_bucket', { le = 2, foo='' }, observations) + local obs_bucket_4 = utils.find_obs('hist_bucket', { le = 4, foo='' }, observations) + local obs_bucket_inf = utils.find_obs('hist_bucket', { le = '+Inf', foo='' }, observations) + + t.assert_equals(#observations, 5, '_sum, _count, and _bucket with 3 labelpairs') + t.assert_equals(obs_sum.value, 8, '3 + 5 = 8') + t.assert_equals(obs_count.value, 2, '2 observed values') + t.assert_equals(obs_bucket_2.value, 0, 'bucket 2 has no values') + t.assert_equals(obs_bucket_4.value, 1, 'bucket 4 has 1 value: 3') + t.assert_equals(obs_bucket_inf.value, 2, 'bucket +inf has 2 values: 3, 5') + + h:observe(3, { foo = 'bar' }) + + collectors = metrics.collectors() + t.assert_equals(utils.len(collectors), 1, 'still histogram seen as only 1 collector') + observations = metrics.collect() + obs_sum = utils.find_obs('hist_sum', { foo = 'bar' }, observations) + obs_count = utils.find_obs('hist_count', { foo = 'bar' }, observations) + obs_bucket_2 = utils.find_obs('hist_bucket', { le = 2, foo = 'bar' }, observations) + obs_bucket_4 = utils.find_obs('hist_bucket', { le = 4, foo = 'bar' }, observations) + obs_bucket_inf = utils.find_obs('hist_bucket', { le = '+Inf', foo = 'bar' }, observations) + + t.assert_equals(#observations, 10, '+ _sum, _count, and _bucket with 3 labelpairs') + t.assert_equals(obs_sum.value, 3, '3 = 3') + t.assert_equals(obs_count.value, 1, '1 observed values') + t.assert_equals(obs_bucket_2.value, 0, 'bucket 2 has no values') + t.assert_equals(obs_bucket_4.value, 1, 'bucket 4 has 1 value: 3') + t.assert_equals(obs_bucket_inf.value, 1, 'bucket +inf has 1 value: 3') +end + +g.test_insert_non_number = function() + local h = metrics.histogram_vec('hist', 'some histogram', nil, {2, 4}) + t.assert_error_msg_contains('Histogram observation should be a number', h.observe, h, true) +end + +g.test_metainfo = function() + local metainfo = {my_useful_info = 'here'} + local h = metrics.histogram_vec('hist', 'some histogram', nil, {2, 4}, metainfo) + t.assert_equals(h.metainfo, metainfo) +end + +g.test_metainfo_immutable = function() + local metainfo = {my_useful_info = 'here'} + local h = metrics.histogram_vec('hist', 'some histogram', nil, {2, 4}, metainfo) + metainfo['my_useful_info'] = 'there' + t.assert_equals(h.metainfo, {my_useful_info = 'here'}) +end From 52f4f6b9d26e3415991ce8fea5c7383bdef7f7dd Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 20:18:04 +0300 Subject: [PATCH 5/9] nit: shared lib extension --- test/helper.lua | 4 ++-- test/utils.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/helper.lua b/test/helper.lua index f4fea5d5..af6d52d3 100644 --- a/test/helper.lua +++ b/test/helper.lua @@ -14,8 +14,8 @@ package.loaded['test.utils'].LUA_PATH = root .. '/?.lua;' .. root .. '/.rocks/share/tarantool/?.lua;' .. root .. '/.rocks/share/tarantool/?/init.lua' -package.loaded['test.utils'].LUA_CPATH = root .. '/.rocks/lib/tarantool/?.so;' .. - root .. '/.rocks/lib/tarantool/?.dylib' +local ext = jit.os == 'OSX' and 'dylib' or 'so' +package.loaded['test.utils'].LUA_CPATH = root .. '/.rocks/lib/tarantool/?.'..ext local t = require('luatest') local ok, cartridge_helpers = pcall(require, 'cartridge.test-helpers') diff --git a/test/utils.lua b/test/utils.lua index c5ec38d9..e77b317b 100644 --- a/test/utils.lua +++ b/test/utils.lua @@ -117,7 +117,7 @@ function utils.is_tarantool_3_config_supported() end -- Empty by default. Empty LUA_PATH satisfies built-in package tests. --- For tarantool/metrics, LUA_PATH, LUA_CPATH is set up through test.helper +-- For tarantool/metrics, LUA_PATH, LUA_CPATH are set up through test.helper utils.LUA_PATH = nil utils.LUA_CPATH = nil From 15f32f889a38f515873f9297d740a627f1a61588 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 20:37:38 +0300 Subject: [PATCH 6/9] bench: adds benchmarks into Makefile --- Makefile | 5 ++++ benchmarks/histogram_bench.lua | 36 ++++++++++++++++++++++++---- metrics/collectors/histogram_vec.lua | 10 +++----- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 798b783e..4efe26a7 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,11 @@ update-pot: update-po: sphinx-intl update -p doc/locale/en/ -d doc/locale/ -l "ru" +bench: .rocks + $(TTCTL) rocks install --server https://moonlibs.org luabench 0.3.0 + $(TTCTL) rocks install --server https://luarocks.org argparse 0.7.1 + .rocks/bin/luabench -d 3s + .PHONY: clean clean: rm -rf .rocks diff --git a/benchmarks/histogram_bench.lua b/benchmarks/histogram_bench.lua index 38184c28..9537e79f 100644 --- a/benchmarks/histogram_bench.lua +++ b/benchmarks/histogram_bench.lua @@ -10,7 +10,7 @@ do local random = math.random ---@param b luabench.B - function M.bench_no_labels_001_observe(b) + function M.bench_001_no_labels_001_observe(b) b:run("histogram:observe", function(sb) for _ = 1, sb.N do lh:observe(random()) end end) @@ -20,13 +20,16 @@ do end ---@param b luabench.B - function M.bench_no_labels_002_collect(b) + function M.bench_001_no_labels_002_collect(b) b:run("histogram:collect", function(sb) for _ = 1, sb.N do lh:collect() end end) b:run("histogram_vec:collect", function(sb) for _ = 1, sb.N do rh:collect() end end) + b:run("histogram_vec:collect_str", function(sb) + for _ = 1, sb.N do rh:collect_str() end + end) end end @@ -36,7 +39,7 @@ do local random = math.random ---@param b luabench.B - function M.bench_one_label_001_observe(b) + function M.bench_002_one_label_001_observe(b) b:run("histogram:observe", function(sb) for _ = 1, sb.N do local x = random() @@ -52,13 +55,38 @@ do end ---@param b luabench.B - function M.bench_one_label_002_collect(b) + function M.bench_002_one_label_002_collect(b) b:run("histogram:collect", function(sb) for _ = 1, sb.N do lh:collect() end end) b:run("histogram_vec:collect", function(sb) for _ = 1, sb.N do rh:collect() end end) + b:run("histogram_vec:collect_str", function(sb) + for _ = 1, sb.N do rh:collect_str() end + end) + end +end + +do + local rs = require('override.metrics.rs') + function M.bench_003_rust_gather(b) + for _ = 1, b.N do + rs.gather() + end + end + + function M.bench_003_lua_gather(b) + for _ = 1, b.N do + local result = {} + for _, c in pairs(metrics.registry.collectors) do + if not c.collect_str then + for _, obs in ipairs(c:collect()) do + table.insert(result, obs) + end + end + end + end end end diff --git a/metrics/collectors/histogram_vec.lua b/metrics/collectors/histogram_vec.lua index 8844ccfb..c0e493da 100644 --- a/metrics/collectors/histogram_vec.lua +++ b/metrics/collectors/histogram_vec.lua @@ -1,6 +1,6 @@ local Shared = require('metrics.collectors.shared') local HistogramVec = Shared:new_class('histogram', {}) -local rs = require('metrics.rs') +local rust = require('metrics.rs') ---@param label_names string[] function HistogramVec:new(name, help, label_names, buckets, metainfo) @@ -8,16 +8,12 @@ function HistogramVec:new(name, help, label_names, buckets, metainfo) local obj = Shared.new(self, name, help, metainfo) obj._label_names = label_names or {} - obj.inner = rs.new_histogram_vec({ + obj.inner = rust.new_histogram_vec({ name = name, help = help, buckets = buckets, -- can be nil }, obj._label_names) - obj._observe = obj.inner.observe - obj._collect = obj.inner.collect - obj._collect_str = obj.inner.collect_str - return obj end @@ -42,7 +38,6 @@ function HistogramVec:observe(value, label_pairs) if type(label_pairs) == 'table' then label_values = to_values(label_pairs, self._label_names) end - -- self._observe(self.inner, value, label_values) self.inner:observe(value, label_values) end @@ -52,6 +47,7 @@ function HistogramVec:remove(label_pairs) self.inner:remove(label_values) end +-- Fast collect function HistogramVec:collect_str() return self.inner:collect_str() end From 2e6d57864731ff430c031aaee9fe228629e541e3 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 23:04:09 +0300 Subject: [PATCH 7/9] rust: cargo clippy --- metrics-rs/src/histogram_vec.rs | 79 +++++++++++++++++++++------------ metrics-rs/src/lib.rs | 13 +++--- metrics-rs/src/registry.rs | 39 ++++++++-------- 3 files changed, 75 insertions(+), 56 deletions(-) diff --git a/metrics-rs/src/histogram_vec.rs b/metrics-rs/src/histogram_vec.rs index b31c977d..bf6f7217 100644 --- a/metrics-rs/src/histogram_vec.rs +++ b/metrics-rs/src/histogram_vec.rs @@ -28,23 +28,26 @@ impl LuaHistogramVec { hopts = hopts.buckets(buckets); } - let label_names: Vec = label_names.pairs() + let label_names: Vec = label_names + .pairs() .map(|kv| -> LuaResult { - let (_,v):(LuaValue, LuaString) = kv?; + let (_, v): (LuaValue, LuaString) = kv?; Ok(v.to_string_lossy()) }) .collect::>>()?; let label_names = label_names.iter().map(|s| &**s).collect::>(); - let hist = Box::new(prometheus::HistogramVec::new(hopts, &label_names) - .map_err(mlua::Error::external)? + let hist = Box::new( + prometheus::HistogramVec::new(hopts, &label_names).map_err(mlua::Error::external)?, ); - registry::register(hist.clone()) - .map_err(mlua::Error::external)?; + registry::register(hist.clone()).map_err(mlua::Error::external)?; - Ok(Self { histogram_vec: hist, name }) + Ok(Self { + histogram_vec: hist, + name, + }) } pub fn observe(&self, value: f64, label_values: Option>) -> LuaResult<()> { @@ -56,7 +59,8 @@ impl LuaHistogramVec { .map_err(mlua::Error::external)? .observe(value); } else { - self.histogram_vec.get_metric_with_label_values(&[]) + self.histogram_vec + .get_metric_with_label_values(&[]) .map_err(mlua::Error::external)? .observe(value); } @@ -67,7 +71,8 @@ impl LuaHistogramVec { pub fn remove(&mut self, lv: Vec) -> LuaResult<()> { let label_values = lv.iter().map(|s| &**s).collect::>(); - self.histogram_vec.remove_label_values(&label_values) + self.histogram_vec + .remove_label_values(&label_values) .map_err(mlua::Error::external) } @@ -81,13 +86,16 @@ impl LuaHistogramVec { lua.create_string(result) } - pub fn collect(&self, lua: &Lua, global_values: Option>) -> LuaResult { + pub fn collect( + &self, + lua: &Lua, + global_values: Option>, + ) -> LuaResult { let result = lua.create_table()?; let mfs = self.histogram_vec.collect(); - let pairs: Option> = global_values.map(|ref hmap| - hmap - .iter() + let pairs: Option> = global_values.map(|ref hmap| { + hmap.iter() .map(|(k, v)| { let mut label = proto::LabelPair::default(); label.set_name(k.to_string()); @@ -95,7 +103,7 @@ impl LuaHistogramVec { label }) .collect() - ); + }); for mut mf in mfs.into_iter() { if mf.get_metric().is_empty() { @@ -126,16 +134,18 @@ impl LuaHistogramVec { let k = pair.get_name(); let v = pair.get_value(); - let k = lua.create_string(&k)?; - let v = lua.create_string(&v)?; + let k = lua.create_string(k)?; + let v = lua.create_string(v)?; - labels.push((k,v)); + labels.push((k, v)); } let h = metric.get_histogram(); let blabels = lua.create_table()?; - for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + for pair in labels.iter() { + blabels.set(&pair.0, &pair.1)? + } // firstly collect count let lmetric = lua.create_table_with_capacity(0, 4)?; @@ -165,7 +175,9 @@ impl LuaHistogramVec { let upper_bound = b.get_upper_bound(); let blabels = lua.create_table()?; - for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + for pair in labels.iter() { + blabels.set(&pair.0, &pair.1)? + } blabels.set("le", upper_bound)?; lmetric.set("label_pairs", blabels)?; @@ -183,7 +195,9 @@ impl LuaHistogramVec { lmetric.set("timestamp", time)?; let blabels = lua.create_table()?; - for pair in labels.iter() { blabels.set(&pair.0, &pair.1)? } + for pair in labels.iter() { + blabels.set(&pair.0, &pair.1)? + } blabels.set("le", "+Inf")?; lmetric.set("label_pairs", blabels)?; @@ -201,7 +215,10 @@ impl Drop for LuaHistogramVec { let histogram_vec = self.histogram_vec.clone(); let r = registry::unregister(histogram_vec); if let Some(err) = r.err() { - eprintln!("[ERR] Failed to unregister LuaHistogramVec: {}: {}", self.name, err); + eprintln!( + "[ERR] Failed to unregister LuaHistogramVec: {}: {}", + self.name, err + ); } } } @@ -216,23 +233,27 @@ impl LuaUserData for LuaHistogramVec { methods.add_meta_method("__serialize", tostring); methods.add_meta_method("__tostring", tostring); - methods.add_method("collect_str", |lua: &Lua, this: &Self, ():()| { + methods.add_method("collect_str", |lua: &Lua, this: &Self, (): ()| { this.collect_str(lua) }); - methods.add_method("collect", |lua: &Lua, this: &Self, global_values: Option>| { - this.collect(lua, global_values) - }); + methods.add_method( + "collect", + |lua: &Lua, this: &Self, global_values: Option>| { + this.collect(lua, global_values) + }, + ); methods.add_method_mut("remove", |_, this: &mut Self, label_values: _| { this.remove(label_values) }); - methods.add_method("observe", |_, this: &Self, (value, label_values):(_, _)| { - this.observe(value, label_values) - }); + methods.add_method( + "observe", + |_, this: &Self, (value, label_values): (_, _)| this.observe(value, label_values), + ); - methods.add_method("unregister", |_, this: &Self, ():()| { + methods.add_method("unregister", |_, this: &Self, (): ()| { let r = registry::unregister(this.histogram_vec.clone()); if let Some(err) = r.err() { return Err(mlua::Error::external(err)); diff --git a/metrics-rs/src/lib.rs b/metrics-rs/src/lib.rs index 2287f7a0..0456bf6f 100644 --- a/metrics-rs/src/lib.rs +++ b/metrics-rs/src/lib.rs @@ -1,19 +1,18 @@ -use std::collections::HashMap; use mlua::prelude::*; +use std::collections::HashMap; -mod registry; mod histogram_vec; -use histogram_vec::{LuaHistogramVec, LuaHistogramOpts}; +mod registry; +use histogram_vec::{LuaHistogramOpts, LuaHistogramVec}; // creates new HistogramVec with given label_names -fn new_histogram_vec(lua: &Lua, (opts, names):(LuaValue, LuaTable)) -> LuaResult { +fn new_histogram_vec(lua: &Lua, (opts, names): (LuaValue, LuaTable)) -> LuaResult { let opts: LuaHistogramOpts = lua.from_value(opts)?; - LuaHistogramVec::new(opts, names) - .map_err(LuaError::external) + LuaHistogramVec::new(opts, names).map_err(LuaError::external) } // gathers all metrics registered in default prometheus::Registry -fn gather(lua: &Lua, ():()) -> LuaResult { +fn gather(lua: &Lua, (): ()) -> LuaResult { let mfs = registry::gather(); let result = prometheus::TextEncoder::new() diff --git a/metrics-rs/src/registry.rs b/metrics-rs/src/registry.rs index 6f95e078..b9008b1f 100644 --- a/metrics-rs/src/registry.rs +++ b/metrics-rs/src/registry.rs @@ -1,11 +1,10 @@ -use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::collections::hash_map::Entry as HEntry; -use std::collections::btree_map::Entry as BEntry; -use prometheus::core::Collector; use mlua::prelude::*; +use prometheus::core::Collector; use prometheus::proto; - +use std::cell::RefCell; +use std::collections::btree_map::Entry as BEntry; +use std::collections::hash_map::Entry as HEntry; +use std::collections::{BTreeMap, HashMap, HashSet}; /// A struct for registering Prometheus collectors, collecting their metrics, and gathering /// them into `MetricFamilies` for exposition. @@ -19,11 +18,11 @@ pub struct Registry { } impl Registry { - pub fn set_labels(&mut self, labels: HashMap) { - self.labels = Some(labels); - } - pub fn register(&mut self, c: Box) -> LuaResult<()> { - let mut desc_id_set = HashSet::new(); + pub fn set_labels(&mut self, labels: HashMap) { + self.labels = Some(labels); + } + pub fn register(&mut self, c: Box) -> LuaResult<()> { + let mut desc_id_set = HashSet::new(); let mut collector_id: u64 = 0; for desc in c.desc() { @@ -72,10 +71,10 @@ impl Registry { } HEntry::Occupied(_) => Err(mlua::Error::external(prometheus::Error::AlreadyReg)), } - } + } - pub fn unregister(&mut self, c: Box) -> LuaResult<()> { - let mut id_set = Vec::new(); + pub fn unregister(&mut self, c: Box) -> LuaResult<()> { + let mut id_set = Vec::new(); let mut collector_id: u64 = 0; for desc in c.desc() { if !id_set.iter().any(|id| *id == desc.id) { @@ -99,10 +98,10 @@ impl Registry { // dim_hashes_by_name is left untouched as those must be consistent // throughout the lifetime of a program. Ok(()) - } + } - pub fn gather(&self) -> Vec { - let mut mf_by_name = BTreeMap::new(); + pub fn gather(&self) -> Vec { + let mut mf_by_name = BTreeMap::new(); for c in self.collectors_by_id.values() { let mfs = c.collect(); @@ -187,11 +186,11 @@ impl Registry { m }) .collect() - } + } } thread_local! { - static DEFAULT_REGISTRY: RefCell = RefCell::new(Registry::default()); + static DEFAULT_REGISTRY: RefCell = RefCell::new(Registry::default()); } /// Registers a new [`Collector`] to be included in metrics collection. It @@ -216,6 +215,6 @@ pub fn gather() -> Vec { DEFAULT_REGISTRY.with_borrow(|r| r.gather()) } -pub fn set_labels(labels: HashMap) { +pub fn set_labels(labels: HashMap) { DEFAULT_REGISTRY.with_borrow_mut(|r| r.set_labels(labels)) } From c2ac38d440fdfe8aed84a930fe28b8acc2c3d37d Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Mon, 3 Mar 2025 23:30:33 +0300 Subject: [PATCH 8/9] bench: added benchmarks for 2 and 3 labels for comparision --- benchmarks/histogram_bench.lua | 106 ++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/benchmarks/histogram_bench.lua b/benchmarks/histogram_bench.lua index 9537e79f..d25ade06 100644 --- a/benchmarks/histogram_bench.lua +++ b/benchmarks/histogram_bench.lua @@ -68,15 +68,117 @@ do end end +do + local lh = luahist('two_labels', 'histogram') + local rh = rusthist('rust_two_labels', 'histogram', {'status', 'method'}) + local random = math.random + + local methods = {'GET', 'POST', 'PUT', 'DELETE'} + + ---@param b luabench.B + function M.bench_002_two_labels_001_observe(b) + b:run("histogram:observe", function(sb) + for _ = 1, sb.N do + local x = random() + lh:observe(x, {status = x < 0.99 and 'ok' or 'fail', method = methods[random(1, 4)]}) + end + end) + b:run("histogram_vec:observe", function(sb) + for _ = 1, sb.N do + local x = random() + rh:observe(x, {status = x < 0.99 and 'ok' or 'fail', method = methods[random(1, 4)]}) + end + end) + end + + ---@param b luabench.B + function M.bench_002_two_labels_002_collect(b) + b:run("histogram:collect", function(sb) + for _ = 1, sb.N do lh:collect() end + end) + b:run("histogram_vec:collect", function(sb) + for _ = 1, sb.N do rh:collect() end + end) + b:run("histogram_vec:collect_str", function(sb) + for _ = 1, sb.N do rh:collect_str() end + end) + end +end + +do + local lh = luahist('three_labels', 'histogram') + local rh = rusthist('rust_three_labels', 'histogram', {'status', 'method', 'func'}) + local random = math.random + + local methods = {'GET', 'POST', 'PUT', 'DELETE'} + local funcs = { + 'api.pet.find_by_status', + 'api.pet.find_by_id', + 'api.pet.get', + 'api.pet.find_by_tags', + 'api.pet.post', + 'api.pet.put', + 'api.pet.delete', + 'api.store.inventory.get', + 'api.store.order.post', + 'api.store.order.get', + 'api.store.order.delete', + 'api.user.get', + 'api.user.put', + 'api.user.delete', + 'api.user.post', + 'api.user.login', + 'api.user.logout', + } + local n_funcs = #funcs + + ---@param b luabench.B + function M.bench_003_three_labels_001_observe(b) + b:run("histogram:observe", function(sb) + for _ = 1, sb.N do + local x = random() + lh:observe(x, { + status = x < 0.99 and 'ok' or 'fail', + method = methods[random(1, 4)], + func = funcs[random(1, n_funcs)], + }) + end + end) + b:run("histogram_vec:observe", function(sb) + for _ = 1, sb.N do + local x = random() + rh:observe(x, { + status = x < 0.99 and 'ok' or 'fail', + method = methods[random(1, 4)], + func = funcs[random(1, n_funcs)], + }) + end + end) + end + + ---@param b luabench.B + function M.bench_003_three_labels_002_collect(b) + b:run("histogram:collect", function(sb) + for _ = 1, sb.N do lh:collect() end + end) + b:run("histogram_vec:collect", function(sb) + for _ = 1, sb.N do rh:collect() end + end) + b:run("histogram_vec:collect_str", function(sb) + for _ = 1, sb.N do rh:collect_str() end + end) + end +end + do local rs = require('override.metrics.rs') - function M.bench_003_rust_gather(b) + function M.bench_004_rust_gather(b) for _ = 1, b.N do rs.gather() end end - function M.bench_003_lua_gather(b) + function M.bench_004_lua_gather(b) for _ = 1, b.N do local result = {} for _, c in pairs(metrics.registry.collectors) do From 4f21216fa85b7da213d720c540886dff65c311d5 Mon Sep 17 00:00:00 2001 From: Vladislav Grubov Date: Tue, 4 Mar 2025 11:28:56 +0300 Subject: [PATCH 9/9] Update benchmarks/histogram_bench.lua Co-authored-by: Georgy Moiseev --- benchmarks/histogram_bench.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/histogram_bench.lua b/benchmarks/histogram_bench.lua index d25ade06..420ae703 100644 --- a/benchmarks/histogram_bench.lua +++ b/benchmarks/histogram_bench.lua @@ -1,4 +1,4 @@ -local metrics = require 'override.metrics' +local metrics = require 'metrics' local luahist = assert(metrics.histogram, 'no histogram') local rusthist = assert(metrics.histogram_vec, 'no histogram_vec')