MySQLプラグインの開発方法と実装例

# 1. MySQLプラグインとは

MySQLにはさまざまな種類のプラグインがあり、特定の機能や拡張をMySQLに追加できます。プラグインごとに実装方法や対応できる範囲が異なるため、目的に応じて適したプラグインタイプを選ぶことが必要です。
ここでは主要なプラグインの種類と、その概要や使用例について説明します。

## 1.1. ストレージエンジンタイプ

データの保存方法や管理方法をカスタマイズできるプラグイン。MySQLでデータを格納する物理的な方法を定義します。例えば、InnoDBやMyISAMがストレージエンジンとして有名です。

できることとしては、

- カスタムデータ構造を使用してテーブルのデータを格納
- データ圧縮や暗号化などの追加機能を提供
- 独自のファイルフォーマットでデータを保存する、新しい圧縮技術を利用する、データの暗号化を実装するなど

があります。

ユーザーがストレージエンジンを使用してテーブルを作成したり、データを読み書きするタイミングで実行されるプラグインです。

## 1.2. INFORMATION SCHEMA</caption>

INFORMATION_SCHEMAに新しいテーブルを追加し、MySQL内部のメタデータを取得するために使用します。例えば、サーバーの統計情報やカスタムメトリクスを追加できます。

具体例
- カスタムメトリクスやサーバー統計情報の取得
- クエリやセッションの詳細なデータ収集
- サーバーのパフォーマンス監視、接続数、クエリ状況、ストレージの使用量などを取得し、カスタムテーブルで表示

クエリ実行時にデータが動的に取得され、カスタムテーブルに表示されます。常にバックグラウンドで動作するわけではなく、ユーザーがSELECT文を発行したタイミングで情報を取得し返します。

