diff --git a/assets/js/app.js b/assets/js/app.js
index ebb0068..64d71eb 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -18,21 +18,65 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
-import {Socket, LongPoll} from "phoenix"
-import {LiveSocket} from "phoenix_live_view"
-import topbar from "../vendor/topbar"
+import { Socket, LongPoll } from "phoenix"
+import { LiveSocket } from "phoenix_live_view"
+import topbar from "topbar"
+import * as echarts from "echarts"
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveTran = document.querySelector("meta[name='live-transport']").getAttribute("content")
const livePath = document.querySelector("meta[name='live-path']").getAttribute("content")
+let hooks = {}
+
+hooks.ObserverEChart = {
+ mounted() {
+ selector = "#" + this.el.id
+
+ this.chart = echarts.init(this.el.querySelector(selector + "-chart"))
+ option = JSON.parse(this.el.querySelector(selector + "-data").textContent)
+
+ this.chart.setOption(option)
+ },
+ updated() {
+ selector = "#" + this.el.id
+ // This flag will indicate to Echart to not merge the data
+ let notMerge = !this.el.dataset.merge ?? true;
+
+ newOption = JSON.parse(this.el.querySelector(selector + "-data").textContent)
+
+ // Compare the new option series with the previous one
+ if (this.previousSeries && JSON.stringify(this.previousSeries) === JSON.stringify(newOption.series)) {
+ // If the data is the same, skip the update
+ console.log('No changes in the data, skipping setOption');
+ return; // Exit without updating the chart
+ }
+
+ // Save the new option as the previous one for future comparisons
+ this.previousSeries = newOption.series;
+
+ // Set the callback in the tooltip formatter (or any other part of the option)
+ var callback = (args) => {
+ this.pushEventTo(this.el, "request-process", { id: args.data.id, series_name: args.seriesName });
+ return args.data.id;
+ }
+
+ newOption.tooltip = {
+ formatter: callback
+ };
+
+ this.chart.setOption(newOption, notMerge)
+ }
+}
+
const liveSocket = new LiveSocket(livePath, Socket, {
transport: liveTran === "longpoll" ? LongPoll : WebSocket,
- params: { _csrf_token: csrfToken }
+ params: { _csrf_token: csrfToken },
+ hooks
})
// Show progress bar on live navigation and form submits
-topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
diff --git a/assets/package-lock.json b/assets/package-lock.json
index ad63067..1ad1666 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -6,6 +6,7 @@
"": {
"license": "MIT",
"dependencies": {
+ "echarts": "^5.5.1",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
@@ -44,6 +45,15 @@
"phoenix": "1.7.18"
}
},
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
"node_modules/phoenix": {
"resolved": "../deps/phoenix",
"link": true
@@ -60,9 +70,31 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/topbar/-/topbar-1.0.1.tgz",
"integrity": "sha512-HZqQSMBiG29vcjOrqKCM9iGY/h69G5gQH7ae83ZCPz5uPmbQKwK0sMEqzVDBiu64tWHJ+kk9NApECrF+FAAvRA=="
+ },
+ "node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+ },
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
}
},
"dependencies": {
+ "echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "requires": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
"phoenix": {
"version": "file:../deps/phoenix"
},
@@ -95,6 +127,19 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/topbar/-/topbar-1.0.1.tgz",
"integrity": "sha512-HZqQSMBiG29vcjOrqKCM9iGY/h69G5gQH7ae83ZCPz5uPmbQKwK0sMEqzVDBiu64tWHJ+kk9NApECrF+FAAvRA=="
+ },
+ "tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+ },
+ "zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "requires": {
+ "tslib": "2.3.0"
+ }
}
}
}
diff --git a/assets/package.json b/assets/package.json
index 736dc84..4376c1a 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -6,6 +6,7 @@
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
- "topbar": "^1.0.1"
+ "topbar": "^1.0.1",
+ "echarts": "^5.5.1"
}
}
diff --git a/guides/overview.md b/guides/overview.md
index ff01fa0..d9c7249 100644
--- a/guides/overview.md
+++ b/guides/overview.md
@@ -18,5 +18,5 @@ and also function callers, as many other possibilities.
## Installation
-See the [installation guide](installation.md) for details on installing and configuring Oban Web
+See the [installation guide](installation.md) for details on installing and configuring Observer Web
for your application.
diff --git a/lib/observer_web/common.ex b/lib/observer_web/common.ex
index 6bbcb26..40b8966 100644
--- a/lib/observer_web/common.ex
+++ b/lib/observer_web/common.ex
@@ -14,7 +14,7 @@ defmodule ObserverWeb.Common do
## Examples
- iex> alias Deployex.Common
+ iex> alias ObserverWeb.Common
...> assert is_binary(Common.uuid4())
...> assert is_binary(Common.uuid4())
...> assert is_binary(Common.uuid4())
diff --git a/lib/observer_web/observer.ex b/lib/observer_web/observer.ex
new file mode 100644
index 0000000..26d6022
--- /dev/null
+++ b/lib/observer_web/observer.ex
@@ -0,0 +1,282 @@
+defmodule ObserverWeb.Observer do
+ @moduledoc """
+ This module will provide observability functions
+
+ ## References:
+ * https://github.com/shinyscorpion/wobserver
+ """
+
+ require Logger
+
+ alias ObserverWeb.Observer.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 """
+ Retreives 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
+
+ # coveralls-ignore-stop
+
+ 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
+
+ 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
+
+ defp structure_links(port) when is_port(port) do
+ new(%{
+ id: port,
+ symbol: @port_symbol,
+ itemStyle: %{color: @port_item_color}
+ })
+ end
+
+ 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)}"
+ Logger.warning("Entity ID not mapped: #{name}")
+
+ 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
+
+ defp name(port) when is_port(port), do: port |> inspect |> String.trim_leading("#Port")
+ # coveralls-ignore-start
+ defp name(reference) when is_reference(reference),
+ do: reference |> inspect |> String.trim_leading("#Reference")
+
+ # coveralls-ignore-stop
+end
diff --git a/lib/observer_web/observer/helper.ex b/lib/observer_web/observer/helper.ex
new file mode 100644
index 0000000..c053f84
--- /dev/null
+++ b/lib/observer_web/observer/helper.ex
@@ -0,0 +1,88 @@
+defmodule ObserverWeb.Observer.Helper do
+ @moduledoc """
+ Helper functions and JSON encoders.
+
+ ## References:
+ * https://github.com/shinyscorpion/wobserver
+ """
+
+ alias Jason.Encoder
+
+ defimpl Encoder, for: PID do
+ @doc """
+ JSON encodes a `PID`.
+
+ Uses `inspect/1` to turn the `pid` into a String and passes the `options` to `Encoder.BitString.encode/1`.
+ """
+ @spec encode(pid :: pid, options :: Jason.Encode.opts()) :: any()
+ def encode(pid, options) do
+ pid
+ |> inspect
+ |> Encoder.BitString.encode(options)
+ end
+ end
+
+ defimpl Encoder, for: Port do
+ @doc """
+ JSON encodes a `Port`.
+
+ Uses `inspect/1` to turn the `port` into a String and passes the `options` to `Encoder.BitString.encode/1`.
+ """
+ @spec encode(port :: port, options :: Jason.Encode.opts()) :: any()
+ def encode(port, options) do
+ port
+ |> inspect
+ |> Encoder.BitString.encode(options)
+ end
+ end
+
+ defimpl Encoder, for: Reference do
+ @doc """
+ JSON encodes a `Reference`.
+
+ Uses `inspect/1` to turn the `reference` into a String and passes the `options` to `Encoder.BitString.encode/1`.
+ """
+ @spec encode(reference :: reference, options :: Jason.Encode.opts()) :: any()
+ def encode(reference, options) do
+ reference
+ |> inspect
+ |> Encoder.BitString.encode(options)
+ end
+ end
+
+ @doc """
+ Formats function information as readable string.
+
+ Only name will be return if only `name` is given.
+
+ Example:
+ ```bash
+ iex> format_function {Logger, :log, 2}
+ "Logger.log/2"
+ ```
+ ```bash
+ iex> format_function :format_function
+ "format_function"
+ ```
+ ```bash
+ iex> format_function nil
+ nil
+ ```
+ """
+ @spec format_function(nil | {atom, atom, integer} | atom) :: String.t() | nil
+ def format_function(nil), do: nil
+ def format_function({module, name, arity}), do: "#{module}.#{name}/#{arity}"
+ def format_function(name), do: "#{name}"
+
+ @doc """
+ Parallel map implemented with `Task`.
+
+ Maps the `function` over the `enum` using `Task.async/1` and `Task.await/1`.
+ """
+ @spec parallel_map(enum :: list, function :: fun) :: list
+ def parallel_map(enum, function) do
+ enum
+ |> Enum.map(&Task.async(fn -> function.(&1) end))
+ |> Enum.map(&Task.await/1)
+ end
+end
diff --git a/lib/observer_web/observer/port.ex b/lib/observer_web/observer/port.ex
new file mode 100644
index 0000000..1938a72
--- /dev/null
+++ b/lib/observer_web/observer/port.ex
@@ -0,0 +1,39 @@
+defmodule ObserverWeb.Observer.Port do
+ @moduledoc """
+ Retrieve Port information
+ """
+
+ alias ObserverWeb.Rpc
+
+ @doc """
+ Return port information
+
+ ## Examples
+
+ iex> alias ObserverWeb.Observer.Port
+ ...> [h | _] = :erlang.ports()
+ ...> assert %{connected: _, id: _, name: _, os_pid: _} = Port.info(h)
+ ...> assert :undefined = Port.info(nil)
+ ...> assert :undefined = Port.info("")
+ """
+ @spec info(atom(), port()) ::
+ :undefined | %{connected: any(), id: any(), name: any(), os_pid: any()}
+ def info(node \\ Node.self(), port)
+
+ def info(node, port) when is_port(port) do
+ case Rpc.call(node, :erlang, :port_info, [port], :infinity) do
+ data when is_list(data) ->
+ %{
+ name: Keyword.get(data, :name, 0),
+ id: Keyword.get(data, :id, 0),
+ connected: Keyword.get(data, :connected, 0),
+ os_pid: Keyword.get(data, :os_pid, 0)
+ }
+
+ _ ->
+ :undefined
+ end
+ end
+
+ def info(_node, _port), do: :undefined
+end
diff --git a/lib/observer_web/observer/process.ex b/lib/observer_web/observer/process.ex
new file mode 100644
index 0000000..a2bf889
--- /dev/null
+++ b/lib/observer_web/observer/process.ex
@@ -0,0 +1,133 @@
+defmodule ObserverWeb.Observer.Process do
+ @moduledoc """
+ Retrieve process links and information
+
+ ## References:
+ * https://github.com/shinyscorpion/wobserver
+ """
+
+ alias ObserverWeb.Observer.Helper
+ alias ObserverWeb.Rpc
+
+ @process_full [
+ :registered_name,
+ :priority,
+ :trap_exit,
+ :initial_call,
+ :current_function,
+ :message_queue_len,
+ :error_handler,
+ :group_leader,
+ :links,
+ :memory,
+ :total_heap_size,
+ :heap_size,
+ :stack_size,
+ :min_heap_size,
+ :garbage_collection,
+ :status,
+ :dictionary,
+ :monitored_by,
+ :monitors
+ ]
+
+ @doc """
+ Creates a complete overview of process stats based on the given `pid`.
+ """
+ @spec info(pid :: pid()) :: :undefined | map
+ def info(pid) do
+ process_info(pid, @process_full, &structure_full/2)
+ end
+
+ ### ==========================================================================
+ ### Private functions
+ ### ==========================================================================
+ defp process_info(pid, information, structurer) do
+ case Rpc.pinfo(pid, information) do
+ :undefined -> :undefined
+ data -> structurer.(data, pid)
+ end
+ end
+
+ defp process_status_module(pid) do
+ {:status, ^pid, {:module, class}, _} = :sys.get_status(pid, 100)
+ class
+ catch
+ # coveralls-ignore-start
+ _, _ ->
+ :unknown
+ # coveralls-ignore-stop
+ end
+
+ defp state(pid) do
+ :sys.get_state(pid, 100)
+ catch
+ _, _ -> :unknown
+ end
+
+ defp initial_call(data) do
+ dictionary_init =
+ data
+ |> Keyword.get(:dictionary, [])
+ |> Keyword.get(:"$initial_call", nil)
+
+ case dictionary_init do
+ nil ->
+ Keyword.get(data, :initial_call, nil)
+
+ call ->
+ call
+ end
+ end
+
+ # Structurers
+
+ defp structure_full(data, pid) do
+ gc = Keyword.get(data, :garbage_collection, [])
+ dictionary = Keyword.get(data, :dictionary)
+
+ %{
+ pid: pid,
+ registered_name: Keyword.get(data, :registered_name, nil),
+ priority: Keyword.get(data, :priority, :normal),
+ trap_exit: Keyword.get(data, :trap_exit, false),
+ message_queue_len: Keyword.get(data, :message_queue_len, 0),
+ error_handler: Keyword.get(data, :error_handler, :none),
+ relations: %{
+ group_leader: Keyword.get(data, :group_leader, nil),
+ ancestors: Keyword.get(dictionary, :"$ancestors", []),
+ links: Keyword.get(data, :links, nil),
+ monitored_by: Keyword.get(data, :monitored_by, nil),
+ monitors: Keyword.get(data, :monitors, nil)
+ },
+ memory: %{
+ total: Keyword.get(data, :memory, 0),
+ stack_and_heap: Keyword.get(data, :total_heap_size, 0),
+ heap_size: Keyword.get(data, :heap_size, 0),
+ stack_size: Keyword.get(data, :stack_size, 0),
+ gc_min_heap_size: Keyword.get(gc, :min_heap_size, 0),
+ gc_full_sweep_after: Keyword.get(gc, :fullsweep_after, 0)
+ },
+ meta: structure_meta(data, pid),
+ state: to_string(:io_lib.format("~tp", [state(pid)]))
+ }
+ end
+
+ defp structure_meta(data, pid) do
+ init = initial_call(data)
+
+ class =
+ case init do
+ {:supervisor, _, _} -> :supervisor
+ {:application_master, _, _} -> :application
+ _ -> process_status_module(pid)
+ end
+
+ %{
+ init: Helper.format_function(init),
+ current: Helper.format_function(Keyword.get(data, :current_function)),
+ status: Keyword.get(data, :status),
+ class: class
+ }
+ end
+end
diff --git a/lib/observer_web/rpc.ex b/lib/observer_web/rpc.ex
index 3ca0034..5ff91f7 100644
--- a/lib/observer_web/rpc.ex
+++ b/lib/observer_web/rpc.ex
@@ -24,6 +24,18 @@ defmodule ObserverWeb.Rpc do
def call(node, module, function, args, timeout),
do: default().call(node, module, function, args, timeout)
+ @doc """
+ Call :rpc.pinfo to request process information (local or remote pid)
+
+ Location transparent version of the BIF :erlang.process_info/2
+
+ https://www.erlang.org/doc/apps/kernel/rpc.html#pinfo/1
+ """
+ @impl true
+ @spec pinfo(pid :: pid, information :: list | atom()) :: any() | :undefined
+ def pinfo(pid, information),
+ do: default().pinfo(pid, information)
+
### ==========================================================================
### Private functions
### ==========================================================================
diff --git a/lib/observer_web/rpc/adapter.ex b/lib/observer_web/rpc/adapter.ex
index 7c5aada..1f24a47 100644
--- a/lib/observer_web/rpc/adapter.ex
+++ b/lib/observer_web/rpc/adapter.ex
@@ -10,4 +10,6 @@ defmodule ObserverWeb.Rpc.Adapter do
args :: list,
timeout :: 0..4_294_967_295 | :infinity
) :: any() | {:badrpc, any()}
+
+ @callback pinfo(pid :: pid, information :: list | atom()) :: any() | :undefined
end
diff --git a/lib/observer_web/rpc/local.ex b/lib/observer_web/rpc/local.ex
index 27c2341..ecba854 100644
--- a/lib/observer_web/rpc/local.ex
+++ b/lib/observer_web/rpc/local.ex
@@ -12,4 +12,9 @@ defmodule ObserverWeb.Rpc.Local do
def call(node, module, function, args, timeout) do
:rpc.call(node, module, function, args, timeout)
end
+
+ @impl true
+ def pinfo(pid, information) do
+ :rpc.pinfo(pid, information)
+ end
end
diff --git a/lib/observer_web/tracer/server.ex b/lib/observer_web/tracer/server.ex
index c2e1a68..f7b6eec 100644
--- a/lib/observer_web/tracer/server.ex
+++ b/lib/observer_web/tracer/server.ex
@@ -74,7 +74,7 @@ defmodule ObserverWeb.Server do
Logger.info("New Trace Session: #{session_id} functions: #{inspect(functions_by_node)}")
tracer_pid = self()
- # The local node (deployex) is always present in the trace list of nodes.
+ # The local node is always present in the trace list of nodes.
# The following list will indicate to the trace handler whether the node
# should be included or filtered out.
monitored_nodes = Map.keys(functions_by_node)
diff --git a/lib/web/components/attention.ex b/lib/web/components/attention.ex
new file mode 100644
index 0000000..e73f888
--- /dev/null
+++ b/lib/web/components/attention.ex
@@ -0,0 +1,44 @@
+defmodule Observer.Web.Components.Attention do
+ @moduledoc false
+ use Phoenix.Component
+
+ attr :id, :string, required: true
+ attr :message, :string, required: true
+ attr :title, :string, required: true
+ attr :class, :string, default: ""
+ slot :inner_form, doc: "the slot for adding form to input data"
+ slot :inner_button, doc: "the slot for adding form to input data"
+
+ def content(assigns) do
+ ~H"""
+
+
+
+
+
+ Info
+
{@title}
+
+
+ {@message}
+
+ {render_slot(@inner_form)}
+
+
+ {render_slot(@inner_button)}
+
+ """
+ end
+end
diff --git a/lib/web/components/core.ex b/lib/web/components/core.ex
index 487895e..f298adf 100644
--- a/lib/web/components/core.ex
+++ b/lib/web/components/core.ex
@@ -24,14 +24,12 @@ defmodule Observer.Web.Components.Core do
attr :row_item, :any,
default: &Function.identity/1,
- doc: "the function for mapping each row before calling the :col and :action slots"
+ doc: "the function for mapping each row before calling the :col slots"
slot :col, required: true do
attr :label, :string
end
- slot :action, doc: "the slot for showing user actions in the last table column"
-
def table_tracing(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
@@ -90,6 +88,82 @@ defmodule Observer.Web.Components.Core do
"""
end
+ @doc ~S"""
+ Renders a table with process styling.
+
+ ## Examples
+
+ <.table id="users" rows={@users}>
+ <:col :let={user} label="id"><%= user.id %>
+ <:col :let={user} label="username"><%= user.username %>
+
+ """
+ attr :id, :string, required: true
+ attr :title, :string, required: true
+ attr :rows, :list, required: true
+ attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+ attr :transition, :boolean, default: false
+
+ attr :row_item, :any,
+ default: &Function.identity/1,
+ doc: "the function for mapping each row before calling the :col slots"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ def table_process(assigns) do
+ assigns =
+ with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
+ assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
+ end
+
+ ~H"""
+
+
+
+
+ {@title}
+
+
+
+
+
+
+
+ {render_slot(col, @row_item.(row))}
+
+
+
+
+
+
+
+
+ """
+ end
+
@doc """
Renders an input with label and error messages.
diff --git a/lib/web/components/layouts.ex b/lib/web/components/layouts.ex
index af73039..c24a973 100644
--- a/lib/web/components/layouts.ex
+++ b/lib/web/components/layouts.ex
@@ -73,7 +73,15 @@ defmodule Observer.Web.Layouts do
"""
end
diff --git a/lib/web/components/layouts/live.html.heex b/lib/web/components/layouts/live.html.heex
index c3c57a4..604fc48 100644
--- a/lib/web/components/layouts/live.html.heex
+++ b/lib/web/components/layouts/live.html.heex
@@ -9,5 +9,6 @@
<.nav socket={@socket} page={@page.name} />
- {@inner_content} <.footer />
+ {@inner_content}
+ <.footer />
diff --git a/lib/web/components/multi_select.ex b/lib/web/components/multi_select.ex
new file mode 100644
index 0000000..5069da5
--- /dev/null
+++ b/lib/web/components/multi_select.ex
@@ -0,0 +1,195 @@
+defmodule Observer.Web.Components.MultiSelect do
+ @moduledoc """
+ Multi select box
+
+ References:
+ * https://www.creative-tim.com/twcomponents/component/multi-select
+
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+
+ attr :id, :string, required: true
+ attr :selected_text, :string, required: true
+ attr :selected, :list, required: true
+ attr :unselected, :list, required: true
+ attr :show_options, :boolean, required: true
+
+ def content(assigns) do
+ ~H"""
+
+
+
+
+
+
+
+ {@selected_text}
+
+
+ <%= for item <- @selected do %>
+ <%= for key <- item.keys do %>
+
+ """
+ end
+
+ defp relations(assigns) do
+ ~H"""
+
+
+ {@title}
+
+
+
+ {@value}
+
+
+
+ """
+ end
+end
diff --git a/lib/web/pages/observer_page.ex b/lib/web/pages/observer_page.ex
deleted file mode 100644
index 9a8ef44..0000000
--- a/lib/web/pages/observer_page.ex
+++ /dev/null
@@ -1,81 +0,0 @@
-defmodule Observer.Web.ObserverPage do
- @moduledoc """
- This is the live component responsible for handling the Observer page
- """
-
- @behaviour Observer.Web.Page
-
- use Observer.Web, :live_component
-
- alias Observer.Web.Page
-
- @impl Phoenix.LiveComponent
- def render(assigns) do
- ~H"""
-
-
-
-
-
-
- Info
-
Attention
-
-
- Incorrect use of the :dbg
- tracer in production can lead to performance degradation, latency and crashes.
- DeployEx Live observer
- 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.
-
-
-
-
-
- """
- end
-
- @impl Page
- def handle_mount(socket) do
- socket
- 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 Observer")
- end
-
- @impl Page
- def handle_parent_event(_message, _value, socket) do
- {:noreply, socket}
- end
-
- @impl Page
- def handle_info(_message, socket) do
- {:noreply, socket}
- end
-end
diff --git a/lib/web/pages/tracing_page.ex b/lib/web/pages/tracing/page.ex
similarity index 82%
rename from lib/web/pages/tracing_page.ex
rename to lib/web/pages/tracing/page.ex
index 4ab75fa..4b02148 100644
--- a/lib/web/pages/tracing_page.ex
+++ b/lib/web/pages/tracing/page.ex
@@ -1,4 +1,4 @@
-defmodule Observer.Web.TracingPage do
+defmodule Observer.Web.Tracing.Page do
@moduledoc """
This is the live component responsible for handling the Tracing debug
"""
@@ -7,6 +7,7 @@ defmodule Observer.Web.TracingPage do
use Observer.Web, :live_component
+ alias Observer.Web.Components.Attention
alias Observer.Web.Components.Core
alias Observer.Web.Components.MultiSelectList
alias Observer.Web.Page
@@ -43,6 +44,19 @@ defmodule Observer.Web.TracingPage do
"""
+ 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)
@@ -54,94 +68,68 @@ defmodule Observer.Web.TracingPage do
|> assign(trace_idle?: trace_idle?)
|> assign(trace_owner?: trace_owner?)
|> assign(show_tracing_options: show_tracing_options)
+ |> assign(attention_msg: attention_msg)
~H"""
-
-
-
-
-
-
- Info
-
Attention
-
-
- Incorrect use of the :dbg
- tracer in production can lead to performance degradation, latency and crashes.
- DeployEx Live 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.
-