All posts

MySQL Monitoring with Grafana and Alloy

A step-by-step guide to monitoring MySQL with Grafana Alloy and Mimir: a least-privilege user, the right collectors, key queries and a ready-made dashboard.

Once the database is in production, the questions change. It is no longer "does it start?" but "how close are we to max connections?", "which queries are eating the time?" and "is the buffer pool big enough?". This post adds MySQL to the same Grafana stack I built in the earlier posts, so those answers sit next to the server and application metrics.

By the end you will have:

  • A dedicated, read-only MySQL user for monitoring
  • Alloy's built-in MySQL exporter shipping metrics to Mimir
  • A set of queries worth knowing when the dashboard isn't enough
  • The MySQL Overview dashboard imported into Grafana

Requirements

Before we start, you need two things in place:

Alloy needs to run on the same server as MySQL for the configuration below, since it connects over 127.0.0.1. If your database lives elsewhere, change the host in the connection string and the user's host part to match.


Create a dedicated MySQL user for Alloy

It's good practice to give Alloy its own read-only monitoring user rather than reusing an application account. If the monitoring credentials leak, the worst someone can do is read status counters, not your data. Log in to MySQL:

sudo mysql -u root -p

Then create the user and grant it the necessary permissions:

CREATE USER 'alloy_monitor'@'localhost' IDENTIFIED BY 'StrongPassword123!' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'alloy_monitor'@'localhost';
GRANT SELECT ON performance_schema.* TO 'alloy_monitor'@'localhost';
FLUSH PRIVILEGES;

Use your own password, not the one from this post. Here is what each part is for:

Grant Why the exporter needs it
PROCESS Reading the process list and InnoDB status
REPLICATION CLIENT Reading replication status, so you can see replica lag
SELECT ON *.* Reading information_schema tables, such as table sizes
SELECT ON performance_schema.* Reading statement, file and memory statistics
MAX_USER_CONNECTIONS 3 A cap, so a misbehaving scraper can never use up your connection pool

The localhost host part means the user can only log in from the database server itself, which is all Alloy needs here.


Add MySQL configuration to Alloy

Open the Alloy config file:

sudo nano /etc/alloy/config.alloy

Append the following at the end:

prometheus.exporter.mysql "mysql" {
  data_source_name = "alloy_monitor:StrongPassword123!@(127.0.0.1:3306)/"

  enable_collectors = [
    "global_status",
    "global_variables",
    "slave_status",
    "info_schema.innodb_metrics",
    "info_schema.processlist",
    "info_schema.tables",
    "perf_schema.eventsstatements",
    "perf_schema.file_instances",
    "perf_schema.memory_events",
  ]
}

prometheus.scrape "mysql" {
  targets    = prometheus.exporter.mysql.mysql.targets
  forward_to = [prometheus.remote_write.mimir.receiver]
  job_name   = "mysql"
}

The first block runs the exporter inside Alloy, so there is no separate mysqld_exporter process to install. The second block scrapes it and forwards the samples to the prometheus.remote_write.mimir component from the base Alloy setup.

What the collectors give you

The exporter enables a small default set of collectors. The list above adds the ones I find useful on a single production server:

Collector What you get
global_status The counters from SHOW GLOBAL STATUS: queries, connections, threads, slow queries, buffer pool reads
global_variables Configuration values like max_connections, so you can compare usage against limits
slave_status Replication state and lag. On a server without replicas it simply returns nothing
info_schema.innodb_metrics InnoDB internals from INNODB_METRICS
info_schema.processlist How many threads sit in each state, useful for spotting lock waits
info_schema.tables Size and row estimates per table
perf_schema.eventsstatements Per-query-digest statistics: how often a query runs and how long it takes
perf_schema.file_instances I/O per file
perf_schema.memory_events Memory usage per instrument

Two of these deserve a warning. info_schema.tables queries metadata for every table, which gets slow on schemas with thousands of tables. perf_schema.eventsstatements creates a series per query digest. The exporter caps it to the top statements, but on an application with lots of distinct queries it is still the biggest source of series. If Mimir storage or scrape time becomes a problem, these are the first two to drop.

Keep the password out of the config file

Hardcoding the password works, but it puts the credential in a file that tends to get copied into backups and repos. Alloy can read it from an environment variable instead. Change the connection string to:

  data_source_name = sys.env("MYSQL_EXPORTER_DSN")

Then add the variable to the environment file the Alloy service loads, on Debian and Ubuntu that is /etc/default/alloy:

MYSQL_EXPORTER_DSN="alloy_monitor:StrongPassword123!@(127.0.0.1:3306)/"

After saving, restart Alloy for the changes to take effect:

sudo service alloy restart

Check that metrics arrive

Before opening dashboards, confirm the pipeline works end to end. First, check that Alloy started without config errors:

sudo journalctl -u alloy -n 50 --no-pager

Errors about the MySQL component usually mean a wrong password or a missing grant. Then open Explore in Grafana, pick the Mimir data source and run:

mysql_up{job="mysql"}

A value of 1 means the exporter can log in to MySQL. A 0 means Alloy is running but the database login fails, so recheck the user and password. No result at all means the samples aren't reaching Mimir, so check the scrape and remote write blocks.

Alloy is now shipping MySQL metrics to Mimir, ready to be visualized in Grafana.


Queries worth knowing

The dashboard covers most needs, but a few queries are worth having at hand in Explore, and they are the base for alerts later:

Question Query
How busy is the server? rate(mysql_global_status_queries[5m])
How close are we to the connection limit? mysql_global_status_threads_connected / mysql_global_variables_max_connections
Are slow queries increasing? rate(mysql_global_status_slow_queries[5m])
Is the buffer pool big enough? rate(mysql_global_status_innodb_buffer_pool_reads[5m]) / rate(mysql_global_status_innodb_buffer_pool_read_requests[5m])

The last one is the share of reads that had to go to disk instead of memory. It should stay close to zero. If it climbs and stays up, the working set no longer fits in innodb_buffer_pool_size.


Grafana Dashboard

Rather than building a dashboard from scratch, you can import the official MySQL Overview dashboard directly from the Grafana dashboard library.

In Grafana, go to Dashboards → New → Import, enter the ID 7362 and click Load. Pick your Mimir data source when asked and import it. The host selector at the top lists every MySQL instance that reports metrics, so the same dashboard works if you add more servers later.

Grafana MySQL dashboard

Some panels may stay empty. That usually means they rely on a collector that isn't enabled in the config above, not that something is broken.


Wrapping Up

This guide builds on the two previous posts and gives you a working MySQL monitoring setup in just a few steps: a least-privilege user, the exporter running inside Alloy, and a dashboard on top. From here, the natural next move is to set up some alerts so you get notified before small problems become big ones - Grafana Alerting for Your Production Stack walks through exactly that, including a ready-made MySQL max connections alert built on the same ratio as above. If you want to go further and add distributed tracing to complete the full observability picture, check out Completing the LGTM Stack with Grafana Tempo.

Thank you for reading, and let's connect!

Related posts

All posts

Let's work together

Whether it's a talk, a project or a question about something I wrote, email is the fastest way to reach me.

[email protected]
Speaking
Conferences, meetups, podcasts and panels on PHP, backend architecture, DevOps and team leadership.
Mentoring
Developers growing into backend, DevOps or their first lead role.
Consulting
Architecture reviews, team processes and scaling problems that need a second opinion.
Collaboration
Side projects and partnerships that solve a real problem.