Threadline.Health (Threadline v1.0.0)

Copy Markdown View Source

Reports capture health and trigger coverage for Threadline installations.

trigger_findings/1 and legacy_key_findings/1 return health findings, while trigger_coverage/1 lists covered, uncovered, and expected-uncovered user tables.

Queries the PostgreSQL system catalog to verify trigger installation status for all user tables.

Mix-task parity

See mix threadline.health.coverage for a viewer with --json, --schema=NAME, and --strict flags. Viewer by default (exits 0); --strict turns :error-severity findings into exit 1. The task does not exit non-zero on uncovered tables even with --strict; use mix threadline.verify_coverage for the positive-list CI gate.

Telemetry

On every successful call, trigger_coverage/1 emits [:threadline, :health, :checked] with measurements %{covered: integer, uncovered: integer, expected_uncovered: integer}. The expected_uncovered measurement is an additive — old subscribers reading only covered/uncovered keep working unchanged).

trigger_findings/1 emits [:threadline, :health, :findings_checked] with measurements %{errors: integer, warnings: integer}, counted over the findings list it returns. legacy_key_findings/1 emits no telemetry event.

Summary

Types

A host table name paired with its trigger coverage status.

A status assigned to a host table by trigger coverage.

A schema name or list of schema names used to select health findings.

Functions

Returns a list of Threadline.Health.Finding structs (:unresolved_legacy_keys) for audit rows captured before their table's trigger was regenerated and still carrying an unresolved primary key that a key-based row lookup cannot resolve. Unlike trigger_findings/1, which is catalog-only, this scans audit_changes per table.

Returns trigger coverage entries for user tables in a schema, defaulting to "public".

Returns a list of Threadline.Health.Finding structs describing detected capture problems: disabled or replica-only triggers, duplicate capture triggers, drifted or missing key columns, and shared per-table functions.

Types

coverage_entry()

@type coverage_entry() :: {coverage_status(), String.t()}

A host table name paired with its trigger coverage status.

coverage_status()

@type coverage_status() :: :covered | :uncovered | :expected_uncovered

A status assigned to a host table by trigger coverage.

legacy_key_findings_opt()

@type legacy_key_findings_opt() ::
  Threadline.repo_opt()
  | {:schema, schema_filter()}
  | {:statement_timeout, pos_integer()}

An option accepted by legacy_key_findings/1.

schema_filter()

@type schema_filter() :: String.t() | [String.t()]

A schema name or list of schema names used to select health findings.

trigger_coverage_opt()

@type trigger_coverage_opt() :: Threadline.repo_opt() | {:schema, String.t()}

An option accepted by trigger_coverage/1.

trigger_findings_opt()

@type trigger_findings_opt() :: Threadline.repo_opt() | {:schema, schema_filter()}

An option accepted by trigger_findings/1.

Functions

legacy_key_findings(opts)

@spec legacy_key_findings([legacy_key_findings_opt()]) :: [
  Threadline.Health.Finding.t()
]

Returns a list of Threadline.Health.Finding structs (:unresolved_legacy_keys) for audit rows captured before their table's trigger was regenerated and still carrying an unresolved primary key that a key-based row lookup cannot resolve. Unlike trigger_findings/1, which is catalog-only, this scans audit_changes per table.

Options

  • :repo — Ecto.Repo module. Required.
  • :schema — schema name string or list of strings. Optional. Omitting it covers every non-system schema.
  • :statement_timeout — milliseconds. Defaults to 15_000. Applied with a transaction-local setting, so it is safe through PgBouncer transaction pooling. When the timeout elapses — typically a missing row-history index — this function raises Postgrex.Error with postgres code :query_canceled; see Step 4.

Each table's probe is capped at 10,000 rows; a capped finding's details["unresolved_count"] is 10000 and its message reads "at least 10000". DELETE rows, rows whose key columns were redacted or are otherwise absent from data_after, and dropped tables are never counted — see What cannot be recovered. A finding's details map has string keys "unresolved_count" (integer), "capped" (boolean), and "key_columns" (list of strings).

Emits no telemetry event.

Unknown option keys are ignored.

Example

Threadline.Health.legacy_key_findings(repo: MyApp.Repo)
#=> [%Threadline.Health.Finding{code: :unresolved_legacy_keys, ...}]

trigger_coverage(opts)

@spec trigger_coverage([trigger_coverage_opt()]) :: [coverage_entry()]

Returns trigger coverage entries for user tables in a schema, defaulting to "public".

Audit tables (audit_transactions, audit_changes, audit_actions) are excluded from the result — they are not expected to have triggers (CAP-10).

A third tuple variant {:expected_uncovered, name} is supported for bookkeeping tables that are intentionally not audited (e.g. schema_migrations). The bucket is computed from a hardcoded baseline plus config :threadline, :health, expected_uncovered_tables: [...], with :audit_anyway removing entries from the union.

Options

  • :repo — Ecto.Repo module. Required.
  • :schema — schema name string. Defaults to "public". Programmatic callers are responsible for sanitizing or trusting their own input — this function does NOT validate :schema against pg_namespace. Surfaces that take untrusted input (LV / Mix task) MUST validate at the edge.

A disabled or replica-only trigger no longer counts as covered — see trigger_findings/1, which reports it as :capture_trigger_disabled.

Other option keys are ignored.

Returns

  • A list of coverage_entry() values in table-name order.
  • Raises KeyError when :repo is missing; repository errors are reraised.

Example

Threadline.Health.trigger_coverage(repo: MyApp.Repo) #=> [{:covered, "users"}, {:expected_uncovered, "schema_migrations"}, {:uncovered, "orders"}]

trigger_findings(opts)

@spec trigger_findings([trigger_findings_opt()]) :: [Threadline.Health.Finding.t()]

Returns a list of Threadline.Health.Finding structs describing detected capture problems: disabled or replica-only triggers, duplicate capture triggers, drifted or missing key columns, and shared per-table functions.

Options

  • :repo — Ecto.Repo module. Required.
  • :schema — schema name string or list of strings. Optional. Omitting it covers every non-system schema (excludes pg_catalog, information_schema, pg_toast*, pg_temp*, and the configured Threadline storage schema's own tables). This is deliberately different from trigger_coverage/1's "public" default: two tables with the same name in different schemas, and a per-table function shared across schemas, cannot be seen one schema at a time. The shared-function check always scans the whole catalog regardless of :schema — the option only filters which findings, by their table's schema, are returned.

A malformed :trigger_capture config raises the same ArgumentError that the internal trigger-capture config loader raises for capture itself.

Findings are sorted by {schema, table, code}, with message as the final tie-break, so two consecutive calls return identical lists. One table may produce more than one finding; there is no short-circuit.

Unknown option keys are ignored.

Example

Threadline.Health.trigger_findings(repo: MyApp.Repo)
#=> [%Threadline.Health.Finding{code: :capture_trigger_disabled, ...}]