View Source Configuring metrics history

If you wish to populate metrics with history saved from telemetry or another data source, modify the dashboard config (in "my_app_web/router.ex") to include a metrics_history key like so:

live_dashboard "/dashboard",
  metrics: MyAppWeb.Telemetry,
  metrics_history: {MyApp.MetricsStorage, :metrics_history, []}

where MetricsStorage 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.

You could store the data in an ETS table or in Redis or the database, or anywhere else, but for this example we'll use a GenServer, with a circular buffer to emit recent telemetry when each client connects.

In your mix.exs, add the following to your deps:

  {:circular_buffer, "~> 0.4.0"},

Then add the following module "lib/my_app_web/metrics_storage.ex":

  defmodule MyAppWeb.MetricsStorage 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

Finally, add the new module to your Application children, and initialize it with some or all of your metrics, such as from MyAppWeb.Telemetry.metrics/0.

  # Start genserver to store transient metrics
  {MyAppWeb.MetricsStorage, MyAppWeb.Telemetry.metrics()},

Now, when you select a tab on the Metrics dashboard, LiveDashboard will call into your module to get the metrics history for that tab.