Vibe-coding an Audit Plugin in Under 3 Minutes

Who says developing MariaDB plugins is hard? I was able to produce one in under 3 minutes!

I of course did it by asking Grok nicely:

The produced result is actually very decent:

/*
  errorwatch.c - MariaDB Audit API plugin

  Records statement execution errors into a dedicated log file.

  This plugin subscribes to the GENERAL audit class and logs
  events of type MYSQL_AUDIT_GENERAL_ERROR (and STATUS events
  that carry a non-zero error code). It writes structured
  entries to /var/log/mariadb/errorwatch.log (or /tmp/ fallback).

  To build (inside MariaDB source tree):
    1. Copy this file and CMakeLists.txt to plugin/errorwatch/
    2. Rebuild MariaDB (cmake ... ; make -jN)
    3. INSTALL PLUGIN errorwatch SONAME 'errorwatch.so';

  Copyright (c) 2026, xAI / Grok
  Licensed under GPL (same as MariaDB)
*/

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <pthread.h>

#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>

#if !defined(__attribute__) && (defined(__cplusplus) || !defined(__GNUC__) || (__GNUC__ == 2 && __GNUC_MINOR__ < 8))
#define __attribute__(A)
#endif

#ifdef _MSC_VER
#define snprintf _snprintf
#endif

/* Dedicated log file path - change as needed */
#define ERRORWATCH_LOG_PATH "/var/log/mariadb/errorwatch.log"
#define ERRORWATCH_TMP_LOG_PATH "/tmp/errorwatch.log"

/* Simple status counter */
static volatile long errorwatch_logged_errors = 0;

/* Thread-safe logging */
static pthread_mutex_t errorwatch_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static FILE *errorwatch_log_file = NULL;

/* Forward declarations */
static int errorwatch_plugin_init(void *arg __attribute__((unused)));
static int errorwatch_plugin_deinit(void *arg __attribute__((unused)));
static void errorwatch_notify(MYSQL_THD thd __attribute__((unused)),
                              unsigned int event_class,
                              const void *event);

/* Audit descriptor - we only need GENERAL class for statement errors */
static struct st_mysql_audit errorwatch_descriptor =
{
  MYSQL_AUDIT_INTERFACE_VERSION,
  NULL,                    /* release_thd */
  errorwatch_notify,
  { MYSQL_AUDIT_GENERAL_CLASSMASK }
};

/* Status variables */
static struct st_mysql_show_var errorwatch_status[] =
{
  {"Errorwatch_logged_errors", (char *)&errorwatch_logged_errors, SHOW_LONG},
  {0, 0, SHOW_UNDEF}
};

/*
  Plugin initialization.
  Opens the dedicated log file (with fallback).
*/
static int errorwatch_plugin_init(void *arg __attribute__((unused)))
{
  errorwatch_log_file = fopen(ERRORWATCH_LOG_PATH, "a");
  if (!errorwatch_log_file)
  {
    /* Fallback to /tmp if /var/log/mariadb is not writable */
    errorwatch_log_file = fopen(ERRORWATCH_TMP_LOG_PATH, "a");
    if (!errorwatch_log_file)
      return 1;  /* init failure */
  }

  /* Write startup header */
  time_t now = time(NULL);
  char ts[64];
  strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", localtime(&now));
  fprintf(errorwatch_log_file,
          "# ErrorWatch plugin v0.1 started at %s\n"
          "# Logging statement execution errors (GENERAL_ERROR / STATUS with error_code != 0)\n"
          "# Format: [timestamp] ERROR <code> Thread=<id> User='<user>' DB='<db>' Query='<query>'\n\n",
          ts);
  fflush(errorwatch_log_file);

  return 0;
}

