Configuring metrics history
If you wish to populate metrics with history saved from telemetry or another data source, modify the dashboard config to include a metrics_history key like so:
live_dashboard "/dashboard",
metrics: MyAppWeb.Telemetry,
metrics_history: {MyApp.MyStorage, :metrics_history, []}
where MyStorage
is a module and :metrics_history
is a function taking a single argument in this example, which will always be a metric. The function must return a list, empty if there is no data, or a list of maps with :label
, :measurement
and :time
keys in every map. The function Phoenix.LiveDashboard.extract_datapoint_for_metric/4
will return a map in exactly this format (with optional time argument if you want to override the default of System.system_time(:microsecond)
), or it may return nil
in which case the data point should not be saved.
As an example, if you want history for all metrics, you can store history for those metrics in a circular buffer and emit recent telemetry when each client connects. LiveDashboard calls into your module for history for the metrics on that tab. You would also need to add the module to your Application children, and initialize it with some or all of your metrics, such as from MyAppWeb.Telemetry.metrics/0
. You could store the data in an ETS table or in Redis or the database, or anywhere else, but for this example we'll show using a GenServer and the circular_buffer library:
defmodule MyApp.MyStorage do
use GenServer
@history_buffer_size 50
def metrics_history(metric) do
GenServer.call(__MODULE__, {:data, metric})
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
@impl true
def init(metrics) do
Process.flag(:trap_exit, true)
metric_histories_map =
metrics
|> Enum.map(fn metric ->
attach_handler(metric)
{metric, CircularBuffer.new(@history_buffer_size)}
end)
|> Map.new()
{:ok, metric_histories_map}
end
@impl true
def terminate(_, metrics) do
for metric <- metrics do
:telemetry.detach({__MODULE__, metric, self()})
end
:ok
end
defp attach_handler(%{event_name: name_list} = metric) do
:telemetry.attach(
{__MODULE__, metric, self()},
name_list,
&__MODULE__.handle_event/4,
metric
)
end
def handle_event(_event_name, data, metadata, metric) do
if data = Phoenix.LiveDashboard.extract_datapoint_for_metric(metric, data, metadata) do
GenServer.cast(__MODULE__, {:telemetry_metric, data, metric})
end
end
@impl true
def handle_cast({:telemetry_metric, data, metric}, state) do
{:noreply, update_in(state[metric], &CircularBuffer.insert(&1, data))}
end
@impl true
def handle_call({:data, metric}, _from, state) do
if history = state[metric] do
{:reply, CircularBuffer.to_list(history), state}
else
{:reply, [], state}
end
end
end