From ffdbb933e6f0ef158aafa1dd90dcf5a38918b642 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Thu, 3 Sep 2026 12:24:39 +0000 Subject: [PATCH] feat(metrics): report process, atom and port counts in runtime metrics --- lib/sentry/config.ex | 2 +- lib/sentry/metrics.ex | 4 +++ lib/sentry/metrics/runtime.ex | 42 ++++++++++++++++++++++-- test/sentry/metrics/runtime_test.exs | 48 +++++++++++++++++++++++++++- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 00a662d4..fcc38e8c 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -519,7 +519,7 @@ defmodule Sentry.Config do default: [], doc: """ Configuration for the BEAM runtime metrics collector, which periodically - reports memory usage, scheduler utilization and run queue depth. + reports memory usage, scheduler utilization, run queue depth and VM resource counts. *Available since 14.0.0*. """, keys: [ diff --git a/lib/sentry/metrics.ex b/lib/sentry/metrics.ex index 72c4843e..78001989 100644 --- a/lib/sentry/metrics.ex +++ b/lib/sentry/metrics.ex @@ -45,6 +45,10 @@ defmodule Sentry.Metrics do `elixir.runtime.mem.binary`, `elixir.runtime.mem.ets`, `elixir.runtime.mem.atom` — in bytes + * `elixir.runtime.process.count`, `elixir.runtime.atom.count`, + `elixir.runtime.port.count` — each with `limit` and `ratio` attributes + for the corresponding hard VM limit + Every reported metric carries `elixir_version` and `otp_release` attributes so measurements can be grouped by runtime version. See the `:metrics` option in the `Sentry` module documentation for the full configuration. diff --git a/lib/sentry/metrics/runtime.ex b/lib/sentry/metrics/runtime.ex index ab394b88..64ff0694 100644 --- a/lib/sentry/metrics/runtime.ex +++ b/lib/sentry/metrics/runtime.ex @@ -15,7 +15,20 @@ defmodule Sentry.Metrics.Runtime do # collectors behave consistently across Sentry SDKs. @min_interval 1_000 - defstruct [:interval, :attributes, :memory_available?, :normal_schedulers, :scheduler_sample] + @system_limits [ + {"process", :process_count, :process_limit}, + {"atom", :atom_count, :atom_limit}, + {"port", :port_count, :port_limit} + ] + + defstruct [ + :interval, + :attributes, + :memory_available?, + :normal_schedulers, + :scheduler_sample, + :system_limits + ] @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts) when is_list(opts) do @@ -44,7 +57,8 @@ defmodule Sentry.Metrics.Runtime do attributes: attributes, memory_available?: memory_available?(), normal_schedulers: normal_schedulers, - scheduler_sample: scheduler_sample(normal_schedulers) + scheduler_sample: scheduler_sample(normal_schedulers), + system_limits: system_limits() }} end @@ -81,6 +95,17 @@ defmodule Sentry.Metrics.Runtime do end) end + Enum.each(state.system_limits, fn {name, count_key, limit} -> + count = :erlang.system_info(count_key) + + gauge(state, "elixir.runtime.#{name}.count", count) + gauge(state, "elixir.runtime.#{name}.limit", limit) + + gauge(state, "elixir.runtime.#{name}.utilization", utilization_of(count, limit), + unit: "ratio" + ) + end) + %{state | scheduler_sample: sample} end @@ -125,10 +150,21 @@ defmodule Sentry.Metrics.Runtime do false end + # `+P`, `+t` and `+Q` fix these ceilings at VM boot, so they are read once. + defp system_limits do + Enum.map(@system_limits, fn {name, count_key, limit_key} -> + {name, count_key, :erlang.system_info(limit_key)} + end) + end + + defp utilization_of(_count, 0), do: 0.0 + defp utilization_of(count, limit), do: count / limit + defp gauge(state, name, value, opts \\ []) defp gauge(%__MODULE__{} = state, name, value, opts) do - Metrics.gauge(name, value, Keyword.put(opts, :attributes, state.attributes)) + attributes = Map.merge(state.attributes, Keyword.get(opts, :attributes, %{})) + Metrics.gauge(name, value, Keyword.put(opts, :attributes, attributes)) end defp normalize_interval(interval) when is_integer(interval) and interval >= @min_interval do diff --git a/test/sentry/metrics/runtime_test.exs b/test/sentry/metrics/runtime_test.exs index 351757a7..4de5f9d6 100644 --- a/test/sentry/metrics/runtime_test.exs +++ b/test/sentry/metrics/runtime_test.exs @@ -6,7 +6,7 @@ defmodule Sentry.Metrics.RuntimeTest do alias Sentry.Metrics.Runtime - @gauge_count 8 + @gauge_count 17 @memory_gauge_count 5 setup do @@ -132,6 +132,52 @@ defmodule Sentry.Metrics.RuntimeTest do end end + describe "system limit metrics" do + test "reports process, atom and port counts", %{ref: ref} do + collect_once() + + names = Enum.map(snapshot(ref), & &1["name"]) + + assert "elixir.runtime.process.count" in names + assert "elixir.runtime.atom.count" in names + assert "elixir.runtime.port.count" in names + end + + test "reports each VM limit as its own gauge", %{ref: ref} do + collect_once() + + metrics = snapshot(ref) + + assert count = find_metric(metrics, "elixir.runtime.process.count") + assert limit = find_metric(metrics, "elixir.runtime.process.limit") + + assert limit["type"] == "gauge" + assert limit["value"] >= count["value"] + end + + test "reports utilization as the count over its limit", %{ref: ref} do + collect_once() + + metrics = snapshot(ref) + + assert count = find_metric(metrics, "elixir.runtime.process.count") + assert limit = find_metric(metrics, "elixir.runtime.process.limit") + assert utilization = find_metric(metrics, "elixir.runtime.process.utilization") + + assert utilization["unit"] == "ratio" + assert_in_delta utilization["value"], count["value"] / limit["value"], 0.0001 + end + + test "keeps varying values out of attributes so each metric stays one series", %{ref: ref} do + collect_once() + + for metric <- snapshot(ref) do + refute Map.has_key?(metric["attributes"], "limit") + refute Map.has_key?(metric["attributes"], "ratio") + end + end + end + describe "delivery" do test "delivers a whole snapshot from a single collection", %{ref: ref} do collect_once()