capitan Integration
slush emits lifecycle events through capitan for observability and debugging.
What capitan Provides
capitan is a type-safe event coordination library:
- Async event emission with per-signal workers
- Typed fields for structured event data
- Listeners and observers for event handling
- No-op if no listeners are registered
Signals Emitted by slush
slush emits four signals:
| Signal | Severity | When |
|---|---|---|
SignalRegistered | Info | Service registered |
SignalAccessed | Debug | Service retrieved successfully |
SignalDenied | Warn | Guard rejected access |
SignalNotFound | Warn | Service not in registry |
Event Fields
Events include typed fields:
| Key | Type | Description |
|---|---|---|
KeyInterface | string | Interface FQDN (lookup key) |
KeyImpl | string | Implementation FQDN |
KeyError | string | Error message (denied/not-found only) |
Listening to Events
Single signal
import "github.com/zoobzio/capitan"
capitan.Hook(slush.SignalDenied, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
errMsg, _ := e.String(slush.KeyError)
log.Printf("Access denied: %s - %s", iface, errMsg)
})
Multiple signals
capitan.Observe(func(e *capitan.Event) {
log.Printf("[%s] %s", e.Signal().Name(), e.String(slush.KeyInterface))
}, slush.SignalRegistered, slush.SignalAccessed, slush.SignalDenied)
All slush signals
slushSignals := []capitan.Signal{
slush.SignalRegistered,
slush.SignalAccessed,
slush.SignalDenied,
slush.SignalNotFound,
}
capitan.Observe(handleSlushEvent, slushSignals...)
Use Cases
Access logging
capitan.Hook(slush.SignalAccessed, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
impl, _ := e.String(slush.KeyImpl)
accessLog.Info("service accessed",
"interface", iface,
"impl", impl,
"time", e.Timestamp())
})
Security alerting
capitan.Hook(slush.SignalDenied, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
errMsg, _ := e.String(slush.KeyError)
// Alert on repeated denials
securityMonitor.RecordDenial(iface, errMsg)
})
Metrics
var (
accessCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "slush_access_total"},
[]string{"interface", "result"},
)
)
capitan.Hook(slush.SignalAccessed, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
accessCounter.WithLabelValues(iface, "success").Inc()
})
capitan.Hook(slush.SignalDenied, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
accessCounter.WithLabelValues(iface, "denied").Inc()
})
Service discovery tracking
var registeredServices sync.Map
capitan.Hook(slush.SignalRegistered, func(e *capitan.Event) {
iface, _ := e.String(slush.KeyInterface)
impl, _ := e.String(slush.KeyImpl)
registeredServices.Store(iface, impl)
})
No-Op When Unused
If you don't register listeners, capitan events are no-ops:
// No listeners registered
slush.Register[Database](db) // Event emitted but discarded
slush.Use[Database](ctx) // Event emitted but discarded
This means slush has no capitan overhead unless you opt in.
Event Flow
Register[Database](db)
│
▼
capitan.Info(ctx, SignalRegistered,
KeyInterface.Field("...Database"),
KeyImpl.Field("...postgresDB"))
│
▼
┌───────────────────────────────────┐
│ capitan worker for SignalRegistered│
│ │
│ → Listener 1: log event │
│ → Listener 2: update metrics │
│ → Listener 3: notify discovery │
└───────────────────────────────────┘
Testing with Events
Mock or capture events in tests:
func TestEmitsRegisteredEvent(t *testing.T) {
var captured *capitan.Event
listener := capitan.Hook(slush.SignalRegistered, func(e *capitan.Event) {
captured = e
})
defer listener.Close()
slush.Reset()
slush.Register[Database](mockDB{})
// Wait for async event
time.Sleep(10 * time.Millisecond)
if captured == nil {
t.Fatal("expected event")
}
iface, _ := captured.String(slush.KeyInterface)
if !strings.Contains(iface, "Database") {
t.Errorf("unexpected interface: %s", iface)
}
}
Next Steps
- sctx Integration — Cryptographic guards
- Troubleshooting — Debug with events
- capitan Documentation — Full capitan guide