Commiq Docs
Plugins

OpenTelemetry

OpenTelemetry

@naikidev/commiq-otel instruments Commiq stores with OpenTelemetry tracing. Each command becomes a span, and events emitted during that command are recorded as span events, giving you end-to-end visibility in any OTel-compatible backend.

Installation

pnpm add @naikidev/commiq-otel @opentelemetry/api 
npm install @naikidev/commiq-otel @opentelemetry/api 
yarn add @naikidev/commiq-otel @opentelemetry/api 
bun add @naikidev/commiq-otel @opentelemetry/api 

@opentelemetry/api is a peer dependency.

Basic Usage

import { createStore, createCommand, sealStore } from "@naikidev/commiq";
import { instrumentStore } from "@naikidev/commiq-otel";

const store = createStore({ count: 0 });
store.addCommandHandler("increment", (ctx) => {
  ctx.setState({ count: ctx.state.count + 1 });
});

const uninstrument = instrumentStore(store, { storeName: "counter" });

const sealed = sealStore(store);
sealed.queue(createCommand("increment", undefined));

API

instrumentStore(store, options)

Subscribes to a store's event stream and creates OpenTelemetry spans for command processing.

Returns a cleanup function that ends active spans and unsubscribes the listener.

function instrumentStore(
  store: StoreWithStream,
  options: InstrumentOptions,
): () => void;

Options

OptionTypeDefaultDescription
storeNamestring(required)Display name used in span attributes
tracerNamestring"commiq"OpenTelemetry tracer name
tracerVersionstringOpenTelemetry tracer version

Tracing Model

Spans

Each command creates a span that lives from commandStarted to commandHandled (or commandHandlingError / commandInterrupted):

commiq.command:increment
  ├─ span event: stateChanged
  └─ span event: itemAdded (custom event)

State changes and custom events emitted during a command are recorded as span events on the parent command span. Events emitted outside a command create standalone spans.

Span Attributes

Command spans (commiq.command:{name}):

AttributeDescription
commiq.storeStore name
commiq.command.nameCommand name
commiq.command.correlation_idUnique correlation ID
commiq.command.caused_byParent event ID (if applicable)
commiq.command.interruptedtrue if the command was interrupted
commiq.command.interrupted_phase"queued" or "running" (only when interrupted)

Standalone event spans (commiq.event:{name}):

AttributeDescription
commiq.storeStore name
commiq.event.nameEvent name
commiq.event.correlation_idUnique correlation ID
commiq.event.caused_byParent event ID (if applicable)

Interrupted Commands

When an interruptable command is cancelled, the command span is:

  1. Set to OK status with message "interrupted"
  2. Attributes commiq.command.interrupted (true) and commiq.command.interrupted_phase ("queued" or "running") are added
  3. The span is ended

This distinguishes intentional cancellation from errors in your tracing backend.

Error Handling

When a command handler throws, the command span is:

  1. Set to ERROR status with the error message
  2. The exception is recorded on the span via span.recordException()
  3. The span is ended

Cleanup

Call the returned function to stop instrumentation:

const uninstrument = instrumentStore(store, { storeName: "counter" });

// Later...
uninstrument();

This ends any in-flight spans and removes the stream listener.

Full Example

Using the OpenTelemetry Node SDK with a console exporter:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node";
import { createStore, createCommand, sealStore } from "@naikidev/commiq";
import { instrumentStore } from "@naikidev/commiq-otel";

// Set up OTel SDK
const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter() });
sdk.start();

// Create and instrument a store
const store = createStore({ count: 0 });
store.addCommandHandler("increment", (ctx) => {
  ctx.setState({ count: ctx.state.count + 1 });
});

const uninstrument = instrumentStore(store, {
  storeName: "counter",
  tracerName: "my-app",
});

const sealed = sealStore(store);
sealed.queue(createCommand("increment", undefined));

// Cleanup
uninstrument();
await sdk.shutdown();

Testing Locally

To visualize traces during development, use any OpenTelemetry-compatible backend:

BackendBest forSetup
JaegerLocal debuggingdocker run -p 16686:16686 jaegertracing/all-in-one
Aspire Dashboard.NET ecosystemSee browser telemetry guide
ZipkinLightweight tracingdocker run -p 9411:9411 openzipkin/zipkin
Grafana TempoProduction-gradeRequires Grafana stack

Configure your OTel SDK exporter to point to your chosen backend. The ConsoleSpanExporter in the example above is useful for quick debugging without any backend setup.

On this page