Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# CHANGELOG (v0.1.X)

## 0.1.9 🚀 ()

### Backwards incompatible changes for 0.1.8
* None

### Bug fixes
* None

### Enhancements
* [[`PR-16`](https://github.com/thiagoesteves/observer_web/pull/16)] Adding Beam VM statistics (ports, atoms and processes)

## 0.1.8 🚀 (2025-04-03)

### Backwards incompatible changes for 0.1.7
Expand Down
27 changes: 22 additions & 5 deletions guides/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ providing a more integrated monitoring experience.

### Metrics

#### Retention period for metrics
#### 1. Retention period for metrics

Observer Web can monitor Beam VM metrics (along with many others) and uses ETS
tables to store the data and there is a possibility of configuration for the retention
Expand All @@ -147,12 +147,12 @@ config :observer_web, ObserverWeb.Telemetry,
> storage, refer to the Configuration section to set up a Central Hub application,
> which can aggregate and retain metrics.

#### Configuration
#### 2. Configuration

Observer Web can operate in two distinct metrics configurations: `Standalone` and `Metric Hub`.
These configurations determine how metrics are collected, stored, and managed.

#### Standalone Configuration (default)
##### Standalone Configuration (default)

In this mode, all applications with Observer Web installed operate independently. Each
application receives and stores its own metrics within its ETS tables. The image below
Expand All @@ -162,7 +162,7 @@ illustrates this configuration:

__**NOTE: No additional configuration is required for this mode**__

#### Metric Hub Configuration
##### Metric Hub Configuration

In this mode, one application is designated as the central hub to store all metrics,
while the remaining applications broadcast their data to this designated hub. This
Expand Down Expand Up @@ -197,6 +197,22 @@ config :observer_web, ObserverWeb.Telemetry,
The application in `observer mode` will also retain its own metrics in addition to
aggregating metrics from other applications.

#### 3. Metrics Polling Interval

Observer Web allows configuration of two polling intervals:
* Phoenix Liveview sockets - Default: `5_000` ms
* Beam VM statistics - Default: `1_000` ms

```elixir
config :observer_web, ObserverWeb.Telemetry,
phx_lv_sckt_poller_interval_ms: 5_000,
beam_vm_poller_interval_ms: 1_000
```

> #### For applications running by [DeployEx][dye] {: .attention}
>
> When using DeployEx, the BEAM VM statistics polling is also used to monitor and, if necessary, restart the application. The polling interval directly affects how quickly these actions are performed. While ports, atoms, and processes are configured via Observer Web, the memory check interval (also used by [DeployEx][dye]) is configured separately—refer to the relevant [documentation][mtc] for details.

### Usage with Web and Clustering

The Observer Web provides observer ability for the local application as well as any other that is
Expand All @@ -216,4 +232,5 @@ via OTP distribution!
[ac]: Observer.Web.Resolver.html#c:resolve_access/1
[ba]: https://hexdocs.pm/basic_auth/readme.html
[oi]: installation.html
[dye]: https://github.com/thiagoesteves/deployex
[dye]: https://github.com/thiagoesteves/deployex
[mtc]: https://hexdocs.pm/telemetry_metrics/Telemetry.Metrics.html
37 changes: 37 additions & 0 deletions lib/observer_web/telemetry/producer/beam_vm.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule ObserverWeb.Telemetry.Producer.BeamVm do
@moduledoc """
This module contains the reporting functions for Beam VM
"""

### ==========================================================================
### Public APIs
### ==========================================================================
def process do
measurements = %{
total: :erlang.system_info(:port_count),
limit: :erlang.system_info(:port_limit)
}

:telemetry.execute([:vm, :port], measurements, %{})

measurements = %{
total: :erlang.system_info(:atom_count),
limit: :erlang.system_info(:atom_limit)
}

:telemetry.execute([:vm, :atom], measurements, %{})

measurements = %{
total: :erlang.system_info(:process_count),
limit: :erlang.system_info(:process_limit)
}

:telemetry.execute([:vm, :process], measurements, %{})

:ok
end

### ==========================================================================
### Private Functions
### ==========================================================================
end
2 changes: 1 addition & 1 deletion lib/observer_web/telemetry/producer/phx_lv_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule ObserverWeb.Telemetry.Producer.PhxLvSocket do
### Public functions
### ==========================================================================

def process_phoenix_liveview_sockets do
def process do
cached_phx_endpoints = Process.get(@cache_key, [])

# Check if the cache is populated, if not, verify if there is any
Expand Down
142 changes: 142 additions & 0 deletions lib/web/components/metrics/vm_limits.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
defmodule Observer.Web.Components.Metrics.VmLimits do
@moduledoc false
use Observer.Web, :html

use Phoenix.Component

alias Observer.Web.Components.Metrics.Common

attr :title, :string, required: true
attr :service, :string, required: true
attr :metric, :string, required: true
attr :metrics, :list, required: true
attr :cols, :integer, default: 2

attr :supported_metrics, :list,
default: [
"vm.atom.total",
"vm.process.total",
"vm.port.total"
]

def content(assigns) do
~H"""
<div :if={@metric in @supported_metrics} style={"grid-column: span #{@cols};"}>
<% id = String.replace("#{@service}-#{@metric}", ["@", ".", "/"], "-") %>
<div class="relative flex flex-col min-w-0 break-words bg-white w-full shadow-lg rounded">
<div class="rounded-t mb-0 px-4 py-3 border border-b border-solid">
<div class="flex flex-wrap items-center">
<div class="relative w-full px-4 max-w-full flex-grow flex-1">
<h3 class="font-semibold text-base text-blueGray-700">
{@title}
</h3>
</div>
</div>
</div>

<% metrics = Enum.map(@metrics.inserts, fn {_id, _index, data, _} -> data end) %>
<% normalized_metrics = normalize(metrics) %>
<% echart_config = config(normalized_metrics) %>

<div
id={id}
phx-hook="LiveMetricsEChart"
data-config={Jason.encode!(echart_config)}
data-reset={Jason.encode!(@metrics.reset?)}
data-columns={Jason.encode!(@cols)}
phx-update="ignore"
>
<div id={"#{id}-chart"} class="h-64" />
</div>
</div>
</div>
"""
end

defp normalize(metrics) do
empty_series_data = %{
limit: [],
total: []
}

# NOTE: Streams are retrieved in the reverse order
{series_data, categories_data} =
Enum.reduce(metrics, {empty_series_data, []}, fn
%ObserverWeb.Telemetry.Data{value: nil} = metric, {series_data, categories_data} ->
timestamp = Common.timestamp_to_string(metric.timestamp)

{%{
limit: [nil] ++ series_data.limit,
total: [nil] ++ series_data.total
}, [timestamp] ++ categories_data}

metric, {series_data, categories_data} ->
timestamp = Common.timestamp_to_string(metric.timestamp)

{%{
limit: [metric.measurements.limit] ++ series_data.limit,
total: [metric.measurements.total] ++ series_data.total
}, [timestamp] ++ categories_data}
end)

datasets =
[
%{
name: "Limit",
type: "line",
data: series_data.limit
},
%{
name: "Total",
type: "line",
data: series_data.total
}
]

%{
datasets: datasets,
categories: categories_data
}
end

defp config(%{datasets: datasets, categories: categories}) do
%{
tooltip: %{
trigger: "axis"
},
legend: %{
data: [
"Limit",
"Total"
],
right: "25%"
},
grid: %{
left: "3%",
right: "4%",
bottom: "3%",
top: "30%",
containLabel: true
},
toolbox: %{
feature: %{
dataZoom: %{},
dataView: %{},
saveAsImage: %{}
}
},
yAxis: %{
type: "value",
axisLabel: %{
formatter: "{value} bytes"
}
},
series: datasets,
xAxis: %{
type: "category",
boundaryGap: false,
data: categories
}
}
end
end
8 changes: 8 additions & 0 deletions lib/web/pages/metrics/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Observer.Web.Metrics.Page do
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.VmRunQueue
alias Observer.Web.Components.MultiSelect
Expand Down Expand Up @@ -114,6 +115,13 @@ defmodule Observer.Web.Metrics.Page do
cols={@form.params["num_cols"]}
metrics={Map.get(@streams, data_key)}
/>
<VmLimits.content
title={"#{metric} [#{app.name}]"}
service={service}
metric={metric}
cols={@form.params["num_cols"]}
metrics={Map.get(@streams, data_key)}
/>
<VmRunQueue.content
title={"#{metric} [#{app.name}]"}
service={service}
Expand Down
32 changes: 20 additions & 12 deletions lib/web/telemetry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,24 @@ defmodule Observer.Web.Telemetry do
end

if_not_test do
alias ObserverWeb.Telemetry.Producer.BeamVm
alias ObserverWeb.Telemetry.Producer.PhxLvSocket

defp add_telemetry_poller,
do: [
{:telemetry_poller, measurements: periodic_measurements(), period: 5_000}
{:telemetry_poller,
name: :observer_web_phoenix_liveview_sockets,
measurements: [{PhxLvSocket, :process, []}],
period:
Application.get_env(:observer_web, ObserverWeb.Telemetry)[
:phx_lv_sckt_poller_interval_ms
] || 5_000},
{:telemetry_poller,
name: :observer_web_beam_vm,
measurements: [{BeamVm, :process, []}],
period:
Application.get_env(:observer_web, ObserverWeb.Telemetry)[:beam_vm_poller_interval_ms] ||
1_000}
]
else
defp add_telemetry_poller, do: []
Expand Down Expand Up @@ -66,17 +81,10 @@ defmodule Observer.Web.Telemetry do
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
summary("vm.total_run_queue_lengths.io"),
summary("vm.port.total"),
summary("vm.atom.total"),
summary("vm.process.total")
]
end

if_not_test do
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
{ObserverWeb.Telemetry.Producer.PhxLvSocket, :process_phoenix_liveview_sockets, []}
]
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule ObserverWeb.MixProject do
use Mix.Project

@source_url "https://github.com/thiagoesteves/observer_web"
@version "0.1.8"
@version "0.1.9"

def project do
[
Expand Down
41 changes: 41 additions & 0 deletions test/observer_web/beam_vm_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule ObserverWeb.BeamVmTest do
use ExUnit.Case, async: false

import Mock

alias ObserverWeb.Telemetry.Producer.BeamVm

test "Check the Beam VM statistic info is being published" do
with_mock :telemetry,
execute: fn
[:vm, :port],
%{
total: _,
limit: _
},
%{} ->
:ok

[:vm, :atom],
%{
total: _,
limit: _
},
%{} ->
:ok

[:vm, :process],
%{
total: _,
limit: _
},
%{} ->
:ok
end,
attach: fn _id, _event, _handler, _config ->
:ok
end do
assert :ok == BeamVm.process()
end
end
end
Loading