Database SRE

MySQL Monitoring with Prometheus and Grafana: Complete Setup Guide

mysqld_exporter exposes 300+ MySQL metrics for Prometheus. Learn to deploy it, configure essential alerting rules for connection saturation and replication lag, and build production dashboards in Grafana.

JusDB Team
Published January 6, 2025
Updated August 1, 2026
7 min read

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.target

The 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.key

A 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: primary

For 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

  1. Stop the exporter and prove the scrape alert fires, routes, and resolves.
  2. Use an invalid staged credential and prove collection failure is distinct from endpoint failure.
  3. Revoke one required grant in a test environment and verify collector errors are visible.
  4. Pause a test replica receiver and applier separately; verify current 8.4 series and the runbook.
  5. Create controlled connection and read pressure, then compare native MySQL values with exported metrics.
  6. Restart MySQL and confirm counter resets do not create false rate spikes.
  7. Upgrade exporter or MySQL in staging and fail the release if required panels or rules return no series.

Official Primary Documentation

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Ola Hallengren's SQL Server Maintenance Solution: Production Setup Guide

Production setup of Ola Hallengren's SQL Server Maintenance Solution: the four jobs that matter, FULL/DIFF/LOG backup cadence for your RPO, DBCC CHECKDB scheduling, IndexOptimize tuning, encryption, and CommandLog-based alerting.

SQL Server13 minMay 27, 2026
Read

PostgreSQL Monitoring with Prometheus and postgres_exporter: A Production Guide

Set up PostgreSQL monitoring with Prometheus and postgres_exporter. Includes install steps, critical alert rules, Grafana dashboard panels, and custom query metrics.

PostgreSQL10 minMar 5, 2026
Read

PostgreSQL 16: New Features Every DBA Should Know

PostgreSQL 16 introduced logical replication from standbys, pg_stat_io, SQL/JSON constructors, COPY improvements, and pg_stat_checkpointer. Full DBA upgrade guide.

PostgreSQL12 minMar 5, 2026
Read