Procházet zdrojové kódy

feat: add a json format support for the /status API

Zaiming (Stone) Shi před 2 roky
rodič
revize
ed7a8659d2

+ 51 - 8
apps/emqx_management/src/emqx_mgmt_api_status.erl

@@ -45,6 +45,17 @@ schema("/status") ->
     #{
         'operationId' => get_status,
         get => #{
+            parameters => [
+                {format,
+                    hoconsc:mk(
+                        string(),
+                        #{
+                            in => query,
+                            default => <<"text">>,
+                            desc => ?DESC(get_status_api_format)
+                        }
+                    )}
+            ],
             description => ?DESC(get_status_api),
             tags => ?TAGS,
             security => [],
@@ -70,7 +81,16 @@ path() ->
     "/status".
 
 init(Req0, State) ->
-    {Code, Headers, Body} = running_status(),
+    Format =
+        try
+            QS = cowboy_req:parse_qs(Req0),
+            {_, F} = lists:keyfind(<<"format">>, 1, QS),
+            F
+        catch
+            _:_ ->
+                <<"text">>
+        end,
+    {Code, Headers, Body} = running_status(Format),
     Req = cowboy_req:reply(Code, Headers, Body, Req0),
     {ok, Req, State}.
 
@@ -78,29 +98,52 @@ init(Req0, State) ->
 %% API Handler funcs
 %%--------------------------------------------------------------------
 
-get_status(get, _Params) ->
-    running_status().
+get_status(get, Params) ->
+    Format = maps:get(<<"format">>, maps:get(query_string, Params, #{}), <<"text">>),
+    running_status(iolist_to_binary(Format)).
 
-running_status() ->
+running_status(Format) ->
     case emqx_dashboard_listener:is_ready(timer:seconds(20)) of
         true ->
-            BrokerStatus = broker_status(),
             AppStatus = application_status(),
-            Body = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
+            Body = do_get_status(AppStatus, Format),
             StatusCode =
                 case AppStatus of
                     running -> 200;
                     not_running -> 503
                 end,
+            ContentType =
+                case Format of
+                    <<"json">> -> <<"applicatin/json">>;
+                    _ -> <<"text/plain">>
+                end,
             Headers = #{
-                <<"content-type">> => <<"text/plain">>,
+                <<"content-type">> => ContentType,
                 <<"retry-after">> => <<"15">>
             },
-            {StatusCode, Headers, list_to_binary(Body)};
+            {StatusCode, Headers, iolist_to_binary(Body)};
         false ->
             {503, #{<<"retry-after">> => <<"15">>}, <<>>}
     end.
 
+do_get_status(AppStatus, <<"json">>) ->
+    BrokerStatus = broker_status(),
+    emqx_utils_json:encode(#{
+        node_name => atom_to_binary(node(), utf8),
+        rel_vsn => vsn(),
+        broker_status => atom_to_binary(BrokerStatus),
+        app_status => atom_to_binary(AppStatus)
+    });
+do_get_status(AppStatus, _) ->
+    BrokerStatus = broker_status(),
+    io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]).
+
+vsn() ->
+    iolist_to_binary([
+        emqx_release:edition_vsn_prefix(),
+        emqx_release:version()
+    ]).
+
 broker_status() ->
     case emqx:is_running() of
         true ->

+ 77 - 2
apps/emqx_management/test/emqx_mgmt_api_status_SUITE.erl

@@ -38,7 +38,10 @@ all() ->
 get_status_tests() ->
     [
         t_status_ok,
-        t_status_not_ok
+        t_status_not_ok,
+        t_status_text_format,
+        t_status_json_format,
+        t_status_bad_format_qs
     ].
 
 groups() ->
@@ -87,8 +90,10 @@ do_request(Opts) ->
         headers := Headers,
         body := Body0
     } = Opts,
+    QS = maps:get(qs, Opts, ""),
     URL = ?HOST ++ filename:join(Path0),
