Skip to content
EventFabric CQRS Framework

Observability

Observability is a first-class citizen in EventFabric. 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 EventFabric:

  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 EventFabric. 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.

EventFabric 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

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

Set environment variables to enable OTEL export:

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 EventFabric will automatically be exported to your configured backend.

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

The EventFabricEventBus 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

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

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

import { withSpan } from "@eventfabric-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");

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

import { withSpan } from "@eventfabric-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);
    }
);
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

All messages in EventFabric (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
// 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,
});

Always use the structured logger instead of console.log:

import { getLogger } from "@eventfabric-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);

Always pass correlation IDs when creating events from commands:

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

Wrap critical business logic with withSpan() for visibility:

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