Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Docs/prometheus_exporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Prometheus Exporter Plugin

Embedded Prometheus metrics exporter for VillageSQL.

For full documentation, architecture, and configuration reference, see
[plugin/prometheus_exporter/README.md](../plugin/prometheus_exporter/README.md).
3 changes: 3 additions & 0 deletions mysql-test/include/plugin.defs
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,6 @@ component_test_execute_prepared_statement plugin_output_directory no COMPONEN

# component_test_execute_regular_statement
component_test_execute_regular_statement plugin_output_directory no COMPONENT_TEST_EXECUTE_REGULAR_STATEMENT

# Prometheus exporter plugin
prometheus_exporter plugin_output_directory no PROMETHEUS_EXPORTER_PLUGIN prometheus_exporter
17 changes: 17 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/basic.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Install prometheus_exporter plugin
INSTALL PLUGIN prometheus_exporter SONAME 'PROMETHEUS_EXPORTER_PLUGIN';
# Verify system variables exist with correct defaults
SHOW VARIABLES LIKE 'prometheus_exporter%';
Variable_name Value
prometheus_exporter_bind_address 127.0.0.1
prometheus_exporter_enabled OFF
prometheus_exporter_port 9104
prometheus_exporter_security_user root
# Verify status variables exist (all zero when disabled)
SHOW STATUS LIKE 'Prometheus_exporter%';
Variable_name Value
Prometheus_exporter_errors_total 0
Prometheus_exporter_requests_total 0
Prometheus_exporter_scrape_duration_microseconds 0
# Uninstall plugin
UNINSTALL PLUGIN prometheus_exporter;
6 changes: 6 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/binlog.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Verify binlog metrics are present
# TYPE mysql_binlog_file_count gauge
# Verify binlog size metric
# TYPE mysql_binlog_size_bytes_total gauge
# Verify values are numeric
mysql_binlog_file_count NUM
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Fetching and validating /metrics output format
OK: format validation passed
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Verify mysql_global_variables_ metrics are present
# TYPE mysql_global_variables_max_connections gauge
# Verify a buffer pool size variable is exported
# TYPE mysql_global_variables_innodb_buffer_pool_size gauge
# Verify the value is numeric
mysql_global_variables_max_connections NUM
8 changes: 8 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/innodb_metrics.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Verify InnoDB metrics are present
# TYPE mysql_innodb_metrics_lock_deadlocks counter
# TYPE mysql_innodb_metrics_lock_deadlock_false_positives counter
# TYPE mysql_innodb_metrics_lock_deadlock_rounds counter
# Verify a known gauge type metric exists (buffer_pool_reads is status_counter in InnoDB)
# TYPE mysql_innodb_metrics_buffer_pool_reads gauge
# Verify a known gauge type metric exists (buffer_pool_size is 'value' type in InnoDB)
# TYPE mysql_innodb_metrics_buffer_pool_size gauge
19 changes: 19 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/metrics_endpoint.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SHOW VARIABLES LIKE 'prometheus_exporter_enabled';
Variable_name Value
prometheus_exporter_enabled ON
# Verify SHOW GLOBAL STATUS metrics
# TYPE mysql_global_status_threads_connected gauge
# Verify SHOW GLOBAL VARIABLES metrics
# TYPE mysql_global_variables_max_connections gauge
# Verify INNODB_METRICS metrics
# TYPE mysql_innodb_metrics_lock_deadlocks counter
# Verify binlog metrics (binary logging is on by default)
# TYPE mysql_binlog_file_count gauge
# Verify metric value line is present and numeric
mysql_global_status_threads_connected NUM
# Test 404 for unknown paths
404
# Verify scrape counter incremented
SHOW STATUS LIKE 'Prometheus_exporter_requests_total';
Variable_name Value
Prometheus_exporter_requests_total NUM
4 changes: 4 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/replica_status.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# On a non-replica server, no mysql_replica_ metrics should appear
0
# But other metrics should still be present
# TYPE mysql_global_status_uptime gauge
23 changes: 23 additions & 0 deletions mysql-test/suite/prometheus_exporter/r/scrape_counter.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Capture baseline counter
SELECT VARIABLE_VALUE INTO @before FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_requests_total';
# Issue exactly 3 scrapes
# Capture counter after scrapes
SELECT VARIABLE_VALUE INTO @after FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_requests_total';
# Assert delta is exactly 3
SELECT (@after - @before) = 3 AS counter_delta_is_three;
counter_delta_is_three
1
# Also verify scrape_duration_microseconds is now > 0
SELECT VARIABLE_VALUE > 0 AS scrape_duration_is_positive
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_scrape_duration_microseconds';
scrape_duration_is_positive
1
# And verify errors_total is still 0 (no errors on successful scrapes)
SELECT VARIABLE_VALUE = 0 AS errors_total_is_zero
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_errors_total';
errors_total_is_zero
1
34 changes: 34 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/basic.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# =============================================================================
# basic.test -- Prometheus Exporter Plugin: Install/Uninstall Lifecycle
#
# Verifies:
# - Plugin can be dynamically installed and uninstalled
# - System variables (enabled, port, bind_address) are registered
# - Status variables (requests_total, errors_total, scrape_duration) exist
# - Default values are correct (enabled=OFF, port=9104, bind=0.0.0.0)
# =============================================================================

