zoobzio December 8, 2025 Edit this page

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.

ContextFields
Request ID, trace IDOrder ID, amount
Authenticated userCustomer ID
Deadline, cancellationEvent payload
Cross-cutting concernsDomain 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.