|
1 | 1 | # How to write your own plugin? |
2 | 2 |
|
3 | | -If you need to customize some of your internal metrics and integrate it into observer_ci, |
4 | | -you only need to write a `observer_cli_plugin` behaviour in a few simple steps to get a nice presentation. |
| 3 | +Observer 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. |
5 | 4 |
|
6 | | -1. Configure observer_cli,tell observer_cli how to find your plugin. |
| 5 | +## 1. Register the plugin module |
7 | 6 |
|
8 | | -```erlang |
9 | | -%% module - Specific module implements plugin behavior. It's mandatory. |
10 | | -%% title - Menu title. It's mandatory. |
11 | | -%% shortcut - Switch plugin by shortcut. It's mandatory. |
12 | | -%% interval - Refresh interval ms. It's optional. default is 1500ms. |
13 | | -%% sort_column - Sort the sheet by this index. It's optional default is 2. |
| 7 | +Add a `plugins` entry to the Observer CLI environment (for example in `mix.exs` or `observer_cli.app.src`): |
14 | 8 |
|
| 9 | +```erlang |
15 | 10 | {plugins, |
16 | | - [ |
17 | | - #{module => observer_cli_plug_behaviour_x, title => "XPlug", |
18 | | - interval => 1600, shortcut => "X", sort_column => 3}, |
19 | | - #{module => observer_cli_plug_behaviour_y, title => "YPlug", |
20 | | - interval =>2000, shortcut => "Y", sort_column => 3} |
21 | | - ] |
22 | | -} |
| 11 | + [ |
| 12 | + #{module => observer_cli_plug_behaviour_x, |
| 13 | + title => "XPlug", |
| 14 | + shortcut => "X", |
| 15 | + interval => 1600, |
| 16 | + sort_column => 3}, |
| 17 | + #{module => observer_cli_plug_behaviour_y, |
| 18 | + title => "YPlug", |
| 19 | + shortcut => "Y", |
| 20 | + interval => 2000, |
| 21 | + sort_column => 3} |
| 22 | + ]}. |
23 | 23 | ``` |
24 | 24 |
|
25 | | -The main view is `HOME` by default(`observer_cli:start()`). |
26 | | -If you want to plugin view as main view, DO:`your_cli:start().` |
| 25 | +**Option reference** |
| 26 | + |
| 27 | +- `module` - module implementing the behaviour (required). |
| 28 | +- `title` - label rendered in the menu bar (required). |
| 29 | +- `shortcut` - single key used to jump to the plugin (required). |
| 30 | +- `interval` - refresh rate in milliseconds (optional, defaults to `1500`). |
| 31 | +- `sort_column` - index used when sorting the sheet (optional, defaults to `2`). |
| 32 | +- `handler` - tuple `{PredicateFun, Module}` for custom row handling (optional, see [Custom handlers](#4-custom-handlers)). |
| 33 | + |
| 34 | +The default entry point is still the `HOME` view (`observer_cli:start()`). To boot straight into plugin mode expose a shim: |
27 | 35 |
|
28 | 36 | ```erlang |
29 | | -% your_cli.erl |
30 | | -start() -> observer_cli:start_plugin(). |
| 37 | +-module(your_cli). |
| 38 | + |
| 39 | +start() -> |
| 40 | + observer_cli:start_plugin(). |
31 | 41 | ``` |
32 | 42 |
|
33 | | -2. Write observer_cli_plugin behaviour. |
34 | | - observer_cli_plugin has 3 callbacks. |
| 43 | +## 2. Implement `observer_cli_plugin` |
| 44 | + |
| 45 | +The behaviour defines three callbacks. |
35 | 46 |
|
36 | | -2.1 attributes. |
| 47 | +### `attributes/1` |
37 | 48 |
|
38 | 49 | ```erlang |
39 | 50 | -callback attributes(PrevState) -> {[Rows], NewState} when |
40 | | - Rows :: #{content => string()|integer()|{byte, pos_integer()}, |
41 | | - width => pos_integer(), color => binary()}. |
| 51 | + Rows :: [ |
| 52 | + #{content => string() | integer() | {byte, pos_integer()} | {percent, float()}, |
| 53 | + width => pos_integer(), |
| 54 | + color => binary()} |
| 55 | + ], |
| 56 | + NewState :: any(). |
42 | 57 | ``` |
43 | 58 |
|
44 | | -for example: |
| 59 | +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. |
45 | 60 |
|
46 | 61 | ```erlang |
47 | 62 | attributes(PrevState) -> |
48 | | - Attrs = [ |
49 | | - [ |
50 | | - #{content => "XXX Ets Size", width => 15}, |
51 | | - #{content => 122, width => 10}, |
52 | | - #{content => "Memory Capcity", width => 15}, |
53 | | - #{content => {percent, 0.12}, width => 16}, |
54 | | - #{content => "XYZ1 Process Mem", width => 19}, |
55 | | - #{content => {byte, 1023 * 1203}, width => 19} |
56 | | - ], |
57 | | - [ |
58 | | - #{content => "YYY Ets Size", width => 15}, |
59 | | - #{content => 43, width => 10}, |
60 | | - #{content => "Disk Capcity", width => 15}, |
61 | | - #{content => {percent, 0.23}, width => 16}, |
62 | | - #{content => "XYZ2 Process Mem", width => 19}, |
63 | | - #{content => {byte, 2034 * 220}, width => 19} |
| 63 | + Attrs = [ |
| 64 | + [ |
| 65 | + #{content => "XXX ETS Size", width => 15}, |
| 66 | + #{content => 122, width => 10}, |
| 67 | + #{content => "Memory Capacity", width => 16}, |
| 68 | + #{content => {percent, 0.12}, width => 10}, |
| 69 | + #{content => "XYZ1 Process Mem", width => 20}, |
| 70 | + #{content => {byte, 1023 * 1203}, width => 14} |
| 71 | + ], |
| 72 | + [ |
| 73 | + #{content => "YYY ETS Size", width => 15}, |
| 74 | + #{content => 43, width => 10}, |
| 75 | + #{content => "Disk Capacity", width => 15}, |
| 76 | + #{content => {percent, 0.23}, width => 10}, |
| 77 | + #{content => "XYZ2 Process Mem", width => 20}, |
| 78 | + #{content => {byte, 2034 * 220}, width => 14} |
| 79 | + ] |
64 | 80 | ], |
65 | | - [ |
66 | | - #{content => "ZZZ Ets Size", width => 15}, |
67 | | - #{content => 108, width => 10}, |
68 | | - #{content => "Volume Capcity", width => 15}, |
69 | | - #{content => {percent, 0.101}, width => 16}, |
70 | | - #{content => "XYZ3 Process Mem", width => 19}, |
71 | | - #{content => {byte, 12823}, width => 19} |
72 | | - ] |
73 | | - ], |
74 | | - NewState = PrevState, |
75 | | - {Attrs, NewState}. |
| 81 | + {Attrs, PrevState}. |
76 | 82 | ``` |
77 | 83 |
|
78 | | -```markdown |
| 84 | +Rendered banner: |
| 85 | + |
| 86 | +``` |
79 | 87 | |Home(H)|XPlug(X)|YPlug(Y)| | 0Days 3:34:50 | |
80 | | -|XXX Ets Size | 122 | Memory Capcity | 12.00% | XYZ1 Process Mem | 1.1737 MB | |
81 | | -|YYY Ets Size | 43 | Disk Capcity | 23.00% | XYZ2 Process Mem | 436.9922 KB | |
82 | | -|ZZZ Ets Size | 108 | Volume Capcity | 10.10% | XYZ3 Process Mem | 12.5225 KB | |
| 88 | +|XXX ETS Size | 122 | Memory Capacity | 12.00% | XYZ1 Process Mem | 1.1737 MB | |
| 89 | +|YYY ETS Size | 43 | Disk Capacity | 23.00% | XYZ2 Process Mem | 436.9922 KB | |
83 | 90 | ``` |
84 | 91 |
|
| 92 | +### `sheet_header/0` |
| 93 | + |
85 | 94 | ```erlang |
86 | 95 | -callback sheet_header() -> [SheetHeader] when |
87 | | - SheetHeader :: #{title => string(), width => pos_integer(), shortcut => string()}. |
| 96 | + SheetHeader :: #{title => string(), |
| 97 | + width => pos_integer(), |
| 98 | + shortcut => string()}. |
88 | 99 | ``` |
89 | 100 |
|
90 | | -for example: |
| 101 | +Defines the tabular columns shown underneath the banner. Shortcuts let the user sort the sheet by pressing the letter. |
91 | 102 |
|
92 | 103 | ```erlang |
93 | 104 | sheet_header() -> |
94 | | - [ |
95 | | - #{title => "Pid", width => 15}, |
96 | | - #{title => "Register", width => 20}, |
97 | | - #{title => "Memory", width => 20, shortcut => "S"}, |
98 | | - #{title => "Reductions", width => 23, shortcut => "R"}, |
99 | | - #{title => "Message Queue Len", width => 23, shortcut => "Q"} |
100 | | - ]. |
| 105 | + [ |
| 106 | + #{title => "Pid", width => 15}, |
| 107 | + #{title => "Register", width => 20}, |
| 108 | + #{title => "Memory", width => 20, shortcut => "S"}, |
| 109 | + #{title => "Reductions", width => 23, shortcut => "R"}, |
| 110 | + #{title => "Message Queue Len", width => 23, shortcut => "Q"} |
| 111 | + ]. |
101 | 112 | ``` |
102 | 113 |
|
103 | | -```markdown |
104 | | -|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) | |
| 114 | +Result: |
| 115 | + |
| 116 | +``` |
| 117 | +|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) | |
105 | 118 | ``` |
106 | 119 |
|
107 | | -```erlang |
108 | | --callback sheet_body(PrevState) -> {[SheetBody], NewState} when |
109 | | - PrevState :: any(), |
110 | | - SheetBody :: list(), |
111 | | - NewState :: any(). |
| 120 | +### `sheet_body/1` |
112 | 121 |
|
| 122 | +```erlang |
| 123 | +-callback sheet_body(PrevState) -> {[SheetBody], NewState}. |
113 | 124 | ``` |
114 | 125 |
|
115 | | -for example: |
| 126 | +Return the table rows. Each row is a list; Observer CLI paginates automatically (PageDown/PageUp or `F/B` keys). |
116 | 127 |
|
117 | 128 | ```erlang |
118 | 129 | sheet_body(PrevState) -> |
119 | | - Body = [ |
120 | | - begin |
121 | | - Register = |
122 | | - case erlang:process_info(Pid, registered_name) of |
123 | | - [] -> []; |
124 | | - {_, Name} -> Name |
125 | | - end, |
126 | | - [ |
127 | | - Pid, |
128 | | - Register, |
129 | | - {byte, element(2, erlang:process_info(Pid, memory))}, |
130 | | - element(2, erlang:process_info(Pid, reductions)), |
131 | | - element(2, erlang:process_info(Pid, message_queue_len)) |
132 | | - ] |
133 | | - end |
134 | | - || Pid <- erlang:processes() |
135 | | - ], |
136 | | - NewState = PrevState, |
137 | | - {Body, NewState}. |
138 | | -``` |
139 | | - |
140 | | -Support `{byte, 1024*10}` to ` 10.0000 KB`; `{percent, 0.12}` to `12.00%`. |
141 | | - |
142 | | -```markdown |
143 | | -|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) | |
144 | | -|1 |<0.242.0> | | 4.5020 MB | 26544288 | 0 | |
145 | | -|2 | <0.206.0> | | 1.2824 MB | 13357885 | 0 | |
146 | | -|3 | <0.10.0> | erl_prim_loader | 1.0634 MB | 10046775 | 0 | |
147 | | -|4 | <0.434.0> | | 419.1719 KB | 10503690 | 0 | |
148 | | -|5 | <0.44.0> | application_contro | 416.6250 KB | 153598 | 0 | |
149 | | -|6 | <0.50.0> | code_server | 416.4219 KB | 301045 | 0 | |
150 | | -|7 | <0.9.0> | rebar_agent | 136.7031 KB | 1337603 | 0 | |
151 | | -|8 | <0.207.0> | | 99.3125 KB | 9629 | 0 | |
152 | | -|9 | <0.58.0> | file_server_2 | 41.3359 KB | 34303 | 0 | |
153 | | -|10 | <0.209.0> | | 27.3438 KB | 31210 | 0 | |
154 | | -|11 | <0.0.0> | init | 25.8516 KB | 8485 | 0 | |
155 | | -|refresh: 1600ms q(quit) Positive Number(set refresh interval time ms) F/B(forward/back) Current pages is 1 | |
| 130 | + Rows = [ |
| 131 | + begin |
| 132 | + Register = |
| 133 | + case erlang:process_info(Pid, registered_name) of |
| 134 | + [] -> []; |
| 135 | + {_, Name} -> Name |
| 136 | + end, |
| 137 | + [ |
| 138 | + Pid, |
| 139 | + Register, |
| 140 | + {byte, element(2, erlang:process_info(Pid, memory))}, |
| 141 | + element(2, erlang:process_info(Pid, reductions)), |
| 142 | + element(2, erlang:process_info(Pid, message_queue_len)) |
| 143 | + ] |
| 144 | + end |
| 145 | + || Pid <- erlang:processes() |
| 146 | + ], |
| 147 | + {Rows, PrevState}. |
156 | 148 | ``` |
157 | 149 |
|
158 | | -Support F/B to page up/down. |
| 150 | +Rendered sample: |
159 | 151 |
|
160 | | -[A more specific plugin](https://github.com/zhongwencool/os_stats) can collect linux system information such as kernel vsn, loadavg, disk, memory usage, cpu utilization, IO statistics. |
| 152 | +``` |
| 153 | +|No |Pid |Register |Memory(S) |Reductions(R) |Message Queue Len(Q) | |
| 154 | +|1 |<0.242.0> | |4.5020 MB | 26544288 | 0 | |
| 155 | +|2 |<0.206.0> | |1.2824 MB | 13357885 | 0 | |
| 156 | +|3 |<0.10.0> |erl_prim_loader |1.0634 MB | 10046775 | 0 | |
| 157 | +... |
| 158 | +|refresh: 1600ms q(quit) Positive Number(set refresh interval time ms) F/B(forward/back) Current page is 1 | |
| 159 | +``` |
161 | 160 |
|
162 | | -3. Handler: specific per-item behavior |
| 161 | +### Formatting helpers |
163 | 162 |
|
164 | | -By default, once you select a row, it will show the process information in `observer_cli_process` view. This is done |
165 | | -by looking for a `pid` in the row, so the first one found will be used and passed to the `observer_cli_process` view. |
| 163 | +- `{byte, Value}` automatically renders human-readable byte units. |
| 164 | +- `{percent, Value}` outputs a percentage with two decimals. |
| 165 | +- `color` can be any ANSI color escape (e.g., `?RED_BG`) to highlight critical cells. |
166 | 166 |
|
167 | | -To customize this behavior, you can implement your own handler. The handler is a tuple with a function and a module. |
168 | | -The function is a predicate that will be used to filter all row's items and, if the resulting list in not empty, the |
169 | | -`Handler:start/3` function will be called. The signature is the same of `observer_cli_process:start/3`. |
| 167 | +## 3. Custom handlers |
170 | 168 |
|
171 | | -The new configuration will look like this: |
| 169 | +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`. |
172 | 170 |
|
173 | 171 | ```erlang |
174 | | -%% module - Specific module implements plugin behavior. It's mandatory. |
175 | | -%% title - Menu title. It's mandatory. |
176 | | -%% shortcut - Switch plugin by shortcut. It's mandatory. |
177 | | -%% interval - Refresh interval ms. It's optional. default is 1500ms. |
178 | | -%% sort_column - Sort the sheet by this index. It's optional default is 2. |
179 | | -%% handler - Specific handler implements per-item behavior. It's optional, default is `{fun is_pid/1,observer_cli_process}`.` |
180 | | - |
181 | 172 | {plugins, |
182 | | - [ |
183 | | - #{module => observer_cli_plug_behaviour_x, title => "XPlug", |
184 | | - interval => 1600, shortcut => "X", sort_column => 3, |
185 | | - handler => {fun is_pid/1, observer_cli_plug_item_behaviour_x}}, |
186 | | - #{module => observer_cli_plug_behaviour_y, title => "YPlug", |
187 | | - interval =>2000, shortcut => "Y", sort_column => 3, |
188 | | - handler => {fun is_binary/1, observer_cli_plug_item_behaviour_y}}, |
189 | | - ] |
190 | | -} |
| 173 | + [ |
| 174 | + #{module => observer_cli_plug_behaviour_x, |
| 175 | + title => "XPlug", |
| 176 | + shortcut => "X", |
| 177 | + interval => 1600, |
| 178 | + sort_column => 3, |
| 179 | + handler => {fun is_pid/1, observer_cli_plug_item_behaviour_x}}, |
| 180 | + #{module => observer_cli_plug_behaviour_y, |
| 181 | + title => "YPlug", |
| 182 | + shortcut => "Y", |
| 183 | + interval => 2000, |
| 184 | + sort_column => 3, |
| 185 | + handler => {fun is_binary/1, observer_cli_plug_item_behaviour_y}} |
| 186 | + ]}. |
191 | 187 | ``` |
| 188 | + |
| 189 | +Use this when a row selection should drill into a custom detail view (for example, ETS metadata or OS metrics). |
| 190 | + |
| 191 | +## 4. Example plugin |
| 192 | + |
| 193 | +[`os_stats`](https://github.com/zhongwencool/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. |
0 commit comments