Tier 1: telemetry events folded into lock-free counters.
handle/4 is the only Kepler code that runs on your processes. Telemetry
handlers execute inline in whatever emitted the event, so a slow handler
slows your checkout path directly. This one does an atomic increment and
returns.
| Aggregate | Hot-path cost |
|---|---|
:count, :rate | one :counters.add/3 |
:sum | one map lookup, one :counters.add/3 |
:average | one map lookup, two :counters.add/3 |
percentile: n | one map lookup, a handful of integer ops, one :counters.add/3 |
Adding filter costs one function call; adding context costs one ETS
insert. Nothing here allocates otherwise, and nothing sends a message.
Values that are missing or not numbers are skipped rather than raised on. A
telemetry handler that raises is detached by :telemetry — permanently, and
after logging on the host's process — so being strict here would mean one
malformed event silently disabling a watch.
Summary
Types
One watch's hot-path work for a telemetry event.
What to increment. Built once at install; never rebuilt at runtime.
Functions
The :telemetry handler. Runs on the emitting process.
Builds the counters and hot-path entry for a telemetry watch.
Reads the counters and folds them into this tick's number.
Types
@type entry() :: {:plain, recorder()} | {:full, recorder(), (map(), map() -> boolean()) | nil, Kepler.RingBuffer.t() | nil, :all | [atom()], Kepler.RingBuffer.t() | nil, [atom()]}
One watch's hot-path work for a telemetry event.
A watch with no filter and no captures compiles to {:plain, recorder}, so
the common case is a two-element match before the increment. Anything else
carries its optional work: the recent ring, and the size-1 ring holding the
enrich fields of the latest event. Each of those costs one ETS write.
@type recorder() :: {:count, :counters.counters_ref()} | {:sum | :average | :histogram, :counters.counters_ref(), atom(), tuple() | nil}
What to increment. Built once at install; never rebuilt at runtime.
Functions
The :telemetry handler. Runs on the emitting process.
Several watches can share one event name, in which case they arrive here as
one list and are folded in a single pass, so :telemetry's own lookup
happens once regardless of how many watches you declared.
@spec install( Kepler.Watch.t(), (map(), map() -> boolean()) | nil, Kepler.RingBuffer.t() | nil, Kepler.RingBuffer.t() | nil ) :: {:counters.counters_ref(), entry()}
Builds the counters and hot-path entry for a telemetry watch.
Returns {counters_ref, entry}. The ref is kept by Kepler.Registry for the
poller to read on the tick; the entry is handed to :telemetry as handler
config, so nothing has to be looked up when an event arrives.
@spec sample(Kepler.Watch.t(), :counters.counters_ref(), term(), pos_integer()) :: {number() | :no_data, term()}
Reads the counters and folds them into this tick's number.
Counters only grow, so every aggregate is a difference against the previous
read. Returns {value, snapshot}, where value is :no_data when nothing
was observed and there is therefore no honest number to compare — the poller
holds the watch's state rather than inventing a zero.