observer_cli 基礎使用擴展指南

簡介

observer_cli是一個針對erlang虛擬機,基於recon實現的模仿werl的observer功能的工具。因爲生產環境一般都是linux系統,而observer是不支持linux系統,有了observer_cli就可以在linux環境下更直觀的觀察環境,及早發現問題。以下是作者自己介紹:

Visualize Erlang/Elixir Nodes On The Command Line base on recon.
Provide a high-performance tool usable both in development and production settings.
Focus on important and detailed information about real-time running system.
Keep minimal consumption.

本文只會簡單的介紹基礎用法,着重點在如果編寫一個自己的擴展插件,安裝和使用部分參看作者寫的內容更詳盡

Git庫地址

安裝

observer_cli支持rebar構建,如果你的項目支持rebar,只需要將observer加入到你的deps目錄中

{deps, [
      {observer_cli, ".*",
       {git, https://github.com/zhongwencool/observer_cli.git, {branch, "master"}}}
]}.

作者用的rebar3,好像不需要填寫項目地址這些?
如果用的rebar而不是rebar3,而且你的項目之前沒有使用recon,那麼還需要修改下項目本身的rebar.config

{deps, [
      {recon, ".*", {git, https://github.com/ferd/recon.git, {branch, "master"}}}
]}.

如果使用了recon那麼只需要保證get-deps的時候recon在observer_cli check前get到即可

除了rebar構建,也支持erlang.mk的構建
另外Elixir也可以使用
這兩種構建方式直接參照項目的ReadMe即可

基礎用法

使用

observer_cli:start().

來啓動本地的observer_cli程序,可以填入一個時間作爲刷新間隔,單位爲毫秒,默認爲1000

observer_cli不僅可以查看本地節點,也可以查看遠程節點

observer_cli:start('[email protected]')
observer_cli:start('[email protected]', Cookie).
observer_cli:start('[email protected]', [{cookie,Cookie},{interval, 1000}]).

以上三種方式都可以啓動,如果沒有指定cookie的時候就需要自己保證當前node和目標node的cookie一致,當然遠程調用的時候還是可以指定刷新間隔

本地節點和遠程節點都必須將observer_cli的庫加進來,自定義插件(後面會介紹)部分的環境變量設置部分必須在查看的節點上確保,而查看的目標節點必須能確實能完成自定義插件配置的功能,目標節點自定義插件的環境變量的設置不是必須的

主界面介紹

主界面
observer_cli提供的功能和observer非常像,而且依靠recon還完成了更細緻的內存分配數據,例如內存的利用率等
標題依次爲

  • 首頁:提供概覽信息,內存分配,進程數量,虛擬機基礎配置,cpu利用率,還有模仿etop的進程列表,可以使用關鍵字完成排序
  • 網絡:提供當前虛擬機已經打開的端口綜述,會列出當前已經打開的端口,同樣可以使用關鍵字完成排序,這部分內容大多依賴recon完成
  • 系統:提供當前系統的概覽,這裏可以看到當前虛擬機內存分配的詳細情況,R18(需要考證)以後的observer也支持了具體的內存分配查看
  • ETS:提供ETS表的查看,和observer功能類似,不過不能直接查看內容,也沒必要。
  • Mnesia:Mnesia數據表的查看
  • App:查看當前虛擬機已經啓動的application,不過不能像observer那種直接以結構樹的形式展示,畢竟是命令行下,如果熟悉架構,這種平鋪列表的內容已經可以滿足解決問題的目的
  • Doc:幫助文檔,提供基礎命令的說明
  • Plugin:最新版本新增的支持自定義插件界面

關於內存和網絡端口詳細數據介紹可以查看recon,當然observer_cli也貼心的做了介紹observer_cli文檔
最新版本的界面和上圖有一丟丟的差距,最新的版本加入了自定義插件部分

主界面上展示的進程可以進入二級菜單針對單個進行進行詳細的查看
在這裏插入圖片描述
在Home頁面輸入當前進程的編號(最左邊的數字)即可,進程較多時可以使用pu/pd進程翻頁,刷新較快時使用p暫停頁面操作

自帶插件功能擴展

這裏以Process界面舉例,加入一個dump當前字典內容的命令,並在界面打印出dump文件所在目錄,通過這個擴展也可以理解observer_cli實現的一些基礎邏輯

Process界面主要由observer_cli_process模塊進行管理,先看一下observer_cli_process模塊的內容

start(Pid, Opts) ->
    #view_opts{process = #process{interval = RefreshMs}} = Opts,
    RenderPid = spawn_link(fun() ->
        ?output(?CLEAR),
        render_worker(info, RefreshMs, Pid, ?INIT_TIME_REF, ?INIT_QUEUE, ?INIT_QUEUE)
                           end),
    manager(RenderPid, Opts).
    
manager(RenderPid, #view_opts{process = ProcOpts} = Opts) ->
    case parse_cmd(Opts, RenderPid) of
        quit ->
            erlang:send(RenderPid, quit);
        {new_interval, NewInterval} ->
            erlang:send(RenderPid, {new_interval, NewInterval}),
            manager(RenderPid,
             Opts#view_opts{process = ProcOpts#process{interval = NewInterval}});
        ViewAction ->
            erlang:send(RenderPid, ViewAction),
            manager(RenderPid, Opts)
    end.
    
render_worker(state, Interval, Pid, TimeRef, RedQ, MemQ) ->
    case render_state(Pid, Interval) of
        ok ->
            next_draw_view(state, TimeRef, Interval, Pid, RedQ, MemQ);
        error ->
            next_draw_view_2(state, TimeRef, Interval, Pid, RedQ, MemQ)
    end.

observer_cli_process本身是我們在選中了查看某一進程詳細信息的時候觸發並開始走創建流程,除了自己本身外會額外spawn一個進程用來管理控制檯的顯示
進程自己本身負責從命令行中讀取命令解讀後抓發給顯示進程,同是還維護定時刷新邏輯和退出邏輯
負責繪製的進程根據當前繪製的選中的狀態,會get數據然後輸入到界面並進入下次循環,邏輯並不複雜

next_draw_view(Status, TimeRef, Interval, Pid, NewRedQ, NewMemQ) ->
    NewTimeRef = observer_cli_lib:next_redraw(TimeRef, Interval),
    next_draw_view_2(Status, NewTimeRef, Interval, Pid, NewRedQ, NewMemQ).

next_draw_view_2(Status, TimeRef, Interval, Pid, NewRedQ, NewMemQ) ->
    receive
        quit ->
            quit;
        {new_interval, NewInterval} ->
            ?output(?CLEAR),
            render_worker(Status, NewInterval, Pid, TimeRef, NewRedQ, NewMemQ);
        info_view ->
            ?output(?CLEAR),
            render_worker(info, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        message_view ->
            ?output(?CLEAR),
            render_worker(message, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        dict_view ->
            ?output(?CLEAR),
            render_worker(dict, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        stack_view ->
            ?output(?CLEAR),
            render_worker(stack, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        state_view ->
            ?output(?CLEAR),
            render_worker(state, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        _Msg ->
            render_worker(Status, Interval, Pid, TimeRef, NewRedQ, NewMemQ)
    end.

next_draw_view和next_draw_view2的區別只有next_draw_view會額外根據當前的界面刷新時間給自己發送一個定時刷新消息而已,所以state的處理中,如果get失敗了就直接進入next_draw_view2沒必要再次循環刷新了

observer_cli_lib:next_redraw生成的消息最後會有_Msg分支catch住根據當前狀態,完成當前界面的刷新
new_interval則是給全局的設置刷新間隔的API的handle,每個模塊都會帶
其他分支則就是具體的功能模塊

現在我們開始給Process模塊加入dump命令

  1. 首先我們給尾行加入我們新加的命令描述
render_last_line() ->
    observer_cli_lib:render_last_line("q(quit) dump(dump dict into file dict.txt)").
  1. 修改parse_cmd命令加入我們新加的dump命令
parse_cmd(ViewOpts, Pid) ->
    case observer_cli_lib:to_list(io:get_line("")) of
        "q\n" ->
            quit;
        "Q\n" ->
            quit;
        "P\n" ->
            info_view;
        "M\n" ->
            message_view;
        "D\n" ->
            dict_view;
        "C\n" ->
            stack_view;
        "S\n" ->
            state_view;
        "H\n" ->
            erlang:exit(Pid, stop),
            observer_cli:start(ViewOpts);
        "dump\n" ->
            {dump_dict,false}; //新加部分
        Number ->
            observer_cli_lib:parse_integer(Number)
    end.

這裏我們在進入刷新邏輯的時候額外加了一個bool變量,因爲dump是一個一次性的命令,不需要刷新,但是我們又要不能破壞刷新邏輯的同時,確保dump只執行一次,且能保持顯示dump文件所在的目錄

  1. 增加dump命令的handle邏輯
next_draw_view_2(Status, TimeRef, Interval, Pid, NewRedQ, NewMemQ) ->
    receive
        quit ->
            quit;
        {new_interval, NewInterval} ->
            ?output(?CLEAR),
            render_worker(Status, NewInterval, Pid, TimeRef, NewRedQ, NewMemQ);
        info_view ->
            ?output(?CLEAR),
            render_worker(info, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        message_view ->
            ?output(?CLEAR),
            render_worker(message, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        dict_view ->
            ?output(?CLEAR),
            render_worker(dict, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        stack_view ->
            ?output(?CLEAR),
            render_worker(stack, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        state_view ->
            ?output(?CLEAR),
            render_worker(state, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        {dump_dict, Done} ->
            ?output(?CLEAR), //新加部分
            render_worker({dump_dict, Done}, Interval, Pid, TimeRef, NewRedQ, NewMemQ);
        _Msg ->
            render_worker(Status, Interval, Pid, TimeRef, NewRedQ, NewMemQ)
    end.

render_worker({dump_dict, Done}, Interval, Pid, TimeRef, RedQ, MemQ) ->
    Menu = render_menu(dict, Interval),
    Line1 = "file:write_file(FileName,io_lib:format(p,[List]),[write]),       ~n",
    LastLine = render_last_line(),
    {Done2, Line2} = case Done of
                true ->
                    {ok, Pwd} = file:get_cwd(),
                    {Done, io_lib:format("Dict write in ~s/~s ~n", [Pwd, "dict.txt"])};
                _ ->
                    case dump_dict(Pid) of
                        undefined ->
                            {undefined, "dict empty! ~n"};
                        FileName ->
                            {ok, Pwd} = file:get_cwd(),
                            {true, io_lib:format("Dict write in ~s/~s ~n", [Pwd, FileName])}
                    end
            end,
    ?output([?CURSOR_TOP, Menu, Line1, Line2, LastLine]),
    next_draw_view({dump_dict, Done2}, TimeRef, Interval, Pid, RedQ, MemQ);
    
dump_dict(Pid) ->
    case erlang:process_info(Pid, dictionary) of
        {dictionary, List} ->
            FileName = "dict.txt",
            file:write_file(FileName, io_lib:format("~p", [List]), [write]),
            FileName;
        undefined ->
            undefined
    end.

render_worker 的內容並不算複雜,根據dump_dict命令帶入的參數決定是否進行dump操作,如果是由dump_dict命令來完成指定Pid的dump操作,如果dump成功就返回true狀態,如果已經是true就保持原狀態
dump_dict寫的很粗糙,如果要進生產環境記得包裝
render_worker的結束我們直接進入下次dump_dict的循環,確保界面上顯示的dump文件所在目錄不會被定時刷新邏輯刷新成其他
現在啓動來看下命令執行結果
主界面
主界面

執行後
執行後

執行結果
執行結果

ok,看來成功了,至此我們就完成了對Process的一個簡單的擴展
如果要做刷新邏輯那就更簡單了只需要編寫自己的render函數,確保每次刷新時get到最新的數據並返回即可

這些新的擴展功能必須要保證要查看的目標節點擁有這些邏輯,如果是用的遠程節點查看,遠程節點只需要有基礎版本的observer_cli庫即可

自定義插件使用

這裏先摘抄一段作者的使用介紹,原文已經介紹的很詳盡了

  1. Configure observer_cli,tell observer_cli how to find your plugin.
  2. Write observer_cli_plugin behaviour.

主要內容在需要完成的3個回調,回調格式如下:

-callback kv_label() -> [Rows] when
    Rows :: #{
    key => string(), key_width => pos_integer(),
    value => string()|integer()|{byte, pos_integer()}, value_width => pos_integer()
    }.

要注意的原文這裏還是用的name,作者的文檔還沒更新,使用key即可
這裏定義的是類似System界面的那種鍵值對結構,如果不想使用可以直接返回一個空字符串,一般返回一個界面的總覽

-callback sheet_header() -> [SheetHeader] when
    SheetHeader :: #{title => string(), width => pos_integer(), shortcut => string()}.

這部分返回表頭其中Shortcut指的是如果表格需要排序,指定快捷鍵後就可以根據當前列排序,還需要注意的是,關鍵詞也會加入到title顯示中,所有width設置的時候也需要考慮進去,當然還要考慮下面的數據的具體長度

-callback sheet_body() -> [SheetBody] when
    SheetBody :: list().

表內容,主要注意和表頭一一對應即可,value支持絕大多數的erlang數據類型,
如果要顯示內存大小之類的字節大小,可以使用observer_cli_lib:to_byte函數,不過用了後排序就不準了。。。
要修復也可以實現,新包裝一個數據結構,然後在observer_cli_plugin:render_sheet_body函數中將

DataSet = lists:map(fun(I) ->
        {0, lists:nth(SortRow, I), I} end,
        Module:sheet_body()),

在這裏對I進行檢查和特殊處理,如果是需要轉變的數據直接轉變

is_memory_type({memory,_}) -> true;
is_memory_type(_) -> false.


render_sheet_body(Module, CurPage, Rows, SortRow, Widths) ->
    DataSet = lists:map(fun(I) ->
        Value = lists:nth(SortRow, I),
        I1 = lists:map(fun(V) ->
            case is_memory_type(V) of
                true ->
                    observer_cli_lib:to_byte(element(2, V));
                _ ->
                    V
            end
                       end, I),
        case is_memory_type(Value) of
            true ->
                {0, element(2, Value), I1};
            _ ->
                {0, Value, I1}
        end
                        end,
        Module:sheet_body()),
    SortData = observer_cli_lib:sublist(DataSet, Rows, CurPage),
    [begin
         List = mix_value_width(Item, Widths, []),
         ?render(List)
     end || {_, _, Item} <- SortData].

這裏可以細緻點,包裝一個observer_cli_memory結構然後include進來,然後配合宏使用

也就是目標節點主要提供這三個回調函數,然後配置到observer_cli 的application env
下就可以在插件界面使用,環境變量配置格式如下

%% module       - Specific module implements plugin behavior. It's mandatory.
%% title        - Menu title. It's mandatory.
%% shortcut     - Switch plugin by shortcut. It's mandatory.
%% interval     - Refresh interval ms. It's optional. default is 1500ms.
%% sort_column  - Sort the sheet by this index. It's optional default is 2.

{plugins,
  [
    #{module => observer_cli_plug_behaviour1, title => "XPlug",
      interval => 1500, shortcut => "X", sort_column => 3},
    #{module => observer_cli_plug_behaviour2, title => "YPlug",
      interval => 1600, shortcut => "Y", sort_column => 3}
  ]
}

一般來說我們不會直接修改deps目錄的內容,所以可以在你的項目啓動的時候,額外配置一個appenv文件,針對observer_cli的application增加plugins變量即可,最好不要直接修改deps下的app文件,需要注意的是這個設置部分是調用發起的節點必須有,也就是界面顯示所在的節點,也就是說如果用的遠程節點查看,則查看用的這個節點必須有這些環境變量才行,而布標節點必提供配置中模塊指定的回調

下面我們加入一個查看當前服務器玩家進程的自定義插件爲例介紹這個功能
在項目內建立observer_cli_role模塊,名字可以隨意指定

kv_label() ->
    [
        [
            #{key => "online_role", key_width => 20,
                value => user_default:n(), value_width => 10}
        ]
    ].

sheet_header() ->
    [
        #{title => "RoleID", width => 15},
        #{title => "RoleName", width => 20},
        #{title => "level", width => 10, shortcut => "Lv"},
        #{title => "VipLv", width => 10, shortcut => "VLv"},
        #{title => "FID", width => 15},
        #{title => "FamilyName", width => 20},
        #{title => "Pid", width => 15},
        #{title => "Memory", width => 20, shortcut => "Me"},
        #{title => "Reductions", width => 24, shortcut => "Re"},
        #{title => "MsgQL", width => 10, shortcut => "Mq"}
    ].
sheet_body() ->
    [begin
         Pid = user_default:rs_pid(RoleID),
         RolePublic = user_default:get_role_public(RoleID),
         [
             RoleID,
             maps:get(rolename, RolePublic),
             maps:get(rolelv, RolePublic),
             maps:get(viplv, RolePublic),
             maps:get(familyid, RolePublic),
             maps:get(familyname, RolePublic),
             Pid,
             {memory,(element(2, erlang:process_info(Pid, memory)))},
             element(2, erlang:process_info(Pid, reductions)),
             element(2, erlang:process_info(Pid, message_queue_len))
         ]
     end || RoleID <- user_default:onlines()
    ].

這裏面用了一些user_default模塊假設存在的功能。這些功能不是非要在user_default模塊提供,調用自己項目的內容即可,注意內存那格,使用了前面說的{memory, Memory}格式,如果沒有修改過排序顯示部分最好直接顯示數字本身,否則會報錯,啓動後切換到自定義插件下,效果如圖
在這裏插入圖片描述

設定了快捷鍵的列就可以使用快捷鍵進行排序,修改後,內存顯示修改後的方式,排序也不會有問題。

ok,現在可以模仿寫一個你自己的插件了,如果自寫插件部分有錯誤,會在目標節點那邊拋出異常,會由項目本身的日誌邏輯catch住方便調試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章