{# Server tab "database": one card per monitored database from smp.database. Context: s, smp, rules, fmt_bytes, latest_custom #} {% set dbs = smp.get('database') or [] %} {% if dbs %}
{% for d in dbs %}{% set r = d.get('redis') or {} %}{% set maxc = d.get('max_connections') or 0 %}{% set conns = d.get('connections') or 0 %}{% set cpct = (100.0 * conns / maxc) if maxc else none %}

{{ d.get('system') or 'db' }} {{ d.get('name') }}

{% if d.get('ok') is sameas false %}unreachable{% else %}ok{% endif %}{% if d.get('version') %} v{{ d.version }}{% endif %}
{% if d.get('ok') is sameas false %}
{{ d.get('error') or 'The agent could not query this database.' }}
{% endif %} {% if d.get('system') == 'redis' or r %}
Memory
{{ fmt_bytes(r.get('used_memory')) }}{% if r.get('maxmemory') %} / {{ fmt_bytes(r.maxmemory) }}{% endif %}
Hit rate
{{ '%.1f'|format(r.hit_rate_pct) if r.get('hit_rate_pct') is not none else '—' }}%
Clients
{{ r.get('connected_clients') if r.get('connected_clients') is not none else '—' }}
Ops/s
{{ r.get('ops_per_sec') if r.get('ops_per_sec') is not none else '—' }}
Keys
{{ r.get('keyspace') if r.get('keyspace') is not none else '—' }}
Evicted
{{ r.get('evicted_keys') or 0 }}
{% else %}
Connections {% if cpct is not none %}{{ conns }} / {{ maxc }} ({{ '%.0f'|format(cpct) }}%){% else %}{{ conns }}{% endif %} · {{ d.get('active_queries') or 0 }} active · {{ d.get('waiting_locks') or 0 }} waiting on locks
{% if cpct is not none %}
{% endif %}
TPS
{{ '%.1f'|format(d.tps) if d.get('tps') is not none else '—' }}
Cache hit
{{ '%.1f'|format(d.cache_hit_pct) if d.get('cache_hit_pct') is not none else '—' }}%
Repl. lag
{% if d.get('replication_lag_s') is not none %}{{ '%.1f'|format(d.replication_lag_s) }} s{% else %}n/a{% endif %}
Size
{{ fmt_bytes(d.get('db_size_bytes')) }}
{{ d.get('slow_queries') or 0 }} slow queries · {{ d.get('deadlocks') or 0 }} deadlocks since the last sample
{% if d.get('top_queries') %}
{% for q in d.top_queries[:10] %}{% endfor %}
Top queries (by total time)CallsMeanTotal
{{ (q.get('query') or '')[:140] }}{{ q.get('calls') }}{{ '%.1f'|format(q.mean_ms) if q.get('mean_ms') is not none else '—' }} ms{{ '%.0f'|format(q.total_ms) if q.get('total_ms') is not none else '—' }} ms
{% else %}
No query statistics: enable pg_stat_statements (PostgreSQL) or performance_schema (MySQL) to see the slowest queries.
{% endif %} {% endif %}
Charted as custom metrics db:{{ d.get('name') }}:connections, tps, cache_hit_pct, replication_lag_s — set limits under Alert rules → Custom metric limits.
{% endfor %}
{% else %}
No database configured on this server's agent yet.
{% endif %}

Configure database monitoring

Credentials stay on the server: add a databases list to /etc/vigil-agent.json and restart the agent (systemctl restart vigil-server-agent). The agent uses the local CLI clients (psql, mysql, redis-cli) with 10 s timeouts and never sends passwords to Vigil.

{
  "databases": [
    {"system": "postgresql", "name": "main",  "dsn": "postgresql://vigil:secret@127.0.0.1:5432/app"},
    {"system": "mysql",      "name": "shop",  "dsn": "mysql://vigil:secret@127.0.0.1:3306/shop"},
    {"system": "redis",      "name": "cache", "dsn": "redis://:secret@127.0.0.1:6379/0"}
  ]
}

PostgreSQL: a read-only role is enough (GRANT pg_monitor TO vigil;). For the top-queries table enable pg_stat_statements (shared_preload_libraries = 'pg_stat_statements', then CREATE EXTENSION pg_stat_statements;). MySQL: GRANT PROCESS, REPLICATION CLIENT, SELECT ON performance_schema.* TO vigil. Alerts: unreachable database, connections ≥ {{ rules.get('db_connections_pct', 90) }}% of max, cache hit < {{ rules.get('db_cache_hit_pct', 90) }}%, replication lag ≥ {{ rules.get('db_replication_lag_s', 30) }} s (change under Alert rules).