Skip to content

Observability

Nagare instruments itself using System.Diagnostics.Activity, the same primitives that ASP.NET Core and Entity Framework use. Any OpenTelemetry collector picks them up. No vendor lock-in, no extra dependencies.

Tracing setup

Register Nagare's activity source with your OpenTelemetry configuration:

csharp
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("Nagare")
        .AddOtlpExporter());

That single AddSource("Nagare") call captures every span the framework emits.

Traced operations

Nagare creates spans for five operations:

Span nameWhen
nagare.aggregate.askA command is sent to an aggregate
nagare.eventstore.appendEvents are written to the store
nagare.eventstore.readEvents are read from the store
nagare.subscription.handleA subscription processes an event
nagare.subscription.checkpointA subscription saves its position

A typical command flow produces three nested spans: aggregate.ask wraps eventstore.read (loading the aggregate) and eventstore.append (writing new events). Subscription processing produces subscription.handle with periodic subscription.checkpoint spans.

Tags

Each span carries attributes that let you filter and group traces:

TagValue
nagare.aggregate.idThe aggregate instance ID
nagare.aggregate.typeThe aggregate's type name
nagare.event.typeThe event stream's type name
nagare.event.countNumber of events in this operation
nagare.command.typeThe command's type name
nagare.subscription.idThe subscription's identifier
nagare.positionGlobal event store position
nagare.versionAggregate version number

These attributes give you a full picture. You can find every command handled by a specific aggregate, trace from the HTTP request through the command to the events it produced, and follow those events through subscriptions.

Event metadata

Beyond tracing spans, you can attach metadata to individual events. This metadata is persisted in the event store and available everywhere the event is read.

csharp
var metadata = new EventMetadata(
    CorrelationId: requestId,
    CausationId: $"http:{Request.Path}",
    UserId: currentUser.Id,
    Timestamp: DateTimeOffset.UtcNow);

await aggregate.Ask(new BorrowBook("user-42"), metadata);

In projections, the metadata is available on the envelope:

csharp
public async Task Handle(EventEnvelope<BookEvent> envelope)
{
    var userId = envelope.Metadata?.UserId;
    var correlationId = envelope.Metadata?.CorrelationId;
    // ...
}

What to put in metadata

FieldPurposeExample
CorrelationIdTrace a chain of events back to the original requestHTTP request ID
CausationIdIdentify what caused this eventThe command or event that triggered it
UserIdAudit trailThe authenticated user
TimestampCustom timestampOverride the store's default timestamp

A middleware is a good place to attach metadata automatically:

csharp
public class CorrelationMiddleware(IHttpContextAccessor http) : ICommandMiddleware
{
    public async Task<IReply> InvokeAsync(AskContext context, AskDelegate next)
    {
        var requestId = http.HttpContext?.TraceIdentifier;
        var userId = http.HttpContext?.User.FindFirst("sub")?.Value;

        var enriched = context with
        {
            Metadata = new EventMetadata(
                CorrelationId: requestId,
                UserId: userId)
        };

        return await next(enriched);
    }
}

Register it once and every command carries correlation data.

Health checks

Nagare registers three health checks that report whether the system is ready to serve traffic.

Event store readiness

EventStoreReadyHealthCheck reports healthy once the event store's database table has been created and verified. It reports unhealthy during startup while the initialization service runs CREATE TABLE IF NOT EXISTS.

Subscription readiness

SubscriptionsReadyHealthCheck tracks each subscription individually. It reports healthy only when every registered subscription has completed its initial catch-up (replayed historical events up to the current position). During startup, it lists which subscriptions are still initializing.

This is useful for Kubernetes readiness probes. A service shouldn't receive traffic until its projections have caught up. Otherwise, queries against read models return stale or empty results.

Repository storage readiness

RepositoryStorageReadyHealthCheck reports healthy once all document store tables have been created. Like the event store check, it transitions from unhealthy to healthy during startup.

Using health checks

The health checks are registered automatically when you add event stores, subscriptions, or repository stores. Wire them into ASP.NET Core's health check endpoint:

csharp
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = _ => true
});

In Kubernetes, point your readiness probe at this endpoint:

yaml
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

The service stays out of the load balancer until the event store is initialized, all subscriptions have caught up, and all document store tables exist.

Connecting the pieces

A production observability setup ties these together:

  1. Tracing shows you what happened: which command was issued, what events it produced, how long each step took
  2. Metadata shows you why: who issued the command, what request triggered it, what earlier event caused it
  3. Health checks show you readiness: is the system caught up and safe to serve traffic

The tracing spans and metadata flow into your existing observability stack (Datadog, Jaeger, Grafana Tempo, Azure Monitor). The health checks integrate with your existing orchestrator. Nagare doesn't impose its own monitoring layer. It fits into whatever you already run.


Next: Configuration

流れ — flow.