/*
  Plugin deinitialization.
  Closes the log file cleanly.
*/
static int errorwatch_plugin_deinit(void *arg __attribute__((unused)))
{
  if (errorwatch_log_file)
  {
    time_t now = time(NULL);
    char ts[64];
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", localtime(&now));
    fprintf(errorwatch_log_file, "\n# ErrorWatch plugin stopped at %s\n", ts);
    fclose(errorwatch_log_file);
    errorwatch_log_file = NULL;
  }
  return 0;
}

/*
  The core audit callback.
  Called by the server for every audit event we subscribed to.
*/
static void errorwatch_notify(MYSQL_THD thd __attribute__((unused)),
                              unsigned int event_class,
                              const void *event)
{
  if (event_class != MYSQL_AUDIT_GENERAL_CLASS || !errorwatch_log_file)
    return;

  const struct mysql_event_general *e =
    (const struct mysql_event_general *) event;

  int error_code = e->general_error_code;

  /* Log on explicit ERROR subclass OR on STATUS that carries an error */
  if (e->event_subclass != MYSQL_AUDIT_GENERAL_ERROR &&
      !(e->event_subclass == MYSQL_AUDIT_GENERAL_STATUS && error_code != 0))
    return;

  /* Increment counter */
  __sync_fetch_and_add(&errorwatch_logged_errors, 1);

  pthread_mutex_lock(&errorwatch_log_mutex);

  if (errorwatch_log_file)
  {
    time_t now = time(NULL);
    char ts[64];
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", localtime(&now));

    const char *user = e->general_user ? e->general_user : "";
    unsigned int user_len = e->general_user_length;

    const char *query = e->general_query ? e->general_query : "";
    unsigned int query_len = e->general_query_length;

    const char *db_str = e->database.str ? e->database.str : "";
    size_t db_len = e->database.length;

    /* Truncate very long queries */
    const unsigned int MAX_QUERY_LEN = 2048;
    if (query_len > MAX_QUERY_LEN)
      query_len = MAX_QUERY_LEN;

    fprintf(errorwatch_log_file,
            "[%s] ERROR %d  Thread=%lu  User='%.*s'  DB='%.*s'  Query='%.*s'\n",
            ts,
            error_code,
            e->general_thread_id,
            (int)user_len, user,
            (int)db_len, db_str,
            (int)query_len, query);

    fflush(errorwatch_log_file);
  }

  pthread_mutex_unlock(&errorwatch_log_mutex);
}

/* MariaDB plugin declaration */
maria_declare_plugin(errorwatch)
{
  MYSQL_AUDIT_PLUGIN,                /* type */
  &errorwatch_descriptor,            /* descriptor */
  "ERRORWATCH",                      /* name */
  "Grok (built by xAI)",             /* author */
  "Audit plugin that records statement execution errors to a dedicated log file (errorwatch.log)",
  PLUGIN_LICENSE_GPL,
  errorwatch_plugin_init,            /* init */
  errorwatch_plugin_deinit,          /* deinit */
  0x0001,                            /* version */
  errorwatch_status,                 /* status vars */
  NULL,                              /* system vars (none for this minimal version) */
  "0.1",                             /* version string */
  MariaDB_PLUGIN_MATURITY_STABLE
}
maria_declare_plugin_end;

It even produced a Makefile:

# CMakeLists.txt for errorwatch audit plugin
MYSQL_ADD_PLUGIN(errorwatch errorwatch.c
  MODULE_ONLY
  COMPONENT Server
)

And compilation instructions:

The plugin has even compiled and worked on my Mac!

MariaDB [(none)]> INSTALL PLUGIN errorwatch SONAME 'errorwatch.so';
Query OK, 0 rows affected (0.368 sec)

MariaDB [(none)]> SELECT * FROM non_existent_table;
ERROR 1046 (3D000): No database selected
MariaDB [(none)]> SELECT 1/0;
+------+
| 1/0  |
+------+
| NULL |
+------+
1 row in set, 1 warning (0.001 sec)

MariaDB [(none)]> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [mysql]> SELECT * FROM non_existent_table;
ERROR 1146 (42S02): Table 'mysql.non_existent_table' doesn't exist

