Current section
Files
Jump to
Current section
Files
lib/web/pages/metrics/page.ex
defmodule Observer.Web.Metrics.Page do
@moduledoc """
This is the live component responsible for handling the Live Metrics
"""
@behaviour Observer.Web.Page
use Observer.Web, :live_component
alias Observer.Web.Components.Attention
alias Observer.Web.Components.Core
alias Observer.Web.Components.Metrics.Phoenix, as: MetricsPhoenix
alias Observer.Web.Components.Metrics.PhxLvSocket
alias Observer.Web.Components.Metrics.VmMemory
alias Observer.Web.Components.Metrics.VmRunQueue
alias Observer.Web.Components.MultiSelect
alias Observer.Web.Page
alias ObserverWeb.Telemetry
@impl Phoenix.LiveComponent
def render(assigns) do
unselected_services_keys =
assigns.node_info.services_keys -- assigns.node_info.selected_services_keys
unselected_metrics_keys =
assigns.node_info.metrics_keys -- assigns.node_info.selected_metrics_keys
attention_msg = ""
mode_color =
case assigns.mode do
:observer ->
"text-white bg-gradient-to-r from-teal-400 via-teal-500 to-teal-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-teal-300 dark:focus:ring-teal-800 shadow-lg shadow-teal-500/50 dark:shadow-lg dark:shadow-teal-800/80"
:broadcast ->
"text-white bg-gradient-to-r from-pink-400 via-pink-500 to-pink-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-pink-300 dark:focus:ring-pink-800 shadow-lg shadow-pink-500/50 dark:shadow-lg dark:shadow-pink-800/80"
_local_or_nil ->
"text-white bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 shadow-lg shadow-cyan-500/50 dark:shadow-lg dark:shadow-cyan-800/80"
end
assigns =
assigns
|> assign(unselected_services_keys: unselected_services_keys)
|> assign(unselected_metrics_keys: unselected_metrics_keys)
|> assign(attention_msg: attention_msg)
|> assign(mode_color: mode_color)
~H"""
<div class="min-h-screen bg-white">
<Attention.content
id="metrics"
title="Configuration"
class="border-orange-400 text-orange-500 rounded-r-xl w-full"
message={@attention_msg}
>
<:inner_form>
<.form
for={@form}
id="metrics-update-form"
class="flex ml-2 mr-2 text-xs rounded-r-xl text-center text-black whitespace-nowrap gap-5"
phx-change="form-update"
>
<Core.input
field={@form[:num_cols]}
type="select"
label="Column Size"
options={["1", "2", "3", "4"]}
/>
<Core.input
field={@form[:start_time]}
type="select"
label="Start Time"
options={["1m", "5m", "15m", "30m", "1h"]}
/>
<div>
<Core.label>Mode</Core.label>
<button
type="button"
class={["#{@mode_color}", "font-medium rounded-lg text-sm mt-2 px-5 py-2 text-center"]}
>
{@mode}
</button>
</div>
</.form>
</:inner_form>
</Attention.content>
<div class="bg-white ">
<MultiSelect.content
id="metrics-multi-select"
selected_text="Selected metrics"
selected={[
%{name: "services", keys: @node_info.selected_services_keys},
%{name: "metrics", keys: @node_info.selected_metrics_keys}
]}
unselected={[
%{name: "services", keys: @unselected_services_keys},
%{name: "metrics", keys: @unselected_metrics_keys}
]}
show_options={@show_metric_options}
/>
<div class="p-2">
<div class="grid grid-cols-4 w-3xl gap-2 items-center ">
<%= for service <- @node_info.selected_services_keys do %>
<%= for metric <- @node_info.selected_metrics_keys do %>
<% app = Enum.find(@node_info.node, &(&1.service == service)) %>
<%= if metric in app.metrics_keys do %>
<% data_key = data_key(service, metric) %>
<VmMemory.content
title={"#{metric} [#{app.name}]"}
service={service}
metric={metric}
cols={@form.params["num_cols"]}
metrics={Map.get(@streams, data_key)}
/>
<VmRunQueue.content
title={"#{metric} [#{app.name}]"}
service={service}
metric={metric}
cols={@form.params["num_cols"]}
metrics={Map.get(@streams, data_key)}
/>
<PhxLvSocket.content
title={"#{metric} [#{app.name}]"}
service={service}
metric={metric}
cols={@form.params["num_cols"]}
metrics={Map.get(@streams, data_key)}
/>
<MetricsPhoenix.content
title={"#{metric} [#{app.name}]"}
service={service}
metric={metric}
cols={@form.params["num_cols"]}
transition={@metric_config[data_key]["transition"]}
metrics={Map.get(@streams, data_key)}
/>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
"""
end
@impl Page
def handle_mount(socket) when is_connected?(socket) do
# Subscribe to notifications if new metric is received
Telemetry.subscribe_for_new_keys()
# Subscribe to notifications if any node is UP or Down
:net_kernel.monitor_nodes(true)
socket
|> assign(:node_info, update_node_info())
|> assign(:node_data, %{})
|> assign(:metric_config, %{})
|> assign(form: to_form(default_form_options()))
|> assign(:show_metric_options, false)
|> assign(:mode, Telemetry.cached_mode())
end
def handle_mount(socket) do
socket
|> assign(:node_info, node_info_new())
|> assign(:node_data, %{})
|> assign(:host_info, nil)
|> assign(:metric_config, %{})
|> assign(form: to_form(default_form_options()))
|> assign(:show_metric_options, false)
|> assign(:mode, nil)
end
@impl Page
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Live Metrics")
end
@impl Page
def handle_parent_event("toggle-options", _value, socket) do
show_metric_options = !socket.assigns.show_metric_options
{:noreply, socket |> assign(:show_metric_options, show_metric_options)}
end
def handle_parent_event(
"form-update",
%{"num_cols" => num_cols, "start_time" => start_time},
%{assigns: %{node_info: node_info}} = socket
) do
start_time_integer = start_time_to_integer(start_time)
socket =
Enum.reduce(node_info.selected_services_keys, socket, fn service_key, service_acc ->
Enum.reduce(node_info.selected_metrics_keys, service_acc, fn metric_key, metric_acc ->
data_key = data_key(service_key, metric_key)
dom_id_fun = &"#{data_key}-#{&1.timestamp}"
metric_acc
|> stream(data_key, [], reset: true)
|> stream(
data_key,
Telemetry.list_data_by_node_key(service_key, metric_key, from: start_time_integer),
dom_id: dom_id_fun
)
|> assign_metric_config(data_key, %{"transition" => false})
end)
end)
{:noreply,
assign(socket, form: to_form(%{"num_cols" => num_cols, "start_time" => start_time}))}
end
def handle_parent_event(
"multi-select-remove-item",
%{"item" => "services", "key" => service_key},
%{assigns: %{node_info: node_info}} = socket
) do
node_info =
update_node_info(
node_info.selected_services_keys -- [service_key],
node_info.selected_metrics_keys
)
socket =
Enum.reduce(node_info.selected_metrics_keys, socket, fn metric_key, acc ->
Telemetry.unsubscribe_for_new_data(service_key, metric_key)
data_key = data_key(service_key, metric_key)
acc
|> stream(data_key, [], reset: true)
|> assign_metric_config(data_key, %{"transition" => false})
end)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_parent_event(
"multi-select-remove-item",
%{"item" => "metrics", "key" => metric_key},
%{assigns: %{node_info: node_info}} = socket
) do
node_info =
update_node_info(
node_info.selected_services_keys,
node_info.selected_metrics_keys -- [metric_key]
)
socket =
Enum.reduce(node_info.selected_services_keys, socket, fn service_key, acc ->
Telemetry.unsubscribe_for_new_data(service_key, metric_key)
data_key = data_key(service_key, metric_key)
acc
|> stream(data_key, [], reset: true)
|> assign_metric_config(data_key, %{"transition" => false})
end)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_parent_event(
"multi-select-add-item",
%{"item" => "services", "key" => service_key},
%{assigns: %{node_info: node_info, form: form}} = socket
) do
node_info =
update_node_info(
node_info.selected_services_keys ++ [service_key],
node_info.selected_metrics_keys
)
start_time = start_time_to_integer(form.params["start_time"])
socket =
Enum.reduce(node_info.selected_metrics_keys, socket, fn metric_key, acc ->
Telemetry.subscribe_for_new_data(service_key, metric_key)
data_key = data_key(service_key, metric_key)
dom_id_fun = &"#{data_key}-#{&1.timestamp}"
acc
|> stream(
data_key,
Telemetry.list_data_by_node_key(service_key, metric_key, from: start_time),
dom_id: dom_id_fun
)
|> assign_metric_config(data_key, %{"transition" => false})
end)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_parent_event(
"multi-select-add-item",
%{"item" => "metrics", "key" => metric_key},
%{assigns: %{node_info: node_info, form: form}} = socket
) do
node_info =
update_node_info(
node_info.selected_services_keys,
node_info.selected_metrics_keys ++ [metric_key]
)
start_time = start_time_to_integer(form.params["start_time"])
socket =
Enum.reduce(node_info.selected_services_keys, socket, fn service_key, acc ->
Telemetry.subscribe_for_new_data(service_key, metric_key)
data_key = data_key(service_key, metric_key)
dom_id_fun = &"#{data_key}-#{&1.timestamp}"
acc
|> stream(
data_key,
Telemetry.list_data_by_node_key(service_key, metric_key, from: start_time),
dom_id: dom_id_fun
)
|> assign_metric_config(data_key, %{"transition" => false})
end)
{:noreply, assign(socket, :node_info, node_info)}
end
@impl Page
def handle_info({:metrics_new_data, service, key, data}, socket) do
data_key = data_key(service, key)
{:noreply,
socket
|> stream_insert(data_key, data)
|> assign_metric_config(data_key, %{"transition" => true})}
end
def handle_info(
{:metrics_new_keys, _service, _new_keys},
%{assigns: %{node_info: node_info}} = socket
) do
node_info =
update_node_info(
node_info.selected_services_keys,
node_info.selected_metrics_keys
)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_info({:nodeup, _node}, %{assigns: %{node_info: node_info}} = socket) do
node_info =
update_node_info(
node_info.selected_services_keys,
node_info.selected_metrics_keys
)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_info({:nodedown, node}, %{assigns: %{node_info: node_info, mode: :local}} = socket) do
service_key = node |> to_string
node_info =
update_node_info(
node_info.selected_services_keys -- [service_key],
node_info.selected_metrics_keys
)
{:noreply, assign(socket, :node_info, node_info)}
end
def handle_info({:nodedown, _node}, socket) do
# NOTE: Do nothing, nodedown MUST NOT change the current
# socket information
{:noreply, socket}
end
defp data_key(service, metric), do: "#{service}::#{metric}"
defp assign_metric_config(
%{assigns: %{metric_config: metric_config}} = socket,
data_key,
attributes
) do
updated_data =
metric_config
|> Map.get(data_key, %{})
|> Map.merge(attributes)
assign(socket, :metric_config, Map.put(metric_config, data_key, updated_data))
end
defp default_form_options, do: %{"num_cols" => "2", "start_time" => "5m"}
defp start_time_to_integer("1m"), do: 1
defp start_time_to_integer("5m"), do: 5
defp start_time_to_integer("15m"), do: 15
defp start_time_to_integer("30m"), do: 30
defp start_time_to_integer("1h"), do: 60
defp node_info_new,
do: %{
services_keys: [],
metrics_keys: [],
selected_services_keys: [],
selected_metrics_keys: [],
node: []
}
defp update_node_info, do: update_node_info([], [])
defp update_node_info(selected_services_keys, selected_metrics_keys) do
initial_map = %{
node_info_new()
| selected_services_keys: selected_services_keys,
selected_metrics_keys: selected_metrics_keys
}
Enum.reduce(Telemetry.list_active_nodes(), initial_map, fn target_node,
%{
services_keys: services_keys,
metrics_keys: metrics_keys,
node: node
} = acc ->
node_metrics_keys = Telemetry.get_keys_by_node(target_node)
service = target_node |> to_string
[name, _hostname] = String.split(service, "@")
metrics_keys = (metrics_keys ++ node_metrics_keys) |> Enum.sort() |> Enum.uniq()
services_keys = Enum.sort(services_keys ++ [service])
node =
if service in selected_services_keys do
[
%{
name: name,
metrics_keys: node_metrics_keys,
service: service
}
| node
]
else
node
end
%{acc | services_keys: services_keys, metrics_keys: metrics_keys, node: node}
end)
end
end