参考リンク [Writing INFORMATION_SCHEMA Plugins](https://dev.mysql.com/doc/extending-mysql/8.0/en/writing-information-schema-plugins.html)

## 1.3. UDFプラグイン
ユーザーが独自にSQL関数を定義し、その関数をクエリの中で利用できるようにするプラグインです。

特定のビジネスロジックや複雑な計算処理を行うカスタム関数を追加することで、MySQLの標準機能を拡張できます。関数はクエリ内で直接呼び出せ、カスタムロジックをデータベース内で効率的に実行可能です。

具体例
- 数学的複雑計算やデータ分析のためにカスタム数学関数の提供
- 例えば高度な統計計算やデータ変換処理を行う関数の実装

## 1.4. 監査プラグイン
MySQLサーバー上で実行されるクエリやユーザーの操作を監視し、その情報をログとして収集するプラグインです。クエリ実行やデータアクセスの監査や、不正アクセスの検知やデバッグ等に活用されます。

具体例
- クエリログ
- ユーザーのログイン監査
- セキュリティ監視
- サーバー上の不正操作の検出

クエリの実行やログイン、その他の特定のイベントが発生したタイミングでフックされ、情報を収集・記録できます。

参考リンク [Writing Audit Plugins](https://dev.mysql.com/doc/extending-mysql/8.0/en/writing-audit-plugins.html)

## 1.5. フルテキストパーサプラグイン
MySQLの全文検索機能を拡張し、独自のテキストパーサを追加するプラグインです。フルテキスト検索のパーサをカスタマイズして、特定の言語や検索ロジックに対応したりできます。

例: 日本語や特殊なトークンを含む言語に対応するカスタムパーサの作成等
`CREATE FULLTEXT INDEX ... WITH PARSER CustomParser;`

参考リンク [Writing Full-Text Parser Plugins](https://dev.mysql.com/doc/extending-mysql/8.0/en/writing-full-text-plugins.html)


## 1.6. デーモンプラグイン
MySQLサーバーのバックグラウンドで動作し続けるプラグインです。サーバーが稼働中に定期的なタスクを実行したり、外部サービスと連携できます。

具体例
- ログ収集デーモンや外部システムとの連携タスク
- 定期的にサーバー状態をモニタリングし、外部システムにデータを送信

サーバー起動時に初期化され、サーバーが停止するまでバックグラウンドで動作します。

参考リンク [Writing Daemon Plugins](https://dev.mysql.com/doc/extending-mysql/8.0/en/writing-daemon-plugins.html)

## 1.7. 認証プラグイン
MySQLの認証方式を拡張するプラグインです。カスタム認証方式を提供し、LDAPやOAuthなどの外部システムとの認証を連携します。

具体例	
- LDAPやOAuthベースのカスタム認証をMySQLに追加
- ユーザーの認証を外部システム（例: LDAP）に委任

# 2. MySQLプラグインの作成方法

## 2.1. 環境

この例ではCentOS 7を利用しています。使用するツール等のバージョンは以下のとおりです。
必要に応じてインストールしてください。

```bash
$ cat /etc/os-release 
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

$ g++ --version
g++ (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ cmake --version
cmake version 3.22.1

CMake suite maintained and supported by Kitware (kitware.com/cmake).

$ mysql --version
mysql  Ver 8.0.36 for Linux on x86_64 (MySQL Community Server - GPL)
```

## 2.2. MySQLのビルド

プラグイン開発で利用するために、MySQL自体のソースコードをビルドする必要があります。

```bash
$ tree -a 
mysql-plugin
├── mysql-8.0.35
│   ├── build
│   ├── CMakeLists.txt
│   └── ...
└── mysql-boost-8.0.35.tar.gz
```
```bash
# ソースコードのダウンロードと解凍
wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.35.tar.gz
tar xvf mysql-boost-8.0.35.tar.gz
cd mysql-8.0.35

# ビルド実行
mkdir build && cd build
cmake ..
make
```

## 2.3. プラグイン作成
### 2.3.1. 一番シンプルなプラグイン

ここではプラグインに機能を追加するためのベースとなるシンプルな例と、開発からインストールまでの流れを説明します。
サンプルプラグインのコードは以下の通りです。インストールされても何もしない一番シンプルなプラグインです。

```bash
$ tree -a 
mysql-plugin
├── mysql-8.0.35
│   ├── build
│   ├── CMakeLists.txt
│   └── ...
├── mysql-boost-8.0.35.tar.gz
└── test_plugin     <- 今回作るプラグイン
     ├── build
     ├── CMakeLists.txt
     └── test_plugin.cc
```
```c++:(root)/test_plugin/test_plugin.cc

#include <mysql/plugin.h>
#include <sql/sql_plugin.h>

// プラグインの初期化時に呼ばれる関数
static int test_plugin_init(void *p) {
    return 0;
}

// プラグインの終了時に呼ばれる関数
static int test_plugin_deinit(void *p) {
    return 0;
}

// MySQLサーバにプラグインの情報を提供する構造体
struct st_mysql_daemon test_plugin = {MYSQL_DAEMON_INTERFACE_VERSION};

// MySQLプラグインの定義
mysql_declare_plugin(test_plugin) {
    MYSQL_DAEMON_PLUGIN,    // プラグインの種類
    &test_plugin,           // プラグインのデータ構造へのポインタ
    "test_plugin",          // プラグイン名
    "Your Name",       // 作成者
    "Test Plugin",          // 説明
    PLUGIN_LICENSE_GPL,     // ライセンス
    test_plugin_init,       // 初期化時に呼ばれる関数
    nullptr,                // check関数のポインタ
    test_plugin_deinit,     // アンロード時に呼ばれる関数
    0x0100,                 // バージョン (16進数)
    nullptr,                // status変数
    nullptr,                // MTR関連の変数
    nullptr,                // プラグイン固有のパラメータ
    0,                      // フラグ
} mysql_declare_plugin_end;
```
:::note warn
CMakeLists.txt内のパスが違っている場合は適宜読み替えてください。


:::
```cmake:(root)/test_plugin/CMakeLists.txt

CMakeLists.txt内のパスが違っている場合は適宜読み替えてください。

CMAKE_MINIMUM_REQUIRED(VERSION 3.0)
PROJECT(test_plugin)

# コンパイラの設定
set(CMAKE_C_COMPILER /opt/rh/devtoolset-11/root/usr/bin/gcc)
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-11/root/usr/bin/g++)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# パスの設定
SET(MYSQL_SOURCE_DIR "/home/$USER/mysql-plugin/mysql-8.0.35")
SET(MYSQL_BUILD_DIR "/home/$USER/mysql-plugin/mysql-8.0.35/build")

INCLUDE_DIRECTORIES(
  ${MYSQL_SOURCE_DIR}
  ${MYSQL_SOURCE_DIR}/include
  ${MYSQL_SOURCE_DIR}/sql
  ${MYSQL_BUILD_DIR}
  ${MYSQL_BUILD_DIR}/include
)

# MySQLプラグインの設定
add_compile_options(-DMYSQL_DYNAMIC_PLUGIN)
ADD_LIBRARY(test_plugin MODULE test_plugin.cc)
TARGET_LINK_LIBRARIES(test_plugin
  /usr/lib64/mysql/libmysqlclient.so
  ${MYSQL_BUILD_DIR}/libservices/libmysqlservices.a
)
SET_TARGET_PROPERTIES(test_plugin PROPERTIES
    PREFIX ""
    COMPILE_FLAGS "-DMYSQL_DYNAMIC_PLUGIN"
)

# MySQLのプラグインディレクトリに配置する
SET(INSTALL_PLUGIN_DIR "/usr/lib64/mysql/plugin")
INSTALL(TARGETS test_plugin LIBRARY DESTINATION ${INSTALL_PLUGIN_DIR})
```

## 2.4. ビルドとインストール

作成したプラグインのビルドと、プラグインディレクトリへのコピーを行います。

```bash
cd test_plugin
mkdir build && cd build
cmake ..
make
sudo make install
```

ビルドが正常に完了したらMySQLにインストールし、ログインします。
このプラグインはインストールされても何もしないので、information_schemaでプラグインがACTIVEになっていることを確認します。

```sql
mysql> INSTALL PLUGIN test_plugin SONAME 'test_plugin.so';
mysql> SELECT * FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'test_plugin'\G
*************************** 1. row ***************************
           PLUGIN_NAME: test_plugin
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: DAEMON
   PLUGIN_TYPE_VERSION: 80035.0
        PLUGIN_LIBRARY: test_plugin.so
PLUGIN_LIBRARY_VERSION: 1.11
         PLUGIN_AUTHOR: Your Name
    PLUGIN_DESCRIPTION: Test Plugin
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
1 row in set (0.00 sec)
```

確認後にアンインストールするには以下のクエリ。

```sql
mysql> UNINSTALL PLUGIN test_plugin;
Query OK, 0 rows affected (0.00 sec)
```

# 3. 監査プラグインの実装例

ここでは実際にプラグインを活用する例として、監査プラグインのサンプルコードを示します。
プラグインの動作内容としては、MySQLへの接続とクエリの実行をログファイルへ記録します。

```bash
$ tree -a 
mysql-plugin
├── mysql-8.0.35
│   ├── build
│   ├── CMakeLists.txt
│   └── ...
├── mysql-boost-8.0.35.tar.gz
│── test_audit_plugin     <- 新たに作成
│   ├── build
│   ├── CMakeLists.txt
│   └── test_audit_plugin.cc
└── test_plugin
     ├── build
     ├── CMakeLists.txt
     └── test_plugin.cc
```

```c++
#include <stdio.h>
#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>

// 接続イベントの処理を行う関数
static void handle_connection_event(const struct mysql_event_connection *connection_event) {
    // ユーザー名とホスト名を取得
    const char *user = connection_event->user.str;
    const char *host = connection_event->host.str;

    const char *msg = NULL;
    // 接続イベント
    if (connection_event->event_subclass == MYSQL_AUDIT_CONNECTION_CONNECT) {
        msg = "connected";
    // 切断イベント
    } else if (connection_event->event_subclass == MYSQL_AUDIT_CONNECTION_DISCONNECT) {
        msg = "disconnected";
    } else {
        return;
    }

    // ログに記録
    FILE *log_file = fopen("/var/log/mysql/mysql-audit.log", "a");
    if (log_file != NULL) {
        fprintf(log_file, "User '%s' %s from '%s'\n", user, msg, host);
        fclose(log_file);
    }
}

// クエリエベントの処理を行う関数
static void handle_query_event(const struct mysql_event_query *query_event) {
    // 開始時のイベントのみ扱う
    if (query_event->event_subclass != MYSQL_AUDIT_QUERY_START) return;

    // ユーザー名とホスト名を取得
    const char *query_str = query_event->query.str;
    const int query_status = query_event->status;
    size_t query_len = query_event->query.length;

    // ログに記録
    FILE *log_file = fopen("/var/log/mysql/mysql-audit.log", "a");
    if (log_file != NULL) {
        fprintf(log_file, "Query executed: %.*s, status %d\n", (int)query_len, query_str, query_status);
        fclose(log_file);
    }
}

// 監査イベントが発生したときに呼ばれるコールバック関数
static int audit_notify(MYSQL_THD thd, mysql_event_class_t event_class, const void *event) {
    switch (event_class) {
        case MYSQL_AUDIT_CONNECTION_CLASS:
            handle_connection_event((const struct mysql_event_connection *)event);
            break;
        case MYSQL_AUDIT_QUERY_CLASS:
            handle_query_event((const struct mysql_event_query *)event);
            break;
    }
    return 0;
}

// ここでどのイベント対象とするか指定します。
// この例だとユーザ接続とクエリ実行を対象としています。
static struct st_mysql_audit test_audit_plugin_descriptor = {
    MYSQL_AUDIT_INTERFACE_VERSION,
    NULL,
    audit_notify,
        {
        (unsigned long) 0,  /* MYSQL_AUDIT_GENERAL_CLASS */
        (unsigned long) ~0, /* MYSQL_AUDIT_CONNECTION_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_PARSE_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_AUTHORIZATION_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_TABLE_ACCESS_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_GLOBAL_VARIABLE_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_SERVER_STARTUP_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_SERVER_SHUTDOWN_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_COMMAND_CLASS */
        (unsigned long) ~0, /* MYSQL_AUDIT_QUERY_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_STORED_PROGRAM_CLASS */
        (unsigned long) 0,  /* MYSQL_AUDIT_AUTHENTICATION_CLASS */
        (unsigned long) 0   /* MYSQL_AUDIT_MESSAGE_CLASS */
    }
};

mysql_declare_plugin(test_audit_plugin) {
    MYSQL_AUDIT_PLUGIN,
    &test_audit_plugin_descriptor,
    "test_audit_plugin",
    "Your Name",
    "Test Audit Plugin",
    PLUGIN_LICENSE_GPL,
    NULL,
    NULL,
    NULL,
    0x0100,
    NULL,
    NULL,
    NULL,
    0,
} mysql_declare_plugin_end;
```

:::note info
CMakeLists.txtは内容的に「2.3.1 一番シンプルなプラグイン」と同様です。
:::

```cmake
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)
PROJECT(test_plugin)

# コンパイラの設定
set(CMAKE_C_COMPILER /opt/rh/devtoolset-11/root/usr/bin/gcc)
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-11/root/usr/bin/g++)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# パスの設定
SET(MYSQL_SOURCE_DIR "/home/$USER/mysql-plugin/mysql-8.0.35")
SET(MYSQL_BUILD_DIR "/home/$USER/mysql-plugin/mysql-8.0.35/build")

INCLUDE_DIRECTORIES(
  ${MYSQL_SOURCE_DIR}
  ${MYSQL_SOURCE_DIR}/include
  ${MYSQL_SOURCE_DIR}/sql
  ${MYSQL_BUILD_DIR}
  ${MYSQL_BUILD_DIR}/include
)

# MySQLプラグインの設定
add_compile_options(-DMYSQL_DYNAMIC_PLUGIN)
ADD_LIBRARY(test_audit_plugin MODULE test_audit_plugin.cc)
TARGET_LINK_LIBRARIES(test_audit_plugin
  /usr/lib64/mysql/libmysqlclient.so
  ${MYSQL_BUILD_DIR}/libservices/libmysqlservices.a
)
SET_TARGET_PROPERTIES(test_audit_plugin PROPERTIES
    PREFIX ""
    COMPILE_FLAGS "-DMYSQL_DYNAMIC_PLUGIN"
)

# MySQLのプラグインディレクトリに配置する
SET(INSTALL_PLUGIN_DIR "/usr/lib64/mysql/plugin")
INSTALL(TARGETS test_plugin LIBRARY DESTINATION ${INSTALL_PLUGIN_DIR})
```

2.4の手順でビルドしてインストールします。

```bash
// mysqldがアクセスできるディレクトリをあらかじめ作成しておきます。
sudo mkdir /var/log/mysql
sudo chown mysql:mysql /var/log/mysql
sudo chmod 750 /var/log/mysql

// ビルド
cd test_audit_plugin
mkdir build && cd build
cmake ..
make
sudo make install

// インストール
mysql> INSTALL PLUGIN test_audit_plugin SONAME 'test_audit_plugin.so';
mysql> SELECT * FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'test_audit_plugin'\G
*************************** 1. row ***************************
           PLUGIN_NAME: test_audit_plugin
        PLUGIN_VERSION: 1.0
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: AUDIT
   PLUGIN_TYPE_VERSION: 4.1
        PLUGIN_LIBRARY: test_audit_plugin.so
PLUGIN_LIBRARY_VERSION: 1.11
         PLUGIN_AUTHOR: Your Name
    PLUGIN_DESCRIPTION: Test Audit Plugin
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
1 row in set (0.00 sec)
```

MySQLにログインしてクエリを打ってみると、/var/log/mysql/mysql-audit.logにログが記録されていることがわかります。

```bash
$ sudo cat /var/log/mysql/mysql-audit.log
User 'testuser' connected from 'localhost'
Query executed: select @@version_comment limit 1, status 0
Query executed: select 1, status 0
Query executed: SELECT * FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'test_audit_plugin', status 0
User 'testuser' disconnected from 'localhost'
```

監査プラグインはイベント情報を用いることで、任意のフォーマットでログを記録できます。
今回サンプルで利用した接続イベントとクエリイベントで利用できる情報は、以下のドキュメントから確認できます。

- [mysql_event_connection Struct Reference](https://dev.mysql.com/doc/dev/mysql-server/latest/structmysql__event__connection.html)
- [mysql_event_query Struct Reference](https://dev.mysql.com/doc/dev/mysql-server/latest/structmysql__event__query.html)

より詳細にプラグイン開発について知りたい場合は、以下の公式ドキュメントを確認すると良いでしょう。
[Writing Audit Plugins](https://dev.mysql.com/doc/extending-mysql/8.0/en/writing-audit-plugins.html)