Skip to content

Commit 45dcf2f

Browse files
rabidpraxisstympysorentwo
authored
Add Request ID to Insights events and notices (#597)
* 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. * Insert request_id into event payload We are storing the request_id in the process dictionary * Override handle_telemetry/4 & module disable * Allows disabling a module with `config.module_name.disabled = true` * The base module now delegates handle_telemetry to an impl function, so "child" modules can do the same. * LiveView request_id for events This stores the request_id in the process tree on either of the :handle_event, :start events. It also clears the request_id on the stop events, as "events" run through the same LiveView process. * Add helper for Oban & extract request_id from job * Fix this * Set request_id and send start event if we see it * Add request_id to error notice * Inject start event in Oban for request_id * 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. * Whoops * Remove compile time matching * Apply suggestions from code review Co-authored-by: Parker Selbert <[email protected]> * Rename uuid & fix * Honeybadger.RequestId module & usage * Tweaks & ecto stacktrace inclusion * More tweaks * Added events_* options to readme * Add docstrings * Apply suggestions from code review Co-authored-by: Parker Selbert <[email protected]> * More PR suggestions * Space. --------- Co-authored-by: Benjamin Curtis <[email protected]> Co-authored-by: Parker Selbert <[email protected]>
1 parent dccbb8d commit 45dcf2f

15 files changed

+417
-72
lines changed

README.md

+35-29
Original file line numberDiff line numberDiff line change
@@ -419,35 +419,41 @@ _NOTE: This works only for the string options, and `environment_name`._
419419

420420
Here are all of the options you can pass in the keyword list:
421421

422-
| Name | Description | Default |
423-
| ------------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------- |
424-
| `app` | Name of your app's OTP Application as an atom | `Mix.Project.config[:app]` |
425-
| `api_key` | Your application's Honeybadger API key | `System.get_env("HONEYBADGER_API_KEY"))` |
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 | `[]` |
428-
| `exclude_envs` | Environments that you want to disable Honeybadger notifications | `[:dev, :test]` |
429-
| `hostname` | Hostname of the system your application is running on | `:inet.gethostname` |
430-
| `origin` | URL for the Honeybadger API | `"https://api.honeybadger.io"` |
431-
| `project_root` | Directory root for where your application is running | `System.cwd/0` |
432-
| `revision` | The project's git revision | `nil` |
433-
| `filter` | Module implementing `Honeybadger.Filter` to filter data before sending to Honeybadger.io | `Honeybadger.Filter.Default` |
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]` |
435-
| `filter_args` | If true, will remove function arguments in backtraces | `true` |
436-
| `filter_disable_url` | If true, will remove the request url | `false` |
437-
| `filter_disable_session` | If true, will remove the request session | `false` |
438-
| `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` |
440-
| `fingerprint_adapter` | Implementation of FingerprintAdapter behaviour | |
441-
| `notice_filter` | Module implementing `Honeybadger.NoticeFilter`. If `nil`, no filtering is done. | `Honeybadger.NoticeFilter.Default` |
442-
| `sasl_logging_only` | If true, will notifiy for SASL errors but not Logger calls | `true` |
443-
| `use_logger` | Enable the Honeybadger Logger for handling errors outside of web requests | `true` |
444-
| `ignored_domains` | Add domains to ignore Error events in `Honeybadger.Logger`. | `[:cowboy]` |
445-
| `breadcrumbs_enabled` | Enable breadcrumb event tracking | `false` |
446-
| `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. | `%{}` |
450-
| `http_adapter` | Module implementing `Honeybadger.HttpAdapter` to send data to Honeybadger.io | Any available adapter (`Req`, `hackney`) |
422+
| Name | Description | Default |
423+
| ------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------- |
424+
| `app` | Name of your app's OTP Application as an atom | `Mix.Project.config[:app]` |
425+
| `api_key` | Your application's Honeybadger API key | `System.get_env("HONEYBADGER_API_KEY"))` |
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 | `[]` |
428+
| `exclude_envs` | Environments that you want to disable Honeybadger notifications | `[:dev, :test]` |
429+
| `hostname` | Hostname of the system your application is running on | `:inet.gethostname` |
430+
| `origin` | URL for the Honeybadger API | `"https://api.honeybadger.io"` |
431+
| `project_root` | Directory root for where your application is running | `System.cwd/0` |
432+
| `revision` | The project's git revision | `nil` |
433+
| `filter` | Module implementing `Honeybadger.Filter` to filter data before sending to Honeybadger.io | `Honeybadger.Filter.Default` |
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]` |
435+
| `filter_args` | If true, will remove function arguments in backtraces | `true` |
436+
| `filter_disable_url` | If true, will remove the request url | `false` |
437+
| `filter_disable_session` | If true, will remove the request session | `false` |
438+
| `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` |
440+
| `fingerprint_adapter` | Implementation of FingerprintAdapter behaviour | |
441+
| `notice_filter` | Module implementing `Honeybadger.NoticeFilter`. If `nil`, no filtering is done. | `Honeybadger.NoticeFilter.Default` |
442+
| `sasl_logging_only` | If true, will notifiy for SASL errors but not Logger calls | `true` |
443+
| `use_logger` | Enable the Honeybadger Logger for handling errors outside of web requests | `true` |
444+
| `ignored_domains` | Add domains to ignore Error events in `Honeybadger.Logger`. | `[:cowboy]` |
445+
| `breadcrumbs_enabled` | Enable breadcrumb event tracking | `false` |
446+
| `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. | `%{}` |
450+
| `http_adapter` | Module implementing `Honeybadger.HttpAdapter` to send data to Honeybadger.io | Any available adapter (`Req`, `hackney`) |
451+
| `events_worker_enabled` | Enable sending events in a separate process | `true` |
452+
| `events_max_batch_retries`| Maximum number of retries for sending events | `3` |
453+
| `events_batch_size` | Maximum number of events to send in a single batch | `1000` |
454+
| `events_max_queue_size` | Maximum number of events to queue before dropping | `10000` |
455+
| `events_timeout` | Timeout in milliseconds for sending events | `5000` |
456+
| `events_throttle_wait` | Time in milliseconds to wait before retrying a failed batch | `60000` |
451457

