Skip to content

Commit dccbb8d

Browse files
rabidpraxisstympysorentwo
authored
Insights instrumentation (#591)
* Basic working Insights instrumentation * Fill in more insights plugins & tweaks * Put old filter_map back & remove comment * Perhaps we should actually send the event * Fix test & pass raw metadata to EventFilter * Plug is doing the same thing as this. * Also remove it from the list * Update lib/honeybadger/insights/ecto.ex Co-authored-by: Benjamin Curtis <[email protected]> * Shuffled used module specific functions around * Add Ecto obfuscation * Apply suggestions from code review Co-authored-by: Parker Selbert <[email protected]> * Add missing end from PR feedback merge * process_measurements/1 refactor Thanks @sorentwo * Add tests and class docs This also includes the `full_url` options for tesla and finch libraries * Update README doc This also won't report an event if the `filter/3` returns nil. * Load support files correctly * Update comment for Finch module Also ignore insights events for Tesla requests that use the Finch adapter, as we will send 2 events. * Fix test * Use async * Apply suggestions from code review Co-authored-by: Parker Selbert <[email protected]> * Use `String.to_existing_atom/1` * Load the atoms earlier * Stop using strings to register telemetry_events My decision to use dotified strings in the configuration to map to instrumented telemetry events kept rubbing the wrong way. It was at the point where I was preloading atoms to make sure the the terms were avaliable so `to_existing_atom` would not error that the pain was enough to reevalute things. The config now uses lists of atoms for instrumented events. I am also using these lists in all the places where callbacks were using the string event name. * Fix nested notifications issue Since we are now using the same adapter that we are instrumenting on, when we send an event to insights we get a telemetry event about sending that event, and then send another event! Recursion! This does not instrument a request event if the user-agent comes from this library. I also had to remove my Finch stub, since the :req dependency adds finch. * Fix tests --------- Co-authored-by: Benjamin Curtis <[email protected]> Co-authored-by: Parker Selbert <[email protected]>
1 parent 2dc0dba commit dccbb8d

25 files changed

+1880
-12
lines changed

README.md

+119-3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,118 @@ rescue
150150
end
151151
```
152152

153+
## Insights Automatic Instrumentation
154+
155+
Honeybadger Insights allows you to automatically track various events in your
156+
application. To enable Insights automatic instrumentation, add the following to
157+
your configuration:
158+
159+
```elixir
160+
config :honeybadger,
161+
insights_enabled: true
162+
```
163+
164+
### Supported Libraries
165+
166+
Honeybadger automatically instruments the following libraries when they are available:
167+
168+
* Absinthe: GraphQL query execution
169+
* Ecto: Database queries
170+
* Finch: HTTP client requests, often used by other libraries like Req
171+
* Tesla: HTTP client requests
172+
* LiveView: Phoenix LiveView lifecycle events
173+
* Oban: Background job processing
174+
* Plug/Phoenix: HTTP requests
175+
176+
### Custom Configuration
177+
178+
Each library can be configured individually using the `insights_config` option:
179+
180+
```elixir
181+
config :honeybadger, insights_config: %{
182+
# Ecto configuration
183+
ecto: %{
184+
# A list of strings or regex patterns of queries to exclude
185+
excluded_queries: [~r/^(begin|commit)/i],
186+
187+
# A list of table/source names to exclude
188+
excluded_sources: ["schema_migrations"]
189+
},
190+
191+
# Finch/Tesla configuration
192+
finch: %{
193+
# Include full URLs instead of just hostnames
194+
full_url: false
195+
},
196+
197+
# Plug configuration
198+
plug: %{
199+
telemetry_events: [[:phoenix, :endpoint, :stop]]
200+
},
201+
202+
# LiveView configuration
203+
live_view: %{
204+
telemetry_events: [
205+
[:phoenix, :live_view, :mount, :stop],
206+
[:phoenix, :live_view, :update, :stop]
207+
]
208+
},
209+
210+
# Absinthe configuration
211+
absinthe: %{
212+
telemetry_events: [
213+
[:absinthe, :execute, :operation, :start],
214+
[:absinthe, :execute, :operation, :stop],
215+
[:absinthe, :execute, :operation, :exception]
216+
]
217+
},
218+
219+
# Oban configuration
220+
oban: %{
221+
telemetry_events: [
222+
[:oban, :job, :stop],
223+
[:oban, :job, :exception]
224+
]
225+
}
226+
}
227+
```
228+
229+
### Event Filtering
230+
231+
You can filter out or customize automatic events sent to Honeybadger Insights
232+
by implementing the `Honeybadger.EventFilter` behaviour:
233+
234+
```elixir
235+
defmodule MyApp.EventFilter do
236+
@behaviour Honeybadger.EventFilter
237+
238+
@default_filter &Honeybadger.EventFilter.Default.filter/3
239+
240+
@impl Honeybadger.EventFilter
241+
# Pattern match on the event type name
242+
def filter_telemetry_event(data, raw, [:phoenix, :live_view, :update, :stop] = event) do
243+
if data.duration < 100 do
244+
data
245+
|> Map.put(:slow, true)
246+
# You might want to run the event through the default filter to remove
247+
# any sensitive data
248+
|> @default_filter.(raw, event)
249+
else
250+
# Ignore events that are fast
251+
nil
252+
end
253+
end
254+
255+
def filter(data, raw, event), do: @default_filter.(data, raw, event)
256+
end
257+
```
258+
259+
Then configure the filter in your application's configuration:
260+
261+
```elixir
262+
config :honeybadger, event_filter: MyApp.EventFilter
263+
```
264+
153265
## Breadcrumbs
154266

155267
Breadcrumbs allow you to record events along a processes execution path. If
@@ -311,26 +423,30 @@ Here are all of the options you can pass in the keyword list:
311423
| ------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------- |
312424
| `app` | Name of your app's OTP Application as an atom | `Mix.Project.config[:app]` |
313425
| `api_key` | Your application's Honeybadger API key | `System.get_env("HONEYBADGER_API_KEY"))` |
314-
| `environment_name` | (required) The name of the environment your app is running in. | `:prod` |
315-
|`exclude_errors` |Filters out errors from being sent to Honeybadger | `[]`|
426+
| `environment_name` | (required) The name of the environment your app is running in. | `:prod` |
427+
| `exclude_errors` | Filters out errors from being sent to Honeybadger | `[]` |
316428
| `exclude_envs` | Environments that you want to disable Honeybadger notifications | `[:dev, :test]` |
317429
| `hostname` | Hostname of the system your application is running on | `:inet.gethostname` |
318430
| `origin` | URL for the Honeybadger API | `"https://api.honeybadger.io"` |
319431
| `project_root` | Directory root for where your application is running | `System.cwd/0` |
320432
| `revision` | The project's git revision | `nil` |
321433
| `filter` | Module implementing `Honeybadger.Filter` to filter data before sending to Honeybadger.io | `Honeybadger.Filter.Default` |
322-
| `filter_keys` | A list of keywords (atoms) to filter. Only valid if `filter` is `Honeybadger.Filter.Default` | `[:password, :credit_card]` |
434+
| `filter_keys` | A list of keywords (atoms) to filter. Only valid if `filter` is `Honeybadger.Filter.Default` | `[:password, :credit_card, :__changed__, :flash, :_csrf_token]` |
323435
| `filter_args` | If true, will remove function arguments in backtraces | `true` |
324436
| `filter_disable_url` | If true, will remove the request url | `false` |
325437
| `filter_disable_session` | If true, will remove the request session | `false` |
326438
| `filter_disable_params` | If true, will remove the request params | `false` |
439+
| `filter_disable_assigns` | If true, will remove the live_view event assigns | `false` |
327440
| `fingerprint_adapter` | Implementation of FingerprintAdapter behaviour | |
328441
| `notice_filter` | Module implementing `Honeybadger.NoticeFilter`. If `nil`, no filtering is done. | `Honeybadger.NoticeFilter.Default` |
329442
| `sasl_logging_only` | If true, will notifiy for SASL errors but not Logger calls | `true` |
330443
| `use_logger` | Enable the Honeybadger Logger for handling errors outside of web requests | `true` |
331444
| `ignored_domains` | Add domains to ignore Error events in `Honeybadger.Logger`. | `[:cowboy]` |
332445
| `breadcrumbs_enabled` | Enable breadcrumb event tracking | `false` |
333446
| `ecto_repos` | Modules with implemented Ecto.Repo behaviour for tracking SQL breadcrumb events | `[]` |
447+
| `event_filter` | Module implementing `Honeybadger.EventFilter`. If `nil`, no filtering is done. | `Honeybadger.EventFilter.Default` |
448+
| `insights_enabled` | Enable sending automatic events to Honeybadger Insights | `false` |
449+
| `insights_config` | Specific library Configuration for Honeybadger Insights. | `%{}` |
334450
| `http_adapter` | Module implementing `Honeybadger.HttpAdapter` to send data to Honeybadger.io | Any available adapter (`Req`, `hackney`) |
335451

336452
### HTTP Adapters

lib/honeybadger.ex

+15
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ defmodule Honeybadger do
163163
164164
config :honeybadger,
165165
ecto_repos: [MyApp.Repo]
166+
167+
#### Insights
166168
"""
167169

168170
use Application
@@ -206,6 +208,19 @@ defmodule Honeybadger do
206208
Honeybadger.Breadcrumbs.Telemetry.attach()
207209
end
208210

211+
if config[:insights_enabled] do
212+
[
213+
Honeybadger.Insights.Plug,
214+
Honeybadger.Insights.Ecto,
215+
Honeybadger.Insights.LiveView,
216+
Honeybadger.Insights.Oban,
217+
Honeybadger.Insights.Absinthe,
218+
Honeybadger.Insights.Finch,
219+
Honeybadger.Insights.Tesla
220+
]
221+
|> Enum.each(& &1.attach())
222+
end
223+
209224
children = [{Client, [config]}, EventsWorker]
210225

211226
Supervisor.start_link(children, strategy: :one_for_one)

lib/honeybadger/event_filter.ex

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule Honeybadger.EventFilter do
2+
@moduledoc """
3+
Specification for filtering instrumented events.
4+
5+
Most users won't need this, but if you need complete control over
6+
filtering, implement this behaviour and configure like:
7+
8+
config :honeybadger,
9+
event_filter: MyApp.MyEventFilter
10+
"""
11+
12+
@doc """
13+
Filters an instrumented telemetry event.
14+
15+
## Parameters
16+
17+
* `data` - The current data for the event
18+
* `raw_event` - The raw event metadata
19+
* `event` - The telemetry event being processed, e.g. [:phoenix, :endpoint, :start]
20+
21+
## Returns
22+
23+
The filtered metadata map that will be sent to Honeybadger or `nil` to skip
24+
the event.
25+
"""
26+
@callback filter_telemetry_event(data :: map(), raw_event :: map(), event :: [atom(), ...]) ::
27+
map() | nil
28+
end
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Honeybadger.EventFilter.Default do
2+
@behaviour Honeybadger.EventFilter
3+
4+
alias Honeybadger.Utils
5+
6+
def filter_telemetry_event(data, _raw, _event) do
7+
data
8+
|> disable(:filter_disable_url, :url)
9+
|> disable(:filter_disable_session, :session)
10+
|> disable(:filter_disable_assigns, :assigns)
11+
|> disable(:filter_disable_params, :params)
12+
|> Utils.sanitize(remove_filtered: true)
13+
end
14+
15+
defp disable(meta, config_key, map_key) do
16+
if Honeybadger.get_env(config_key) do
17+
Map.drop(meta, [map_key])
18+
else
19+
meta
20+
end
21+
end
22+
end

lib/honeybadger/insights/absinthe.ex

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
defmodule Honeybadger.Insights.Absinthe do
2+
@moduledoc """
3+
Captures telemetry events from GraphQL operations executed via Absinthe.
4+
5+
## Default Configuration
6+
7+
By default, this module listens for the following Absinthe telemetry events:
8+
9+
"absinthe.execute.operation.stop"
10+
"absinthe.execute.operation.exception"
11+
12+
## Custom Configuration
13+
14+
You can customize the telemetry events to listen for by updating the insights_config:
15+
16+
config :honeybadger, insights_config: %{
17+
absinthe: %{
18+
telemetry_events: [
19+
[:absinthe, :execute, :operation, :stop],
20+
[:absinthe, :execute, :operation, :exception],
21+
[:absinthe, :resolve, :field, :stop]
22+
]
23+
}
24+
}
25+
26+
Note that adding field-level events like "absinthe.resolve.field.stop" can
27+
significantly increase the number of telemetry events generated.
28+
"""
29+
30+
use Honeybadger.Insights.Base
31+
32+
@required_dependencies [Absinthe]
33+
34+
@telemetry_events [
35+
[:absinthe, :execute, :operation, :stop],
36+
[:absinthe, :execute, :operation, :exception]
37+
]
38+
39+
# This is not loaded by default since it can add a ton of events, but is here
40+
# in case it is added to the insights_config.
41+
def extract_metadata(%{resolution: resolution}, [:absinthe, :resolve, :field, :stop]) do
42+
%{
43+
field_name: resolution.definition.name,
44+
parent_type: resolution.parent_type.name,
45+
state: resolution.state
46+
}
47+
end
48+
49+
def extract_metadata(meta, _name) do
50+
%{
51+
operation_name: get_operation_name(meta),
52+
operation_type: get_operation_type(meta),
53+
selections: get_graphql_selections(meta),
54+
schema: get_schema(meta),
55+
errors: get_errors(meta)
56+
}
57+
end
58+
59+
defp get_schema(%{blueprint: blueprint}) when is_map(blueprint), do: Map.get(blueprint, :schema)
60+
defp get_schema(_), do: nil
61+
62+
defp get_errors(%{blueprint: blueprint}) when is_map(blueprint) do
63+
case Map.get(blueprint, :result) do
64+
result when is_map(result) -> Map.get(result, :errors)
65+
_ -> nil
66+
end
67+
end
68+
69+
defp get_errors(_), do: nil
70+
71+
defp get_graphql_selections(%{blueprint: blueprint}) when is_map(blueprint) do
72+
operation = current_operation(blueprint)
73+
74+
case operation do
75+
nil ->
76+
[]
77+
78+
operation ->
79+
case Map.get(operation, :selections) do
80+
selections when is_list(selections) ->
81+
selections
82+
|> Enum.map(fn selection -> Map.get(selection, :name) end)
83+
|> Enum.uniq()
84+
85+
_ ->
86+
[]
87+
end
88+
end
89+
end
90+
91+
defp get_graphql_selections(_), do: []
92+
93+
defp get_operation_type(%{blueprint: blueprint}) when is_map(blueprint) do
94+
operation = current_operation(blueprint)
95+
96+
case operation do
97+
nil -> nil
98+
operation -> Map.get(operation, :type)
99+
end
100+
end
101+
102+
defp get_operation_type(_), do: nil
103+
104+
defp get_operation_name(%{blueprint: blueprint}) when is_map(blueprint) do
105+
operation = current_operation(blueprint)
106+
107+
case operation do
108+
nil -> nil
109+
operation -> Map.get(operation, :name)
110+
end
111+
end
112+
113+
defp get_operation_name(_), do: nil
114+
115+
# Replace Absinthe.Blueprint.current_operation/1
116+
defp current_operation(blueprint) do
117+
case Map.get(blueprint, :operations) do
118+
operations when is_list(operations) ->
119+
Enum.find(operations, fn op -> Map.get(op, :current) == true end)
120+
121+
_ ->
122+
nil
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)