Skip to content

Commit 83eaa67

Browse files
v0idpwnjosevalimgreg-rychlewski
authored
Allow changing how stacktrace is pre-processed for logging (#658)
Adds a new option, `:log_stacktrace_mfa`, which can be set at the configuration or query level (like `:log` and `:stacktrace`). This option allows filtering or modifying stacktrace-derived info in query logs. For example, one might want to filter out additional modules beyond those `Ecto.SQL` excludes by default, or include more stack items. Additionally, refactors the old function that determined which stack item to log into the new public function `Ecto.Adapters.SQL.first_non_ecto_stacktrace/3`, which is now the default for the `:log_stacktrace_mfa` option. The function also takes an additional parameter for the logged stacktrace target size, as we believe many users may want to increase it. Sample usage: ```elixir # Using a custom function config :my_app, MyApp.Repo, stacktrace: true, log_stacktrace_mfa: {MyApp.Utils, :prepare_ecto_stacktrace, []} # Using the default one, with an increased stacktrace size to 3 config :my_app, MyApp.Repo, stacktrace: true, log_stacktrace_mfa: {Ecto.Adapters.SQL, :first_non_ecto_stacktrace, [3]} ``` --------- Co-authored-by: José Valim <[email protected]> Co-authored-by: Greg Rychlewski <[email protected]>
1 parent 8dc5a77 commit 83eaa67

File tree

4 files changed

+112
-34
lines changed

4 files changed

+112
-34
lines changed

Diff for: .github/workflows/ci.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
fail-fast: false
5353
matrix:
5454
elixirbase:
55-
- "1.11.4-erlang-23.3.4.9-alpine-3.16.9"
55+
- "1.14.5-erlang-23.3.4.9-alpine-3.16.9"
5656
postgres:
5757
- "16.2-alpine"
5858
- "11.11-alpine"
@@ -61,7 +61,7 @@ jobs:
6161
pool_count:
6262
- "1"
6363
include:
64-
- elixirbase: "1.11.4-erlang-23.3.4.9-alpine-3.16.9"
64+
- elixirbase: "1.14.5-erlang-23.3.4.9-alpine-3.16.9"
6565
postgres: "16.2-alpine"
6666
pool_count: "4"
6767
steps:
@@ -79,7 +79,7 @@ jobs:
7979
fail-fast: false
8080
matrix:
8181
elixirbase:
82-
- "1.11.4-erlang-23.3.4.9-alpine-3.16.9"
82+
- "1.14.5-erlang-23.3.4.9-alpine-3.16.9"
8383
mysql:
8484
- "5.7"
8585
- "8.0"
@@ -96,7 +96,7 @@ jobs:
9696
fail-fast: false
9797
matrix:
9898
elixirbase:
99-
- "1.11.4-erlang-23.3.4.9-alpine-3.16.9"
99+
- "1.14.5-erlang-23.3.4.9-alpine-3.16.9"
100100
mssql:
101101
- "2017"
102102
- "2019"

Diff for: integration_test/sql/logging.exs

+11
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ defmodule Ecto.Integration.LoggingTest do
137137

138138
:ok
139139
end) =~ stacktrace_entry(__ENV__.line)
140+
141+
out = capture_log(fn ->
142+
TestRepo.all(Post, Keyword.put(@stacktrace_opts, :log_stacktrace_mfa, {Ecto.Adapters.SQL, :first_non_ecto_stacktrace, [2]}))
143+
144+
:ok
145+
end)
146+
147+
assert out =~ stacktrace_entry(__ENV__.line - 2)
148+
149+
# We are a bit liberal with what we expect as we don't want to tie to internal ExUnit code
150+
assert out =~ ~r/ ExUnit.CaptureLog.*/
140151
end
141152

142153
test "with custom log level" do

Diff for: lib/ecto/adapters/sql.ex

+96-29
Original file line numberDiff line numberDiff line change
@@ -659,9 +659,16 @@ defmodule Ecto.Adapters.SQL do
659659
end
660660

661661
defp sql_call(adapter_meta, callback, args, params, opts) do
662-
%{pid: pool, telemetry: telemetry, sql: sql, opts: default_opts} = adapter_meta
662+
%{
663+
pid: pool,
664+
telemetry: telemetry,
665+
sql: sql,
666+
opts: default_opts,
667+
log_stacktrace_mfa: log_stacktrace_mfa
668+
} = adapter_meta
669+
663670
conn = get_conn_or_pool(pool, adapter_meta)
664-
opts = with_log(telemetry, params, opts ++ default_opts)
671+
opts = with_log(telemetry, log_stacktrace_mfa, params, opts ++ default_opts)
665672
args = args ++ [params, opts]
666673
apply(sql, callback, [conn | args])
667674
end
@@ -861,6 +868,9 @@ defmodule Ecto.Adapters.SQL do
861868

