Skip to content

Commit 047b185

Browse files
committed
Get the first non-ecto entry for stacktraces
1 parent 29bdfa1 commit 047b185

File tree

2 files changed

+159
-105
lines changed

2 files changed

+159
-105
lines changed

integration_test/sql/logging.exs

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,70 +5,131 @@ defmodule Ecto.Integration.LoggingTest do
55
alias Ecto.Integration.PoolRepo
66
alias Ecto.Integration.Post
77

8-
test "log entry is sent to telemetry" do
9-
log = fn event_name, measurements, metadata ->
10-
assert Enum.at(event_name, -1) == :query
11-
assert %{result: {:ok, _res}} = metadata
12-
assert measurements.total_time == measurements.query_time + measurements.decode_time + measurements.queue_time
13-
assert measurements.idle_time
14-
send(self(), :logged)
8+
import ExUnit.CaptureLog
9+
10+
describe "telemetry" do
11+
test "dispatches event" do
12+
log = fn event_name, measurements, metadata ->
13+
assert Enum.at(event_name, -1) == :query
14+
assert %{result: {:ok, _res}} = metadata
15+
16+
assert measurements.total_time ==
17+
measurements.query_time + measurements.decode_time + measurements.queue_time
18+
19+
assert measurements.idle_time
20+
send(self(), :logged)
21+
end
22+
23+
Process.put(:telemetry, log)
24+
_ = PoolRepo.all(Post)
25+
assert_received :logged
1526
end
1627

17-
Process.put(:telemetry, log)
18-
_ = PoolRepo.all(Post)
19-
assert_received :logged
20-
end
28+
test "dispatches event with stacktrace" do
29+
log = fn _event_name, _measurements, metadata ->
30+
assert %{stacktrace: [_ | _]} = metadata
31+
send(self(), :logged)
32+
end
2133

22-
test "log entry is sent to telemetry with custom options" do
23-
log = fn event_name, _measurements, metadata ->
24-
assert Enum.at(event_name, -1) == :query
25-
assert metadata.options == [:custom_metadata]
26-
send(self(), :logged)
34+
Process.put(:telemetry, log)
35+
_ = PoolRepo.all(Post, stacktrace: true)
36+
assert_received :logged
2737
end
2838

29-
Process.put(:telemetry, log)
30-
_ = PoolRepo.all(Post, telemetry_options: [:custom_metadata])
31-
assert_received :logged
32-
end
39+
test "dispatches event with custom options" do
40+
log = fn event_name, _measurements, metadata ->
41+
assert Enum.at(event_name, -1) == :query
42+
assert metadata.options == [:custom_metadata]
43+
send(self(), :logged)
44+
end
3345

34-
test "log entry sent under another event name" do
35-
log = fn [:custom], measurements, metadata ->
36-
assert %{result: {:ok, _res}} = metadata
37-
assert measurements.total_time == measurements.query_time + measurements.decode_time + measurements.queue_time
38-
assert measurements.idle_time
39-
send(self(), :logged)
46+
Process.put(:telemetry, log)
47+
_ = PoolRepo.all(Post, telemetry_options: [:custom_metadata])
48+
assert_received :logged
4049
end
4150

42-
Process.put(:telemetry, log)
43-
_ = PoolRepo.all(Post, telemetry_event: [:custom])
44-
assert_received :logged
45-
end
51+
test "dispatches under another event name" do
52+
log = fn [:custom], measurements, metadata ->
53+
assert %{result: {:ok, _res}} = metadata
4654

47-
test "log entry is not sent to telemetry under nil event name" do
48-
Process.put(:telemetry, fn _, _ -> raise "never called" end)
49-
_ = TestRepo.all(Post, telemetry_event: nil)
50-
refute_received :logged
51-
end
55+
assert measurements.total_time ==
56+
measurements.query_time + measurements.decode_time + measurements.queue_time
57+
58+
assert measurements.idle_time
59+
send(self(), :logged)
60+
end
61+
62+
Process.put(:telemetry, log)
63+
_ = PoolRepo.all(Post, telemetry_event: [:custom])
64+
assert_received :logged
65+
end
5266

53-
test "log entry when some measurements are nil" do
54-
assert ExUnit.CaptureLog.capture_log(fn ->
55-
TestRepo.query("BEG", [], log: :error)
56-
end) =~ "[error]"
67+
test "is not dispatched with no event name" do
68+
Process.put(:telemetry, fn _, _ -> raise "never called" end)
69+
_ = TestRepo.all(Post, telemetry_event: nil)
70+
refute_received :logged
71+
end
5772
end
5873

