You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/doc/PLUGIN_API.md

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.

  1. Phase A — load. ProxySQL parses 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).
  2. Phase B — register_schemas (optional, ABI 2+). If the descriptor wires 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.
  3. 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.
  4. Phase D — init. The plugin's 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.
  5. 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

  1. The plugin's stop() callback is called.
  2. The plugin should stop its threads, close sockets, and release resources.
  3. ProxySQL unloads the .so.

The Plugin Contract

A plugin must:

  1. Be compiled as a shared library (.so) with the same C++17 toolchain as the ProxySQL core.
  2. Export a single extern "C" function named proxysql_plugin_descriptor_v1.
  3. Return a pointer to a static ProxySQL_PluginDescriptor struct.

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, where runtime_mysql_users is repopulated from the in-memory MySQL_Authentication state 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

...; COMMIT;` from the module's own in-memory state.

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 both admin_db and config_db).
  • The plugin module owns the runtime state (an in-memory object).
  • The 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).

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 PLUGIN command 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_version values in [1, 3]. Newly built plugins should set abi_version = PROXYSQL_PLUGIN_ABI_VERSION.
  • Compiler coupling: Plugins must match the ProxySQL core's C++ compiler and standard library due to std::string in ProxySQL_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 Makefile within 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 lifecycle
  • plugins/mysqlx/src/mysqlx_admin_schema.cpp — Table and command registration
  • plugins/mysqlx/Makefile — Build configuration