check / check (push) Waiting to run
The target list showed nothing about how a target's deliveries were going. Each target now shows Delivered and Failed, each in total and in the last 24 hours. The totals are the per-target running totals kept for the statistics pane, so retention does not reduce them; the 24-hour figures are one count over the deliveries' final-status index, for all of the webhook's targets at once. Pending and retrying deliveries count in neither. If the event database cannot be read, each row says so instead of showing zeros. The archive details and Download button on database targets are kept. Model: opus-5-5
181 lines
4.8 KiB
Go
181 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"gorm.io/gorm"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
// TargetRowView is one row of the target list on a webhook's page.
|
|
type TargetRowView struct {
|
|
delivery.TargetView
|
|
|
|
// Deliveries counts the target's delivered and failed deliveries,
|
|
// and is nil when the webhook's event database could not be read.
|
|
Deliveries *TargetDeliveries
|
|
|
|
// Archive is a database target's archive file, and nil for a target
|
|
// of any other type.
|
|
Archive *ArchiveFileView
|
|
}
|
|
|
|
// TargetDeliveries is how many of a target's deliveries became
|
|
// delivered and how many failed: in total, which retention does not
|
|
// reduce, and in the last 24 hours. Deliveries still pending or
|
|
// retrying count in neither.
|
|
type TargetDeliveries struct {
|
|
Delivered int64
|
|
Failed int64
|
|
|
|
DeliveredLast24Hours int64
|
|
FailedLast24Hours int64
|
|
}
|
|
|
|
// ArchiveFileView is what a database target's row shows about its
|
|
// archive file.
|
|
type ArchiveFileView struct {
|
|
Name string
|
|
|
|
// Note stands in for the size and the last write when there are
|
|
// none to show, and is empty when there are.
|
|
Note string
|
|
|
|
// Size is the size on disk. Written is how long ago the file was
|
|
// last written, and WrittenUTC the full time the page shows on
|
|
// hover.
|
|
Size string
|
|
Written string
|
|
WrittenUTC string
|
|
}
|
|
|
|
// targetRows projects a webhook's targets for the target list on its
|
|
// page.
|
|
func (h *Handlers) targetRows(
|
|
webhook *database.Webhook, targets []database.Target,
|
|
) []TargetRowView {
|
|
views := delivery.NewTargetViews(targets)
|
|
rows := make([]TargetRowView, len(views))
|
|
|
|
deliveries, err := h.loadTargetDeliveries(webhook.ID)
|
|
if err != nil {
|
|
h.log.Error(
|
|
"failed to read target delivery counts",
|
|
"webhook_id", webhook.ID,
|
|
"error", err,
|
|
)
|
|
}
|
|
|
|
// NewTargetViews returns one view per target, in order.
|
|
for i := range views {
|
|
rows[i].TargetView = views[i]
|
|
|
|
if err == nil {
|
|
counts := deliveries[targets[i].ID]
|
|
rows[i].Deliveries = &counts
|
|
}
|
|
|
|
if targets[i].Type == database.TargetTypeDatabase {
|
|
rows[i].Archive = h.archiveFileView(webhook, &targets[i])
|
|
}
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
// loadTargetDeliveries reads the delivery counts of a webhook's targets
|
|
// from its event database, keyed by target. A target with no deliveries
|
|
// is left out, and so is every target when the event database does not
|
|
// exist yet, since opening it would create it.
|
|
func (h *Handlers) loadTargetDeliveries(
|
|
webhookID string,
|
|
) (map[string]TargetDeliveries, error) {
|
|
if !h.dbMgr.DBExists(webhookID) {
|
|
return map[string]TargetDeliveries{}, nil
|
|
}
|
|
|
|
webhookDB, err := h.dbMgr.GetDB(webhookID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return readTargetDeliveries(webhookDB, time.Now())
|
|
}
|
|
|
|
// readTargetDeliveries counts each target's deliveries that became
|
|
// delivered and those that failed: in total from the targets' running
|
|
// totals, and in the 24 hours before now from the deliveries' status
|
|
// index. Each is one query for all the targets, and neither reads every
|
|
// stored delivery.
|
|
func readTargetDeliveries(
|
|
db *gorm.DB, now time.Time,
|
|
) (map[string]TargetDeliveries, error) {
|
|
var totals []database.TargetTotals
|
|
|
|
err := db.Find(&totals).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading target totals: %w", err)
|
|
}
|
|
|
|
lastDay, err := finishedByTarget(db, now.Add(-longWindow))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byTarget := make(map[string]TargetDeliveries, len(totals))
|
|
|
|
for _, total := range totals {
|
|
byTarget[total.TargetID] = TargetDeliveries{
|
|
Delivered: total.Delivered,
|
|
Failed: total.Failed,
|
|
}
|
|
}
|
|
|
|
for _, finished := range lastDay {
|
|
counts := byTarget[finished.TargetID]
|
|
counts.DeliveredLast24Hours = finished.Delivered
|
|
counts.FailedLast24Hours = finished.Failed
|
|
byTarget[finished.TargetID] = counts
|
|
}
|
|
|
|
return byTarget, nil
|
|
}
|
|
|
|
// archiveFileView describes a database target's archive file from the
|
|
// file's metadata alone; the archive is never opened. The file is found
|
|
// by the name the archive writer uses, so it follows a rename of the
|
|
// webhook or the target.
|
|
func (h *Handlers) archiveFileView(
|
|
webhook *database.Webhook, target *database.Target,
|
|
) *ArchiveFileView {
|
|
path := delivery.ArchivePath(h.dbMgr, webhook, target)
|
|
view := &ArchiveFileView{Name: filepath.Base(path)}
|
|
|
|
file, err := delivery.StatArchive(path)
|
|
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
view.Note = "not created yet"
|
|
case err != nil:
|
|
h.log.Error(
|
|
"failed to read archive file metadata",
|
|
"target_id", target.ID,
|
|
"error", err,
|
|
)
|
|
|
|
view.Note = "could not be read"
|
|
default:
|
|
view.Size = humanize.Bytes(uint64(file.Size)) //nolint:gosec // never negative
|
|
view.Written = humanize.Time(file.Written)
|
|
view.WrittenUTC = file.Written.UTC().Format(time.DateTime) + " UTC"
|
|
}
|
|
|
|
return view
|
|
}
|