862869
log = Keyword.get(config, :log, :debug)
863870

871+
log_stacktrace_mfa =
872+
Keyword.get(config, :log_stacktrace_mfa, {__MODULE__, :first_non_ecto_stacktrace, [1]})
873+
864874
if log not in @valid_log_levels do
865875
raise """
866876
invalid value for :log option in Repo config
@@ -872,7 +882,7 @@ defmodule Ecto.Adapters.SQL do
872882
"""
873883
end
874884

875-
stacktrace = Keyword.get(config, :stacktrace, nil)
885+
stacktrace = Keyword.get(config, :stacktrace)
876886
telemetry_prefix = Keyword.fetch!(config, :telemetry_prefix)
877887
telemetry = {config[:repo], log, telemetry_prefix ++ [:query]}
878888

@@ -885,6 +895,7 @@ defmodule Ecto.Adapters.SQL do
885895
telemetry: telemetry,
886896
sql: connection,
887897
stacktrace: stacktrace,
898+
log_stacktrace_mfa: log_stacktrace_mfa,
888899
opts: Keyword.take(config, @pool_opts)
889900
}
890901

@@ -1097,8 +1108,15 @@ defmodule Ecto.Adapters.SQL do
10971108

10981109
@doc false
10991110
def reduce(adapter_meta, statement, params, opts, acc, fun) do
1100-
%{pid: pool, telemetry: telemetry, sql: sql, opts: default_opts} = adapter_meta
1101-
opts = with_log(telemetry, params, opts ++ default_opts)
1111+
%{
1112+
pid: pool,
1113+
telemetry: telemetry,
1114+
sql: sql,
1115+
log_stacktrace_mfa: log_stacktrace_mfa,
1116+
opts: default_opts
1117+
} = adapter_meta
1118+
1119+
opts = with_log(telemetry, log_stacktrace_mfa, params, opts ++ default_opts)
11021120

11031121
case get_conn(pool) do
11041122
%DBConnection{conn_mode: :transaction} = conn ->
@@ -1113,8 +1131,15 @@ defmodule Ecto.Adapters.SQL do
11131131

11141132
@doc false
11151133
def into(adapter_meta, statement, params, opts) do
1116-
%{pid: pool, telemetry: telemetry, sql: sql, opts: default_opts} = adapter_meta
1117-
opts = with_log(telemetry, params, opts ++ default_opts)
1134+
%{
1135+
pid: pool,
1136+
telemetry: telemetry,
1137+
sql: sql,
1138+
opts: default_opts,
1139+
log_stacktrace_mfa: log_stacktrace_mfa
1140+
} = adapter_meta
1141+
1142+
opts = with_log(telemetry, log_stacktrace_mfa, params, opts ++ default_opts)
11181143

11191144
case get_conn(pool) do
11201145
%DBConnection{conn_mode: :transaction} = conn ->
@@ -1231,11 +1256,11 @@ defmodule Ecto.Adapters.SQL do
12311256

12321257
## Log
12331258

1234-
defp with_log(telemetry, params, opts) do
1235-
[log: &log(telemetry, params, &1, opts)] ++ opts
1259+
defp with_log(telemetry, log_stacktrace_mfa, params, opts) do
1260+
[log: &log(telemetry, log_stacktrace_mfa, params, &1, opts)] ++ opts
12361261
end
12371262

