Telemetry
View SourceReckonDB is instrumented with telemetry — the
standard BEAM instrumentation library. Every significant operation emits a
telemetry event; you attach handlers to turn those events into logs, metrics,
traces, or alerts. Nothing leaves the node on its own: telemetry is in-process
pub/sub, and emitting an event for which no handler is attached costs a single
ETS lookup.
This guide covers how the mechanism works, how to attach handlers (including metrics exporters), and the full event catalogue.
How it works
An event is a list of atoms — a stable name like [reckon_db, stream, write, stop].
When the operation happens, ReckonDB calls:
telemetry:execute(Event, Measurements, Metadata).- Measurements — a map of numbers (durations, counts, sizes). These feed counters and histograms.
- Metadata — a map of context (store_id, stream_id, reason). These become labels/tags you filter and group by.
A handler is a 4-arity function attached to one or more events. It runs synchronously in the process that emitted the event, so handlers must be cheap — do the heavy lifting (aggregation, network I/O) in a separate process, or use an exporter library that already does.
Quick start
Attach the built-in logger handler and you will see ReckonDB events in your logs:
ok = reckon_db_telemetry:attach_default_handler().This is done automatically at application start when the telemetry_handlers
environment key includes logger (the default — see Configuration).
Attach your own handler
reckon_db_telemetry:attach(my_metrics, fun my_app_metrics:handle/4, #{}).my_app_metrics:handle/4 receives (EventName, Measurements, Metadata, Config)
for every ReckonDB event. A minimal example that counts stream writes:
handle([reckon_db, stream, write, stop], #{event_count := N}, #{store_id := Store}, _Cfg) ->
my_counter:add({writes, Store}, N),
ok;
handle(_Event, _Measurements, _Metadata, _Cfg) ->
ok.Detach when done:
reckon_db_telemetry:detach(my_metrics).The facade
reckon_db_telemetry is the public entry point:
| Function | Purpose |
|---|---|
attach_default_handler/0 | Attach the built-in logger handler. |
detach_default_handler/0 | Remove it. |
attach/3 | Attach a custom HandlerId, Fun, Config across ReckonDB events. |
detach/1 | Remove a handler by id. |
emit/3 | Emit an event (thin wrapper over telemetry:execute/3). |
span/3 | Run a function bracketed by :start/:stop events with a duration. |
Note on
attach/3and the default handler. Both attach to the list returned by the internalall_events/0, which currently covers the core stream / subscription / snapshot / cluster / store / emitter events. Events in the extended categories below (temporal, scavenge, causation, backpressure, schema, memory, consistency, health, link) are emitted but are not in that list — to receive them, attach directly to the event name:telemetry:attach(my_id, [reckon_db, consistency, quorum, lost], fun m:h/4, #{}).
Metrics exporters
For a datacenter deployment, bridge telemetry to your metrics backend instead of (or alongside) the logger:
- Prometheus — use
telemetry_metrics_prometheusto define counters/histograms over these events and scrape them. - OpenTelemetry — use
opentelemetry+ a telemetry bridge to export spans/metrics via OTLP. - StatsD / custom — attach a handler that forwards to your collector.
Because the event names are stable, exporter configuration is just a mapping
from event → metric. Example (Elixir telemetry_metrics style, shown for
reference):
counter("reckon_db.stream.write.stop", tags: [:store_id])
distribution("reckon_db.stream.write.stop.duration", unit: {:native, :microsecond})
last_value("reckon_db.consistency.quorum.lost")Configuration
At application start ReckonDB reads:
{reckon_db, [
{telemetry_handlers, [logger]} %% default
]}.[logger]— attach the built-in logger handler (default).[]— attach nothing; emit events silently until you attach your own.- Add your own atoms and extend
reckon_db_app's handler wiring, or simply callreckon_db_telemetry:attach/3from your own supervision tree.
Event catalogue
All events, their measurements, and metadata. Durations are in microseconds unless the key says otherwise.
Stream
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, stream, write, start] | system_time | store_id, stream_id, event_count, expected_version |
[reckon_db, stream, write, stop] | duration, event_count | store_id, stream_id, new_version |
[reckon_db, stream, write, error] | duration | store_id, stream_id, reason |
[reckon_db, stream, read, start] | system_time | store_id, stream_id, start_version, count, direction |
[reckon_db, stream, read, stop] | duration, event_count | store_id, stream_id |
Subscription
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, subscription, created] | system_time | store_id, subscription_id, type, selector |
[reckon_db, subscription, deleted] | system_time | store_id, subscription_id |
[reckon_db, subscription, event_delivered] | duration | store_id, subscription_id, event_id, event_type |
[reckon_db, subscription, checkpoint] | position | store_id, subscription_name |
[reckon_db, subscription, backpressure, warning] | queue_size, max_size | store_id, subscription_key |
[reckon_db, subscription, backpressure, dropped] | count | store_id, subscription_key, strategy |
Snapshot
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, snapshot, created] | system_time, size_bytes | store_id, stream_id, version |
[reckon_db, snapshot, read] | duration, size_bytes | store_id, stream_id, version |
Store
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, store, started] | system_time | store_id, mode, data_dir |
[reckon_db, store, stopped] | system_time, uptime_ms | store_id, reason |
Cluster
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, cluster, node, up] | system_time | store_id, node, member_count |
[reckon_db, cluster, node, down] | system_time | store_id, node, member_count, reason |
[reckon_db, cluster, leader, elected] | system_time | store_id, leader_node, previous_leader |
The
leader, electedevent fires on first-leader detection and every leadership change (see Cluster Consistency). It is the recommended hook for "run this singleton on whichever node currently leads store X" — attach, compareleader_nodetonode(), start/stop your work accordingly.
Emitter
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, emitter, broadcast] | duration | store_id, subscription_id, event_id, recipient_count |
[reckon_db, emitter, pool, created] | system_time | store_id, subscription_id, pool_size |
Temporal queries
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, temporal, read_until] | duration, event_count | store_id, stream_id, timestamp |
[reckon_db, temporal, read_range] | duration, event_count | store_id, stream_id, timestamp ({from, to}) |
Scavenge
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, scavenge, complete] | duration, deleted_count | store_id, stream_id, archived |
Causation
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, causation, query] | duration, event_count | store_id, id, query_type |
Schema
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, schema, registered] | version | store_id, event_type |
[reckon_db, schema, unregistered] | version (0) | store_id, event_type |
[reckon_db, schema, upcasted] | duration | store_id, event_type, from_version, to_version |
Memory pressure
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, memory, pressure_changed] | usage_ratio | old_level, new_level |
Consistency
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, consistency, check, complete] | duration_us | store_id, status, checks |
[reckon_db, consistency, status, changed] | system_time | store_id, old_status, new_status |
[reckon_db, consistency, split_brain, detected] | system_time | store_id, result |
[reckon_db, consistency, quorum, lost] | system_time | store_id, available_nodes, required_quorum |
[reckon_db, consistency, quorum, restored] | system_time | store_id, available_nodes, required_quorum |
Health prober
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, health, probe, complete] | duration_us, success_count, failure_count | store_id |
[reckon_db, health, node, failed] | system_time, consecutive_failures | store_id, node |
[reckon_db, health, node, recovered] | system_time | store_id, node |
Stream links
| Event | Measurements | Metadata |
|---|---|---|
[reckon_db, link, created] | system_time | store_id, link_name |
[reckon_db, link, deleted] | system_time | store_id, link_name |
[reckon_db, link, started] | system_time | store_id, link_name |
[reckon_db, link, stopped] | system_time | store_id, link_name |
What to watch
A pragmatic starter dashboard:
- Throughput / latency —
stream.write.stopandstream.read.stopcounts + duration histograms, grouped bystore_id. - Errors — rate of
stream.write.error. - Cluster health —
consistency.quorum.lost/quorum.restored,cluster.leader.elected(leadership churn),health.node.failed. - Backpressure —
subscription.backpressure.warning/droppedsignal that a subscriber can't keep up. - Memory —
memory.pressure_changedtransitions.
The event names are the source of truth; the definitive list with inline
measurement/metadata docs lives in include/reckon_db_telemetry.hrl.