Threadline.Telemetry (Threadline v0.12.0)

Copy Markdown View Source

Telemetry integration helpers for Threadline.

Threadline emits the following :telemetry events. No event carries row values, actor identifiers, correlation ids, or free-text reasons — with one narrow exception: [:threadline, :retention, :purge, :exception]'s reason/stacktrace metadata, forwarded verbatim from a raised exception for incident diagnosis (see the Telemetry guide's "Keep row data out of your handlers" section before logging it anywhere durable). See the guide for an attach_many example per family, a telemetry_metrics example, handler-safety and cardinality warnings, and a recipe for observing Threadline's own repo queries through your host's [:my_app, :repo, :query] event.

EventMeasurementsMetadataWhen emitted
[:threadline, :transaction, :committed]table_count—an AuditTransaction is committed
[:threadline, :action, :recorded]status—Threadline.record_action/2 completes, whether it succeeds or fails
[:threadline, :health, :checked]covered, expected_uncovered, uncovered—Threadline.Health.trigger_coverage/1 returns
[:threadline, :health, :checked, :error]—exceptiona polled coverage check raises
[:threadline, :health, :findings_checked]errors, warnings—Threadline.Health.trigger_findings/1 returns
[:threadline, :operator_surface, :authorize]resultpath, scope_keysan operator-surface mount or request is authorized, denied, or errors
[:threadline, :operator_surface, :export_authorize]count, result—an export-specific authorization check raises
[:threadline, :operator_surface, :actor_ref_mismatch]count—the session actor and the scope-derived actor disagree
[:threadline, :export, :completed]duration, row_countformat, truncatedan export (eager CSV/JSON, the async orchestrator job, or the chunked operator-surface download) finishes successfully
[:threadline, :export, :failed]duration, row_countformat, error_kind, exceptionan export (eager CSV/JSON, the async orchestrator job, or the chunked operator-surface download) fails
[:threadline, :retention, :purge, :start]monotonic_time, system_timedry_run, telemetry_span_contextafter purge/1's input checks pass, when the purge work begins
[:threadline, :retention, :purge, :stop]batches_run, deleted_changes, deleted_transactions, duration, monotonic_timedry_run, telemetry_span_contextwhen the run or preview returns
[:threadline, :retention, :purge, :exception]duration, monotonic_timedry_run, kind, reason, stacktrace, telemetry_span_contextwhen the database raises mid-run
[:threadline, :retention, :batch_purged]deleted_changes, deleted_transactions, duration—after a purge_loop step's change delete_all and full orphan drain both return, once per step including the terminating empty one

[:threadline, :transaction, :committed] is automatically emitted (with table_count: 0) when Threadline.record_action/2 succeeds. For accurate per-transaction counts, call Threadline.Telemetry.transaction_committed/2 explicitly after a known DB transaction commit.

Every emission in this library goes through a @doc false helper function in this module; :telemetry.execute/3 and :telemetry.span/3 are called only here. Every event this module can emit, along with its measurement and metadata keys, is held in an internal registry exposed through __events__/0 (@doc false) — not a public events/0 — for use by this library's own test suite.

Usage

Attach handlers in your application's start/2 callback:

:telemetry.attach(
  "my-app-audit",
  [:threadline, :action, :recorded],
  &MyApp.Instrumentation.handle_event/4,
  nil
)

Summary

Functions

Emits the [:threadline, :operator_surface, :actor_ref_mismatch] event with %{count: 1} measurements and no metadata, as a pure incidence counter when the session actor and the scope-derived actor disagree.

Emits the [:threadline, :operator_surface, :export_authorize] event with %{result: :error, count: 1} measurements and no metadata, for an export-specific authorization callback that raised.

Emits the [:threadline, :export, :completed] event for one logical export that finished successfully.

Emits the [:threadline, :export, :failed] event for one logical export that failed.

Emits the [:threadline, :health, :findings_checked] event with error and warning counts, measured over the list Threadline.Health.trigger_findings/1 is about to return.