1238-
defp log({repo, log, event_name}, params, entry, opts) do
1263+
defp log({repo, log, event_name}, log_stacktrace_mfa, params, entry, opts) do
12391264
%{
12401265
connection_time: query_time,
12411266
decode_time: decode_time,
@@ -1286,14 +1311,36 @@ defmodule Ecto.Adapters.SQL do
12861311
{true, level} ->
12871312
Logger.log(
12881313
level,
1289-
fn -> log_iodata(measurements, repo, source, query, log_params, result, stacktrace) end,
1314+
fn ->
1315+
log_iodata(
1316+
measurements,
1317+
repo,
1318+
source,
1319+
query,
1320+
log_params,
1321+
result,
1322+
stacktrace,
1323+
opts[:log_stacktrace_mfa] || log_stacktrace_mfa
1324+
)
1325+
end,
12901326
ansi_color: sql_color(query)
12911327
)
12921328

12931329
{opts_level, args_level} ->
12941330
Logger.log(
12951331
opts_level || args_level,
1296-
fn -> log_iodata(measurements, repo, source, query, log_params, result, stacktrace) end,
1332+
fn ->
1333+
log_iodata(
1334+
measurements,
1335+
repo,
1336+
source,
1337+
query,
1338+
log_params,
1339+
result,
1340+
stacktrace,
1341+
opts[:log_stacktrace_mfa] || log_stacktrace_mfa
1342+
)
1343+
end,
12971344
ansi_color: sql_color(query)
12981345
)
12991346
end
@@ -1310,7 +1357,7 @@ defmodule Ecto.Adapters.SQL do
13101357
defp log_measurements([], total, acc),
13111358
do: Map.new([total_time: total] ++ acc)
13121359

1313-
defp log_iodata(measurements, repo, source, query, params, result, stacktrace) do
1360+
defp log_iodata(measurements, repo, source, query, params, result, stacktrace, stacktrace_mfa) do
13141361
[
13151362
"QUERY",
13161363
?\s,
@@ -1324,7 +1371,7 @@ defmodule Ecto.Adapters.SQL do
13241371
query,
13251372
?\s,
13261373
inspect(params, charlists: false),
1327-
log_stacktrace(stacktrace, repo)
1374+
log_stacktrace(stacktrace, repo, stacktrace_mfa)
13281375
]
13291376
end
13301377

@@ -1351,22 +1398,23 @@ defmodule Ecto.Adapters.SQL do
13511398
end
13521399
end
13531400

1354-
defp log_stacktrace(stacktrace, repo) do
1355-
with [_ | _] <- stacktrace,
1356-
{module, function, arity, info} <- last_non_ecto(Enum.reverse(stacktrace), repo, nil) do
1401+
defp log_stacktrace([_ | _] = stacktrace, repo, {module, function, args}) do
1402+
entries = apply(module, function, [stacktrace, %{repo: repo} | args])
1403+
1404+
Enum.with_index(entries, fn {module, function, arity, info}, idx ->
13571405
[
13581406
?\n,
13591407
IO.ANSI.light_black(),
1360-
"↳ ",
1408+
if(idx == 0, do: "↳ ", else: " "),
13611409
Exception.format_mfa(module, function, arity),
13621410
log_stacktrace_info(info),
13631411
IO.ANSI.reset()
13641412
]
1365-
else
1366-
_ -> []
1367-
end
1413+
end)
13681414
end
13691415

1416+
defp log_stacktrace(_, _, _), do: []
1417+
13701418
defp log_stacktrace_info([file: file, line: line] ++ _) do
13711419
[", at: ", file, ?:, Integer.to_string(line)]
13721420
end
@@ -1377,21 +1425,40 @@ defmodule Ecto.Adapters.SQL do
13771425

13781426
@repo_modules [Ecto.Repo.Queryable, Ecto.Repo.Schema, Ecto.Repo.Transaction]
13791427

1380-
defp last_non_ecto([{mod, _, _, _} | _stacktrace], repo, last)
1428+
@doc """
1429+
Receives a stacktrace, and return the first N items before Ecto entries
1430+
1431+
This function is used by default in the `:log_stacktrace_mfa` config, with
1432+
a size of 1.
1433+
"""
1434+
@spec first_non_ecto_stacktrace(
1435+
Exception.stacktrace(),
1436+
%{repo: Ecto.Repo.t()},
1437+
non_neg_integer()
1438+
) :: Exception.stacktrace()
1439+
def first_non_ecto_stacktrace(stacktrace, %{repo: repo}, size) do
1440+
stacktrace
1441+
|> Enum.reverse()
1442+
|> last_non_ecto_entries(repo, [])
1443+
|> Enum.take(size)
1444+
end
1445+
1446+
defp last_non_ecto_entries([{mod, _, _, _} | _], repo, acc)
13811447
when mod == repo or mod in @repo_modules,
1382-
do: last
1448+
do: acc
13831449

1384-
defp last_non_ecto([last | stacktrace], repo, _last),
1385-
do: last_non_ecto(stacktrace, repo, last)
1450+
defp last_non_ecto_entries([entry | rest], repo, acc),
1451+
do: last_non_ecto_entries(rest, repo, [entry | acc])
13861452

1387-
defp last_non_ecto([], _repo, last),
1388-
do: last
1453+
defp last_non_ecto_entries([], _, acc), do: acc
13891454

13901455
## Connection helpers
13911456

13921457
defp checkout_or_transaction(fun, adapter_meta, opts, callback) do
1393-
%{pid: pool, telemetry: telemetry, opts: default_opts} = adapter_meta
1394-
opts = with_log(telemetry, [], opts ++ default_opts)
1458+
%{pid: pool, telemetry: telemetry, opts: default_opts, log_stacktrace_mfa: log_stacktrace_mfa} =
1459+
adapter_meta
1460+
1461+
opts = with_log(telemetry, log_stacktrace_mfa, [], opts ++ default_opts)
13951462

13961463
callback = fn conn ->
13971464
previous_conn = put_conn(pool, conn)

Diff for: mix.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule EctoSQL.MixProject do
99
[
1010
app: :ecto_sql,
1111
version: @version,
12-
elixir: "~> 1.11",
12+
elixir: "~> 1.14",
1313
deps: deps(),
1414
test_paths: test_paths(System.get_env("ECTO_ADAPTER")),
1515
xref: [

0 commit comments

Comments
 (0)