Skip to content

Reference

OpenTelemetry

Reference for go-sequent's optional obs package: spans and metrics for command latency, success and error counts, optimistic-concurrency conflicts and outbox depth, plus provider setup and limits.

OpenTelemetry

Package: obs

The obs package gives operators a view of the write path through the vendor-neutral OpenTelemetry API. It is optional. With no provider configured, every instrument is a no-op.

The instrumentation name is github.com/go-sequent/go-sequent.

Purpose

CommandService.Execute is where latency and conflicts show up for a ledger: transaction, append, SyncInTxn projectors. obs exposes command duration, outcome, optimistic-concurrency conflicts and, optionally, outbox depth. You do not have to pick an APM inside the library.

OpenTelemetry data is a runtime signal. It is not the audit source of truth. Reconciliation and compliance still rely on the event stream, the hash chain, and the command records (command.AuditOf, commands table).

What you can see

  • Slow commands: sequent.command.duration_ms by command.type.
  • Success and error rates: sequent.command.total with status=ok|error.
  • Conflict storms on hot aggregates: sequent.optimistic_conflict.total.
  • Outbox backlog: sequent.outbox.depth, when you supply a depth function.

Wiring

obs is not command middleware. Attach it with the façade option, which sets App.Obs and Service.Obs:

// App side: install a TracerProvider and MeterProvider and an exporter first.
inst, err := obs.New(obs.Config{
    OutboxDepth: func(ctx context.Context) int64 {
        n, _ := ob.Depth(ctx)
        return n
    }, // optional; nil skips sequent.outbox.depth
})
app := sequent.New(sequent.Config{},
    sequent.WithObs(inst),
    sequent.WithStore(store, tx),
)

You can also assign svc.Obs = inst directly. nil means no telemetry.

You own the providers

obs.New uses the global otel.Tracer and otel.Meter. If your application has not called otel.SetTracerProvider and otel.SetMeterProvider, the instruments are no-ops, which is safe in tests. The library does not ship OTLP, Prometheus or stdout exporters. The host process installs those.

What Execute does with it

  1. StartCommand starts the span sequent.Execute.
  2. The transaction and UnitOfWork run.
  3. On ErrOptimisticConcurrency from Commit, RecordConflict increments the conflict counter.
  4. RecordCommand records latency and the outcome counter, then ends the span.

A batched Execute uses command.type equal to the first command’s type plus +batch.

Emitted spans and metrics

Kind Name Notes
Span sequent.Execute Attribute command.type. On error: RecordError and status Error.
Histogram sequent.command.duration_ms command.type; milliseconds
Counter sequent.command.total command.type, status = ok or error
Counter sequent.optimistic_conflict.total No extra attributes
Counter sequent.events.appended The API exists. The framework does not call it on append.
Observable gauge sequent.outbox.depth Only if Config.OutboxDepth is set

The public API is Instruments, Config, New, StartCommand, RecordCommand, RecordConflict and RecordEvents.

What it does not do

  • No bundled exporter, and it does not create providers.
  • No automatic HTTP or gRPC instrumentation.
  • No spans for projectors, workflows or the outbox poller.
  • RecordEvents is not hooked into Repository.Commit or EventStore.Append. It exists for apps to call at their own boundary, so sequent.events.appended stays at zero unless you do.
  • It does not replace the audit trail.

Keep secrets and personal data out of span attributes and logs. The built-in attributes contain only the command type.

Using the signals

  • Alert on a rising error rate and on p99 of sequent.command.duration_ms per command type.
  • A growing sequent.optimistic_conflict.total points to contended aggregates or to a failover catching up. See Unique keys and OCC and Failover.
  • A steadily climbing sequent.outbox.depth means the poller is behind or stuck. See Publisher.

Docs track the go-sequent library. When APIs drift, the source wins.