diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 44cf145..7f45232 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -3,10 +3,17 @@ defmodule ErrorTracker do En Elixir based built-in error tracking solution. """ - def report(exception, stacktrace, context \\ %{}) do + @typedoc """ + A map containing the relevant context for a particular error. + """ + @type context :: %{String.t() => any()} + + def report(exception, stacktrace, given_context \\ %{}) do {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace) {:ok, error} = ErrorTracker.Error.new(exception, stacktrace) + context = Map.merge(get_context(), given_context) + error = repo().insert!(error, on_conflict: [set: [status: :unresolved]], @@ -26,4 +33,18 @@ defmodule ErrorTracker do def prefix do Application.get_env(:error_tracker, :prefix, "public") end + + @spec set_context(context()) :: context() + def set_context(params) when is_map(params) do + current_context = Process.get(:error_tracker_context, %{}) + + Process.put(:error_tracker_context, Map.merge(current_context, params)) + + params + end + + @spec get_context() :: context() + def get_context do + Process.get(:error_tracker_context, %{}) + end end diff --git a/lib/error_tracker/integrations/oban.ex b/lib/error_tracker/integrations/oban.ex index 3f5758b..cc8e815 100644 --- a/lib/error_tracker/integrations/oban.ex +++ b/lib/error_tracker/integrations/oban.ex @@ -8,20 +8,34 @@ defmodule ErrorTracker.Integrations.Oban do modify anything on your application. """ + # https://hexdocs.pm/oban/Oban.Telemetry.html + @events [ + [:oban, :job, :start], + [:oban, :job, :exception] + ] + def attach do if Application.spec(:oban) do - :telemetry.attach(__MODULE__, [:oban, :job, :exception], &handle_event/4, :no_config) + :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config) end end - def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do - %{job: job, reason: exception, stacktrace: stacktrace} = metadata + def handle_event([:oban, :job, :start], _measurements, metadata, :no_config) do + %{job: job} = metadata - ErrorTracker.report(exception, stacktrace, %{ - job_id: job.id, - job_attempt: job.attempt, - job_queue: job.queue, - job_worker: job.worker + ErrorTracker.set_context(%{ + "job.args" => job.args, + "job.attempt" => job.attempt, + "job.id" => job.id, + "job.priority" => job.priority, + "job.queue" => job.queue, + "job.worker" => job.worker }) end + + def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do + %{reason: exception, stacktrace: stacktrace} = metadata + + ErrorTracker.report(exception, stacktrace) + end end diff --git a/lib/error_tracker/integrations/phoenix.ex b/lib/error_tracker/integrations/phoenix.ex index bfa4a43..1bb577d 100644 --- a/lib/error_tracker/integrations/phoenix.ex +++ b/lib/error_tracker/integrations/phoenix.ex @@ -10,32 +10,32 @@ defmodule ErrorTracker.Integrations.Phoenix do alias ErrorTracker.Integrations.Plug, as: PlugIntegration + # https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-instrumentation + @events [ + [:phoenix, :router_dispatch, :start], + [:phoenix, :router_dispatch, :exception] + ] + def attach do if Application.spec(:phoenix) do - :telemetry.attach( - __MODULE__, - [:phoenix, :router_dispatch, :exception], - &__MODULE__.handle_exception/4, - [] - ) + :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config) end end - def handle_exception( - [:phoenix, :router_dispatch, :exception], - _measurements, - %{reason: %Plug.Conn.WrapperError{conn: conn, reason: reason, stack: stack}}, - _opts - ) do - PlugIntegration.report_error(conn, reason, stack) + def handle_event([:phoenix, :router_dispatch, :start], _measurements, metadata, :no_config) do + PlugIntegration.set_context(metadata.conn) end - def handle_exception( - [:phoenix, :router_dispatch, :exception], - _measurements, - %{reason: reason, stacktrace: stack, conn: conn}, - _opts - ) do - PlugIntegration.report_error(conn, reason, stack) + def handle_event([:phoenix, :router_dispatch, :exception], _measurements, metadata, :no_config) do + {reason, stack} = + case metadata do + %{reason: %Plug.Conn.WrapperError{reason: reason, stack: stack}} -> + {reason, stack} + + %{reason: reason, stacktrace: stack} -> + {reason, stack} + end + + PlugIntegration.report_error(metadata.conn, reason, stack) end end diff --git a/lib/error_tracker/integrations/plug.ex b/lib/error_tracker/integrations/plug.ex index d5f42ab..6c56bbd 100644 --- a/lib/error_tracker/integrations/plug.ex +++ b/lib/error_tracker/integrations/plug.ex @@ -46,10 +46,11 @@ defmodule ErrorTracker.Integrations.Plug do defoverridable call: 2 def call(conn, opts) do + unquote(__MODULE__).set_context(conn) super(conn, opts) rescue e in Plug.Conn.WrapperError -> - unquote(__MODULE__).report_error(conn, e, e.stack) + unquote(__MODULE__).report_error(e.conn, e.reason, e.stack) Plug.Conn.WrapperError.reraise(e) @@ -68,13 +69,46 @@ defmodule ErrorTracker.Integrations.Plug do end end - def report_error(_conn, reason, stack) do + def report_error(conn, reason, stack) do unless Process.get(:error_tracker_router_exception_reported) do try do - ErrorTracker.report(reason, stack) + ErrorTracker.report(reason, stack, build_context(conn)) after Process.put(:error_tracker_router_exception_reported, true) end end end + + def set_context(conn = %Plug.Conn{}) do + conn |> build_context |> ErrorTracker.set_context() + end + + defp build_context(conn = %Plug.Conn{}) do + %{ + "request.host" => conn.host, + "request.path" => conn.request_path, + "request.query" => conn.query_string, + "request.method" => conn.method, + "request.ip" => remote_ip(conn), + "request.headers" => Map.new(conn.req_headers), + # Depending on the error source, the request params may have not been fetched yet + "request.params" => unless(is_struct(conn.params, Plug.Conn.Unfetched), do: conn.params) + } + end + + defp remote_ip(conn = %Plug.Conn{}) do + remote_ip = + case Plug.Conn.get_req_header(conn, "x-forwarded-for") do + [x_forwarded_for | _] -> + x_forwarded_for |> String.split(",", parts: 2) |> List.first() + + [] -> + case :inet.ntoa(conn.remote_ip) do + {:error, _} -> "" + address -> to_string(address) + end + end + + String.trim(remote_ip) + end end