diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index 8f373728..7c06be7f 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -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 diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index bb66ba83..20114ce6 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -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. + """ + ] + ] ] ] diff --git a/lib/sentry/integrations/telemetry.ex b/lib/sentry/integrations/telemetry.ex new file mode 100644 index 00000000..033474b4 --- /dev/null +++ b/lib/sentry/integrations/telemetry.ex @@ -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) + 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 diff --git a/mix.exs b/mix.exs index 4fd0a46a..fb020ee9 100644 --- a/mix.exs +++ b/mix.exs @@ -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" @@ -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}] ], diff --git a/pages/telemetry-integration.md b/pages/telemetry-integration.md new file mode 100644 index 00000000..5e69c58c --- /dev/null +++ b/pages/telemetry-integration.md @@ -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/). diff --git a/test/sentry/integrations/telemetry_test.exs b/test/sentry/integrations/telemetry_test.exs new file mode 100644 index 00000000..412b6b97 --- /dev/null +++ b/test/sentry/integrations/telemetry_test.exs @@ -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