Skip to content

Observability ​

Observability is a first-class citizen in Nimbus. The framework is designed so that developers can focus on business logic without implementing logging, tracing, and metrics from the ground up.

Every core component - from message routing to event handling - comes with automatic instrumentation. This means:

  • Zero boilerplate - Tracing spans and metrics are created automatically
  • Consistent structure - All logs follow the same format across your application
  • Correlation built-in - Every message carries a correlation ID for distributed tracing

The three pillars of observability in Nimbus:

  1. Logging - Structured console output with configurable formatters
  2. Tracing - Distributed traces via OpenTelemetry spans
  3. Metrics - Counters and histograms for monitoring

An in Depth Example

This guide also has an in depth example of a working application built with Nimbus. Combining DDD, CQRS and Event Sourcing.

Check out the In Depth Example page to learn how everything is connected and works out in a real-world application.

OpenTelemetry Standards ​

Nimbus uses the OpenTelemetry API (@opentelemetry/api) for all observability instrumentation. This provides:

  • Vendor-agnostic - Export to any OTLP-compatible backend (Jaeger, Zipkin, Grafana, Honeycomb, Datadog, etc.)
  • Industry standard - Wide ecosystem support and community adoption
  • Future-proof - Backed by CNCF with active development

Deno Native Observability ​

Nimbus builds upon Deno's native OpenTelemetry support. With Deno 2.x, you can enable OTEL export with zero additional dependencies.

Enabling OpenTelemetry ​

Set environment variables to enable OTEL export:

bash
export OTEL_DENO=true
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-otlp-endpoint.com/otlp"
export OTEL_SERVICE_NAME=your-service-name
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production

deno run -A src/main.ts

All traces, metrics, and logs from Nimbus will automatically be exported to your configured backend.

Built-in Instrumentation ​

MessageRouter ​

The MessageRouter automatically creates spans for every routed message:

Tracing:

  • Span name: router.route
  • Attributes: messaging.system, messaging.router_name, messaging.destination, correlation_id

Metrics:

  • router_messages_routed_total - Counter for total messages routed (with status: success|error)
  • router_routing_duration_seconds - Histogram of routing duration

EventBus ​

The NimbusEventBus instruments both publishing and handling:

Tracing:

  • eventbus.publish span for event publishing
  • eventbus.handle span for event handling with retry tracking

Metrics:

  • eventbus_events_published_total - Counter for published events
  • eventbus_events_delivered_total - Counter for delivered events (with status: success|error)
  • eventbus_event_handling_duration_seconds - Histogram of handler execution time
  • eventbus_retry_attempts_total - Counter for retry attempts
  • eventbus_event_size_bytes - Histogram of event sizes

EventSourcingDB ​

The EventSourcingDB package instruments writes, reads, and observers:

Tracing:

  • eventsourcingdb.writeEvents / eventsourcingdb.readEvents client spans
  • eventsourcingdb.observeEvent consumer span per observed event (with id/type/subject attributes and handler retry events)

Metrics:

  • eventsourcingdb_operation_total - Counter for read/write operations (with status: success|error)
  • eventsourcingdb_operation_duration_seconds - Histogram of operation duration
  • eventsourcingdb_event_size_bytes - Histogram of written event sizes (subject, event_type)
  • eventsourcingdb_observer_events_handled_total - Counter for handled events (status: success|skipped)
  • eventsourcingdb_observer_handling_duration_seconds - Histogram of observer handler duration
  • eventsourcingdb_observer_handler_retry_attempts_total - Counter for handler retries
  • eventsourcingdb_observer_connection_retry_attempts_total - Counter for scheduled reconnects
  • eventsourcingdb_observer_connection_reconnects_total - Counter for successful reconnects (when events flow again)

See Event Observer and Write Events for details.

Logger ​

The Logger outputs structured logs to the console. When combined with Deno's OTEL support, logs are automatically exported alongside traces and metrics.

Custom Tracing with withSpan() ​

For business logic that needs custom tracing, use the withSpan() higher-order function:

typescript
import { withSpan } from "@nimbus-cqrs/core";

const fetchUser = withSpan(
    {
        name: "fetchUser",
        attributes: {
            "user.source": "database",
        },
    },
    async (userId: string) => {
        return await db.users.findById(userId);
    },
);

// Usage - automatically traced
const user = await fetchUser("123");

Adding Dynamic Attributes ​

Access the span within your function to add attributes based on runtime data:

typescript
import { withSpan } from "@nimbus-cqrs/core";
import { Span } from "@opentelemetry/api";

const processOrder = withSpan(
    { name: "processOrder" },
    async (orderId: string, span: Span) => {
        const order = await db.orders.findById(orderId);

        // Add attributes based on the order
        span.setAttribute("order.total", order.total);
        span.setAttribute("order.items", order.items.length);

        return await processPayment(order);
    },
);

Options ​

OptionTypeDescription
namestringThe span name displayed in your tracing UI
tracerNamestringTracer name (defaults to "nimbus")
kindSpanKindSpan kind (defaults to INTERNAL)
attributesAttributesInitial attributes to set on the span

Correlation IDs ​

All messages in Nimbus (Commands, Queries, Events) carry a correlationid field. This enables:

  • Request tracing - Follow a request through commands, events, and queries
  • Log correlation - Group related logs together
  • Distributed tracing - Track requests across services

The correlation ID is automatically:

  • Generated when creating messages with createCommand(), createQuery(), or createEvent()
  • Propagated from commands to events they produce
  • Included in log output when provided
  • Added as a span attribute for tracing
typescript
// Correlation ID is passed from command to event
const command = createCommand<InviteUserCommand>({
    type: INVITE_USER_COMMAND_TYPE,
    source: "nimbus.overlap.at",
    correlationid: getCorrelationId(c), // From HTTP request
    data: body,
});

// In the handler, create event with same correlation ID
const event = createEvent<UserInvitedEvent>({
    type: USER_INVITED_EVENT_TYPE,
    source: "nimbus.overlap.at",
    correlationid: command.correlationid, // Propagate
    data: state,
});

Best Practices ​

Use Structured Logging ​

Always use the structured logger instead of console.log:

typescript
import { getLogger } from "@nimbus-cqrs/core";

// Good - structured and traceable
getLogger().info({
    message: "User created",
    category: "Users",
    data: { userId: user.id },
    correlationId: command.correlationid,
});

// Avoid - unstructured
console.log("User created:", user.id);

Propagate Correlation IDs ​

Always pass correlation IDs when creating events from commands:

typescript
const event = createEvent<UserInvitedEvent>({
    type: USER_INVITED_EVENT_TYPE,
    source: "nimbus.overlap.at",
    correlationid: command.correlationid, // Always propagate
    data: state,
});

Use withSpan for Important Operations ​

Wrap critical business logic with withSpan() for visibility:

typescript
const validatePayment = withSpan(
    { name: "validatePayment" },
    async (paymentDetails: PaymentDetails) => {
        // Critical logic is now traced
    },
);