mariadb_exporter 0.6.0

MariaDB metric exporter for Prometheus
Documentation
[![Test & Build](https://github.com/nbari/mariadb_exporter/actions/workflows/build.yml/badge.svg)](https://github.com/nbari/mariadb_exporter/actions/workflows/build.yml)
[![codecov](https://codecov.io/gh/nbari/mariadb_exporter/graph/badge.svg?token=LR19CK9679)](https://codecov.io/gh/nbari/mariadb_exporter)
[![Crates.io](https://img.shields.io/crates/v/mariadb_exporter.svg)](https://crates.io/crates/mariadb_exporter)
[![License](https://img.shields.io/crates/l/mariadb_exporter.svg)](LICENSE)

# mariadb_exporter

MariaDB metrics exporter for Prometheus written in Rust.

## Features

* **Modular collectors** – Enable only what you need; heavy/optional collectors stay off by default.
* **Compatibility** – Metric names align with Prometheus `mysqld_exporter` (prefixed `mariadb_`).
* **Lean defaults** – Essential availability, InnoDB, and replication metrics enabled by default; optional collectors opt-in.
* **Low footprint** – Designed to minimize cardinality and avoid expensive scans.
* **Resilient** – Always serves `/metrics` (HTTP 200) even when MariaDB is unreachable. During an outage, `mariadb_up` becomes `0`, and DB-dependent metrics are omitted to avoid stale data.

## Download or build

Install via Cargo:

```bash
cargo install mariadb_exporter
```

## Usage

### Recommended Setup (Secure)

**Best practice: Use Unix socket with dedicated user**

Create the exporter user with minimal privileges:

```sql
-- Create user for local socket connection only with connection limit
CREATE USER 'exporter'@'localhost' IDENTIFIED BY '' WITH MAX_USER_CONNECTIONS 3;

-- Grant minimal required permissions for all collectors
GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'exporter'@'localhost';

FLUSH PRIVILEGES;
```

Run the exporter:

```bash
mariadb_exporter --dsn "mysql:///mysql?socket=/var/run/mysqld/mysqld.sock&user=exporter"
```

**Why this is secure:**
- ✅ No password needed (socket authentication)
- ✅ User restricted to `localhost` only (no network access)
- ✅ Minimal privileges (read-only + monitoring)
- ✅ Connection limit prevents resource exhaustion
- ✅ No exposure to network attacks

### Alternative: TCP Connection

For remote monitoring or testing:

```bash
mariadb_exporter --dsn "mysql://exporter:password@host:3306/mysql"
```

Create user for network access:

```sql
CREATE USER 'exporter'@'%' IDENTIFIED BY 'strong_password_here' WITH MAX_USER_CONNECTIONS 3;
GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'exporter'@'%';
FLUSH PRIVILEGES;
```

### Common DSN Formats

* **Unix socket (recommended)**: `mysql:///mysql?socket=/var/run/mysqld/mysqld.sock&user=exporter`
* TCP: `mysql://user:password@host:3306/database`
* TLS required: `mysql://user:password@host/mysql?ssl-mode=REQUIRED`
* TLS verify identity: `mysql://user:password@host/mysql?ssl-mode=VERIFY_IDENTITY&ssl-ca=/path/to/ca.pem`

### Change Port

Default port is `9306`:

```bash
mariadb_exporter --dsn "..." --port 9187
```

## Available collectors

Collectors are toggled with `--collector.<name>` or `--no-collector.<name>`.

* `--collector.default` (enabled) – Core status (uptime, threads, connections, traffic), InnoDB basics, replication basics, binlog stats, config flags, version, `mariadb_up`, audit log enabled status.
* `--collector.exporter` (enabled) – Exporter self-metrics (process, scrape, cardinality).
* `--collector.innodb` – Advanced InnoDB metrics from `SHOW ENGINE INNODB STATUS`: LSN tracking, checkpoint age, active transactions, semaphore waits, adaptive hash index stats.
* `--collector.tls` – TLS session + cipher info.
* `--collector.query_response_time` – Buckets from `query_response_time` plugin.
* `--collector.statements` – Statement digest summaries/top latency from `performance_schema`.
* `--collector.schema` – Table size/row estimates (largest 20 non-system tables).
* `--collector.replication` – Replica role/lag/thread status, relay log size/pos, binlog file count, and per-channel replication metrics for multi-source replicas.
* `--collector.locks` – Metadata/table lock waits from `performance_schema`.
* `--collector.metadata``metadata_lock_info` table counts.
* `--collector.userstat` – Per-user stats (requires `@@userstat=1` and `USER_STATISTICS`).

### Enabled by default

* `default`
* `exporter`

Everything else is opt-in.

Replication lag semantics:
* `mariadb_slave_status_seconds_behind_master` and `mariadb_replica_seconds_behind_master_seconds` use `-1` for unknown lag (`NULL`, stopped replication, query failure, or not a replica).
* On multi-source replicas, aggregate lag is the worst known channel lag (max), and aggregate `*_io_running` / `*_sql_running` are `1` only when all channels are running.
* Per-channel replication metrics are exported as `mariadb_replica_*_by_channel{channel_name,connection_name}`.

### Enable all collectors

To enable **all collectors** for maximum visibility:

```bash
mariadb_exporter \
  --dsn "mysql:///mysql?socket=/var/run/mysqld/mysqld.sock&user=exporter" \
  --collector.default \
  --collector.exporter \
  --collector.innodb \
  --collector.tls \
  --collector.query_response_time \
  --collector.statements \
  --collector.schema \
  --collector.replication \
  --collector.locks \
  --collector.metadata \
  --collector.userstat
```

Or using environment variables:

```bash
export MARIADB_EXPORTER_DSN="mysql:///mysql?socket=/var/run/mysqld/mysqld.sock&user=exporter"
export MARIADB_EXPORTER_PORT="9306"

mariadb_exporter \
  --collector.default \
  --collector.exporter \
  --collector.innodb \
  --collector.tls \
  --collector.query_response_time \
  --collector.statements \
  --collector.schema \
  --collector.replication \
  --collector.locks \
  --collector.metadata \
  --collector.userstat
```

**Note:** Some collectors require additional privileges or database configuration:
- `innodb` – Requires `PROCESS` privilege (included in recommended setup)
- `tls` – Only shows data if TLS/SSL is enabled
- `query_response_time` – Requires `query_response_time` plugin enabled
- `statements` – Requires `performance_schema` enabled
- `schema` – Queries `information_schema` (can be slow on large databases)
- `locks`, `metadata` – Require `performance_schema` enabled
- `userstat` – Requires `@@userstat=1` and `USER_STATISTICS` enabled

### InnoDB Advanced Metrics

The `--collector.innodb` provides deep visibility into InnoDB internals by parsing `SHOW ENGINE INNODB STATUS`:

**Metrics exposed:**
* `mariadb_innodb_lsn_current` – Current log sequence number
* `mariadb_innodb_lsn_flushed` – LSN flushed to disk
* `mariadb_innodb_lsn_checkpoint` – Last checkpoint LSN
* `mariadb_innodb_checkpoint_age_bytes` – Uncheckpointed bytes (LSN current - checkpoint)
* `mariadb_innodb_active_transactions` – Count of active InnoDB transactions
* `mariadb_innodb_semaphore_waits_total` – Semaphore wait events (internal contention)
* `mariadb_innodb_semaphore_wait_time_ms_total` – Total semaphore wait time
* `mariadb_innodb_adaptive_hash_searches_total` – Adaptive hash index hits
* `mariadb_innodb_adaptive_hash_searches_btree_total` – AHI misses requiring B-tree lookup

**Use cases:**
* Monitor checkpoint age to prevent log file overflow
* Track LSN progression for write workload analysis
* Detect long-running transactions
* Identify internal InnoDB contention (semaphore waits)
* Measure adaptive hash index efficiency

**Requirements:**
* `PROCESS` privilege (for `SHOW ENGINE INNODB STATUS`)

**Enable with:**
```bash
mariadb_exporter --collector.default --collector.innodb
```

## Project layout

```
mariadb_exporter
├── bin
├── cli
├── collectors
│   ├── config.rs
│   ├── default
│   ├── exporter
│   ├── innodb
│   ├── locks
│   ├── metadata
│   ├── mod.rs
│   ├── query_response_time
│   ├── register_macro.rs
│   ├── registry.rs
│   ├── replication
│   ├── schema
│   ├── statements
│   ├── tls
│   ├── userstat
│   └── util.rs
└── src/lib.rs
```

Each collector lives in its own subdirectory for clarity and easy extension.

## Testing

Run tests:

```bash
cargo test
```

Run with container-backed integration (requires a container runtime):

```bash
just test
```

If you use rootless Podman, export `DOCKER_HOST` so `testcontainers` can connect:

```bash
export DOCKER_HOST="unix:///run/user/$UID/podman/podman.sock"
```

Test with Unix socket connection (production-like setup):

```bash
# Test with combined MariaDB + exporter container (most realistic)
just test-socket
```

Lint:

```bash
cargo clippy --all-targets --all-features
```

### Socket Connection Testing

For detailed information on testing with Unix socket connections, see [TESTING_SOCKET.md](TESTING_SOCKET.md).

Quick start:

```bash
# Test with combined MariaDB + exporter container (most realistic)
just test-socket
```

## Developer Guidelines

### Architecture

The project follows a modular collector architecture:

```
mariadb_exporter/
├── bin/                 # Binary entry point
├── cli/                 # CLI argument parsing
├── collectors/          # All metric collectors
│   ├── mod.rs          # Collector trait and registration
│   ├── registry.rs     # Collector orchestration
│   ├── config.rs       # Collector enable/disable logic
│   └── */              # Individual collector modules
└── exporter/           # HTTP server (Axum)
```

### Adding a New Collector

1. Create a subdirectory under `src/collectors/` with a `mod.rs`
2. Define a struct implementing the `Collector` trait:
   - `register_metrics(&self, registry: &Registry)` - Register Prometheus metrics
   - `collect(&self, pool: &MySqlPool)` - Fetch data and update metrics (async)
   - `enabled_by_default(&self)` - Whether collector runs by default
3. Add ONE line to `register_collectors!` macro in `src/collectors/mod.rs`:
   ```rust
   register_collectors! {
       // ... existing collectors ...
       your_collector => YourCollector,
   }
   ```

The macro automatically generates all registration boilerplate.

### Strict Linting Rules

This project enforces strict clippy lints (see `Cargo.toml`):

- **DENY**: `unwrap_used`, `expect_used`, `panic`, `indexing_slicing`, `await_holding_lock`
- Use `?` for error propagation, never `.unwrap()` or `.expect()`
- Use `.get()` instead of `[index]` for slicing
- Use pattern matching or `.ok()` instead of `.unwrap()`

Exceptions are allowed only in test code with `#[allow(clippy::unwrap_used)]`.

### Testing

```bash
# Run unit tests
cargo test

# Run with container integration
just test

# Lint (must pass)
cargo clippy --all-targets --all-features

# Validate Grafana dashboard
just validate-dashboard
```

### Dashboard Development

When adding metrics to the Grafana dashboard:

1. Ensure metrics are exported by collectors
2. Add panels following existing structure (clean, professional, no emojis)
3. Use template variables (`$job`, `$instance`)
4. Add clear descriptions (Goal/Action format)
5. Validate before committing: `just validate-dashboard`

See [grafana/README.md](grafana/README.md) for detailed dashboard documentation.

### Commit Guidelines

- Run tests before committing: `cargo test`
- Run clippy: `cargo clippy --all-targets --all-features`
- Validate dashboard if modified: `just validate-dashboard`
- Keep commit messages clear and descriptive

## Notes

* User statistics: enable with `SET GLOBAL userstat=ON;` (or `@@userstat=1`) to expose `userstat` metrics.
* Metadata locks: load `metadata_lock_info` plugin for the `metadata` collector.
* Performance schema is needed for statements/locks collectors to return data.
* Optional collectors skip gracefully when prerequisites aren't present.