zoobzio January 24, 2026 Edit this page

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:

SignalSeverityWhen
SignalRegisteredInfoService registered
SignalAccessedDebugService retrieved successfully
SignalDeniedWarnGuard rejected access
SignalNotFoundWarnService not in registry

Event Fields

Events include typed fields:

KeyTypeDescription
KeyInterfacestringInterface FQDN (lookup key)
KeyImplstringImplementation FQDN
KeyErrorstringError 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