Times every query a given Ecto.Repo runs, kind: "query". Call once at startup, passing the
repo module itself:
ForgeOpsTracker.Integrations.Ecto.attach(MyApp.Repo)Unlike ForgeOpsTracker.Integrations.Phoenix/Oban, this needs the repo handed over explicitly
rather than attaching to one fixed global event name: confirmed directly against the installed
ecto/ecto_sql source, Ecto.Adapters.SQL's own query logging emits telemetry_prefix ++ [:query], where telemetry_prefix defaults to the repo module's own name, underscored
(MyApp.Repo -> [:my_app, :repo]), but is stored on the repo's own runtime config
(repo.config()[:telemetry_prefix], which also respects a host app that set a custom prefix
explicitly), not a name this module could otherwise guess. Call once per repo if the host app
has more than one.
measurements.total_time is already query_time + decode_time + queue_time summed (Ecto's own
Ecto.Adapters.SQL computes this exact total for its own query logging), native time units.
metadata.source is the table name directly for any query built from Ecto.Query (the common
case), nil for a raw SQL call; see ForgeOpsTracker.QueryNaming for how each is turned into a
low-cardinality transaction_name.
Also records an automatic "query" breadcrumb (message is just transaction_name, data holds
duration_ms), gated independently on track_breadcrumbs the same way performance recording is
independently gated on track_performance.
Also records a "database" span per query into whatever trace the calling process has open (the
Phoenix or Oban integration opens one), named like the transaction. Its data carries
"db.statement", the SQL with every string and number replaced by ?, and "db.system", from
the repo's adapter (Ecto.Adapters.Postgres is "postgresql"). Query params are never sent.
With explain_slow_queries on, a slow plain SELECT on a PostgreSQL repo is also handed to
ForgeOpsTracker.QueryPlanner, which EXPLAINs it later through explain_with_repo/3.
Summary
Functions
Registers the telemetry handler for repo. Safe to call more than once for the same repo.
Runs EXPLAIN (FORMAT JSON), never EXPLAIN ANALYZE, for sql with its original params, and
returns the plan. Called only from ForgeOpsTracker.QueryPlanner's own Task, never the
process that ran the query, so repo.transaction/2 checks out a separate connection from the
pool (the app's connection and transaction are never touched). The transaction is made READ
ONLY with a 2 second statement_timeout and always rolled back. Raises on any failure, which
the planner logs and drops.
Functions
@spec attach(module()) :: :ok
Registers the telemetry handler for repo. Safe to call more than once for the same repo.
Runs EXPLAIN (FORMAT JSON), never EXPLAIN ANALYZE, for sql with its original params, and
returns the plan. Called only from ForgeOpsTracker.QueryPlanner's own Task, never the
process that ran the query, so repo.transaction/2 checks out a separate connection from the
pool (the app's connection and transaction are never touched). The transaction is made READ
ONLY with a 2 second statement_timeout and always rolled back. Raises on any failure, which
the planner logs and drops.