Skip to content

Commit 0d8ae49

Browse files
authored
Telemetry events (#38)
This change adds four telemetry events to allow third parties to attach to events regarding the life cycle of Events and Occurrences. It currently emits events when: - A new error is recorded - An error is marked as resolved - An error is marked as unresolved - An occurrence is stored Documentation has been updated.
1 parent 55641da commit 0d8ae49

File tree

5 files changed

+150
-10
lines changed

5 files changed

+150
-10
lines changed

dev.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,30 @@ defmodule ErrorTrackerDevWeb.Endpoint do
149149
def maybe_exception(conn, _), do: conn
150150
end
151151

152+
defmodule ErrorTrackerDev.Telemetry do
153+
require Logger
154+
155+
def start do
156+
:telemetry.attach_many(
157+
"error-tracker-events",
158+
[
159+
[:error_tracker, :error, :new],
160+
[:error_tracker, :error, :resolved],
161+
[:error_tracker, :error, :unresolved],
162+
[:error_tracker, :occurrence, :new]
163+
],
164+
&__MODULE__.handle_event/4,
165+
[]
166+
)
167+
168+
Logger.info("Telemtry attached")
169+
end
170+
171+
def handle_event(event, measure, metadata, _opts) do
172+
dbg([event, measure, metadata])
173+
end
174+
end
175+
152176
defmodule Migration0 do
153177
use Ecto.Migration
154178

@@ -165,6 +189,8 @@ Task.async(fn ->
165189
ErrorTrackerDevWeb.Endpoint
166190
]
167191

192+
ErrorTrackerDev.Telemetry.start()
193+
168194
{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)
169195

170196
# Automatically run the migrations on boot

guides/Getting Started.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,11 @@ You can also use `ErrorTracker.report/3` and set some custom context that will b
126126
ErrorTracker also provides a dashboard built with Phoenix LiveView that can be used to see and manage the recorded errors.
127127

128128
This is completely optional, and you can find more information about it in the `ErrorTracker.Web` module documentation.
129+
130+
## Notifications
131+
132+
We currently do not support notifications out of the box.
133+
134+
However, we provideo some detailed Telemetry events that you may use to implement your own notifications following your custom rules and notification channels.
135+
136+
If you want to take a look at the events you can attach to, take a look at `ErrorTracker.Telemetry` module documentation.

lib/error_tracker.ex

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ defmodule ErrorTracker do
6464
"""
6565
@type context :: %{String.t() => any()}
6666

67+
import Ecto.Query
68+
6769
alias ErrorTracker.Error
6870
alias ErrorTracker.Repo
71+
alias ErrorTracker.Telemetry
6972

7073
@doc """
7174
Report an exception to be stored.
@@ -104,15 +107,9 @@ defmodule ErrorTracker do
104107

105108
context = Map.merge(get_context(), given_context)
106109

107-
error =
108-
Repo.insert!(error,
109-
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
110-
conflict_target: :fingerprint
111-
)
110+
{_error, occurrence} = upsert_error!(error, stacktrace, context, reason)
112111

113-
error
114-
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason)
115-
|> Repo.insert!()
112+
occurrence
116113
end
117114

118115
@doc """
@@ -124,7 +121,10 @@ defmodule ErrorTracker do
124121
def resolve(error = %Error{status: :unresolved}) do
125122
changeset = Ecto.Changeset.change(error, status: :resolved)
126123

127-
Repo.update(changeset)
124+
with {:ok, updated_error} <- Repo.update(changeset) do
125+
Telemetry.resolved_error(updated_error)
126+
{:ok, updated_error}
127+
end
128128
end
129129

130130
@doc """
@@ -133,7 +133,10 @@ defmodule ErrorTracker do
133133
def unresolve(error = %Error{status: :resolved}) do
134134
changeset = Ecto.Changeset.change(error, status: :unresolved)
135135

136-
Repo.update(changeset)
136+
with {:ok, updated_error} <- Repo.update(changeset) do
137+
Telemetry.unresolved_error(updated_error)
138+
{:ok, updated_error}
139+
end
137140
end
138141

