A production MySQL monitoring stack needs three trustworthy layers: MySQL exposes native status and Performance Schema data; mysqld_exporter converts selected values into Prometheus metrics; Prometheus evaluates time-windowed rules and Grafana visualizes them. Each layer must be versioned and tested. A copied dashboard cannot tell you whether a collector is enabled, a metric was renamed, a counter reset, or a threshold fits your workload.
Deployment Scope and Current Version
As of August 1, 2026, the Prometheus project's latest published mysqld_exporter release is 0.19.0. Pin the reviewed release and verify the checksum for the correct operating system and architecture from the official release page. Do not leave an unversioned download in bootstrap automation. This guide targets MySQL 8.4 and exporter 0.19.0; revalidate privileges, collectors, and metric names after either component changes.
Place one exporter close to each MySQL instance so a scrape identifies one database role and failure domain. Scrape replicas separately. Give every target stable labels such as service, environment, Region, cluster, and intended role; do not put user IDs, query text, or other unbounded values into labels.
Create a Bounded Exporter Account
The upstream exporter documentation recommends PROCESS, REPLICATION CLIENT, and SELECT, with a small per-user connection limit. Restrict the account host to the exporter network identity instead of %. Review grants again if you enable non-default collectors because some query additional schemas or objects.
CREATE USER 'mysqld_exporter'@'127.0.0.1'
IDENTIFIED BY 'generated-secret-from-your-vault'
WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.*
TO 'mysqld_exporter'@'127.0.0.1';
SHOW GRANTS FOR 'mysqld_exporter'@'127.0.0.1';Generate the real password through the organization's secret system and inject it out of band; never commit it or place it directly in a systemd unit, shell history, or process argument. Rotate it by updating the database account and credential file through an atomic, tested procedure. If the exporter connects across a network, require MySQL TLS and validate the server certificate. The exporter supports ssl-ca and client certificate settings in its MySQL option file.
[client]
user=mysqld_exporter
password=INJECTED_BY_SECRET_MANAGER
host=127.0.0.1
port=3306
# For an off-host database, use its verified DNS name and a trusted ssl-ca.Own the directory by root, make it searchable only by the exporter group, and make the credential file readable only by root and that group. The password remains plaintext at rest unless your secret delivery mechanism provides a protected runtime mount, so treat host access as credential access.
Run a Pinned, Hardened systemd Service
Install the verified 0.19.0 binary as /usr/local/bin/mysqld_exporter, create a non-login mysqld_exporter operating-system account, and use a dedicated unit:
[Unit]
Description=Prometheus MySQL exporter
Wants=network-online.target
After=network-online.target
[Service]
User=mysqld_exporter
Group=mysqld_exporter
ExecStart=/usr/local/bin/mysqld_exporter \
--config.my-cnf=/etc/mysqld_exporter/client.cnf \
--web.listen-address=10.20.30.50:9104 \
--web.config.file=/etc/mysqld_exporter/web.yml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetThe example private address is illustrative; bind the exporter only to the host's approved monitoring interface and restrict the port with network policy. Configure the referenced web file with a certificate and private key:
tls_server_config:
cert_file: /etc/mysqld_exporter/tls/server.crt
key_file: /etc/mysqld_exporter/tls/server.keyA local Prometheus agent can instead scrape a loopback listener. Do not expose /metrics to the public internet. If TLS terminates at a proxy, monitor that proxy too so certificate or authentication failure is distinguishable from a database connection failure.
Start with default collectors. Exporter 0.19 enables global status, global variables, and replica status by default. Add optional Performance Schema or Information Schema collectors one at a time after measuring scrape duration, query cost, cardinality, and required grants. A huge metric set is not the same as useful observability.
Configure and Verify the Scrape
scrape_configs:
- job_name: mysql
scrape_interval: 15s
scrape_timeout: 10s
scheme: https
tls_config:
ca_file: /etc/prometheus/certs/exporter-ca.pem
static_configs:
- targets: ['mysql-primary.internal:9104']
labels:
service: orders
environment: production
role: primaryFor a loopback sidecar, use HTTP and remove remote authentication settings; for a remote exporter, configure the matching exporter-toolkit web file. Validate Prometheus configuration and rules with promtool check config and promtool check rules before reload. Then inspect the live /metrics output and Prometheus target page. Verify up, mysql_up, scrape duration, sample count, MySQL labels, and every series used by a rule.
up == 0 means Prometheus could not scrape the exporter endpoint. mysql_up == 0 means the exporter answered but could not collect from MySQL. Route those failures differently: the first points to exporter/network/TLS health; the second points to database reachability, credentials, TLS, privileges, or collector errors.
Use Current MySQL 8.4 Metric Names
Global status and variable collectors produce lowercase metric names such as mysql_global_status_threads_connected, mysql_global_variables_max_connections, mysql_global_status_innodb_buffer_pool_reads, and mysql_global_status_innodb_buffer_pool_read_requests. These are current series for exporter 0.19.0, but still verify them on the deployed binary.
The collector is still named slave_status for compatibility. On MySQL 8.4 it reads SHOW REPLICA STATUS and derives names from current output columns. That produces series such as mysql_slave_status_replica_io_running, mysql_slave_status_replica_sql_running, and mysql_slave_status_seconds_behind_source. Older dashboards that query ...slave_io_running or ...seconds_behind_master can silently show no data against an 8.4 server. Test every panel and alert after upgrades.
Start With Actionable Recording and Alert Rules
groups:
- name: mysql-health
rules:
- record: mysql:connection_utilization:ratio
expr: |
mysql_global_status_threads_connected
/ clamp_min(mysql_global_variables_max_connections, 1)
- record: mysql:innodb_physical_read_ratio:rate5m
expr: |
rate(mysql_global_status_innodb_buffer_pool_reads[5m])
/ clamp_min(
rate(mysql_global_status_innodb_buffer_pool_read_requests[5m]),
1
)
- alert: MySQLExporterUnreachable
expr: up{job="mysql"} == 0
for: 2m
labels:
severity: page
annotations:
summary: 'Prometheus cannot scrape the MySQL exporter'
- alert: MySQLCollectionFailed
expr: mysql_up{job="mysql"} == 0
for: 2m
labels:
severity: page
annotations:
summary: 'Exporter cannot collect from MySQL'
- alert: MySQLReplicaThreadStopped
expr: |
mysql_slave_status_replica_io_running{role="replica"} == 0
or mysql_slave_status_replica_sql_running{role="replica"} == 0
for: 2m
labels:
severity: page
annotations:
summary: 'A MySQL 8.4 replica thread is stopped'Page on symptoms with a clear operator action. Connection utilization may deserve a warning only when it persists and correlates with queueing or request errors. The physical-read ratio is diagnostic, not a universal 99-percent objective. Slow queries are a cumulative counter; use rate(mysql_global_status_slow_queries[5m]) and a threshold learned from the configured slow-query definition and workload baseline. Replication seconds can be NULL, absent, idle, or misleading; pair lag with thread state, GTID or position progress, and a business heartbeat. Test missing-series behavior so a deleted or renamed metric does not make the alert look healthy.
Build Grafana Around an Incident Workflow
- Service row: request rate, errors, latency, saturation, deploy annotations, and MySQL/exporter availability.
- Connections: connected and running threads, connection rate, aborted connects, max connections, and pool telemetry.
- Queries: question or query rate with documented semantics, slow-query rate, rows examined, temporary tables, and Performance Schema digest links.
- InnoDB: logical and physical reads, dirty pages, checkpoint and redo pressure, row-lock waits, and storage latency.
- Replication: receiver/applier state per channel, seconds behind source where present, GTID or log-position progress, relay-log growth, and heartbeat age.
- Exporter: scrape duration, scrape errors, samples, collector failures, process CPU/memory, and binary version.
Importing a community dashboard is only a draft. Remove panels backed by absent series, correct MySQL 8.4 names, bound variables, and add units and runbook links. Set dashboard refresh and query ranges so Grafana does not overload Prometheus during an incident. The database monitoring architecture guide covers cross-engine observability; the slow-query analysis guide covers statement-level diagnosis.
Acceptance Tests
- Stop the exporter and prove the scrape alert fires, routes, and resolves.
- Use an invalid staged credential and prove collection failure is distinct from endpoint failure.
- Revoke one required grant in a test environment and verify collector errors are visible.
- Pause a test replica receiver and applier separately; verify current 8.4 series and the runbook.
- Create controlled connection and read pressure, then compare native MySQL values with exported metrics.
- Restart MySQL and confirm counter resets do not create false rate spikes.
- Upgrade exporter or MySQL in staging and fail the release if required panels or rules return no series.