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

This commit is contained in:
Zaiming (Stone) Shi 2023-04-25 22:22:03 +02:00
parent 9260b5ec6c
commit ed7a8659d2
4 changed files with 171 additions and 20 deletions

View File

@ -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 ->

View File

@ -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.

View File

@ -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."""
}

View File

@ -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 格式。"""
}