Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to support ecto 3 + switch to using Process dictionary #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions lib/one_plus_n_detector.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
defmodule OnePlusNDetector do
use Application

@moduledoc """
Ecto Repo's logger adapter.

Expand All @@ -10,32 +8,55 @@ defmodule OnePlusNDetector do
]
"""

require Logger
alias OnePlusNDetector.Detector

@impl true
def start(_type, _args) do
import Supervisor.Spec, warn: false
@exclude_sources ["oban_jobs", "oban_peers"]
@exclude_queries ["commit", "begin"]
@exclude_match ["oban_jobs", "oban_peers", "oban_insert", "pg_notify", "pg_try_advisory_xact_lock"]

def setup(repo_module) do
config = repo_module.config()
prefix = config[:telemetry_prefix]
# ^ Telemetry event id for Ecto queries
query_event = prefix ++ [:query]

Logger.debug "Setting up n+1 logging..."

children = [
worker(OnePlusNDetector.Detector, []),
]
:telemetry.attach(
"one_plus_n_detector",
query_event,
&OnePlusNDetector.handle_event/4,
[]
)
end

Supervisor.start_link(children, strategy: :one_for_one)
def handle_event(
_,
_measurements,
%{query: query, source: source} = _metadata,
_config
)
when (is_nil(source) or source not in @exclude_sources) and
query not in @exclude_queries do
if not String.contains?(query, @exclude_match), do: analyze(query)
end

def analyze(%Ecto.LogEntry{query: query} = entry) do
# We need to make sure our app is started or start it ourselves
{:ok, _} = Application.ensure_all_started(:one_plus_n_detector)
def handle_event(
_,
_measurements,
_metadata,
_config
) do
# skip
end

def analyze(query) do
case Detector.check(query) do
{:match, _query, _count} ->
:nothing
{:no_match, _previous_query, count} ->
if count > 2 do
IO.puts "---------> 1+n SQL query detected, total count: #{count}"
end
{:match, count} ->
Logger.warning "---------> Possible n+1 SQL query detected! number of occurrences: #{count}, query: #{query}"
_ ->
# no match
end

entry
end
end
32 changes: 13 additions & 19 deletions lib/one_plus_n_detector/detector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,32 @@ defmodule OnePlusNDetector.Detector do
Checks a query against the previous one and increments counter of collisions or swaps previous query with the last one.
"""

use GenServer

# Increase counter or swaps query
def check("SELECT" <> _rest = query) do
GenServer.call(__MODULE__, {:check, query})
do_check(query, get(query))
end

def check(_query) do
GenServer.call(__MODULE__, :reset)
nil
end

def start_link() do
GenServer.start_link(__MODULE__, %{query: nil, counter: 0}, name: __MODULE__)
defp get(query) do
Process.get("one_plus_n_detector: #{query}")
end

@impl true
def init(state) do
{:ok, state}
defp put(query, counter) do
Process.put("one_plus_n_detector: #{query}", counter)
end

@impl true
def handle_call({:check, query}, _from, %{query: query, counter: counter} = state) do
{:reply, {:match, query, counter + 1}, Map.put(state, :counter, counter + 1)}
def do_check(query, nil) do
put(query, 1)
:first
end

@impl true
def handle_call({:check, query}, _from, %{query: previous_query, counter: previous_count}) do
{:reply, {:no_match, previous_query, previous_count}, %{query: query, counter: 1}}
def do_check(query, counter) do
counter = counter + 1
put(query, counter)
{:match, counter}
end

@impl true
def handle_call(:reset, _from, %{query: previous_query, counter: previous_count}) do
{:reply, {:no_match, previous_query, previous_count}, %{query: nil, counter: 0}}
end
end
5 changes: 2 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ defmodule OnePlusNDetector.MixProject do

def application do
[
extra_applications: [:logger],
mod: {OnePlusNDetector, []}
extra_applications: [:logger]
]
end

defp deps do
[
{:ecto, "~> 2.0"},
{:ecto, "~> 3.0"},
{:ex_doc, ">= 0.0.0", only: :dev},
]
end
Expand Down