0.2.77
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/adminevents"
|
||||
"github.com/ponzischeme89/memby/server/internal/notify"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -62,6 +63,11 @@ type Dispatcher struct {
|
||||
log *slog.Logger
|
||||
client *http.Client
|
||||
events *adminevents.Bus
|
||||
// notify is the audit trail every outbound notification lands in. This package is the
|
||||
// one producer that reports to it rather than being driven by it: the dispatcher has
|
||||
// its own queue, pacing and transport registry, and routing deliveries through
|
||||
// notify.Send would make the audit trail the thing deciding what Discord receives.
|
||||
notify *notify.Service
|
||||
|
||||
transports map[string]Transport
|
||||
queue chan job
|
||||
@@ -80,9 +86,12 @@ type Dispatcher struct {
|
||||
// SetPaused installs the server-wide quiet-time gate before Start is called.
|
||||
func (d *Dispatcher) SetPaused(paused func() bool) { d.paused = paused }
|
||||
|
||||
func New(st *store.Store, log *slog.Logger, events *adminevents.Bus) *Dispatcher {
|
||||
func New(
|
||||
st *store.Store, log *slog.Logger, events *adminevents.Bus, notifier *notify.Service,
|
||||
) *Dispatcher {
|
||||
dispatcher := &Dispatcher{
|
||||
store: st, log: log.With("component", "integrations"), events: events,
|
||||
notify: notifier,
|
||||
client: &http.Client{Timeout: requestTimeout},
|
||||
transports: map[string]Transport{},
|
||||
queue: make(chan job, queueDepth),
|
||||
@@ -211,11 +220,12 @@ func (d *Dispatcher) post(ctx context.Context, integration store.Integration, ev
|
||||
if err != nil {
|
||||
message = err.Error()
|
||||
}
|
||||
took := time.Since(started)
|
||||
if d.store != nil {
|
||||
record := store.IntegrationDelivery{
|
||||
IntegrationID: integration.ID, EventType: event.Type,
|
||||
Success: err == nil, StatusCode: status,
|
||||
DurationMS: time.Since(started).Milliseconds(), Error: message,
|
||||
DurationMS: took.Milliseconds(), Error: message,
|
||||
}
|
||||
if writeErr := d.store.RecordIntegrationDelivery(
|
||||
context.WithoutCancel(ctx), record,
|
||||
@@ -223,9 +233,53 @@ func (d *Dispatcher) post(ctx context.Context, integration store.Integration, ev
|
||||
d.log.Warn("delivery not recorded", "integration", integration.ID, "error", writeErr)
|
||||
}
|
||||
}
|
||||
// The per-integration delivery history above answers "is this destination healthy",
|
||||
// which is what the integrations page asks. This is the other question — "did Memby
|
||||
// tell anybody about that event" — and it is answered in one place for every channel,
|
||||
// which is the whole reason the notification log exists.
|
||||
d.notify.Log(ctx, notify.Notification{
|
||||
Channel: notify.ChannelWebhook,
|
||||
Kind: event.Type,
|
||||
Source: "integrations",
|
||||
Title: event.Title,
|
||||
Body: event.Summary,
|
||||
// The destination's NAME, never its address: a Discord webhook URL is the
|
||||
// credential, and this row is rendered in the console.
|
||||
Target: integration.Name,
|
||||
SourceKey: integration.ID,
|
||||
Metadata: map[string]any{
|
||||
"integrationId": integration.ID,
|
||||
"kind": integration.Kind,
|
||||
"statusCode": status,
|
||||
},
|
||||
}, deliveryOutcome(status, err), took)
|
||||
return err
|
||||
}
|
||||
|
||||
// deliveryOutcome turns a transport's answer into an audit status.
|
||||
//
|
||||
// A webhook is the one channel that gets Delivered rather than Sent: somebody else's
|
||||
// service actually acknowledged this, where writing a row into a viewer's list is
|
||||
// finished the moment it returns with nobody to confirm it. The status code is kept in
|
||||
// the detail because "failed" on its own sends an operator to the wrong place — a 404 is
|
||||
// a webhook that has been deleted, a 429 is one that is merely busy.
|
||||
func deliveryOutcome(status int, err error) notify.Outcome {
|
||||
if err != nil {
|
||||
if status > 0 {
|
||||
return notify.Outcome{
|
||||
Status: notify.StatusFailed,
|
||||
Detail: fmt.Sprintf("HTTP %d: %s", status, err.Error()),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return notify.Failed(err)
|
||||
}
|
||||
if status > 0 {
|
||||
return notify.Delivered(fmt.Sprintf("HTTP %d", status))
|
||||
}
|
||||
return notify.Delivered("")
|
||||
}
|
||||
|
||||
// announceFailure puts a failed delivery back into the feed the operator is reading.
|
||||
//
|
||||
// It publishes a *different* type from the event that failed, and integration.failed is
|
||||
|
||||
Reference in New Issue
Block a user