doc/PLUGIN_API.md
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.
proxysql.cnf..so) that exports a single C entry point.init, start, stop) in order.init, the plugin receives a services struct with callbacks it can use
to register tables, commands, access databases, and log messages.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).
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.
proxysql.cnf and populates the
plugins list. For each plugin path, ProxySQL calls dlopen(),
resolves the proxysql_plugin_descriptor_v1 symbol, and validates
the descriptor (abi_version, name, callback pointers).register_schemas, the loader invokes it with a
ProxySQL_PluginServices whose register_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 return nullptr. 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 leave register_schemas
null (or that declare ABI 1) skip this phase entirely and do all
their setup in Phase D.merge_plugin_tables + CREATE TABLE). On DDL failure ProxySQL
aborts startup.init() callback is called,
receiving a fully live ProxySQL_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.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.stop() callback is called..so.A plugin must:
.so) with the same C++17 toolchain as the
ProxySQL core.extern "C" function named proxysql_plugin_descriptor_v1.ProxySQL_PluginDescriptor struct.All types are defined in include/ProxySQL_Plugin.h:
#include "ProxySQL_Plugin.h"
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.
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.
extern "C" const ProxySQL_PluginDescriptor *proxysql_plugin_descriptor_v1() {
return &my_descriptor;
}
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;
};
register_tablevoid 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, where runtime_mysql_users is repopulated from
the in-memory MySQL_Authentication state on demand).register_commandvoid 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_messagevoid 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_statsdbSQLite3DB *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_snapshotSQLite3_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 4+)struct ProxySQL_PluginRuntimeView {
const char *table_name;
void (*refresh)(SQLite3DB *db, void *opaque);
void *opaque;
ProxySQL_PluginDBKind db_kind; // ABI 4: at tail, defaults to admin_db (0)
};
bool register_runtime_view(const ProxySQL_PluginRuntimeView &view);
Declare an admin-side view of plugin-module state. The named
table_name typically lives in admin_db (for runtime_<something>)
or stats_db (for stats_<something>), depending on db_kind. The
table holds no persistent rows — the chassis invokes refresh(db, opaque) before any admin SELECT that references the table as a
whole identifier, passing the DB handle matching db_kind. 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 <table>; 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.
struct ProxySQL_PluginCommandContext {
SQLite3DB *admindb;
SQLite3DB *configdb;
SQLite3DB *statsdb;
};
Passed to every command callback. Provides direct access to the three databases.
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.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;
}
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.
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
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.
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:
<X> in both admin_db and config_db).runtime_<X> table in admin_db is repopulated by the plugin's
refresh callback registered via services.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).
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.
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.
// 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});
}
LOAD PLUGIN command to load a plugin at runtime. ProxySQL must be restarted
to add or remove plugins.proxysql.cnf. If one plugin depends on another, the dependency must be
listed first.abi_version values in [1, 3]. Newly built plugins should set abi_version = PROXYSQL_PLUGIN_ABI_VERSION.std::string in ProxySQL_PluginCommandResult.The MySQL X Protocol plugin (plugins/mysqlx/) is the reference implementation
of a full ProxySQL plugin. It demonstrates:
Makefile within the ProxySQL build treeKey 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