Files
Api-Gateway/app/templates/monitoring.html
T
Samuel AmarandClaude Haiku 4.5 77d7a50fa9 Initial commit: API gateway with admin console
- FastAPI async gateway with httpx proxying to multiple upstreams
- SQLite database with SQLAlchemy ORM
- Admin console: manage services, users, API keys, endpoint access
- Per-key, per-endpoint granular access control
- OpenAPI document sync and caching (5-minute TTL)
- Request/response logging with full transaction inspection
- In-memory rate limiting (per-key, fixed-window)
- Tiered log retention (7d payloads, 90d rows, incremental vacuum)
- TLS verification toggle per service (for self-signed certificates)
- Service connectivity validation with automatic endpoint refresh
- Request browser with filters and deep-link inspection
- Docker setup with persistent volume
- Modal forms for create/edit flows

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-29 14:00:22 +02:00

188 lines
7.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Monitoring · API Gateway{% endblock %}
{% block content %}
<h1>Usage Monitoring</h1>
<div class="row" style="margin-bottom:16px">
<div class="range-picker" role="group" aria-label="Time range" style="margin:0">
<button class="ghost" data-hours="1">1 h</button>
<button class="ghost" data-hours="6">6 h</button>
<button class="ghost" data-hours="24">24 h</button>
<button class="ghost" data-hours="72">3 days</button>
<button class="ghost" data-hours="168">7 days</button>
<button class="ghost" data-hours="720">30 days</button>
</div>
<select id="f-service" class="right" style="max-width:170px">
<option value="">All services</option>
{% for s in services %}<option value="{{ s.id }}">{{ s.name }}</option>{% endfor %}
</select>
<select id="f-user" style="max-width:150px">
<option value="">All users</option>
{% for u in users %}<option value="{{ u.id }}">{{ u.username }}</option>{% endfor %}
</select>
<select id="f-key" style="max-width:150px">
<option value="">All keys</option>
{% for k in keys %}<option value="{{ k.id }}">{{ k.name }}</option>{% endfor %}
</select>
</div>
<div class="tiles">
<div class="tile"><div class="label">Requests</div><div class="value" id="t-total"></div></div>
<div class="tile"><div class="label">Errors (5xx)</div><div class="value" id="t-errors"></div></div>
<div class="tile"><div class="label">Error rate</div><div class="value" id="t-rate"></div></div>
<div class="tile"><div class="label">Avg latency</div><div class="value" id="t-latency"></div></div>
</div>
<div class="grid cols-2">
<div class="card">
<h2>Requests over time</h2>
<div class="chart-box"><canvas id="chart-traffic"></canvas></div>
</div>
<div class="card">
<h2>Average latency over time (ms)</h2>
<div class="chart-box"><canvas id="chart-latency-ts"></canvas></div>
</div>
<div class="card">
<h2>Requests by service</h2>
<div class="chart-box"><canvas id="chart-services"></canvas></div>
</div>
<div class="card">
<h2>Responses by status code</h2>
<div class="chart-box"><canvas id="chart-status"></canvas></div>
</div>
<div class="card">
<h2>Requests by user</h2>
<div class="chart-box"><canvas id="chart-users"></canvas></div>
</div>
<div class="card">
<h2>Requests by endpoint</h2>
<div class="chart-box"><canvas id="chart-endpoints"></canvas></div>
</div>
<div class="card">
<h2>Average latency by service (ms)</h2>
<div class="chart-box"><canvas id="chart-latency"></canvas></div>
</div>
<div class="card">
<h2>Top API keys by usage</h2>
<div class="chart-box"><canvas id="chart-keys"></canvas></div>
</div>
</div>
<div class="card">
<h2>Latest 100 requests</h2>
<table>
<thead><tr><th>Time (UTC)</th><th>Service</th><th>Key</th><th>User</th><th>Endpoint</th><th>Request</th><th>Status</th><th>Latency</th><th>Client IP</th></tr></thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td class="strong">{{ log.service.name if log.service else "" }}</td>
<td>{{ log.api_key.name if log.api_key else "" }}</td>
<td>{{ log.api_key.user.username if log.api_key else "" }}</td>
<td>{% if log.endpoint %}<code>{{ log.endpoint.path }}</code>{% else %}{% endif %}</td>
<td><a href="/admin/requests?inspect={{ log.id }}">{{ log.method }} {{ log.path[:50] }}</a></td>
<td class="status-{{ log.status_code // 100 }}xx">{{ log.status_code }}</td>
<td>{{ "%.1f" | format(log.latency_ms) }} ms</td>
<td>{{ log.client_ip }}</td>
</tr>
{% else %}
<tr><td colspan="9">No requests logged yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
const charts = {};
let hours = 24;
function qs() {
const p = new URLSearchParams({ hours });
for (const [param, id] of [['service_id', 'f-service'], ['user_id', 'f-user'], ['key_id', 'f-key']]) {
const v = document.getElementById(id).value;
if (v) p.set(param, v);
}
return p.toString();
}
function barChart(id, labels, data, horizontal = false) {
if (charts[id]) charts[id].destroy();
charts[id] = new Chart(document.getElementById(id), {
type: 'bar',
data: { labels, datasets: [{ data, backgroundColor: GW.blue, borderRadius: 4, maxBarThickness: 26 }] },
options: {
maintainAspectRatio: false,
indexAxis: horizontal ? 'y' : 'x',
plugins: { legend: { display: false } },
scales: horizontal
? { x: { beginAtZero: true }, y: { grid: { display: false } } }
: { x: { grid: { display: false } }, y: { beginAtZero: true, ticks: { precision: 0 } } },
},
});
}
function lineChart(id, labels, datasets, showLegend) {
if (charts[id]) charts[id].destroy();
charts[id] = new Chart(document.getElementById(id), {
type: 'line',
data: { labels, datasets },
options: {
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: showLegend } },
scales: {
x: { grid: { display: false }, ticks: { maxTicksLimit: 14 } },
y: { beginAtZero: true },
},
},
});
}
const lineStyle = { borderWidth: 2, pointRadius: 0, pointHoverRadius: 4, tension: 0.3, spanGaps: true };
async function load() {
const query = qs();
const [sum, ts, lts, bs, st, lat, keys, byUser, byEndpoint] = await Promise.all([
'summary', 'timeseries', 'latency-timeseries', 'by-service', 'by-status',
'latency-by-service', 'top-keys', 'by-user', 'by-endpoint',
].map(ep => GW.fetchJSON(`/admin/api/stats/${ep}?${query}`)));
document.getElementById('t-total').textContent = sum.total_requests.toLocaleString();
document.getElementById('t-errors').textContent = sum.error_count.toLocaleString();
document.getElementById('t-rate').textContent = sum.error_rate + '%';
document.getElementById('t-latency').innerHTML = sum.avg_latency_ms + '<small> ms</small>';
lineChart('chart-traffic', ts.labels, [
{ label: 'Succeeded', data: ts.ok, borderColor: GW.blue, backgroundColor: GW.blue, ...lineStyle },
{ label: 'Errors (5xx)', data: ts.errors, borderColor: GW.red, backgroundColor: GW.red, ...lineStyle },
], true);
lineChart('chart-latency-ts', lts.labels, [
{ label: 'Avg latency (ms)', data: lts.avg_ms, borderColor: GW.blue, backgroundColor: GW.blue, ...lineStyle },
], false);
barChart('chart-services', bs.labels, bs.counts, true);
barChart('chart-status', st.labels, st.counts);
barChart('chart-users', byUser.labels, byUser.counts, true);
barChart('chart-endpoints', byEndpoint.labels, byEndpoint.counts, true);
barChart('chart-latency', lat.labels, lat.avg_ms, true);
barChart('chart-keys', keys.labels, keys.counts, true);
}
document.querySelectorAll('.range-picker button').forEach(b =>
b.addEventListener('click', () => {
hours = +b.dataset.hours;
document.querySelectorAll('.range-picker button').forEach(x =>
x.classList.toggle('active', x === b));
load();
}));
['f-service', 'f-user', 'f-key'].forEach(id =>
document.getElementById(id).addEventListener('change', load));
document.querySelector('.range-picker button[data-hours="24"]').classList.add('active');
load();
</script>
{% endblock %}