Skip to content

Commit 7bc8c11

Browse files
authored
Error context (#2)
Set the relevant context for the current process and include it when reporting Plug, Phoenix and Oban errors. Users cann add their own context using `ErrorTracker.put_context`.
1 parent 21c7097 commit 7bc8c11

4 files changed

Lines changed: 101 additions & 32 deletions

File tree

lib/error_tracker.ex

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ defmodule ErrorTracker do
33
En Elixir based built-in error tracking solution.
44
"""
55

6-
def report(exception, stacktrace, context \\ %{}) do
6+
@typedoc """
7+
A map containing the relevant context for a particular error.
8+
"""
9+
@type context :: %{String.t() => any()}
10+
11+
def report(exception, stacktrace, given_context \\ %{}) do
712
{:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
813
{:ok, error} = ErrorTracker.Error.new(exception, stacktrace)
914

15+
context = Map.merge(get_context(), given_context)
16+
1017
error =
1118
repo().insert!(error,
1219
on_conflict: [set: [status: :unresolved]],
@@ -26,4 +33,18 @@ defmodule ErrorTracker do
2633
def prefix do
2734
Application.get_env(:error_tracker, :prefix, "public")
2835
end
36+
37+
@spec set_context(context()) :: context()
38+
def set_context(params) when is_map(params) do
39+
current_context = Process.get(:error_tracker_context, %{})
40+
41+
Process.put(:error_tracker_context, Map.merge(current_context, params))
42+
43+
params
44+
end
45+
46+
@spec get_context() :: context()
47+
def get_context do
48+
Process.get(:error_tracker_context, %{})
49+
end
2950
end

lib/error_tracker/integrations/oban.ex

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,34 @@ defmodule ErrorTracker.Integrations.Oban do
88
modify anything on your application.
99
"""
1010

11+
# https://hexdocs.pm/oban/Oban.Telemetry.html
12+
@events [
13+
[:oban, :job, :start],
14+
[:oban, :job, :exception]
15+
]
16+
1117
def attach do
1218
if Application.spec(:oban) do
13-
:telemetry.attach(__MODULE__, [:oban, :job, :exception], &handle_event/4, :no_config)
19+
:telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
1420
end
1521
end
1622

17-
def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
18-
%{job: job, reason: exception, stacktrace: stacktrace} = metadata
23+
def handle_event([:oban, :job, :start], _measurements, metadata, :no_config) do
24+
%{job: job} = metadata
1925

20-
ErrorTracker.report(exception, stacktrace, %{
21-
job_id: job.id,
22-
job_attempt: job.attempt,
23-
job_queue: job.queue,
24-
job_worker: job.worker
26+
ErrorTracker.set_context(%{
27+
"job.args" => job.args,
28+
"job.attempt" => job.attempt,
29+
"job.id" => job.id,
30+
"job.priority" => job.priority,
31+
"job.queue" => job.queue,
32+
"job.worker" => job.worker
2533
})
2634
end
35+
36+
def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
37+
%{reason: exception, stacktrace: stacktrace} = metadata
38+
39+
ErrorTracker.report(exception, stacktrace)
40+
end
2741
end

lib/error_tracker/integrations/phoenix.ex

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,32 @@ defmodule ErrorTracker.Integrations.Phoenix do
1010

1111
alias ErrorTracker.Integrations.Plug, as: PlugIntegration
1212

13+
# https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-instrumentation
14+
@events [
15+
[:phoenix, :router_dispatch, :start],
16+
[:phoenix, :router_dispatch, :exception]
17+
]
18+
1319
def attach do
1420
if Application.spec(:phoenix) do
15-
:telemetry.attach(
16-
__MODULE__,
17-
[:phoenix, :router_dispatch, :exception],
18-
&__MODULE__.handle_exception/4,
19-
[]
20-
)
21+
:telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
2122
end
2223
end
2324

24-
def handle_exception(
25-
[:phoenix, :router_dispatch, :exception],
26-
_measurements,
27-
%{reason: %Plug.Conn.WrapperError{conn: conn, reason: reason, stack: stack}},
28-
_opts
29-
) do
30-
PlugIntegration.report_error(conn, reason, stack)
25+
def handle_event([:phoenix, :router_dispatch, :start], _measurements, metadata, :no_config) do
26+
PlugIntegration.set_context(metadata.conn)
3127
end
3228

33-
def handle_exception(
34-
[:phoenix, :router_dispatch, :exception],
35-
_measurements,
36-
%{reason: reason, stacktrace: stack, conn: conn},
37-
_opts
38-
) do
39-
PlugIntegration.report_error(conn, reason, stack)
29+
def handle_event([:phoenix, :router_dispatch, :exception], _measurements, metadata, :no_config) do
30+
{reason, stack} =
31+
case metadata do
32+
%{reason: %Plug.Conn.WrapperError{reason: reason, stack: stack}} ->
33+
{reason, stack}
34+
35+
%{reason: reason, stacktrace: stack} ->
36+
{reason, stack}
37+
end
38+
39+
PlugIntegration.report_error(metadata.conn, reason, stack)
4040
end
4141
end

lib/error_tracker/integrations/plug.ex

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ defmodule ErrorTracker.Integrations.Plug do
4646
defoverridable call: 2
4747

4848
def call(conn, opts) do
49+
unquote(__MODULE__).set_context(conn)
4950
super(conn, opts)
5051
rescue
5152
e in Plug.Conn.WrapperError ->
52-
unquote(__MODULE__).report_error(conn, e, e.stack)
53+
unquote(__MODULE__).report_error(e.conn, e.reason, e.stack)
5354

5455
Plug.Conn.WrapperError.reraise(e)
5556

@@ -68,13 +69,46 @@ defmodule ErrorTracker.Integrations.Plug do
6869
end
6970
end
7071

71-
def report_error(_conn, reason, stack) do
72+
def report_error(conn, reason, stack) do
7273
unless Process.get(:error_tracker_router_exception_reported) do
7374
try do
74-
ErrorTracker.report(reason, stack)
75+
ErrorTracker.report(reason, stack, build_context(conn))
7576
after
7677
Process.put(:error_tracker_router_exception_reported, true)
7778
end
7879
end
7980
end
81+
82+
def set_context(conn = %Plug.Conn{}) do
83+
conn |> build_context |> ErrorTracker.set_context()
84+
end
85+
86+
defp build_context(conn = %Plug.Conn{}) do
87+
%{
88+
"request.host" => conn.host,
89+
"request.path" => conn.request_path,
90+
"request.query" => conn.query_string,
91+
"request.method" => conn.method,
92+
"request.ip" => remote_ip(conn),
93+
"request.headers" => Map.new(conn.req_headers),
94+
# Depending on the error source, the request params may have not been fetched yet
95+
"request.params" => unless(is_struct(conn.params, Plug.Conn.Unfetched), do: conn.params)
96+
}
97+
end
98+
99+
defp remote_ip(conn = %Plug.Conn{}) do
100+
remote_ip =
101+
case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
102+
[x_forwarded_for | _] ->
103+
x_forwarded_for |> String.split(",", parts: 2) |> List.first()
104+
105+
[] ->
106+
case :inet.ntoa(conn.remote_ip) do
107+
{:error, _} -> ""
108+
address -> to_string(address)
109+
end
110+
end
111+
112+
String.trim(remote_ip)
113+
end
80114
end

0 commit comments

Comments
 (0)