# `ExDataSketch.Telemetry`
[🔗](https://github.com/thanos/ex_data_sketch/blob/main/lib/ex_data_sketch/telemetry.ex#L1)

Structured telemetry event emission for ExDataSketch.

This module provides a unified interface for emitting telemetry events
at meaningful operation boundaries within the ExDataSketch library.
Individual `update/2` calls do **not** emit events (they can run at
billions per second). Instead, events are emitted at batch/compound
operations like `from_enumerable/2`, `merge_many/1`, `serialize/1`,
`deserialize/1`, and all storage and pipeline operations.

## Configuration

Telemetry can be disabled entirely or per-category:

    # Disable all telemetry (default: true)
    config :ex_data_sketch, telemetry_enabled: false

    # Disable specific categories (default: all true)
    config :ex_data_sketch, telemetry: [
      sketch: true,
      persistence: true,
      stream: true,
      pipeline: true,
      window: true,
      server: true
    ]

When telemetry is disabled, `:telemetry.execute/3` is never called,
ensuring zero overhead in production.

## Event Names

### Sketch Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :sketch, :ingest]` | `duration`, `size_bytes` | `sketch_type` |
| `[:ex_data_sketch, :sketch, :merge]` | `duration`, `merge_count` | `sketch_type` |
| `[:ex_data_sketch, :sketch, :serialize]` | `duration`, `size_bytes` | `sketch_type` |
| `[:ex_data_sketch, :sketch, :deserialize]` | `duration`, `size_bytes` | `sketch_type` |

> **Note on `:ingest` coverage:** Emitted from `from_enumerable/2`, which
> `ExDataSketch.XorFilter` (built via `build/2`, a one-shot immutable
> construction with no telemetry wrapper) and `ExDataSketch.FilterChain`
> (no `from_enumerable/2` of its own -- it wraps already-built
> sub-sketches) do not have, so neither ever emits this event. Of the 14
> families that do, all report `size_bytes` alongside `duration` except
> `ExDataSketch.Cuckoo` (its `put_many/2` returns `{:ok, sketch} |
> {:error, :full, sketch}`, not a bare sketch, so its `:ingest` wrapper
> only reports `duration`).

### Persistence Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :persistence, :save]` | `duration`, `size_bytes` | `sketch_type`, `backend`, `key` |
| `[:ex_data_sketch, :persistence, :load]` | `duration` | `sketch_type`, `backend`, `key` |
| `[:ex_data_sketch, :persistence, :merge]` | `duration` | `sketch_type`, `backend`, `key` |
| `[:ex_data_sketch, :persistence, :delete]` | `duration` | `backend`, `key` |

> **Note on `:delete` metadata:** No `sketch_type` is available at deletion
> time because the sketch struct has already been discarded.

### Stream Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :stream, :reduce]` | (none) | `sketch_type` |
| `[:ex_data_sketch, :stream, :partition_merge]` | `duration`, `partition_count` | `sketch_type` |

> **Note on `:reduce` measurements:** The `Flow.reduce/3` integration emits
> this event as a completion signal from `Flow.on_trigger/2`. Because the
> reduce runs inside the Flow runtime, the timing is not accessible; the
> event carries no measurements beyond `sketch_type` in metadata.

### Pipeline Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :pipeline, :accumulate]` | `duration`, `count` | `sketch_type`, `batch_size` |
| `[:ex_data_sketch, :pipeline, :periodic_flush]` | `duration` | `sketch_type` |

> **Note on `:periodic_flush` duration:** The `duration` measurement
> represents the time elapsed since the previous flush (or since process
> start), not the time taken to perform the flush itself.

### Window Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :window, :roll]` | `slot_count`, `dropped_count` | `sketch_type`, `oldest_age_ms` |

> **Note on `:roll`:** Emitted by `ExDataSketch.Window.update/2,3`,
> `update_many/2`, and `tick/2` whenever at least one slot ages out of the
> `keep` window. Not emitted by `estimate/1` or `merged/1`, which filter
> expired slots transiently for the read without mutating or persisting
> the window's stored state.

### Server Events

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:ex_data_sketch, :server, :snapshot]` | `duration`, `size_bytes` | `sketch_type`, `backend`, `key` |
| `[:ex_data_sketch, :server, :snapshot_failed]` | `duration` | `sketch_type`, `backend`, `key`, `reason` |
| `[:ex_data_sketch, :server, :restore]` | `duration` | `sketch_type`, `backend`, `key`, `found` |
| `[:ex_data_sketch, :server, :flush]` | `duration` | `sketch_type` |
| `[:ex_data_sketch, :server, :drop]` | `queue_len` | `sketch_type` |

> **Note on `:restore`:** Emitted once, when `ExDataSketch.Server` starts,
> whenever `:snapshot` is configured. `found` is `false` both when no
> snapshot exists yet and when loading one failed for any other reason;
> either way the server starts from a fresh sketch.
>
> **Note on `:drop`:** Emitted when `:max_queue` is configured and an
> `update/2` or `update_many/2` cast arrives while the server's mailbox is
> at or above that threshold. The update is discarded, not queued.
> `update_sync/2` is never subject to `:max_queue` and never emits this
> event.

## Usage

Users attach handlers via `:telemetry.attach/4`:

     :telemetry.attach("my-handler", [:ex_data_sketch, :sketch, :ingest], fn _name, measurements, metadata, _config ->
       Logger.info("Ingested #{metadata.sketch_type}: #{measurements.size_bytes} bytes in #{measurements.duration} ns")
     end, nil)

Or use `ExDataSketch.Telemetry.OpenTelemetry.setup/0` to bridge to
OpenTelemetry spans when the `:opentelemetry_api` dependency is available.

# `event_name`

```elixir
@type event_name() :: [atom(), ...]
```

# `measurements`

```elixir
@type measurements() :: %{required(atom()) =&gt; number()}
```

# `metadata`

```elixir
@type metadata() :: %{required(atom()) =&gt; term()}
```

# `all_event_names`

```elixir
@spec all_event_names() :: [event_name()]
```

Returns all canonical event names.

## Examples

    iex> length(ExDataSketch.Telemetry.all_event_names()) > 0
    true

# `categories`

```elixir
@spec categories() :: [atom()]
```

Returns all supported event categories.

## Examples

    iex> ExDataSketch.Telemetry.categories()
    [:sketch, :persistence, :stream, :pipeline, :window, :server]

# `enabled?`

```elixir
@spec enabled?(atom()) :: boolean()
```

Returns whether telemetry events should be emitted for the given category.

Checks the global `telemetry_enabled` config first, then the per-category
config under the `:telemetry` key.

## Examples

    iex> is_boolean(ExDataSketch.Telemetry.enabled?(:sketch))
    true

# `event_name`

```elixir
@spec event_name(atom(), atom()) :: event_name()
```

Returns the canonical event name for a given event type.

Useful for attaching handlers programmatically.

## Examples

    iex> ExDataSketch.Telemetry.event_name(:sketch, :ingest)
    [:ex_data_sketch, :sketch, :ingest]

    iex> ExDataSketch.Telemetry.event_name(:persistence, :save)
    [:ex_data_sketch, :persistence, :save]

# `execute`

```elixir
@spec execute(event_name(), measurements(), metadata(), atom()) :: :ok
```

Emits a telemetry event if telemetry is enabled for the given category.

This function checks both the global `telemetry_enabled` config and the
per-category config before emitting. When disabled, it returns `:ok`
immediately without calling `:telemetry.execute/3`.

## Arguments

- `event_name` -- the telemetry event name as a list of atoms.
- `measurements` -- a map of numeric measurements.
- `metadata` -- a map of event metadata.
- `category` -- the event category (`:sketch`, `:persistence`,
  `:stream`, `:pipeline`, or `:window`).

## Examples

    ExDataSketch.Telemetry.execute(
      [:ex_data_sketch, :sketch, :ingest],
      %{count: 1000, duration: 500_000},
      %{sketch_type: :hll},
      :sketch
    )

# `sketch_type`

```elixir
@spec sketch_type(struct()) :: atom()
```

Returns the sketch type atom for a sketch struct.

Used in telemetry metadata to identify which sketch type produced an event.

## Examples

    iex> ExDataSketch.Telemetry.sketch_type(%ExDataSketch.HLL{})
    :hll

    iex> ExDataSketch.Telemetry.sketch_type(%ExDataSketch.CMS{})
    :cms

# `sketch_type_from_module`

```elixir
@spec sketch_type_from_module(module()) :: atom()
```

Returns the sketch type atom for a sketch module.

Used internally by storage backends to derive telemetry metadata
from a module atom (e.g., `ExDataSketch.HLL` -> `:hll`).

## Examples

    iex> ExDataSketch.Telemetry.sketch_type_from_module(ExDataSketch.HLL)
    :hll

    iex> ExDataSketch.Telemetry.sketch_type_from_module(ExDataSketch.CMS)
    :cms

# `span`

```elixir
@spec span(event_name(), measurements(), metadata(), atom(), (-&gt; result)) :: result
when result: var
```

Executes a function and emits a start/stop telemetry event with duration.

The `event_name` is used as-is for the stop event. Measurements include
`duration` in native time units. Base measurements are merged with the
computed duration.

Returns the result of `fun.`

## Arguments

- `event_name` -- the telemetry event name.
- `base_measurements` -- a map of pre-computed measurements (can be empty).
- `metadata` -- a map of event metadata.
- `category` -- the event category.
- `fun` -- the zero-arity function to time.

## Examples

    result = ExDataSketch.Telemetry.span(
      [:ex_data_sketch, :sketch, :merge],
      %{merge_count: 10},
      %{sketch_type: :hll},
      :sketch,
      fn -> ExDataSketch.HLL.merge_many(sketches) end
    )

# `span_with_result`

```elixir
@spec span_with_result(
  event_name(),
  measurements(),
  metadata(),
  atom(),
  (-&gt; result),
  (result -&gt;
     measurements())
) ::
  result
when result: var
```

Emits a telemetry event with timing, returning the result alongside
derived measurements.

Similar to `span/5` but accepts a callback that receives the result and
returns additional measurements to merge. This is useful when measurements
depend on the result (e.g., `size_bytes` from `HLL.from_enumerable/2`).

## Arguments

- `event_name` -- the telemetry event name.
- `base_measurements` -- a map of pre-computed measurements.
- `metadata` -- a map of event metadata.
- `category` -- the event category.
- `fun` -- the zero-arity function to time.
- `result_callback` -- a function receiving the result and returning
  a map of additional measurements to merge.

## Examples

    {sketch, measurements} = ExDataSketch.Telemetry.span_with_result(
      [:ex_data_sketch, :sketch, :ingest],
      %{},
      %{sketch_type: :hll},
      :sketch,
      fn -> ExDataSketch.HLL.from_enumerable(items, p: 14) end,
      fn sketch -> %{size_bytes: ExDataSketch.HLL.size_bytes(sketch)} end
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
