diff --git a/lib/sentry/application.ex b/lib/sentry/application.ex index e5c574df..3a8e6116 100644 --- a/lib/sentry/application.ex +++ b/lib/sentry/application.ex @@ -86,6 +86,7 @@ defmodule Sentry.Application do with {:ok, pid} <- Supervisor.start_link(children, strategy: :one_for_one, name: Sentry.Supervisor) do start_integrations(integrations_config) + maybe_attach_runtime_metrics() maybe_add_logger_handler() {:ok, pid} end @@ -93,11 +94,26 @@ defmodule Sentry.Application do @impl true def prep_stop(state) do + maybe_detach_runtime_metrics() # Flush while the telemetry processor and its HTTP client are still alive. Sentry.flush() state end + defp maybe_attach_runtime_metrics do + runtime_config = Config.metrics()[:runtime] + + if runtime_config[:enabled] do + Sentry.Metrics.Runtime.attach(runtime_config) + end + end + + defp maybe_detach_runtime_metrics do + if Config.metrics()[:runtime][:enabled] do + Sentry.Metrics.Runtime.detach() + end + end + defp cache_loaded_applications do apps_with_vsns = if Config.report_deps?() do diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 8a98e673..a37fbd8f 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -504,6 +504,45 @@ defmodule Sentry.Config do default: [], keys: integrations_schema ], + metrics: [ + type: :keyword_list, + default: [], + doc: """ + Configuration for metrics the SDK collects on its own. + *Available since 14.0.0*. + """, + keys: [ + runtime: [ + type: :keyword_list, + default: [], + doc: """ + Configuration for BEAM runtime metrics. + *Available since 14.0.0*. + """, + keys: [ + enabled: [ + type: :boolean, + default: false, + doc: """ + Whether to report BEAM runtime metrics. + *Available since 14.0.0*. + """ + ], + version_attributes: [ + type: :boolean, + default: false, + doc: """ + Whether to attach `elixir_version` and `otp_release` attributes to every + reported measurement, so metrics can be grouped by runtime version. Off by + default: the versions change only on upgrade, and attaching them to every + point starts a fresh series for each metric on every rolling deploy. + *Available since 14.0.0*. + """ + ] + ] + ] + ] + ], logs: [ type: {:or, [{:in, [nil]}, {:keyword_list, logs_schema}]}, type_doc: "`t:keyword/0` or `nil`", @@ -1127,6 +1166,9 @@ defmodule Sentry.Config do @spec integrations() :: keyword() def integrations, do: fetch!(:integrations) + @spec metrics() :: keyword() + def metrics, do: fetch!(:metrics) + @spec tracing?() :: boolean() def tracing? do (Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() and diff --git a/lib/sentry/metrics.ex b/lib/sentry/metrics.ex index 36176388..0a0b8fc5 100644 --- a/lib/sentry/metrics.ex +++ b/lib/sentry/metrics.ex @@ -25,6 +25,43 @@ defmodule Sentry.Metrics do # Record a distribution Sentry.Metrics.distribution("response.time", 42.5, unit: "millisecond") + ## Automatically Collected Metrics + + The SDK can also report BEAM runtime health on its own, without any calls to the + functions above. This is opt-in: + + config :sentry, + metrics: [ + runtime: [enabled: true] + ] + + Once enabled, these gauges are reported: + + * `elixir.runtime.mem.*` — `total`, `processes`, `processes_used`, `system`, + `atom`, `atom_used`, `binary`, `code` and `ets`, in bytes + + ### Collection Frequency + + The SDK does not collect these itself. It listens to the events that + [telemetry_poller](https://hexdocs.pm/telemetry_poller) already emits, so how often + they are reported is how often that library polls. Its default poller runs every + 5 seconds, which is more often than most applications need for runtime health and + costs metric volume, so consider slowing it down: + + config :telemetry_poller, default: [period: 30_000] + + Add the dependency if you do not have it already: + + {:telemetry_poller, "~> 1.0"} + + ### Runtime Version Attributes + + Setting `version_attributes: true` adds `elixir_version` and `otp_release` + attributes to every reported measurement, so metrics can be grouped by runtime + version. It is off by default, because those values change only on upgrade and + attaching them to every point starts a fresh series for each metric on every + rolling deploy. + ## Configuration Metrics can be filtered using the `:before_send_metric` callback: diff --git a/lib/sentry/metrics/runtime.ex b/lib/sentry/metrics/runtime.ex new file mode 100644 index 00000000..593c253f --- /dev/null +++ b/lib/sentry/metrics/runtime.ex @@ -0,0 +1,79 @@ +defmodule Sentry.Metrics.Runtime do + @moduledoc false + + alias Sentry.Metrics + + @handler_id "sentry-runtime-metrics" + @origin "auto.elixir.runtime_metrics" + + @memory_event [:vm, :memory] + + @events [@memory_event] + + @memory_keys [ + :total, + :processes, + :processes_used, + :system, + :atom, + :atom_used, + :binary, + :code, + :ets + ] + + @spec attach(keyword()) :: :ok + def attach(opts) when is_list(opts) do + :ok = detach() + + _ = :telemetry.attach_many(@handler_id, @events, &__MODULE__.handle_event/4, config(opts)) + + :ok + end + + @spec detach() :: :ok + def detach do + _ = :telemetry.detach(@handler_id) + :ok + end + + @doc false + @spec handle_event( + :telemetry.event_name(), + :telemetry.event_measurements(), + :telemetry.event_metadata(), + :telemetry.handler_config() + ) :: :ok + def handle_event(@memory_event, measurements, _metadata, config) do + report_measured(config, measurements, @memory_keys, "elixir.runtime.mem", "byte") + end + + defp report_measured(config, measurements, keys, prefix, unit) do + Enum.each(Map.take(measurements, keys), fn {key, value} -> + gauge(config, "#{prefix}.#{key}", value, unit) + end) + end + + defp config(opts) do + attributes = + Map.merge( + %{"sentry.origin" => @origin}, + version_attributes(Keyword.get(opts, :version_attributes, false)) + ) + + %{attributes: attributes} + end + + defp version_attributes(false), do: %{} + + defp version_attributes(true) do + %{ + "elixir_version" => System.version(), + "otp_release" => List.to_string(:erlang.system_info(:otp_release)) + } + end + + defp gauge(%{attributes: attributes}, name, value, unit) do + Metrics.gauge(name, value, unit: unit, attributes: attributes) + end +end diff --git a/mix-1.13-1.14.lock b/mix-1.13-1.14.lock index dafdc910..c5cfd0a5 100644 --- a/mix-1.13-1.14.lock +++ b/mix-1.13-1.14.lock @@ -70,6 +70,7 @@ "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, diff --git a/mix-1.15-1.17.lock b/mix-1.15-1.17.lock index d2b74a3a..88dfe3f3 100644 --- a/mix-1.15-1.17.lock +++ b/mix-1.15-1.17.lock @@ -73,6 +73,7 @@ "spitfire": {:hex, :spitfire, "0.4.1", "69e90335d00ca328295e1e1e77cac5d7575aa6d34274e3467ebfc654b8858be3", [:mix], [], "hexpm", "27d86f67681179682b15c6758d64ac2eb2b3637ed8340800c8b885c69754cdcd"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, diff --git a/mix-1.18.lock b/mix-1.18.lock index 7a80dda5..b6dcc47f 100644 --- a/mix-1.18.lock +++ b/mix-1.18.lock @@ -74,6 +74,7 @@ "spitfire": {:hex, :spitfire, "0.4.2", "5c719208d4eeb810e5b2a2aa1024d1e4b2974b7ca422c274b2486666e5159735", [:mix], [], "hexpm", "9bbbbffe93e6f88ccf193487ef56b83c2646a6dc3875bb0975d4654bfe96c5bb"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, diff --git a/mix.exs b/mix.exs index 7299ef71..a08760c2 100644 --- a/mix.exs +++ b/mix.exs @@ -140,6 +140,7 @@ defmodule Sentry.Mixfile do {:phoenix_live_view, "~> 0.20 or ~> 1.0", optional: true}, {:plug, dep_version(:plug, current_elixir_version()), optional: true}, {:telemetry, "~> 0.4 or ~> 1.0", optional: true}, + {:telemetry_poller, "~> 1.0", optional: true}, # Dev and test dependencies {:igniter, dep_version(:igniter, current_elixir_version()), only: :test}, diff --git a/mix.lock b/mix.lock index dc192145..63e7fed4 100644 --- a/mix.lock +++ b/mix.lock @@ -72,6 +72,7 @@ "spitfire": {:hex, :spitfire, "0.4.2", "5c719208d4eeb810e5b2a2aa1024d1e4b2974b7ca422c274b2486666e5159735", [:mix], [], "hexpm", "9bbbbffe93e6f88ccf193487ef56b83c2646a6dc3875bb0975d4654bfe96c5bb"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, diff --git a/test/sentry/application_test.exs b/test/sentry/application_test.exs index 403c5289..edfce6c0 100644 --- a/test/sentry/application_test.exs +++ b/test/sentry/application_test.exs @@ -361,6 +361,35 @@ defmodule Sentry.ApplicationTest do end end + describe "runtime metrics" do + test "the handler is not attached by default" do + restart_sentry_with([]) + + refute runtime_metrics_attached?() + end + + test "the handler is attached when runtime metrics are enabled" do + restart_sentry_with(metrics: [runtime: [enabled: true]]) + + assert runtime_metrics_attached?() + end + + test "the handler is detached when the application stops" do + restart_sentry_with(metrics: [runtime: [enabled: true]]) + assert runtime_metrics_attached?() + + assert :ok = Application.stop(:sentry) + + refute runtime_metrics_attached?() + end + end + + defp runtime_metrics_attached? do + [:vm, :memory] + |> :telemetry.list_handlers() + |> Enum.any?(&(&1.id == "sentry-runtime-metrics")) + end + defp hold_response(conn, owner) do send(owner, {:request_started, self()}) diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 3a96f9f6..fd821a18 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -487,6 +487,20 @@ defmodule Sentry.ConfigTest do end end + describe ":metrics" do + test "leaves runtime metrics disabled by default" do + runtime = Config.validate!([])[:metrics][:runtime] + + assert runtime[:enabled] == false + end + + test "does not attach runtime version attributes by default" do + runtime = Config.validate!([])[:metrics][:runtime] + + assert runtime[:version_attributes] == false + end + end + describe ":org_id" do test "defaults to nil" do assert Config.validate!([])[:org_id] == nil diff --git a/test/sentry/metrics/runtime_test.exs b/test/sentry/metrics/runtime_test.exs new file mode 100644 index 00000000..b7cdf031 --- /dev/null +++ b/test/sentry/metrics/runtime_test.exs @@ -0,0 +1,127 @@ +defmodule Sentry.Metrics.RuntimeTest do + use Sentry.Case, async: false + + import Sentry.Test.Assertions + import Sentry.TestHelpers + + alias Sentry.Metrics.Runtime + alias Sentry.Test, as: SentryTest + + @memory_measurements %{ + total: 100_000, + processes: 40_000, + processes_used: 39_000, + system: 60_000, + atom: 1_000, + atom_used: 900, + binary: 5_000, + code: 20_000, + ets: 3_000 + } + + setup do + SentryTest.setup_sentry() + end + + describe "memory metrics" do + test "reports every key of the measurement map as a gauge in bytes" do + metrics = emit_memory() + + for {key, value} <- @memory_measurements do + metric = find_metric!(metrics, "elixir.runtime.mem.#{key}") + + assert metric.value == value + assert metric.unit == "byte" + end + end + + test "skips keys the measurement map does not carry" do + metrics = emit([:vm, :memory], %{total: 1}) + + assert [%{name: "elixir.runtime.mem.total"}] = metrics + end + end + + describe "metric attributes" do + test "tags every metric with the runtime metrics origin" do + for metric <- emit_memory() do + assert metric.attributes["sentry.origin"] == "auto.elixir.runtime_metrics" + end + end + + test "names every metric under the elixir.runtime namespace" do + for metric <- emit_memory() do + assert String.starts_with?(metric.name, "elixir.runtime.") + end + end + + test "attaches the Elixir and OTP versions when version_attributes is enabled" do + for metric <- emit_memory(version_attributes: true) do + assert metric.attributes["elixir_version"] == System.version() + + assert metric.attributes["otp_release"] == + List.to_string(:erlang.system_info(:otp_release)) + end + end + + test "omits the version attributes by default" do + for metric <- emit_memory() do + refute Map.has_key?(metric.attributes, "elixir_version") + refute Map.has_key?(metric.attributes, "otp_release") + end + end + end + + describe "wiring against a real telemetry_poller" do + test "maps the builtin memory measurement onto Sentry gauges" do + attach() + poller = start_idle_poller([:memory]) + + :ok = SentryTest.allow_sentry_reports(self(), poller) + collect_once(poller) + + metric = assert_sentry_metric(:gauge, name: "elixir.runtime.mem.total") + assert metric.value > 0 + end + end + + defp emit_memory(opts \\ []), do: emit([:vm, :memory], @memory_measurements, opts) + + defp emit(event, measurements, opts \\ []) do + attach(opts) + :telemetry.execute(event, measurements, %{}) + flush_telemetry_processor() + + metrics = SentryTest.pop_sentry_metrics() + refute metrics == [] + metrics + end + + defp attach(opts \\ []) do + :ok = Runtime.attach(opts) + on_exit(&Runtime.detach/0) + end + + defp start_idle_poller(measurements) do + start_supervised!( + {:telemetry_poller, + [ + name: :"test_poller_#{System.unique_integer([:positive])}", + init_delay: :timer.hours(1), + period: :timer.hours(1), + measurements: measurements + ]} + ) + end + + defp collect_once(poller) do + send(poller, :collect) + _ = :telemetry_poller.list_measurements(poller) + :ok + end + + defp find_metric!(metrics, name) do + Enum.find(metrics, &(&1.name == name)) || + flunk("no #{name} in #{inspect(Enum.map(metrics, & &1.name))}") + end +end diff --git a/test_integrations/phoenix_app/config/test.exs b/test_integrations/phoenix_app/config/test.exs index c653d7f1..cbd8c2f2 100644 --- a/test_integrations/phoenix_app/config/test.exs +++ b/test_integrations/phoenix_app/config/test.exs @@ -39,6 +39,7 @@ config :sentry, test_mode: true, send_result: :sync, traces_sample_rate: 1.0, + metrics: [runtime: [enabled: true]], logs: [ level: :info, excluded_domains: [:cowboy, :ranch], diff --git a/test_integrations/phoenix_app/test/phoenix_app/runtime_metrics_test.exs b/test_integrations/phoenix_app/test/phoenix_app/runtime_metrics_test.exs new file mode 100644 index 00000000..de626169 --- /dev/null +++ b/test_integrations/phoenix_app/test/phoenix_app/runtime_metrics_test.exs @@ -0,0 +1,98 @@ +defmodule Sentry.Integrations.Phoenix.RuntimeMetricsTest do + use PhoenixAppWeb.ConnCase, async: false + + import Sentry.TestHelpers + + alias Sentry.Test, as: SentryTest + + @memory_keys [ + :total, + :processes, + :processes_used, + :system, + :atom, + :atom_used, + :binary, + :code, + :ets + ] + + describe "memory metrics from a real telemetry_poller" do + setup do + SentryTest.setup_sentry(collect_envelopes: [type: "trace_metric"]) + end + + test "reports every key erlang:memory/0 measures as a gauge in bytes" do + metrics = collect_runtime_metrics([:memory]) + + for key <- @memory_keys do + metric = find_metric!(metrics, "elixir.runtime.mem.#{key}") + + assert metric.value > 0 + assert metric.unit == "byte" + end + end + + test "delivers the gauges to Sentry as trace_metric envelope items", %{ref: ref} do + [:memory] |> start_vm_poller() |> allow_and_collect() + Sentry.TelemetryProcessor.flush() + + [batch] = collect_sentry_metric_items(ref, 1, timeout: 2000) + item = Enum.find(batch["items"], &(&1["name"] == "elixir.runtime.mem.total")) + + assert item["type"] == "gauge" + assert item["unit"] == "byte" + assert item["value"] > 0 + assert item["attributes"]["sentry.origin"]["value"] == "auto.elixir.runtime_metrics" + end + end + + describe "the application wiring" do + test "attaches the runtime metrics handler at boot" do + handler_ids = [:vm, :memory] |> :telemetry.list_handlers() |> Enum.map(& &1.id) + + assert "sentry-runtime-metrics" in handler_ids + end + + test "runs the default telemetry_poller the SDK relies on for its events" do + assert is_pid(Process.whereis(:telemetry_poller_default)) + end + end + + defp collect_runtime_metrics(measurements) do + measurements |> start_vm_poller() |> allow_and_collect() + Sentry.TelemetryProcessor.flush() + + metrics = SentryTest.pop_sentry_metrics() + refute metrics == [] + metrics + end + + defp start_vm_poller(measurements) do + start_supervised!( + {:telemetry_poller, + [ + name: :"runtime_metrics_poller_#{System.unique_integer([:positive])}", + init_delay: :timer.hours(1), + period: :timer.hours(1), + measurements: measurements + ]} + ) + end + + defp allow_and_collect(poller) do + :ok = SentryTest.allow_sentry_reports(self(), poller) + collect_once(poller) + end + + defp collect_once(poller) do + send(poller, :collect) + _ = :telemetry_poller.list_measurements(poller) + :ok + end + + defp find_metric!(metrics, name) do + Enum.find(metrics, &(&1.name == name)) || + flunk("no #{name} in #{inspect(Enum.map(metrics, & &1.name))}") + end +end