Kepler.Source.Telemetry (Kepler v0.1.0)

Copy Markdown View Source

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.

AggregateHot-path cost
:count, :rateone :counters.add/3
:sumone map lookup, one :counters.add/3
:averageone map lookup, two :counters.add/3
percentile: none 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

entry()

@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.

recorder()

@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

handle(event_name, measurements, metadata, entries)

@spec handle([atom()], map(), map(), [entry()]) :: :ok

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.

install(watch, filter, ring, enricher)

@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.

sample(watch, ref, previous, window_ms)

@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.