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.VmLimits alias Observer.Web.Components.Metrics.VmMemory alias Observer.Web.Components.Metrics.VmPortMemory alias Observer.Web.Components.Metrics.VmProcessMemory 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) |> assign( services_unselected_highlight: (Node.list() ++ [Node.self()]) |> Enum.map(&Atom.to_string/1) ) ~H"""
<:inner_form> <.form for={@form} id="metrics-update-form" class="flex ml-2 mr-2 text-xs rounded-r-xl text-center text-zinc-800 dark:text-white whitespace-nowrap gap-5" phx-change="form-update" >
Mode
<%= 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) %> <% end %> <% end %> <% end %>
""" 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 start_time_to_integer("6h"), do: 360 defp start_time_to_integer("12h"), do: 720 defp start_time_to_integer("1d"), do: 1_440 defp start_time_to_integer("3d"), do: 4_320 defp start_time_to_integer("1w"), do: 10_080 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