Context
Every event carries the context.Context passed at emission. This enables request tracing, cancellation, and propagating request-scoped values through event flows.
Accessing Context
Listeners receive context as the first argument:
capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {
// ctx is the context passed to Emit
})
The same context is also available via e.Context().
Request Tracing
Propagate trace IDs through event flows:
type ctxKey string
const requestIDKey ctxKey = "request_id"
func HandleRequest(w http.ResponseWriter, r *http.Request) {
// Extract or generate request ID
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
// Add to context
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
// Events carry the request ID
capitan.Emit(ctx, orderCreated, orderID.Field("ORDER-123"))
}
// Listener can access the request ID
capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {
requestID, _ := ctx.Value(requestIDKey).(string)
log.Printf("[%s] Processing order", requestID)
})
With OpenTelemetry
Propagate spans through events:
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "HandleRequest")
defer span.End()
// Event carries the trace context
capitan.Emit(ctx, orderCreated, orderID.Field("ORDER-123"))
}
capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {
// Continue the trace
ctx, span := tracer.Start(ctx, "ProcessOrder")
defer span.End()
span.AddEvent("order.created", trace.WithAttributes(
attribute.String("signal", e.Signal().Name()),
))
// Process order...
})
Events emitted from listeners continue the trace:
capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {
ctx, span := tracer.Start(ctx, "ProcessOrder")
defer span.End()
// Downstream event inherits trace context
capitan.Emit(ctx, inventoryReserved, fields...)
})
Cancellation
Context cancellation affects event processing at two points.
Before Queueing
If the context is canceled while Emit is waiting for buffer space, the event is dropped:
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// If buffer is full and timeout expires, event is dropped
capitan.Emit(ctx, signal, fields...)
Before Processing
If the context was canceled while the event was queued, it's skipped:
capitan.Hook(signal, func(ctx context.Context, e *capitan.Event) {
// This won't execute if ctx was canceled before processing
})
Checking Cancellation in Listeners
For long-running listeners, check context periodically:
capitan.Hook(batchProcess, func(ctx context.Context, e *capitan.Event) {
items := getItems(e)
for _, item := range items {
// Check if request was canceled
if ctx.Err() != nil {
log.Printf("Processing canceled: %v", ctx.Err())
return
}
processItem(item)
}
})
Timeouts
Emit with timeouts to bound queue wait time:
func EmitWithTimeout(signal capitan.Signal, fields ...capitan.Field) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
capitan.Emit(ctx, signal, fields...)
if ctx.Err() != nil {
return fmt.Errorf("emit timeout: %w", ctx.Err())
}
return nil
}
Note: This bounds queue wait time, not listener execution time. Listeners run to completion regardless of context state.
Request-Scoped Values
Pass request-scoped data through context:
type User struct {
ID string
Role string
}
type ctxKey string
const userKey ctxKey = "user"
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := authenticate(r)
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Listener accesses user from context
capitan.Hook(orderCreated, func(ctx context.Context, e *capitan.Event) {
user, ok := ctx.Value(userKey).(User)
if !ok {
log.Println("No user in context")
return
}
log.Printf("Order created by user %s (role: %s)", user.ID, user.Role)
})
Observer Context Patterns
Observers can use context for filtering or enrichment:
capitan.Observe(func(ctx context.Context, e *capitan.Event) {
entry := map[string]any{
"signal": e.Signal().Name(),
"timestamp": e.Timestamp(),
}
// Enrich with request context if available
if requestID, ok := ctx.Value(requestIDKey).(string); ok {
entry["request_id"] = requestID
}
if user, ok := ctx.Value(userKey).(User); ok {
entry["user_id"] = user.ID
}
logger.Info(entry)
})
Context vs Fields
Use context for request-scoped metadata. Use fields for event-specific data.
| Context | Fields |
|---|---|
| Request ID, trace ID | Order ID, amount |
| Authenticated user | Customer ID |
| Deadline, cancellation | Event payload |
| Cross-cutting concerns | Domain data |
// Context: request metadata
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
// Fields: event data
capitan.Emit(ctx, orderCreated,
orderID.Field("ORDER-123"),
total.Field(99.99),
)
Listeners that need the request ID get it from context. Listeners that need the order ID get it from fields.