Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ defmodule Sentry.Application do
if config[:quantum][:cron][:enabled] do
Sentry.Integrations.Quantum.Cron.attach_telemetry_handler()
end

if config[:telemetry][:report_handler_failures] do
Sentry.Integrations.Telemetry.attach()
end
end

defp resolve_in_app_module_allow_list do
Expand Down
18 changes: 18 additions & 0 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ defmodule Sentry.Config do
]
]
]
],
telemetry: [
type: :keyword_list,
doc: """
Configuration for the [Telemetry](https://hexdocs.pm/telemetry) integration.
*Available since v10.10.0*.
""",
keys: [
report_handler_failures: [
type: :boolean,
default: false,
doc: """
Whether to report failures (to Sentry) that happen in telemetry handlers. These failures
result in the handlers being detached, so capturing them in Sentry can be useful
to detect and fix these issues as soon as possible.
"""
]
]
]
]

Expand Down
66 changes: 66 additions & 0 deletions lib/sentry/integrations/telemetry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Sentry.Integrations.Telemetry do
@moduledoc """
Sentry integration for Telemetry.

*Available since v10.10.0*.
"""

@moduledoc since: "10.10.0"

@failure_event [:telemetry, :handler, :failure]

@doc false
@spec attach() :: :ok
def attach do
_ =
:telemetry.attach(
"#{inspect(__MODULE__)}-telemetry-failures",
@failure_event,
&handle_event/4,
:no_config
)

:ok
end

@doc false
@spec handle_event(
:telemetry.event_name(),
:telemetry.event_measurements(),
:telemetry.event_metadata(),
:telemetry.handler_config()
) :: :ok
def handle_event(@failure_event, _measurements, %{} = metadata, :no_config) do
stacktrace = metadata[:stacktrace] || []
handler_id = stringified_handler_id(metadata[:handler_id])

options = [
stacktrace: stacktrace,
tags: %{
telemetry_handler_id: handler_id,
event_name: inspect(metadata[:event_name])
}
]

_ =
case {metadata[:kind], metadata[:reason]} do
{:error, reason} ->
exception = Exception.normalize(:error, reason, stacktrace)
Sentry.capture_exception(exception, options)

{kind, reason} ->
options =
Keyword.merge(options,
extra: %{kind: inspect(kind), reason: inspect(reason)},
interpolation_parameters: [handler_id]
)

Sentry.capture_message("Telemetry handler %s failed", options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a warning or even an error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already an error that gets reported to Sentry. Not sure what you mean here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whatyouhide oh I just assumed that capture_message doesn't send an error by default, and sends an info-level message instead. IIRC Sentry displays and manages these messages differently, especially when it comes to notifications.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're thinking about logging. These get displayed as errors.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whatyouhide messages in Sentry do have levels too, and our SDK supports level option in capture_message. I just checked and luckily the default level is error.

One thing I noticed is that the UI displays "(no error message)" which may be a little confusing for the users.

image

end

:ok
end

defp stringified_handler_id(handler_id) when is_binary(handler_id), do: handler_id
defp stringified_handler_id(handler_id), do: inspect(handler_id)
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule Sentry.Mixfile do
"pages/setup-with-plug-and-phoenix.md",
"pages/oban-integration.md",
"pages/quantum-integration.md",
"pages/telemetry-integration.md",
"pages/upgrade-8.x.md",
"pages/upgrade-9.x.md",
"pages/upgrade-10.x.md"
Expand All @@ -43,7 +44,8 @@ defmodule Sentry.Mixfile do
Integrations: [
"pages/setup-with-plug-and-phoenix.md",
"pages/oban-integration.md",
"pages/quantum-integration.md"
"pages/quantum-integration.md",
"pages/telemetry-integration.md"
],
"Upgrade Guides": [~r{^pages/upgrade}]
],
Expand Down
5 changes: 5 additions & 0 deletions pages/telemetry-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Telemetry Integration

The Sentry SDK supports integrating with [Telemetry](https://github.com/beam-telemetry/telemetry).

For documentation and setup instructions, see the [Sentry website](https://docs.sentry.io/platforms/elixir/integrations/telemetry/).
75 changes: 75 additions & 0 deletions test/sentry/integrations/telemetry_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule Sentry.Integrations.TelemetryTest do
use ExUnit.Case, async: true

alias Sentry.Integrations.Telemetry

describe "handle_event/4" do
test "reports errors" do
Sentry.Test.start_collecting()

handle_failure_event(:error, %RuntimeError{message: "oops"}, [])

assert [event] = Sentry.Test.pop_sentry_reports()

assert event.tags == %{
telemetry_handler_id: "my_handler",
event_name: "[:my_app, :some_event]"
}

assert event.original_exception == %RuntimeError{message: "oops"}
end

test "reports Erlang errors (normalized)" do
Sentry.Test.start_collecting()

handle_failure_event(:error, {:badmap, :foo}, [])

assert [event] = Sentry.Test.pop_sentry_reports()

assert event.tags == %{
telemetry_handler_id: "my_handler",
event_name: "[:my_app, :some_event]"
}

assert event.original_exception == %BadMapError{term: :foo}
end

for kind <- [:throw, :exit] do
test "reports #{kind}s" do
Sentry.Test.start_collecting()

handle_failure_event(unquote(kind), :foo, [])

assert [event] = Sentry.Test.pop_sentry_reports()

assert event.message.message == "Telemetry handler %s failed"
assert event.message.formatted == "Telemetry handler my_handler failed"

assert event.tags == %{
telemetry_handler_id: "my_handler",
event_name: "[:my_app, :some_event]"
}

assert event.extra == %{kind: inspect(unquote(kind)), reason: ":foo"}

assert event.original_exception == nil
end
end
end

defp handle_failure_event(kind, reason, stacktrace) do
Telemetry.handle_event(
[:telemetry, :handler, :failure],
%{system_time: System.system_time(:native), monotonic_time: System.monotonic_time(:native)},
%{
handler_id: "my_handler",
handler_config: %{my_key: "my value"},
event_name: [:my_app, :some_event],
kind: kind,
reason: reason,
stacktrace: stacktrace
},
:no_config
)
end
end