check / check (push) Waiting to run
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
213 lines
4.9 KiB
Go
213 lines
4.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
)
|
|
|
|
// The spans of the two recent windows the statistics pane reports on:
|
|
// the last 10 minutes and the last 24 hours.
|
|
const (
|
|
shortWindow = 10 * time.Minute
|
|
longWindow = 24 * time.Hour
|
|
)
|
|
|
|
// percent turns a fraction into a percentage.
|
|
const percent = 100
|
|
|
|
// WebhookStats holds the figures in the statistics pane at the top of
|
|
// the webhook page.
|
|
type WebhookStats struct {
|
|
Entrypoints int
|
|
ActiveEntrypoints int
|
|
Targets int
|
|
ActiveTargets int
|
|
|
|
// Totals holds the lifetime counts of events, deliveries and
|
|
// failures, and how many of each retention has removed.
|
|
Totals database.Totals
|
|
|
|
// InProgress counts the deliveries still pending or retrying.
|
|
InProgress int64
|
|
|
|
// LastEventAt is when the newest stored event arrived, or nil when
|
|
// none is stored.
|
|
LastEventAt *time.Time
|
|
|
|
Last10Minutes RecentWindow
|
|
Last24Hours RecentWindow
|
|
}
|
|
|
|
// RecentWindow holds what happened in one recent window: the events
|
|
// received in it, and the deliveries that became delivered or failed in
|
|
// it.
|
|
type RecentWindow struct {
|
|
Events int64
|
|
Delivered int64
|
|
Failed int64
|
|
}
|
|
|
|
// FailurePercent is the share of the deliveries finished in the window
|
|
// that failed, or a dash when none finished. Deliveries still pending
|
|
// or retrying are not counted either way.
|
|
func (w RecentWindow) FailurePercent() string {
|
|
finished := w.Delivered + w.Failed
|
|
if finished == 0 {
|
|
return "—"
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"%.1f%%", percent*float64(w.Failed)/float64(finished),
|
|
)
|
|
}
|
|
|
|
// loadWebhookStats gathers the figures for the statistics pane from the
|
|
// webhook's entrypoints and targets, as the page has already loaded
|
|
// them, and from its event database. It returns nil, and logs why, when
|
|
// the event database cannot be read.
|
|
func (h *Handlers) loadWebhookStats(
|
|
webhookID string,
|
|
entrypoints []database.Entrypoint,
|
|
targets []database.Target,
|
|
) *WebhookStats {
|
|
stats := &WebhookStats{
|
|
Entrypoints: len(entrypoints),
|
|
Targets: len(targets),
|
|
}
|
|
|
|
for i := range entrypoints {
|
|
if entrypoints[i].Active {
|
|
stats.ActiveEntrypoints++
|
|
}
|
|
}
|
|
|
|
for i := range targets {
|
|
if targets[i].Active {
|
|
stats.ActiveTargets++
|
|
}
|
|
}
|
|
|
|
// Opening an event database that does not exist would create it,
|
|
// and it would hold nothing to count.
|
|
if !h.dbMgr.DBExists(webhookID) {
|
|
return stats
|
|
}
|
|
|
|
webhookDB, err := h.dbMgr.GetDB(webhookID)
|
|
if err == nil {
|
|
err = readEventStats(webhookDB, time.Now(), stats)
|
|
}
|
|
|
|
if err != nil {
|
|
h.log.Error(
|
|
"failed to read webhook statistics",
|
|
"webhook_id", webhookID,
|
|
"error", err,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// readEventStats fills in the figures that come from the webhook's
|
|
// event database. None of them reads every stored row: the totals are
|
|
// one row, and every other figure is read from an index, over only the
|
|
// rows it counts.
|
|
func readEventStats(
|
|
db *gorm.DB, now time.Time, stats *WebhookStats,
|
|
) error {
|
|
err := db.Take(&stats.Totals).Error
|
|
if err != nil {
|
|
return fmt.Errorf("reading running totals: %w", err)
|
|
}
|
|
|
|
err = db.Model(&database.Delivery{}).
|
|
Where("status IN ?", []database.DeliveryStatus{
|
|
database.DeliveryStatusPending,
|
|
database.DeliveryStatusRetrying,
|
|
}).
|
|
Count(&stats.InProgress).Error
|
|
if err != nil {
|
|
return fmt.Errorf("counting deliveries in progress: %w", err)
|
|
}
|
|
|
|
var newest []time.Time
|
|
|
|
err = db.Model(&database.Event{}).
|
|
Order("created_at DESC").
|
|
Limit(1).
|
|
Pluck("created_at", &newest).Error
|
|
if err != nil {
|
|
return fmt.Errorf("reading newest event time: %w", err)
|
|
}
|
|
|
|
if len(newest) > 0 {
|
|
stats.LastEventAt = &newest[0]
|
|
}
|
|
|
|
stats.Last10Minutes, err = readRecentWindow(
|
|
db, now.Add(-shortWindow),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stats.Last24Hours, err = readRecentWindow(
|
|
db, now.Add(-longWindow),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// readRecentWindow counts the events received, and the deliveries that
|
|
// became delivered or failed, since the given time.
|
|
func readRecentWindow(
|
|
db *gorm.DB, since time.Time,
|
|
) (RecentWindow, error) {
|
|
var w RecentWindow
|
|
|
|
err := db.Model(&database.Event{}).
|
|
Where("created_at >= ?", since).
|
|
Count(&w.Events).Error
|
|
if err != nil {
|
|
return w, fmt.Errorf("counting recent events: %w", err)
|
|
}
|
|
|
|
w.Delivered, err = countFinishedSince(
|
|
db, database.DeliveryStatusDelivered, since,
|
|
)
|
|
if err != nil {
|
|
return w, err
|
|
}
|
|
|
|
w.Failed, err = countFinishedSince(
|
|
db, database.DeliveryStatusFailed, since,
|
|
)
|
|
|
|
return w, err
|
|
}
|
|
|
|
// countFinishedSince counts the deliveries that reached the given
|
|
// final status since the given time.
|
|
func countFinishedSince(
|
|
db *gorm.DB, status database.DeliveryStatus, since time.Time,
|
|
) (int64, error) {
|
|
var n int64
|
|
|
|
err := db.Model(&database.Delivery{}).
|
|
Where("status = ? AND finished_at >= ?", status, since).
|
|
Count(&n).Error
|
|
if err != nil {
|
|
return 0, fmt.Errorf(
|
|
"counting deliveries %s recently: %w", status, err,
|
|
)
|
|
}
|
|
|
|
return n, nil
|
|
}
|