Current section

Files

Jump to
instruments lib fast_gauge.ex
Raw

lib/fast_gauge.ex

defmodule Instruments.FastGauge do
@moduledoc false
# Instruments.FastGauge sets up one ETS table per scheduler, and calls to
# `gauge/3` insert into the table for the current scheduler.
# The key for the table entry is built out of the gauge name and options.
# The value for the table entry includes the gauge value as well as the timestamp
# it was recorded at.
#
# The Instruments.FastGauge process periodically reports the most recent value
# for every table key, deleting entries that have been reported or ignored.
@table_prefix :instruments_gauges
@max_tables 128
@report_interval_ms Application.compile_env(
:instruments,
:fast_gauge_report_interval,
10_000
)
@report_jitter_range_ms Application.compile_env(
:instruments,
:fast_gauge_report_jitter_range,
-500..500
)
@compile {:inline, get_table_key: 2, latest_table_entry: 2}
@type table_entry :: {gauge_value :: number(), recorded_timestamp :: pos_integer()}
use GenServer
def start_link(_ \\ []) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(:ok) do
table_count = :erlang.system_info(:schedulers)
for scheduler_id <- 1..table_count do
:ets.new(table_name(scheduler_id), [:named_table, :public, :set])
end
reporter_module = Application.get_env(:instruments, :reporter_module, Instruments.Statix)
schedule_report()
{:ok, {reporter_module, table_count}}
end
## Public
@spec gauge(iodata, integer) :: :ok
@spec gauge(iodata, integer, Statix.options()) :: :ok
def gauge(name, value, options \\ []) do
table_key = get_table_key(name, options)
timestamp = System.monotonic_time()
:ets.insert(current_table(), [{table_key, {value, timestamp}}])
end
## GenServer callbacks
def handle_info(:report, {reporter_module, table_count} = state) do
1..table_count
|> Enum.map(fn scheduler_id -> table_name(scheduler_id) end)
|> Enum.reduce(%{}, fn table_name, acc ->
table_results =
table_name
|> :ets.tab2list()
|> Map.new()
Enum.each(table_results, &:ets.delete_object(table_name, &1))
Map.merge(acc, table_results, fn _key, table_entry_old, table_entry_new ->
latest_table_entry(table_entry_old, table_entry_new)
end)
end)
|> Enum.each(fn {table_key, {value, _recorded_timestamp}} ->
report_stat({table_key, value}, reporter_module)
end)
schedule_report()
{:noreply, state}
end
## Private
defp current_table() do
table_name(:erlang.system_info(:scheduler_id))
end
defp get_table_key(name, []) do
{name, []}
end
defp get_table_key(name, options) do
case Keyword.get(options, :tags) do
[] ->
{name, options}
[_] ->
{name, options}
tags when is_list(tags) ->
{name, Keyword.replace!(options, :tags, Enum.sort(tags))}
_ ->
{name, options}
end
end
@spec latest_table_entry(table_entry(), table_entry()) :: table_entry()
defp latest_table_entry(
{_gauge_value_left, recorded_timestamp_left} = left,
{_gauge_value_right, recorded_timestamp_right}
)
when recorded_timestamp_left > recorded_timestamp_right do
left
end
defp latest_table_entry(_left, right) do
right
end
defp report_stat({{metric_name, opts}, value}, reporter_module) do
reporter_module.gauge(metric_name, value, opts)
end
defp schedule_report() do
wait_time = @report_interval_ms + Enum.random(@report_jitter_range_ms)
Process.send_after(self(), :report, wait_time)
end
for scheduler_id <- 1..@max_tables do
defp table_name(unquote(scheduler_id)) do
unquote(:"#{@table_prefix}_#{scheduler_id}")
end
end
end