22 KiB
ProxySQL Plugin API
ProxySQL supports dynamically loaded plugins via .so shared libraries. A plugin
can extend ProxySQL with new protocols, admin tables, admin commands, or any
other functionality by registering itself through a well-defined C++ ABI.
Overview
- Plugins are loaded at startup from paths specified in
proxysql.cnf. - Each plugin is a shared library (
.so) that exports a single C entry point. - ProxySQL calls the plugin's lifecycle hooks (
init,start,stop) in order. - During
init, the plugin receives a services struct with callbacks it can use to register tables, commands, access databases, and log messages. - ProxySQL does not know or care what a plugin does — the plugin self-registers everything it needs.
Loading a Plugin
Add the plugin path to proxysql.cnf:
plugins = (
"/path/to/my_plugin.so"
)
Multiple plugins can be listed:
plugins = (
"/path/to/protocol_a.so",
"/path/to/protocol_b.so"
)
The plugins directive is read from the configuration file only. It is not
persisted to the ProxySQL admin database. If the database exists when ProxySQL
starts, other settings are loaded from the database instead of the config file,
but the plugins list is always read from the config file (parsed in an early
startup phase before the database takes precedence).
Startup Sequence
ProxySQL uses a four-phase plugin lifecycle. Every phase but Phase B
is mandatory; Phase B is optional via the register_schemas descriptor
field and only enabled when the plugin declares ABI version 2 or higher.
- Phase A — load. ProxySQL parses
proxysql.cnfand populates thepluginslist. For each plugin path, ProxySQL callsdlopen(), resolves theproxysql_plugin_descriptor_v1symbol, and validates the descriptor (abi_version,name, callback pointers). - Phase B — register_schemas (optional, ABI 2+). If the
descriptor wires
register_schemas, the loader invokes it with aProxySQL_PluginServiceswhoseregister_table/register_command/register_command_alias/register_runtime_view(ABI 3+) entries are LIVE but whose DB-handle getters (get_admindb,get_configdb,get_statsdb) are non-null stubs that returnnullptr. The plugin declares the tables it owns, its admin commands, and any admin-side runtime views it wants the chassis to project from module state; it MUST NOT touch DB handles here. Plugins that leaveregister_schemasnull (or that declare ABI 1) skip this phase entirely and do all their setup in Phase D. - Phase C — admin materialization. The admin module initializes
and materializes the SQLite schemas collected during Phase B
(
merge_plugin_tables+CREATE TABLE). On DDL failure ProxySQL aborts startup. - Phase D — init. The plugin's
init()callback is called, receiving a fully liveProxySQL_PluginServices(DB handles now valid). Plugins that opted out of Phase B register their tables AND commands here; plugins that used Phase B only finish their context setup. - Phase E — start. The plugin's
start()callback is called. The plugin should start its threads, open listener sockets, and load runtime configuration. After this returns, ProxySQL is ready and the plugin is live.
Shutdown Sequence
- The plugin's
stop()callback is called. - The plugin should stop its threads, close sockets, and release resources.
- ProxySQL unloads the
.so.
The Plugin Contract
A plugin must:
- Be compiled as a shared library (
.so) with the same C++17 toolchain as the ProxySQL core. - Export a single
extern "C"function namedproxysql_plugin_descriptor_v1. - Return a pointer to a static
ProxySQL_PluginDescriptorstruct.
ABI Header
All types are defined in include/ProxySQL_Plugin.h:
#include "ProxySQL_Plugin.h"
The Descriptor
struct ProxySQL_PluginDescriptor {
const char *name; // Human-readable plugin name
uint32_t abi_version; // PROXYSQL_PLUGIN_ABI_VERSION (1, 2, or 3)
proxysql_plugin_init_cb init; // bool (*)(ProxySQL_PluginServices *)
proxysql_plugin_start_cb start; // bool (*)()
proxysql_plugin_stop_cb stop; // bool (*)()
proxysql_plugin_status_json_cb status_json; // const char *(*)()
proxysql_plugin_register_schemas_cb register_schemas; // ABI 2+, optional
};
| Field | Type | Description |
|---|---|---|
name |
const char* |
Plugin identifier, used in logging. |
abi_version |
uint32_t |
Set from PROXYSQL_PLUGIN_ABI_VERSION. Value 1 = pre-chassis descriptor (six fields). Value 2 = adds register_schemas (four-phase lifecycle). Value 3 = same descriptor layout as 2; ProxySQL_PluginServices adds a tail-appended register_runtime_view. A v3/v3.1 ProxySQL core rejects abi_version > 1; the current PROXYSQL40 core accepts [1, 3]. |
init |
callback | Phase D — called with live services; register tables and commands here (or finish context setup if register_schemas already did it). |
start |
callback | Phase E — start threads, open sockets, load config. |
stop |
callback | Called on shutdown. Pairs with init, not start: if init returned true and start later failed, stop is still called so the plugin can release resources it allocated in init. |
status_json |
callback | Return a static JSON string describing plugin status. |
register_schemas |
callback | Phase B (ABI 2+). Optional; leave null to skip Phase B entirely. Services passed here have register_table / register_command / register_command_alias / register_runtime_view LIVE but DB-handle getters returning nullptr. |
All callbacks return bool (except status_json which returns const char*).
Return true on success, false on failure. A false return from
register_schemas, init, or start causes ProxySQL to exit.
ABI version
include/ProxySQL_Plugin.h exposes PROXYSQL_PLUGIN_ABI_VERSION (3 under
PROXYSQL40, undefined in pre-chassis builds — the descriptor is then a
legacy six-field struct with abi_version = 1). Plugins MUST assign
abi_version from this macro rather than hard-coding a literal; the
core's loader uses it to detect layout skew and reject plugins built
for an unsupported ABI. ABI 3 keeps the descriptor layout identical to
ABI 2 — the only addition is a tail-appended register_runtime_view
field on ProxySQL_PluginServices — so plugins that compile against
ABI 2 still load on the current core; the trailing services field is
simply invisible to them. See ProxySQL_Plugin.h for the exact rules.
The Entry Point
extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() {
return &my_descriptor;
}
Services Available to Plugins
During init(), the plugin receives a ProxySQL_PluginServices struct
containing function pointers the plugin can call:
struct ProxySQL_PluginServices {
proxysql_plugin_register_table_cb register_table;
proxysql_plugin_register_command_cb register_command;
proxysql_plugin_snapshot_cb get_mysql_users_snapshot;
proxysql_plugin_snapshot_cb get_mysql_servers_snapshot;
proxysql_plugin_snapshot_cb get_mysql_group_replication_hostgroups_snapshot;
proxysql_plugin_log_message_cb log_message;
proxysql_plugin_db_handle_cb get_admindb;
proxysql_plugin_db_handle_cb get_configdb;
proxysql_plugin_db_handle_cb get_statsdb;
// ABI 2 (PROXYSQL40) tail extensions:
proxysql_plugin_register_query_hook_cb register_query_hook;
proxysql_plugin_get_prometheus_registry_cb get_prometheus_registry;
proxysql_plugin_register_command_alias_cb register_command_alias;
// ABI 3 tail extension:
proxysql_plugin_register_runtime_view_cb register_runtime_view;
};
Service Callbacks
register_table
void register_table(const ProxySQL_PluginTableDef &def);
Register a SQLite table in one of ProxySQL's databases. Tables are created
automatically before start() is called.
struct ProxySQL_PluginTableDef {
ProxySQL_PluginDBKind db_kind; // Which database: admin_db, config_db, or stats_db
const char *table_name; // Table name (e.g., "my_plugin_config")
const char *table_def; // CREATE TABLE statement
};
ProxySQL_PluginDBKind values:
| Value | Database | Purpose |
|---|---|---|
admin_db |
In-memory | Runtime/admin tables, queryable via admin interface |
config_db |
On-disk | Persistent configuration (survives restarts) |
stats_db |
In-memory | Statistics/metrics tables |
Convention: For configuration tables that support the standard
memory↔runtime↔disk tier model, register the editable table in both
admin_db and config_db. Register a separate runtime_-prefixed
table in admin_db only — but treat it as an admin-side projection,
not as a tier the plugin maintains: declare it via register_table,
then declare a refresh callback for it via register_runtime_view (ABI
3+). The callback is invoked by the chassis before any admin SELECT
against the table. This mirrors the canonical core pattern (mysql_users
runtime_mysql_users, whereruntime_mysql_usersis repopulated from the in-memoryMySQL_Authenticationstate on demand).
register_command
void register_command(const char *sql, proxysql_plugin_admin_command_cb cb);
Register an admin command handler. When a user issues the given SQL command through the admin interface, ProxySQL calls the registered callback.
ProxySQL_PluginCommandResult my_command(
const ProxySQL_PluginCommandContext &ctx,
const char *sql
);
Important: Command matching is case-insensitive with whitespace normalization.
Only register the canonical form (e.g., "LOAD MYPLUGIN USERS TO RUNTIME").
Alias resolution (e.g., "TO RUN" → "TO RUNTIME") must be handled in
Admin_Handler.cpp in the ProxySQL core — plugins only see the canonical form.
log_message
void log_message(int level, const char *message);
Log a message through ProxySQL's logging system. The numeric levels match
ProxySQL's internal proxy_* severity scheme — anything other than 3 or 4 is
emitted as info:
| Level | Meaning |
|---|---|
| 3 | Error |
| 4 | Warning |
| any other value | Info |
get_admindb, get_configdb, get_statsdb
SQLite3DB *get_admindb();
SQLite3DB *get_configdb();
SQLite3DB *get_statsdb();
Return a pointer to the respective SQLite database. Use these to query and
modify plugin tables at runtime. These are valid only during start() and later
(not during init()).
get_mysql_users_snapshot, get_mysql_servers_snapshot, get_mysql_group_replication_hostgroups_snapshot
SQLite3_result *get_mysql_users_snapshot();
SQLite3_result *get_mysql_servers_snapshot();
SQLite3_result *get_mysql_group_replication_hostgroups_snapshot();
Return a snapshot of ProxySQL's internal MySQL topology state. These allow a plugin to access the current user list, server list, or group replication hostgroups without directly coupling to internal data structures.
register_runtime_view (ABI 3+)
struct ProxySQL_PluginRuntimeView {
const char *table_name;
void (*refresh)(SQLite3DB *admindb, void *opaque);
void *opaque;
};
bool register_runtime_view(const ProxySQL_PluginRuntimeView &view);
Declare an admin-side view of plugin-module state. The named
table_name lives in admin_db (typically runtime_<something>) and
holds no persistent rows — the chassis invokes refresh(admindb, opaque) before any admin SELECT that references the table as a
whole identifier. The match is case-insensitive but identifier-aware:
a query against runtime_<table>_extra (longer suffix) or
stats_runtime_<table> (longer prefix) does NOT trigger the refresh
for runtime_<table>. The refresh callback is expected to do
(typically) `BEGIN; DELETE FROM ; INSERT/REPLACE INTO
The chassis deep-copies table_name so the plugin need not keep the
pointed-to string alive after registration. The callback pointer must
have static lifetime (typically a free function in the plugin .so).
opaque is plugin-owned and passed back unchanged on each invocation;
plugins that don't need it should pass nullptr.
Returns true on successful registration, false if table_name is
already registered (by this or another plugin) or if refresh is
nullptr.
register_runtime_view is live both during register_schemas (Phase B)
and init (Phase D). Plugins typically register views alongside the
editable tables they project. See the separation-of-duties contract
under Admin Integration Patterns below
for why this exists.
Admin Command Context and Result
Context
struct ProxySQL_PluginCommandContext {
SQLite3DB *admindb;
SQLite3DB *configdb;
SQLite3DB *statsdb;
};
Passed to every command callback. Provides direct access to the three databases.
Result
struct ProxySQL_PluginCommandResult {
int error_code; // 0 = success, non-zero = error
uint64_t rows_affected;
std::string message; // Optional message (empty = no message)
};
Return a result from your command callback:
error_code == 0: ProxySQL sends an OK packet to the client.error_code != 0: ProxySQL sends an error packet with the message.
Minimal Plugin Example
This is a complete, minimal plugin that registers one table and one command:
// my_plugin.cpp
#include "ProxySQL_Plugin.h"
#include <cstdio>
namespace {
ProxySQL_PluginServices* g_services = nullptr;
ProxySQL_PluginCommandResult handle_ping(const ProxySQL_PluginCommandContext&, const char*) {
return {0, 0, "pong"};
}
bool my_init(ProxySQL_PluginServices *services) {
g_services = services;
// Register a configuration table
ProxySQL_PluginTableDef table {
ProxySQL_PluginDBKind::admin_db,
"my_plugin_config",
"CREATE TABLE my_plugin_config ("
" key VARCHAR NOT NULL PRIMARY KEY,"
" value VARCHAR NOT NULL DEFAULT ''"
")"
};
services->register_table(table);
// Register an admin command
services->register_command("MYPLUGIN PING", &handle_ping);
return true;
}
bool my_start() {
// Start threads, open sockets, etc.
return true;
}
bool my_stop() {
// Stop threads, close sockets, etc.
return true;
}
const char* my_status_json() {
return "{\"name\":\"my_plugin\",\"state\":\"running\"}";
}
const ProxySQL_PluginDescriptor my_descriptor = {
"my_plugin", // name
1, // abi_version
&my_init, // init
&my_start, // start
&my_stop, // stop
&my_status_json // status_json
};
} // namespace
extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() {
return &my_descriptor;
}
Build Requirements
Compiler Compatibility
Plugins must be compiled with the same C++ compiler and standard library as
the ProxySQL core. This is because ProxySQL_PluginCommandResult contains
std::string, which has an ABI that varies between compilers and standard
library versions. The safest approach is to build plugins within the ProxySQL
build tree.
Example Makefile
CXX = g++
CXXFLAGS = -std=c++17 -shared -fPIC -O2
INCLUDES = -I$(PROXYSQL_SRC)/include
my_plugin.so: my_plugin.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $<
clean:
rm -f my_plugin.so
Build:
make PROXYSQL_SRC=/path/to/proxysql
Linking
Plugins are loaded with RTLD_NOW | RTLD_LOCAL. They should not link against
libproxysql.a or any ProxySQL internal libraries. The plugin communicates
with ProxySQL exclusively through the callbacks in ProxySQL_PluginServices.
If a plugin needs SQLite3DB functionality (querying tables), it accesses it
through the get_admindb() / get_configdb() / get_statsdb() service
callbacks, not by linking against ProxySQL's SQLite wrapper.
Admin Integration Patterns
Separation of duties: Admin, the module, and the runtime view
ProxySQL's three-tier configuration model is, in storage terms:
DISK (config_db) ↔ MEMORY (admin_db editable tables) ↔ RUNTIME (in-module state)
The crucial point is that only the first two are persistent SQLite tables.
"RUNTIME" is the plugin module's in-memory state — typically an object
guarded by its own mutex (e.g. MysqlxConfigStore). The
runtime_<X> table you register in admin_db is not module
storage; it is an admin-side view of module state, projected on
demand.
Therefore the canonical division of work is:
- Admin owns the editable tables (
<X>in bothadmin_dbandconfig_db). - The plugin module owns the runtime state (an in-memory object).
- The
runtime_<X>table inadmin_dbis repopulated by the plugin's refresh callback registered viaservices.register_runtime_view(...).
Concretely:
| Command | What it does |
|---|---|
LOAD <X> TO RUNTIME |
Plugin reads the editable admin_db.<X> and hands rows to its module via a typed install API that swaps state under the module's lock. Does not touch runtime_<X>. |
SAVE <X> [FROM RUNTIME] TO MEMORY |
Plugin dumps its in-memory state and REPLACE INTOs the editable admin_db.<X>. Does not read runtime_<X>. |
LOAD <X> FROM DISK / SAVE <X> TO DISK |
Plain BEGIN/DELETE/INSERT/COMMIT between config_db.<X> and admin_db.<X>. No module involvement. |
SELECT ... FROM runtime_<X> (admin port) |
Chassis fires the registered refresh callback, which wipes runtime_<X> and re-projects the module's current state. |
This mirrors the core's own MySQL_Authentication / runtime_mysql_users
pattern (see lib/ProxySQL_Admin.cpp::save_mysql_users_runtime_to_database).
Disk-tier sync invariant
The disk-tier copies (LOAD/SAVE FROM/TO DISK) are still subject to the
empty-source-must-still-clear-destination rule. Run the
DELETE+INSERT unconditionally inside a single transaction and check
each execute() return; an empty source means "no rows", not "leave the
destination alone". PR #5643 fixed an early implementation that had
this wrong on the disk path.
The runtime path does not need this discipline because the module-side install API is a typed swap, not a copy: replacing the in-memory state with an empty set is a single atomic operation.
Registering Admin Commands
Commands are registered with the canonical form. Aliases (e.g., TO RUN for TO RUNTIME, FROM MEM for FROM MEMORY) are registered by
the plugin via register_command_alias (ABI 2+); the chassis resolves
incoming admin SQL to the canonical form before invoking the plugin's
callback. There is no longer a hardcoded alias ladder in
Admin_Handler.cpp.
Table and view Registration Patterns
// Editable configuration table: visible in both admin and config databases.
void register_config_table(ProxySQL_PluginServices& services,
const char* name, const char* def) {
services.register_table({ProxySQL_PluginDBKind::admin_db, name, def});
services.register_table({ProxySQL_PluginDBKind::config_db, name, def});
}
// Admin-side projection of module state. Declare the empty table in
// admin_db, then wire a refresh callback that reprojects from the
// module before any admin SELECT.
void register_runtime_view_table(ProxySQL_PluginServices& services,
const char* name, const char* def,
void (*refresh)(SQLite3DB*, void*),
void* opaque) {
services.register_table({ProxySQL_PluginDBKind::admin_db, name, def});
services.register_runtime_view({name, refresh, opaque});
}
// Stats table: stats database only.
void register_stats_table(ProxySQL_PluginServices& services,
const char* name, const char* def) {
services.register_table({ProxySQL_PluginDBKind::stats_db, name, def});
}
Limitations
- No hot-loading: Plugins can only be loaded at startup. There is no
LOAD PLUGINcommand to load a plugin at runtime. ProxySQL must be restarted to add or remove plugins. - No dependency resolution: Plugins are loaded in the order listed in
proxysql.cnf. If one plugin depends on another, the dependency must be listed first. - ABI version range: The current core accepts
abi_versionvalues in[1, 3]. Newly built plugins should setabi_version = PROXYSQL_PLUGIN_ABI_VERSION. - Compiler coupling: Plugins must match the ProxySQL core's C++ compiler
and standard library due to
std::stringinProxySQL_PluginCommandResult.
Reference Implementation
The MySQL X Protocol plugin (plugins/mysqlx/) is the reference implementation
of a full ProxySQL plugin. It demonstrates:
- Multi-file plugin structure with separate headers/sources
- Custom
Makefilewithin the ProxySQL build tree - Admin table registration (config + runtime + stats tables)
- Admin command handlers with the three-tier model
- Plugin-owned threads with listener sockets
- TLS integration via ProxySQL's global SSL context
- Connection pooling for backend connections
- A standalone test suite using a custom test harness
Key files:
plugins/mysqlx/src/mysqlx_plugin.cpp— Plugin entry point and lifecycleplugins/mysqlx/src/mysqlx_admin_schema.cpp— Table and command registrationplugins/mysqlx/Makefile— Build configuration