feat(metrics): per-app observability dashboard over execution_logs (A2)

Aggregates the existing execution_logs table (no hot-path instrumentation):
ExecutionLogRepository::summarize_for_app returns counts, error rate, latency
avg/p50/p95 (percentile_cont), by-status/by-source breakdowns, and an hourly
series over a trailing window (clamped 1h..90d). New metrics_api router at
GET /api/v1/admin/apps/{id}/metrics?window= (AppLogRead), and a dashboard
Metrics tab (summary cards + an inline-SVG per-hour bar chart, no JS dep).

Pinned by a manager-core test asserting the exact rollup (percentiles included)
and the empty-app zero case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-15 21:42:18 +02:00
parent 35345e62de
commit 6b2dcd41a6
10 changed files with 774 additions and 20 deletions

View File

@@ -218,6 +218,33 @@ export interface ExecutionLog {
created_at: string;
}
/** A2: one `(key, count)` row of a metrics breakdown. */
export interface MetricsCount {
key: string;
count: number;
}
/** A2: one hour-bucket of the metrics time series. */
export interface MetricsBucket {
ts: string;
total: number;
errors: number;
}
/** A2: the observability rollup for one app over a trailing window. */
export interface MetricsSummary {
window_hours: number;
total: number;
errors: number;
error_rate: number;
avg_duration_ms: number;
p50_duration_ms: number;
p95_duration_ms: number;
by_status: MetricsCount[];
by_source: MetricsCount[];
series: MetricsBucket[];
}
export interface CreateScriptInput {
app_id: string;
name: string;
@@ -1169,6 +1196,14 @@ export const api = {
)
},
metrics: {
/** A2: aggregate execution metrics for an app over the trailing window (hours). */
summary: (idOrSlug: string, windowHours = 24) =>
adminRequest<MetricsSummary>(
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/metrics?window=${windowHours}`
)
},
deadLetters: {
count: (idOrSlug: string) =>
adminRequest<{ unresolved: number }>(