Skip to content

Commit 22eac8c

Browse files
alexluongclaude
andcommitted
refactor(opevents,apirouter): Event value + payload catalog for operator topics
Emit now takes an opevents.Event value (topic, tenant, data) instead of three positional args, and payloads.go becomes the wire-contract catalog: the alert payload shapes plus typed constructors, and apirouter's tenant.subscription.updated payload moves there behind one too, replacing the raw topic string + local struct. The alert monitor's consumer interface and call sites are adapted mechanically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 413346a commit 22eac8c

8 files changed

Lines changed: 179 additions & 41 deletions

File tree

internal/alert/monitor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
// AlertEmitter is the interface for emitting alert events. Satisfied by opevents.Emitter.
1717
type AlertEmitter interface {
18-
Emit(ctx context.Context, topic string, tenantID string, data any) error
18+
Emit(ctx context.Context, ev opevents.Event) error
1919
}
2020

2121
// DestinationDisabler handles disabling destinations.
@@ -198,7 +198,7 @@ func (m *alertMonitor) HandleAttempt(ctx context.Context, attempt DeliveryAttemp
198198
}
199199

200200
emitFn := func(ctx context.Context) error {
201-
if err := m.emitter.Emit(ctx, opevents.TopicAlertExhaustedRetries, attempt.Destination.TenantID, erData); err != nil {
201+
if err := m.emitter.Emit(ctx, opevents.Event{Topic: opevents.TopicAlertExhaustedRetries, TenantID: attempt.Destination.TenantID, Data: erData}); err != nil {
202202
return err
203203
}
204204
m.logger.Ctx(ctx).Audit("alert sent",
@@ -299,7 +299,7 @@ func (m *alertMonitor) handleConsecutiveFailure(ctx context.Context, attempt Del
299299
Event: attempt.Event,
300300
Attempt: attempt.Attempt,
301301
}
302-
if err := m.emitter.Emit(ctx, opevents.TopicAlertDestinationDisabled, attempt.Destination.TenantID, disabledData); err != nil {
302+
if err := m.emitter.Emit(ctx, opevents.Event{Topic: opevents.TopicAlertDestinationDisabled, TenantID: attempt.Destination.TenantID, Data: disabledData}); err != nil {
303303
return false, fmt.Errorf("failed to emit destination disabled alert: %w", err)
304304
}
305305
}
@@ -316,7 +316,7 @@ func (m *alertMonitor) handleConsecutiveFailure(ctx context.Context, attempt Del
316316
Threshold: level,
317317
},
318318
}
319-
if err := m.emitter.Emit(ctx, opevents.TopicAlertConsecutiveFailure, attempt.Destination.TenantID, cfData); err != nil {
319+
if err := m.emitter.Emit(ctx, opevents.Event{Topic: opevents.TopicAlertConsecutiveFailure, TenantID: attempt.Destination.TenantID, Data: cfData}); err != nil {
320320
return false, fmt.Errorf("failed to emit consecutive failure alert: %w", err)
321321
}
322322

internal/alert/monitor_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/hookdeck/outpost/internal/alert"
1414
"github.com/hookdeck/outpost/internal/idempotence"
15+
"github.com/hookdeck/outpost/internal/opevents"
1516
"github.com/hookdeck/outpost/internal/models"
1617
"github.com/hookdeck/outpost/internal/util/testutil"
1718
)
@@ -20,8 +21,8 @@ type mockAlertEmitter struct {
2021
mock.Mock
2122
}
2223

23-
func (m *mockAlertEmitter) Emit(ctx context.Context, topic string, tenantID string, data any) error {
24-
args := m.Called(ctx, topic, tenantID, data)
24+
func (m *mockAlertEmitter) Emit(ctx context.Context, ev opevents.Event) error {
25+
args := m.Called(ctx, ev.Topic, ev.TenantID, ev.Data)
2526
return args.Error(0)
2627
}
2728

internal/apirouter/destination_handlers.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hookdeck/outpost/internal/idgen"
1616
"github.com/hookdeck/outpost/internal/logging"
1717
"github.com/hookdeck/outpost/internal/models"
18+
"github.com/hookdeck/outpost/internal/opevents"
1819
"github.com/hookdeck/outpost/internal/telemetry"
1920
"github.com/hookdeck/outpost/internal/tenantstore"
2021
"github.com/hookdeck/outpost/internal/util/maputil"
@@ -24,16 +25,7 @@ import (
2425
// SubscriptionEmitter emits operator events for subscription changes.
2526
// Satisfied by opevents.Emitter.
2627
type SubscriptionEmitter interface {
27-
Emit(ctx context.Context, topic string, tenantID string, data any) error
28-
}
29-
30-
// TenantSubscriptionUpdatedData is the data payload for tenant.subscription.updated events.
31-
type TenantSubscriptionUpdatedData struct {
32-
TenantID string `json:"tenant_id"`
33-
Topics []string `json:"topics"`
34-
PreviousTopics []string `json:"previous_topics"`
35-
DestinationsCount int `json:"destinations_count"`
36-
PreviousDestinationsCount int `json:"previous_destinations_count"`
28+
Emit(ctx context.Context, ev opevents.Event) error
3729
}
3830

3931
type DestinationHandlers struct {
@@ -572,14 +564,13 @@ func (h *DestinationHandlers) emitSubscriptionUpdateIfChanged(ctx context.Contex
572564
return
573565
}
574566

575-
data := TenantSubscriptionUpdatedData{
567+
if err := h.emitter.Emit(ctx, opevents.TenantSubscriptionUpdatedEvent(opevents.TenantSubscriptionUpdatedData{
576568
TenantID: tenantID,
577569
Topics: tenant.Topics,
578570
PreviousTopics: prev.topics,
579571
DestinationsCount: tenant.DestinationsCount,
580572
PreviousDestinationsCount: prev.destinationsCount,
581-
}
582-
if err := h.emitter.Emit(ctx, "tenant.subscription.updated", tenantID, data); err != nil {
573+
})); err != nil {
583574
h.logger.Ctx(ctx).Error("failed to emit subscription update", zap.Error(err))
584575
}
585576
}

internal/apirouter/destination_handlers_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/hookdeck/outpost/internal/apirouter"
1110
"github.com/hookdeck/outpost/internal/destregistry"
1211
"github.com/hookdeck/outpost/internal/models"
12+
"github.com/hookdeck/outpost/internal/opevents"
1313
"github.com/hookdeck/outpost/internal/tenantstore"
1414
"github.com/stretchr/testify/assert"
1515
"github.com/stretchr/testify/require"
@@ -1279,7 +1279,7 @@ func TestAPI_SubscriptionUpdated(t *testing.T) {
12791279
assert.Equal(t, "tenant.subscription.updated", call.topic)
12801280
assert.Equal(t, "t1", call.tenantID)
12811281

1282-
data := call.data.(apirouter.TenantSubscriptionUpdatedData)
1282+
data := call.data.(opevents.TenantSubscriptionUpdatedData)
12831283
assert.Equal(t, 1, data.DestinationsCount)
12841284
assert.Equal(t, 0, data.PreviousDestinationsCount)
12851285
})
@@ -1295,7 +1295,7 @@ func TestAPI_SubscriptionUpdated(t *testing.T) {
12951295
require.Equal(t, http.StatusOK, resp.Code)
12961296
require.Len(t, h.subscriptionEmitter.calls, 1)
12971297

1298-
data := h.subscriptionEmitter.calls[0].data.(apirouter.TenantSubscriptionUpdatedData)
1298+
data := h.subscriptionEmitter.calls[0].data.(opevents.TenantSubscriptionUpdatedData)
12991299
assert.Equal(t, 0, data.DestinationsCount)
13001300
assert.Equal(t, 1, data.PreviousDestinationsCount)
13011301
})
@@ -1315,7 +1315,7 @@ func TestAPI_SubscriptionUpdated(t *testing.T) {
13151315
require.Equal(t, http.StatusOK, resp.Code)
13161316
require.Len(t, h.subscriptionEmitter.calls, 1)
13171317

1318-
data := h.subscriptionEmitter.calls[0].data.(apirouter.TenantSubscriptionUpdatedData)
1318+
data := h.subscriptionEmitter.calls[0].data.(opevents.TenantSubscriptionUpdatedData)
13191319
assert.Contains(t, data.Topics, "user.deleted")
13201320
assert.Contains(t, data.PreviousTopics, "user.created")
13211321
})

internal/apirouter/router_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hookdeck/outpost/internal/logging"
1717
"github.com/hookdeck/outpost/internal/logstore"
1818
"github.com/hookdeck/outpost/internal/models"
19+
"github.com/hookdeck/outpost/internal/opevents"
1920
"github.com/hookdeck/outpost/internal/portal"
2021
"github.com/hookdeck/outpost/internal/publishmq"
2122
"github.com/hookdeck/outpost/internal/telemetry"
@@ -244,8 +245,8 @@ type emitCall struct {
244245
data any
245246
}
246247

247-
func (m *mockSubscriptionEmitter) Emit(_ context.Context, topic string, tenantID string, data any) error {
248-
m.calls = append(m.calls, emitCall{topic: topic, tenantID: tenantID, data: data})
248+
func (m *mockSubscriptionEmitter) Emit(_ context.Context, ev opevents.Event) error {
249+
m.calls = append(m.calls, emitCall{topic: ev.Topic, tenantID: ev.TenantID, data: ev.Data})
249250
return nil
250251
}
251252

internal/opevents/emitter.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ const (
1515
backoffFactor = 2
1616
)
1717

18+
// Event is a request to emit an operator event. Callers (alert eval, apirouter)
19+
// build it and hand it to Emit, which owns envelope construction and delivery.
20+
type Event struct {
21+
Topic string
22+
TenantID string
23+
Data any
24+
}
25+
1826
// Emitter is the interface for emitting operator events.
1927
type Emitter interface {
20-
Emit(ctx context.Context, topic string, tenantID string, data any) error
28+
Emit(ctx context.Context, ev Event) error
2129
}
2230

2331
// emitter is the default Emitter implementation.
@@ -54,23 +62,23 @@ func NewEmitter(sink Sink, deploymentID string, topics []string) Emitter {
5462
}
5563
}
5664

57-
func (e *emitter) Emit(ctx context.Context, topic string, tenantID string, data any) error {
65+
func (e *emitter) Emit(ctx context.Context, ev Event) error {
5866
// Topic filtering: nil filter means accept all ("*")
59-
if e.topicFilter != nil && !e.topicFilter[topic] {
67+
if e.topicFilter != nil && !e.topicFilter[ev.Topic] {
6068
return nil
6169
}
6270

63-
rawData, err := json.Marshal(data)
71+
rawData, err := json.Marshal(ev.Data)
6472
if err != nil {
6573
return fmt.Errorf("opevents: failed to marshal data: %w", err)
6674
}
6775

6876
event := &OperatorEvent{
6977
ID: idgen.String(),
70-
Topic: topic,
78+
Topic: ev.Topic,
7179
Time: time.Now(),
7280
DeploymentID: e.deploymentID,
73-
TenantID: tenantID,
81+
TenantID: ev.TenantID,
7482
Data: rawData,
7583
}
7684

@@ -104,6 +112,6 @@ func (e *emitter) sendWithRetry(ctx context.Context, event *OperatorEvent) error
104112
// noopEmitter discards all events. Used when operator events are disabled.
105113
type noopEmitter struct{}
106114

107-
func (e *noopEmitter) Emit(ctx context.Context, topic string, tenantID string, data any) error {
115+
func (e *noopEmitter) Emit(ctx context.Context, ev Event) error {
108116
return nil
109117
}

internal/opevents/emitter_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestEmitter_Emit(t *testing.T) {
5151
sink := &mockSink{}
5252
em := opevents.NewEmitter(sink, "deploy-1", []string{"*"})
5353

54-
err := em.Emit(context.Background(), "any.topic", "tenant-1", map[string]string{"key": "val"})
54+
err := em.Emit(context.Background(), opevents.Event{Topic: "any.topic", TenantID: "tenant-1", Data: map[string]string{"key": "val"}})
5555
require.NoError(t, err)
5656

5757
events := sink.sentEvents()
@@ -69,7 +69,7 @@ func TestEmitter_Emit(t *testing.T) {
6969
opevents.TopicTenantSubscriptionUpdated,
7070
})
7171

72-
err := em.Emit(context.Background(), opevents.TopicAlertConsecutiveFailure, "t1", "data")
72+
err := em.Emit(context.Background(), opevents.Event{Topic: opevents.TopicAlertConsecutiveFailure, TenantID: "t1", Data: "data"})
7373
require.NoError(t, err)
7474
assert.Len(t, sink.sentEvents(), 1)
7575
})
@@ -79,7 +79,7 @@ func TestEmitter_Emit(t *testing.T) {
7979
sink := &mockSink{}
8080
em := opevents.NewEmitter(sink, "", []string{opevents.TopicAlertConsecutiveFailure})
8181

82-
err := em.Emit(context.Background(), opevents.TopicTenantSubscriptionUpdated, "t1", "data")
82+
err := em.Emit(context.Background(), opevents.Event{Topic: opevents.TopicTenantSubscriptionUpdated, TenantID: "t1", Data: "data"})
8383
require.NoError(t, err)
8484
assert.Empty(t, sink.sentEvents())
8585
})
@@ -93,7 +93,7 @@ func TestEmitter_Emit(t *testing.T) {
9393
Count int `json:"count"`
9494
}
9595

96-
err := em.Emit(context.Background(), "test.topic", "tenant-42", payload{Count: 7})
96+
err := em.Emit(context.Background(), opevents.Event{Topic: "test.topic", TenantID: "tenant-42", Data: payload{Count: 7}})
9797
require.NoError(t, err)
9898

9999
events := sink.sentEvents()
@@ -116,7 +116,7 @@ func TestEmitter_Emit(t *testing.T) {
116116
sink := &mockSink{}
117117
em := opevents.NewEmitter(sink, "", []string{"*"})
118118

119-
err := em.Emit(context.Background(), "t", "tenant", "data")
119+
err := em.Emit(context.Background(), opevents.Event{Topic: "t", TenantID: "tenant", Data: "data"})
120120
require.NoError(t, err)
121121

122122
events := sink.sentEvents()
@@ -134,7 +134,7 @@ func TestEmitter_Emit(t *testing.T) {
134134
sink := &mockSink{}
135135
em := opevents.NewEmitter(sink, "", []string{})
136136

137-
err := em.Emit(context.Background(), "any.topic", "t1", "data")
137+
err := em.Emit(context.Background(), opevents.Event{Topic: "any.topic", TenantID: "t1", Data: "data"})
138138
require.NoError(t, err)
139139
assert.Empty(t, sink.sentEvents(), "noop emitter should not send events")
140140
})
@@ -150,7 +150,7 @@ func TestEmitter_Emit(t *testing.T) {
150150
}
151151
em := opevents.NewEmitter(sink, "", []string{"*"})
152152

153-
err := em.Emit(context.Background(), "t", "t1", "data")
153+
err := em.Emit(context.Background(), opevents.Event{Topic: "t", TenantID: "t1", Data: "data"})
154154
require.NoError(t, err)
155155
assert.Len(t, sink.sentEvents(), 1)
156156
})
@@ -166,7 +166,7 @@ func TestEmitter_Emit(t *testing.T) {
166166
}
167167
em := opevents.NewEmitter(sink, "", []string{"*"})
168168

169-
err := em.Emit(context.Background(), "t", "t1", "data")
169+
err := em.Emit(context.Background(), opevents.Event{Topic: "t", TenantID: "t1", Data: "data"})
170170
require.Error(t, err)
171171
assert.Contains(t, err.Error(), "fail-3")
172172
assert.Empty(t, sink.sentEvents())
@@ -182,7 +182,7 @@ func TestEmitter_Emit(t *testing.T) {
182182
}
183183
em := opevents.NewEmitter(sink, "", []string{"*"})
184184

185-
err := em.Emit(ctx, "t", "t1", "data")
185+
err := em.Emit(ctx, opevents.Event{Topic: "t", TenantID: "t1", Data: "data"})
186186
require.Error(t, err)
187187
})
188188
}

0 commit comments

Comments
 (0)