59-
test "log entry with custom log level" do
60-
assert ExUnit.CaptureLog.capture_log(fn ->
61-
TestRepo.insert!(%Post{title: "1"}, [log: :error])
62-
end) =~ "[error]"
74+
describe "logs" do
75+
@stacktrace_opts [stacktrace: true, log: :error]
6376

64-
# We cannot assert on the result because it depends on the suite log level
65-
ExUnit.CaptureLog.capture_log(fn ->
66-
TestRepo.insert!(%Post{title: "1"}, [log: true])
67-
end)
77+
defp stacktrace_entry(line) do
78+
"↳ anonymous fn/0 in Ecto.Integration.LoggingTest.\"test logs includes stacktraces\"/1, " <>
79+
"at: integration_test/sql/logging.exs##{line - 3}"
80+
end
6881

69-
# But this assertion is always true
70-
assert ExUnit.CaptureLog.capture_log(fn ->
71-
TestRepo.insert!(%Post{title: "1"}, [log: false])
72-
end) == ""
82+
test "when some measurements are nil" do
83+
assert capture_log(fn -> TestRepo.query("BEG", [], log: :error) end) =~
84+
"[error]"
85+
end
86+
87+
test "includes stacktraces" do
88+
assert capture_log(fn ->
89+
TestRepo.all(Post, @stacktrace_opts)
90+
91+
:ok
92+
end) =~ stacktrace_entry(__ENV__.line)
93+
94+
assert capture_log(fn ->
95+
TestRepo.insert(%Post{}, @stacktrace_opts)
96+
97+
:ok
98+
end) =~ stacktrace_entry(__ENV__.line)
99+
100+
assert capture_log(fn ->
101+
# Test cascading options
102+
Ecto.Multi.new()
103+
|> Ecto.Multi.insert(:post, %Post{})
104+
|> TestRepo.transaction(@stacktrace_opts)
105+
106+
:ok
107+
end) =~ stacktrace_entry(__ENV__.line)
108+
109+
assert capture_log(fn ->
110+
# In theory we should point to the call _inside_ run
111+
# but all multi calls point to the transaction starting point.
112+
Ecto.Multi.new()
113+
|> Ecto.Multi.run(:all, fn _, _ -> {:ok, TestRepo.all(Post, @stacktrace_opts)} end)
114+
|> TestRepo.transaction()
115+
116+
:ok
117+
end) =~ stacktrace_entry(__ENV__.line)
118+
end
119+
120+
test "with custom log level" do
121+
assert capture_log(fn -> TestRepo.insert!(%Post{title: "1"}, log: :error) end) =~
122+
"[error]"
123+
124+
# We cannot assert on the result because it depends on the suite log level
125+
capture_log(fn ->
126+
TestRepo.insert!(%Post{title: "1"}, log: true)
127+
end)
128+
129+
# But this assertion is always true
130+
assert capture_log(fn ->
131+
TestRepo.insert!(%Post{title: "1"}, log: false)
132+
end) == ""
133+
end
73134
end
74135
end

lib/ecto/adapters/sql.ex

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,9 @@ defmodule Ecto.Adapters.SQL do
10631063
} = entry
10641064

10651065
source = Keyword.get(opts, :source)
1066-
query_string = String.Chars.to_string(query)
1066+
query = String.Chars.to_string(query)
1067+
result = with {:ok, _query, res} <- result, do: {:ok, res}
1068+
stacktrace = Keyword.get(opts, :stacktrace)
10671069

