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:
- Logging - Structured console output with configurable formatters
- Tracing - Distributed traces via OpenTelemetry spans
- 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);
}
);
| Option | Type | Description |
|---|
name | string | The span name displayed in your tracing UI |
tracerName | string | Tracer name (defaults to "nimbus") |
kind | SpanKind | Span kind (defaults to INTERNAL) |
attributes | Attributes | Initial 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
}
);