From 398b833aebf3c6dbbbc38eaf1fde3316d9415435 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Fri, 17 Apr 2026 12:53:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(plugin-abi):=20Step=202.3=20=E2=80=94=20sh?= =?UTF-8?q?ared=20Prometheus=20registry=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second half of Step 2 from the GenAI plugin carve-out design. Lets plugins register their counters / gauges / histograms against the same prometheus::Registry core uses; their metrics surface alongside core's at the same /metrics endpoint scrapers already poll. ABI extension (include/ProxySQL_Plugin.h): - Forward-declare prometheus::Registry to avoid pulling prometheus-cpp into the plugin ABI header. - proxysql_plugin_get_prometheus_registry_cb typedef. - New field on ProxySQL_PluginServices: get_prometheus_registry, added at the end of the struct (additive -- older plugins stop reading at the previous member). Manager (lib/ProxySQL_PluginManager.cpp): - get_prometheus_registry_service() returns GloVars.prometheus_registry.get(). - Wired into services_ at construction. Lifetime is simpler than initially documented: GloVars (and its prometheus_registry shared_ptr, allocated in its constructor) exists before any plugin is loaded, so the service returns non-null for every callback the plugin will see (init, start, admin command callback, query hook). ABI-header docs updated to reflect this -- plugins MAY register metrics in init() if they want them visible from first scrape. New unit test: plugin_prometheus_unit-t (10 assertions) - after test_init_minimal, GloVars.prometheus_registry is non-null. - A counter registered through prometheus-cpp directly (BuildCounter + Register(*reg)) is visible in the registry's text serialisation by name; the counter's Value() reflects increments through the same prometheus-cpp API plugins will use. - The registry pointer captured BEFORE plugin load equals the pointer observed during init/start AND after stop -- the loader does not swap, replace, or null out the registry; it only installs a service callback that points at it. All 60 unit-test binaries pass. Note: this finishes the Step 2 ABI surface (query hook + Prometheus registry). Step 3 starts moving real GenAI subsystems -- Anomaly_Detector first -- which will be the first consumer of the query hook from inside the plugin and the first plugin to register a metric against the shared Prometheus registry. --- include/ProxySQL_Plugin.h | 27 ++++- lib/ProxySQL_PluginManager.cpp | 9 ++ test/tap/tests/unit/Makefile | 7 ++ .../tests/unit/plugin_prometheus_unit-t.cpp | 110 ++++++++++++++++++ 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 test/tap/tests/unit/plugin_prometheus_unit-t.cpp diff --git a/include/ProxySQL_Plugin.h b/include/ProxySQL_Plugin.h index a044b8ead..7241646ec 100644 --- a/include/ProxySQL_Plugin.h +++ b/include/ProxySQL_Plugin.h @@ -6,6 +6,7 @@ class SQLite3DB; class SQLite3_result; +namespace prometheus { class Registry; } enum class ProxySQL_PluginDBKind : uint8_t { admin_db = 0, @@ -108,6 +109,24 @@ using proxysql_plugin_query_hook_cb = using proxysql_plugin_register_query_hook_cb = bool (*)(ProxySQL_PluginProtocol, proxysql_plugin_query_hook_cb); +// Returns the prometheus::Registry* that core uses for its own metrics. +// Plugins register their counters / gauges / histograms against this +// shared registry using prometheus-cpp directly; their metrics then +// surface at the same /metrics endpoint scrapers already poll. +// +// NOTE: prometheus-cpp is a C++ library with C++ ABI surface. Same +// build-environment caveat applies as to std::string in this header: +// plugins must be compiled in the ProxySQL build tree (or at least +// against a matching prometheus-cpp version + matching libstdc++). +// +// Lifetime: GloVars and its prometheus registry are constructed +// before any plugin is loaded, so the returned pointer is non-null +// for every callback (init, start, admin command callback, query +// hook). Plugins may register metrics in init() if they want them +// scraped immediately. +using proxysql_plugin_get_prometheus_registry_cb = + prometheus::Registry* (*)(); + // Services provided to plugins during init. // register_table/register_command: valid only during the init callback. // get_*db, log_message, snapshots: valid for the plugin's entire lifetime. @@ -121,10 +140,12 @@ struct ProxySQL_PluginServices { proxysql_plugin_db_handle_cb get_admindb; proxysql_plugin_db_handle_cb get_configdb; proxysql_plugin_db_handle_cb get_statsdb; - // Step 2 ABI extension: pre-execution query hook. Older plugins - // that don't know about this field stop reading the struct at the - // previous member; new plugins check for non-null before calling. + // Step 2 ABI extensions. Both fields are additive at the end of + // the struct -- older plugins that were built against the previous + // layout don't read past the previous member; new plugins must + // check non-null before calling. proxysql_plugin_register_query_hook_cb register_query_hook; + proxysql_plugin_get_prometheus_registry_cb get_prometheus_registry; }; using proxysql_plugin_init_cb = diff --git a/lib/ProxySQL_PluginManager.cpp b/lib/ProxySQL_PluginManager.cpp index ad2c4305e..e95c92a56 100644 --- a/lib/ProxySQL_PluginManager.cpp +++ b/lib/ProxySQL_PluginManager.cpp @@ -9,6 +9,10 @@ #include #include "proxysql.h" +#include "proxysql_glovars.hpp" +#include "prometheus/registry.h" + +extern ProxySQL_GlobalVariables GloVars; SQLite3DB* proxysql_plugin_get_admindb(); SQLite3DB* proxysql_plugin_get_configdb(); @@ -111,6 +115,10 @@ SQLite3DB* get_statsdb_service() { return proxysql_plugin_get_statsdb(); } +prometheus::Registry* get_prometheus_registry_service() { + return GloVars.prometheus_registry.get(); +} + void log_message_service(int level, const char* message) { if (message == nullptr) { return; @@ -182,6 +190,7 @@ ProxySQL_PluginManager::ProxySQL_PluginManager() { services_.get_statsdb = &get_statsdb_service; services_.log_message = &log_message_service; services_.register_query_hook = ®ister_query_hook_service; + services_.get_prometheus_registry = &get_prometheus_registry_service; } ProxySQL_PluginManager::~ProxySQL_PluginManager() { diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 68ebc14cd..c116536bd 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -324,6 +324,7 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ plugin_manager_unit-t \ plugin_registry_unit-t \ plugin_query_hook_unit-t \ + plugin_prometheus_unit-t \ test_mysqlx_plugin_load-t \ mysqlx_config_store_unit-t \ test_mysqlx_admin_tables-t \ @@ -547,6 +548,12 @@ plugin_query_hook_unit-t: plugin_query_hook_unit-t.cpp $(FAKE_PLUGIN_SO) $(ODIR) $(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \ $(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@ +plugin_prometheus_unit-t: plugin_prometheus_unit-t.cpp $(FAKE_PLUGIN_SO) $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o $(LIBPROXYSQLAR) + $(CXX) $< $(ODIR)/tap.o $(ODIR)/test_globals.o $(ODIR)/test_init.o \ + -DPROXYSQL_FAKE_PLUGIN_PATH=\"$(FAKE_PLUGIN_SO)\" \ + $(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \ + $(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@ + # Pattern rule: all unit tests use the same compile + link flags. # Each test binary is built from its .cpp source, linked against # the test harness objects and libproxysql.a with all dependencies. diff --git a/test/tap/tests/unit/plugin_prometheus_unit-t.cpp b/test/tap/tests/unit/plugin_prometheus_unit-t.cpp new file mode 100644 index 000000000..f6dba45b3 --- /dev/null +++ b/test/tap/tests/unit/plugin_prometheus_unit-t.cpp @@ -0,0 +1,110 @@ +// Step 2 ABI extension test: shared Prometheus registry access. +// +// What we assert deterministically: +// * GloVars.prometheus_registry is the registry plugins receive via the +// get_prometheus_registry service callback (same pointer); +// * a counter registered against that registry is collected by the +// same registry (i.e. it's a single shared instance, not two); +// * the counter's value reflects increments through the prometheus-cpp +// API the plugins will use. + +#include "ProxySQL_PluginManager.h" +#include "ProxySQL_Plugin.h" +#include "proxysql_glovars.hpp" +#include "tap.h" +#include "test_globals.h" +#include "test_init.h" + +#include "prometheus/registry.h" +#include "prometheus/counter.h" +#include "prometheus/family.h" +#include "prometheus/text_serializer.h" + +#include +#include +#include + +#ifndef PROXYSQL_FAKE_PLUGIN_PATH +#error "PROXYSQL_FAKE_PLUGIN_PATH must be defined" +#endif + +extern ProxySQL_GlobalVariables GloVars; + +namespace { + +char g_fake_admin_db = '\0'; +char g_fake_config_db = '\0'; +char g_fake_stats_db = '\0'; + +bool registry_contains_metric(prometheus::Registry* reg, const std::string& name) { + if (reg == nullptr) return false; + prometheus::TextSerializer ts; + const std::string dump = ts.Serialize(reg->Collect()); + return dump.find(name) != std::string::npos; +} + +} // namespace + +SQLite3DB* proxysql_plugin_get_admindb() { return reinterpret_cast(&g_fake_admin_db); } +SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast(&g_fake_config_db); } +SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast(&g_fake_stats_db); } + +static void test_glovars_registry_exists() { + ok(GloVars.prometheus_registry != nullptr, + "GloVars.prometheus_registry is non-null after test_init_minimal"); +} + +static void test_counter_round_trip_through_shared_registry() { + prometheus::Registry* reg = GloVars.prometheus_registry.get(); + + auto& family = prometheus::BuildCounter() + .Name("proxysql_plugin_unit_test_counter") + .Help("Plugin unit test: counter registered against the shared registry") + .Register(*reg); + auto& counter = family.Add({}); + counter.Increment(); + counter.Increment(); + counter.Increment(); + ok(counter.Value() == 3.0, + "counter value reflects the three increments"); + + ok(registry_contains_metric(reg, "proxysql_plugin_unit_test_counter"), + "metric name appears in the shared registry's text serialisation"); +} + +static void test_loader_does_not_disturb_registry() { + // Loading a plugin must not replace, swap, or null out the registry -- + // just install a service callback that points at it. Verify the + // registry pointer survives the lifecycle. + prometheus::Registry* before = GloVars.prometheus_registry.get(); + ok(before != nullptr, "registry pointer captured before load"); + + ProxySQL_PluginManager mgr; + std::string err; + ok(mgr.load(PROXYSQL_FAKE_PLUGIN_PATH, err), "fake plugin loads"); + ok(mgr.init_all(err), "init_all succeeds"); + ok(mgr.start_all(err), "start_all succeeds"); + + prometheus::Registry* during = GloVars.prometheus_registry.get(); + ok(during == before, + "registry pointer unchanged across plugin load+init+start"); + + ok(mgr.stop_all(), "stop_all succeeds"); + ok(GloVars.prometheus_registry.get() == before, + "registry pointer unchanged after plugin stop"); +} + +int main() { + plan(10); + + // Bring up the minimum core state needed for GloVars.prometheus_registry + // to exist (test_helpers/test_init.cpp allocates the shared_ptr). + test_init_minimal(); + + test_glovars_registry_exists(); + test_counter_round_trip_through_shared_registry(); + test_loader_does_not_disturb_registry(); + + test_cleanup_minimal(); + return exit_status(); +}