--source include/not_windows.inc

# Check that plugin is available
disable_query_log;
if (`SELECT @@have_dynamic_loading != 'YES'`) {
--skip prometheus_exporter plugin requires dynamic loading
}
if (!$PROMETHEUS_EXPORTER_PLUGIN) {
--skip prometheus_exporter plugin requires the environment variable \$PROMETHEUS_EXPORTER_PLUGIN to be set (normally done by mtr)
}
enable_query_log;

--echo # Install prometheus_exporter plugin
--replace_result $PROMETHEUS_EXPORTER_PLUGIN PROMETHEUS_EXPORTER_PLUGIN
eval INSTALL PLUGIN prometheus_exporter SONAME '$PROMETHEUS_EXPORTER_PLUGIN';

--echo # Verify system variables exist with correct defaults
SHOW VARIABLES LIKE 'prometheus_exporter%';

--echo # Verify status variables exist (all zero when disabled)
SHOW STATUS LIKE 'Prometheus_exporter%';

--echo # Uninstall plugin
UNINSTALL PLUGIN prometheus_exporter;
1 change: 1 addition & 0 deletions mysql-test/suite/prometheus_exporter/t/binlog-master.opt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19108 --prometheus-exporter-bind-address=127.0.0.1
11 changes: 11 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/binlog.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--source include/not_windows.inc

--echo # Verify binlog metrics are present
--exec curl -s http://127.0.0.1:19108/metrics | grep "^# TYPE mysql_binlog_file_count gauge" | head -1

--echo # Verify binlog size metric
--exec curl -s http://127.0.0.1:19108/metrics | grep "^# TYPE mysql_binlog_size_bytes_total gauge" | head -1

--echo # Verify values are numeric
--replace_regex /[0-9]+/NUM/
--exec curl -s http://127.0.0.1:19108/metrics | grep "^mysql_binlog_file_count " | head -1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19109 --prometheus-exporter-bind-address=127.0.0.1
85 changes: 85 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/format_validation.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# =============================================================================
# format_validation.test -- Validates Prometheus exposition format correctness
#
# Uses a perl block to fetch /metrics and validate:
# - Every # TYPE line has a valid type (counter, gauge, untyped)
# - Every # TYPE line is followed by a metric line with matching name
# - Metric names match [a-z_][a-z0-9_]*
# - Every metric value is numeric or NaN/+Inf/-Inf per Prometheus spec
# =============================================================================

--source include/not_windows.inc

--echo # Fetching and validating /metrics output format

--perl
use strict;
use warnings;

my $output = `curl -s http://127.0.0.1:19109/metrics`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout and fail-fast handling to the curl fetch.

At Line 19, curl -s can block indefinitely and make this test flaky under listener stalls. Add --max-time, --fail, and explicit exit-status handling.

