Skip to content
Draft
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
2 changes: 1 addition & 1 deletion lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
4 changes: 4 additions & 0 deletions lib/sentry/metrics.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 39 additions & 3 deletions lib/sentry/metrics/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
48 changes: 47 additions & 1 deletion test/sentry/metrics/runtime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading