defmodule Phoenix.LiveDashboard.MetricsPage do
@moduledoc false
use Phoenix.LiveDashboard.PageBuilder, refresher?: false
@menu_text "Metrics"
@impl true
def mount(params, %{metrics: {mod, fun}, metrics_history: history}, socket) do
all_metrics = apply(mod, fun, [])
metrics_per_nav = Enum.group_by(all_metrics, &nav_name/1)
nav = params["nav"]
metrics = metrics_per_nav[nav]
{first_nav, _} = Enum.at(metrics_per_nav, 0, {nil, nil})
socket = assign(socket, items: Map.keys(metrics_per_nav))
cond do
nav && is_nil(metrics) ->
to = live_dashboard_path(socket, socket.assigns.page, nav: first_nav)
{:ok, push_navigate(socket, to: to)}
metrics && connected?(socket) ->
Phoenix.LiveDashboard.TelemetryListener.listen(socket.assigns.page.node, metrics)
send_history_for_metrics(metrics, history, nav)
{:ok, assign(socket, metrics: Enum.with_index(metrics), nav: nav)}
first_nav && is_nil(nav) ->
to = live_dashboard_path(socket, socket.assigns.page, nav: first_nav)
{:ok, push_navigate(socket, to: to)}
true ->
{:ok, assign(socket, metrics: nil, nav: nav)}
end
end
defp nav_name(metric) do
to_string(metric.reporter_options[:nav] || hd(metric.name))
end
defp format_nav_name("vm"), do: "VM"
defp format_nav_name(nav), do: Phoenix.Naming.camelize(nav)
@impl true
def menu_link(_, %{dashboard_running?: false}) do
:skip
end
def menu_link(%{metrics: nil}, _) do
{:disabled, @menu_text, "https://hexdocs.pm/phoenix_live_dashboard/metrics.html"}
end
def menu_link(_, _) do
{:ok, @menu_text}
end
@impl true
def render(assigns) do
~H"""
<.live_nav_bar id="metrics_nav_bar" page={@page}>
<:item :for={item <- @items} name={item} label={format_nav_name(item)} method="redirect">
<%= for {metric, id} <- @metrics do %>
<.live_metric_chart id={id(id, @nav)} metric={metric} />
<% end %>
"""
end
@doc false
attr :id, :string,
required: true,
doc: "Because is a stateful `Phoenix.LiveComponent` an unique id is needed."
attr :metric, :any, required: true, doc: "Metric to be represented in the chart"
def live_metric_chart(%{id: id, metric: metric}) do
assigns = assigns_from_metric(id, metric)
~H"""
"""
end
def assigns_from_metric(id, metric) do
kind = chart_kind(metric.__struct__)
%{
id: id,
title: chart_title(metric),
hint: metric.description,
kind: kind,
label: chart_label(metric),
tags: metric.tags,
prune_threshold: prune_threshold(metric),
unit: chart_unit(metric.unit),
bucket_size: bucket_size(kind, metric)
}
end
defp chart_title(metric) do
"#{Enum.join(metric.name, ".")}#{chart_title_tags(metric.tags)}"
end
defp chart_title_tags([]), do: ""
defp chart_title_tags(tags), do: " (#{Enum.join(tags, "-")})"
defp chart_label(%{} = metric) do
metric.name
|> List.last()
|> Phoenix.Naming.humanize()
end
defp chart_kind(Telemetry.Metrics.Counter), do: :counter
defp chart_kind(Telemetry.Metrics.LastValue), do: :last_value
defp chart_kind(Telemetry.Metrics.Sum), do: :sum
defp chart_kind(Telemetry.Metrics.Summary), do: :summary
defp chart_kind(Telemetry.Metrics.Distribution), do: :distribution
defp chart_unit(:byte), do: "bytes"
defp chart_unit(:kilobyte), do: "KB"
defp chart_unit(:megabyte), do: "MB"
defp chart_unit(:nanosecond), do: "ns"
defp chart_unit(:microsecond), do: "µs"
defp chart_unit(:millisecond), do: "ms"
defp chart_unit(:second), do: "s"
defp chart_unit(:unit), do: ""
defp chart_unit(unit) when is_atom(unit), do: Atom.to_string(unit)
@default_prune_threshold 1_000
defp prune_threshold(metric) do
metric.reporter_options[:prune_threshold] || @default_prune_threshold
end
defp bucket_size(:distribution, metric), do: normalize_bucket_size(metric)
defp bucket_size(_kind, _metric), do: nil
@default_bucket_size 20
defp normalize_bucket_size(metric) do
metric.reporter_options[:bucket_size] || @default_bucket_size
end
defp send_updates_for_entries(entries, nav) do
for {id, label, measurement, time} <- entries do
data = [{label, measurement, time}]
send_data_to_chart(id(id, nav), data)
end
end
defp send_updates_for_entries_in_chunks(entries, nav) do
## Batch historical data up into chunks of 500 to reduce the number
## of messages sent over the wire, but keep them small enough that
## the client still feels responsive.
entries
|> Enum.group_by(
fn {id, _, _, _} -> id end,
fn {_, label, measurement, time} -> {label, measurement, time} end
)
|> Enum.each(fn {id, data} ->
data
|> Enum.chunk_every(500)
|> Enum.each(fn chunk -> send_data_to_chart(id(id, nav), chunk) end)
end)
end
defp id(id, nav), do: "#{nav}-#{id}"
@impl true
def handle_info({:telemetry, entries}, socket) do
send_updates_for_entries(entries, socket.assigns.nav)
{:noreply, socket}
end
defp send_history_for_metrics(_, nil, _), do: :noop
defp send_history_for_metrics(metrics, history, nav) do
for {metric, id} <- Enum.with_index(metrics) do
metric
|> history_for(id, history)
|> send_updates_for_entries_in_chunks(nav)
end
end
defp history_for(metric, id, {module, function, opts}) do
history = apply(module, function, [metric | opts])
for %{label: label, measurement: measurement, time: time} <- history do
{id, label, measurement, time}
end
end
end