# `Lotus.Telemetry`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.1.0/lib/lotus/telemetry.ex#L1)

Telemetry events emitted by Lotus.

Lotus uses `:telemetry` to emit events for query execution, cache operations,
and schema introspection. You can attach handlers to these events for monitoring,
logging, or integration with tools like Phoenix LiveDashboard or AppSignal.

## Run Events

A run is one call to `Lotus.run_query/2`, `Lotus.run_statement/3` or
`Lotus.Runner.run_statement/3`: `:before_query`, the execute step (or the
result cache), `:before_execute`, `:after_query`. The run events bracket all
of it, on every path — a result served from the cache, a statement a plug
halted in any phase — and always carry the caller's `:context`. A consumer
that records who ran what, whether it was served from the cache and whether
it was refused attaches to these three events.

The query events below cover only the execute step: a result served from
the cache emits no query event, and neither does a run a plug halted before
execution.

### `[:lotus, :run, :start]`

**Measurements:** `:system_time`.

**Metadata:**

  * `:source` - The data source name
  * `:statement` - The `Lotus.Query.Statement` the caller supplied, before
    any `:before_query` rewrite
  * `:context` - The caller-supplied context (or `nil`)
  * `:vars` - The bound query variables, by name

### `[:lotus, :run, :stop]`

Emitted when a run completes and its result is returned to the caller.

**Measurements:** `:duration` (native units), `:row_count`.

**Metadata:** the start metadata, with `:statement` now the statement that
ran, plus:

  * `:result` - The `Lotus.Result` returned to the caller
  * `:relations` - What preflight knew about the statement: a list of
    `{schema, table}`, `{:unrestricted, reason}` or `{:skipped, reason}`
  * `:origin` - `:executed` or `:cached`

### `[:lotus, :run, :exception]`

Emitted when any phase of a run fails, including a middleware halt.

**Measurements:** `:duration`.

**Metadata:** the start metadata plus:

  * `:phase` - `:before_query`, `:sanitize`, `:preflight`,
    `:before_execute`, `:execute` or `:after_query`
  * `:reason` - The error or halt reason the caller receives
  * `:kind` - `:error`
  * `:statement`, `:relations`, `:origin` - Present when the failure came
    after the execute step, describing what ran

## Query Events

### `[:lotus, :query, :start]`

Emitted when a query begins execution.

**Measurements:**

  * `:system_time` - The system time at the start of the query (in native units)

**Metadata:**

  * `:source` - The data source name (e.g. `"main"`, `"warehouse"`)
  * `:statement` - The `Lotus.Query.Statement` being executed. Its `:body`
    is the adapter-native payload and `:params` the bound values.
  * `:context` - The caller-supplied context (or `nil`)

### `[:lotus, :query, :stop]`

Emitted when a query completes successfully.

**Measurements:**

  * `:duration` - The query duration (in native time units)
  * `:row_count` - The number of rows returned

**Metadata:**

  * `:source` - The data source name
  * `:statement` - The `Lotus.Query.Statement` that was executed
  * `:context` - The caller-supplied context (or `nil`)
  * `:result` - The `Lotus.Result` struct

### `[:lotus, :query, :exception]`

Emitted when a query fails with an exception.

**Measurements:**

  * `:duration` - The time elapsed before the failure (in native time units)

**Metadata:**

  * `:source` - The data source name
  * `:statement` - The `Lotus.Query.Statement` that was executed
  * `:context` - The caller-supplied context (or `nil`)
  * `:kind` - The kind of exception (`:error`, `:exit`, or `:throw`)
  * `:reason` - The exception or error reason
  * `:stacktrace` - The stacktrace

## Cache Events

### `[:lotus, :cache, :hit]`

Emitted when a cache lookup finds an existing entry.

**Measurements:**

  * `:count` - Always `1`

**Metadata:**

  * `:key` - The cache key

### `[:lotus, :cache, :miss]`

Emitted when a cache lookup does not find an entry.

**Measurements:**

  * `:count` - Always `1`

**Metadata:**

  * `:key` - The cache key

