Add PgBouncer to dev and PgBouncer stats to admin Infra page

- Add pgbouncer service to docker-compose-dev.yml with dev config
- Route DATABASE_URL through pgbouncer:6432 in dev (matches prod)
- Add PgBouncer pools and stats tables to /admin/infra with color-coded
  avg query/xact times and client waiting indicators
- php, messenger, cron now depend on pgbouncer instead of database directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-26 10:33:51 +01:00
parent 1a336edac5
commit 74c10a60f5
7 changed files with 204 additions and 5 deletions

View File

@@ -109,6 +109,100 @@
</div>
{% endfor %}
</div>
{# PgBouncer #}
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">PgBouncer</h2>
<div class="flex flex-wrap gap-6 mb-8">
{% if pgbouncer.connected %}
{# Pools #}
<div class="flex-1 min-w-[500px]">
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Pools</h2>
{% if pgbouncer.pools|length > 0 %}
<div class="overflow-x-auto">
<table class="admin-table w-full">
<thead>
<tr>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Database</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">User</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Mode</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Clients actifs</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Clients en attente</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Serveurs actifs</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Serveurs idle</th>
</tr>
</thead>
<tbody>
{% for pool in pgbouncer.pools %}
<tr>
<td class="font-black text-sm">{{ pool.database }}</td>
<td class="text-sm">{{ pool.user }}</td>
<td class="text-center"><span class="admin-badge-green text-xs font-black uppercase">{{ pool.pool_mode }}</span></td>
<td class="text-center font-black">{{ pool.cl_active }}</td>
<td class="text-center font-black {% if pool.cl_waiting > 0 %}text-yellow-600{% endif %}">{{ pool.cl_waiting }}</td>
<td class="text-center font-black">{{ pool.sv_active }}</td>
<td class="text-center font-black text-gray-400">{{ pool.sv_idle }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-gray-400">Aucun pool actif.</p>
{% endif %}
</div>
</div>
{# Stats #}
<div class="flex-1 min-w-[500px]">
<div class="admin-card">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Stats</h2>
{% if pgbouncer.stats|length > 0 %}
<div class="overflow-x-auto">
<table class="admin-table w-full">
<thead>
<tr>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Database</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Transactions</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Requetes</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Avg xact (us)</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Avg query (us)</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Recu</th>
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Envoye</th>
</tr>
</thead>
<tbody>
{% for stat in pgbouncer.stats %}
<tr>
<td class="font-black text-sm">{{ stat.database }}</td>
<td class="text-right text-sm font-bold">{{ stat.total_xact_count|number_format(0, '.', ' ') }}</td>
<td class="text-right text-sm font-bold">{{ stat.total_query_count|number_format(0, '.', ' ') }}</td>
<td class="text-right text-sm {% if stat.avg_xact_time > 100000 %}text-red-600{% elseif stat.avg_xact_time > 10000 %}text-yellow-600{% else %}text-green-600{% endif %} font-bold">{{ stat.avg_xact_time|number_format(0, '.', ' ') }}</td>
<td class="text-right text-sm {% if stat.avg_query_time > 50000 %}text-red-600{% elseif stat.avg_query_time > 5000 %}text-yellow-600{% else %}text-green-600{% endif %} font-bold">{{ stat.avg_query_time|number_format(0, '.', ' ') }}</td>
<td class="text-right text-sm text-gray-500">{{ (stat.total_received / 1048576)|number_format(1) }} MB</td>
<td class="text-right text-sm text-gray-500">{{ (stat.total_sent / 1048576)|number_format(1) }} MB</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-gray-400">Aucune stat disponible.</p>
{% endif %}
</div>
</div>
{% else %}
<div class="flex-1 min-w-[400px]">
<div class="admin-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-black uppercase tracking-widest">PgBouncer</h2>
<span class="admin-badge-red text-xs font-black uppercase">Deconnecte</span>
</div>
<p class="text-sm text-red-500 font-bold">{{ pgbouncer.error }}</p>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% macro row(label, value) %}