# kiroku-otel changelog
## Unreleased
## 0.2.0.0 — 2026-05-31
### Breaking Changes
#### Complete FSM-state span coverage (`Kiroku.Otel.Subscription`, MasterPlan 7 EP-2)
- The per-batch span is now keyed on the new `KirokuEventSubscriptionDelivered`
event and named `kiroku.subscription.deliver` (replacing the `spanFetch` /
`kiroku.subscription.fetch` span). It is emitted **once per delivered batch on
every target** (`$all`, category, consumer group) and in **both** phases,
carrying `kiroku.batch.rows` and a `kiroku.subscription.state` of `"catchup"`
or `"live"`. This closes the gap where an `$all` subscription's live phase
produced no per-batch span. `KirokuEventSubscriptionFetched` is now ignored by
the tracer (matched for exhaustiveness) so the DB-driven live path does not emit
two spans per batch.
- `kiroku.subscription.stopped` is **always** emitted on `Stopped` (carrying
`kiroku.subscription.stop_reason` and `kiroku.checkpoint.global_position`), so
the terminal state appears in the trace even for a healthy worker that stops
from `Live` with no open episode span.
- Internal: the per-key span state is striped from one shared `MVar` into a
lock-free `IORef (Map SpanKey (IORef OpenState))` (each key single-writer; the
outer registry mutated only on `Started`/`Stopped`), so the now-per-batch span
work never serializes workers on a shared lock. `base`-only, no public API
change, behavior-identical.
- New exported span-name constants `spanDeliver` / `spanStopped` (replacing
`spanFetch`). A database-backed end-to-end test now runs a real `$all` worker
with the tracer installed and asserts the catch-up, live `deliver`, and
`stopped` spans all appear.
### New Features
#### Subscription-state tracing (`Kiroku.Otel.Subscription`, MasterPlan 6 EP-5)
- `subscriptionTraceHandler :: Tracer -> IO (KirokuEvent -> IO ())` — a ready-made
`eventHandler` callback that turns the subscription worker's finite-state
lifecycle into OpenTelemetry spans. Install it on `ConnectionSettings`
(or a per-subscription) `eventHandler` and every subscription emits spans.
- Short, promptly-ending spans (the SDK only exports a span on `endSpan`, so
there is no worker-lifetime span): per-episode `kiroku.subscription.catchup` /
`paused` / `reconnecting` / `retrying`, per-batch `kiroku.subscription.fetch`,
and standalone `kiroku.subscription.dead_letter` / `db_error`. Each carries a
`kiroku.*` attribute set (subscription name, state, consumer-group member/size,
checkpoint position, attempt, batch rows, event position, dead-letter/stop
reason); the span-name and attribute-key constants are exported.
- The library remains separate from `kiroku-store`, so `kiroku-store` gains no
OpenTelemetry dependency. The handler keeps its open spans in a thread-safe
`MVar` keyed by `(subscription name, member)`. Requires a **batch span
processor** so the synchronous callback never blocks the worker on export.
### Other Changes
#### OpenTelemetry 1.0 and current semantic-convention keys
- `kiroku-otel` now depends on the OpenTelemetry 1.0 package family:
`hs-opentelemetry-api ^>=1.0`, `hs-opentelemetry-propagator-w3c ^>=1.0`,
and `hs-opentelemetry-semantic-conventions ^>=1.40`.
- Subscription spans now emit generated OpenTelemetry semantic-convention
attribute keys alongside the existing `kiroku.*` keys. Delivery spans include
`messaging.system = "kiroku"`, `messaging.destination.name`,
`messaging.operation.type = "process"`, and
`messaging.batch.message_count`; database-error spans include
`db.system.name = "postgresql"` and `db.operation.name`.
## 0.1.0.0 — 2026-05-23
### New Features
- Initial release. Exposes `Kiroku.Otel.TraceContext` with two helpers:
- `injectTraceContext :: SpanContext -> EventData -> EventData` — encodes
the supplied `SpanContext` to W3C `traceparent` / `tracestate` strings
and merges them into the event's `metadata` JSON object. Existing keys
in `metadata` are preserved.
- `extractTraceContext :: RecordedEvent -> Maybe SpanContext` — reads
the same JSON keys back out and decodes them through
`OpenTelemetry.Propagator.W3CTraceContext.decodeSpanContext`. Returns
`Nothing` when `metadata` is absent, is not a JSON object, lacks a
`traceparent` key, or carries an unparseable value. Never throws.