### `[:lotus, :cache, :put]`

Emitted when a value is stored in the cache.

**Measurements:**

  * `:count` - Always `1`

**Metadata:**

  * `:key` - The cache key
  * `:ttl_ms` - The TTL in milliseconds

## Schema Introspection Events

### `[:lotus, :schema, :introspection, :start]`

Emitted when a schema introspection operation begins.

**Measurements:**

  * `:system_time` - The system time at the start (in native units)

**Metadata:**

  * `:operation` - The introspection operation (e.g., `:list_schemas`, `:list_tables`,
    `:describe_table`, `:get_table_stats`, `:list_relations`)
  * `:source` - The data source name

### `[:lotus, :schema, :introspection, :stop]`

Emitted when a schema introspection operation completes.

**Measurements:**

  * `:duration` - The operation duration (in native time units)

**Metadata:**

  * `:operation` - The introspection operation
  * `:source` - The data source name
  * `:result` - `:ok` or `:error`

## Content Events

A content change is one create, update or delete of a query, visualization,
dashboard, dashboard card, dashboard filter or filter mapping, including
enabling or disabling public sharing and moving a card in a reorder. The
content events bracket each change on every path — a refusal, a failed
validation, an update that writes nothing — and always carry the caller's
`:context`. A reorder emits one span per card it moves.

### `[:lotus, :content, :change, :start]`

**Measurements:** `:system_time`.

**Metadata:**

  * `:op` - `:create`, `:update`, `:delete`, `:enable_sharing` or
    `:disable_sharing`
  * `:resource` - `:query`, `:visualization`, `:dashboard`,
    `:dashboard_card`, `:dashboard_filter` or `:filter_mapping`
  * `:record` - The struct as stored, or `nil` on a create
  * `:context` - The caller-supplied context (or `nil`)

### `[:lotus, :content, :change, :stop]`

Emitted when the change completes. The metadata is the
`:after_content_change` middleware payload. It is also emitted for an update
that wrote nothing, which fires no `:after_content_change`.

**Measurements:** `:duration` (native units).

**Metadata:** the start metadata, with `:record` now the struct as written,
plus:

  * `:changes` - Each changed field with its written value; `%{}` on a
    delete and on an update that wrote nothing

### `[:lotus, :content, :change, :exception]`

Emitted when the change fails: a `:before_content_change` plug refused it, the
changeset was invalid, or the write raised, threw or exited. In a reorder,
every card change of the call shares the outcome.

**Measurements:** `:duration`.

**Metadata:** the start metadata plus:

  * `:kind` - `:error`, or `:throw` or `:exit` when the call did not return
  * `:reason` - The error the caller receives: `{:halted, reason}` for a
    refusal, the `Ecto.Changeset` for a failed validation
  * `:stacktrace` - Present when the call raised, threw or exited

## Example

Attach a handler in your application's `start/2` callback:

    :telemetry.attach_many(
      "lotus-logger",
      [
        [:lotus, :query, :stop],
        [:lotus, :query, :exception],
        [:lotus, :cache, :hit],
        [:lotus, :cache, :miss]
      ],
      &MyApp.TelemetryHandler.handle_event/4,
      nil
    )

A simple logging handler:

    defmodule MyApp.TelemetryHandler do
      require Logger

      def handle_event([:lotus, :query, :stop], measurements, metadata, _config) do
        duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
        Logger.info("Lotus query completed in #{duration_ms}ms, rows: #{measurements.row_count}")
      end

      def handle_event([:lotus, :query, :exception], measurements, metadata, _config) do
        duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
        Logger.error("Lotus query failed after #{duration_ms}ms: #{inspect(metadata.reason)}")
      end

      def handle_event([:lotus, :cache, :hit], _measurements, metadata, _config) do
        Logger.debug("Lotus cache hit: #{metadata.key}")
      end

      def handle_event([:lotus, :cache, :miss], _measurements, metadata, _config) do
        Logger.debug("Lotus cache miss: #{metadata.key}")
      end
    end

---

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