How to write your own plugin?
View SourceObserver CLI exposes a small behaviour (observer_cli_plugin) that lets you present custom metrics alongside the built-in views. This guide walks through the required configuration and each callback so you can build your own panels quickly.
1. Register the plugin module
Add a plugins entry to the Observer CLI environment (for example in mix.exs or observer_cli.app.src):
{plugins,
[
#{module => observer_cli_plug_behaviour_x,
title => "XPlug",
shortcut => "X",
interval => 1600,
sort_column => 3},
#{module => observer_cli_plug_behaviour_y,
title => "YPlug",
shortcut => "Y",
interval => 2000,
sort_column => 3}
]}.Option reference
module- module implementing the behaviour (required).title- label rendered in the menu bar (required).shortcut- single key used to jump to the plugin (required).interval- refresh rate in milliseconds (optional, defaults to1500).sort_column- index used when sorting the sheet (optional, defaults to2).handler- tuple{PredicateFun, Module}for custom row handling (optional, see Custom handlers).
The default entry point is still the HOME view (observer_cli:start()). To boot straight into plugin mode expose a shim:
-module(your_cli).
start() ->
observer_cli:start_plugin().2. Implement observer_cli_plugin
The behaviour defines three callbacks.
attributes/1
-callback attributes(PrevState) -> {[Rows], NewState} when
Rows :: [
#{content => string() | integer() | {byte, pos_integer()} | {percent, float()},
width => pos_integer(),
color => binary()}
],
NewState :: any().This callback drives the banner directly under the menu. The structure is a list of rows; each row is a list of maps describing individual cells.
attributes(PrevState) ->
Attrs = [
[
#{content => "XXX ETS Size", width => 15},
#{content => 122, width => 10},
#{content => "Memory Capacity", width => 16},
#{content => {percent, 0.12}, width => 10},
#{content => "XYZ1 Process Mem", width => 20},
#{content => {byte, 1023 * 1203}, width => 14}
],
[
#{content => "YYY ETS Size", width => 15},
#{content => 43, width => 10},
#{content => "Disk Capacity", width => 15},
#{content => {percent, 0.23}, width => 10},
#{content => "XYZ2 Process Mem", width => 20},
#{content => {byte, 2034 * 220}, width => 14}
]
],
{Attrs, PrevState}.Rendered banner:
|Home(H)|XPlug(X)|YPlug(Y)| | 0Days 3:34:50 |
|XXX ETS Size | 122 | Memory Capacity | 12.00% | XYZ1 Process Mem | 1.1737 MB |
|YYY ETS Size | 43 | Disk Capacity | 23.00% | XYZ2 Process Mem | 436.9922 KB |sheet_header/0
-callback sheet_header() -> [SheetHeader] when
SheetHeader :: #{title => string(),
width => pos_integer(),
shortcut => string()}.Defines the tabular columns shown underneath the banner. Shortcuts let the user sort the sheet by pressing the letter.
sheet_header() ->
[
#{title => "Pid", width => 15},
#{title => "Register", width => 20},
#{title => "Memory", width => 20, shortcut => "S"},
#{title => "Reductions", width => 23, shortcut => "R"},
#{title => "Message Queue Len", width => 23, shortcut => "Q"}
].Result:
|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) |sheet_body/1
-callback sheet_body(PrevState) -> {[SheetBody], NewState}.Return the table rows. Each row is a list; Observer CLI paginates automatically (PageDown/PageUp or F/B keys).
sheet_body(PrevState) ->
Rows = [
begin
Register =
case erlang:process_info(Pid, registered_name) of
[] -> [];
{_, Name} -> Name
end,
[
Pid,
Register,
{byte, element(2, erlang:process_info(Pid, memory))},
element(2, erlang:process_info(Pid, reductions)),
element(2, erlang:process_info(Pid, message_queue_len))
]
end
|| Pid <- erlang:processes()
],
{Rows, PrevState}.Rendered sample:
|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) |
|1 |<0.242.0> | |4.5020 MB | 26544288 | 0 |
|2 |<0.206.0> | |1.2824 MB | 13357885 | 0 |
|3 |<0.10.0> |erl_prim_loader |1.0634 MB | 10046775 | 0 |
...
|refresh: 1600ms q(quit) Positive Number(set refresh interval time ms) F/B(forward/back) Current page is 1 |Formatting helpers
{byte, Value}automatically renders human-readable byte units.{percent, Value}outputs a percentage with two decimals.colorcan be any ANSI color escape (e.g.,?RED_BG) to highlight critical cells.
3. Custom handlers
By default, selecting a row in your plugin opens the standard observer_cli_process view for the first pid found in that row. To override this, add a handler tuple to the plugin definition. The predicate receives every element in the row and should return true for the items you need. When the predicate matches, HandlerModule:start/3 is invoked with the same contract as observer_cli_process:start/3.
{plugins,
[
#{module => observer_cli_plug_behaviour_x,
title => "XPlug",
shortcut => "X",
interval => 1600,
sort_column => 3,
handler => {fun is_pid/1, observer_cli_plug_item_behaviour_x}},
#{module => observer_cli_plug_behaviour_y,
title => "YPlug",
shortcut => "Y",
interval => 2000,
sort_column => 3,
handler => {fun is_binary/1, observer_cli_plug_item_behaviour_y}}
]}.Use this when a row selection should drill into a custom detail view (for example, ETS metadata or OS metrics).
4. Example plugin
os_stats shows a complete implementation that surfaces Linux kernel information, load averages, disk usage, memory, CPU, and IO statistics via the same behaviour. Use it as inspiration for structuring larger dashboards.