ML-162: enable /api/v1/errors endpoint
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
defmodule MusicLibrary.Errors do
|
||||
@moduledoc """
|
||||
Queries for production errors tracked via ErrorTracker.
|
||||
|
||||
Reads from the error_tracker_errors and error_tracker_occurrences tables
|
||||
(owned by the `ErrorTracker` library) through `MusicLibrary.Repo`. The
|
||||
tables are created by ErrorTracker's own migrations and do not require
|
||||
any additional schema work.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias ErrorTracker.{Error, Occurrence}
|
||||
alias MusicLibrary.Repo
|
||||
|
||||
@type list_opts :: [
|
||||
status: :resolved | :unresolved,
|
||||
muted: boolean(),
|
||||
search: String.t(),
|
||||
limit: pos_integer(),
|
||||
offset: non_neg_integer()
|
||||
]
|
||||
|
||||
@type list_result :: %{
|
||||
errors: [Error.t()],
|
||||
total: non_neg_integer()
|
||||
}
|
||||
|
||||
@spec list_errors(list_opts()) :: list_result()
|
||||
def list_errors(opts \\ []) do
|
||||
status = Keyword.get(opts, :status)
|
||||
muted = Keyword.get(opts, :muted)
|
||||
search = Keyword.get(opts, :search)
|
||||
limit = Keyword.get(opts, :limit, 50)
|
||||
offset = Keyword.get(opts, :offset, 0)
|
||||
|
||||
base = base_query(status: status, muted: muted, search: search)
|
||||
|
||||
total = Repo.aggregate(base, :count, :id)
|
||||
|
||||
errors =
|
||||
base
|
||||
|> order_by(desc: :last_occurrence_at)
|
||||
|> limit(^limit)
|
||||
|> offset(^offset)
|
||||
|> Repo.all()
|
||||
|
||||
%{errors: errors, total: total}
|
||||
end
|
||||
|
||||
@spec get_error(pos_integer()) :: {:ok, Error.t()} | {:error, :not_found}
|
||||
def get_error(id) do
|
||||
case Repo.get(Error, id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
error ->
|
||||
occurrences =
|
||||
from(o in Occurrence,
|
||||
where: o.error_id == ^id,
|
||||
order_by: [desc: o.inserted_at]
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
occurrence_count = length(occurrences)
|
||||
first_occurrence_at = get_first_occurrence_at(occurrences)
|
||||
|
||||
result =
|
||||
%{error | occurrences: occurrences}
|
||||
|> Map.put(:occurrence_count, occurrence_count)
|
||||
|> Map.put(:first_occurrence_at, first_occurrence_at)
|
||||
|
||||
{:ok, result}
|
||||
end
|
||||
end
|
||||
|
||||
# -- private helpers --
|
||||
|
||||
defp base_query(filters) do
|
||||
Error
|
||||
|> maybe_filter_status(filters[:status])
|
||||
|> maybe_filter_muted(filters[:muted])
|
||||
|> maybe_filter_search(filters[:search])
|
||||
end
|
||||
|
||||
defp maybe_filter_status(query, nil), do: query
|
||||
defp maybe_filter_status(query, status), do: where(query, [e], e.status == ^status)
|
||||
|
||||
defp maybe_filter_muted(query, nil), do: query
|
||||
defp maybe_filter_muted(query, muted), do: where(query, [e], e.muted == ^muted)
|
||||
|
||||
defp maybe_filter_search(query, nil), do: query
|
||||
defp maybe_filter_search(query, ""), do: query
|
||||
|
||||
defp maybe_filter_search(query, search) do
|
||||
where(query, [e], like(e.reason, ^"%#{search}%"))
|
||||
end
|
||||
|
||||
defp get_first_occurrence_at([]), do: nil
|
||||
defp get_first_occurrence_at(occurrences), do: List.last(occurrences).inserted_at
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
defmodule MusicLibraryWeb.ErrorController do
|
||||
use MusicLibraryWeb, :controller
|
||||
|
||||
alias MusicLibrary.Errors
|
||||
|
||||
def index(conn, params) do
|
||||
status = parse_status(params["status"])
|
||||
muted = parse_muted(params["muted"])
|
||||
search = params["search"]
|
||||
limit = parse_int(params["limit"], 50)
|
||||
offset = parse_int(params["offset"], 0)
|
||||
|
||||
opts =
|
||||
[]
|
||||
|> maybe_put(:status, status)
|
||||
|> maybe_put(:muted, muted)
|
||||
|> Keyword.put(:search, search)
|
||||
|> Keyword.put(:limit, limit)
|
||||
|> Keyword.put(:offset, offset)
|
||||
|
||||
%{errors: errors, total: total} = Errors.list_errors(opts)
|
||||
|
||||
render(conn, :index, errors: errors, total: total, limit: limit, offset: offset)
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
{id_int, ""} = Integer.parse(id)
|
||||
|
||||
case Errors.get_error(id_int) do
|
||||
{:ok, error} ->
|
||||
render(conn, :show, error: error)
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Not Found"})
|
||||
end
|
||||
end
|
||||
|
||||
# -- private helpers --
|
||||
|
||||
defp parse_status(nil), do: nil
|
||||
defp parse_status("resolved"), do: :resolved
|
||||
defp parse_status("unresolved"), do: :unresolved
|
||||
defp parse_status(_), do: nil
|
||||
|
||||
defp parse_muted(nil), do: nil
|
||||
defp parse_muted("true"), do: true
|
||||
defp parse_muted("false"), do: false
|
||||
defp parse_muted(_), do: nil
|
||||
|
||||
defp parse_int(nil, default), do: default
|
||||
|
||||
defp parse_int(value, default) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{int, ""} -> int
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_int(_, default), do: default
|
||||
|
||||
defp maybe_put(kw, _key, nil), do: kw
|
||||
defp maybe_put(kw, key, value), do: Keyword.put(kw, key, value)
|
||||
end
|
||||
@@ -1,21 +1,85 @@
|
||||
defmodule MusicLibraryWeb.ErrorJSON do
|
||||
@moduledoc """
|
||||
This module is invoked by your endpoint in case of errors on JSON requests.
|
||||
JSON rendering for production errors via the API and for Phoenix error responses.
|
||||
|
||||
See config/config.exs.
|
||||
This module serves dual purpose:
|
||||
- Renders error/occurrence data for the /api/v1/errors endpoints
|
||||
- Renders generic error responses (404, 500, etc.) for the Phoenix endpoint
|
||||
when the request accepts JSON (configured in config/config.exs render_errors)
|
||||
"""
|
||||
use MusicLibraryWeb, :json
|
||||
|
||||
# If you want to customize a particular status code,
|
||||
# you may add your own clauses, such as:
|
||||
#
|
||||
# def render("500.json", _assigns) do
|
||||
# %{errors: %{detail: "Internal Server Error"}}
|
||||
# end
|
||||
|
||||
# By default, Phoenix returns the status message from
|
||||
# the template name. For example, "404.json" becomes
|
||||
# "Not Found".
|
||||
# Catch-all for Phoenix error template rendering (404, 500, etc.)
|
||||
def render(template, _assigns) do
|
||||
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
|
||||
%{error: Phoenix.Controller.status_message_from_template(template)}
|
||||
end
|
||||
|
||||
def index(%{errors: errors, total: total, limit: limit, offset: offset}) do
|
||||
%{
|
||||
errors: Enum.map(errors, &error/1),
|
||||
total: total,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}
|
||||
end
|
||||
|
||||
def show(%{error: error}) do
|
||||
%{
|
||||
error: error_with_occurrences(error)
|
||||
}
|
||||
end
|
||||
|
||||
defp error(e) do
|
||||
%{
|
||||
id: e.id,
|
||||
kind: e.kind,
|
||||
reason: e.reason,
|
||||
source_line: e.source_line,
|
||||
source_function: e.source_function,
|
||||
status: atom_to_string(e.status),
|
||||
fingerprint: e.fingerprint,
|
||||
last_occurrence_at: datetime_to_iso8601(e.last_occurrence_at),
|
||||
muted: e.muted,
|
||||
inserted_at: datetime_to_iso8601(e.inserted_at),
|
||||
updated_at: datetime_to_iso8601(e.updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
defp error_with_occurrences(e) do
|
||||
error(e)
|
||||
|> Map.put(:occurrence_count, Map.get(e, :occurrence_count, 0))
|
||||
|> Map.put(:first_occurrence_at, datetime_to_iso8601(Map.get(e, :first_occurrence_at)))
|
||||
|> Map.put(:occurrences, Enum.map(Map.get(e, :occurrences, []), &occurrence/1))
|
||||
end
|
||||
|
||||
defp occurrence(o) do
|
||||
%{
|
||||
id: o.id,
|
||||
reason: o.reason,
|
||||
context: o.context,
|
||||
breadcrumbs: o.breadcrumbs,
|
||||
stacktrace: %{
|
||||
lines: Enum.map(o.stacktrace.lines, &stacktrace_line/1)
|
||||
},
|
||||
error_id: o.error_id,
|
||||
inserted_at: datetime_to_iso8601(o.inserted_at)
|
||||
}
|
||||
end
|
||||
|
||||
defp stacktrace_line(line) do
|
||||
%{
|
||||
application: line.application,
|
||||
module: line.module,
|
||||
function: line.function,
|
||||
arity: line.arity,
|
||||
file: line.file,
|
||||
line: line.line
|
||||
}
|
||||
end
|
||||
|
||||
defp datetime_to_iso8601(nil), do: nil
|
||||
defp datetime_to_iso8601(dt), do: DateTime.to_iso8601(dt)
|
||||
|
||||
defp atom_to_string(nil), do: nil
|
||||
defp atom_to_string(atom), do: Atom.to_string(atom)
|
||||
end
|
||||
|
||||
@@ -130,6 +130,8 @@ defmodule MusicLibraryWeb.Router do
|
||||
get "/collection/random", CollectionController, :random
|
||||
get "/collection/on_this_day", CollectionController, :on_this_day
|
||||
get "/collection", CollectionController, :index
|
||||
get "/errors", ErrorController, :index
|
||||
get "/errors/:id", ErrorController, :show
|
||||
get "/assets/:transform_payload", AssetController, :show
|
||||
get "/backup", ArchiveController, :backup
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user