Current section

Files

Jump to
excessibility lib telemetry_capture enrichers query.ex
Raw

lib/telemetry_capture/enrichers/query.ex

defmodule Excessibility.TelemetryCapture.Enrichers.Query do
@moduledoc """
Enriches timeline events with query/database information.
Counts:
- Total Ecto records loaded in assigns
- NotLoaded associations (potential N+1 indicators)
## Usage
Runs automatically during timeline building. Adds query metrics
to each timeline event.
## Example Output
%{
sequence: 3,
event: "handle_event:filter",
query_records_loaded: 47,
query_not_loaded_count: 12
}
"""
@behaviour Excessibility.TelemetryCapture.Enricher
def name, do: :query
def enrich(assigns, _opts) do
{records_loaded, not_loaded_count} = count_queries(assigns)
%{
query_records_loaded: records_loaded,
query_not_loaded_count: not_loaded_count
}
end
defp count_queries(assigns) do
count_queries_recursive(assigns, {0, 0})
end
defp count_queries_recursive(value, {records, not_loaded}) when is_struct(value) do
cond do
# Check if it's a NotLoaded association
value.__struct__ == Ecto.Association.NotLoaded ->
{records, not_loaded + 1}
# It's an Ecto struct - count it and recurse into its fields
ecto_struct?(value) ->
value
|> Map.from_struct()
|> count_queries_recursive({records + 1, not_loaded})
# Other struct - just recurse
true ->
value
|> Map.from_struct()
|> count_queries_recursive({records, not_loaded})
end
end
defp count_queries_recursive(value, acc) when is_map(value) do
Enum.reduce(value, acc, fn {_key, val}, acc_inner ->
count_queries_recursive(val, acc_inner)
end)
end
defp count_queries_recursive(value, acc) when is_list(value) do
Enum.reduce(value, acc, fn item, acc_inner ->
count_queries_recursive(item, acc_inner)
end)
end
defp count_queries_recursive(_value, acc), do: acc
# Check if a struct is an Ecto schema
# Ecto schemas have __meta__ field or are database-backed
defp ecto_struct?(struct) do
Map.has_key?(struct, :__meta__) or
(Map.has_key?(struct, :__struct__) and
struct.__struct__ not in [Ecto.Association.NotLoaded])
end
end