defmodule ObserverWeb.Apps do @moduledoc """ This module will provide observability for the applications processes and ports ## References: * https://github.com/shinyscorpion/wobserver """ alias ObserverWeb.Apps.Helper alias ObserverWeb.Rpc @link_line_color "#CCC" @monitor_line_color "#D1A1E5" @monitored_by_line_color "#4DB8FF" @process_symbol "emptycircle" @process_item_color "#93C5FD" @app_process_symbol "emptydiamond" @app_process_item_color "#A1887F" @supervisor_symbol "emptyroundRect" @supervisor_item_color "#F87171" @port_symbol "emptytriangle" @port_item_color "#FBBF24" @reference_symbol "emptyrect" @reference_item_color "#28A745" @type t :: %__MODULE__{ id: pid() | port() | reference() | nil, children: list(), name: String.t(), symbol: String.t(), lineStyle: map(), itemStyle: map() } @derive Jason.Encoder defstruct id: nil, children: [], name: "", symbol: @process_symbol, lineStyle: %{color: @link_line_color}, itemStyle: %{color: @process_item_color} @doc """ Lists all running applications. """ @spec list(node :: atom()) :: list({atom, String.t(), String.t()}) def list(node \\ Node.self()) do Rpc.call(node, :application_controller, :which_applications, [], :infinity) |> Enum.filter(&alive?(node, &1)) |> Enum.map(&structure_application/1) end @doc """ Retrieves information about the application and its respective linked processes, ports and references. """ @spec info(node :: atom(), app :: atom) :: map def info(node \\ Node.self(), app \\ :kernel) do app_pid = Rpc.call(node, :application_controller, :get_master, [app], :infinity) children = node |> Rpc.call(:application_master, :get_child, [app_pid], :infinity) |> structure_id(app_pid) new(%{ id: app_pid, children: children, symbol: @app_process_symbol, itemStyle: %{color: @app_process_item_color} }) end ### ========================================================================== ### Private functions ### ========================================================================== defp alive?(node, {app, _, _}) do node |> Rpc.call(:application_controller, :get_master, [app], :infinity) |> is_pid catch # coveralls-ignore-start _, _ -> false # coveralls-ignore-stop end defp structure_application({name, description, version}) do %{ name: name, description: to_string(description), version: to_string(version) } end defp structure_id({pid, name}, parent) do {_, dictionary} = Rpc.pinfo(pid, :dictionary) case Keyword.get(dictionary, :"$ancestors") do [ancestor_parent] -> child = structure_id({name, pid, :supervisor, []}, ancestor_parent) [ new(%{ id: ancestor_parent, children: [child], symbol: @app_process_symbol, itemStyle: %{color: @app_process_item_color} }) ] _ -> # coveralls-ignore-start child = structure_id({name, pid, :supervisor, []}, parent) [child] # coveralls-ignore-stop end end defp structure_id({_, :undefined, _, _}, _parent), do: nil defp structure_id({_, pid, :supervisor, _}, parent) do {:links, links} = Rpc.pinfo(pid, :links) links = links -- [parent] children = pid |> :supervisor.which_children() |> Kernel.++(Enum.filter(links, fn link -> is_port(link) end)) |> Helper.parallel_map(&structure_id(&1, pid)) |> Enum.filter(&(&1 != nil)) new(%{ id: pid, children: children, symbol: @supervisor_symbol, itemStyle: %{color: @supervisor_item_color} }) end defp structure_id({_, pid, :worker, _}, parent) do {:links, links} = Rpc.pinfo(pid, :links) {:monitored_by, monitored_by_pids} = Rpc.pinfo(pid, :monitored_by) {:monitors, monitors} = Rpc.pinfo(pid, :monitors) links = links -- [parent] children = Enum.map(links, &structure_links(&1)) monitored_by_pids = Enum.map(monitored_by_pids, &monitored_by(&1)) monitors = Enum.map(monitors, &monitor(&1)) new(%{ id: pid, children: children ++ monitored_by_pids ++ monitors }) end # coveralls-ignore-start defp structure_id(id, _parent) when is_port(id) do new(%{id: id, symbol: @port_symbol}) end defp structure_id(id, _parent) when is_reference(id) do new(%{id: id, symbol: @reference_symbol}) end defp structure_id(id, _parent) do new(%{id: id}) end # coveralls-ignore-stop # Check https://www.erlang.org/docs/26/man/erlang#process_info-2 # coveralls-ignore-start defp monitored_by(reference) when is_reference(reference) do new(%{ id: reference, symbol: @reference_symbol, itemStyle: %{color: @reference_item_color}, lineStyle: %{color: @monitored_by_line_color} }) end defp monitored_by(port) when is_port(port) do new(%{ id: port, symbol: @port_symbol, itemStyle: %{color: @port_item_color}, lineStyle: %{color: @monitored_by_line_color} }) end # coveralls-ignore-stop defp monitored_by(pid) when is_pid(pid) do new(%{ id: pid, lineStyle: %{color: @monitored_by_line_color} }) end # Check https://www.erlang.org/docs/26/man/erlang#process_info-2 # coveralls-ignore-start defp monitor({:port, port}) do new(%{ id: port, lineStyle: %{color: @monitor_line_color} }) end # coveralls-ignore-stop defp monitor({:process, pid}) do new(%{ id: pid, lineStyle: %{color: @monitor_line_color} }) end # coveralls-ignore-start defp structure_links(port) when is_port(port) do new(%{ id: port, symbol: @port_symbol, itemStyle: %{color: @port_item_color} }) end # coveralls-ignore-stop defp structure_links(pid) when is_pid(pid) do new(%{id: pid}) end # coveralls-ignore-start defp structure_links(reference) when is_reference(reference) do new(%{id: reference}) end # coveralls-ignore-stop @spec new(map()) :: struct() def new(%{id: id} = attrs) when is_port(id) or is_pid(id) or is_reference(id) do name = name(id) struct(__MODULE__, Map.put(attrs, :name, name)) end # coveralls-ignore-start def new(%{id: id} = attrs) do name = "#{inspect(id)}" struct( __MODULE__, attrs |> Map.put(:name, name) |> Map.put(:id, nil) ) end # coveralls-ignore-stop defp name(pid) when is_pid(pid) do case Rpc.pinfo(pid, :registered_name) do {_, registered_name} -> to_string(registered_name) |> String.trim_leading("Elixir.") _ -> pid |> inspect |> String.trim_leading("#PID") end end # coveralls-ignore-start defp name(port) when is_port(port), do: port |> inspect |> String.trim_leading("#Port") defp name(reference) when is_reference(reference), do: reference |> inspect |> String.trim_leading("#Reference") # coveralls-ignore-stop end