defmodule Observer.Web.Tracing.Page do @moduledoc """ This is the live component responsible for handling the Tracing debug """ @behaviour Observer.Web.Page use Observer.Web, :live_component alias Observer.Web.Components.Attention alias Observer.Web.Components.CopyToClipboard alias Observer.Web.Components.Core alias Observer.Web.Components.MultiSelectList alias Observer.Web.Page alias ObserverWeb.Tracer @impl Phoenix.LiveComponent def render(assigns) do unselected_services_keys = assigns.node_info.services_keys -- assigns.node_info.selected_services_keys unselected_modules_keys = assigns.node_info.modules_keys -- assigns.node_info.selected_modules_keys unselected_functions_keys = assigns.node_info.functions_keys -- assigns.node_info.selected_functions_keys unselected_match_spec_keys = assigns.node_info.match_spec_keys -- assigns.node_info.selected_match_spec_keys trace_state = Tracer.state() trace_idle? = trace_state.status == :idle trace_owner? = trace_state.session_id == assigns.trace_session_id # Hide options when running show_tracing_options = trace_idle? and trace_owner? and assigns.show_tracing_options match_spec_info = ~H""" (more_info) """ attention_msg = ~H""" Incorrect use of the :dbg tracer in production can lead to performance degradation, latency and crashes. Observer Web tracing enforces limits on the maximum number of messages and applies a timeout (in seconds) to ensure the debugger doesn't remain active unintentionally. Check out the Erlang Debugger for more detailed information. """ assigns = assigns |> assign(unselected_services_keys: unselected_services_keys) |> assign(unselected_modules_keys: unselected_modules_keys) |> assign(unselected_functions_keys: unselected_functions_keys) |> assign(unselected_match_spec_keys: unselected_match_spec_keys) |> assign(match_spec_info: match_spec_info) |> assign(trace_idle?: trace_idle?) |> assign(trace_idle?: trace_idle?) |> assign(trace_owner?: trace_owner?) |> assign(show_tracing_options: show_tracing_options) |> assign(attention_msg: attention_msg) ~H"""
<:inner_form> <.form for={@form} id="tracing-update-form" class="flex ml-2 mr-2 text-xs text-center text-zinc-800 dark:text-white whitespace-nowrap gap-5" phx-change="form-update" > <:inner_button>
<:col :let={{_id, tracing_message}} label="SERVICE"> {tracing_message.service} <:col :let={{_id, tracing_message}} label="INDEX"> {tracing_message.index} <:col :let={{_id, tracing_message}} label="TYPE"> {tracing_message.type} <:col :let={{id, tracing_message}} label="CONTENT">
{tracing_message.content}
""" end @impl Page def handle_mount(socket) when is_connected?(socket) do # 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(:trace_session_id, nil) |> assign(:show_tracing_options, false) |> assign(:form, to_form(default_form_options())) |> assign(:form_search, to_form(default_form_search_options())) |> stream(:tracing_messages, []) end def handle_mount(socket) do socket |> assign(:node_info, node_info_new()) |> assign(:node_data, %{}) |> assign(:trace_session_id, nil) |> assign(:show_tracing_options, false) |> assign(form: to_form(default_form_options())) |> assign(form_search: to_form(default_form_search_options())) |> stream(:tracing_messages, []) end @impl Page def handle_params(params, _uri, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end defp apply_action(socket, :index, _params) do socket |> assign(:page_title, "Live Tracing") end @impl Page def handle_parent_event("toggle-options", _value, socket) do show_tracing_options = !socket.assigns.show_tracing_options {:noreply, socket |> assign(:show_tracing_options, show_tracing_options)} end def handle_parent_event( "form-update", %{"max_messages" => max_messages, "session_timeout_seconds" => session_timeout_seconds}, socket ) do {:noreply, assign(socket, form: to_form(%{ "max_messages" => max_messages, "session_timeout_seconds" => session_timeout_seconds }) )} end def handle_parent_event( "form-multi-select-list-update-search", %{"_target" => target} = values, socket ) do new_params = Enum.reduce(target, %{}, fn key, acc -> Map.put(acc, key, values[key]) end) form_search = socket.assigns.form_search.params |> Map.merge(new_params) |> to_form() {:noreply, assign(socket, form_search: form_search)} end def handle_parent_event( "tracing-apps-stop", _data, %{assigns: %{trace_session_id: trace_session_id}} = socket ) do Tracer.stop_trace(trace_session_id) {:noreply, assign(socket, :trace_session_id, nil)} end def handle_parent_event( "tracing-apps-run", _data, %{assigns: %{node_info: node_info, form: form}} = socket ) do tracer_state = Tracer.state() if tracer_state.status == :idle do functions_to_monitor = Enum.reduce(node_info.selected_services_keys, [], fn service_key, service_acc -> service_info = Enum.find(node_info.node, &(&1.service == service_key)) service_acc ++ Enum.reduce(node_info.selected_modules_keys, [], fn module_key, module_acc -> module_key_atom = String.to_existing_atom(module_key) node_functions_info = Enum.find(service_info.functions, &(&1.module == module_key_atom)) functions = Enum.reduce(node_info.selected_functions_keys, [], fn function_key, function_acc -> function = Map.get(node_functions_info.functions, function_key, nil) # credo:disable-for-lines:14 if module_key in service_info.modules_keys and function do function_acc ++ [ %{ node: String.to_existing_atom(service_key), module: module_key_atom, function: function.name, arity: function.arity, match_spec: node_info.selected_match_spec_keys } ] else function_acc end end) # If the module doesn't have any of the requested functions the default is to # include the whole module if functions == [] do module_acc ++ [ %{ node: String.to_existing_atom(service_key), module: module_key_atom, function: :_, arity: :_, match_spec: node_info.selected_match_spec_keys } ] else module_acc ++ functions end end) end) case Tracer.start_trace(functions_to_monitor, %{ max_messages: String.to_integer(form.params["max_messages"]), session_timeout_ms: String.to_integer(form.params["session_timeout_seconds"]) * 1_000 }) do {:ok, %{session_id: session_id}} -> {:noreply, socket |> assign(:trace_session_id, session_id) |> stream(:tracing_messages, [], reset: true)} # coveralls-ignore-start {:error, _} -> {:noreply, assign(socket, :trace_session_id, nil)} # coveralls-ignore-stop end else {:noreply, assign(socket, :trace_session_id, nil)} end 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_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-remove-item", %{"item" => "modules", "key" => module_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys -- [module_key], node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-remove-item", %{"item" => "functions", "key" => function_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys, node_info.selected_functions_keys -- [function_key], node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-remove-item", %{"item" => "match_spec", "key" => match_spec_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys -- [match_spec_key] ) {: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}} = socket ) do node_info = update_node_info( node_info.selected_services_keys ++ [service_key], node_info.selected_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-add-item", %{"item" => "modules", "key" => module_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys ++ [module_key], node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-add-item", %{"item" => "functions", "key" => function_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys, node_info.selected_functions_keys ++ [function_key], node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_parent_event( "multi-select-add-item", %{"item" => "match_spec", "key" => match_spec_key}, %{assigns: %{node_info: node_info}} = socket ) do node_info = update_node_info( node_info.selected_services_keys, node_info.selected_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys ++ [match_spec_key] ) {:noreply, assign(socket, :node_info, node_info)} end @impl Page 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_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_info({:nodedown, node}, %{assigns: %{node_info: node_info}} = socket) do service_key = node |> to_string node_info = update_node_info( node_info.selected_services_keys -- [service_key], node_info.selected_modules_keys, node_info.selected_functions_keys, node_info.selected_match_spec_keys ) {:noreply, assign(socket, :node_info, node_info)} end def handle_info({:new_trace_message, _session_id, node, index, type, message}, socket) do data = %{ service: node, id: ObserverWeb.Common.uuid4(), index: index, type: type, content: message } {:noreply, stream(socket, :tracing_messages, [data])} end def handle_info({event, _session_id}, socket) when event in [:trace_session_timeout, :stop_tracing] do {:noreply, socket |> assign(:trace_session_id, nil) |> assign(:show_tracing_options, false)} end defp default_form_options do %{ "max_messages" => "3", "session_timeout_seconds" => "30" } end defp default_form_search_options, do: %{"modules" => "", "functions" => ""} defp node_info_new do match_spec_keys = Tracer.get_default_functions_matchspecs() |> Map.keys() |> Enum.sort() %{ services_keys: [], modules_keys: [], functions_keys: [], match_spec_keys: match_spec_keys, selected_services_keys: [], selected_modules_keys: [], selected_functions_keys: [], selected_match_spec_keys: [], node: [] } end defp update_node_info, do: update_node_info([], [], [], []) defp update_node_info( selected_services_keys, selected_modules_keys, selected_functions_keys, selected_match_spec_keys ) do initial_map = %{ node_info_new() | selected_services_keys: selected_services_keys, selected_modules_keys: selected_modules_keys, selected_functions_keys: selected_functions_keys, selected_match_spec_keys: selected_match_spec_keys } Enum.reduce(Node.list() ++ [Node.self()], initial_map, fn instance_node, %{ services_keys: services_keys, modules_keys: modules_keys, functions_keys: functions_keys, match_spec_keys: match_spec_keys, node: node } = acc -> service = instance_node |> to_string service_selected? = service in selected_services_keys [name, _hostname] = String.split(service, "@") services_keys = (services_keys ++ [service]) |> Enum.sort() instance_module_keys = if service_selected? do Tracer.get_modules(instance_node) |> Enum.map(&to_string/1) else [] end {instance_functions_keys, functions} = Enum.reduce(instance_module_keys, {[], []}, fn module, {keys, fun} -> # credo:disable-for-lines:6 if module in selected_modules_keys do module_functions_info = Tracer.get_module_functions_info(instance_node, String.to_existing_atom(module)) function_keys = Map.keys(module_functions_info.functions) |> Enum.map(&to_string/1) {keys ++ function_keys, fun ++ [module_functions_info]} else {keys, fun} end end) modules_keys = (modules_keys ++ instance_module_keys) |> Enum.sort() |> Enum.uniq() functions_keys = (functions_keys ++ instance_functions_keys) |> Enum.sort() |> Enum.uniq() node = if service_selected? do [ %{ name: name, modules_keys: instance_module_keys, function_keys: instance_functions_keys, match_spec_keys: match_spec_keys, service: service, functions: functions } | node ] else node end %{ acc | services_keys: services_keys, modules_keys: modules_keys, functions_keys: functions_keys, match_spec_keys: match_spec_keys, node: node } end) end end