Emits the [:threadline, :health, :checked] event with covered / uncovered / expected_uncovered measurements.

Emits the [:threadline, :health, :checked, :error] event when a polled coverage check fails. The dashboard keeps the last-good snapshot and ALWAYS reschedules the next poll; this event lets adopters alert on transient or sustained failure.

Emits the [:threadline, :operator_surface, :authorize] event.

Emits [:threadline, :transaction, :committed] with the given table count.

Functions

emit_actor_ref_mismatch()

Emits the [:threadline, :operator_surface, :actor_ref_mismatch] event with %{count: 1} measurements and no metadata, as a pure incidence counter when the session actor and the scope-derived actor disagree.

emit_export_authorize_error()

Emits the [:threadline, :operator_surface, :export_authorize] event with %{result: :error, count: 1} measurements and no metadata, for an export-specific authorization callback that raised.

emit_export_completed(format, row_count, truncated, started_at)

Emits the [:threadline, :export, :completed] event for one logical export that finished successfully.

format is the user-facing export format (:csv, :json, or :ndjson — the async orchestrator job is always :csv). row_count is the number of rows returned or streamed. truncated is whether the export hit its row cap. started_at is a System.monotonic_time/0 value captured by the caller before the export began; this helper computes duration from it.

emit_export_failed(format, row_count, error_kind, exception, started_at)

Emits the [:threadline, :export, :failed] event for one logical export that failed.

row_count is the number of rows written or streamed before the failure (0 for the eager functions, since they fail before returning anything). error_kind is one of :exception, :client_closed, :storage_error, or :transaction_failed. exception is the raised exception struct, or nil when the failure was not a raise — only the struct's module is forwarded, never its message, which can echo audited database values. started_at is the same System.monotonic_time/0 value passed to emit_export_completed/4.

emit_findings_checked(errors, warnings)

Emits the [:threadline, :health, :findings_checked] event with error and warning counts, measured over the list Threadline.Health.trigger_findings/1 is about to return.

emit_health_checked(covered, uncovered, expected_uncovered)

Emits the [:threadline, :health, :checked] event with covered / uncovered / expected_uncovered measurements.

The expected_uncovered measurement key is (additive). External subscribers that destructure only %{covered: c, uncovered: u} continue to work unchanged.

emit_health_checked_error(exception)

Emits the [:threadline, :health, :checked, :error] event when a polled coverage check fails. The dashboard keeps the last-good snapshot and ALWAYS reschedules the next poll; this event lets adopters alert on transient or sustained failure.

Takes the raised exception struct itself, not a message. Metadata is %{exception: module} — the exception's struct module only. The message is intentionally not forwarded: exception messages can echo database values.

emit_operator_surface_authorize(result, path_or_nil, scope)

Emits the [:threadline, :operator_surface, :authorize] event.

result is the authorization outcome atom (:granted, :denied, or :error). path_or_nil is a fixed, caller-supplied path string (the mount's own compile-time route template, not a live request path), or nil when the caller has none to offer (a LiveView mount, or an HTTP auth plug that chooses not to forward one). Callers must never derive this value from a live conn.request_path/similar — doing so could forward a dynamic, possibly-identifying route segment (e.g. a tenant id a host nested the mount under); see the Telemetry guide's cardinality warning. scope is the host-returned scope map, or nil/anything else when there is none. Metadata is %{path: binary, scope_keys: [atom]} — scope_keys holds only the scope map's KEYS, sorted, never its values, so no identity data is forwarded.

transaction_committed(transaction, opts \\ [])

Emits [:threadline, :transaction, :committed] with the given table count.

Call this after a DB transaction that you know produced AuditTransaction records, when you need accurate table_count measurements.

Example

{:ok, txn} = MyApp.Repo.transaction(fn ->
  # ... your writes ...
end)
Threadline.Telemetry.transaction_committed(txn, table_count: 3)