10681070
params =
10691071
Enum.map(params, fn
@@ -1084,12 +1086,12 @@ defmodule Ecto.Adapters.SQL do
10841086
metadata = %{
10851087
type: :ecto_sql_query,
10861088
repo: repo,
1087-
result: log_result(result),
1089+
result: result,
10881090
params: params,
1089-
query: query_string,
1091+
query: query,
10901092
source: source,
1091-
options: Keyword.get(opts, :telemetry_options, []),
1092-
stacktrace: Keyword.get(opts, :stacktrace)
1093+
stacktrace: stacktrace,
1094+
options: Keyword.get(opts, :telemetry_options, [])
10931095
}
10941096

10951097
if event_name = Keyword.get(opts, :telemetry_event, event_name) do
@@ -1100,8 +1102,8 @@ defmodule Ecto.Adapters.SQL do
11001102
true ->
11011103
Logger.log(
11021104
log,
1103-
fn -> log_iodata(measurements, metadata) end,
1104-
ansi_color: sql_color(query_string)
1105+
fn -> log_iodata(measurements, repo, source, query, params, result, stacktrace) end,
1106+
ansi_color: sql_color(query)
11051107
)
11061108

11071109
false ->
@@ -1110,8 +1112,8 @@ defmodule Ecto.Adapters.SQL do
11101112
level ->
11111113
Logger.log(
11121114
level,
1113-
fn -> log_iodata(measurements, metadata) end,
1114-
ansi_color: sql_color(query_string)
1115+
fn -> log_iodata(measurements, repo, source, query, params, result, stacktrace) end,
1116+
ansi_color: sql_color(query)
11151117
)
11161118
end
11171119

@@ -1127,17 +1129,7 @@ defmodule Ecto.Adapters.SQL do
11271129
defp log_measurements([], total, acc),
11281130
do: Map.new([total_time: total] ++ acc)
11291131

1130-
defp log_result({:ok, _query, res}), do: {:ok, res}
1131-
defp log_result(other), do: other
1132-
1133-
defp log_iodata(measurements, metadata) do
1134-
%{
1135-
params: params,
1136-
query: query,
1137-
result: result,
1138-
source: source
1139-
} = metadata
1140-
1132+
defp log_iodata(measurements, repo, source, query, params, result, stacktrace) do
11411133
[
11421134
"QUERY",
11431135
?\s,
@@ -1151,43 +1143,10 @@ defmodule Ecto.Adapters.SQL do
11511143
query,
11521144
?\s,
11531145
inspect(params, charlists: false),
1154-
log_stacktrace(metadata.stacktrace)
1146+
log_stacktrace(stacktrace, repo)
11551147
]
11561148
end
11571149

1158-
defp log_stacktrace(stacktrace) do
1159-
with [_ | _] <- stacktrace,
1160-
{module, function, arity, metadata} <- first_non_ecto(stacktrace, nil) do
1161-
[
1162-
?\n,
1163-
"↳ ",
1164-
Exception.format_mfa(module, function, arity),
1165-
", at: ",
1166-
inspect(metadata)
1167-
]
1168-
else
1169-
_ -> []
1170-
end
1171-
end
1172-
1173-
defp first_non_ecto([last_item], first_non_ecto) do
1174-
if is_ecto?(last_item), do: nil, else: first_non_ecto
1175-
end
1176-
1177-
defp first_non_ecto([head | tail], first_non_ecto) do
1178-
if is_ecto?(head) do
1179-
first_non_ecto(tail, nil)
1180-
else
1181-
first_non_ecto(tail, first_non_ecto || head)
1182-
end
1183-
end
1184-
1185-
defp is_ecto?({module, _f, _a, _m}) do
1186-
module
1187-
|> Atom.to_string()
1188-
|> String.starts_with?("Elixir.Ecto.")
1189-
end
1190-
11911150
defp log_ok_error({:ok, _res}), do: "OK"
11921151
defp log_ok_error({:error, _err}), do: "ERROR"
11931152

@@ -1211,6 +1170,40 @@ defmodule Ecto.Adapters.SQL do
12111170
end
12121171
end
12131172

1173+
defp log_stacktrace(stacktrace, repo) do
1174+
with [_ | _] <- stacktrace,
1175+
{module, function, arity, info} <- last_non_ecto(Enum.reverse(stacktrace), repo, nil) do
1176+
[
1177+
?\n,
1178+
"↳ ",
1179+
Exception.format_mfa(module, function, arity),
1180+
log_stacktrace_info(info)
1181+
]
1182+
else
1183+
_ -> []
1184+
end
1185+
end
1186+
1187+
defp log_stacktrace_info([file: file, line: line] ++ _) do
1188+
[", at: ", file, ?#, Integer.to_string(line)]
1189+
end
1190+
1191+
defp log_stacktrace_info(_) do
1192+
[]
1193+
end
1194+
1195+
@repo_modules [Ecto.Repo.Queryable, Ecto.Repo.Schema, Ecto.Repo.Transaction]
1196+
1197+
defp last_non_ecto([{mod, _, _, _} | _stacktrace], repo, last)
1198+
when mod == repo or mod in @repo_modules,
1199+
do: last
1200+
1201+
defp last_non_ecto([last | stacktrace], repo, _last),
1202+
do: last_non_ecto(stacktrace, repo, last)
1203+
1204+
defp last_non_ecto([], _repo, last),
1205+
do: last
1206+
12141207
## Connection helpers
12151208

12161209
defp checkout_or_transaction(fun, adapter_meta, opts, callback) do

0 commit comments

Comments
 (0)