MariaDB [(none)]> SHOW STATUS LIKE 'Errorwatch%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| Errorwatch_logged_errors | 4     |
+--------------------------+-------+
1 row in set (0.003 sec)

MariaDB [(none)]> exit;
Bye
blackbook:~/dev/server-main/bld$tail -f /tmp/errorwatch.log 
# ErrorWatch plugin v0.1 started at 2026-05-21 10:20:26
# Logging statement execution errors (GENERAL_ERROR / STATUS with error_code != 0)
# Format: [timestamp] ERROR <code> Thread=<id> User='<user>' DB='<db>' Query='<query>'

[2026-05-21 10:20:56] ERROR 1046  Thread=4  User='gkodinov[gkodinov] @ localhost []'  DB=''  Query='SELECT * FROM non_existent_table'
[2026-05-21 10:20:56] ERROR 1046  Thread=4  User='gkodinov[gkodinov] @ localhost []'  DB=''  Query='SELECT * FROM non_existent_table'
[2026-05-21 10:21:09] ERROR 1146  Thread=4  User='gkodinov[gkodinov] @ localhost []'  DB='mysql'  Query='SELECT * FROM non_existent_table'
[2026-05-21 10:21:09] ERROR 1146  Thread=4  User='gkodinov[gkodinov] @ localhost []'  DB='mysql'  Query='SELECT * FROM non_existent_table'

Tests are Important!

While I had Grok’s attention I’ve given it a follow up task:

Note the “please” part! It’s important!

This second prompt made it realize its naiveté and parametrize the plugin better. It has added a new system variable (with a default of /tmp/errorwatch.log), complete with an “update” function to rotate to the newly specified file. And it did get rid of the /var/log … compiled-in default. It did fall into the usual trap though: no validation of the user-provided path at all! But it did bump the version of the plugin to 0.2! And, most importantly, produced the following test description:

Unfortunately, the resulting plugin update failed to compile:

Gotcha! Well, it’s a trap of our own doing really: no clear way to allocate memory in a plugin. After manually replacing my_xxx functions with their C library equivalents I was greeted by an assert:

CURRENT_TEST: main.errorwatch
mysqltest: At line 10: query 'INSTALL PLUGIN errorwatch SONAME 'errorwatch.so'' failed: <Unknown> (2013): Lost connection to server during query

The result from queries just before the failure was:
# === Installing errorwatch plugin ===
INSTALL PLUGIN errorwatch SONAME 'errorwatch.so';

<snip>

Version: '13.0.1-MariaDB-debug-log'  socket: '/Users/gkodinov/dev/server-main/bld/mysql-test/var/tmp/mysqld.1.sock'  port: 19000  Source distribution
Assertion failed: (!comment || !comment[0] || comment[strlen(comment)-1] != '.'), function <unknown>, file set_var.cc, line 174.
260521 11:33:06 [ERROR] /Users/gkodinov/dev/server-main/bld/sql/mariadbd got signal 6 ;
Sorry, we probably made a mistake, and this is a bug.

<snip>

