Skip to content

Commit c2dc311

Browse files
authored
Breadcrumbs (#235)
* RingBuffer * Basic working version * Added Collector * Adding error breadcrumb in notice * Fixing some specs, ignore DateTime in to_encodable * Added Utils.sanitize * Moved notice breadcrumb creation out of notice.new * Now sanitizing breadcrumb metadata * Filter breadcrumbs * Convert structs to Map in sanitizer * Dropped elixir 1.7 and added 1.9 to the matrix * Storing error breadcrumb * Basic setup for telemetry events * Added specs for telemetry * Updated docs
1 parent 6efbe99 commit c2dc311

27 files changed

+821
-31
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
language: elixir
22
sudo: false
33
elixir:
4-
- 1.7
54
- 1.8
5+
- 1.9
66
otp_release:
77
- 21.2
88
- 22.0

README.md

+68
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,53 @@ rescue
119119
end
120120
```
121121

122+
## Breadcrumbs
123+
124+
Breadcrumbs allow you to record events along a processes execution path. If
125+
an error is thrown, the set of breadcrumb events will be sent along with the
126+
notice. These breadcrumbs can contain useful hints while debugging.
127+
128+
Breadcrumbs are stored in the logger context, referenced by the calling
129+
process. If you are sending messages between processes, breadcrumbs will not
130+
transfer automatically. Since a typical system might have many processes, it
131+
is advised that you be conservative when storing breadcrumbs as each
132+
breadcrumb consumes memory.
133+
134+
### Enabling Breadcrumbs
135+
136+
As of version `0.13.0`, Breadcrumbs are _available_ yet _disabled_. You must
137+
explicitly enable them if you want breadcrumbs to be reported. We plan on
138+
enabling this by default in a future release.
139+
140+
Toggle `breadcrumbs_enabled` in the config to start sending Breadcrumbs with
141+
notices:
142+
143+
```elixir
144+
config :honeybadger,
145+
breadcrumbs_enabled: true
146+
```
147+
148+
### Automatic Breadcrumbs
149+
150+
We leverage the `telemetry` library to automatically create breadcrumbs from
151+
specific events.
152+
153+
__Phoenix__
154+
155+
If you are using `phoenix` (>= v1.4.7) we add a breadcrumb from the router
156+
start event.
157+
158+
__Ecto__
159+
160+
We can create breadcrumbs from Ecto SQL calls if you are using `ecto_sql` (>=
161+
v3.1.0). You also must specify in the config which ecto adapters you want to
162+
be instrumented:
163+
164+
```elixir
165+
config :honeybadger,
166+
ecto_repos: [MyApp.Repo]
167+
```
168+
122169
## Sample Application
123170

124171
If you'd like to see the module in action before you integrate it with your apps, check out our [sample Phoenix application](https://github.com/honeybadger-io/crywolf-elixir).
@@ -187,6 +234,8 @@ Here are all of the options you can pass in the keyword list:
187234
| `filter_disable_params` | If true, will remove the request params | `false` |
188235
| `notice_filter` | Module implementing `Honeybadger.NoticeFilter`. If `nil`, no filtering is done. | `Honeybadger.NoticeFilter.Default` |
189236
| `use_logger` | Enable the Honeybadger Logger for handling errors outside of web requests | `true` |
237+
| `breadcrumbs_enabled` | Enable breadcrumb event tracking | `false` |
238+
| `ecto_repos` | Modules with implemented Ecto.Repo behaviour for tracking SQL breadcrumb events | `[]` |
190239

191240
## Public Interface
192241

@@ -248,6 +297,25 @@ end)
248297

249298
---
250299

300+
### `Honeybadger.add_breadcrumb/2`: Store breadcrumb within process
301+
302+
Appends a breadcrumb to the notice. Use this when you want to add some custom
303+
data to your breadcrumb trace in effort to help debugging. If a notice is
304+
reported to Honeybadger, all breadcrumbs within the execution path will be
305+
appended to the notice. You will be able to view the breadcrumb trace in the
306+
Honeybadger interface to see what events led up to the notice.
307+
308+
#### Examples:
309+
310+
```elixir
311+
Honeybadger.add_breadcrumb("Email sent", metadata: %{
312+
user: user.id,
313+
message: message
314+
})
315+
```
316+
317+
---
318+
251319
## Proxy configuration
252320

253321
If your server needs a proxy to access honeybadger, add the following to your config

dummy/mixapp/mix.lock

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
"plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
1010
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
1111
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
12+
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
1213
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
1314
}

lib/honeybadger.ex

+114-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ defmodule Honeybadger do
1616
environment_name: :prod,
1717
app: :my_app_name,
1818
exclude_envs: [:dev, :test],
19+
breadcrumbs_enabled: false,
20+
ecto_repos: [MyAppName.Ecto.Repo],
1921
hostname: "myserver.domain.com",
2022
origin: "https://api.honeybadger.io",
2123
proxy: "http://proxy.net:PORT",
@@ -121,11 +123,51 @@ defmodule Honeybadger do
121123
end
122124
123125
See `Honeybadger.Filter` for details on implementing your own filter.
126+
127+
### Breadcrumbs
128+
129+
Breadcrumbs allow you to record events along a processes execution path. If
130+
an error is thrown, the set of breadcrumb events will be sent along with the
131+
notice. These breadcrumbs can contain useful hints while debugging.
132+
133+
Breadcrumbs are stored in the logger context, referenced by the calling
134+
process. If you are sending messages between processes, breadcrumbs will not
135+
transfer automatically. Since a typical system might have many processes, it
136+
is advised that you be conservative when storing breadcrumbs as each
137+
breadcrumb consumes memory.
138+
139+
Ensure that you enable breadcrumbs in the config (as it is disabled by
140+
default):
141+
142+
config :honeybadger,
143+
breadcrumbs_enabled: true
144+
145+
See `Honeybadger.add_breadcrumb` for info on how to add custom breadcrumbs.
146+
147+
### Automatic Breadcrumbs
148+
149+
We leverage the `telemetry` library to automatically create breadcrumbs from
150+
specific events.
151+
152+
#### Phoenix
153+
154+
If you are using `phoenix` (>= v1.4.7) we add a breadcrumb from the router
155+
start event.
156+
157+
#### Ecto
158+
159+
We can create breadcrumbs from Ecto SQL calls if you are using `ecto_sql` (>=
160+
v3.1.0). You also must specify in the config which ecto adapters you want to
161+
be instrumented:
162+
163+
config :honeybadger,
164+
ecto_repos: [MyApp.Repo]
124165
"""
125166

126167
use Application
127168

128169
alias Honeybadger.{Client, Notice}
170+
alias Honeybadger.Breadcrumbs.{Collector, Breadcrumb}
129171

130172
defmodule MissingEnvironmentNameError do
131173
defexception message: """
@@ -152,6 +194,10 @@ defmodule Honeybadger do
152194
_ = Logger.add_backend(Honeybadger.Logger)
153195
end
154196

197+
if config[:breadcrumbs_enabled] do
198+
Honeybadger.Breadcrumbs.Telemetry.attach()
199+
end
200+
155201
children = [
156202
worker(Client, [config])
157203
]
@@ -203,11 +249,53 @@ defmodule Honeybadger do
203249
"""
204250
@spec notify(Notice.noticeable(), map(), list()) :: :ok
205251
def notify(exception, metadata \\ %{}, stacktrace \\ []) do
252+
# Grab process local breadcrumbs if not passed with call and add notice breadcrumb
253+
breadcrumbs =
254+
metadata
255+
|> Map.get(:breadcrumbs, Collector.breadcrumbs())
256+
|> Collector.put(notice_breadcrumb(exception))
257+
|> Collector.output()
258+
259+
metadata_with_breadcrumbs =
260+
metadata
261+
|> Map.delete(:breadcrumbs)
262+
|> contextual_metadata()
263+
|> Map.put(:breadcrumbs, breadcrumbs)
264+
206265
exception
207-
|> Notice.new(contextual_metadata(metadata), stacktrace)
266+
|> Notice.new(metadata_with_breadcrumbs, stacktrace)
208267
|> Client.send_notice()
209268
end
210269

270+
@doc """
271+
Stores a breadcrumb item.
272+
273+
Appends a breadcrumb to the notice. Use this when you want to add some custom
274+
data to your breadcrumb trace in effort to help debugging. If a notice is
275+
reported to Honeybadger, all breadcrumbs within the execution path will be
276+
appended to the notice. You will be able to view the breadcrumb trace in the
277+
Honeybadger interface to see what events led up to the notice.
278+
279+
## Breadcrumb with metadata
280+
281+
Honeybadger.add_breadcrumb("email sent", metadata: %{
282+
user: user.id, message: message
283+
})
284+
=> :ok
285+
286+
## Breadcrumb with specified category. This will display a query icon in the interface
287+
288+
Honeybadger.add_breadcrumb("ETS Lookup", category: "query", metadata: %{
289+
key: key,
290+
value: value
291+
})
292+
=> :ok
293+
"""
294+
@spec add_breadcrumb(String.t(), Breadcrumb.opts()) :: :ok
295+
def add_breadcrumb(message, opts \\ []) when is_binary(message) and is_list(opts) do
296+
Collector.add(Breadcrumb.new(message, opts))
297+
end
298+
211299
@doc """
212300
Retrieves the context that will be sent to the Honeybadger API when an exception occurs in the
213301
current process.
@@ -216,7 +304,7 @@ defmodule Honeybadger do
216304
"""
217305
@spec context() :: map()
218306
def context do
219-
Logger.metadata() |> Map.new()
307+
Logger.metadata() |> Map.new() |> Map.delete(Collector.metadata_key())
220308
end
221309

222310
@doc """
@@ -291,6 +379,30 @@ defmodule Honeybadger do
291379

292380
# Helpers
293381

382+
# Allows for Notice breadcrumb to have custom text as message if an error is
383+
# not passed to the notice function. We can assume if it was passed an error
384+
# then there will be an error breadcrumb right before this one.
385+
defp notice_breadcrumb(exception) do
386+
reason =
387+
case exception do
388+
title when is_binary(title) ->
389+
title
390+
391+
error when is_atom(error) and not is_nil(error) ->
392+
:error
393+
|> Exception.normalize(error)
394+
|> Map.get(:message, to_string(error))
395+
396+
_ ->
397+
nil
398+
end
399+
400+
["Honeybadger Notice", reason]
401+
|> Enum.reject(&is_nil/1)
402+
|> Enum.join(": ")
403+
|> Breadcrumb.new(category: "notice")
404+
end
405+
294406
defp put_dynamic_env(config) do
295407
hostname = fn ->
296408
:inet.gethostname()
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule Honeybadger.Breadcrumbs.Breadcrumb do
2+
@moduledoc false
3+
4+
@derive Jason.Encoder
5+
6+
@type t :: %__MODULE__{
7+
message: String.t(),
8+
category: String.t(),
9+
timestamp: DateTime.t(),
10+
metadata: map()
11+
}
12+
13+
@type opts :: [{:metadata, map()} | {:category, String.t()}]
14+
@enforce_keys [:message, :category, :timestamp, :metadata]
15+
16+
@default_category "custom"
17+
@default_metadata %{}
18+
19+
defstruct [:message, :category, :timestamp, :metadata]
20+
21+
@spec new(String.t(), opts()) :: t()
22+
def new(message, opts) do
23+
%__MODULE__{
24+
message: message,
25+
category: opts[:category] || @default_category,
26+
timestamp: DateTime.utc_now(),
27+
metadata: opts[:metadata] || @default_metadata
28+
}
29+
end
30+
31+
@spec from_error(any()) :: t()
32+
def from_error(error) do
33+
error = Exception.normalize(:error, error, [])
34+
35+
%{__struct__: error_mod} = error
36+
37+
new(
38+
Honeybadger.Utils.module_to_string(error_mod),
39+
metadata: %{message: error_mod.message(error)},
40+
category: "error"
41+
)
42+
end
43+
end
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule Honeybadger.Breadcrumbs.Collector do
2+
@moduledoc false
3+
4+
@doc """
5+
The Collector provides an interface for accessing and affecting the current
6+
set of breadcrumbs. Most operations are delegated to the supplied Buffer
7+
implementation. This is mainly for internal use.
8+
"""
9+
10+
alias Honeybadger.Breadcrumbs.{RingBuffer, Breadcrumb}
11+
alias Honeybadger.Utils
12+
13+
@buffer_impl RingBuffer
14+
@buffer_size 40
15+
@metadata_key :hb_breadcrumbs
16+
17+
@type t :: %{enabled: boolean(), trail: [Breadcrumb.t()]}
18+
19+
@spec output() :: t()
20+
def output(), do: output(breadcrumbs())
21+
22+
@spec output(@buffer_impl.t()) :: t()
23+
def output(breadcrumbs) do
24+
%{
25+
enabled: Honeybadger.get_env(:breadcrumbs_enabled),
26+
trail: @buffer_impl.to_list(breadcrumbs)
27+
}
28+
end
29+
30+
@spec put(@buffer_impl.t(), Breadcrumb.t()) :: @buffer_impl.t()
31+
def put(breadcrumbs, breadcrumb) do
32+
@buffer_impl.add(
33+
breadcrumbs,
34+
Map.update(breadcrumb, :metadata, %{}, &Utils.sanitize(&1, max_depth: 1))
35+
)
36+
end
37+
38+
@spec add(Breadcrumb.t()) :: :ok
39+
def add(breadcrumb) do
40+
if Honeybadger.get_env(:breadcrumbs_enabled) do
41+
Logger.metadata([{@metadata_key, put(breadcrumbs(), breadcrumb)}])
42+
end
43+
44+
:ok
45+
end
46+
47+
@spec clear() :: :ok
48+
def clear() do
49+
Logger.metadata([{@metadata_key, @buffer_impl.new(@buffer_size)}])
50+
end
51+
52+
def metadata_key(), do: @metadata_key
53+
54+
@spec breadcrumbs() :: @buffer_impl.t()
55+
def breadcrumbs() do
56+
Logger.metadata()
57+
|> Keyword.get(@metadata_key, @buffer_impl.new(@buffer_size))
58+
end
59+
end

0 commit comments

Comments
 (0)