From 899e6cda02d3aa5b7bdf3d8dcbb9d630d231117a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Tue, 6 Aug 2024 20:11:07 +0200 Subject: [PATCH 01/10] Send Telemetry events --- lib/error_tracker.ex | 12 +++++++++--- lib/error_tracker/telemetry.ex | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 lib/error_tracker/telemetry.ex diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 327ce86..3085c3c 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -66,6 +66,7 @@ defmodule ErrorTracker do alias ErrorTracker.Error alias ErrorTracker.Repo + alias ErrorTracker.Telemetry @doc """ Report an exception to be stored. @@ -110,9 +111,14 @@ defmodule ErrorTracker do conflict_target: :fingerprint ) - error - |> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason) - |> Repo.insert!() + occurrence = + error + |> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason) + |> Repo.insert!() + + Telemetry.execute_new_occurrence(occurrence) + + occurrence end @doc """ diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex new file mode 100644 index 0000000..3df57db --- /dev/null +++ b/lib/error_tracker/telemetry.ex @@ -0,0 +1,11 @@ +defmodule ErrorTracker.Telemetry do + @moduledoc """ + TODO + """ + + def execute_new_occurrence(occurrence) do + measurements = %{system_time: System.system_time()} + metadata = %{occurrence: occurrence} + :telemetry.execute([:error_tracker, :new_occurrence], measurements, metadata) + end +end From 6bfd8f448e0bef7fd4dadb9ce052ac6cdf71decf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 11:38:00 +0200 Subject: [PATCH 02/10] Add more events --- lib/error_tracker.ex | 42 ++++++++++++++++++++++++---------- lib/error_tracker/repo.ex | 4 ++++ lib/error_tracker/telemetry.ex | 18 ++++++++++++--- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 3085c3c..c5d9fc7 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -105,18 +105,7 @@ defmodule ErrorTracker do context = Map.merge(get_context(), given_context) - error = - Repo.insert!(error, - on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]], - conflict_target: :fingerprint - ) - - occurrence = - error - |> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason) - |> Repo.insert!() - - Telemetry.execute_new_occurrence(occurrence) + {_error, occurrence} = upsert_error!(error, stacktrace, context, reason) occurrence end @@ -186,4 +175,33 @@ defmodule ErrorTracker do {to_string(kind), to_string(other)} end end + + defp upsert_error!(error, stacktrace, context, reason) do + existing_error = Repo.get_by(Error, fingerprint: error.fingerprint) + + error = + Repo.insert!(error, + on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]], + conflict_target: :fingerprint + ) + + occurrence = + error + |> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason) + |> Repo.insert!() + + # If the error existed and was marked as resolved before this exception, + # sent a Telemetry event + if existing_error && existing_error.status == :resolved, + do: Telemetry.unresolved_error(error) + + # If it is a new error, sent a Telemetry event + if is_nil(existing_error), + do: Telemetry.new_error(error) + + # Always send a new occurrence Telemetry event + Telemetry.new_occurrence(occurrence) + + {error, occurrence} + end end diff --git a/lib/error_tracker/repo.ex b/lib/error_tracker/repo.ex index fa29432..02bcb02 100644 --- a/lib/error_tracker/repo.ex +++ b/lib/error_tracker/repo.ex @@ -17,6 +17,10 @@ defmodule ErrorTracker.Repo do dispatch(:get!, [queryable, id], opts) end + def get_by(queryable, filters, opts \\ []) do + dispatch(:get_by, [queryable, filters], opts) + end + def all(queryable, opts \\ []) do dispatch(:all, [queryable], opts) end diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 3df57db..6e97061 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -3,9 +3,21 @@ defmodule ErrorTracker.Telemetry do TODO """ - def execute_new_occurrence(occurrence) do + @doc false + def new_error(error) do measurements = %{system_time: System.system_time()} - metadata = %{occurrence: occurrence} - :telemetry.execute([:error_tracker, :new_occurrence], measurements, metadata) + :telemetry.execute([:error_tracker, :new_error], measurements, %{error: error}) + end + + @doc false + def unresolved_error(error) do + measurements = %{system_time: System.system_time()} + :telemetry.execute([:error_tracker, :unresolved_error], measurements, %{error: error}) + end + + @doc false + def new_occurrence(occurrence) do + measurements = %{system_time: System.system_time()} + :telemetry.execute([:error_tracker, :new_occurrence], measurements, %{occurrence: occurrence}) end end From 801f1c406276bf34ed86d16efb9f34ba510450e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 11:58:35 +0200 Subject: [PATCH 03/10] Emit resolve/unresolve events --- lib/error_tracker.ex | 10 ++++++++-- lib/error_tracker/telemetry.ex | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index c5d9fc7..5b4a719 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -119,7 +119,10 @@ defmodule ErrorTracker do def resolve(error = %Error{status: :unresolved}) do changeset = Ecto.Changeset.change(error, status: :resolved) - Repo.update(changeset) + with {:ok, updated_error} <- Repo.update(changeset) do + Telemetry.resolved_error(updated_error) + {:ok, updated_error} + end end @doc """ @@ -128,7 +131,10 @@ defmodule ErrorTracker do def unresolve(error = %Error{status: :resolved}) do changeset = Ecto.Changeset.change(error, status: :unresolved) - Repo.update(changeset) + with {:ok, updated_error} <- Repo.update(changeset) do + Telemetry.unresolved_error(updated_error) + {:ok, updated_error} + end end @doc """ diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 6e97061..a50eda3 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -15,6 +15,12 @@ defmodule ErrorTracker.Telemetry do :telemetry.execute([:error_tracker, :unresolved_error], measurements, %{error: error}) end + @doc false + def resolved_error(error) do + measurements = %{system_time: System.system_time()} + :telemetry.execute([:error_tracker, :unresolved_error], measurements, %{error: error}) + end + @doc false def new_occurrence(occurrence) do measurements = %{system_time: System.system_time()} From 55aaf4a2e1861e7e026d4cf78ceae021be1420fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 11:59:45 +0200 Subject: [PATCH 04/10] Update event name --- lib/error_tracker/telemetry.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index a50eda3..500c588 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -18,7 +18,7 @@ defmodule ErrorTracker.Telemetry do @doc false def resolved_error(error) do measurements = %{system_time: System.system_time()} - :telemetry.execute([:error_tracker, :unresolved_error], measurements, %{error: error}) + :telemetry.execute([:error_tracker, :resolved_error], measurements, %{error: error}) end @doc false From 58e60a5b3171306a9da7ae4d6e34efd3bf57a672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 12:01:18 +0200 Subject: [PATCH 05/10] Update events --- lib/error_tracker/telemetry.ex | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 500c588..9a46539 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -6,24 +6,28 @@ defmodule ErrorTracker.Telemetry do @doc false def new_error(error) do measurements = %{system_time: System.system_time()} - :telemetry.execute([:error_tracker, :new_error], measurements, %{error: error}) + metadata = %{error: error} + :telemetry.execute([:error_tracker, :error, :new], measurements, metadata) end @doc false def unresolved_error(error) do measurements = %{system_time: System.system_time()} - :telemetry.execute([:error_tracker, :unresolved_error], measurements, %{error: error}) + metadata = %{error: error} + :telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata) end @doc false def resolved_error(error) do measurements = %{system_time: System.system_time()} - :telemetry.execute([:error_tracker, :resolved_error], measurements, %{error: error}) + metadata = %{error: error} + :telemetry.execute([:error_tracker, :error, :resolved], measurements, metadata) end @doc false def new_occurrence(occurrence) do measurements = %{system_time: System.system_time()} - :telemetry.execute([:error_tracker, :new_occurrence], measurements, %{occurrence: occurrence}) + metadata = %{occurrence: occurrence} + :telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata) end end From d2fc4896aadaaac646647b102376ff88d74b3c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 12:10:17 +0200 Subject: [PATCH 06/10] Add telemetry documentation --- lib/error_tracker/telemetry.ex | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 9a46539..1f58327 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -1,6 +1,45 @@ defmodule ErrorTracker.Telemetry do @moduledoc """ - TODO + Telemetry events of ErrorTracker. + + ErrorTracker emits some events to allow third parties to receive information + of errors and occurrences stored. + + We emit four type of events which allows to track the lifetime of errors of + your application. + + ### Error events + + Those occur during the lifetime of an error: + + * `[:error_tracker, :error, :new]`: is emitted when a new error is stored and + no previous occurrences were known. + + * `[:error_tracker, :error, :resolved]`: is emitted when a new error is marked + as resolved on the UI. + + * `[:error_tracker, :error, :unresolved]`: is emitted when a new error is + marked as unresolved on the UI or a new occurrence is registered, moving the + error to the unresolved state. + + ### Occurrence events + + There is only one event emitted for occurrences: + + * `[:error_tracker, :occurrence, :new]`: is emitted when a new occurrence is + stored. + + ### Measures and metadata + + Each event is emitted with some measures and metadata, which can be used to + receive information without having to query the database again: + + | event | measures | metadata | + | --------------------------------------- | -------------- | ------------- | + | `[:error_tracker, :error, :new]` | `:system_time` | `:error` | + | `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error` | + | `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` | + | `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence` | """ @doc false From 92146f0b9f17336185ed282a993cc0590381ff9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 12:15:49 +0200 Subject: [PATCH 07/10] Update documentation --- guides/Getting Started.md | 8 ++++++++ lib/error_tracker/telemetry.ex | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/guides/Getting Started.md b/guides/Getting Started.md index 64a7d5d..19745f8 100644 --- a/guides/Getting Started.md +++ b/guides/Getting Started.md @@ -126,3 +126,11 @@ You can also use `ErrorTracker.report/3` and set some custom context that will b ErrorTracker also provides a dashboard built with Phoenix LiveView that can be used to see and manage the recorded errors. This is completely optional, and you can find more information about it in the `ErrorTracker.Web` module documentation. + +## Notifications + +We currently do not support notifications out of the box. + +However, we provideo some detailed Telemetry events that you may use to implement your own notifications following your custom rules and notification channels. + +If you want to take a look at the events you can attach to, take a look at `ErrorTracker.Telemetry` module documentation. diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 1f58327..19fe2d0 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -5,9 +5,6 @@ defmodule ErrorTracker.Telemetry do ErrorTracker emits some events to allow third parties to receive information of errors and occurrences stored. - We emit four type of events which allows to track the lifetime of errors of - your application. - ### Error events Those occur during the lifetime of an error: From 2ae60ee08b14fa2c8ef0eb85838a32b13c98c187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 12:24:24 +0200 Subject: [PATCH 08/10] Log telemetry events on dev script --- dev.exs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dev.exs b/dev.exs index b09a6c6..a714943 100644 --- a/dev.exs +++ b/dev.exs @@ -149,6 +149,30 @@ defmodule ErrorTrackerDevWeb.Endpoint do def maybe_exception(conn, _), do: conn end +defmodule ErrorTrackerDev.Telemetry do + require Logger + + def start do + :telemetry.attach_many( + "error-tracker-events", + [ + [:error_tracker, :error, :new], + [:error_tracker, :error, :resolved], + [:error_tracker, :error, :unresolved], + [:error_tracker, :occurrence, :new] + ], + &__MODULE__.handle_event/4, + [] + ) + + Logger.info("Telemtry attached") + end + + def handle_event(event, measure, metadata, _opts) do + dbg([event, measure, metadata]) + end +end + defmodule Migration0 do use Ecto.Migration @@ -165,6 +189,8 @@ Task.async(fn -> ErrorTrackerDevWeb.Endpoint ] + ErrorTrackerDev.Telemetry.start() + {:ok, _} = Supervisor.start_link(children, strategy: :one_for_one) # Automatically run the migrations on boot From 14c189c0fb07114ec7b558d27e303f209b041ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 12:36:15 +0200 Subject: [PATCH 09/10] Update docs --- lib/error_tracker/telemetry.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error_tracker/telemetry.ex b/lib/error_tracker/telemetry.ex index 19fe2d0..94f31e7 100644 --- a/lib/error_tracker/telemetry.ex +++ b/lib/error_tracker/telemetry.ex @@ -7,7 +7,7 @@ defmodule ErrorTracker.Telemetry do ### Error events - Those occur during the lifetime of an error: + Those occur during the life cycle of an error: * `[:error_tracker, :error, :new]`: is emitted when a new error is stored and no previous occurrences were known. From 210f158f2255ebff8a8ab8f708ac5d252a850d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20de=20Arriba?= Date: Wed, 7 Aug 2024 19:10:01 +0200 Subject: [PATCH 10/10] Only get status from existing error --- lib/error_tracker.ex | 15 +++++++++------ lib/error_tracker/repo.ex | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/error_tracker.ex b/lib/error_tracker.ex index 5b4a719..b755c67 100644 --- a/lib/error_tracker.ex +++ b/lib/error_tracker.ex @@ -64,6 +64,8 @@ defmodule ErrorTracker do """ @type context :: %{String.t() => any()} + import Ecto.Query + alias ErrorTracker.Error alias ErrorTracker.Repo alias ErrorTracker.Telemetry @@ -183,7 +185,8 @@ defmodule ErrorTracker do end defp upsert_error!(error, stacktrace, context, reason) do - existing_error = Repo.get_by(Error, fingerprint: error.fingerprint) + existing_status = + Repo.one(from e in Error, where: [fingerprint: ^error.fingerprint], select: e.status) error = Repo.insert!(error, @@ -198,12 +201,12 @@ defmodule ErrorTracker do # If the error existed and was marked as resolved before this exception, # sent a Telemetry event - if existing_error && existing_error.status == :resolved, - do: Telemetry.unresolved_error(error) - # If it is a new error, sent a Telemetry event - if is_nil(existing_error), - do: Telemetry.new_error(error) + case existing_status do + :resolved -> Telemetry.unresolved_error(error) + :unresolved -> :noop + nil -> Telemetry.new_error(error) + end # Always send a new occurrence Telemetry event Telemetry.new_occurrence(occurrence) diff --git a/lib/error_tracker/repo.ex b/lib/error_tracker/repo.ex index 02bcb02..ab8c448 100644 --- a/lib/error_tracker/repo.ex +++ b/lib/error_tracker/repo.ex @@ -17,8 +17,8 @@ defmodule ErrorTracker.Repo do dispatch(:get!, [queryable, id], opts) end - def get_by(queryable, filters, opts \\ []) do - dispatch(:get_by, [queryable, filters], opts) + def one(queryable, opts \\ []) do + dispatch(:one, [queryable], opts) end def all(queryable, opts \\ []) do