defmodule EctoQueryExplorer do @moduledoc """ Collects and analyzes Ecto query telemetry data. EctoQueryExplorer hooks into Ecto's telemetry events to capture query execution data, including stacktraces that show where each query originated. Data is stored in ETS during runtime and can be dumped to SQLite for analysis. ## How it works 1. Attaches to `[:my_app, :repo, :query]` telemetry events 2. Captures query text, timing, parameters, and stacktraces in ETS 3. On demand, dumps ETS data to SQLite via `EctoQueryExplorer.Data.dump2sqlite/1` 4. Query the SQLite database using `EctoQueryExplorer.Queries` functions ## Configuration config :ecto_query_explorer, otp_app: :my_app, repo: MyApp.EctoQueryExplorerRepo, ets_table_name: :ecto_query_explorer_data, samples_to_keep: 5, source_ecto_repos: [MyApp.Repo] ## Database schema The SQLite database contains these tables: - `queries` - unique query texts with execution counts - `samples` - individual query executions with timing data - `stacktraces` - unique call stacks - `stacktrace_entries` - individual stack frames - `functions` - unique module/function/arity combinations - `locations` - unique file/line combinations - `epochs` - data provenance tracking for multi-pod environments See the schema diagram in the repository for relationships. """ use GenServer # Client def start_link(_options \\ []) do options = Application.get_all_env(:ecto_query_explorer) |> Keyword.put_new(:last_sample_id, 0) GenServer.start_link(__MODULE__, options, name: __MODULE__) end def handle_event(event, measurements, metadata, config) do GenServer.cast(__MODULE__, {:handle_event, event, measurements, metadata, config}) end # Server @impl true def init(state) do :ets.new(state[:ets_table_name], [:ordered_set, :named_table]) events = Enum.map( state[:source_ecto_repos], &(telemetry_prefix(state[:otp_app], &1) ++ [:query]) ) config = [ repo: state[:repo], ets_table_name: state[:ets_table_name], samples_to_keep: state[:samples_to_keep] ] case :telemetry.attach_many(__MODULE__, events, &EctoQueryExplorer.handle_event/4, config) do :ok -> {:ok, state} {:error, reason} -> {:stop, reason} end end @impl true def handle_cast({:handle_event, event, measurements, metadata, config}, state) do sample_id = state[:last_sample_id] + 1 EctoQueryExplorer.Handler.handle_event(event, measurements, metadata, config, %{ sample_id: sample_id }) {:noreply, Keyword.update!(state, :last_sample_id, fn _existing_value -> sample_id end)} end defp telemetry_prefix(otp_app, repo) do case otp_app |> Application.get_env(repo, []) |> Keyword.get(:telemetry_prefix) do prefix when is_list(prefix) -> prefix _ -> repo |> Module.split() |> Enum.map(&(&1 |> Macro.underscore() |> String.to_atom())) end end end