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/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 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 new file mode 100644 index 00000000..420ae703 --- /dev/null +++ b/benchmarks/histogram_bench.lua @@ -0,0 +1,195 @@ +local metrics = require '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_001_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_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 + +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_002_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_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 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_004_rust_gather(b) + for _ = 1, b.N do + rs.gather() + end + end + + function M.bench_004_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 + +return M 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.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..2626af48 --- /dev/null +++ b/metrics-rs/Cargo.toml @@ -0,0 +1,19 @@ +[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"] + +[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 new file mode 100644 index 00000000..bf6f7217 --- /dev/null +++ b/metrics-rs/src/histogram_vec.rs @@ -0,0 +1,264 @@ +use std::collections::HashMap; + +use mlua::prelude::*; +use prometheus::core::Collector; +use prometheus::proto; +use serde::{Deserialize, Serialize}; + +use crate::registry; + +#[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)?, + ); + + registry::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(); + + 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)?; + 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("label_pairs", blabels)?; + result.raw_push(lmetric)?; + + if b.get_upper_bound().is_infinite() { + inf_seen = true; + } + } + + 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 blabels = lua.create_table()?; + for pair in labels.iter() { + blabels.set(&pair.0, &pair.1)? + } + blabels.set("le", "+Inf")?; + + lmetric.set("label_pairs", blabels)?; + result.raw_push(lmetric)?; + } + } + } + + Ok(result) + } +} + +impl Drop for LuaHistogramVec { + fn drop(&mut self) { + 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 + ); + } + } +} + +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), + ); + + 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 new file mode 100644 index 00000000..0456bf6f --- /dev/null +++ b/metrics-rs/src/lib.rs @@ -0,0 +1,39 @@ +use mlua::prelude::*; +use std::collections::HashMap; + +mod histogram_vec; +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 { + let opts: LuaHistogramOpts = lua.from_value(opts)?; + LuaHistogramVec::new(opts, names).map_err(LuaError::external) +} + +// gathers all metrics registered in default prometheus::Registry +fn gather(lua: &Lua, (): ()) -> LuaResult { + let mfs = registry::gather(); + + let result = prometheus::TextEncoder::new() + .encode_to_string(&mfs) + .map_err(mlua::Error::external)?; + + 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 { + let r = lua.create_table()?; + + 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..b9008b1f --- /dev/null +++ b/metrics-rs/src/registry.rs @@ -0,0 +1,220 @@ +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. +#[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 d36f1c9a..f69184ef 100644 --- a/metrics/api.lua +++ b/metrics/api.lua @@ -8,6 +8,9 @@ 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 rs = require('metrics.rs') local registry = rawget(_G, '__metrics_registry') if not registry then @@ -86,6 +89,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', @@ -123,6 +135,11 @@ local function set_global_labels(label_pairs) end registry:set_labels(label_pairs) + rs.set_labels(label_pairs) +end + +local function get_global_labels() + return registry:get_labels() end return { @@ -132,6 +149,7 @@ return { counter = counter, gauge = gauge, histogram = histogram, + histogram_vec = histogram_vec, summary = summary, collect = collect, @@ -140,4 +158,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 new file mode 100644 index 00000000..c0e493da --- /dev/null +++ b/metrics/collectors/histogram_vec.lua @@ -0,0 +1,68 @@ +local Shared = require('metrics.collectors.shared') +local HistogramVec = Shared:new_class('histogram', {}) +local rust = require('metrics.rs') + +---@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.inner = rust.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) + 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) + end + 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.inner:remove(label_values) +end + +-- Fast collect +function HistogramVec:collect_str() + return self.inner:collect_str() +end + +-- Slow collect +function HistogramVec:collect() + 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/init.lua b/metrics/init.lua index 9adb6662..f8da6388 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, @@ -27,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..14e3361d 100644 --- a/metrics/plugins/prometheus.lua +++ b/metrics/plugins/prometheus.lua @@ -1,6 +1,8 @@ local metrics = require('metrics') require('checks') +local 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 = 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 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 diff --git a/test/helper.lua b/test/helper.lua index 8371c2bc..af6d52d3 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' +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') 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..e77b317b 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 are set up through test.helper utils.LUA_PATH = nil +utils.LUA_CPATH = nil return utils