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,
         'operationId' => get_status,
         get => #{
         get => #{
+            parameters => [
+                {format,
+                    hoconsc:mk(
+                        string(),
+                        #{
+                            in => query,
+                            default => <<"text">>,
+                            desc => ?DESC(get_status_api_format)
+                        }
+                    )}
+            ],
             description => ?DESC(get_status_api),
             description => ?DESC(get_status_api),
             tags => ?TAGS,
             tags => ?TAGS,
             security => [],
             security => [],
@@ -70,7 +81,16 @@ path() ->
     "/status".
     "/status".
 
 
 init(Req0, State) ->
 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),
     Req = cowboy_req:reply(Code, Headers, Body, Req0),
     {ok, Req, State}.
     {ok, Req, State}.
 
 
@@ -78,29 +98,52 @@ init(Req0, State) ->
 %% API Handler funcs
 %% 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
     case emqx_dashboard_listener:is_ready(timer:seconds(20)) of
         true ->
         true ->
-            BrokerStatus = broker_status(),
             AppStatus = application_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 =
             StatusCode =
                 case AppStatus of
                 case AppStatus of
                     running -> 200;
                     running -> 200;
                     not_running -> 503
                     not_running -> 503
                 end,
                 end,
+            ContentType =
+                case Format of
+                    <<"json">> -> <<"applicatin/json">>;
+                    _ -> <<"text/plain">>
+                end,
             Headers = #{
             Headers = #{
-                <<"content-type">> => <<"text/plain">>,
+                <<"content-type">> => ContentType,
                 <<"retry-after">> => <<"15">>
                 <<"retry-after">> => <<"15">>
             },
             },
-            {StatusCode, Headers, list_to_binary(Body)};
+            {StatusCode, Headers, iolist_to_binary(Body)};
         false ->
         false ->
             {503, #{<<"retry-after">> => <<"15">>}, <<>>}
             {503, #{<<"retry-after">> => <<"15">>}, <<>>}
     end.
     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() ->
 broker_status() ->
     case emqx:is_running() of
     case emqx:is_running() of
         true ->
         true ->

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

@@ -38,7 +38,10 @@ all() ->
 get_status_tests() ->
 get_status_tests() ->
     [
     [
         t_status_ok,
         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() ->
 groups() ->
@@ -87,8 +90,10 @@ do_request(Opts) ->
         headers := Headers,
         headers := Headers,
         body := Body0
         body := Body0
     } = Opts,
     } = Opts,
+    QS = maps:get(qs, Opts, ""),
     URL = ?HOST ++ filename:join(Path0),
     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
     %% we must not use `httpc' here, because it keeps retrying when it
     %% receives a 503 with `retry-after' header, and there's no option
     %% receives a 503 with `retry-after' header, and there's no option
     %% to stop that behavior...
     %% to stop that behavior...
@@ -165,3 +170,73 @@ t_status_not_ok(Config) ->
         Headers
         Headers
     ),
     ),
     ok.
     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 {
 emqx_mgmt_api_status {
 
 
 get_status_api.desc:
 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.
 Returns status code 200 if the EMQX application is up and running, 503 otherwise.
 This API was introduced in v5.0.10.
 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:
 get_status_api.label:
 """Service health check"""
 """Service health check"""
 
 
 get_status_response200.desc:
 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"""
 emqx is running"""
 
 
 get_status_response503.desc:
 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 {
 emqx_mgmt_api_status {
 
 
 get_status_api.desc:
 get_status_api.desc:
-"""作为节点的健康检查。 返回一个纯文本的响应,描述节点状态。
+"""节点的健康检查。 返回节点状态的描述信息
 
 
 如果 EMQX 应用程序已经启动并运行,返回状态代码 200,否则返回 503。
 如果 EMQX 应用程序已经启动并运行,返回状态代码 200,否则返回 503。
 
 
 这个API是在v5.0.10中引入的。
 这个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_api.label:
 """服务健康检查"""
 """服务健康检查"""
 
 
 get_status_response200.desc:
 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"""
 emqx is running"""
 
 
 get_status_response503.desc:
 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 格式。"""
 
 
 }
 }