452458
### HTTP Adapters
453459

lib/honeybadger.ex

+34-3
Original file line numberDiff line numberDiff line change
@@ -301,15 +301,16 @@ defmodule Honeybadger do
301301
|> Collector.put(notice_breadcrumb(exception))
302302
|> Collector.output()
303303

304-
metadata_with_breadcrumbs =
304+
metadata =
305305
metadata
306306
|> Map.delete(:breadcrumbs)
307307
|> contextual_metadata()
308308
|> Map.put(:breadcrumbs, breadcrumbs)
309+
|> maybe_add_request_id()
309310

310311
notice =
311312
exception
312-
|> Notice.new(metadata_with_breadcrumbs, stacktrace, fingerprint)
313+
|> Notice.new(metadata, stacktrace, fingerprint)
313314
|> put_notice_fingerprint()
314315

315316
exclude_error_value = Application.get_env(:honeybadger, :exclude_errors)
@@ -362,7 +363,10 @@ defmodule Honeybadger do
362363
def event(event_data) do
363364
ts = DateTime.utc_now() |> DateTime.to_string()
364365

365-
data = Map.put_new(event_data, :ts, ts)
366+
data =
367+
event_data
368+
|> Map.put_new(:ts, ts)
369+
|> maybe_add_request_id()
366370

367371
if get_env(:events_worker_enabled) do
368372
EventsWorker.push(data)
@@ -441,6 +445,33 @@ defmodule Honeybadger do
441445
:ok
442446
end
443447

448+
@doc """
449+
Sets the request ID for the current process.
450+
"""
451+
def put_request_id(request_id) do
452+
Honeybadger.RequestId.put(request_id)
453+
end
454+
455+
@doc """
456+
Retrieves the request ID for the current process.
457+
"""
458+
def get_request_id do
459+
Honeybadger.RequestId.get()
460+
end
461+
462+
@doc false
463+
def clear_request_id do
464+
Honeybadger.RequestId.put(nil)
465+
end
466+
467+
@doc false
468+
def maybe_add_request_id(data) when is_map(data) do
469+
case Honeybadger.RequestId.get() do
470+
nil -> data
471+
request_id -> Map.put(data, :request_id, request_id)
472+
end
473+
end
474+
444475
@doc """
445476
Fetch configuration specific to the :honeybadger application.
446477

lib/honeybadger/insights/base.ex

+30-3
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ defmodule Honeybadger.Insights.Base do
7171
Attaches telemetry handlers if all required dependencies are available.
7272
"""
7373
def attach do
74-
if dependencies_available?() do
74+
if dependencies_available?() and !get_insights_config(:disabled, false) do
7575
Enum.each(get_telemetry_events(), &attach_event/1)
7676