-    {ok, #{host := Host, port := Port, path := Path}} = emqx_http_lib:uri_parse(URL),
+    {ok, #{host := Host, port := Port, path := Path1}} = emqx_http_lib:uri_parse(URL),
+    Path = Path1 ++ QS,
     %% we must not use `httpc' here, because it keeps retrying when it
     %% receives a 503 with `retry-after' header, and there's no option
     %% to stop that behavior...
@@ -165,3 +170,73 @@ t_status_not_ok(Config) ->
         Headers
     ),
     ok.
+
+t_status_text_format(Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => "?format=text",
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        {match, _},
+        re:run(Resp, <<"emqx is running$">>)
+    ),
+    ok.
+
+t_status_json_format(Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => "?format=json",
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        #{<<"app_status">> := <<"running">>},
+        emqx_utils_json:decode(Resp)
+    ),
+    ok.
+
+t_status_bad_format_qs(Config) ->
+    lists:foreach(
+        fun(QS) ->
+            test_status_bad_format_qs(QS, Config)
+        end,
+        [
+            "?a=b",
+            "?format=",
+            "?format=x"
+        ]
+    ).
+
+%% when query-sting is invalid, fallback to text format
+test_status_bad_format_qs(QS, Config) ->
+    Path = ?config(get_status_path, Config),
+    #{
+        body := Resp,
+        status_code := StatusCode
+    } = do_request(#{
+        method => get,
+        path => Path,
+        qs => QS,
+        headers => [],
+        body => no_body
+    }),
+    ?assertEqual(200, StatusCode),
+    ?assertMatch(
+        {match, _},
+        re:run(Resp, <<"emqx is running$">>)
+    ),
+    ok.

+ 26 - 5
rel/i18n/emqx_mgmt_api_status.hocon

@@ -1,21 +1,42 @@
 emqx_mgmt_api_status {
 
 get_status_api.desc:
-"""Serves as a health check for the node.  Returns a plain text response describing the status of the node.  This endpoint requires no authentication.
+"""Serves as a health check for the node.
+Returns response to describe the status of the node and the application.
+
+This endpoint requires no authentication.
 
 Returns status code 200 if the EMQX application is up and running, 503 otherwise.
 This API was introduced in v5.0.10.
-The GET `/status` endpoint (without the `/api/...` prefix) is also an alias to this endpoint and works in the same way.  This alias has been available since v5.0.0."""
+The GET `/status` endpoint (without the `/api/...` prefix) is also an alias to this endpoint and works in the same way.
+This alias has been available since v5.0.0.
+
+Starting from v5.0.25 or e5.0.4, you can also use 'format' parameter to get JSON format information.
+"""
 
 get_status_api.label:
 """Service health check"""
 
 get_status_response200.desc:
-"""Node emqx@127.0.0.1 is started
+"""If 'format' parameter is 'json', then it returns a JSON like below:<br/>
+{
+  "rel_vsn": "v5.0.23",
+  "node_name": "emqx@127.0.0.1",
+  "broker_status": "started",
+  "app_status": "running"
+}
+<br/>
+Otherwise it returns free text strings as below:<br/>
+Node emqx@127.0.0.1 is started
 emqx is running"""
 
 get_status_response503.desc:
-"""Node emqx@127.0.0.1 is stopped
-emqx is not_running"""
+"""When EMQX application is temporary not running or being restarted, it may return 'emqx is not_running'.
+If the 'format' parameter is provided 'json', the nthe 'app_status' field in the JSON object is 'not_running'.
+"""
+
+get_status_api_format.desc:
+"""Specify the response format, 'text' (default) to return the HTTP body in free text,
+or 'json' to return the HTTP body with a JSON object."""
 
 }

+ 17 - 5
rel/i18n/zh/emqx_mgmt_api_status.hocon

@@ -1,22 +1,34 @@
 emqx_mgmt_api_status {
 
 get_status_api.desc:
-"""作为节点的健康检查。 返回一个纯文本的响应,描述节点状态。
+"""节点的健康检查。 返回节点状态的描述信息
 
 如果 EMQX 应用程序已经启动并运行,返回状态代码 200,否则返回 503。
 
 这个API是在v5.0.10中引入的。
-GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。 这个别名从v5.0.0开始就有了。"""
+GET `/status`端点(没有`/api/...`前缀)也是这个端点的一个别名,工作方式相同。 这个别名从v5.0.0开始就有了。
+自 v5.0.25 和 e5.0.4 开始,可以通过指定 'format' 参数来得到 JSON 格式的信息。"""
 
 get_status_api.label:
 """服务健康检查"""
 
 get_status_response200.desc:
-"""Node emqx@127.0.0.1 is started
+"""如果 'format' 参数为 'json',则返回如下JSON:<br/>
+{
+  "rel_vsn": "v5.0.23",
+  "node_name": "emqx@127.0.0.1",
+  "broker_status": "started",
+  "app_status": "running"
+}
+<br/>
+否则返回2行自由格式的文本,第一行描述节点的状态,第二行描述 EMQX 应用运行状态。例如:<br/>
+Node emqx@127.0.0.1 is started
 emqx is running"""
 
 get_status_response503.desc:
-"""Node emqx@127.0.0.1 is stopped
-emqx is not_running"""
+"""如果 EMQX 应用暂时没有启动,或正在重启,则可能返回 'emqx is not_running'"""
+
+get_status_api_format.desc:
+"""指定返回的内容格式。使用 'text'(默认)则返回自由格式的字符串; 'json' 则返回 JSON 格式。"""
 
 }