Event log: show only the events with a failed or a pending delivery (closes #390)
check / check (push) Successful in 3m19s
check / check (push) Successful in 3m19s
The event log gains three links above the list: All, Failed (N) and Pending (N), carried in a `show` query parameter; any other value shows every event. Failed lists the events with a failed delivery, Pending those with one pending or retrying, each once, under the full list's 50-row limit and order. A filtered list finds the matching deliveries through `idx_deliveries_status`, looks up their events by ID and sorts them, and reads the rows of only the events shown; its counts read the deliveries alone. The line beside the heading says what a filter counts, and Replay returns to the list it was pressed in. The README and the comments naming the query parameters the service reads include `show`. Model: opus-5-5
This commit is contained in:
@@ -1110,6 +1110,15 @@ func (h *Handlers) ownedWebhook(
|
||||
return webhook, true
|
||||
}
|
||||
|
||||
// The event log's show query parameter and its two values: the events
|
||||
// with a failed delivery, and those with a delivery still pending or
|
||||
// retrying.
|
||||
const (
|
||||
showParam = "show"
|
||||
showFailed = "failed"
|
||||
showPending = "pending"
|
||||
)
|
||||
|
||||
// HandleSourceLogs shows the request/response logs for a
|
||||
// webhook.
|
||||
func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
||||
@@ -1129,17 +1138,36 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Any other value of show lists every event, as no value
|
||||
// does.
|
||||
show := r.URL.Query().Get(showParam)
|
||||
|
||||
statuses := eventLogStatuses(show)
|
||||
if statuses == nil {
|
||||
show = ""
|
||||
}
|
||||
|
||||
evts, total, ok := h.loadEventsWithDeliveries(
|
||||
w, r, webhook, targets,
|
||||
w, r, webhook, targets, statuses,
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
failed, pending, err := h.countFailedAndPendingEvents(webhook.ID)
|
||||
if err != nil {
|
||||
h.serverError(w, r, "failed to count events", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
tmplKeyWebhook: &webhook,
|
||||
"Events": evts,
|
||||
"TotalEvents": total,
|
||||
tmplKeyWebhook: &webhook,
|
||||
"Events": evts,
|
||||
"TotalEvents": total,
|
||||
"Show": show,
|
||||
"FailedEvents": failed,
|
||||
"PendingEvents": pending,
|
||||
}
|
||||
|
||||
h.renderTemplate(w, r, "source_logs.html", data)
|
||||
@@ -1201,9 +1229,10 @@ func (h *Handlers) loadTargetMap(
|
||||
|
||||
// loadEventsWithDeliveries loads the recentEventLimit newest events
|
||||
// and their deliveries from the per-webhook database, and the total
|
||||
// number of events stored. Events come back as capped projections
|
||||
// rather than database.Event rows: see eventLogColumns for why the
|
||||
// cut happens in SQL.
|
||||
// number of events stored. Given delivery statuses, both cover only
|
||||
// the events with a delivery in one of them. Events come back as
|
||||
// capped projections rather than database.Event rows: see
|
||||
// eventLogColumns for why the cut happens in SQL.
|
||||
//
|
||||
// The bool reports whether the load succeeded. It is false
|
||||
// once this has answered the request with an error, and the
|
||||
@@ -1213,6 +1242,7 @@ func (h *Handlers) loadEventsWithDeliveries(
|
||||
r *http.Request,
|
||||
webhook database.Webhook,
|
||||
targetMap map[string]eventLogTarget,
|
||||
statuses []database.DeliveryStatus,
|
||||
) ([]EventLogView, int64, bool) {
|
||||
if !h.dbMgr.DBExists(webhook.ID) {
|
||||
return nil, 0, true
|
||||
@@ -1227,7 +1257,14 @@ func (h *Handlers) loadEventsWithDeliveries(
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
rows, totalEvents := loadEventLogRows(webhookDB, webhook.ID)
|
||||
rows, totalEvents, err := loadEventLogRows(
|
||||
webhookDB, webhook.ID, statuses,
|
||||
)
|
||||
if err != nil {
|
||||
h.serverError(w, r, "failed to load events", err)
|
||||
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
result, ok := h.eventLogViews(
|
||||
w, r, webhookDB, webhook.ID, rows, targetMap,
|
||||
@@ -1321,25 +1358,142 @@ func (h *Handlers) eventLogViews(
|
||||
|
||||
// loadEventLogRows reads the event log projection of the
|
||||
// recentEventLimit newest events, newest first, and the total number
|
||||
// of events stored.
|
||||
// of events stored, both narrowed by statuses as eventsWithStatus
|
||||
// narrows them.
|
||||
func loadEventLogRows(
|
||||
webhookDB *gorm.DB, webhookID string,
|
||||
) ([]eventLogRow, int64) {
|
||||
var totalEvents int64
|
||||
|
||||
webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Count(&totalEvents)
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) ([]eventLogRow, int64, error) {
|
||||
totalEvents, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, statuses,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var rows []eventLogRow
|
||||
|
||||
webhookDB.Model(&database.Event{}).Select(
|
||||
err = eventsWithStatus(webhookDB, webhookID, statuses).Select(
|
||||
eventLogColumns, maxRenderedBodyBytes, maxRenderedBodyBytes,
|
||||
).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Order("created_at DESC").Limit(recentEventLimit).Find(&rows)
|
||||
).Order("created_at DESC").Limit(recentEventLimit).Find(&rows).Error
|
||||
|
||||
return rows, totalEvents
|
||||
return rows, totalEvents, err
|
||||
}
|
||||
|
||||
// eventLogStatuses returns the delivery statuses the event log's show
|
||||
// value lists events by, or nil for one that lists every event.
|
||||
func eventLogStatuses(show string) []database.DeliveryStatus {
|
||||
switch show {
|
||||
case showFailed:
|
||||
return []database.DeliveryStatus{database.DeliveryStatusFailed}
|
||||
case showPending:
|
||||
return []database.DeliveryStatus{
|
||||
database.DeliveryStatusPending,
|
||||
database.DeliveryStatusRetrying,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// eventsWithStatus selects the webhook's events, or, given statuses,
|
||||
// the recentEventLimit newest of those with at least one delivery in
|
||||
// one of them.
|
||||
//
|
||||
// Given statuses, its cost follows the matching deliveries. SQLite
|
||||
// never reorders a CROSS JOIN, so it reads each join's left side
|
||||
// first: the distinct event IDs of the matching deliveries, through
|
||||
// idx_deliveries_status; then each of those events by ID, sorted to
|
||||
// keep the newest; then the rows of only the events kept, so no other
|
||||
// event's body is read. With a plain "id IN (matching deliveries)"
|
||||
// condition instead, SQLite, which keeps no statistics on these
|
||||
// tables, walks every event newest first.
|
||||
func eventsWithStatus(
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) *gorm.DB {
|
||||
if statuses == nil {
|
||||
return webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
)
|
||||
}
|
||||
|
||||
matching := webhookDB.Model(&database.Delivery{}).
|
||||
Distinct("event_id").Where("status IN ?", statuses)
|
||||
|
||||
newest := webhookDB.Table("(?) AS matching", matching).
|
||||
Joins("CROSS JOIN events ON events.id = matching.event_id").
|
||||
Where(
|
||||
"events.webhook_id = ? AND events.deleted_at IS NULL",
|
||||
webhookID,
|
||||
).
|
||||
Order("events.created_at DESC").Limit(recentEventLimit).
|
||||
Select("events.id AS event_id")
|
||||
|
||||
return webhookDB.Table("(?) AS newest", newest).
|
||||
Joins("CROSS JOIN events ON events.id = newest.event_id")
|
||||
}
|
||||
|
||||
// countEventsWithStatus counts the webhook's events with at least one
|
||||
// delivery in one of the statuses, or every event when statuses is
|
||||
// nil. Given statuses, it counts the distinct events of the matching
|
||||
// deliveries and reads nothing but those deliveries, through
|
||||
// idx_deliveries_status, where counting the events would read every
|
||||
// event row. That is the same number, because retention deletes an
|
||||
// event's deliveries with it.
|
||||
func countEventsWithStatus(
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) (int64, error) {
|
||||
var count int64
|
||||
|
||||
if statuses == nil {
|
||||
err := webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
err := webhookDB.Model(&database.Delivery{}).Distinct("event_id").
|
||||
Where("status IN ?", statuses).Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countFailedAndPendingEvents returns how many of the webhook's events
|
||||
// the event log lists when it shows only those with a failed delivery,
|
||||
// and when it shows only those with a delivery pending or retrying.
|
||||
func (h *Handlers) countFailedAndPendingEvents(
|
||||
webhookID string,
|
||||
) (int64, int64, error) {
|
||||
if !h.dbMgr.DBExists(webhookID) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
webhookDB, err := h.dbMgr.GetDB(webhookID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
failed, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, eventLogStatuses(showFailed),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
pending, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, eventLogStatuses(showPending),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return failed, pending, nil
|
||||
}
|
||||
|
||||
// resubmitCounts reports, for each of the page's events, how many
|
||||
|
||||
Reference in New Issue
Block a user