🔧 Proposed patch
-my $output = `curl -s http://127.0.0.1:19109/metrics`;
+my $output = `curl -sS --fail --max-time 5 http://127.0.0.1:19109/metrics`;
+if ($? != 0) {
+  print "ERROR: failed to fetch /metrics\n";
+  exit 1;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
my $output = `curl -s http://127.0.0.1:19109/metrics`;
my $output = `curl -sS --fail --max-time 5 http://127.0.0.1:19109/metrics`;
if ($? != 0) {
print "ERROR: failed to fetch /metrics\n";
exit 1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mysql-test/suite/prometheus_exporter/t/format_validation.test` at line 19,
The curl backtick invocation assigning to $output can hang; change the command
to include timeout and fail flags (e.g. add --max-time 5 --fail --silent) and
then check the shell exit status ($?) immediately after the backtick call,
failing the test if non-zero (die or ok/fail) so the test is fail-fast on fetch
errors; locate the assignment to $output (my $output = `curl -s
http://127.0.0.1:19109/metrics`), replace the command string with one that
includes --max-time and --fail, and add a conditional checking $? to handle and
report the error before proceeding.

my @lines = split /\n/, $output;
my $errors = 0;
my $metrics_count = 0;
my $expect_metric_name = undef;

for (my $i = 0; $i < scalar @lines; $i++) {
my $line = $lines[$i];

# Skip empty lines
next if $line =~ /^\s*$/;

# Check # TYPE lines
if ($line =~ /^# TYPE /) {
if (defined $expect_metric_name) {
print "FORMAT ERROR: missing metric line for TYPE '$expect_metric_name'\n";
$errors++;
}
if ($line =~ /^# TYPE ([a-z_][a-z0-9_]*) (counter|gauge|untyped)$/) {
$expect_metric_name = $1;
} else {
print "FORMAT ERROR: invalid TYPE line: $line\n";
$errors++;
$expect_metric_name = undef;
}
next;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Skip other comment lines
next if $line =~ /^#/;

# Metric value line
if ($line =~ /^([a-z_][a-z0-9_]*) (.+)$/) {
my ($name, $value) = ($1, $2);
$metrics_count++;

# Check name matches expected from TYPE line
if (defined $expect_metric_name && $name ne $expect_metric_name) {
print "FORMAT ERROR: expected metric '$expect_metric_name' but got '$name'\n";
$errors++;
}
$expect_metric_name = undef;

# Check value is numeric (Prometheus exposition format: numeric or NaN/Inf)
unless ($value =~ /^(-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?|NaN|\+Inf|-Inf)$/) {
print "FORMAT ERROR: non-numeric value '$value' for metric '$name'\n";
$errors++;
}
} else {
print "FORMAT ERROR: unrecognized line: $line\n";
$errors++;
}
}

if (defined $expect_metric_name) {
print "FORMAT ERROR: missing metric line for TYPE '$expect_metric_name'\n";
$errors++;
}

if ($errors == 0 && $metrics_count > 0) {
print "OK: format validation passed\n";
} elsif ($metrics_count == 0) {
print "ERROR: no metrics found in output\n";
} else {
print "FAIL: $errors format errors found in $metrics_count metrics\n";
}
EOF
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19105 --prometheus-exporter-bind-address=127.0.0.1
11 changes: 11 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/global_variables.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--source include/not_windows.inc

--echo # Verify mysql_global_variables_ metrics are present
--exec curl -s http://127.0.0.1:19105/metrics | grep "^# TYPE mysql_global_variables_max_connections gauge" | head -1

--echo # Verify a buffer pool size variable is exported
--exec curl -s http://127.0.0.1:19105/metrics | grep "^# TYPE mysql_global_variables_innodb_buffer_pool_size gauge" | head -1

--echo # Verify the value is numeric
--replace_regex /[0-9]+/NUM/
--exec curl -s http://127.0.0.1:19105/metrics | grep "^mysql_global_variables_max_connections " | head -1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19106 --prometheus-exporter-bind-address=127.0.0.1
10 changes: 10 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/innodb_metrics.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--source include/not_windows.inc

--echo # Verify InnoDB metrics are present
--exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_" | head -3

--echo # Verify a known gauge type metric exists (buffer_pool_reads is status_counter in InnoDB)
--exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_reads " | head -1

--echo # Verify a known gauge type metric exists (buffer_pool_size is 'value' type in InnoDB)
--exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_size " | head -1
Comment on lines +7 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert exact Prometheus types for the two canonical InnoDB metrics.

These patterns currently only confirm metric family presence; they don’t enforce counter for buffer_pool_reads or gauge for buffer_pool_size.

🔧 Suggested tightening
---exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_reads " | head -1
+--exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_reads counter$" | head -1

---exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_size " | head -1
+--exec curl -s http://127.0.0.1:19106/metrics | grep "^# TYPE mysql_innodb_metrics_buffer_pool_size gauge$" | head -1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mysql-test/suite/prometheus_exporter/t/innodb_metrics.test` around lines 7 -
10, The test currently only checks the presence of the metric families; tighten
the assertions to require exact Prometheus types by changing the grep patterns
to match the full type lines for the two metrics: require "^# TYPE
mysql_innodb_metrics_buffer_pool_reads counter$" for
mysql_innodb_metrics_buffer_pool_reads and "^# TYPE
mysql_innodb_metrics_buffer_pool_size gauge$" for
mysql_innodb_metrics_buffer_pool_size (update the two curl|grep commands in
innodb_metrics.test that reference those metric names).

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19104 --prometheus-exporter-bind-address=127.0.0.1
37 changes: 37 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/metrics_endpoint.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# =============================================================================
# metrics_endpoint.test -- Prometheus Exporter: HTTP Endpoint & All Collectors
#
# Verifies:
# - HTTP endpoint serves Prometheus text format
# - 4 of 5 collector prefixes appear in output (replica not testable without replica setup)
# - 404 returned for unknown paths
# - Scrape counter increments
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# =============================================================================

--source include/not_windows.inc

# Verify plugin is loaded and enabled
SHOW VARIABLES LIKE 'prometheus_exporter_enabled';

--echo # Verify SHOW GLOBAL STATUS metrics
--exec curl -s http://127.0.0.1:19104/metrics | grep "^# TYPE mysql_global_status_threads_connected" | head -1

--echo # Verify SHOW GLOBAL VARIABLES metrics
--exec curl -s http://127.0.0.1:19104/metrics | grep "^# TYPE mysql_global_variables_max_connections" | head -1

--echo # Verify INNODB_METRICS metrics
--exec curl -s http://127.0.0.1:19104/metrics | grep "^# TYPE mysql_innodb_metrics_" | head -1

--echo # Verify binlog metrics (binary logging is on by default)
--exec curl -s http://127.0.0.1:19104/metrics | grep "^# TYPE mysql_binlog_file_count" | head -1

--echo # Verify metric value line is present and numeric
--replace_regex /[0-9]+/NUM/
--exec curl -s http://127.0.0.1:19104/metrics | grep "^mysql_global_status_threads_connected " | head -1

--echo # Test 404 for unknown paths
--exec curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:19104/notfound

--echo # Verify scrape counter incremented
--replace_regex /[0-9]+/NUM/
SHOW STATUS LIKE 'Prometheus_exporter_requests_total';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19107 --prometheus-exporter-bind-address=127.0.0.1
7 changes: 7 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/replica_status.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
--source include/not_windows.inc

--echo # On a non-replica server, no mysql_replica_ metrics should appear
--exec curl -s http://127.0.0.1:19107/metrics | grep -c "mysql_replica_" || true

--echo # But other metrics should still be present
--exec curl -s http://127.0.0.1:19107/metrics | grep "^# TYPE mysql_global_status_uptime" | head -1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$PROMETHEUS_EXPORTER_PLUGIN_OPT $PROMETHEUS_EXPORTER_PLUGIN_LOAD --prometheus-exporter-enabled=ON --prometheus-exporter-port=19110 --prometheus-exporter-bind-address=127.0.0.1
35 changes: 35 additions & 0 deletions mysql-test/suite/prometheus_exporter/t/scrape_counter.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# =============================================================================
# scrape_counter.test -- Verify the requests_total counter actually increments
#
# Captures the counter before and after a specific number of curl requests
# and asserts the exact delta. Catches regressions where the counter is
# silently not incremented.
# =============================================================================

--source include/not_windows.inc

--echo # Capture baseline counter
SELECT VARIABLE_VALUE INTO @before FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_requests_total';

--echo # Issue exactly 3 scrapes
--exec curl -s http://127.0.0.1:19110/metrics > /dev/null
--exec curl -s http://127.0.0.1:19110/metrics > /dev/null
--exec curl -s http://127.0.0.1:19110/metrics > /dev/null

--echo # Capture counter after scrapes
SELECT VARIABLE_VALUE INTO @after FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_requests_total';

--echo # Assert delta is exactly 3
SELECT (@after - @before) = 3 AS counter_delta_is_three;

--echo # Also verify scrape_duration_microseconds is now > 0
SELECT VARIABLE_VALUE > 0 AS scrape_duration_is_positive
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_scrape_duration_microseconds';

--echo # And verify errors_total is still 0 (no errors on successful scrapes)
SELECT VARIABLE_VALUE = 0 AS errors_total_is_zero
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Prometheus_exporter_errors_total';
20 changes: 20 additions & 0 deletions plugin/prometheus_exporter/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) 2025, VillageSQL and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

MYSQL_ADD_PLUGIN(prometheus_exporter
prometheus_exporter.cc
MODULE_ONLY
MODULE_OUTPUT_NAME "prometheus_exporter"
)
Loading