Resubmit a stored event as a new undelivered event (closes #250)
All checks were successful
check / check (push) Successful in 3m3s
All checks were successful
check / check (push) Successful in 3m3s
Capturing real webhook traffic and firing it repeatedly at a backend under development is a primary function of this service, and per-delivery replay cannot do it: it only ever resolves the delivery's own original target, so a target created for a dev backend has no prior delivery and nothing can be replayed to it. The event log now offers a per-event Resubmit action. It stores a NEW event copying the stored one's method, headers, body and content type verbatim, and fans it out to the webhook's currently ACTIVE targets, resolved fresh by the query the receiver uses -- so a target created long after the original event arrived receives it. The original event's deliveries have no bearing on where the copy goes, inactive targets are skipped as the receiver skips them, and the action is repeatable: replay's in-flight refusal is deliberately not ported, because firing one captured event over and over is the point. The receiver and the resubmit path share one construction and one fan-out site. An eventSource value carries where the fields came from, live request or stored event, and createAndFanOut writes the event and its pending deliveries in one transaction and hands the tasks to the same Notifier, so a resubmitted delivery is retried, SSRF-guarded and circuit-broken exactly as a first one is. buildDeliveryTasks returns an error instead of writing a response, which is what lets both callers share it. The stored event is read once, before the write transaction, with a cast to blob, so a body over delivery.MaxInlineBodySize is copied byte for byte and the engine loads it from the new event row. A nullable resubmitted_from_id records provenance -- empty for an event that arrived on the receiver -- and the event log reports the relationship in both directions, without which the log is unreadable after a few resubmits of one event. The route sits in the owned-source group, so auth, CSRF and the body cap apply, with its own rate limit bucket and an events_resubmitted_total counter. Inbound signature verification is not re-run: there is no inbound signature to check on a copy an authenticated, CSRF-protected operator action submits. Per-delivery replay is unchanged; it serves recovery, which resubmit does not replace. The README claimed in four places that replay was unimplemented, one of them telling the operator that a delivery stranded by a target type change was lost; all four are corrected and resubmit is documented beside replay.
This commit is contained in:
@@ -83,6 +83,7 @@ type Set struct {
|
||||
deliveriesFailed *prometheus.CounterVec
|
||||
deliveryRetries *prometheus.CounterVec
|
||||
deliveryReplays *prometheus.CounterVec
|
||||
eventsResubmitted prometheus.Counter
|
||||
deliveryDuration *prometheus.HistogramVec
|
||||
deliveriesPending *prometheus.GaugeVec
|
||||
deliveriesRetrying *prometheus.GaugeVec
|
||||
@@ -166,6 +167,23 @@ func (s *Set) DeliveryReplayed(t database.TargetType) {
|
||||
Inc()
|
||||
}
|
||||
|
||||
// EventResubmitted counts one stored event an operator re-injected
|
||||
// from the event log.
|
||||
//
|
||||
// It counts the operator action once, not the deliveries it fans out
|
||||
// to: those already move the attempt, outcome and duration series, and
|
||||
// the new event moves events_received_total, since it is a stored
|
||||
// event that the delivery side will be compared against. This counter
|
||||
// is what separates a resubmitted event from a received one.
|
||||
//
|
||||
// It carries no labels. The only label available at the call site
|
||||
// would be the route pattern, which has exactly one value and so would
|
||||
// distinguish nothing; the target types the event fans out to belong
|
||||
// to the delivery series, not to this one.
|
||||
func (s *Set) EventResubmitted() {
|
||||
s.eventsResubmitted.Inc()
|
||||
}
|
||||
|
||||
// DeliveryStatusChanged counts a delivery's transition into a new
|
||||
// status. The mapping from status to counter lives here, next to the
|
||||
// collectors, so the engine has a single call for every transition it
|
||||
@@ -298,6 +316,15 @@ func (s *Set) registerCounters(factory promauto.Factory) {
|
||||
},
|
||||
[]string{targetTypeLabel},
|
||||
)
|
||||
|
||||
s.eventsResubmitted = factory.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: "events_resubmitted_total",
|
||||
Help: "Stored events an operator re-injected from " +
|
||||
"the event log as new events.",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Set) registerGauges(factory promauto.Factory) {
|
||||
|
||||
Reference in New Issue
Block a user