Xqlite.Telemetry (Xqlite v0.7.0)
View Source:telemetry integration for xqlite.
Strictly opt-in
Telemetry is gated by a compile-time flag. Default: false.
When disabled, every emission site in xqlite compiles to a no-op —
there are no :telemetry.execute/3 or :telemetry.span/3 calls in
the bytecode at all. Zero per-call overhead. Designed for
resource-constrained environments (Nerves, embedded, hot loops)
where the cost of even an unused :telemetry.execute/3 matters.
To enable, set this in your application's config/config.exs and
rebuild xqlite (mix deps.compile xqlite --force):
config :xqlite, :telemetry_enabled, trueConventions
- Event names: atom lists prefixed with
:xqlite. Sub-systems get their own segment (:hook,:cancel,:transaction,:savepoint). - Spans: every operation that has a clear "start" and "end"
uses
:telemetry.span/3's convention —:start,:stop,:exceptionevents with a stable:telemetry_span_contextreference linking them. - Time units: integer nanoseconds everywhere. No
_nssuffix on key names —durationandmonotonic_timeare always nanoseconds. Convert to microseconds (/1_000) or milliseconds (/1_000_000) at handler time. - Time source:
System.monotonic_time(:nanosecond), not:os.system_time/0. Stable across NTP adjustments and clock changes; consumers map to wall-clock at handler time if needed. - Identifiers: raw refs (
reference()) for connections, tokens, streams. No abstraction layer — consumers map to stable IDs themselves at attach time. - Cancellation outcome: an operation that gets cancelled fires
its normal
:stopevent withmetadata.error_reason == :operation_cancelled(NOT:exception). A separate[:xqlite, :cancel, :honored]event also fires.
Event surface — Tier A (operations)
These events fire automatically when telemetry is compiled in. No
registration needed; just attach a handler with :telemetry.attach/4.
Connection lifecycle
[:xqlite, :open, :start | :stop | :exception]
measurements: %{monotonic_time, duration}
metadata: %{path, mode, result_class, error_reason}
[:xqlite, :close, :start | :stop]
measurements: %{monotonic_time, duration}
metadata: %{conn, path}:mode is one of :file, :memory, :readonly, :memory_readonly,
:temp. :result_class is :ok or :error. :error_reason is
nil on success or the structured error reason atom on failure.
Query / Execute
[:xqlite, :query, :start | :stop | :exception]
measurements: %{monotonic_time, duration, num_rows (on :stop)}
metadata: %{conn, sql, params_count, cancellable?, result_class, error_reason}
[:xqlite, :execute, :start | :stop | :exception]
measurements: %{monotonic_time, duration, affected_rows (on :stop)}
metadata: %{conn, sql, params_count, cancellable?, result_class, error_reason}
[:xqlite, :execute_batch, :start | :stop | :exception]
measurements: %{monotonic_time, duration}
metadata: %{conn, sql_batch_size_bytes, cancellable?, result_class, error_reason}
[:xqlite, :query_with_changes, :start | :stop | :exception]
measurements: %{monotonic_time, duration, num_rows, changes}
metadata: %{conn, sql, params_count, cancellable?, result_class, error_reason}
[:xqlite, :explain_analyze, :start | :stop | :exception]
measurements: %{monotonic_time, duration, wall_time_ns, rows_produced, scan_count}
metadata: %{conn, sql, result_class, error_reason}wall_time_ns is SQLite's own clock measurement of the executed
statement (from EXPLAIN ANALYZE). :cancellable? is true iff
the operation was invoked through a *_cancellable NIF or
Xqlite.query_cancellable etc.
Transactions
Transactions span across multiple NIF calls; we emit single events
rather than spans because the matching :stop may come from any
later commit/rollback at any time.
[:xqlite, :transaction, :begin]
measurements: %{monotonic_time}
metadata: %{conn, mode}
[:xqlite, :transaction, :commit]
measurements: %{monotonic_time}
metadata: %{conn}
[:xqlite, :transaction, :rollback]
measurements: %{monotonic_time}
metadata: %{conn, reason}
[:xqlite, :savepoint, :create | :release | :rollback_to]
measurements: %{monotonic_time}
metadata: %{conn, name}:mode is :deferred, :immediate, or :exclusive. :reason
on rollback is :user_initiated, :constraint, :deferred_fk,
or :error.
Streams
[:xqlite, :stream, :open, :start | :stop | :exception]
measurements: %{monotonic_time, duration}
metadata: %{conn, sql, batch_size, type_extensions_count}
[:xqlite, :stream, :fetch]
measurements: %{monotonic_time, duration, rows_returned}
metadata: %{stream_handle, done?}
[:xqlite, :stream, :close]
measurements: %{monotonic_time, total_duration, total_rows}
metadata: %{stream_handle, reason}[:xqlite, :stream, :fetch] fires every batch — potentially
thousands of times per stream. The cost is sub-microsecond when
no handler is attached and zero when telemetry is disabled at
compile time. If you attach a heavy handler, expect proportional
cost; consider sampling or a dedicated metrics handler.
Backup
[:xqlite, :backup, :start | :stop | :exception]
measurements: %{monotonic_time, duration, byte_size}
metadata: %{conn, schema, dest_path, result_class, error_reason}
[:xqlite, :backup_with_progress, :start | :step | :stop | :exception]
:start measurements: %{monotonic_time}
:step measurements: %{monotonic_time, pages_remaining, pagecount, step_duration}
:stop measurements: %{monotonic_time, total_duration, total_pages}
metadata: %{conn, schema, dest_path, pages_per_step, result_class, error_reason}WAL checkpoint, serialize, deserialize, extension
[:xqlite, :wal_checkpoint, :start | :stop | :exception]
measurements: %{monotonic_time, duration, log_pages, checkpointed_pages}
metadata: %{conn, mode, schema, busy?}
[:xqlite, :serialize, :start | :stop | :exception]
measurements: %{monotonic_time, duration, byte_size}
metadata: %{conn, schema}
[:xqlite, :deserialize, :start | :stop | :exception]
measurements: %{monotonic_time, duration, byte_size}
metadata: %{conn, schema, read_only?}
[:xqlite, :extension, :load, :start | :stop | :exception]
measurements: %{monotonic_time, duration}
metadata: %{conn, path, entry_point}WAL :mode is :passive, :full, :restart, or :truncate.
:busy? is true if the checkpoint did not complete because of
reader/writer contention.
PRAGMA
[:xqlite, :pragma, :get | :set]
measurements: %{monotonic_time, duration}
metadata: %{conn, name, value (on :set only)}Session extension
[:xqlite, :session, :new | :attach | :delete]
measurements: %{monotonic_time}
metadata: %{session, conn (on :new), table (on :attach)}
[:xqlite, :session, :changeset | :patchset]
measurements: %{monotonic_time, duration, byte_size}
metadata: %{session}
[:xqlite, :session, :apply, :start | :stop | :exception]
measurements: %{monotonic_time, duration}
metadata: %{conn, conflict_strategy}Blob I/O
[:xqlite, :blob, :open | :close | :reopen]
measurements: %{monotonic_time, byte_size (on :open / :reopen)}
metadata: %{blob, conn (on :open), table, column, row_id, read_only?}
[:xqlite, :blob, :read | :write]
measurements: %{monotonic_time, duration, bytes, offset}
metadata: %{blob}Cancellation
[:xqlite, :cancel, :token_created]
measurements: %{monotonic_time}
metadata: %{token}
[:xqlite, :cancel, :signalled]
measurements: %{monotonic_time}
metadata: %{token}
[:xqlite, :cancel, :honored]
measurements: %{monotonic_time, lag}
metadata: %{token, operation, conn}:lag is the duration in nanoseconds between
[:xqlite, :cancel, :signalled] and [:xqlite, :cancel, :honored]
for the same token. :operation is the operation that the cancel
signal interrupted: :query, :execute, :execute_batch, or
:backup_with_progress.
Event surface — Tier B (hook bridge, opt-in registration)
The hook bridge layer turns multi-subscriber hook deliveries into
telemetry events. NOT auto-attached — the user explicitly calls
bridge/2 on a connection to wire the hooks they care about.
[:xqlite, :hook, :busy]
measurements: %{retries, elapsed}
metadata: %{conn, tag}
[:xqlite, :hook, :commit]
measurements: %{monotonic_time}
metadata: %{conn, tag}
[:xqlite, :hook, :rollback]
measurements: %{monotonic_time}
metadata: %{conn, tag}
[:xqlite, :hook, :update]
measurements: %{monotonic_time}
metadata: %{conn, action, db_name, table, rowid, tag}
[:xqlite, :hook, :wal]
measurements: %{pages}
metadata: %{conn, db_name, tag}
[:xqlite, :hook, :progress]
measurements: %{count, elapsed}
metadata: %{conn, hook_tag, tag}
[:xqlite, :hook, :log]
measurements: %{}
metadata: %{code, base_code, message}:tag (in the metadata) is the user-supplied tag from bridge/2
for distinguishing connections in dashboards. :hook_tag (only
on [:xqlite, :hook, :progress]) is the tag passed to
Xqlite.register_progress_hook/3.
See bridge/2 and unbridge/2 for the registration API. Bridge
is implemented on top of the same multi-subscriber primitives that
power direct hook usage — registering the bridge on a connection
is independent of any other subscribers, and unbridging never
affects them.
Compile-time disabled mode
When :telemetry_enabled is false (the default), the macros in
this module expand to no-ops and the underlying operations skip
emission entirely. Verify with enabled?/0:
iex> Xqlite.Telemetry.enabled?()
falseIn this mode, bridge/2 returns {:error, :telemetry_disabled}
rather than silently registering hooks that produce no events.
Reading the source
This module is small on purpose. The two macros (emit/3 and
span/3) are what every call site in lib/xqlite/*.ex invokes.
They take the :telemetry_enabled flag at compile time and either
emit normal :telemetry calls or expand to direct evaluation of
the inner block. The macros live here, not in each caller, so the
compile-time check happens in one place.
Summary
Functions
Bridges per-connection hook deliveries into :telemetry events.
Bridges the global SQLite log hook into :telemetry events.
Returns whether telemetry is compiled in.
Returns the current monotonic time in nanoseconds.
Tears down a bridge — unregisters every subscribed hook and stops the forwarder GenServer.
Functions
Bridges per-connection hook deliveries into :telemetry events.
Subscribes to the requested hooks on conn via the standard
register_*_hook API and forwards each delivery as an
[:xqlite, :hook, :*] telemetry event. Returns
{:ok, %Xqlite.Telemetry.Bridge{}} on success — pass that struct
to unbridge/1 to tear down.
Options
:hooks— list of hook kinds to subscribe to. Either an explicit list ([:wal, :commit, :rollback, :update, :progress]) or:all(default) for every per-connection hook.:tag— arbitrary term forwarded as:tagin every[:xqlite, :hook, :*]event's metadata. Useful when one handler receives bridged events from multiple connections.:progress— keyword opts forwarded toregister_progress_hook/3(defaultevery_n: 1000).
Returns {:error, :telemetry_disabled} when telemetry is
compile-disabled — the bridge would otherwise install hooks that
produce nothing.
Note on busy_handler
busy_handler is single-subscriber and not part of the per-conn
bridge. To get busy events as telemetry, register your own busy
handler with a forwarder pid that emits the desired event.
See Xqlite.Telemetry.Bridge for the rationale.
Bridges the global SQLite log hook into :telemetry events.
Subscribes to the process-wide log hook and re-emits each diagnostic
as [:xqlite, :hook, :log]. Returns
{:ok, %Xqlite.Telemetry.Bridge{}} — call unbridge/1 to detach.
Options
:tag— arbitrary term forwarded as:tagin event metadata.
@spec enabled?() :: boolean()
Returns whether telemetry is compiled in.
Reads the value of :telemetry_enabled at xqlite compile time.
Always a constant after compilation; safe to call anywhere.
@spec monotonic_time() :: integer()
Returns the current monotonic time in nanoseconds.
Inlined helper used in metadata maps that record event timestamps.
Equivalent to System.monotonic_time(:nanosecond); provided for
readability at call sites and for a single canonical source for
the rest of xqlite.
@spec unbridge(struct()) :: :ok
Tears down a bridge — unregisters every subscribed hook and stops the forwarder GenServer.