7777
:ok
@@ -114,8 +114,17 @@ defmodule Honeybadger.Insights.Base do
114114

115115
@doc """
116116
Handles telemetry events and processes the data.
117+
This implementation forwards to handle_event_impl which can be overridden
118+
by child modules to customize behavior while still calling the parent implementation.
117119
"""
118-
def handle_telemetry(event, measurements, metadata, _opts) do
120+
def handle_telemetry(event_name, measurements, metadata, opts) do
121+
handle_event_impl(event_name, measurements, metadata, opts)
122+
end
123+
124+
@doc """
125+
Implementation of handle_telemetry that can be called by overriding methods.
126+
"""
127+
def handle_event_impl(event, measurements, metadata, _opts) do
119128
name = Honeybadger.Utils.dotify(event)
120129

121130
unless ignore?(metadata) do
@@ -133,6 +142,20 @@ defmodule Honeybadger.Insights.Base do
133142
:ok
134143
end
135144

145+
@doc false
146+
defp maybe_put(map, key, value) do
147+
if value != nil do
148+
Map.put(map, key, value)
149+
else
150+
map
151+
end
152+
end
153+
154+
@doc false
155+
defp get_module_name(module) when is_atom(module), do: inspect(module)
156+
defp get_module_name(module) when is_binary(module), do: module
157+
defp get_module_name(_), do: nil
158+
136159
@doc """
137160
Determines if an event should be ignored based on its metadata.
138161
Child modules should override this for specific filtering logic.
@@ -153,7 +176,11 @@ defmodule Honeybadger.Insights.Base do
153176
def process_event(event_data) when is_map(event_data), do: Honeybadger.event(event_data)
154177
def process_event(_event_data), do: nil
155178

156-
defoverridable extract_metadata: 2, process_event: 1, get_telemetry_events: 0, ignore?: 1
179+
defoverridable handle_telemetry: 4,
180+
extract_metadata: 2,
181+
process_event: 1,
182+
get_telemetry_events: 0,
183+
ignore?: 1
157184
end
158185
end
159186
end

lib/honeybadger/insights/ecto.ex

+34
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ defmodule Honeybadger.Insights.Ecto do
2121
~r/schema_migrations/
2222
],
2323
24+
# Format & include the stacktrace with each query. You must also
25+
# update your repo config to enable:
26+
#
27+
# config :my_app, MyApp.Repo,
28+
# stacktrace: true
29+
#
30+
include_stacktrace: true,
31+
2432
# A list of table/source names to exclude
2533
excluded_sources: [
2634
"schema_migrations",
@@ -62,6 +70,32 @@ defmodule Honeybadger.Insights.Ecto do
6270
meta
6371
|> Map.take([:query, :decode_time, :query_time, :queue_time, :source])
6472
|> Map.update!(:query, &obfuscate(&1, meta.repo.__adapter__()))
73+
|> include_stacktrace(meta)
74+
end
75+
76+
defp include_stacktrace(data, %{stacktrace: stacktrace}) do
77+
if get_insights_config(:include_stacktrace, false) do
78+
Map.put(data, :stacktrace, format_stacktrace(stacktrace))
79+
else
80+
data
81+
end
82+
end
83+
84+
defp include_stacktrace(data, _), do: data
85+
86+
defp format_stacktrace(stacktrace) do
87+
Enum.map(stacktrace, &format_frame/1)
88+
end
89+
90+
defp format_frame({module, function, arity, location}) do
91+
position =
92+
if is_list(location) and Keyword.has_key?(location, :file) do
93+
"#{location[:file]}:#{location[:line]}"
94+
else
95+
nil
96+
end
97+
98+
[position, Exception.format_mfa(module, function, arity)]
6599
end
66100

67101
def ignore?(%{query: query, source: source}) do

lib/honeybadger/insights/live_view.ex

+43-21
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ defmodule Honeybadger.Insights.LiveView do
66
77
By default, this module listens for the following LiveView telemetry events:
88
9-
"phoenix.live_component.handle_event.stop"
109
"phoenix.live_view.mount.stop"
10+
"phoenix.live_component.handle_event.stop"
1111
"phoenix.live_view.update.stop"
1212
1313
## Custom Configuration
@@ -17,8 +17,11 @@ defmodule Honeybadger.Insights.LiveView do
1717
config :honeybadger, insights_config: %{
1818
live_view: %{
1919
telemetry_events: [
20-
[:phoenix, :live_component, :handle_event, :stop],
2120
[:phoenix, :live_view, :mount, :stop],
21+
[:phoenix, :live_component, :handle_event, :stop],
22+
[:phoenix, :live_component, :update, :stop]
23+
[:phoenix, :live_view, :handle_event, :stop],
24+
[:phoenix, :live_view, :handle_params, :stop],
2225
[:phoenix, :live_view, :update, :stop]
2326
]
2427
}
@@ -30,39 +33,58 @@ defmodule Honeybadger.Insights.LiveView do
3033
@required_dependencies [Phoenix.LiveView]
3134

3235
@telemetry_events [
33-
[:phoenix, :live_component, :handle_event, :stop],
36+
# LiveView events
3437
[:phoenix, :live_view, :mount, :stop],
35-
[:phoenix, :live_view, :update, :stop]
38+
[:phoenix, :live_view, :handle_params, :stop],
39+
[:phoenix, :live_view, :handle_event, :stop],
40+
41+
# LiveComponent events
42+
[:phoenix, :live_component, :handle_event, :stop],
43+
[:phoenix, :live_component, :update, :stop]
3644
]
3745

38-
def extract_metadata(meta, _) do
46+
def get_telemetry_events() do
47+
events = get_insights_config(:telemetry_events, @telemetry_events)
48+
49+
[[:phoenix, :live_view, :mount, :start]] ++ events
50+
end
51+
52+
def handle_telemetry(
53+
[:phoenix, :live_view, :mount, :start] = event,
54+
measurements,
55+
metadata,
56+
opts
57+
) do
58+
Honeybadger.RequestId.inherit_or_initialize()
59+
60+
if event in get_insights_config(:telemetry_events, @telemetry_events) do
61+
handle_event_impl(event, measurements, metadata, opts)
62+
end
63+
end
64+
65+
def extract_metadata(meta, _event) do
3966
%{
4067
url: Map.get(meta, :uri),
41-
socket_id: Map.get(meta, :socket_id),
68+
socket_id: extract_socket_id(meta),
4269
view: extract_view(meta),
70+
component: extract_component(meta),
4371
assigns: extract_assigns(meta),
4472
params: Map.get(meta, :params),
4573
event: Map.get(meta, :event)
4674
}
4775
end
4876

49-
defp extract_view(%{socket: socket}) do
50-
socket.view |> get_module_name()
51-
rescue
52-
_ -> nil
53-
end
77+
defp extract_component(%{component: component}), do: get_module_name(component)
78+
defp extract_component(%{socket: %{live_component: component}}), do: get_module_name(component)
79+
defp extract_component(_), do: nil
5480

55-
defp extract_view(_), do: nil
81+
defp extract_socket_id(%{socket_id: id}), do: id
82+
defp extract_socket_id(%{socket: %{id: id}}), do: id
83+
defp extract_socket_id(_), do: nil
5684

57-
defp extract_assigns(%{socket: socket}) do
58-
socket.assigns
59-
rescue
60-
_ -> nil
61-
end
85+
defp extract_view(%{socket: %{view: view}}), do: get_module_name(view)
86+
defp extract_view(_), do: nil
6287

88+
defp extract_assigns(%{socket: %{assigns: assigns}}), do: assigns
6389
defp extract_assigns(_), do: nil
64-
65-
# Helper to get module name as string
66-
defp get_module_name(module) when is_atom(module), do: inspect(module)
67-
defp get_module_name(_), do: nil
6890
end

0 commit comments

Comments
 (0)