Current section

Files

Jump to
observer_web lib web pages tracing selection.ex
Raw

lib/web/pages/tracing/selection.ex

defmodule Observer.Web.Tracing.Selection do
@moduledoc """
Shared node/module/function selection state, used by both the Tracing page and the Profiling
page - the two pages that let a user pick which functions to trace before running a session.
"""
alias ObserverWeb.Tracer
@type t :: %{
services_keys: [String.t()],
modules_keys: [String.t()],
functions_keys: [String.t()],
selected_services_keys: [String.t()],
selected_modules_keys: [String.t()],
selected_functions_keys: [String.t()],
node: list()
}
@doc """
Empty selection state. Does not call out to any node, so it's safe to use before the socket is
connected (the disconnected/dead render).
"""
@spec new :: t()
def new do
%{
services_keys: [],
modules_keys: [],
functions_keys: [],
selected_services_keys: [],
selected_modules_keys: [],
selected_functions_keys: [],
node: []
}
end
@doc """
Rebuilds the selection state for the current cluster membership, given the currently selected
keys. Only queries modules/functions for nodes/modules that are actually selected.
"""
@spec update([String.t()], [String.t()], [String.t()]) :: t()
def update(selected_services_keys, selected_modules_keys, selected_functions_keys) do
initial_map = %{
new()
| selected_services_keys: selected_services_keys,
selected_modules_keys: selected_modules_keys,
selected_functions_keys: selected_functions_keys
}
Enum.reduce(Node.list() ++ [Node.self()], initial_map, fn instance_node,
%{
services_keys: services_keys,
modules_keys: modules_keys,
functions_keys: functions_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,
service: service,
functions: functions
}
| node
]
else
node
end
%{
acc
| services_keys: services_keys,
modules_keys: modules_keys,
functions_keys: functions_keys,
node: node
}
end)
end
@doc """
Builds the `functions_by_node`-shaped list `ObserverWeb.Tracer.start_trace/2` expects, from the
current selection. `match_spec_keys` is attached to every entry (the Tracing page passes the
user-selected match specs; the Profiling page, which has no match-spec picker, passes `[]`).
"""
@spec build_functions_to_monitor(t(), [String.t()]) :: list()
def build_functions_to_monitor(node_info, match_spec_keys \\ []) do
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 ->
# node_functions_info is nil when the selected module doesn't exist on this
# service (heterogeneous clusters) - the modules_keys check below already filters
# those out, but the lookup must not crash before reaching it.
function =
node_functions_info && 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: 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: match_spec_keys
}
]
else
module_acc ++ functions
end
end)
end)
end
end