139142
@doc """
@@ -180,4 +183,34 @@ defmodule ErrorTracker do
180183
{to_string(kind), to_string(other)}
181184
end
182185
end
186+
187+
defp upsert_error!(error, stacktrace, context, reason) do
188+
existing_status =
189+
Repo.one(from e in Error, where: [fingerprint: ^error.fingerprint], select: e.status)
190+
191+
error =
192+
Repo.insert!(error,
193+
on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
194+
conflict_target: :fingerprint
195+
)
196+
197+
occurrence =
198+
error
199+
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context, reason: reason)
200+
|> Repo.insert!()
201+
202+
# If the error existed and was marked as resolved before this exception,
203+
# sent a Telemetry event
204+
# If it is a new error, sent a Telemetry event
205+
case existing_status do
206+
:resolved -> Telemetry.unresolved_error(error)
207+
:unresolved -> :noop
208+
nil -> Telemetry.new_error(error)
209+
end
210+
211+
# Always send a new occurrence Telemetry event
212+
Telemetry.new_occurrence(occurrence)
213+
214+
{error, occurrence}
215+
end
183216
end

lib/error_tracker/repo.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ defmodule ErrorTracker.Repo do
1717
dispatch(:get!, [queryable, id], opts)
1818
end
1919

20+
def one(queryable, opts \\ []) do
21+
dispatch(:one, [queryable], opts)
22+
end
23+
2024
def all(queryable, opts \\ []) do
2125
dispatch(:all, [queryable], opts)
2226
end

lib/error_tracker/telemetry.ex

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
defmodule ErrorTracker.Telemetry do
2+
@moduledoc """
3+
Telemetry events of ErrorTracker.
4+
5+
ErrorTracker emits some events to allow third parties to receive information
6+
of errors and occurrences stored.
7+
8+
### Error events
9+
10+
Those occur during the life cycle of an error:
11+
12+
* `[:error_tracker, :error, :new]`: is emitted when a new error is stored and
13+
no previous occurrences were known.
14+
15+
* `[:error_tracker, :error, :resolved]`: is emitted when a new error is marked
16+
as resolved on the UI.
17+
18+
* `[:error_tracker, :error, :unresolved]`: is emitted when a new error is
19+
marked as unresolved on the UI or a new occurrence is registered, moving the
20+
error to the unresolved state.
21+
22+
### Occurrence events
23+
24+
There is only one event emitted for occurrences:
25+
26+
* `[:error_tracker, :occurrence, :new]`: is emitted when a new occurrence is
27+
stored.
28+
29+
### Measures and metadata
30+
31+
Each event is emitted with some measures and metadata, which can be used to
32+
receive information without having to query the database again:
33+
34+
| event | measures | metadata |
35+
| --------------------------------------- | -------------- | ------------- |
36+
| `[:error_tracker, :error, :new]` | `:system_time` | `:error` |
37+
| `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error` |
38+
| `[:error_tracker, :error, :resolved]` | `:system_time` | `:error` |
39+
| `[:error_tracker, :occurrence, :new]` | `:system_time` | `:occurrence` |
40+
"""
41+
42+
@doc false
43+
def new_error(error) do
44+
measurements = %{system_time: System.system_time()}
45+
metadata = %{error: error}
46+
:telemetry.execute([:error_tracker, :error, :new], measurements, metadata)
47+
end
48+
49+
@doc false
50+
def unresolved_error(error) do
51+
measurements = %{system_time: System.system_time()}
52+
metadata = %{error: error}
53+
:telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata)
54+
end
55+
56+
@doc false
57+
def resolved_error(error) do
58+
measurements = %{system_time: System.system_time()}
59+
metadata = %{error: error}
60+
:telemetry.execute([:error_tracker, :error, :resolved], measurements, metadata)
61+
end
62+
63+
@doc false
64+
def new_occurrence(occurrence) do
65+
measurements = %{system_time: System.system_time()}
66+
metadata = %{occurrence: occurrence}
67+
:telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata)
68+
end
69+
end

0 commit comments

Comments
 (0)