feat(plugin-abi): Step 2.3 — shared Prometheus registry access

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.
ProtocolX
Rene Cannao 1 month ago
parent 55556979e0
commit 398b833aeb

@ -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 =

@ -9,6 +9,10 @@
#include <strings.h>
#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 = &register_query_hook_service;
services_.get_prometheus_registry = &get_prometheus_registry_service;
}
ProxySQL_PluginManager::~ProxySQL_PluginManager() {

@ -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.

@ -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 <memory>
#include <string>
#include <vector>
#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<SQLite3DB*>(&g_fake_admin_db); }
SQLite3DB* proxysql_plugin_get_configdb() { return reinterpret_cast<SQLite3DB*>(&g_fake_config_db); }
SQLite3DB* proxysql_plugin_get_statsdb() { return reinterpret_cast<SQLite3DB*>(&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();
}
Loading…
Cancel
Save