Thread pointer: 0x9fd216088
stack_bottom = 0x16b7b6e70 thread_stack 0x49000
Printing to addr2line failed
0   mariadbd                            0x0000000105a87d44 my_print_stacktrace + 64
0   mariadbd                            0x000000010498e1c4 handle_fatal_signal + 612
0   libsystem_platform.dylib            0x000000018ca73744 _sigtramp + 56
0   libsystem_pthread.dylib             0x000000018ca698d8 pthread_kill + 296
0   libsystem_c.dylib                   0x000000018c970644 abort + 148
0   libsystem_c.dylib                   0x000000018c96f8a0 err + 0
0   mariadbd                            0x0000000104caf334 _ZN7sys_varC2EP13sys_var_chainPKcS3_ili16get_opt_arg_type20enum_mysql_show_typexP8PolyLockNS_18binlog_status_enumEPFbPS_P3THDP7set_varEPFbS9_SB_13enum_var_typeES3_ + 876
0   mariadbd                            0x0000000104e97fa0 _ZN17sys_var_pluginvarC2EP13sys_var_chainPKcP13st_plugin_intP16st_mysql_sys_varS3_ + 196
0   mariadbd                            0x0000000104e98b3c _ZN17sys_var_pluginvarC1EP13sys_var_chainPKcP13st_plugin_intP16st_mysql_sys_varS3_ + 68
0   mariadbd                            0x0000000104e9ca4c _ZL19test_plugin_optionsP11st_mem_rootP13st_plugin_intPiPPc + 1740
0   mariadbd                            0x0000000104e91e0c _ZL17plugin_initializeP11st_mem_rootP13st_plugin_intPiPPcb + 644
0   mariadbd                            0x0000000104e95444 _ZL16finalize_installP3THDP5TABLEPK25st_mysql_const_lex_stringPiPPc + 480
0   mariadbd                            0x0000000104e946b4 _Z20mysql_install_pluginP3THDPK25st_mysql_const_lex_stringS3_ + 876
0   mariadbd                            0x0000000104e62840 _Z21mysql_execute_commandP3THDb + 27236
0   mariadbd                            0x0000000104e56090 _Z11mysql_parseP3THDPcjP12Parser_state + 940
0   mariadbd                            0x0000000104e53020 _Z16dispatch_command19enum_server_commandP3THDPcjb + 3588
0   mariadbd                            0x0000000104e56b5c _Z10do_commandP3THDb + 1568
0   mariadbd                            0x00000001050ac838 _Z24do_handle_one_connectionP7CONNECTb + 396
0   mariadbd                            0x00000001050ac570 handle_one_connection + 100
0   mariadbd                            0x0000000105428544 pfs_spawn_thread + 284
0   libsystem_pthread.dylib             0x000000018ca69c58 _pthread_start + 136
0   libsystem_pthread.dylib             0x000000018ca64c1c thread_start + 8

It’s the server’s plugin code enforcing that the description of a system variable does not end with a dot (‘.’)! How elitist!
After applying my wetware skills I did eventually fix all of the issues revealed by the test. Of which there were many: bad memory management in system variable handling, non-existent SQL syntax extension SHOW PLUGINS LIKE, wrong perl code generated to check the log file (when one can use the much more convenient mtr tools). Took some work!

My Professional Opinion

Overall: it’s not hard to get the boilerplate for plugin development going. No, it’s not 100% fool-proof (Yet?). And it definitely got confused on the test task. So don’t rush to fire your developers just yet. But it saves a lot of time and effort!

Could It have done better? Probably! I’m no prompt engineer. I did my best there. But It’s supposed to be assisting me. Not the other way around!

Now The Egg Hunt Begins!

Be a vibe code reviewer! Tell me in the comments what It did wrong! And where did It hallucinate! Do expose it All ™! Destroy that wicked Jacquard loom with words!

I’ll start: there were 3 compilation warnings:

[build] /Users/gkodinov/dev/server-main/plugin/errorwatch/errorwatch.c:91:10: warning: mixing declarations and code is incompatible with standards before C99 [-Wdeclaration-after-statement]
[build]    91 |   time_t now = time(NULL);
[build]       |          ^
[build] /Users/gkodinov/dev/server-main/plugin/errorwatch/errorwatch.c:154:17: warning: mixing declarations and code is incompatible with standards before C99 [-Wdeclaration-after-statement]
[build]   154 |     const char *user = e->general_user ? e->general_user : "";
[build]       |                 ^
[build] /Users/gkodinov/dev/server-main/plugin/errorwatch/errorwatch.c:133:37: warning: mixing declarations and code is incompatible with standards before C99 [-Wdeclaration-after-statement]
[build]   133 |   const struct mysql_event_general *e =
[build]       |                                     ^