Add a statistics pane to the webhook page (closes #368)
check / check (push) Failing after 1m15s

Each webhook's event database keeps one row of running totals: events,
deliveries and failures, and how many of each retention removed.
Storing an event, creating a delivery, a delivery becoming failed and
the retention sweep each update it in the transaction that writes or
deletes the rows it counts. Deliveries get a finished_at column, the
last column of the status index, so the last-10-minutes and
last-24-hours figures are index-range counts. The pane is its own
template, included at the top of the page.

The schema changes in place with nothing back-filled, so an existing
database must be recreated.

Model: opus-5-5
This commit is contained in:
2026-10-01 19:31:49 +00:00
parent b79e4649a1
commit 2036d0fc80
21 changed files with 1200 additions and 50 deletions
+52 -5
View File
@@ -123,7 +123,7 @@ func TestEventTierQueriesUseTheirIndexes(t *testing.T) {
Order("attempt_num ASC").Find(&results),
"idx_delivery_results_delivery_id (delivery_id=? AND deleted_at=?)")
// Retention's three deletes (reapExpired), whose subqueries are built
// Retention's deletes (deleteExpired), whose subqueries are built
// afresh for each statement as it builds them.
expiredEventIDs := func() *gorm.DB {
return dry.Model(&database.Event{}).Select("id").
@@ -135,6 +135,14 @@ func TestEventTierQueriesUseTheirIndexes(t *testing.T) {
Select("id").Where("event_id IN (?)", expiredEventIDs()),
).Delete(&database.DeliveryResult{}),
"idx_delivery_results_delivery_id (delivery_id=?)", byEvent, byAge)
var failed struct{ Count int64 }
assertPlanUses(t, db, dry.Unscoped().Model(&database.Delivery{}).
Select("count(CASE WHEN status = ? THEN 1 END) AS count",
database.DeliveryStatusFailed).
Where("event_id IN (?)", expiredEventIDs()).
Take(&failed),
"idx_deliveries_event_id (event_id=?)", byAge)
assertPlanUses(t, db, dry.Unscoped().Where(
"event_id IN (?)", expiredEventIDs(),
).Delete(&database.Delivery{}),
@@ -142,6 +150,36 @@ func TestEventTierQueriesUseTheirIndexes(t *testing.T) {
assertPlanUses(t, db, dry.Unscoped().Where(
"created_at < ?", cutoff,
).Delete(&database.Event{}), "idx_events_created_at (created_at<?)")
// The webhook page's statistics (readEventStats in the handlers):
// deliveries in progress, deliveries finished and events received
// since a time, and the newest event, which must come straight
// off an index rather than from sorting every event.
var (
count int64
newest []time.Time
)
assertPlanUses(t, db, dry.Model(&database.Delivery{}).
Where("status IN ?", []database.DeliveryStatus{
database.DeliveryStatusPending,
database.DeliveryStatusRetrying,
}).Count(&count), byStatus)
assertPlanUses(t, db, dry.Model(&database.Delivery{}).
Where("status = ? AND finished_at >= ?",
database.DeliveryStatusFailed, cutoff).Count(&count),
"idx_deliveries_status "+
"(status=? AND deleted_at=? AND finished_at>?)")
assertPlanUses(t, db, dry.Model(&database.Event{}).
Where("created_at >= ?", cutoff).Count(&count),
"idx_events_deleted_at_created_at "+
"(deleted_at=? AND created_at>?)")
newestEvent := dry.Model(&database.Event{}).
Order("created_at DESC").Limit(1).Pluck("created_at", &newest)
assertPlanUses(t, db, newestEvent,
"idx_events_deleted_at_created_at (deleted_at=?)")
assert.NotContains(t, queryPlan(t, db, newestEvent), "TEMP B-TREE")
}
// assertPlanUses asserts that SQLite's plan for a statement GORM built
@@ -152,6 +190,18 @@ func assertPlanUses(
) {
t.Helper()
plan := queryPlan(t, db, built)
for _, index := range indexes {
assert.Contains(t, plan, index, built.Statement.SQL.String())
}
}
// queryPlan returns SQLite's plan for a statement GORM built in a dry
// run, run with the same SQL and arguments GORM would send.
func queryPlan(t *testing.T, db, built *gorm.DB) string {
t.Helper()
var plan []struct{ Detail string }
require.NoError(t, db.Raw(
@@ -159,8 +209,5 @@ func assertPlanUses(
built.Statement.Vars...,
).Scan(&plan).Error)
for _, index := range indexes {
assert.Contains(t, fmt.Sprint(plan), index,
built.Statement.SQL.String())
}
return fmt.Sprint(plan)
}