diff --git a/.gitignore b/.gitignore
index eda7d5652..4c689ce9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.eunit
+*.conf.all
test-data/
deps
!deps/.placeholder
diff --git a/Makefile b/Makefile
index 96eb3ad6e..17437e5f9 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-10:1.13.3-24.2.1-1-a
export EMQX_DEFAULT_RUNNER = alpine:3.15.1
export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh)
export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh)
-export EMQX_DASHBOARD_VERSION ?= v0.28.0
+export EMQX_DASHBOARD_VERSION ?= v0.29.0
export DOCKERFILE := deploy/docker/Dockerfile
export EMQX_REL_FORM ?= tgz
ifeq ($(OS),Windows_NT)
@@ -61,7 +61,7 @@ get-dashboard:
@$(SCRIPTS)/get-dashboard.sh
.PHONY: eunit
-eunit: $(REBAR)
+eunit: $(REBAR) conf-segs
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c
.PHONY: proper
@@ -218,6 +218,7 @@ $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
.PHONY:
conf-segs:
@scripts/merge-config.escript
+ @scripts/merge-i18n.escript
## elixir target is to create release packages using Elixir's Mix
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config
index 51418dce2..f3295fc1c 100644
--- a/apps/emqx/rebar.config
+++ b/apps/emqx/rebar.config
@@ -24,13 +24,12 @@
{deps, [
{lc, {git, "https://github.com/emqx/lc.git", {tag, "0.2.1"}}},
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}},
- {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.8.6"}}},
{jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}},
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}},
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
- {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}},
+ {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}},
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl
index e570ed7cf..30b2fb618 100644
--- a/apps/emqx/src/emqx_frame.erl
+++ b/apps/emqx/src/emqx_frame.erl
@@ -310,7 +310,6 @@ parse_packet(
(PacketId =/= undefined) andalso
StrictMode andalso validate_packet_id(PacketId),
{Properties, Payload} = parse_properties(Rest1, Ver, StrictMode),
- ok = ensure_topic_name_valid(StrictMode, TopicName, Properties),
Publish = #mqtt_packet_publish{
topic_name = TopicName,
packet_id = PacketId,
@@ -425,7 +424,6 @@ parse_will_message(
{Props, Rest} = parse_properties(Bin, Ver, StrictMode),
{Topic, Rest1} = parse_utf8_string(Rest, StrictMode),
{Payload, Rest2} = parse_binary_data(Rest1),
- ok = ensure_topic_name_valid(StrictMode, Topic, Props),
{
Packet#mqtt_packet_connect{
will_props = Props,
@@ -623,15 +621,6 @@ parse_binary_data(Bin) when
->
?PARSE_ERR(malformed_binary_data_length).
-ensure_topic_name_valid(false, _TopicName, _Properties) ->
- ok;
-ensure_topic_name_valid(true, TopicName, _Properties) when TopicName =/= <<>> ->
- ok;
-ensure_topic_name_valid(true, <<>>, #{'Topic-Alias' := _}) ->
- ok;
-ensure_topic_name_valid(true, <<>>, _) ->
- error(empty_topic_name).
-
%%--------------------------------------------------------------------
%% Serialize MQTT Packet
%%--------------------------------------------------------------------
diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl
index 02cb91900..a745f76fb 100644
--- a/apps/emqx/src/emqx_listeners.erl
+++ b/apps/emqx/src/emqx_listeners.erl
@@ -87,13 +87,22 @@ do_list_raw() ->
Listeners = maps:to_list(RawWithDefault),
lists:flatmap(fun format_raw_listeners/1, Listeners).
-format_raw_listeners({Type, Conf}) ->
+format_raw_listeners({Type0, Conf}) ->
+ Type = binary_to_atom(Type0),
lists:map(
fun({LName, LConf0}) when is_map(LConf0) ->
- Running = is_running(binary_to_atom(Type), listener_id(Type, LName), LConf0),
+ Bind = parse_bind(LConf0),
+ Running = is_running(Type, listener_id(Type, LName), LConf0#{bind => Bind}),
LConf1 = maps:remove(<<"authentication">>, LConf0),
- LConf2 = maps:put(<<"running">>, Running, LConf1),
- {Type, LName, LConf2}
+ LConf2 = maps:remove(<<"limiter">>, LConf1),
+ LConf3 = maps:put(<<"running">>, Running, LConf2),
+ CurrConn =
+ case Running of
+ true -> current_conns(Type, LName, Bind);
+ false -> 0
+ end,
+ LConf4 = maps:put(<<"current_connections">>, CurrConn, LConf3),
+ {Type0, LName, LConf4}
end,
maps:to_list(Conf)
).
@@ -112,16 +121,7 @@ is_running(ListenerId) ->
end.
is_running(Type, ListenerId, Conf) when Type =:= tcp; Type =:= ssl ->
- ListenOn =
- case Conf of
- #{bind := Bind} ->
- Bind;
- #{<<"bind">> := Bind} ->
- case emqx_schema:to_ip_port(binary_to_list(Bind)) of
- {ok, L} -> L;
- {error, _} -> binary_to_integer(Bind)
- end
- end,
+ #{bind := ListenOn} = Conf,
try esockd:listener({ListenerId, ListenOn}) of
Pid when is_pid(Pid) ->
true
@@ -545,3 +545,10 @@ str(B) when is_binary(B) ->
binary_to_list(B);
str(S) when is_list(S) ->
S.
+
+parse_bind(#{<<"bind">> := Bind}) when is_integer(Bind) -> Bind;
+parse_bind(#{<<"bind">> := Bind}) ->
+ case emqx_schema:to_ip_port(binary_to_list(Bind)) of
+ {ok, L} -> L;
+ {error, _} -> binary_to_integer(Bind)
+ end.
diff --git a/apps/emqx/test/emqx_frame_SUITE.erl b/apps/emqx/test/emqx_frame_SUITE.erl
index 8dad58243..906b976ac 100644
--- a/apps/emqx/test/emqx_frame_SUITE.erl
+++ b/apps/emqx/test/emqx_frame_SUITE.erl
@@ -157,17 +157,6 @@ t_parse_malformed_utf8_string(_) ->
ParseState = emqx_frame:initial_parse_state(#{strict_mode => true}),
?ASSERT_FRAME_THROW(utf8_string_invalid, emqx_frame:parse(MalformedPacket, ParseState)).
-t_parse_empty_topic_name(_) ->
- Packet = ?PUBLISH_PACKET(?QOS_1, <<>>, 1, #{}, <<>>),
- ?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => false})),
- ?ASSERT_FRAME_THROW(empty_topic_name, parse_serialize(Packet, #{strict_mode => true})).
-
-t_parse_empty_topic_name_with_alias(_) ->
- Props = #{'Topic-Alias' => 16#AB},
- Packet = ?PUBLISH_PACKET(?QOS_1, <<>>, 1, Props, <<>>),
- ?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => false})),
- ?assertEqual(Packet, parse_serialize(Packet, #{strict_mode => true})).
-
t_serialize_parse_v3_connect(_) ->
Bin =
<<16, 37, 0, 6, 77, 81, 73, 115, 100, 112, 3, 2, 0, 60, 0, 23, 109, 111, 115, 113, 112, 117,
diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl
index d053b1c4b..ed0117b96 100644
--- a/apps/emqx_authn/src/emqx_authn_api.erl
+++ b/apps/emqx_authn/src/emqx_authn_api.erl
@@ -149,8 +149,8 @@ fields(response_users) ->
paginated_list_type(ref(response_user));
fields(pagination_meta) ->
[
- {page, non_neg_integer()},
- {limit, non_neg_integer()},
+ {page, pos_integer()},
+ {limit, pos_integer()},
{count, non_neg_integer()}
].
@@ -431,8 +431,10 @@ schema("/authentication/:id/users") ->
description => <<"List users in authenticator in global authentication chain">>,
parameters => [
param_auth_id(),
- {page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})},
- {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})},
+ {page,
+ mk(pos_integer(), #{in => query, desc => <<"Page Index">>, required => false})},
+ {limit,
+ mk(pos_integer(), #{in => query, desc => <<"Page Limit">>, required => false})},
{like_username,
mk(binary(), #{
in => query,
@@ -481,8 +483,10 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
parameters => [
param_listener_id(),
param_auth_id(),
- {page, mk(integer(), #{in => query, desc => <<"Page Index">>, required => false})},
- {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, required => false})}
+ {page,
+ mk(pos_integer(), #{in => query, desc => <<"Page Index">>, required => false})},
+ {limit,
+ mk(pos_integer(), #{in => query, desc => <<"Page Limit">>, required => false})}
],
responses => #{
200 => emqx_dashboard_swagger:schema_with_example(
@@ -1158,8 +1162,17 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
end.
list_users(ChainName, AuthenticatorID, QueryString) ->
- Response = emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString),
- emqx_mgmt_util:generate_response(Response).
+ case emqx_authentication:list_users(ChainName, AuthenticatorID, QueryString) of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Reason} ->
+ {400, #{
+ code => <<"INVALID_PARAMETER">>,
+ message => list_to_binary(io_lib:format("Reason ~p", [Reason]))
+ }};
+ Result ->
+ {200, Result}
+ end.
update_config(Path, ConfigRequest) ->
emqx_conf:update(Path, ConfigRequest, #{
diff --git a/apps/emqx_authz/src/emqx_authz_api_cache.erl b/apps/emqx_authz/src/emqx_authz_api_cache.erl
new file mode 100644
index 000000000..e6d3b941c
--- /dev/null
+++ b/apps/emqx_authz/src/emqx_authz_api_cache.erl
@@ -0,0 +1,74 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_authz_api_cache).
+
+-behaviour(minirest_api).
+
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1
+]).
+
+-export([
+ clean_cache/2
+]).
+
+-define(BAD_REQUEST, 'BAD_REQUEST').
+
+api_spec() ->
+ emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
+
+paths() ->
+ [
+ "/authorization/cache"
+ ].
+
+%%--------------------------------------------------------------------
+%% Schema for each URI
+%%--------------------------------------------------------------------
+
+schema("/authorization/cache") ->
+ #{
+ 'operationId' => clean_cache,
+ delete =>
+ #{
+ description => <<"Clean all authorization cache in the cluster.">>,
+ responses =>
+ #{
+ 204 => <<"No Content">>,
+ 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>)
+ }
+ }
+ }.
+
+clean_cache(delete, _) ->
+ case emqx_mgmt:clean_authz_cache_all() of
+ ok ->
+ {204};
+ {error, Reason} ->
+ {400, #{
+ code => <<"BAD_REQUEST">>,
+ message => bin(Reason)
+ }}
+ end.
+
+%%--------------------------------------------------------------------
+%% Internal functions
+%%--------------------------------------------------------------------
+
+bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
diff --git a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl
index 2f06ccdbc..dcdcf6878 100644
--- a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl
+++ b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl
@@ -405,14 +405,23 @@ fields(meta) ->
%%--------------------------------------------------------------------
users(get, #{query_string := QueryString}) ->
- Response = emqx_mgmt_api:node_query(
- node(),
- QueryString,
- ?ACL_TABLE,
- ?ACL_USERNAME_QSCHEMA,
- ?QUERY_USERNAME_FUN
- ),
- emqx_mgmt_util:generate_response(Response);
+ case
+ emqx_mgmt_api:node_query(
+ node(),
+ QueryString,
+ ?ACL_TABLE,
+ ?ACL_USERNAME_QSCHEMA,
+ ?QUERY_USERNAME_FUN
+ )
+ of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Result ->
+ {200, Result}
+ end;
users(post, #{body := Body}) when is_list(Body) ->
lists:foreach(
fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
@@ -423,14 +432,23 @@ users(post, #{body := Body}) when is_list(Body) ->
{204}.
clients(get, #{query_string := QueryString}) ->
- Response = emqx_mgmt_api:node_query(
- node(),
- QueryString,
- ?ACL_TABLE,
- ?ACL_CLIENTID_QSCHEMA,
- ?QUERY_CLIENTID_FUN
- ),
- emqx_mgmt_util:generate_response(Response);
+ case
+ emqx_mgmt_api:node_query(
+ node(),
+ QueryString,
+ ?ACL_TABLE,
+ ?ACL_CLIENTID_QSCHEMA,
+ ?QUERY_CLIENTID_FUN
+ )
+ of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Result ->
+ {200, Result}
+ end;
clients(post, #{body := Body}) when is_list(Body) ->
lists:foreach(
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
diff --git a/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl
new file mode 100644
index 000000000..306fe3f13
--- /dev/null
+++ b/apps/emqx_authz/test/emqx_authz_api_cache_SUITE.erl
@@ -0,0 +1,79 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_authz_api_cache_SUITE).
+
+-compile(nowarn_export_all).
+-compile(export_all).
+
+-import(emqx_dashboard_api_test_helpers, [request/2, uri/1]).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("common_test/include/ct.hrl").
+
+all() ->
+ emqx_common_test_helpers:all(?MODULE).
+
+groups() ->
+ [].
+
+init_per_suite(Config) ->
+ ok = emqx_common_test_helpers:start_apps(
+ [emqx_conf, emqx_authz, emqx_dashboard, emqx_management],
+ fun set_special_configs/1
+ ),
+ Config.
+
+end_per_suite(_Config) ->
+ {ok, _} = emqx:update_config(
+ [authorization],
+ #{
+ <<"no_match">> => <<"allow">>,
+ <<"cache">> => #{<<"enable">> => <<"true">>},
+ <<"sources">> => []
+ }
+ ),
+ ok = stop_apps([emqx_resource, emqx_connector]),
+ emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf, emqx_management]),
+ ok.
+
+set_special_configs(emqx_dashboard) ->
+ emqx_dashboard_api_test_helpers:set_default_config();
+set_special_configs(emqx_authz) ->
+ {ok, _} = emqx:update_config([authorization, cache, enable], true),
+ {ok, _} = emqx:update_config([authorization, no_match], deny),
+ {ok, _} = emqx:update_config([authorization, sources], []),
+ ok;
+set_special_configs(_App) ->
+ ok.
+
+t_clean_cahce(_) ->
+ {ok, C} = emqtt:start_link([{clientid, <<"emqx0">>}, {username, <<"emqx0">>}]),
+ {ok, _} = emqtt:connect(C),
+ {ok, _, _} = emqtt:subscribe(C, <<"a/b/c">>, 0),
+ ok = emqtt:publish(C, <<"a/b/c">>, <<"{\"x\":1,\"y\":1}">>, 0),
+
+ {ok, 200, Result3} = request(get, uri(["clients", "emqx0", "authorization", "cache"])),
+ ?assertEqual(2, length(jsx:decode(Result3))),
+
+ request(delete, uri(["authorization", "cache"])),
+
+ {ok, 200, Result4} = request(get, uri(["clients", "emqx0", "authorization", "cache"])),
+ ?assertEqual(0, length(jsx:decode(Result4))),
+
+ ok.
+
+stop_apps(Apps) ->
+ lists:foreach(fun application:stop/1, Apps).
diff --git a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_api_i18n.conf b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_api_i18n.conf
new file mode 100644
index 000000000..b8d043e9a
--- /dev/null
+++ b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_api_i18n.conf
@@ -0,0 +1,23 @@
+emqx_auto_subscribe_api {
+ list_auto_subscribe_api {
+ desc {
+ en: """Get auto subscribe topic list"""
+ zh: """获取自动订阅主题列表"""
+ }
+ }
+
+ update_auto_subscribe_api {
+ desc {
+ en: """Update auto subscribe topic list"""
+ zh: """更新自动订阅主题列表"""
+ }
+ }
+
+ update_auto_subscribe_api_response409 {
+ desc {
+ en: """Auto Subscribe topics max limit"""
+ zh: """超出自定订阅主题列表长度限制"""
+ }
+ }
+
+}
diff --git a/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf
new file mode 100644
index 000000000..b93a186b1
--- /dev/null
+++ b/apps/emqx_auto_subscribe/i18n/emqx_auto_subscribe_i18n.conf
@@ -0,0 +1,85 @@
+emqx_auto_subscribe_schema {
+ auto_subscribe {
+ desc {
+ en: """After the device logs in successfully, the subscription is automatically completed for the device through the pre-defined subscription representation. Supports the use of placeholders."""
+ zh: """设备登陆成功之后,通过预设的订阅表示符,为设备自动完成订阅。支持使用占位符。"""
+ }
+ lable {
+ en: """Auto Subscribe"""
+ zh: """自动订阅"""
+ }
+ }
+
+ topic {
+ desc {
+ en: """Topic name, placeholders are supported. For example: client/${clientid}/username/${username}/host/${host}/port/${port}
+Required field, and cannot be empty string"""
+ zh: """订阅标识符,支持使用占位符,例如 client/${clientid}/username/${username}/host/${host}/port/${port}
+必填,且不可为空字符串"""
+ }
+ label {
+ en: """Topic"""
+ zh: """订阅标识符"""
+ }
+ }
+
+ qos {
+ desc {
+ en: """Default value 0. Quality of service.
+At most once (0)
+At least once (1)
+Exactly once (2)"""
+ zh: """缺省值为 0,服务质量,
+QoS 0:消息最多传递一次,如果当时客户端不可用,则会丢失该消息。
+QoS 1:消息传递至少 1 次。
+QoS 2:消息仅传送一次。"""
+ }
+ label {
+ en: """Quality of Service"""
+ zh: """服务质量"""
+ }
+ }
+
+ rh {
+ desc {
+ en: """Default value 0. This option is used to specify whether the server forwards the retained message to the client when establishing a subscription.
+Retain Handling is equal to 0, as long as the client successfully subscribes, the server will send the retained message.
+Retain Handling is equal to 1, if the client successfully subscribes and this subscription does not exist previously, the server sends the retained message. After all, sometimes the client re-initiate the subscription just to change the QoS, but it does not mean that it wants to receive the reserved messages again.
+Retain Handling is equal to 2, even if the client successfully subscribes, the server does not send the retained message."""
+ zh: """指定订阅建立时服务端是否向客户端发送保留消息,
+可选值 0:只要客户端订阅成功,服务端就发送保留消息。
+可选值 1:客户端订阅成功且该订阅此前不存在,服务端才发送保留消息。毕竟有些时候客户端重新发起订阅可能只是为了改变一下 QoS,并不意味着它想再次接收保留消息。
+可选值 2:即便客户订阅成功,服务端也不会发送保留消息。"""
+ }
+ label {
+ en: """Retain Handling"""
+ zh: """Retain Handling"""
+ }
+ }
+
+ rap {
+ desc {
+ en: """Default value 0. This option is used to specify whether the server retains the RETAIN mark when forwarding messages to the client, and this option does not affect the RETAIN mark in the retained message. Therefore, when the option Retain As Publish is set to 0, the client will directly distinguish whether this is a normal forwarded message or a retained message according to the RETAIN mark in the message, instead of judging whether this message is the first received after subscribing(the forwarded message may be sent before the retained message, which depends on the specific implementation of different brokers)."""
+ zh: """缺省值为 0,这一选项用来指定服务端向客户端转发消息时是否要保留其中的 RETAIN 标识,注意这一选项不会影响保留消息中的 RETAIN 标识。因此当 Retain As Publish 选项被设置为 0 时,客户端直接依靠消息中的 RETAIN 标识来区分这是一个正常的转发消息还是一个保留消息,而不是去判断消息是否是自己订阅后收到的第一个消息(转发消息甚至可能会先于保留消息被发送,视不同 Broker 的具体实现而定)。"""
+ }
+ label {
+ en: """Retain As Publish"""
+ zh: """Retain As Publish"""
+ }
+ }
+
+ nl {
+ desc {
+ en: """Default value 0.
+MQTT v3.1.1: if you subscribe to the topic published by yourself, you will receive all messages that you published.
+MQTT v5: if you set this option as 1 when subscribing, the server will not forward the message you published to you."""
+ zh: """缺省值为0,
+MQTT v3.1.1:如果设备订阅了自己发布消息的主题,那么将收到自己发布的所有消息。
+MQTT v5:如果设备在订阅时将此选项设置为 1,那么服务端将不会向设备转发自己发布的消息"""
+ }
+ label {
+ en: """No Local"""
+ zh: """No Local"""
+ }
+ }
+}
diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src
index 92d6932ca..353928592 100644
--- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src
+++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.app.src
@@ -1,16 +1,17 @@
%% -*- mode: erlang -*-
-{application, emqx_auto_subscribe,
- [{description, "An OTP application"},
- {vsn, "0.1.0"},
- {registered, []},
- {mod, {emqx_auto_subscribe_app, []}},
- {applications,
- [kernel,
- stdlib
- ]},
- {env,[]},
- {modules, []},
+{application, emqx_auto_subscribe, [
+ {description, "An OTP application"},
+ {vsn, "0.1.0"},
+ {registered, []},
+ {mod, {emqx_auto_subscribe_app, []}},
+ {applications, [
+ kernel,
+ stdlib,
+ emqx
+ ]},
+ {env, []},
+ {modules, []},
- {licenses, ["Apache 2.0"]},
- {links, []}
- ]}.
+ {licenses, ["Apache 2.0"]},
+ {links, []}
+]}.
diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl
index 067c750c1..27dcb38d9 100644
--- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl
+++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe.erl
@@ -20,19 +20,21 @@
-define(MAX_AUTO_SUBSCRIBE, 20).
--export([load/0, unload/0]). %
+%
+-export([load/0, unload/0]).
--export([ max_limit/0
- , list/0
- , update/1
- , post_config_update/5
- ]).
+-export([
+ max_limit/0,
+ list/0,
+ update/1,
+ post_config_update/5
+]).
%% hook callback
-export([on_client_connected/3]).
load() ->
- emqx_conf:add_handler([auto_subscribe, topics], ?MODULE),
+ ok = emqx_conf:add_handler([auto_subscribe, topics], ?MODULE),
update_hook().
unload() ->
@@ -56,7 +58,8 @@ post_config_update(_KeyPath, _Req, NewTopics, _OldConf, _AppEnvs) ->
on_client_connected(ClientInfo, ConnInfo, {TopicHandler, Options}) ->
case erlang:apply(TopicHandler, handle, [ClientInfo, ConnInfo, Options]) of
- [] -> ok;
+ [] ->
+ ok;
TopicTables ->
_ = self() ! {subscribe, TopicTables},
ok
@@ -71,17 +74,21 @@ format(Rules) when is_list(Rules) ->
[format(Rule) || Rule <- Rules];
format(Rule = #{topic := Topic}) when is_map(Rule) ->
#{
- topic => Topic,
- qos => maps:get(qos, Rule, 0),
- rh => maps:get(rh, Rule, 0),
- rap => maps:get(rap, Rule, 0),
- nl => maps:get(nl, Rule, 0)
+ topic => Topic,
+ qos => maps:get(qos, Rule, 0),
+ rh => maps:get(rh, Rule, 0),
+ rap => maps:get(rap, Rule, 0),
+ nl => maps:get(nl, Rule, 0)
}.
update_(Topics) when length(Topics) =< ?MAX_AUTO_SUBSCRIBE ->
- case emqx_conf:update([auto_subscribe, topics],
- Topics,
- #{rawconf_with_defaults => true, override_to => cluster}) of
+ case
+ emqx_conf:update(
+ [auto_subscribe, topics],
+ Topics,
+ #{rawconf_with_defaults => true, override_to => cluster}
+ )
+ of
{ok, #{raw_config := NewTopics}} ->
{ok, NewTopics};
{error, Reason} ->
diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl
index 7883193cf..48ec92fb4 100644
--- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl
+++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl
@@ -29,6 +29,7 @@
-define(EXCEED_LIMIT, 'EXCEED_LIMIT').
-define(BAD_REQUEST, 'BAD_REQUEST').
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl").
api_spec() ->
@@ -41,20 +42,21 @@ schema("/mqtt/auto_subscribe") ->
#{
'operationId' => auto_subscribe,
get => #{
- description => <<"Auto subscribe list">>,
+ description => ?DESC(list_auto_subscribe_api),
responses => #{
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe")
}
},
put => #{
- description => <<"Update auto subscribe topic list">>,
+ description => ?DESC(update_auto_subscribe_api),
'requestBody' => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
responses => #{
200 => hoconsc:ref(emqx_auto_subscribe_schema, "auto_subscribe"),
- 400 => emqx_mgmt_util:error_schema(
- <<"Request body required">>, [?BAD_REQUEST]),
- 409 => emqx_mgmt_util:error_schema(
- <<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}}
+ 409 => emqx_dashboard_swagger:error_codes(
+ [?EXCEED_LIMIT],
+ ?DESC(update_auto_subscribe_api_response409))
+ }
+ }
}.
%%%==============================================================================================
diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl
index 245339ee1..6770c6774 100644
--- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl
+++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl
@@ -17,6 +17,7 @@
-behaviour(hocon_schema).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("emqx/include/emqx_placeholder.hrl").
@@ -32,34 +33,31 @@ roots() ->
fields("auto_subscribe") ->
[ {topics, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "topic")),
- #{desc => "List of auto-subscribe topics."})}
+ #{desc => ?DESC(auto_subscribe)})}
];
fields("topic") ->
[ {topic, sc(binary(), #{
+ required => true,
example => topic_example(),
- desc => "Topic name, placeholders is supported. For example: "
- ++ binary_to_list(topic_example())})}
+ desc => ?DESC("topic")})}
, {qos, sc(emqx_schema:qos(), #{
default => 0,
- desc => "Quality of service. MQTT definition."})}
+ desc => ?DESC("qos")})}
, {rh, sc(range(0,2), #{
default => 0,
- desc => "Retain handling. MQTT 5.0 definition."})}
+ desc => ?DESC("rh")})}
, {rap, sc(range(0, 1), #{
default => 0,
- desc => "Retain as Published. MQTT 5.0 definition."})}
+ desc => ?DESC("rap")})}
, {nl, sc(range(0, 1), #{
default => 0,
- desc => "Not local. MQTT 5.0 definition."})}
+ desc => ?DESC(nl)})}
].
-desc("auto_subscribe") ->
- "Configuration for `auto_subscribe` feature.";
-desc("topic") ->
- "";
-desc(_) ->
- undefined.
+desc("auto_subscribe") -> ?DESC("auto_subscribe");
+desc("topic") -> ?DESC("topic");
+desc(_) -> undefined.
topic_example() ->
<<"/clientid/", ?PH_S_CLIENTID,
diff --git a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
index 58ca56d7d..5d7ebf57e 100644
--- a/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
+++ b/apps/emqx_auto_subscribe/test/emqx_auto_subscribe_SUITE.erl
@@ -31,9 +31,11 @@
-define(TOPICS, [?TOPIC_C, ?TOPIC_U, ?TOPIC_H, ?TOPIC_P, ?TOPIC_A, ?TOPIC_S]).
--define(ENSURE_TOPICS , [<<"/c/auto_sub_c">>
- , <<"/u/auto_sub_u">>
- , ?TOPIC_S]).
+-define(ENSURE_TOPICS, [
+ <<"/c/auto_sub_c">>,
+ <<"/u/auto_sub_u">>,
+ ?TOPIC_S
+]).
-define(CLIENT_ID, <<"auto_sub_c">>).
-define(CLIENT_USERNAME, <<"auto_sub_u">>).
@@ -45,60 +47,58 @@ init_per_suite(Config) ->
mria:start(),
application:stop(?APP),
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
- meck:expect(emqx_schema, fields, fun("auto_subscribe") ->
- meck:passthrough(["auto_subscribe"]) ++
- emqx_auto_subscribe_schema:fields("auto_subscribe");
- (F) -> meck:passthrough([F])
- end),
+ meck:expect(emqx_schema, fields, fun
+ ("auto_subscribe") ->
+ meck:passthrough(["auto_subscribe"]) ++
+ emqx_auto_subscribe_schema:fields("auto_subscribe");
+ (F) ->
+ meck:passthrough([F])
+ end),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
- meck:expect(emqx_resource, remove, fun(_) -> ok end ),
+ meck:expect(emqx_resource, remove, fun(_) -> ok end),
application:load(emqx_dashboard),
application:load(?APP),
- ok = emqx_common_test_helpers:load_config(emqx_auto_subscribe_schema,
- <<"auto_subscribe {
- topics = [
- {
- topic = \"/c/${clientid}\"
- },
- {
- topic = \"/u/${username}\"
- },
- {
- topic = \"/h/${host}\"
- },
- {
- topic = \"/p/${port}\"
- },
- {
- topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"
- },
- {
- topic = \"/topic/simple\"
- qos = 1
- rh = 0
- rap = 0
- nl = 0
- }
- ]
- }">>),
- emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard, ?APP],
- fun set_special_configs/1),
+ ok = emqx_common_test_helpers:load_config(
+ emqx_auto_subscribe_schema,
+ <<"auto_subscribe {\n"
+ " topics = [\n"
+ " {\n"
+ " topic = \"/c/${clientid}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/u/${username}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/h/${host}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/p/${port}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/client/${clientid}/username/${username}/host/${host}/port/${port}\"\n"
+ " },\n"
+ " {\n"
+ " topic = \"/topic/simple\"\n"
+ " qos = 1\n"
+ " rh = 0\n"
+ " rap = 0\n"
+ " nl = 0\n"
+ " }\n"
+ " ]\n"
+ " }">>
+ ),
+ emqx_common_test_helpers:start_apps(
+ [emqx_conf, emqx_dashboard, ?APP],
+ fun set_special_configs/1
+ ),
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
@@ -106,10 +106,10 @@ set_special_configs(_) ->
topic_config(T) ->
#{
topic => T,
- qos => 0,
- rh => 0,
- rap => 0,
- nl => 0
+ qos => 0,
+ rh => 0,
+ rap => 0,
+ nl => 0
}.
end_per_suite(_) ->
@@ -148,7 +148,6 @@ t_update(_) ->
?assertEqual(1, erlang:length(GETResponseMap)),
ok.
-
check_subs(Count) ->
Subs = ets:tab2list(emqx_suboption),
ct:pal("---> ~p ~p ~n", [Subs, Count]),
diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl
index f9a3a6434..60cc94930 100644
--- a/apps/emqx_bridge/src/emqx_bridge.erl
+++ b/apps/emqx_bridge/src/emqx_bridge.erl
@@ -97,11 +97,12 @@ on_message_publish(Message = #message{topic = Topic, flags = Flags}) ->
send_to_matched_egress_bridges(Topic, Msg) ->
lists:foreach(fun (Id) ->
try send_message(Id, Msg) of
- ok -> ok;
- Error -> ?SLOG(error, #{msg => "send_message_to_bridge_failed",
- bridge => Id, error => Error})
+ {error, Reason} ->
+ ?SLOG(error, #{msg => "send_message_to_bridge_failed",
+ bridge => Id, error => Reason});
+ _ -> ok
catch Err:Reason:ST ->
- ?SLOG(error, #{msg => "send_message_to_bridge_crash",
+ ?SLOG(error, #{msg => "send_message_to_bridge_exception",
bridge => Id, error => Err, reason => Reason,
stacktrace => ST})
end
diff --git a/apps/emqx_conf/src/emqx_conf.erl b/apps/emqx_conf/src/emqx_conf.erl
index 372875d25..f8eabbbd1 100644
--- a/apps/emqx_conf/src/emqx_conf.erl
+++ b/apps/emqx_conf/src/emqx_conf.erl
@@ -25,7 +25,7 @@
-export([update/3, update/4]).
-export([remove/2, remove/3]).
-export([reset/2, reset/3]).
--export([dump_schema/1, dump_schema/2]).
+-export([dump_schema/1, dump_schema/3]).
-export([schema_module/0]).
%% for rpc
@@ -80,15 +80,22 @@ get_node_and_config(KeyPath) ->
{node(), emqx:get_config(KeyPath, config_not_found)}.
%% @doc Update all value of key path in cluster-override.conf or local-override.conf.
--spec update(emqx_map_lib:config_key_path(), emqx_config:update_request(),
- emqx_config:update_opts()) ->
+-spec update(
+ emqx_map_lib:config_key_path(),
+ emqx_config:update_request(),
+ emqx_config:update_opts()
+) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
update(KeyPath, UpdateReq, Opts) ->
check_cluster_rpc_result(emqx_conf_proto_v1:update(KeyPath, UpdateReq, Opts)).
%% @doc Update the specified node's key path in local-override.conf.
--spec update(node(), emqx_map_lib:config_key_path(), emqx_config:update_request(),
- emqx_config:update_opts()) ->
+-spec update(
+ node(),
+ emqx_map_lib:config_key_path(),
+ emqx_config:update_request(),
+ emqx_config:update_opts()
+) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()} | emqx_rpc:badrpc().
update(Node, KeyPath, UpdateReq, Opts0) when Node =:= node() ->
emqx:update_config(KeyPath, UpdateReq, Opts0#{override_to => local});
@@ -126,25 +133,41 @@ reset(Node, KeyPath, Opts) ->
%% @doc Called from build script.
-spec dump_schema(file:name_all()) -> ok.
dump_schema(Dir) ->
- dump_schema(Dir, emqx_conf_schema).
+ I18nFile = emqx:etc_file("i18n.conf"),
+ dump_schema(Dir, emqx_conf_schema, I18nFile).
-dump_schema(Dir, SchemaModule) ->
- SchemaMdFile = filename:join([Dir, "config.md"]),
- io:format(user, "===< Generating: ~s~n", [SchemaMdFile ]),
- ok = gen_doc(SchemaMdFile, SchemaModule),
+dump_schema(Dir, SchemaModule, I18nFile) ->
+ lists:foreach(
+ fun(Lang) ->
+ gen_config_md(Dir, I18nFile, SchemaModule, Lang),
+ gen_hot_conf_schema_json(Dir, I18nFile, Lang)
+ end,
+ [en, zh]
+ ),
+ gen_schema_json(Dir, I18nFile, SchemaModule).
- %% for scripts/spellcheck.
+%% for scripts/spellcheck.
+gen_schema_json(Dir, I18nFile, SchemaModule) ->
SchemaJsonFile = filename:join([Dir, "schema.json"]),
io:format(user, "===< Generating: ~s~n", [SchemaJsonFile]),
- JsonMap = hocon_schema_json:gen(SchemaModule),
+ Opts = #{desc_file => I18nFile, lang => "en"},
+ JsonMap = hocon_schema_json:gen(SchemaModule, Opts),
IoData = jsx:encode(JsonMap, [space, {indent, 4}]),
- ok = file:write_file(SchemaJsonFile, IoData),
+ ok = file:write_file(SchemaJsonFile, IoData).
- %% hot-update configuration schema
- HotConfigSchemaFile = filename:join([Dir, "hot-config-schema.json"]),
+gen_hot_conf_schema_json(Dir, I18nFile, Lang) ->
+ emqx_dashboard:init_i18n(I18nFile, Lang),
+ JsonFile = "hot-config-schema-" ++ atom_to_list(Lang) ++ ".json",
+ HotConfigSchemaFile = filename:join([Dir, JsonFile]),
io:format(user, "===< Generating: ~s~n", [HotConfigSchemaFile]),
ok = gen_hot_conf_schema(HotConfigSchemaFile),
- ok.
+ emqx_dashboard:clear_i18n().
+
+gen_config_md(Dir, I18nFile, SchemaModule, Lang0) ->
+ Lang = atom_to_list(Lang0),
+ SchemaMdFile = filename:join([Dir, "config-" ++ Lang ++ ".md"]),
+ io:format(user, "===< Generating: ~s~n", [SchemaMdFile]),
+ ok = gen_doc(SchemaMdFile, SchemaModule, I18nFile, Lang).
%% @doc return the root schema module.
-spec schema_module() -> module().
@@ -158,63 +181,96 @@ schema_module() ->
%% Internal functions
%%--------------------------------------------------------------------
--spec gen_doc(file:name_all(), module()) -> ok.
-gen_doc(File, SchemaModule) ->
+-spec gen_doc(file:name_all(), module(), file:name_all(), string()) -> ok.
+gen_doc(File, SchemaModule, I18nFile, Lang) ->
Version = emqx_release:version(),
Title = "# " ++ emqx_release:description() ++ " " ++ Version ++ " Configuration",
BodyFile = filename:join([code:lib_dir(emqx_conf), "etc", "emqx_conf.md"]),
{ok, Body} = file:read_file(BodyFile),
- Doc = hocon_schema_md:gen(SchemaModule, #{title => Title, body => Body}),
+ Opts = #{title => Title, body => Body, desc_file => I18nFile, lang => Lang},
+ Doc = hocon_schema_md:gen(SchemaModule, Opts),
file:write_file(File, Doc).
check_cluster_rpc_result(Result) ->
case Result of
- {ok, _TnxId, Res} -> Res;
+ {ok, _TnxId, Res} ->
+ Res;
{retry, TnxId, Res, Nodes} ->
%% The init MFA return ok, but other nodes failed.
%% We return ok and alert an alarm.
- ?SLOG(error, #{msg => "failed_to_update_config_in_cluster", nodes => Nodes,
- tnx_id => TnxId}),
+ ?SLOG(error, #{
+ msg => "failed_to_update_config_in_cluster",
+ nodes => Nodes,
+ tnx_id => TnxId
+ }),
Res;
- {error, Error} -> %% all MFA return not ok or {ok, term()}.
+ %% all MFA return not ok or {ok, term()}.
+ {error, Error} ->
Error
end.
%% Only gen hot_conf schema, not all configuration fields.
gen_hot_conf_schema(File) ->
- {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(emqx_mgmt_api_configs,
- #{schema_converter => fun hocon_schema_to_spec/2}),
- ApiSpec = lists:foldl(fun({Path, Spec, _, _}, Acc) ->
- NewSpec = maps:fold(fun(Method, #{responses := Responses}, SubAcc) ->
- case Responses of
- #{<<"200">> :=
- #{<<"content">> := #{<<"application/json">> := #{<<"schema">> := Schema}}}} ->
- SubAcc#{Method => Schema};
- _ -> SubAcc
- end
- end, #{}, Spec),
- Acc#{list_to_atom(Path) => NewSpec} end, #{}, ApiSpec0),
+ {ApiSpec0, Components0} = emqx_dashboard_swagger:spec(
+ emqx_mgmt_api_configs,
+ #{schema_converter => fun hocon_schema_to_spec/2}
+ ),
+ ApiSpec = lists:foldl(
+ fun({Path, Spec, _, _}, Acc) ->
+ NewSpec = maps:fold(
+ fun(Method, #{responses := Responses}, SubAcc) ->
+ case Responses of
+ #{
+ <<"200">> :=
+ #{
+ <<"content">> := #{
+ <<"application/json">> := #{<<"schema">> := Schema}
+ }
+ }
+ } ->
+ SubAcc#{Method => Schema};
+ _ ->
+ SubAcc
+ end
+ end,
+ #{},
+ Spec
+ ),
+ Acc#{list_to_atom(Path) => NewSpec}
+ end,
+ #{},
+ ApiSpec0
+ ),
Components = lists:foldl(fun(M, Acc) -> maps:merge(M, Acc) end, #{}, Components0),
- IoData = jsx:encode(#{
- info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
- paths => ApiSpec,
- components => #{schemas => Components}
- }, [space, {indent, 4}]),
+ IoData = jsx:encode(
+ #{
+ info => #{title => <<"EMQX Hot Conf Schema">>, version => <<"0.1.0">>},
+ paths => ApiSpec,
+ components => #{schemas => Components}
+ },
+ [space, {indent, 4}]
+ ),
file:write_file(File, IoData).
--define(INIT_SCHEMA, #{fields => #{}, translations => #{},
- validations => [], namespace => undefined}).
+-define(INIT_SCHEMA, #{
+ fields => #{},
+ translations => #{},
+ validations => [],
+ namespace => undefined
+}).
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
--define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
- ?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)])).
+-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
+ iolist_to_binary([
+ <<"#/components/schemas/">>,
+ ?TO_REF(emqx_dashboard_swagger:namespace(_M_), _F_)
+ ])
+).
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
- [{Module, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
- [{LocalModule, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
@@ -226,50 +282,97 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => enum, symbols => Items}, []};
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {#{<<"type">> => object,
- <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
- SubRefs};
+ {
+ #{
+ <<"type">> => object,
+ <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
+ },
+ SubRefs
+ };
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
- {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
- {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {[Schema | Acc], SubRefs ++ RefsAcc}
- end, {[], []}, Types),
+ {OneOf, Refs} = lists:foldl(
+ fun(Type, {Acc, RefsAcc}) ->
+ {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
+ {[Schema | Acc], SubRefs ++ RefsAcc}
+ end,
+ {[], []},
+ Types
+ ),
{#{<<"oneOf">> => OneOf}, Refs};
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
{#{type => enum, symbols => [Atom]}, []}.
-typename_to_spec("user_id_type()", _Mod) -> #{type => enum, symbols => [clientid, username]};
-typename_to_spec("term()", _Mod) -> #{type => string};
-typename_to_spec("boolean()", _Mod) -> #{type => boolean};
-typename_to_spec("binary()", _Mod) -> #{type => string};
-typename_to_spec("float()", _Mod) -> #{type => number};
-typename_to_spec("integer()", _Mod) -> #{type => number};
-typename_to_spec("non_neg_integer()", _Mod) -> #{type => number, minimum => 1};
-typename_to_spec("number()", _Mod) -> #{type => number};
-typename_to_spec("string()", _Mod) -> #{type => string};
-typename_to_spec("atom()", _Mod) -> #{type => string};
-
-typename_to_spec("duration()", _Mod) -> #{type => duration};
-typename_to_spec("duration_s()", _Mod) -> #{type => duration};
-typename_to_spec("duration_ms()", _Mod) -> #{type => duration};
-typename_to_spec("percent()", _Mod) -> #{type => percent};
-typename_to_spec("file()", _Mod) -> #{type => string};
-typename_to_spec("ip_port()", _Mod) -> #{type => ip_port};
-typename_to_spec("url()", _Mod) -> #{type => url};
-typename_to_spec("bytesize()", _Mod) -> #{type => 'byteSize'};
-typename_to_spec("wordsize()", _Mod) -> #{type => 'byteSize'};
-typename_to_spec("qos()", _Mod) -> #{type => enum, symbols => [0, 1, 2]};
-typename_to_spec("comma_separated_list()", _Mod) -> #{type => comma_separated_string};
-typename_to_spec("comma_separated_atoms()", _Mod) -> #{type => comma_separated_string};
-typename_to_spec("pool_type()", _Mod) -> #{type => enum, symbols => [random, hash]};
+typename_to_spec("user_id_type()", _Mod) ->
+ #{type => enum, symbols => [clientid, username]};
+typename_to_spec("term()", _Mod) ->
+ #{type => string};
+typename_to_spec("boolean()", _Mod) ->
+ #{type => boolean};
+typename_to_spec("binary()", _Mod) ->
+ #{type => string};
+typename_to_spec("float()", _Mod) ->
+ #{type => number};
+typename_to_spec("integer()", _Mod) ->
+ #{type => number};
+typename_to_spec("non_neg_integer()", _Mod) ->
+ #{type => number, minimum => 1};
+typename_to_spec("number()", _Mod) ->
+ #{type => number};
+typename_to_spec("string()", _Mod) ->
+ #{type => string};
+typename_to_spec("atom()", _Mod) ->
+ #{type => string};
+typename_to_spec("duration()", _Mod) ->
+ #{type => duration};
+typename_to_spec("duration_s()", _Mod) ->
+ #{type => duration};
+typename_to_spec("duration_ms()", _Mod) ->
+ #{type => duration};
+typename_to_spec("percent()", _Mod) ->
+ #{type => percent};
+typename_to_spec("file()", _Mod) ->
+ #{type => string};
+typename_to_spec("ip_port()", _Mod) ->
+ #{type => ip_port};
+typename_to_spec("url()", _Mod) ->
+ #{type => url};
+typename_to_spec("bytesize()", _Mod) ->
+ #{type => 'byteSize'};
+typename_to_spec("wordsize()", _Mod) ->
+ #{type => 'byteSize'};
+typename_to_spec("qos()", _Mod) ->
+ #{type => enum, symbols => [0, 1, 2]};
+typename_to_spec("comma_separated_list()", _Mod) ->
+ #{type => comma_separated_string};
+typename_to_spec("comma_separated_atoms()", _Mod) ->
+ #{type => comma_separated_string};
+typename_to_spec("pool_type()", _Mod) ->
+ #{type => enum, symbols => [random, hash]};
typename_to_spec("log_level()", _Mod) ->
- #{type => enum, symbols => [debug, info, notice, warning, error,
- critical, alert, emergency, all]};
-typename_to_spec("rate()", _Mod) -> #{type => string};
-typename_to_spec("capacity()", _Mod) -> #{type => string};
-typename_to_spec("burst_rate()", _Mod) -> #{type => string};
-typename_to_spec("failure_strategy()", _Mod) -> #{type => enum, symbols => [force, drop, throw]};
-typename_to_spec("initial()", _Mod) -> #{type => string};
+ #{
+ type => enum,
+ symbols => [
+ debug,
+ info,
+ notice,
+ warning,
+ error,
+ critical,
+ alert,
+ emergency,
+ all
+ ]
+ };
+typename_to_spec("rate()", _Mod) ->
+ #{type => string};
+typename_to_spec("capacity()", _Mod) ->
+ #{type => string};
+typename_to_spec("burst_rate()", _Mod) ->
+ #{type => string};
+typename_to_spec("failure_strategy()", _Mod) ->
+ #{type => enum, symbols => [force, drop, throw]};
+typename_to_spec("initial()", _Mod) ->
+ #{type => string};
typename_to_spec(Name, Mod) ->
Spec = range(Name),
Spec1 = remote_module_type(Spec, Name, Mod),
@@ -282,11 +385,13 @@ default_type(Type) -> Type.
range(Name) ->
case string:split(Name, "..") of
- [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
+ %% 1..10 1..inf -inf..10
+ [MinStr, MaxStr] ->
Schema = #{type => number},
Schema1 = add_integer_prop(Schema, minimum, MinStr),
add_integer_prop(Schema1, maximum, MaxStr);
- _ -> nomatch
+ _ ->
+ nomatch
end.
%% Module:Type
@@ -295,21 +400,25 @@ remote_module_type(nomatch, Name, Mod) ->
[_Module, Type] -> typename_to_spec(Type, Mod);
_ -> nomatch
end;
-remote_module_type(Spec, _Name, _Mod) -> Spec.
+remote_module_type(Spec, _Name, _Mod) ->
+ Spec.
%% [string()] or [integer()] or [xxx].
typerefl_array(nomatch, Name, Mod) ->
case string:trim(Name, leading, "[") of
- Name -> nomatch;
+ Name ->
+ nomatch;
Name1 ->
case string:trim(Name1, trailing, "]") of
- Name1 -> notmatch;
+ Name1 ->
+ notmatch;
Name2 ->
Schema = typename_to_spec(Name2, Mod),
#{type => array, items => Schema}
end
end;
-typerefl_array(Spec, _Name, _Mod) -> Spec.
+typerefl_array(Spec, _Name, _Mod) ->
+ Spec.
%% integer(1)
integer(nomatch, Name) ->
@@ -317,12 +426,13 @@ integer(nomatch, Name) ->
{Int, []} -> #{type => enum, symbols => [Int], default => Int};
_ -> nomatch
end;
-integer(Spec, _Name) -> Spec.
+integer(Spec, _Name) ->
+ Spec.
add_integer_prop(Schema, Key, Value) ->
case string:to_integer(Value) of
{error, no_integer} -> Schema;
- {Int, []}when Key =:= minimum -> Schema#{Key => Int};
+ {Int, []} when Key =:= minimum -> Schema#{Key => Int};
{Int, []} -> Schema#{Key => Int}
end.
@@ -333,4 +443,5 @@ to_bin(List) when is_list(List) ->
end;
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
-to_bin(X) -> X.
+to_bin(X) ->
+ X.
diff --git a/apps/emqx_conf/test/emqx_conf_schema_tests.erl b/apps/emqx_conf/test/emqx_conf_schema_tests.erl
index 1d962b514..b222a49ae 100644
--- a/apps/emqx_conf/test/emqx_conf_schema_tests.erl
+++ b/apps/emqx_conf/test/emqx_conf_schema_tests.erl
@@ -9,5 +9,6 @@
doc_gen_test() ->
Dir = "tmp",
ok = filelib:ensure_dir(filename:join("tmp", foo)),
- _ = emqx_conf:dump_schema(Dir),
+ I18nFile = filename:join(["_build", "test", "lib", "emqx_dashboard", "etc", "i18n.conf.all"]),
+ _ = emqx_conf:dump_schema(Dir, emqx_conf_schema, I18nFile),
ok.
diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl
index 0e1cc3344..f53fa17e5 100644
--- a/apps/emqx_connector/src/emqx_connector_http.erl
+++ b/apps/emqx_connector/src/emqx_connector_http.erl
@@ -95,7 +95,7 @@ For example: `http://localhost:9901/`
, desc => "The type of the pool. Can be one of `random`, `hash`."
})}
, {pool_size,
- sc(non_neg_integer(),
+ sc(pos_integer(),
#{ default => 8
, desc => "The pool size."
})}
diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl
index fbf42ba75..bd02ccc02 100644
--- a/apps/emqx_connector/src/emqx_connector_mongo.erl
+++ b/apps/emqx_connector/src/emqx_connector_mongo.erl
@@ -76,8 +76,8 @@ fields(sharded) ->
, {w_mode, fun w_mode/1}
] ++ mongo_fields();
fields(topology) ->
- [ {pool_size, fun internal_pool_size/1}
- , {max_overflow, fun emqx_connector_schema_lib:pool_size/1}
+ [ {pool_size, fun emqx_connector_schema_lib:pool_size/1}
+ , {max_overflow, fun max_overflow/1}
, {overflow_ttl, fun duration/1}
, {overflow_check_period, fun duration/1}
, {local_threshold_ms, fun duration/1}
@@ -114,12 +114,6 @@ mongo_fields() ->
] ++
emqx_connector_schema_lib:ssl_fields().
-internal_pool_size(type) -> integer();
-internal_pool_size(desc) -> "Pool size on start.";
-internal_pool_size(default) -> 1;
-internal_pool_size(validator) -> [?MIN(1)];
-internal_pool_size(_) -> undefined.
-
%% ===================================================================
on_start(InstId, Config = #{mongo_type := Type,
@@ -334,6 +328,11 @@ duration(desc) -> "Time interval, such as timeout or TTL.";
duration(required) -> false;
duration(_) -> undefined.
+max_overflow(type) -> non_neg_integer();
+max_overflow(desc) -> "Max Overflow.";
+max_overflow(default) -> 0;
+max_overflow(_) -> undefined.
+
replica_set_name(type) -> binary();
replica_set_name(desc) -> "Name of the replica set.";
replica_set_name(required) -> false;
diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl
index 7ad058e64..53fae8d31 100644
--- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl
+++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl
@@ -34,7 +34,7 @@
]).
-type database() :: binary().
--type pool_size() :: integer().
+-type pool_size() :: pos_integer().
-type username() :: binary().
-type password() :: binary().
@@ -72,7 +72,7 @@ database(required) -> true;
database(validator) -> [?NOT_EMPTY("the value of the field 'database' cannot be empty")];
database(_) -> undefined.
-pool_size(type) -> integer();
+pool_size(type) -> pos_integer();
pool_size(desc) -> "Size of the connection pool.";
pool_size(default) -> 8;
pool_size(validator) -> [?MIN(1)];
diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl
index 355b86aaf..25291059d 100644
--- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl
+++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl
@@ -93,7 +93,7 @@ topic filters for 'remote_topic' of ingress connections.
"messages in case of ACK not received.",
#{default => "15s"})}
, {max_inflight,
- sc(integer(),
+ sc(non_neg_integer(),
#{ default => 32
, desc => "Max inflight (sent, but un-acked) messages of the MQTT protocol"
})}
diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf
index 180ba8c3a..f0d77c589 100644
--- a/apps/emqx_dashboard/etc/emqx_dashboard.conf
+++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf
@@ -10,34 +10,29 @@ dashboard {
sample_interval = 10s
## JWT token expiration time.
token_expired_time = 60m
- listeners = [
- {
- protocol = http
- num_acceptors = 4
- max_connections = 512
- bind = 18083
- backlog = 512
- send_timeout = 5s
- inet6 = false
- ipv6_v6only = false
- }
- # ,
- # {
- # protocol = https
- # bind = "127.0.0.1:18084"
- # num_acceptors = 2
- # backlog = 512
- # send_timeout = 5s
- # inet6 = false
- # ipv6_v6only = false
- # certfile = "etc/certs/cert.pem"
- # keyfile = "etc/certs/key.pem"
- # cacertfile = "etc/certs/cacert.pem"
- # verify = verify_peer
- # versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"]
- # ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"]
- # }
- ]
+ listeners.http {
+ num_acceptors = 4
+ max_connections = 512
+ bind = 18083
+ backlog = 512
+ send_timeout = 5s
+ inet6 = false
+ ipv6_v6only = false
+ }
+ #listeners.https {
+ # bind = "127.0.0.1:18084"
+ # num_acceptors = 4
+ # backlog = 512
+ # send_timeout = 5s
+ # inet6 = false
+ # ipv6_v6only = false
+ # certfile = "etc/certs/cert.pem"
+ # keyfile = "etc/certs/key.pem"
+ # cacertfile = "etc/certs/cacert.pem"
+ # verify = verify_peer
+ # versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"]
+ # ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"]
+ #}
## CORS Support. don't set cors true if you don't know what it means.
# cors = false
diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf
new file mode 100644
index 000000000..2a588b752
--- /dev/null
+++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf
@@ -0,0 +1,12 @@
+emqx_dashboard_schema {
+ protocol {
+ desc {
+ en: "Protocol Name"
+ zh: "协议名"
+ }
+ label: {
+ en: "Protocol"
+ zh: "协议"
+ }
+ }
+}
diff --git a/apps/emqx_dashboard/rebar.config b/apps/emqx_dashboard/rebar.config
index 8a44ace37..df2158467 100644
--- a/apps/emqx_dashboard/rebar.config
+++ b/apps/emqx_dashboard/rebar.config
@@ -1,9 +1,6 @@
%% -*- mode: erlang -*-
-{deps,
- [ {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.8.6"}}}
- , {emqx, {path, "../emqx"}}
- ]}.
+{deps, [{emqx, {path, "../emqx"}}]}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl
index 513d7ff50..7f7a029a7 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard.erl
@@ -18,11 +18,19 @@
-define(APP, ?MODULE).
+-export([
+ start_listeners/0,
+ start_listeners/1,
+ stop_listeners/1,
+ stop_listeners/0
+]).
--export([ start_listeners/0
- , start_listeners/1
- , stop_listeners/1
- , stop_listeners/0]).
+-export([
+ init_i18n/2,
+ init_i18n/0,
+ get_i18n/0,
+ clear_i18n/0
+]).
%% Authorization
-export([authorize/1]).
@@ -48,6 +56,7 @@ stop_listeners() ->
start_listeners(Listeners) ->
{ok, _} = application:ensure_all_started(minirest),
+ init_i18n(),
Authorization = {?MODULE, authorize},
GlobalSpec = #{
openapi => "3.0.0",
@@ -58,12 +67,15 @@ start_listeners(Listeners) ->
'securitySchemes' => #{
'basicAuth' => #{type => http, scheme => basic},
'bearerAuth' => #{type => http, scheme => bearer}
- }}},
- Dispatch = [ {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
- , {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}
- , {?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []}
- , {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
- ],
+ }
+ }
+ },
+ Dispatch = [
+ {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}},
+ {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}},
+ {?BASE_PATH ++ "/[...]", emqx_dashboard_bad_api, []},
+ {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}
+ ],
BaseMinirest = #{
base_path => ?BASE_PATH,
modules => minirest_api:find_api_modules(apps()),
@@ -74,75 +86,109 @@ start_listeners(Listeners) ->
middlewares => [cowboy_router, ?EMQX_MIDDLE, cowboy_handler]
},
Res =
- lists:foldl(fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
- Minirest = BaseMinirest#{protocol => Protocol},
- case minirest:start(Name, RanchOptions, Minirest) of
- {ok, _} ->
- ?ULOG("Start listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Bind)]),
- Acc;
- {error, _Reason} ->
- %% Don't record the reason because minirest already does(too much logs noise).
- [Name | Acc]
- end
- end, [], listeners(Listeners)),
+ lists:foldl(
+ fun({Name, Protocol, Bind, RanchOptions}, Acc) ->
+ Minirest = BaseMinirest#{protocol => Protocol},
+ case minirest:start(Name, RanchOptions, Minirest) of
+ {ok, _} ->
+ ?ULOG("Start listener ~ts on ~ts successfully.~n", [
+ Name, emqx_listeners:format_addr(Bind)
+ ]),
+ Acc;
+ {error, _Reason} ->
+ %% Don't record the reason because minirest already does(too much logs noise).
+ [Name | Acc]
+ end
+ end,
+ [],
+ listeners(Listeners)
+ ),
+ clear_i18n(),
case Res of
[] -> ok;
_ -> {error, Res}
end.
stop_listeners(Listeners) ->
- [begin
- case minirest:stop(Name) of
- ok ->
- ?ULOG("Stop listener ~ts on ~ts successfully.~n", [Name, emqx_listeners:format_addr(Port)]);
- {error, not_found} ->
- ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
+ [
+ begin
+ case minirest:stop(Name) of
+ ok ->
+ ?ULOG("Stop listener ~ts on ~ts successfully.~n", [
+ Name, emqx_listeners:format_addr(Port)
+ ]);
+ {error, not_found} ->
+ ?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
+ end
end
- end || {Name, _, Port, _} <- listeners(Listeners)],
+ || {Name, _, Port, _} <- listeners(Listeners)
+ ],
ok.
+get_i18n() ->
+ application:get_env(emqx_dashboard, i18n).
+
+init_i18n(File, Lang) ->
+ Cache = hocon_schema:new_cache(File),
+ application:set_env(emqx_dashboard, i18n, #{lang => atom_to_binary(Lang), cache => Cache}).
+
+clear_i18n() ->
+ case application:get_env(emqx_dashboard, i18n) of
+ {ok, #{cache := Cache}} ->
+ hocon_schema:delete_cache(Cache),
+ application:unset_env(emqx_dashboard, i18n);
+ undefined ->
+ ok
+ end.
+
%%--------------------------------------------------------------------
%% internal
apps() ->
- [App || {App, _, _} <- application:loaded_applications(),
+ [
+ App
+ || {App, _, _} <- application:loaded_applications(),
case re:run(atom_to_list(App), "^emqx") of
- {match,[{0,4}]} -> true;
+ {match, [{0, 4}]} -> true;
_ -> false
- end].
+ end
+ ].
listeners(Listeners) ->
- [begin
- Protocol = maps:get(protocol, ListenerOption0, http),
- {ListenerOption, Bind} = ip_port(ListenerOption0),
- Name = listener_name(Protocol, ListenerOption),
- RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
- {Name, Protocol, Bind, RanchOptions}
- end || ListenerOption0 <- Listeners].
+ lists:map(fun({Protocol, Conf}) ->
+ {Conf1, Bind} = ip_port(Conf),
+ {listener_name(Protocol, Conf1), Protocol, Bind, ranch_opts(Conf1)}
+ end, maps:to_list(Listeners)).
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
-ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
+ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
+init_i18n() ->
+ File = i18n_file(),
+ Lang = emqx_conf:get([dashboard, i18n_lang], en),
+ init_i18n(File, Lang).
ranch_opts(RanchOptions) ->
- Keys = [ {ack_timeout, handshake_timeout}
- , connection_type
- , max_connections
- , num_acceptors
- , shutdown
- , socket],
+ Keys = [
+ {ack_timeout, handshake_timeout},
+ connection_type,
+ max_connections,
+ num_acceptors,
+ shutdown,
+ socket
+ ],
{S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys),
R#{socket_opts => maps:fold(fun key_only/3, [], S)}.
-
-key_take(Key, {All, R}) ->
- {K, KX} = case Key of
- {K1, K2} -> {K1, K2};
- _ -> {Key, Key}
- end,
+key_take(Key, {All, R}) ->
+ {K, KX} =
+ case Key of
+ {K1, K2} -> {K1, K2};
+ _ -> {Key, Key}
+ end,
case maps:get(K, All, undefined) of
undefined ->
{All, R};
@@ -150,20 +196,22 @@ key_take(Key, {All, R}) ->
{maps:remove(K, All), R#{KX => V}}
end.
-key_only(K , true , S) -> [K | S];
-key_only(_K, false, S) -> S;
-key_only(K , V , S) -> [{K, V} | S].
+key_only(K, true, S) -> [K | S];
+key_only(_K, false, S) -> S;
+key_only(K, V, S) -> [{K, V} | S].
listener_name(Protocol, #{port := Port, ip := IP}) ->
- Name = "dashboard:"
- ++ atom_to_list(Protocol) ++ ":"
- ++ inet:ntoa(IP) ++ ":"
- ++ integer_to_list(Port),
+ Name =
+ "dashboard:" ++
+ atom_to_list(Protocol) ++ ":" ++
+ inet:ntoa(IP) ++ ":" ++
+ integer_to_list(Port),
list_to_atom(Name);
listener_name(Protocol, #{port := Port}) ->
- Name = "dashboard:"
- ++ atom_to_list(Protocol) ++ ":"
- ++ integer_to_list(Port),
+ Name =
+ "dashboard:" ++
+ atom_to_list(Protocol) ++ ":" ++
+ integer_to_list(Port),
list_to_atom(Name).
authorize(Req) ->
@@ -180,11 +228,13 @@ authorize(Req) ->
{error, <<"not_allowed">>} ->
return_unauthorized(
?WRONG_USERNAME_OR_PWD,
- <<"Check username/password">>);
+ <<"Check username/password">>
+ );
{error, _} ->
return_unauthorized(
?WRONG_USERNAME_OR_PWD_OR_API_KEY_OR_API_SECRET,
- <<"Check username/password or api_key/api_secret">>)
+ <<"Check username/password or api_key/api_secret">>
+ )
end;
{error, _} ->
return_unauthorized(<<"WORNG_USERNAME_OR_PWD">>, <<"Check username/password">>)
@@ -199,12 +249,22 @@ authorize(Req) ->
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
end;
_ ->
- return_unauthorized(<<"AUTHORIZATION_HEADER_ERROR">>,
- <<"Support authorization: basic/bearer ">>)
+ return_unauthorized(
+ <<"AUTHORIZATION_HEADER_ERROR">>,
+ <<"Support authorization: basic/bearer ">>
+ )
end.
return_unauthorized(Code, Message) ->
- {401, #{<<"WWW-Authenticate">> =>
- <<"Basic Realm=\"minirest-server\"">>},
- #{code => Code, message => Message}
- }.
+ {401,
+ #{
+ <<"WWW-Authenticate">> =>
+ <<"Basic Realm=\"minirest-server\"">>
+ },
+ #{code => Code, message => Message}}.
+
+i18n_file() ->
+ case application:get_env(emqx_dashboard, i18n_file) of
+ undefined -> emqx:etc_file("i18n.conf");
+ {ok, File} -> File
+ end.
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor.erl
index 52c2c25af..fbb294601 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_monitor.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor.erl
@@ -18,6 +18,8 @@
-include("emqx_dashboard.hrl").
+-include_lib("emqx/include/logger.hrl").
+
-behaviour(gen_server).
-boot_mnesia({mnesia, [boot]}).
@@ -128,7 +130,16 @@ current_rate() ->
current_rate(all) ->
current_rate();
current_rate(Node) when Node == node() ->
- do_call(current_rate);
+ try
+ {ok, Rate} = do_call(current_rate),
+ {ok, Rate}
+ catch _E:R ->
+ ?SLOG(warning, #{msg => "Dashboard monitor error", reason => R}),
+ %% Rate map 0, ensure api will not crash.
+ %% When joining cluster, dashboard monitor restart.
+ Rate0 = [{Key, 0} || Key <- ?GAUGE_SAMPLER_LIST ++ maps:values(?DELTA_SAMPLER_RATE_MAP)],
+ {ok, maps:from_list(Rate0)}
+ end;
current_rate(Node) ->
case emqx_dashboard_proto_v1:current_rate(Node) of
{badrpc, Reason} ->
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl
index 35597c9ec..b9ffc7cd1 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_monitor_api.erl
@@ -107,7 +107,7 @@ fields(sampler) ->
Samplers =
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
|| SamplerName <- ?SAMPLER_LIST],
- [{time_stamp, hoconsc:mk(integer(), #{desc => <<"Timestamp">>})} | Samplers];
+ [{time_stamp, hoconsc:mk(non_neg_integer(), #{desc => <<"Timestamp">>})} | Samplers];
fields(sampler_current) ->
[{SamplerName, hoconsc:mk(integer(), #{desc => swagger_desc(SamplerName)})}
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
index f518b8091..6f187a694 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
@@ -15,90 +15,140 @@
%%--------------------------------------------------------------------
-module(emqx_dashboard_schema).
--include_lib("typerefl/include/types.hrl").
+-include_lib("hocon/include/hoconsc.hrl").
--export([ roots/0
- , fields/1
- , namespace/0
- , desc/1
- ]).
+-export([
+ roots/0,
+ fields/1,
+ namespace/0,
+ desc/1
+]).
namespace() -> <<"dashboard">>.
roots() -> ["dashboard"].
fields("dashboard") ->
- [ {listeners,
- sc(hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "http"),
- hoconsc:ref(?MODULE, "https")])),
- #{ desc =>
-"HTTP(s) listeners are identified by their protocol type and are
-used to serve dashboard UI and restful HTTP API.
-Listeners must have a unique combination of port number and IP address.
-For example, an HTTP listener can listen on all configured IP addresses
-on a given port for a machine by specifying the IP address 0.0.0.0.
-Alternatively, the HTTP listener can specify a unique IP address for each listener,
-but use the same port."})}
- , {default_username, fun default_username/1}
- , {default_password, fun default_password/1}
- , {sample_interval, sc(emqx_schema:duration_s(),
- #{ default => "10s"
- , desc => "How often to update metrics displayed in the dashboard.
"
- "Note: `sample_interval` should be a divisor of 60."
- })}
- , {token_expired_time, sc(emqx_schema:duration(),
- #{ default => "30m"
- , desc => "JWT token expiration time."
- })}
- , {cors, fun cors/1}
+ [
+ {listeners,
+ sc(
+ ref("listeners"),
+ #{
+ desc =>
+ "HTTP(s) listeners are identified by their protocol type and are\n"
+ "used to serve dashboard UI and restful HTTP API.
\n"
+ "Listeners must have a unique combination of port number and IP address.
\n"
+ "For example, an HTTP listener can listen on all configured IP addresses\n"
+ "on a given port for a machine by specifying the IP address 0.0.0.0.
\n"
+ "Alternatively, the HTTP listener can specify a unique IP address for each listener,\n"
+ "but use the same port."
+ }
+ )},
+ {default_username, fun default_username/1},
+ {default_password, fun default_password/1},
+ {sample_interval,
+ sc(
+ emqx_schema:duration_s(),
+ #{
+ default => "10s",
+ desc =>
+ "How often to update metrics displayed in the dashboard.
"
+ "Note: `sample_interval` should be a divisor of 60."
+ }
+ )},
+ {token_expired_time,
+ sc(
+ emqx_schema:duration(),
+ #{
+ default => "30m",
+ desc => "JWT token expiration time."
+ }
+ )},
+ {cors, fun cors/1},
+ {i18n_lang, fun i18n_lang/1}
+ ];
+fields("listeners") ->
+ [
+ {"http",
+ sc(
+ ref("http"),
+ #{
+ desc => "TCP listeners",
+ required => {false, recursively}
+ }
+ )},
+ {"https",
+ sc(
+ ref("https"),
+ #{
+ desc => "SSL listeners",
+ required => {false, recursively}
+ }
+ )}
];
-
fields("http") ->
- [ {"protocol", sc(
- hoconsc:enum([http, https]),
- #{ desc => "HTTP/HTTPS protocol."
- , required => true
- , default => http
- })}
- , {"bind", fun bind/1}
- , {"num_acceptors", sc(
- integer(),
- #{ default => 4
- , desc => "Socket acceptor pool size for TCP protocols."
- })}
- , {"max_connections",
- sc(integer(),
- #{ default => 512
- , desc => "Maximum number of simultaneous connections."
- })}
- , {"backlog",
- sc(integer(),
- #{ default => 1024
- , desc => "Defines the maximum length that the queue of pending connections can grow to."
- })}
- , {"send_timeout",
- sc(emqx_schema:duration(),
- #{ default => "5s"
- , desc => "Send timeout for the socket."
- })}
- , {"inet6",
- sc(boolean(),
- #{ default => false
- , desc => "Sets up the listener for IPv6."
- })}
- , {"ipv6_v6only",
- sc(boolean(),
- #{ default => false
- , desc => "Disable IPv4-to-IPv6 mapping for the listener."
- })}
+ [
+ {"bind", fun bind/1},
+ {"num_acceptors",
+ sc(
+ integer(),
+ #{
+ default => 4,
+ desc => "Socket acceptor pool size for TCP protocols."
+ }
+ )},
+ {"max_connections",
+ sc(
+ integer(),
+ #{
+ default => 512,
+ desc => "Maximum number of simultaneous connections."
+ }
+ )},
+ {"backlog",
+ sc(
+ integer(),
+ #{
+ default => 1024,
+ desc =>
+ "Defines the maximum length that the queue of pending connections can grow to."
+ }
+ )},
+ {"send_timeout",
+ sc(
+ emqx_schema:duration(),
+ #{
+ default => "5s",
+ desc => "Send timeout for the socket."
+ }
+ )},
+ {"inet6",
+ sc(
+ boolean(),
+ #{
+ default => false,
+ desc => "Sets up the listener for IPv6."
+ }
+ )},
+ {"ipv6_v6only",
+ sc(
+ boolean(),
+ #{
+ default => false,
+ desc => "Disable IPv4-to-IPv6 mapping for the listener."
+ }
+ )}
];
-
fields("https") ->
fields("http") ++
- proplists:delete("fail_if_no_peer_cert",
- emqx_schema:server_ssl_opts_schema(#{}, true)).
+ proplists:delete(
+ "fail_if_no_peer_cert",
+ emqx_schema:server_ssl_opts_schema(#{}, true)
+ ).
desc("dashboard") ->
"Configuration for EMQX dashboard.";
+desc("listeners") ->
+ "Configuration for the dashboard listener.";
desc("http") ->
"Configuration for the dashboard listener (plaintext).";
desc("https") ->
@@ -119,23 +169,44 @@ default_username(desc) -> "The default username of the automatically created das
default_username('readOnly') -> true;
default_username(_) -> undefined.
-default_password(type) -> string();
-default_password(default) -> "public";
-default_password(required) -> true;
-default_password('readOnly') -> true;
-default_password(sensitive) -> true;
-default_password(desc) -> """
-The initial default password for dashboard 'admin' user.
-For safety, it should be changed as soon as possible.""";
-default_password(_) -> undefined.
+default_password(type) ->
+ string();
+default_password(default) ->
+ "public";
+default_password(required) ->
+ true;
+default_password('readOnly') ->
+ true;
+default_password(sensitive) ->
+ true;
+default_password(desc) ->
+ ""
+ "\n"
+ "The initial default password for dashboard 'admin' user.\n"
+ "For safety, it should be changed as soon as possible."
+ "";
+default_password(_) ->
+ undefined.
-cors(type) -> boolean();
-cors(default) -> false;
-cors(required) -> false;
+cors(type) ->
+ boolean();
+cors(default) ->
+ false;
+cors(required) ->
+ false;
cors(desc) ->
-"Support Cross-Origin Resource Sharing (CORS).
-Allows a server to indicate any origins (domain, scheme, or port) other than
-its own from which a browser should permit loading resources.";
-cors(_) -> undefined.
+ "Support Cross-Origin Resource Sharing (CORS).\n"
+ "Allows a server to indicate any origins (domain, scheme, or port) other than\n"
+ "its own from which a browser should permit loading resources.";
+cors(_) ->
+ undefined.
+
+i18n_lang(type) -> ?ENUM([en, zh]);
+i18n_lang(default) -> en;
+i18n_lang('readOnly') -> true;
+i18n_lang(desc) -> "Internationalization language support.";
+i18n_lang(_) -> undefined.
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
+
+ref(Field) -> hoconsc:ref(?MODULE, Field).
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
index a4bbe74fa..a6a2d1bc4 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl
@@ -28,101 +28,136 @@
-export([filter_check_request/2, filter_check_request_and_translate_body/2]).
-ifdef(TEST).
--export([ parse_spec_ref/3
- , components/2
- ]).
+-export([
+ parse_spec_ref/3,
+ components/2
+]).
-endif.
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
--define(DEFAULT_FIELDS, [example, allowReserved, style, format, readOnly,
- explode, maxLength, allowEmptyValue, deprecated, minimum, maximum]).
+-define(DEFAULT_FIELDS, [
+ example,
+ allowReserved,
+ style,
+ format,
+ readOnly,
+ explode,
+ maxLength,
+ allowEmptyValue,
+ deprecated,
+ minimum,
+ maximum
+]).
--define(INIT_SCHEMA, #{fields => #{}, translations => #{},
- validations => [], namespace => undefined}).
+-define(INIT_SCHEMA, #{
+ fields => #{},
+ translations => #{},
+ validations => [],
+ namespace => undefined
+}).
-define(TO_REF(_N_, _F_), iolist_to_binary([to_bin(_N_), ".", to_bin(_F_)])).
--define(TO_COMPONENTS_SCHEMA(_M_, _F_), iolist_to_binary([<<"#/components/schemas/">>,
- ?TO_REF(namespace(_M_), _F_)])).
--define(TO_COMPONENTS_PARAM(_M_, _F_), iolist_to_binary([<<"#/components/parameters/">>,
- ?TO_REF(namespace(_M_), _F_)])).
+-define(TO_COMPONENTS_SCHEMA(_M_, _F_),
+ iolist_to_binary([
+ <<"#/components/schemas/">>,
+ ?TO_REF(namespace(_M_), _F_)
+ ])
+).
+-define(TO_COMPONENTS_PARAM(_M_, _F_),
+ iolist_to_binary([
+ <<"#/components/parameters/">>,
+ ?TO_REF(namespace(_M_), _F_)
+ ])
+).
-define(MAX_ROW_LIMIT, 1000).
-define(DEFAULT_ROW, 100).
--type(request() :: #{bindings => map(), query_string => map(), body => map()}).
--type(request_meta() :: #{module => module(), path => string(), method => atom()}).
+-type request() :: #{bindings => map(), query_string => map(), body => map()}.
+-type request_meta() :: #{module => module(), path => string(), method => atom()}.
--type(filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}).
--type(filter() :: fun((request(), request_meta()) -> filter_result())).
+-type filter_result() :: {ok, request()} | {400, 'BAD_REQUEST', binary()}.
+-type filter() :: fun((request(), request_meta()) -> filter_result()).
--type(spec_opts() :: #{check_schema => boolean() | filter(),
- translate_body => boolean(),
- schema_converter => fun((hocon_schema:schema(), Module::atom()) -> map())
- }).
+-type spec_opts() :: #{
+ check_schema => boolean() | filter(),
+ translate_body => boolean(),
+ schema_converter => fun((hocon_schema:schema(), Module :: atom()) -> map())
+}.
--type(route_path() :: string() | binary()).
--type(route_methods() :: map()).
--type(route_handler() :: atom()).
--type(route_options() :: #{filter => filter() | undefined}).
+-type route_path() :: string() | binary().
+-type route_methods() :: map().
+-type route_handler() :: atom().
+-type route_options() :: #{filter => filter() | undefined}.
--type(api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}).
--type(api_spec_component() :: map()).
+-type api_spec_entry() :: {route_path(), route_methods(), route_handler(), route_options()}.
+-type api_spec_component() :: map().
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
%% @equiv spec(Module, #{check_schema => false})
--spec(spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}).
+-spec spec(module()) -> {list(api_spec_entry()), list(api_spec_component())}.
spec(Module) -> spec(Module, #{check_schema => false}).
--spec(spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}).
+-spec spec(module(), spec_opts()) -> {list(api_spec_entry()), list(api_spec_component())}.
spec(Module, Options) ->
Paths = apply(Module, paths, []),
{ApiSpec, AllRefs} =
- lists:foldl(fun(Path, {AllAcc, AllRefsAcc}) ->
- {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
- CheckSchema = support_check_schema(Options),
- {[{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
- Refs ++ AllRefsAcc}
- end, {[], []}, Paths),
+ lists:foldl(
+ fun(Path, {AllAcc, AllRefsAcc}) ->
+ {OperationId, Specs, Refs} = parse_spec_ref(Module, Path, Options),
+ CheckSchema = support_check_schema(Options),
+ {
+ [{filename:join("/", Path), Specs, OperationId, CheckSchema} | AllAcc],
+ Refs ++ AllRefsAcc
+ }
+ end,
+ {[], []},
+ Paths
+ ),
{ApiSpec, components(lists:usort(AllRefs), Options)}.
--spec(namespace() -> hocon_schema:name()).
+-spec namespace() -> hocon_schema:name().
namespace() -> "public".
--spec(fields(hocon_schema:name()) -> hocon_schema:fields()).
+-spec fields(hocon_schema:name()) -> hocon_schema:fields().
fields(page) ->
Desc = <<"Page number of the results to fetch.">>,
Meta = #{in => query, desc => Desc, default => 1, example => 1},
- [{page, hoconsc:mk(integer(), Meta)}];
+ [{page, hoconsc:mk(pos_integer(), Meta)}];
fields(limit) ->
- Desc = iolist_to_binary([<<"Results per page(max ">>,
- integer_to_binary(?MAX_ROW_LIMIT), <<")">>]),
+ Desc = iolist_to_binary([
+ <<"Results per page(max ">>,
+ integer_to_binary(?MAX_ROW_LIMIT),
+ <<")">>
+ ]),
Meta = #{in => query, desc => Desc, default => ?DEFAULT_ROW, example => 50},
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}].
--spec(schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map()).
+-spec schema_with_example(hocon_schema:type(), term()) -> hocon_schema:field_schema_map().
schema_with_example(Type, Example) ->
hoconsc:mk(Type, #{examples => #{<<"example">> => Example}}).
--spec(schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map()).
+-spec schema_with_examples(hocon_schema:type(), map()) -> hocon_schema:field_schema_map().
schema_with_examples(Type, Examples) ->
hoconsc:mk(Type, #{examples => #{<<"examples">> => Examples}}).
--spec(error_codes(list(atom())) -> hocon_schema:fields()).
+-spec error_codes(list(atom())) -> hocon_schema:fields().
error_codes(Codes) ->
error_codes(Codes, <<"Error code to troubleshoot problems.">>).
--spec(error_codes(nonempty_list(atom()), binary()) -> hocon_schema:fields()).
-error_codes(Codes = [_ | _], MsgExample) ->
+-spec error_codes(nonempty_list(atom()), binary() | {desc, module(), term()}) ->
+ hocon_schema:fields().
+error_codes(Codes = [_ | _], MsgDesc) ->
[
{code, hoconsc:mk(hoconsc:enum(Codes))},
- {message, hoconsc:mk(string(), #{
- desc => <<"Details description of the error.">>,
- example => MsgExample
- })}
+ {message,
+ hoconsc:mk(string(), #{
+ desc => MsgDesc
+ })}
].
%%------------------------------------------------------------------------------
@@ -143,10 +178,13 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
- catch throw:{_, ValidErrors} ->
- Msg = [io_lib:format("~ts : ~p", [Key, Reason]) ||
- {validation_error, #{path := Key, reason := Reason}} <- ValidErrors],
- {400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
+ catch
+ throw:{_, ValidErrors} ->
+ Msg = [
+ io_lib:format("~ts : ~p", [Key, Reason])
+ || {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
+ ],
+ {400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
end.
check_and_translate(Schema, Map, Opts) ->
@@ -169,30 +207,51 @@ parse_spec_ref(Module, Path, Options) ->
Schema =
try
erlang:apply(Module, schema, [Path])
- catch error: Reason -> %% better error message
- throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
+ %% better error message
+ catch
+ error:Reason ->
+ throw({error, #{mfa => {Module, schema, [Path]}, reason => Reason}})
end,
- {Specs, Refs} = maps:fold(fun(Method, Meta, {Acc, RefsAcc}) ->
- (not lists:member(Method, ?METHODS))
- andalso throw({error, #{module => Module, path => Path, method => Method}}),
- {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
- {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
- end, {#{}, []},
- maps:without(['operationId'], Schema)),
+ {Specs, Refs} = maps:fold(
+ fun(Method, Meta, {Acc, RefsAcc}) ->
+ (not lists:member(Method, ?METHODS)) andalso
+ throw({error, #{module => Module, path => Path, method => Method}}),
+ {Spec, SubRefs} = meta_to_spec(Meta, Module, Options),
+ {Acc#{Method => Spec}, SubRefs ++ RefsAcc}
+ end,
+ {#{}, []},
+ maps:without(['operationId'], Schema)
+ ),
{maps:get('operationId', Schema), Specs, Refs}.
check_parameters(Request, Spec, Module) ->
#{bindings := Bindings, query_string := QueryStr} = Request,
- BindingsBin = maps:fold(fun(Key, Value, Acc) ->
- Acc#{atom_to_binary(Key) => Value}
- end, #{}, Bindings),
+ BindingsBin = maps:fold(
+ fun(Key, Value, Acc) ->
+ Acc#{atom_to_binary(Key) => Value}
+ end,
+ #{},
+ Bindings
+ ),
check_parameter(Spec, BindingsBin, QueryStr, Module, #{}, #{}).
check_parameter([?REF(Fields) | Spec], Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
- check_parameter([?R_REF(LocalMod, Fields) | Spec],
- Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
-check_parameter([?R_REF(Module, Fields) | Spec],
- Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc) ->
+ check_parameter(
+ [?R_REF(LocalMod, Fields) | Spec],
+ Bindings,
+ QueryStr,
+ LocalMod,
+ BindingsAcc,
+ QueryStrAcc
+ );
+check_parameter(
+ [?R_REF(Module, Fields) | Spec],
+ Bindings,
+ QueryStr,
+ LocalMod,
+ BindingsAcc,
+ QueryStrAcc
+) ->
Params = apply(Module, fields, [Fields]),
check_parameter(Params ++ Spec, Bindings, QueryStr, LocalMod, BindingsAcc, QueryStrAcc);
check_parameter([], _Bindings, _QueryStr, _Module, NewBindings, NewQueryStr) ->
@@ -209,7 +268,7 @@ check_parameter([{Name, Type} | Spec], Bindings, QueryStr, Module, BindingsAcc,
Option = #{},
NewQueryStr = hocon_tconf:check_plain(Schema, QueryStr, Option),
NewQueryStrAcc = maps:merge(QueryStrAcc, NewQueryStr),
- check_parameter(Spec, Bindings, QueryStr, Module,BindingsAcc, NewQueryStrAcc)
+ check_parameter(Spec, Bindings, QueryStr, Module, BindingsAcc, NewQueryStrAcc)
end.
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
@@ -230,21 +289,24 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
%% {good_nest_2, mk(ref(?MODULE, good_ref), #{})}
%% ]}
%% ]
-check_request_body(#{body := Body}, Spec, _Module, CheckFun, false)when is_list(Spec) ->
- lists:foldl(fun({Name, Type}, Acc) ->
- Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
- maps:merge(Acc, CheckFun(Schema, Body, #{}))
- end, #{}, Spec);
-
+check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
+ lists:foldl(
+ fun({Name, Type}, Acc) ->
+ Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
+ maps:merge(Acc, CheckFun(Schema, Body, #{}))
+ end,
+ #{},
+ Spec
+ );
%% requestBody => #{content => #{ 'application/octet-stream' =>
%% #{schema => #{ type => string, format => binary}}}
-check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false)when is_map(Spec) ->
+check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
Body.
%% tags, description, summary, security, deprecated
meta_to_spec(Meta, Module, Options) ->
{Params, Refs1} = parameters(maps:get(parameters, Meta, []), Module),
- {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module),
+ {RequestBody, Refs2} = request_body(maps:get('requestBody', Meta, []), Module, Options),
{Responses, Refs3} = responses(maps:get(responses, Meta, #{}), Module, Options),
{
generate_method_desc(to_spec(Meta, Params, RequestBody, Responses)),
@@ -258,95 +320,132 @@ to_spec(Meta, Params, RequestBody, Responses) ->
Spec = to_spec(Meta, Params, [], Responses),
maps:put('requestBody', RequestBody, Spec).
-generate_method_desc(Spec0 = #{desc := Desc}) ->
- Spec = maps:remove(desc, Spec0),
- Spec#{description => to_bin(Desc)};
-generate_method_desc(Spec = #{description := Desc}) ->
- Spec#{description => to_bin(Desc)};
+generate_method_desc(Spec = #{desc := _Desc}) ->
+ trans_description(maps:remove(desc, Spec), Spec);
+generate_method_desc(Spec = #{description := _Desc}) ->
+ trans_description(Spec, Spec);
generate_method_desc(Spec) ->
Spec.
parameters(Params, Module) ->
{SpecList, AllRefs} =
- lists:foldl(fun(Param, {Acc, RefsAcc}) ->
- case Param of
- ?REF(StructName) -> to_ref(Module, StructName, Acc, RefsAcc);
- ?R_REF(RModule, StructName) -> to_ref(RModule, StructName, Acc, RefsAcc);
- {Name, Type} ->
- In = hocon_schema:field_schema(Type, in),
- In =:= undefined andalso
- throw({error, <<"missing in:path/query field in parameters">>}),
- Required = hocon_schema:field_schema(Type, required),
- Default = hocon_schema:field_schema(Type, default),
- HoconType = hocon_schema:field_schema(Type, type),
- Meta = init_meta(Default),
- {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
- Spec0 = init_prop([required | ?DEFAULT_FIELDS],
- #{schema => maps:merge(ParamType, Meta), name => Name, in => In}, Type),
- Spec1 = trans_required(Spec0, Required, In),
- Spec2 = trans_desc(Spec1, Type),
- {[Spec2 | Acc], Refs ++ RefsAcc}
- end
- end, {[], []}, Params),
+ lists:foldl(
+ fun(Param, {Acc, RefsAcc}) ->
+ case Param of
+ ?REF(StructName) ->
+ to_ref(Module, StructName, Acc, RefsAcc);
+ ?R_REF(RModule, StructName) ->
+ to_ref(RModule, StructName, Acc, RefsAcc);
+ {Name, Type} ->
+ In = hocon_schema:field_schema(Type, in),
+ In =:= undefined andalso
+ throw({error, <<"missing in:path/query field in parameters">>}),
+ Required = hocon_schema:field_schema(Type, required),
+ Default = hocon_schema:field_schema(Type, default),
+ HoconType = hocon_schema:field_schema(Type, type),
+ Meta = init_meta(Default),
+ {ParamType, Refs} = hocon_schema_to_spec(HoconType, Module),
+ Spec0 = init_prop(
+ [required | ?DEFAULT_FIELDS],
+ #{schema => maps:merge(ParamType, Meta), name => Name, in => In},
+ Type
+ ),
+ Spec1 = trans_required(Spec0, Required, In),
+ Spec2 = trans_description(Spec1, Type),
+ {[Spec2 | Acc], Refs ++ RefsAcc}
+ end
+ end,
+ {[], []},
+ Params
+ ),
{lists:reverse(SpecList), AllRefs}.
init_meta(undefined) -> #{};
init_meta(Default) -> #{default => Default}.
init_prop(Keys, Init, Type) ->
- lists:foldl(fun(Key, Acc) ->
- case hocon_schema:field_schema(Type, Key) of
- undefined -> Acc;
- Schema -> Acc#{Key => to_bin(Schema)}
- end
- end, Init, Keys).
+ lists:foldl(
+ fun(Key, Acc) ->
+ case hocon_schema:field_schema(Type, Key) of
+ undefined -> Acc;
+ Schema -> Acc#{Key => to_bin(Schema)}
+ end
+ end,
+ Init,
+ Keys
+ ).
trans_required(Spec, true, _) -> Spec#{required => true};
trans_required(Spec, _, path) -> Spec#{required => true};
trans_required(Spec, _, _) -> Spec.
trans_desc(Init, Hocon, Func, Name) ->
- Spec0 = trans_desc(Init, Hocon),
+ Spec0 = trans_description(Init, Hocon),
case Func =:= fun hocon_schema_to_spec/2 of
- true -> Spec0;
+ true ->
+ Spec0;
false ->
- Spec1 = Spec0#{label => Name},
+ Spec1 = trans_label(Spec0, Hocon, Name),
case Spec1 of
#{description := _} -> Spec1;
_ -> Spec1#{description => <>}
end
end.
-trans_desc(Spec, Hocon) ->
- case hocon_schema:field_schema(Hocon, desc) of
- undefined ->
- case hocon_schema:field_schema(Hocon, description) of
- undefined ->
- Spec;
- Desc ->
- Spec#{description => to_bin(Desc)}
- end;
- Desc -> Spec#{description => to_bin(Desc)}
+trans_description(Spec, Hocon) ->
+ case trans_desc(<<"desc">>, Hocon, undefined) of
+ undefined -> Spec;
+ Value -> Spec#{description => Value}
end.
-request_body(#{content := _} = Content, _Module) -> {Content, []};
-request_body([], _Module) -> {[], []};
-request_body(Schema, Module) ->
+trans_label(Spec, Hocon, Default) ->
+ Label = trans_desc(<<"label">>, Hocon, Default),
+ Spec#{label => Label}.
+
+trans_desc(Key, Hocon, Default) ->
+ case resolve_desc(Key, desc_struct(Hocon)) of
+ undefined -> Default;
+ Value -> to_bin(Value)
+ end.
+
+desc_struct(Hocon) ->
+ case hocon_schema:field_schema(Hocon, desc) of
+ undefined -> hocon_schema:field_schema(Hocon, description);
+ Struct -> Struct
+ end.
+
+resolve_desc(_Key, Bin) when is_binary(Bin) -> Bin;
+resolve_desc(Key, Struct) ->
+ {ok, #{cache := Cache, lang := Lang}} = emqx_dashboard:get_i18n(),
+ Desc = hocon_schema:resolve_schema(Struct, Cache),
+ case is_map(Desc) of
+ true -> emqx_map_lib:deep_get([Key, Lang], Desc, undefined);
+ false -> Desc
+ end.
+
+request_body(#{content := _} = Content, _Module, _Options) ->
+ {Content, []};
+request_body([], _Module, _Options) ->
+ {[], []};
+request_body(Schema, Module, Options) ->
{{Props, Refs}, Examples} =
case hoconsc:is_schema(Schema) of
true ->
HoconSchema = hocon_schema:field_schema(Schema, type),
SchemaExamples = hocon_schema:field_schema(Schema, examples),
{hocon_schema_to_spec(HoconSchema, Module), SchemaExamples};
- false -> {parse_object(Schema, Module, #{}), undefined}
+ false ->
+ {parse_object(Schema, Module, Options), undefined}
end,
- {#{<<"content">> => content(Props, Examples)},
- Refs}.
+ {#{<<"content">> => content(Props, Examples)}, Refs}.
responses(Responses, Module, Options) ->
{Spec, Refs, _, _} = maps:fold(fun response/3, {#{}, [], Module, Options}, Responses),
{Spec, Refs}.
+response(Status, ?DESC(_Mod, _Id) = Schema, {Acc, RefsAcc, Module, Options}) ->
+ Desc = trans_description(#{}, #{desc => Schema}),
+ {Acc#{integer_to_binary(Status) => Desc}, RefsAcc, Module, Options};
response(Status, Bin, {Acc, RefsAcc, Module, Options}) when is_binary(Bin) ->
{Acc#{integer_to_binary(Status) => #{description => Bin}}, RefsAcc, Module, Options};
%% Support swagger raw object(file download).
@@ -359,33 +458,49 @@ response(Status, ?R_REF(_Mod, _Name) = RRef, {Acc, RefsAcc, Module, Options}) ->
SchemaToSpec = schema_converter(Options),
{Spec, Refs} = SchemaToSpec(RRef, Module),
Content = content(Spec),
- {Acc#{integer_to_binary(Status) =>
- #{<<"content">> => Content}}, Refs ++ RefsAcc, Module, Options};
+ {
+ Acc#{
+ integer_to_binary(Status) =>
+ #{<<"content">> => Content}
+ },
+ Refs ++ RefsAcc,
+ Module,
+ Options
+ };
response(Status, Schema, {Acc, RefsAcc, Module, Options}) ->
case hoconsc:is_schema(Schema) of
true ->
Hocon = hocon_schema:field_schema(Schema, type),
Examples = hocon_schema:field_schema(Schema, examples),
{Spec, Refs} = hocon_schema_to_spec(Hocon, Module),
- Init = trans_desc(#{}, Schema),
+ Init = trans_description(#{}, Schema),
Content = content(Spec, Examples),
{
Acc#{integer_to_binary(Status) => Init#{<<"content">> => Content}},
- Refs ++ RefsAcc, Module, Options
+ Refs ++ RefsAcc,
+ Module,
+ Options
};
false ->
{Props, Refs} = parse_object(Schema, Module, Options),
- Init = trans_desc(#{}, Schema),
+ Init = trans_description(#{}, Schema),
Content = Init#{<<"content">> => content(Props)},
{Acc#{integer_to_binary(Status) => Content}, Refs ++ RefsAcc, Module, Options}
end.
components(Refs, Options) ->
- lists:sort(maps:fold(fun(K, V, Acc) -> [#{K => V} | Acc] end, [],
- components(Options, Refs, #{}, []))).
+ lists:sort(
+ maps:fold(
+ fun(K, V, Acc) -> [#{K => V} | Acc] end,
+ [],
+ components(Options, Refs, #{}, [])
+ )
+ ).
-components(_Options, [], SpecAcc, []) -> SpecAcc;
-components(Options, [], SpecAcc, SubRefAcc) -> components(Options, SubRefAcc, SpecAcc, []);
+components(_Options, [], SpecAcc, []) ->
+ SpecAcc;
+components(Options, [], SpecAcc, SubRefAcc) ->
+ components(Options, SubRefAcc, SpecAcc, []);
components(Options, [{Module, Field} | Refs], SpecAcc, SubRefsAcc) ->
Props = hocon_schema_fields(Module, Field),
Namespace = namespace(Module),
@@ -404,7 +519,9 @@ hocon_schema_fields(Module, StructName) ->
case apply(Module, fields, [StructName]) of
#{fields := Fields, desc := _} ->
%% evil here, as it's match hocon_schema's internal representation
- Fields; %% TODO: make use of desc ?
+
+ %% TODO: make use of desc ?
+ Fields;
Other ->
Other
end.
@@ -415,15 +532,13 @@ hocon_schema_fields(Module, StructName) ->
namespace(Module) ->
case hocon_schema:namespace(Module) of
undefined -> Module;
- NameSpace -> re:replace(to_bin(NameSpace), ":","-",[global])
+ NameSpace -> re:replace(to_bin(NameSpace), ":", "-", [global])
end.
hocon_schema_to_spec(?R_REF(Module, StructName), _LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)},
- [{Module, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(Module, StructName)}, [{Module, StructName}]};
hocon_schema_to_spec(?REF(StructName), LocalModule) ->
- {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)},
- [{LocalModule, StructName}]};
+ {#{<<"$ref">> => ?TO_COMPONENTS_SCHEMA(LocalModule, StructName)}, [{LocalModule, StructName}]};
hocon_schema_to_spec(Type, LocalModule) when ?IS_TYPEREFL(Type) ->
{typename_to_spec(typerefl:name(Type), LocalModule), []};
hocon_schema_to_spec(?ARRAY(Item), LocalModule) ->
@@ -435,66 +550,111 @@ hocon_schema_to_spec(?ENUM(Items), _LocalModule) ->
{#{type => string, enum => Items}, []};
hocon_schema_to_spec(?MAP(Name, Type), LocalModule) ->
{Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {#{<<"type">> => object,
- <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}},
- SubRefs};
+ {
+ #{
+ <<"type">> => object,
+ <<"properties">> => #{<<"$", (to_bin(Name))/binary>> => Schema}
+ },
+ SubRefs
+ };
hocon_schema_to_spec(?UNION(Types), LocalModule) ->
- {OneOf, Refs} = lists:foldl(fun(Type, {Acc, RefsAcc}) ->
- {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
- {[Schema | Acc], SubRefs ++ RefsAcc}
- end, {[], []}, Types),
+ {OneOf, Refs} = lists:foldl(
+ fun(Type, {Acc, RefsAcc}) ->
+ {Schema, SubRefs} = hocon_schema_to_spec(Type, LocalModule),
+ {[Schema | Acc], SubRefs ++ RefsAcc}
+ end,
+ {[], []},
+ Types
+ ),
{#{<<"oneOf">> => OneOf}, Refs};
hocon_schema_to_spec(Atom, _LocalModule) when is_atom(Atom) ->
{#{type => string, enum => [Atom]}, []}.
%% todo: Find a way to fetch enum value from user_id_type().
-typename_to_spec("user_id_type()", _Mod) -> #{type => string, enum => [clientid, username]};
-typename_to_spec("term()", _Mod) -> #{type => string, example => "any"};
-typename_to_spec("boolean()", _Mod) -> #{type => boolean, example => true};
-typename_to_spec("binary()", _Mod) -> #{type => string, example => <<"binary-example">>};
-typename_to_spec("float()", _Mod) -> #{type => number, example => 3.14159};
-typename_to_spec("integer()", _Mod) -> #{type => integer, example => 100};
-typename_to_spec("non_neg_integer()", _Mod) -> #{type => integer, minimum => 1, example => 100};
-typename_to_spec("number()", _Mod) -> #{type => number, example => 42};
-typename_to_spec("string()", _Mod) -> #{type => string, example => <<"string-example">>};
-typename_to_spec("atom()", _Mod) -> #{type => string, example => atom};
+typename_to_spec("user_id_type()", _Mod) ->
+ #{type => string, enum => [clientid, username]};
+typename_to_spec("term()", _Mod) ->
+ #{type => string};
+typename_to_spec("boolean()", _Mod) ->
+ #{type => boolean};
+typename_to_spec("binary()", _Mod) ->
+ #{type => string};
+typename_to_spec("float()", _Mod) ->
+ #{type => number};
+typename_to_spec("integer()", _Mod) ->
+ #{type => integer};
+typename_to_spec("non_neg_integer()", _Mod) ->
+ #{type => integer, minimum => 0};
+typename_to_spec("pos_integer()", _Mod) ->
+ #{type => integer, minimum => 1};
+typename_to_spec("number()", _Mod) ->
+ #{type => number};
+typename_to_spec("string()", _Mod) ->
+ #{type => string};
+typename_to_spec("atom()", _Mod) ->
+ #{type => string};
typename_to_spec("epoch_second()", _Mod) ->
- #{<<"oneOf">> => [
- #{type => integer, example => 1640995200, description => <<"epoch-second">>},
- #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
- };
-typename_to_spec("epoch_millisecond()", _Mod) ->
- #{<<"oneOf">> => [
- #{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
- #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}]
+ #{
+ <<"oneOf">> => [
+ #{type => integer, example => 1640995200, description => <<"epoch-second">>},
+ #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
+ ]
};
-typename_to_spec("duration()", _Mod) -> #{type => string, example => <<"12m">>};
-typename_to_spec("duration_s()", _Mod) -> #{type => string, example => <<"1h">>};
-typename_to_spec("duration_ms()", _Mod) -> #{type => string, example => <<"32s">>};
-typename_to_spec("percent()", _Mod) -> #{type => number, example => <<"12%">>};
-typename_to_spec("file()", _Mod) -> #{type => string, example => <<"/path/to/file">>};
-typename_to_spec("ip_port()", _Mod) -> #{type => string, example => <<"127.0.0.1:80">>};
+typename_to_spec("epoch_millisecond()", _Mod) ->
+ #{
+ <<"oneOf">> => [
+ #{type => integer, example => 1640995200000, description => <<"epoch-millisecond">>},
+ #{type => string, example => <<"2022-01-01T00:00:00.000Z">>, format => <<"date-time">>}
+ ]
+ };
+typename_to_spec("duration()", _Mod) ->
+ #{type => string, example => <<"12m">>};
+typename_to_spec("duration_s()", _Mod) ->
+ #{type => string, example => <<"1h">>};
+typename_to_spec("duration_ms()", _Mod) ->
+ #{type => string, example => <<"32s">>};
+typename_to_spec("percent()", _Mod) ->
+ #{type => number, example => <<"12%">>};
+typename_to_spec("file()", _Mod) ->
+ #{type => string, example => <<"/path/to/file">>};
+typename_to_spec("ip_port()", _Mod) ->
+ #{type => string, example => <<"127.0.0.1:80">>};
typename_to_spec("ip_ports()", _Mod) ->
#{type => string, example => <<"127.0.0.1:80, 127.0.0.2:80">>};
-typename_to_spec("url()", _Mod) -> #{type => string, example => <<"http://127.0.0.1">>};
-typename_to_spec("connect_timeout()", Mod) -> typename_to_spec("timeout()", Mod);
-typename_to_spec("timeout()", _Mod) -> #{<<"oneOf">> => [#{type => string, example => infinity},
- #{type => integer, example => 100}], example => infinity};
-typename_to_spec("bytesize()", _Mod) -> #{type => string, example => <<"32MB">>};
-typename_to_spec("wordsize()", _Mod) -> #{type => string, example => <<"1024KB">>};
-typename_to_spec("map()", _Mod) -> #{type => object, example => #{}};
-typename_to_spec("#{" ++ _, Mod) -> typename_to_spec("map()", Mod);
-typename_to_spec("qos()", _Mod) -> #{type => string, enum => [0, 1, 2], example => 0};
-typename_to_spec("{binary(), binary()}", _Mod) -> #{type => object, example => #{}};
+typename_to_spec("url()", _Mod) ->
+ #{type => string, example => <<"http://127.0.0.1">>};
+typename_to_spec("connect_timeout()", Mod) ->
+ typename_to_spec("timeout()", Mod);
+typename_to_spec("timeout()", _Mod) ->
+ #{
+ <<"oneOf">> => [
+ #{type => string, example => infinity},
+ #{type => integer}
+ ],
+ example => infinity
+ };
+typename_to_spec("bytesize()", _Mod) ->
+ #{type => string, example => <<"32MB">>};
+typename_to_spec("wordsize()", _Mod) ->
+ #{type => string, example => <<"1024KB">>};
+typename_to_spec("map()", _Mod) ->
+ #{type => object, example => #{}};
+typename_to_spec("#{" ++ _, Mod) ->
+ typename_to_spec("map()", Mod);
+typename_to_spec("qos()", _Mod) ->
+ #{type => string, enum => [0, 1, 2]};
+typename_to_spec("{binary(), binary()}", _Mod) ->
+ #{type => object, example => #{}};
typename_to_spec("comma_separated_list()", _Mod) ->
#{type => string, example => <<"item1,item2">>};
typename_to_spec("comma_separated_atoms()", _Mod) ->
#{type => string, example => <<"item1,item2">>};
typename_to_spec("pool_type()", _Mod) ->
- #{type => string, enum => [random, hash], example => hash};
+ #{type => string, enum => [random, hash]};
typename_to_spec("log_level()", _Mod) ->
- #{ type => string,
- enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
+ #{
+ type => string,
+ enum => [debug, info, notice, warning, error, critical, alert, emergency, all]
};
typename_to_spec("rate()", _Mod) ->
#{type => string, example => <<"10M/s">>};
@@ -515,16 +675,18 @@ typename_to_spec(Name, Mod) ->
Spec2 = typerefl_array(Spec1, Name, Mod),
Spec3 = integer(Spec2, Name),
Spec3 =:= nomatch andalso
- throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
- Spec3.
+ throw({error, #{msg => <<"Unsupported Type">>, type => Name, module => Mod}}),
+ Spec3.
range(Name) ->
case string:split(Name, "..") of
- [MinStr, MaxStr] -> %% 1..10 1..inf -inf..10
+ %% 1..10 1..inf -inf..10
+ [MinStr, MaxStr] ->
Schema = #{type => integer},
Schema1 = add_integer_prop(Schema, minimum, MinStr),
add_integer_prop(Schema1, maximum, MaxStr);
- _ -> nomatch
+ _ ->
+ nomatch
end.
%% Module:Type
@@ -533,34 +695,39 @@ remote_module_type(nomatch, Name, Mod) ->
[_Module, Type] -> typename_to_spec(Type, Mod);
_ -> nomatch
end;
-remote_module_type(Spec, _Name, _Mod) -> Spec.
+remote_module_type(Spec, _Name, _Mod) ->
+ Spec.
%% [string()] or [integer()] or [xxx].
typerefl_array(nomatch, Name, Mod) ->
case string:trim(Name, leading, "[") of
- Name -> nomatch;
+ Name ->
+ nomatch;
Name1 ->
case string:trim(Name1, trailing, "]") of
- Name1 -> notmatch;
+ Name1 ->
+ notmatch;
Name2 ->
Schema = typename_to_spec(Name2, Mod),
#{type => array, items => Schema}
end
end;
-typerefl_array(Spec, _Name, _Mod) -> Spec.
+typerefl_array(Spec, _Name, _Mod) ->
+ Spec.
%% integer(1)
integer(nomatch, Name) ->
case string:to_integer(Name) of
- {Int, []} -> #{type => integer, enum => [Int], example => Int, default => Int};
+ {Int, []} -> #{type => integer, enum => [Int], default => Int};
_ -> nomatch
end;
-integer(Spec, _Name) -> Spec.
+integer(Spec, _Name) ->
+ Spec.
add_integer_prop(Schema, Key, Value) ->
case string:to_integer(Value) of
{error, no_integer} -> Schema;
- {Int, []}when Key =:= minimum -> Schema#{Key => Int, example => Int};
+ {Int, []} when Key =:= minimum -> Schema#{Key => Int};
{Int, []} -> Schema#{Key => Int}
end.
@@ -571,39 +738,53 @@ to_bin(List) when is_list(List) ->
end;
to_bin(Boolean) when is_boolean(Boolean) -> Boolean;
to_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8);
-to_bin(X) -> X.
+to_bin(X) ->
+ X.
parse_object(PropList = [_ | _], Module, Options) when is_list(PropList) ->
{Props, Required, Refs} =
- lists:foldl(fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
- NameBin = to_bin(Name),
- case hoconsc:is_schema(Hocon) of
- true ->
- HoconType = hocon_schema:field_schema(Hocon, type),
- Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
- SchemaToSpec = schema_converter(Options),
- Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
- {Prop, Refs1} = SchemaToSpec(HoconType, Module),
- NewRequiredAcc =
- case is_required(Hocon) of
- true -> [NameBin | RequiredAcc];
- false -> RequiredAcc
- end,
- {[{NameBin, maps:merge(Prop, Init)} | Acc], NewRequiredAcc, Refs1 ++ RefsAcc};
- false ->
- {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
- {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
- end
- end, {[], [], []}, PropList),
+ lists:foldl(
+ fun({Name, Hocon}, {Acc, RequiredAcc, RefsAcc}) ->
+ NameBin = to_bin(Name),
+ case hoconsc:is_schema(Hocon) of
+ true ->
+ HoconType = hocon_schema:field_schema(Hocon, type),
+ Init0 = init_prop([default | ?DEFAULT_FIELDS], #{}, Hocon),
+ SchemaToSpec = schema_converter(Options),
+ Init = trans_desc(Init0, Hocon, SchemaToSpec, NameBin),
+ {Prop, Refs1} = SchemaToSpec(HoconType, Module),
+ NewRequiredAcc =
+ case is_required(Hocon) of
+ true -> [NameBin | RequiredAcc];
+ false -> RequiredAcc
+ end,
+ {
+ [{NameBin, maps:merge(Prop, Init)} | Acc],
+ NewRequiredAcc,
+ Refs1 ++ RefsAcc
+ };
+ false ->
+ {SubObject, SubRefs} = parse_object(Hocon, Module, Options),
+ {[{NameBin, SubObject} | Acc], RequiredAcc, SubRefs ++ RefsAcc}
+ end
+ end,
+ {[], [], []},
+ PropList
+ ),
Object = #{<<"type">> => object, <<"properties">> => lists:reverse(Props)},
case Required of
[] -> {Object, Refs};
_ -> {maps:put(required, Required, Object), Refs}
end;
parse_object(Other, Module, Options) ->
- erlang:throw({error,
- #{msg => <<"Object only supports not empty proplists">>,
- args => Other, module => Module, options => Options}}).
+ erlang:throw(
+ {error, #{
+ msg => <<"Object only supports not empty proplists">>,
+ args => Other,
+ module => Module,
+ options => Options
+ }}
+ ).
is_required(Hocon) ->
hocon_schema:field_schema(Hocon, required) =:= true.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
index db587edbd..89f6f2199 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl
@@ -75,18 +75,13 @@ end_per_suite(_Config) ->
mria:stop().
set_special_configs(emqx_management) ->
- Listeners = [#{protocol => http, port => 8081}],
+ Listeners = #{http => #{port => 8081}},
Config = #{listeners => Listeners,
applications => [#{id => "admin", secret => "public"}]},
emqx_config:put([emqx_management], Config),
ok;
set_special_configs(emqx_dashboard) ->
- Listeners = [#{protocol => http, port => 18083}],
- Config = #{listeners => Listeners,
- default_username => <<"admin">>,
- default_password => <<"public">>
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
index ca40a5329..3ad44e3b8 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl
@@ -32,12 +32,22 @@ set_default_config() ->
set_default_config(<<"admin">>).
set_default_config(DefaultUsername) ->
- Config = #{listeners => [#{protocol => http,
- port => 18083}],
+ Config = #{listeners => #{
+ http => #{
+ port => 18083
+ }
+ },
default_username => DefaultUsername,
- default_password => <<"public">>
+ default_password => <<"public">>,
+ i18n_lang => en
},
emqx_config:put([dashboard], Config),
+ I18nFile = filename:join([
+ filename:dirname(code:priv_dir(emqx_dashboard)),
+ "etc",
+ "i18n.conf.all"
+ ]),
+ application:set_env(emqx_dashboard, i18n_file, I18nFile),
ok.
request(Method, Url) ->
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
index 341ba5691..878212c61 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl
@@ -35,15 +35,7 @@ init_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
index bed79b349..ff9926336 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl
@@ -35,15 +35,7 @@ init_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([emqx_dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
index b75ca412e..b7430e586 100644
--- a/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl
@@ -40,15 +40,7 @@ end_per_suite(Config) ->
Config.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
diff --git a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
index 3b54c54c4..4abfa136a 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl
@@ -4,6 +4,7 @@
%% API
-export([paths/0, api_spec/0, schema/1, fields/1]).
+-export([init_per_suite/1, end_per_suite/1]).
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
@@ -26,6 +27,27 @@ groups() -> [
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
].
+init_per_suite(Config) ->
+ mria:start(),
+ application:load(emqx_dashboard),
+ emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
+ emqx_dashboard:init_i18n(),
+ Config.
+
+set_special_configs(emqx_dashboard) ->
+ emqx_dashboard_api_test_helpers:set_default_config(),
+ ok;
+set_special_configs(_) ->
+ ok.
+
+end_per_suite(Config) ->
+ end_suite(),
+ Config.
+
+end_suite() ->
+ application:unload(emqx_management),
+ emqx_common_test_helpers:stop_apps([emqx_dashboard]).
+
t_in_path(_Config) ->
Expect =
[#{description => <<"Indicates which sorts of issues to return">>,
@@ -40,9 +62,9 @@ t_in_query(_Config) ->
Expect =
[#{description => <<"results per page (max 100)">>,
example => 1, in => query, name => per_page,
- schema => #{example => 1, maximum => 100, minimum => 1, type => integer}},
+ schema => #{maximum => 100, minimum => 1, type => integer}},
#{description => <<"QOS">>, in => query, name => qos,
- schema => #{enum => [0, 1, 2], example => 0, type => string}}],
+ schema => #{enum => [0, 1, 2], type => string}}],
validate("/test/in/query", Expect),
ok.
@@ -74,12 +96,12 @@ t_public_ref(_Config) ->
], Refs),
ExpectRefs = [
#{<<"public.limit">> => #{description => <<"Results per page(max 1000)">>,
- example => 50,in => query,name => limit,
- schema => #{default => 100,example => 1,maximum => 1000,
+ in => query,name => limit, example => 50,
+ schema => #{default => 100,maximum => 1000,
minimum => 1,type => integer}}},
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
- example => 1,in => query,name => page,
- schema => #{default => 1,example => 100,type => integer}}}],
+ in => query,name => page,example => 1,
+ schema => #{default => 1,minimum => 1,type => integer}}}],
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})),
ok.
@@ -92,11 +114,11 @@ t_in_mix(_Config) ->
example => <<"12m">>,in => path,name => state,required => true,
schema => #{example => <<"1h">>,type => string}},
#{example => 10,in => query,name => per_page, required => false,
- schema => #{default => 5,example => 1,maximum => 50,minimum => 1, type => integer}},
- #{in => query,name => is_admin, schema => #{example => true,type => boolean}},
+ schema => #{default => 5,maximum => 50,minimum => 1, type => integer}},
+ #{in => query,name => is_admin, schema => #{type => boolean}},
#{in => query,name => timeout,
schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
- #{example => 30,maximum => 60,minimum => 30, type => integer}]}}],
+ #{maximum => 60,minimum => 30, type => integer}]}}],
ExpectMeta = #{
tags => [tags, good],
description => <<"good description">>,
@@ -116,15 +138,15 @@ t_without_in(_Config) ->
t_require(_Config) ->
ExpectSpec = [#{
in => query,name => userid, required => false,
- schema => #{example => <<"binary-example">>, type => string}}],
+ schema => #{type => string}}],
validate("/required/false", ExpectSpec),
ok.
t_nullable(_Config) ->
NullableFalse = [#{in => query,name => userid, required => true,
- schema => #{example => <<"binary-example">>, type => string}}],
+ schema => #{type => string}}],
NullableTrue = [#{in => query,name => userid,
- schema => #{example => <<"binary-example">>, type => string}, required => false}],
+ schema => #{type => string}, required => false}],
validate("/nullable/false", NullableFalse),
validate("/nullable/true", NullableTrue),
ok.
diff --git a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
index bf48046c2..bfbb5e95e 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl
@@ -3,41 +3,36 @@
-behaviour(minirest_api).
-behaviour(hocon_schema).
-%% API
--export([paths/0, api_spec/0, schema/1, fields/1]).
--export([t_object/1, t_nest_object/1, t_api_spec/1,
- t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1,
- t_ref_array_with_key/1, t_ref_array_without_key/1, t_sub_fields/1
-]).
--export([
- t_object_trans/1, t_object_notrans/1, t_nest_object_trans/1, t_local_ref_trans/1,
- t_remote_ref_trans/1, t_nest_ref_trans/1,
- t_ref_array_with_key_trans/1, t_ref_array_without_key_trans/1,
- t_ref_trans_error/1, t_object_trans_error/1
-]).
--export([all/0, suite/0, groups/0]).
+-compile(nowarn_export_all).
+-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
-all() -> [{group, spec}, {group, validation}].
+all() -> emqx_common_test_helpers:all(?MODULE).
-suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
- {spec, [parallel], [
- t_api_spec, t_object, t_nest_object,
- t_local_ref, t_remote_ref, t_bad_ref, t_none_ref,
- t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]},
- {validation, [parallel],
- [
- t_object_trans, t_object_notrans, t_local_ref_trans, t_remote_ref_trans,
- t_ref_array_with_key_trans, t_ref_array_without_key_trans, t_nest_ref_trans,
- t_ref_trans_error, t_object_trans_error
- %% t_nest_object_trans,
- ]}
-].
+init_per_suite(Config) ->
+ mria:start(),
+ application:load(emqx_dashboard),
+ emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
+ emqx_dashboard:init_i18n(),
+ Config.
+
+set_special_configs(emqx_dashboard) ->
+ emqx_dashboard_api_test_helpers:set_default_config(),
+ ok;
+set_special_configs(_) ->
+ ok.
+
+end_per_suite(Config) ->
+ end_suite(),
+ Config.
+
+end_suite() ->
+ application:unload(emqx_management),
+ emqx_common_test_helpers:stop_apps([emqx_dashboard]).
t_object(_Config) ->
Spec = #{
@@ -48,7 +43,7 @@ t_object(_Config) ->
#{required => [<<"timeout">>, <<"per_page">>],
<<"properties">> =>[
{<<"per_page">>, #{description => <<"good per page desc">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}]}},
@@ -69,13 +64,13 @@ t_nest_object(_Config) ->
#{required => [<<"timeout">>],
<<"properties">> =>
[{<<"per_page">>, #{description => <<"good per page desc">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}]}},
{<<"nest_object">>,
#{<<"properties">> =>
- [{<<"good_nest_1">>, #{example => 100, type => integer}},
+ [{<<"good_nest_1">>, #{type => integer}},
{<<"good_nest_2">>, #{<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
<<"type">> => object}},
@@ -110,7 +105,7 @@ t_remote_ref(_Config) ->
{_, Components} = validate("/ref/remote", Spec, Refs),
ExpectComponents = [
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"properties">> => [
- {<<"page">>, #{description => <<"good page">>,example => 1,
+ {<<"page">>, #{description => <<"good page">>,
maximum => 100,minimum => 1,type => integer}},
{<<"another_ref">>, #{<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
@@ -141,8 +136,7 @@ t_nest_ref(_Config) ->
{<<"webhook-host">>, #{default => <<"127.0.0.1:80">>,
example => <<"127.0.0.1:80">>,type => string}},
{<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
- {<<"tag">>, #{description => <<"tag">>,
- example => <<"binary-example">>,type => string}}],
+ {<<"tag">>, #{description => <<"tag">>,type => string}}],
<<"type">> => object}}]),
{_, Components} = validate("/ref/nest/ref", Spec, Refs),
?assertEqual(ExpectComponents, Components),
@@ -186,7 +180,7 @@ t_ref_array_with_key(_Config) ->
<<"type">> => object, <<"properties">> =>
[
{<<"per_page">>, #{description => <<"good per page desc">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}]}},
@@ -281,7 +275,7 @@ t_object_notrans(_Config) ->
?assertEqual(Body, ActualBody),
ok.
-t_nest_object_trans(_Config) ->
+todo_t_nest_object_check(_Config) ->
Path = "/nest/object",
Body = #{
<<"timeout">> => "10m",
@@ -306,7 +300,7 @@ t_nest_object_trans(_Config) ->
body => #{<<"per_page">> => 10,
<<"timeout">> => 600}
},
- {ok, NewRequest} = trans_requestBody(Path, Body),
+ {ok, NewRequest} = check_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
@@ -487,6 +481,10 @@ trans_requestBody(Path, Body) ->
trans_requestBody(Path, Body,
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
+check_requestBody(Path, Body) ->
+ trans_requestBody(Path, Body,
+ fun emqx_dashboard_swagger:filter_check_request/2).
+
trans_requestBody(Path, Body, Filter) ->
Meta = #{module => ?MODULE, method => post, path => Path},
Request = #{bindings => #{}, query_string => #{}, body => Body},
diff --git a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
index 3b68c8eff..72f99ea52 100644
--- a/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
+++ b/apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl
@@ -10,22 +10,31 @@
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
--export([all/0, suite/0, groups/0]).
--export([paths/0, api_spec/0, schema/1, fields/1]).
--export([t_simple_binary/1, t_object/1, t_nest_object/1, t_empty/1, t_error/1,
- t_raw_local_ref/1, t_raw_remote_ref/1, t_hocon_schema_function/1, t_complicated_type/1,
- t_local_ref/1, t_remote_ref/1, t_bad_ref/1, t_none_ref/1, t_nest_ref/1, t_sub_fields/1,
- t_ref_array_with_key/1, t_ref_array_without_key/1, t_api_spec/1]).
+-compile(nowarn_export_all).
+-compile(export_all).
-all() -> [{group, spec}].
-suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
- {spec, [parallel], [
- t_api_spec, t_simple_binary, t_object, t_nest_object, t_error, t_complicated_type,
- t_raw_local_ref, t_raw_remote_ref, t_empty, t_hocon_schema_function,
- t_local_ref, t_remote_ref, t_bad_ref, t_none_ref, t_sub_fields,
- t_ref_array_with_key, t_ref_array_without_key, t_nest_ref]}
-].
+all() -> emqx_common_test_helpers:all(?MODULE).
+
+init_per_suite(Config) ->
+ mria:start(),
+ application:load(emqx_dashboard),
+ emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
+ emqx_dashboard:init_i18n(),
+ Config.
+
+set_special_configs(emqx_dashboard) ->
+ emqx_dashboard_api_test_helpers:set_default_config(),
+ ok;
+set_special_configs(_) ->
+ ok.
+
+end_per_suite(Config) ->
+ end_suite(),
+ Config.
+
+end_suite() ->
+ application:unload(emqx_management),
+ emqx_common_test_helpers:stop_apps([emqx_dashboard]).
t_simple_binary(_config) ->
Path = "/simple/bin",
@@ -41,7 +50,7 @@ t_object(_config) ->
#{<<"schema">> => #{required => [<<"timeout">>, <<"per_page">>],
<<"properties">> => [
{<<"per_page">>, #{description => <<"good per page desc">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
{<<"inner_ref">>, #{<<"$ref">> =>
@@ -58,16 +67,15 @@ t_error(_Config) ->
<<"properties">> =>
[
{<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
- {<<"message">>, #{description => <<"Details description of the error.">>,
- example => <<"Bad request desc">>, type => string}}]
+ {<<"message">>, #{description => <<"Bad request desc">>, type => string}}]
}}}},
Error404 = #{<<"content">> =>
#{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
<<"properties">> =>
[
{<<"code">>, #{enum => ['Not-Found'], type => string}},
- {<<"message">>, #{description => <<"Details description of the error.">>,
- example => <<"Error code to troubleshoot problems.">>, type => string}}]
+ {<<"message">>, #{
+ description => <<"Error code to troubleshoot problems.">>, type => string}}]
}}}},
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId),
@@ -83,12 +91,12 @@ t_nest_object(_Config) ->
Object =
#{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
#{required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
- {<<"per_page">>, #{description => <<"good per page desc">>, example => 1,
+ {<<"per_page">>, #{description => <<"good per page desc">>,
maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
{<<"nest_object">>, #{<<"type">> => object, <<"properties">> => [
- {<<"good_nest_1">>, #{example => 100, type => integer}},
+ {<<"good_nest_1">>, #{type => integer}},
{<<"good_nest_2">>, #{<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}
}]}},
@@ -176,16 +184,15 @@ t_complicated_type(_Config) ->
Object = #{<<"content">> => #{<<"application/json">> =>
#{<<"schema">> => #{<<"properties">> =>
[
- {<<"no_neg_integer">>, #{example => 100, minimum => 1, type => integer}},
+ {<<"no_neg_integer">>, #{minimum => 0, type => integer}},
{<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
{<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
{<<"connect_timeout">>, #{example => infinity, <<"oneOf">> => [
#{example => infinity, type => string},
- #{example => 100, type => integer}]}},
- {<<"pool_type">>, #{enum => [random, hash], example => hash, type => string}},
+ #{type => integer}]}},
+ {<<"pool_type">>, #{enum => [random, hash], type => string}},
{<<"timeout">>, #{example => infinity,
- <<"oneOf">> =>
- [#{example => infinity, type => string}, #{example => 100, type => integer}]}},
+ <<"oneOf">> => [#{example => infinity, type => string}, #{type => integer}]}},
{<<"bytesize">>, #{example => <<"32MB">>, type => string}},
{<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
{<<"maps">>, #{example => #{}, type => object}},
@@ -194,7 +201,7 @@ t_complicated_type(_Config) ->
{<<"log_level">>,
#{enum => [debug, info, notice, warning, error, critical, alert, emergency, all],
type => string}},
- {<<"fix_integer">>, #{default => 100, enum => [100], example => 100,type => integer}}
+ {<<"fix_integer">>, #{default => 100, enum => [100],type => integer}}
],
<<"type">> => object}}}},
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
@@ -210,17 +217,16 @@ t_ref_array_with_key(_Config) ->
Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
{<<"per_page">>, #{description => <<"good per page desc">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"timeout">>, #{default => 5, <<"oneOf">> =>
[#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
- {<<"assert">>, #{description => <<"money">>, example => 3.14159, type => number}},
- {<<"number_ex">>, #{description => <<"number example">>,
- example => 42, type => number}},
+ {<<"assert">>, #{description => <<"money">>, type => number}},
+ {<<"number_ex">>, #{description => <<"number example">>, type => number}},
{<<"percent_ex">>, #{description => <<"percent example">>,
example => <<"12%">>, type => number}},
{<<"duration_ms_ex">>, #{description => <<"duration ms example">>,
example => <<"32s">>, type => string}},
- {<<"atom_ex">>, #{description => <<"atom ex">>, example => atom, type => string}},
+ {<<"atom_ex">>, #{description => <<"atom ex">>, type => string}},
{<<"array_refs">>, #{items => #{<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}, type => array}}
]}
@@ -245,12 +251,12 @@ t_hocon_schema_function(_Config) ->
#{<<"emqx_swagger_remote_schema.ref1">> => #{<<"type">> => object,
<<"properties">> => [
{<<"protocol">>, #{enum => [http, https], type => string}},
- {<<"port">>, #{default => 18083, example => 100, type => integer}}]
+ {<<"port">>, #{default => 18083, type => integer}}]
}},
#{<<"emqx_swagger_remote_schema.ref2">> => #{<<"type">> => object,
<<"properties">> => [
{<<"page">>, #{description => <<"good page">>,
- example => 1, maximum => 100, minimum => 1, type => integer}},
+ maximum => 100, minimum => 1, type => integer}},
{<<"another_ref">>, #{<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}
]
@@ -270,9 +276,9 @@ t_hocon_schema_function(_Config) ->
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]},
type => array}},
{<<"default_username">>,
- #{default => <<"admin">>, example => <<"string-example">>, type => string}},
+ #{default => <<"admin">>, type => string}},
{<<"default_password">>,
- #{default => <<"public">>, example => <<"string-example">>, type => string}},
+ #{default => <<"public">>, type => string}},
{<<"sample_interval">>,
#{default => <<"10s">>, example => <<"1h">>, type => string}},
{<<"token_expired_time">>,
diff --git a/apps/emqx_exhook/src/emqx_exhook_api.erl b/apps/emqx_exhook/src/emqx_exhook_api.erl
index 27f8203ba..cf460d546 100644
--- a/apps/emqx_exhook/src/emqx_exhook_api.erl
+++ b/apps/emqx_exhook/src/emqx_exhook_api.erl
@@ -170,7 +170,7 @@ fields(node_metrics) ->
fields(node_status) ->
[
{node, mk(string(), #{})},
- {status, mk(enum([running, waiting, stopped, error]), #{})}
+ {status, mk(enum([connected, connecting, unconnected, disable, error]), #{})}
];
fields(hook_info) ->
[
diff --git a/apps/emqx_exhook/src/emqx_exhook_mgr.erl b/apps/emqx_exhook/src/emqx_exhook_mgr.erl
index f11b91ae4..1ccebb816 100644
--- a/apps/emqx_exhook/src/emqx_exhook_mgr.erl
+++ b/apps/emqx_exhook/src/emqx_exhook_mgr.erl
@@ -63,21 +63,25 @@
-export([roots/0]).
%% Running servers
--type state() :: #{
- running := servers(),
- %% Wait to reload servers
- waiting := servers(),
- %% Marked stopped servers
- stopped := servers(),
- %% Timer references
- trefs := map(),
- orders := orders()
-}.
+-type state() :: #{servers := servers()}.
--type server_name() :: binary().
--type servers() :: #{server_name() => server()}.
--type server() :: server_options().
-type server_options() :: map().
+-type server_name() :: binary().
+
+-type status() ::
+ connected
+ | connecting
+ | unconnected
+ | disable.
+
+-type server() :: #{
+ status := status(),
+ timer := reference(),
+ order := integer(),
+ %% include the content of server_options
+ atom() => any()
+}.
+-type servers() :: #{server_name() => server()}.
-type position() ::
front
@@ -85,19 +89,10 @@
| {before, binary()}
| {'after', binary()}.
--type orders() :: #{server_name() => integer()}.
-
--type server_info() :: #{
- name := server_name(),
- status := running | waiting | stopped,
-
- atom() => term()
-}.
-
-define(DEFAULT_TIMEOUT, 60000).
-define(REFRESH_INTERVAL, timer:seconds(5)).
--export_type([servers/0, server/0, server_info/0]).
+-export_type([servers/0, server/0]).
%%--------------------------------------------------------------------
%% APIs
@@ -113,7 +108,7 @@ start_link() ->
list() ->
call(list).
--spec lookup(server_name()) -> not_found | server_info().
+-spec lookup(server_name()) -> not_found | server().
lookup(Name) ->
call({lookup, Name}).
@@ -195,104 +190,56 @@ init([]) ->
process_flag(trap_exit, true),
emqx_conf:add_handler([exhook, servers], ?MODULE),
ServerL = emqx:get_config([exhook, servers]),
- {Waiting, Running, Stopped} = load_all_servers(ServerL),
- Orders = reorder(ServerL),
+ Servers = load_all_servers(ServerL),
+ Servers2 = reorder(ServerL, Servers),
refresh_tick(),
- {ok,
- ensure_reload_timer(
- #{
- waiting => Waiting,
- running => Running,
- stopped => Stopped,
- trefs => #{},
- orders => Orders
- }
- )}.
+ {ok, #{servers => Servers2}}.
--spec load_all_servers(list(server_options())) -> {servers(), servers(), servers()}.
+-spec load_all_servers(list(server_options())) -> servers().
load_all_servers(ServerL) ->
- load_all_servers(ServerL, #{}, #{}, #{}).
+ load_all_servers(ServerL, #{}).
-load_all_servers([#{name := Name} = Options | More], Waiting, Running, Stopped) ->
- case emqx_exhook_server:load(Name, Options) of
- {ok, ServerState} ->
- save(Name, ServerState),
- load_all_servers(More, Waiting, Running#{Name => Options}, Stopped);
- {error, _} ->
- load_all_servers(More, Waiting#{Name => Options}, Running, Stopped);
- disable ->
- load_all_servers(More, Waiting, Running, Stopped#{Name => Options})
- end;
-load_all_servers([], Waiting, Running, Stopped) ->
- {Waiting, Running, Stopped}.
+load_all_servers([#{name := Name} = Options | More], Servers) ->
+ {_, Server} = do_load_server(options_to_server(Options)),
+ load_all_servers(More, Servers#{Name => Server});
+load_all_servers([], Servers) ->
+ Servers.
handle_call(
list,
_From,
- State = #{
- running := Running,
- waiting := Waiting,
- stopped := Stopped,
- orders := Orders
- }
+ State = #{servers := Servers}
) ->
- R = get_servers_info(running, Running),
- W = get_servers_info(waiting, Waiting),
- S = get_servers_info(stopped, Stopped),
-
- Servers = R ++ W ++ S,
- OrderServers = sort_name_by_order(Servers, Orders),
-
+ Infos = get_servers_info(Servers),
+ OrderServers = sort_name_by_order(Infos, Servers),
{reply, OrderServers, State};
handle_call(
{update_config, {move, _Name, _Position}, NewConfL},
_From,
- State
+ #{servers := Servers} = State
) ->
- Orders = reorder(NewConfL),
- {reply, ok, State#{orders := Orders}};
+ Servers2 = reorder(NewConfL, Servers),
+ {reply, ok, State#{servers := Servers2}};
handle_call({update_config, {delete, ToDelete}, _}, _From, State) ->
- {ok,
- #{
- orders := Orders,
- stopped := Stopped
- } = State2} = do_unload_server(ToDelete, State),
-
- State3 = State2#{
- stopped := maps:remove(ToDelete, Stopped),
- orders := maps:remove(ToDelete, Orders)
- },
-
emqx_exhook_metrics:on_server_deleted(ToDelete),
- {reply, ok, State3};
+ #{servers := Servers} = State2 = do_unload_server(ToDelete, State),
+
+ Servers2 = maps:remove(ToDelete, Servers),
+
+ {reply, ok, update_servers(Servers2, State2)};
handle_call(
{update_config, {add, RawConf}, NewConfL},
_From,
- #{running := Running, waiting := Waitting, stopped := Stopped} = State
+ #{servers := Servers} = State
) ->
{_, #{name := Name} = Conf} = emqx_config:check_config(?MODULE, RawConf),
-
- case emqx_exhook_server:load(Name, Conf) of
- {ok, ServerState} ->
- save(Name, ServerState),
- State2 = State#{running := Running#{Name => Conf}};
- {error, _} ->
- StateT = State#{waiting := Waitting#{Name => Conf}},
- State2 = ensure_reload_timer(StateT);
- disable ->
- State2 = State#{stopped := Stopped#{Name => Conf}}
- end,
- Orders = reorder(NewConfL),
- {reply, ok, State2#{orders := Orders}};
+ {Result, Server} = do_load_server(options_to_server(Conf)),
+ Servers2 = Servers#{Name => Server},
+ Servers3 = reorder(NewConfL, Servers2),
+ {reply, Result, State#{servers := Servers3}};
handle_call({lookup, Name}, _From, State) ->
- case where_is_server(Name, State) of
- not_found ->
- Result = not_found;
- {Where, #{Name := Conf}} ->
- Result = maps:merge(Conf, #{status => Where})
- end,
- {reply, Result, State};
+ {reply, where_is_server(Name, State), State};
handle_call({update_config, {update, Name, _Conf}, NewConfL}, _From, State) ->
{Result, State2} = restart_server(Name, NewConfL, State),
{reply, Result, State2};
@@ -303,7 +250,7 @@ handle_call({server_info, Name}, _From, State) ->
case where_is_server(Name, State) of
not_found ->
Result = not_found;
- {Status, _} ->
+ #{status := Status} ->
HooksMetrics = emqx_exhook_metrics:server_metrics(Name),
Result = #{
status => Status,
@@ -314,25 +261,9 @@ handle_call({server_info, Name}, _From, State) ->
handle_call(
all_servers_info,
_From,
- #{
- running := Running,
- waiting := Waiting,
- stopped := Stopped
- } = State
+ #{servers := Servers} = State
) ->
- MakeStatus = fun(Status, Servers, Acc) ->
- lists:foldl(
- fun(Name, IAcc) -> IAcc#{Name => Status} end,
- Acc,
- maps:keys(Servers)
- )
- end,
- Status = lists:foldl(
- fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end,
- #{},
- [{running, Running}, {waiting, Waiting}, {stopped, Stopped}]
- ),
-
+ Status = maps:map(fun(_Name, #{status := Status}) -> Status end, Servers),
Metrics = emqx_exhook_metrics:servers_metrics(),
Result = #{
@@ -352,23 +283,8 @@ handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({timeout, _Ref, {reload, Name}}, State) ->
- {Result, NState} = do_load_server(Name, State),
- case Result of
- ok ->
- {noreply, NState};
- {error, not_found} ->
- {noreply, NState};
- {error, Reason} ->
- ?SLOG(
- warning,
- #{
- msg => "failed_to_reload_exhook_callback_server",
- reason => Reason,
- name => Name
- }
- ),
- {noreply, ensure_reload_timer(NState)}
- end;
+ {_, NState} = do_reload_server(Name, State),
+ {noreply, NState};
handle_info(refresh_tick, State) ->
refresh_tick(),
emqx_exhook_metrics:update(?REFRESH_INTERVAL),
@@ -376,14 +292,13 @@ handle_info(refresh_tick, State) ->
handle_info(_Info, State) ->
{noreply, State}.
-terminate(_Reason, State = #{running := Running}) ->
+terminate(_Reason, State = #{servers := Servers}) ->
_ = maps:fold(
fun(Name, _, AccIn) ->
- {ok, NAccIn} = do_unload_server(Name, AccIn),
- NAccIn
+ do_unload_server(Name, AccIn)
end,
State,
- Running
+ Servers
),
_ = unload_exhooks(),
ok.
@@ -401,122 +316,83 @@ unload_exhooks() ->
|| {Name, {M, F, _A}} <- ?ENABLED_HOOKS
].
--spec do_load_server(server_name(), state()) ->
- {{error, not_found}, state()}
- | {{error, already_started}, state()}
- | {ok, state()}.
-do_load_server(Name, State = #{orders := Orders}) ->
- case where_is_server(Name, State) of
- not_found ->
- {{error, not_found}, State};
- {running, _} ->
- {ok, State};
- {Where, Map} ->
- State2 = clean_reload_timer(Name, State),
- {Options, Map2} = maps:take(Name, Map),
- State3 = State2#{Where := Map2},
- #{
- running := Running,
- stopped := Stopped
- } = State3,
- case emqx_exhook_server:load(Name, Options) of
- {ok, ServerState} ->
- save(Name, ServerState),
- update_order(Orders),
- ?SLOG(info, #{
- msg => "load_exhook_callback_server_ok",
- name => Name
- }),
- {ok, State3#{running := maps:put(Name, Options, Running)}};
- {error, Reason} ->
- {{error, Reason}, State};
- disable ->
- {ok, State3#{stopped := Stopped#{Name => Options}}}
+do_load_server(#{name := Name} = Server) ->
+ case emqx_exhook_server:load(Name, Server) of
+ {ok, ServerState} ->
+ save(Name, ServerState),
+ {ok, Server#{status => connected}};
+ disable ->
+ {ok, set_disable(Server)};
+ {ErrorType, Reason} = Error ->
+ ?SLOG(
+ error,
+ #{
+ msg => "failed_to_load_exhook_callback_server",
+ reason => Reason,
+ name => Name
+ }
+ ),
+ case ErrorType of
+ load_error ->
+ {ok, ensure_reload_timer(Server)};
+ _ ->
+ {Error, Server#{status => unconnected}}
end
end.
--spec do_unload_server(server_name(), state()) -> {ok, state()}.
-do_unload_server(Name, #{stopped := Stopped} = State) ->
+do_load_server(#{name := Name} = Server, #{servers := Servers} = State) ->
+ {Result, Server2} = do_load_server(Server),
+ {Result, update_servers(Servers#{Name => Server2}, State)}.
+
+-spec do_reload_server(server_name(), state()) ->
+ {{error, term()}, state()}
+ | {ok, state()}.
+do_reload_server(Name, State = #{servers := Servers}) ->
case where_is_server(Name, State) of
- {stopped, _} ->
- {ok, State};
- {waiting, Waiting} ->
- {Options, Waiting2} = maps:take(Name, Waiting),
- {ok,
- clean_reload_timer(
- Name,
- State#{
- waiting := Waiting2,
- stopped := maps:put(Name, Options, Stopped)
- }
- )};
- {running, Running} ->
- Service = server(Name),
- ok = unsave(Name),
- ok = emqx_exhook_server:unload(Service),
- {Options, Running2} = maps:take(Name, Running),
- {ok, State#{
- running := Running2,
- stopped := maps:put(Name, Options, Stopped)
- }};
not_found ->
- {ok, State}
+ {{error, not_found}, State};
+ #{timer := undefined} ->
+ {ok, State};
+ Server ->
+ clean_reload_timer(Server),
+ do_load_server(Server, Servers)
end.
--spec ensure_reload_timer(state()) -> state().
-ensure_reload_timer(
- State = #{
- waiting := Waiting,
- stopped := Stopped,
- trefs := TRefs
- }
-) ->
- Iter = maps:iterator(Waiting),
-
- {Waitting2, Stopped2, TRefs2} =
- ensure_reload_timer(maps:next(Iter), Waiting, Stopped, TRefs),
-
- State#{
- waiting := Waitting2,
- stopped := Stopped2,
- trefs := TRefs2
- }.
-
-ensure_reload_timer(none, Waiting, Stopped, TimerRef) ->
- {Waiting, Stopped, TimerRef};
-ensure_reload_timer(
- {Name, #{auto_reconnect := Intv}, Iter},
- Waiting,
- Stopped,
- TimerRef
-) when is_integer(Intv) ->
- Next = maps:next(Iter),
- case maps:is_key(Name, TimerRef) of
- true ->
- ensure_reload_timer(Next, Waiting, Stopped, TimerRef);
- _ ->
- Ref = erlang:start_timer(Intv, self(), {reload, Name}),
- TimerRef2 = maps:put(Name, Ref, TimerRef),
- ensure_reload_timer(Next, Waiting, Stopped, TimerRef2)
- end;
-ensure_reload_timer({Name, Opts, Iter}, Waiting, Stopped, TimerRef) ->
- ensure_reload_timer(
- maps:next(Iter),
- maps:remove(Name, Waiting),
- maps:put(Name, Opts, Stopped),
- TimerRef
- ).
-
--spec clean_reload_timer(server_name(), state()) -> state().
-clean_reload_timer(Name, State = #{trefs := TRefs}) ->
- case maps:take(Name, TRefs) of
- error ->
+-spec do_unload_server(server_name(), state()) -> state().
+do_unload_server(Name, #{servers := Servers} = State) ->
+ case where_is_server(Name, State) of
+ not_found ->
State;
- {TRef, NTRefs} ->
- _ = erlang:cancel_timer(TRef),
- State#{trefs := NTRefs}
+ #{status := disable} ->
+ State;
+ Server ->
+ clean_reload_timer(Server),
+ case server(Name) of
+ undefined ->
+ State;
+ Service ->
+ ok = unsave(Name),
+ ok = emqx_exhook_server:unload(Service),
+ Servers2 = Servers#{Name := set_disable(Server)},
+ State#{servers := Servers2}
+ end
end.
+ensure_reload_timer(#{timer := Timer} = Server) when is_reference(Timer) ->
+ Server;
+ensure_reload_timer(#{name := Name, auto_reconnect := Intv} = Server) when is_integer(Intv) ->
+ Ref = erlang:start_timer(Intv, self(), {reload, Name}),
+ Server#{status := connecting, timer := Ref};
+ensure_reload_timer(Server) ->
+ Server#{status := unconnected}.
+
+-spec clean_reload_timer(server()) -> ok.
+clean_reload_timer(#{timer := undefined}) ->
+ ok;
+clean_reload_timer(#{timer := Timer}) ->
+ _ = erlang:cancel_timer(Timer),
+ ok.
+
-spec do_move(binary(), position(), list(server_options())) ->
not_found | list(server_options()).
do_move(Name, Position, ConfL) ->
@@ -545,37 +421,32 @@ move_to([H | T], Position, Server, HeadL) ->
move_to([], _Position, _Server, _HeadL) ->
not_found.
--spec reorder(list(server_options())) -> orders().
-reorder(ServerL) ->
- Orders = reorder(ServerL, 1, #{}),
+-spec reorder(list(server_options()), servers()) -> servers().
+reorder(ServerL, Servers) ->
+ Orders = reorder(ServerL, 1, Servers),
update_order(Orders),
Orders.
-reorder([#{name := Name} | T], Order, Orders) ->
- reorder(T, Order + 1, Orders#{Name => Order});
-reorder([], _Order, Orders) ->
- Orders.
+reorder([#{name := Name} | T], Order, Servers) ->
+ reorder(T, Order + 1, update_order(Name, Servers, Order));
+reorder([], _Order, Servers) ->
+ Servers.
-get_servers_info(Status, Map) ->
+update_order(Name, Servers, Order) ->
+ Server = maps:get(Name, Servers),
+ Servers#{Name := Server#{order := Order}}.
+
+get_servers_info(Svrs) ->
Fold = fun(Name, Conf, Acc) ->
[
- maps:merge(Conf, #{
- status => Status,
- hooks => hooks(Name)
- })
+ maps:merge(Conf, #{hooks => hooks(Name)})
| Acc
]
end,
- maps:fold(Fold, [], Map).
+ maps:fold(Fold, [], Svrs).
-where_is_server(Name, #{running := Running}) when is_map_key(Name, Running) ->
- {running, Running};
-where_is_server(Name, #{waiting := Waiting}) when is_map_key(Name, Waiting) ->
- {waiting, Waiting};
-where_is_server(Name, #{stopped := Stopped}) when is_map_key(Name, Stopped) ->
- {stopped, Stopped};
-where_is_server(_, _) ->
- not_found.
+where_is_server(Name, #{servers := Servers}) ->
+ maps:get(Name, Servers, not_found).
-type replace_fun() :: fun((server_options()) -> server_options()).
@@ -604,15 +475,10 @@ restart_server(Name, ConfL, State) ->
case where_is_server(Name, State) of
not_found ->
{{error, not_found}, State};
- {Where, Map} ->
- State2 = State#{Where := Map#{Name := Conf}},
- {ok, State3} = do_unload_server(Name, State2),
- case do_load_server(Name, State3) of
- {ok, State4} ->
- {ok, State4};
- {Error, State4} ->
- {Error, State4}
- end
+ Server ->
+ Server2 = maps:merge(Server, Conf),
+ State2 = do_unload_server(Name, State),
+ do_load_server(Server2, State2)
end
end.
@@ -620,9 +486,11 @@ sort_name_by_order(Names, Orders) ->
lists:sort(
fun
(A, B) when is_binary(A) ->
- maps:get(A, Orders) < maps:get(B, Orders);
+ emqx_map_lib:deep_get([A, order], Orders) <
+ emqx_map_lib:deep_get([B, order], Orders);
(#{name := A}, #{name := B}) ->
- maps:get(A, Orders) < maps:get(B, Orders)
+ emqx_map_lib:deep_get([A, order], Orders) <
+ emqx_map_lib:deep_get([B, order], Orders)
end,
Names
).
@@ -630,6 +498,16 @@ sort_name_by_order(Names, Orders) ->
refresh_tick() ->
erlang:send_after(?REFRESH_INTERVAL, self(), ?FUNCTION_NAME).
+options_to_server(Options) ->
+ maps:merge(Options, #{status => unconnected, timer => undefined, order => 0}).
+
+update_servers(Servers, State) ->
+ update_order(Servers),
+ State#{servers := Servers}.
+
+set_disable(Server) ->
+ Server#{status := disable, timer := undefined}.
+
%%--------------------------------------------------------------------
%% Server state persistent
save(Name, ServerState) ->
@@ -661,8 +539,17 @@ server(Name) ->
Service -> Service
end.
-update_order(Orders) ->
+update_order(Servers) ->
Running = running(),
+ Orders = maps:filter(
+ fun
+ (Name, #{status := connected}) ->
+ lists:member(Name, Running);
+ (_, _) ->
+ false
+ end,
+ Servers
+ ),
Running2 = sort_name_by_order(Running, Orders),
persistent_term:put(?APP, Running2).
diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl
index 06a98e920..101a08fa2 100644
--- a/apps/emqx_exhook/src/emqx_exhook_schema.erl
+++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl
@@ -92,7 +92,7 @@ fields(server) ->
)},
{pool_size,
sc(
- integer(),
+ pos_integer(),
#{
default => 8,
example => 8,
diff --git a/apps/emqx_exhook/src/emqx_exhook_server.erl b/apps/emqx_exhook/src/emqx_exhook_server.erl
index b804d1478..36f5f403a 100644
--- a/apps/emqx_exhook/src/emqx_exhook_server.erl
+++ b/apps/emqx_exhook/src/emqx_exhook_server.erl
@@ -86,40 +86,44 @@
%% Load/Unload APIs
%%--------------------------------------------------------------------
--spec load(binary(), map()) -> {ok, server()} | {error, term()} | disable.
+-spec load(binary(), map()) -> {ok, server()} | {error, term()} | {load_error, term()} | disable.
load(_Name, #{enable := false}) ->
disable;
load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts) ->
ReqOpts = #{timeout => Timeout, failed_action => FailedAction},
- {SvrAddr, ClientOpts} = channel_opts(Opts),
- case
- emqx_exhook_sup:start_grpc_client_channel(
- Name,
- SvrAddr,
- ClientOpts
- )
- of
- {ok, _ChannPoolPid} ->
- case do_init(Name, ReqOpts) of
- {ok, HookSpecs} ->
- %% Register metrics
- Prefix = lists:flatten(io_lib:format("exhook.~ts.", [Name])),
- ensure_metrics(Prefix, HookSpecs),
- %% Ensure hooks
- ensure_hooks(HookSpecs),
- {ok, #{
- name => Name,
- options => ReqOpts,
- channel => _ChannPoolPid,
- hookspec => HookSpecs,
- prefix => Prefix
- }};
+ case channel_opts(Opts) of
+ {ok, {SvrAddr, ClientOpts}} ->
+ case
+ emqx_exhook_sup:start_grpc_client_channel(
+ Name,
+ SvrAddr,
+ ClientOpts
+ )
+ of
+ {ok, _ChannPoolPid} ->
+ case do_init(Name, ReqOpts) of
+ {ok, HookSpecs} ->
+ %% Register metrics
+ Prefix = lists:flatten(io_lib:format("exhook.~ts.", [Name])),
+ ensure_metrics(Prefix, HookSpecs),
+ %% Ensure hooks
+ ensure_hooks(HookSpecs),
+ {ok, #{
+ name => Name,
+ options => ReqOpts,
+ channel => _ChannPoolPid,
+ hookspec => HookSpecs,
+ prefix => Prefix
+ }};
+ {error, Reason} ->
+ emqx_exhook_sup:stop_grpc_client_channel(Name),
+ {load_error, Reason}
+ end;
{error, _} = E ->
- emqx_exhook_sup:stop_grpc_client_channel(Name),
E
end;
- {error, _} = E ->
- E
+ Error ->
+ Error
end.
%% @private
@@ -130,7 +134,7 @@ channel_opts(Opts = #{url := URL}) ->
),
case uri_string:parse(URL) of
#{scheme := <<"http">>, host := Host, port := Port} ->
- {format_http_uri("http", Host, Port), ClientOpts};
+ {ok, {format_http_uri("http", Host, Port), ClientOpts}};
#{scheme := <<"https">>, host := Host, port := Port} ->
SslOpts =
case maps:get(ssl, Opts, undefined) of
@@ -154,9 +158,9 @@ channel_opts(Opts = #{url := URL}) ->
transport_opts => SslOpts
}
},
- {format_http_uri("https", Host, Port), NClientOpts};
+ {ok, {format_http_uri("https", Host, Port), NClientOpts}};
Error ->
- error({bad_server_url, URL, Error})
+ {error, {bad_server_url, URL, Error}}
end.
format_http_uri(Scheme, Host, Port) ->
diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl
index 771a2879c..b8a8d9902 100644
--- a/apps/emqx_gateway/src/emqx_gateway_api.erl
+++ b/apps/emqx_gateway/src/emqx_gateway_api.erl
@@ -284,12 +284,12 @@ fields(gateway_overview) ->
)},
{max_connections,
mk(
- integer(),
+ pos_integer(),
#{desc => <<"The Gateway allowed maximum connections/clients">>}
)},
{current_connections,
mk(
- integer(),
+ non_neg_integer(),
#{desc => <<"The Gateway current connected connections/clients">>}
)},
{listeners,
@@ -410,11 +410,11 @@ convert_listener_struct(Schema) ->
),
lists:keystore(listeners, 1, Schema1, {listeners, ListenerSchema}).
-remove_listener_and_authn(Schmea) ->
+remove_listener_and_authn(Schema) ->
lists:keydelete(
authentication,
1,
- lists:keydelete(listeners, 1, Schmea)
+ lists:keydelete(listeners, 1, Schema)
).
listeners_schema(?R_REF(_Mod, tcp_listeners)) ->
diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl
index 644b44574..9b37145f8 100644
--- a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl
+++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl
@@ -384,7 +384,7 @@ params_paging_in_qs() ->
[
{page,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
required => false,
@@ -394,7 +394,7 @@ params_paging_in_qs() ->
)},
{limit,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
required => false,
diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl
index dfeceb24e..6eccdc045 100644
--- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl
+++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl
@@ -101,30 +101,39 @@ clients(get, #{
bindings := #{name := Name0},
query_string := QString
}) ->
- with_gateway(Name0, fun(GwName, _) ->
+ Fun = fun(GwName, _) ->
TabName = emqx_gateway_cm:tabname(info, GwName),
- case maps:get(<<"node">>, QString, undefined) of
- undefined ->
- Response = emqx_mgmt_api:cluster_query(
- QString,
- TabName,
- ?CLIENT_QSCHEMA,
- ?QUERY_FUN
- ),
- emqx_mgmt_util:generate_response(Response);
- Node1 ->
- Node = binary_to_atom(Node1, utf8),
- QStringWithoutNode = maps:without([<<"node">>], QString),
- Response = emqx_mgmt_api:node_query(
- Node,
- QStringWithoutNode,
- TabName,
- ?CLIENT_QSCHEMA,
- ?QUERY_FUN
- ),
- emqx_mgmt_util:generate_response(Response)
+ Result =
+ case maps:get(<<"node">>, QString, undefined) of
+ undefined ->
+ emqx_mgmt_api:cluster_query(
+ QString,
+ TabName,
+ ?CLIENT_QSCHEMA,
+ ?QUERY_FUN
+ );
+ Node0 ->
+ Node1 = binary_to_atom(Node0, utf8),
+ QStringWithoutNode = maps:without([<<"node">>], QString),
+ emqx_mgmt_api:node_query(
+ Node1,
+ QStringWithoutNode,
+ TabName,
+ ?CLIENT_QSCHEMA,
+ ?QUERY_FUN
+ )
+ end,
+ case Result of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Response ->
+ {200, Response}
end
- end).
+ end,
+ with_gateway(Name0, Fun).
clients_insta(get, #{
bindings := #{
@@ -392,21 +401,21 @@ format_channel_info({_, Infos, Stats} = R) ->
{heap_size, Stats, 0},
{reductions, Stats, 0}
],
- eval(FetchX ++ extra_feilds(R)).
+ eval(FetchX ++ extra_fields(R)).
-extra_feilds({_, Infos, _Stats} = R) ->
- extra_feilds(
+extra_fields({_, Infos, _Stats} = R) ->
+ extra_fields(
maps:get(protocol, maps:get(clientinfo, Infos)),
R
).
-extra_feilds(lwm2m, {_, Infos, _Stats}) ->
+extra_fields(lwm2m, {_, Infos, _Stats}) ->
ClientInfo = maps:get(clientinfo, Infos, #{}),
[
{endpoint_name, ClientInfo},
{lifetime, ClientInfo}
];
-extra_feilds(_, _) ->
+extra_fields(_, _) ->
[].
eval(Ls) ->
@@ -495,7 +504,7 @@ schema("/gateway/:name/clients/:clientid/subscriptions") ->
#{
200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:array(ref(subscription)),
- examples_subsctiption_list()
+ examples_subscription_list()
)
}
)
@@ -506,14 +515,14 @@ schema("/gateway/:name/clients/:clientid/subscriptions") ->
parameters => params_client_insta(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
ref(subscription),
- examples_subsctiption()
+ examples_subscription()
),
responses =>
?STANDARD_RESP(
#{
201 => emqx_dashboard_swagger:schema_with_examples(
ref(subscription),
- examples_subsctiption()
+ examples_subscription()
)
}
)
@@ -664,7 +673,7 @@ params_paging() ->
[
{page,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
required => false,
@@ -674,7 +683,7 @@ params_paging() ->
)},
{limit,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
desc => <<"Page Limit">>,
@@ -1089,7 +1098,7 @@ examples_client() ->
}
}.
-examples_subsctiption_list() ->
+examples_subscription_list() ->
#{
general_subscription_list =>
#{
@@ -1103,7 +1112,7 @@ examples_subsctiption_list() ->
}
}.
-examples_subsctiption() ->
+examples_subscription() ->
#{
general_subscription =>
#{
diff --git a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl
index 44d938c60..aebe77a7c 100644
--- a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl
+++ b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl
@@ -191,7 +191,7 @@ users(get, #{bindings := #{name := Name0, id := Id}, query_string := Qs}) ->
Name0,
Id,
fun(_GwName, #{id := AuthId, chain_name := ChainName}) ->
- emqx_authn_api:list_users(ChainName, AuthId, page_pramas(Qs))
+ emqx_authn_api:list_users(ChainName, AuthId, page_params(Qs))
end
);
users(post, #{
@@ -261,7 +261,7 @@ import_users(post, #{
%%--------------------------------------------------------------------
%% Utils
-page_pramas(Qs) ->
+page_params(Qs) ->
maps:with([<<"page">>, <<"limit">>], Qs).
%%--------------------------------------------------------------------
@@ -555,7 +555,7 @@ params_paging_in_qs() ->
[
{page,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
required => false,
@@ -565,7 +565,7 @@ params_paging_in_qs() ->
)},
{limit,
mk(
- integer(),
+ pos_integer(),
#{
in => query,
required => false,
@@ -627,10 +627,12 @@ fields(tcp_listener_opts) ->
{high_watermark, mk(binary(), #{})},
{nodelay, mk(boolean(), #{})},
{reuseaddr, boolean()},
+ %% TODO: duri
{send_timeout, binary()},
{send_timeout_close, boolean()}
];
fields(ssl_listener_opts) ->
+ %% TODO: maybe use better ssl options schema from emqx_ssl_lib or somewhere
[
{cacertfile, binary()},
{certfile, binary()},
@@ -762,7 +764,7 @@ common_listener_opts() ->
required => false,
desc =>
<<
- "The Mounpoint for clients of the listener. "
+ "The Mountpoint for clients of the listener. "
"The gateway-level mountpoint configuration can be overloaded "
"when it is not null or empty string"
>>
@@ -774,7 +776,7 @@ common_listener_opts() ->
emqx_authn_schema:authenticator_type(),
#{
required => {false, recursively},
- desc => <<"The authenticatior for this listener">>
+ desc => <<"The authenticator for this listener">>
}
)}
] ++ emqx_gateway_schema:proxy_protocol_opts().
diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl
index 0f64fecfc..385be93ad 100644
--- a/apps/emqx_gateway/src/emqx_gateway_schema.erl
+++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl
@@ -28,9 +28,9 @@
-include_lib("typerefl/include/types.hrl").
-type ip_port() :: tuple().
--type duration() :: integer().
--type duration_s() :: integer().
--type bytesize() :: integer().
+-type duration() :: non_neg_integer().
+-type duration_s() :: non_neg_integer().
+-type bytesize() :: pos_integer().
-type comma_separated_list() :: list().
-typerefl_from_string({ip_port/0, emqx_schema, to_ip_port}).
@@ -117,7 +117,7 @@ fields(stomp_frame) ->
[
{max_headers,
sc(
- integer(),
+ non_neg_integer(),
#{
default => 10,
desc => "The maximum number of Header"
@@ -125,7 +125,7 @@ fields(stomp_frame) ->
)},
{max_headers_length,
sc(
- integer(),
+ non_neg_integer(),
#{
default => 1024,
desc => "The maximum string length of the Header Value"
diff --git a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl
index 2e04680a7..2b933b178 100644
--- a/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl
+++ b/apps/emqx_gateway/test/emqx_gateway_authz_SUITE.erl
@@ -307,12 +307,12 @@ t_case_stomp_subscribe(_) ->
)
),
- timer:sleep(100),
+ timer:sleep(200),
Msg = emqx_message:make(Topic, Payload),
emqx:publish(Msg),
- timer:sleep(100),
- {ok, Data1} = gen_tcp:recv(Sock, 0, 2000),
+ timer:sleep(200),
+ {ok, Data1} = gen_tcp:recv(Sock, 0, 10000),
{ok, Frame1, _, _} = Mod:parse(Data1),
Checker(Frame1)
end,
@@ -406,7 +406,11 @@ t_case_exproto_subscribe(_) ->
%% Helpers
%%------------------------------------------------------------------------------
try_publish_recv(Topic, Publish, Checker) ->
+ try_publish_recv(Topic, Publish, Checker, 500).
+
+try_publish_recv(Topic, Publish, Checker, Timeout) ->
emqx:subscribe(Topic),
+ timer:sleep(200),
Clear = fun(Msg) ->
emqx:unsubscribe(Topic),
Checker(Msg)
@@ -416,7 +420,7 @@ try_publish_recv(Topic, Publish, Checker) ->
receive
{deliver, Topic, Msg} ->
Clear(Msg)
- after 500 ->
+ after Timeout ->
Clear(timeout)
end.
diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf
new file mode 100644
index 000000000..c6f518c86
--- /dev/null
+++ b/apps/emqx_management/i18n/emqx_mgmt_api_alarms_i18n.conf
@@ -0,0 +1,80 @@
+emqx_mgmt_api_alarms {
+
+ list_alarms_api {
+ desc {
+ en: """List alarms"""
+ zh: """列出告警,获取告警列表"""
+ }
+ }
+
+ delete_alarms_api {
+ desc {
+ en: """Remove all deactivated alarms"""
+ zh: """删除所有历史告警(非活跃告警)"""
+ }
+ }
+
+ delete_alarms_api_response204 {
+ desc {
+ en: """Remove all deactivated alarms ok"""
+ zh: """删除所有历史告警(非活跃告警)成功"""
+ }
+ }
+
+ get_alarms_qs_activated {
+ desc {
+ en: """Activate alarms, or deactivate alarms. Default is false"""
+ zh: """活跃中的告警,或历史告警(非活跃告警),默认为 false"""
+ }
+ }
+
+ node {
+ desc {
+ en: """Alarm in node"""
+ zh: """告警节点名称"""
+ }
+ }
+
+ name {
+ desc {
+ en: """Alarm name"""
+ zh: """告警名称"""
+ }
+ }
+
+ message {
+ desc {
+ en: """Alarm readable information"""
+ zh: """告警信息"""
+ }
+ }
+
+ details {
+ desc {
+ en: """Alarm details information"""
+ zh: """告警详细信息"""
+ }
+ }
+
+ duration {
+ desc {
+ en: """Alarms duration time; UNIX time stamp, millisecond"""
+ zh: """告警持续时间,单位:毫秒"""
+ }
+ }
+
+ activate_at {
+ desc {
+ en: """Alarms activate time, RFC 3339"""
+ zh: """告警开始时间,使用 rfc3339 标准时间格式"""
+ }
+ }
+
+ deactivate_at {
+ desc {
+ en: """Alarms deactivate time, RFC 3339"""
+ zh: """告警结束时间,使用 rfc3339 标准时间格式"""
+ }
+ }
+
+}
diff --git a/apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf b/apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf
new file mode 100644
index 000000000..686293d0b
--- /dev/null
+++ b/apps/emqx_management/i18n/emqx_mgmt_api_banned_i18n.conf
@@ -0,0 +1,113 @@
+emqx_mgmt_api_banned {
+
+ list_banned_api {
+ desc {
+ en: """List banned."""
+ zh: """列出黑名单"""
+ }
+ label {
+ en: """List Banned"""
+ zh: """列出黑名单"""
+ }
+ }
+
+ create_banned_api {
+ desc {
+ en: """Create banned."""
+ zh: """创建黑名单"""
+ }
+ }
+
+ create_banned_api_response400 {
+ desc {
+ en: """Banned already existed, or bad args."""
+ zh: """黑名单已存在,或参数格式有错误"""
+ }
+ }
+
+ delete_banned_api {
+ desc {
+ en: """Delete banned"""
+ zh: """删除黑名单"""
+ }
+ }
+
+ delete_banned_api_response404 {
+ desc {
+ en: """Banned not found. May be the banned time has been exceeded"""
+ zh: """黑名单未找到,可能为已经超期失效"""
+ }
+ }
+
+ create_banned {
+ desc {
+ en: """List banned."""
+ zh: """列出黑名单"""
+ }
+ label {
+ en: """List Banned"""
+ zh: """列出黑名单"""
+ }
+ }
+
+ as {
+ desc {
+ en: """Banned type clientid, username, peerhost"""
+ zh: """黑名单类型,可选 clientid、username、peerhost"""
+ }
+ label {
+ en: """Banned Type"""
+ zh: """黑名单类型"""
+ }
+ }
+ who {
+ desc {
+ en: """Client info as banned type"""
+ zh: """设备信息"""
+ }
+ label {
+ en: """Banned Info"""
+ zh: """黑名单信息"""
+ }
+ }
+ by {
+ desc {
+ en: """Commander"""
+ zh: """黑名单创建者"""
+ }
+ label {
+ en: """Commander"""
+ zh: """黑名单创建者"""
+ }
+ }
+ reason {
+ desc {
+ en: """Banned reason"""
+ zh: """黑名单创建原因"""
+ }
+ label {
+ en: """Reason"""
+ zh: """原因"""
+ }
+ }
+ at {
+ desc {
+ en: """Create banned time, rfc3339, now if not specified"""
+ zh: """黑名单创建时间,默认为当前"""
+ }
+ label {
+ en: """Create banned time"""
+ zh: """黑名单创建时间"""
+ }
+ }
+ until {
+ desc {
+ en: """Cancel banned time, rfc3339, now + 5 minute if not specified"""
+ zh: """黑名单结束时间,默认为创建时间 + 5 分钟"""
+ }
+ label {
+ en: """Cancel banned time"""
+ zh: """黑名单结束时间"""
+ }
+ }
+}
diff --git a/apps/emqx_management/rebar.config b/apps/emqx_management/rebar.config
index f75bd9ce5..73cbf471f 100644
--- a/apps/emqx_management/rebar.config
+++ b/apps/emqx_management/rebar.config
@@ -1,20 +1,28 @@
%% -*- mode: erlang -*-
-{deps, [ {emqx, {path, "../emqx"}}
- ]}.
+{deps, [{emqx, {path, "../emqx"}}]}.
{edoc_opts, [{preprocess, true}]}.
-{erl_opts, [warn_unused_vars,
- warn_shadow_vars,
- warn_unused_import,
- warn_obsolete_guard,
- warnings_as_errors,
- debug_info,
- {parse_transform}]}.
+{erl_opts, [
+ warn_unused_vars,
+ warn_shadow_vars,
+ warn_unused_import,
+ warn_obsolete_guard,
+ warnings_as_errors,
+ debug_info,
+ {parse_transform}
+]}.
-{xref_checks, [undefined_function_calls, undefined_functions,
- locals_not_used, deprecated_function_calls,
- warnings_as_errors, deprecated_functions]}.
+{xref_checks, [
+ undefined_function_calls,
+ undefined_functions,
+ locals_not_used,
+ deprecated_function_calls,
+ warnings_as_errors,
+ deprecated_functions
+]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
+
+{project_plugins, [erlfmt]}.
diff --git a/apps/emqx_management/src/emqx_management.app.src b/apps/emqx_management/src/emqx_management.app.src
index 283eb21f0..0ca60e3d6 100644
--- a/apps/emqx_management/src/emqx_management.app.src
+++ b/apps/emqx_management/src/emqx_management.app.src
@@ -1,15 +1,17 @@
%% -*- mode: erlang -*-
-{application, emqx_management,
- [{description, "EMQX Management API and CLI"},
- {vsn, "5.0.0"}, % strict semver, bump manually!
- {modules, []},
- {registered, [emqx_management_sup]},
- {applications, [kernel,stdlib,emqx_plugins,minirest,emqx]},
- {mod, {emqx_mgmt_app,[]}},
- {env, []},
- {licenses, ["Apache-2.0"]},
- {maintainers, ["EMQX Team "]},
- {links, [{"Homepage", "https://emqx.io/"},
- {"Github", "https://github.com/emqx/emqx-management"}
- ]}
- ]}.
+{application, emqx_management, [
+ {description, "EMQX Management API and CLI"},
+ % strict semver, bump manually!
+ {vsn, "5.0.0"},
+ {modules, []},
+ {registered, [emqx_management_sup]},
+ {applications, [kernel, stdlib, emqx_plugins, minirest, emqx]},
+ {mod, {emqx_mgmt_app, []}},
+ {env, []},
+ {licenses, ["Apache-2.0"]},
+ {maintainers, ["EMQX Team "]},
+ {links, [
+ {"Homepage", "https://emqx.io/"},
+ {"Github", "https://github.com/emqx/emqx-management"}
+ ]}
+]}.
diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl
index 937233c2c..90c05781d 100644
--- a/apps/emqx_management/src/emqx_management_schema.erl
+++ b/apps/emqx_management/src/emqx_management_schema.erl
@@ -19,9 +19,11 @@
-behaviour(hocon_schema).
--export([ namespace/0
- , roots/0
- , fields/1]).
+-export([
+ namespace/0,
+ roots/0,
+ fields/1
+]).
namespace() -> management.
diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl
index b460b69b4..40bc1af25 100644
--- a/apps/emqx_management/src/emqx_mgmt.erl
+++ b/apps/emqx_management/src/emqx_mgmt.erl
@@ -25,76 +25,82 @@
-include_lib("emqx/include/emqx_mqtt.hrl").
%% Nodes and Brokers API
--export([ list_nodes/0
- , lookup_node/1
- , list_brokers/0
- , lookup_broker/1
- , node_info/0
- , node_info/1
- , broker_info/0
- , broker_info/1
- ]).
+-export([
+ list_nodes/0,
+ lookup_node/1,
+ list_brokers/0,
+ lookup_broker/1,
+ node_info/0,
+ node_info/1,
+ broker_info/0,
+ broker_info/1
+]).
%% Metrics and Stats
--export([ get_metrics/0
- , get_metrics/1
- , get_stats/0
- , get_stats/1
- ]).
+-export([
+ get_metrics/0,
+ get_metrics/1,
+ get_stats/0,
+ get_stats/1
+]).
%% Clients, Sessions
--export([ lookup_client/2
- , lookup_client/3
- , kickout_client/1
- , list_authz_cache/1
- , list_client_subscriptions/1
- , client_subscriptions/2
- , clean_authz_cache/1
- , clean_authz_cache/2
- , clean_authz_cache_all/0
- , clean_authz_cache_all/1
- , set_ratelimit_policy/2
- , set_quota_policy/2
- , set_keepalive/2
- ]).
+-export([
+ lookup_client/2,
+ lookup_client/3,
+ kickout_client/1,
+ list_authz_cache/1,
+ list_client_subscriptions/1,
+ client_subscriptions/2,
+ clean_authz_cache/1,
+ clean_authz_cache/2,
+ clean_authz_cache_all/0,
+ clean_authz_cache_all/1,
+ set_ratelimit_policy/2,
+ set_quota_policy/2,
+ set_keepalive/2
+]).
%% Internal funcs
-export([do_call_client/2]).
%% Subscriptions
--export([ list_subscriptions/1
- , list_subscriptions_via_topic/2
- , list_subscriptions_via_topic/3
- , lookup_subscriptions/1
- , lookup_subscriptions/2
+-export([
+ list_subscriptions/1,
+ list_subscriptions_via_topic/2,
+ list_subscriptions_via_topic/3,
+ lookup_subscriptions/1,
+ lookup_subscriptions/2,
- , do_list_subscriptions/0
- ]).
+ do_list_subscriptions/0
+]).
%% PubSub
--export([ subscribe/2
- , do_subscribe/2
- , publish/1
- , unsubscribe/2
- , do_unsubscribe/2
- ]).
+-export([
+ subscribe/2,
+ do_subscribe/2,
+ publish/1,
+ unsubscribe/2,
+ do_unsubscribe/2
+]).
%% Alarms
--export([ get_alarms/1
- , get_alarms/2
- , deactivate/2
- , delete_all_deactivated_alarms/0
- , delete_all_deactivated_alarms/1
- ]).
+-export([
+ get_alarms/1,
+ get_alarms/2,
+ deactivate/2,
+ delete_all_deactivated_alarms/0,
+ delete_all_deactivated_alarms/1
+]).
%% Banned
--export([ create_banned/1
- , delete_banned/1
- ]).
+-export([
+ create_banned/1,
+ delete_banned/1
+]).
%% Common Table API
--export([ max_row_limit/0
- ]).
+-export([max_row_limit/0]).
-define(APP, emqx_management).
@@ -113,24 +119,26 @@ list_nodes() ->
lookup_node(Node) -> node_info(Node).
node_info() ->
- Memory = emqx_vm:get_memory(),
+ Memory = emqx_vm:get_memory(),
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
BrokerInfo = emqx_sys:info(),
- Info#{node => node(),
- otp_release => otp_rel(),
- memory_total => proplists:get_value(allocated, Memory),
- memory_used => proplists:get_value(used, Memory),
- process_available => erlang:system_info(process_limit),
- process_used => erlang:system_info(process_count),
+ Info#{
+ node => node(),
+ otp_release => otp_rel(),
+ memory_total => proplists:get_value(allocated, Memory),
+ memory_used => proplists:get_value(used, Memory),
+ process_available => erlang:system_info(process_limit),
+ process_used => erlang:system_info(process_count),
- max_fds => proplists:get_value(
- max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
- connections => ets:info(emqx_channel, size),
- node_status => 'Running',
- uptime => proplists:get_value(uptime, BrokerInfo),
- version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
- role => mria_rlog:role()
- }.
+ max_fds => proplists:get_value(
+ max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))
+ ),
+ connections => ets:info(emqx_channel, size),
+ node_status => 'Running',
+ uptime => proplists:get_value(uptime, BrokerInfo),
+ version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
+ role => mria_rlog:role()
+ }.
node_info(Node) ->
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
@@ -167,18 +175,21 @@ get_metrics(Node) ->
get_stats() ->
GlobalStatsKeys =
- [ 'retained.count'
- , 'retained.max'
- , 'topics.count'
- , 'topics.max'
- , 'subscriptions.shared.count'
- , 'subscriptions.shared.max'
+ [
+ 'retained.count',
+ 'retained.max',
+ 'topics.count',
+ 'topics.max',
+ 'subscriptions.shared.count',
+ 'subscriptions.shared.max'
],
CountStats = nodes_info_count([
begin
Stats = get_stats(Node),
delete_keys(Stats, GlobalStatsKeys)
- end || Node <- mria_mnesia:running_nodes()]),
+ end
+ || Node <- mria_mnesia:running_nodes()
+ ]),
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
maps:merge(CountStats, GlobalStats).
@@ -207,21 +218,28 @@ nodes_info_count(PropList) ->
%%--------------------------------------------------------------------
lookup_client({clientid, ClientId}, FormatFun) ->
- lists:append([lookup_client(Node, {clientid, ClientId}, FormatFun)
- || Node <- mria_mnesia:running_nodes()]);
-
+ lists:append([
+ lookup_client(Node, {clientid, ClientId}, FormatFun)
+ || Node <- mria_mnesia:running_nodes()
+ ]);
lookup_client({username, Username}, FormatFun) ->
- lists:append([lookup_client(Node, {username, Username}, FormatFun)
- || Node <- mria_mnesia:running_nodes()]).
+ lists:append([
+ lookup_client(Node, {username, Username}, FormatFun)
+ || Node <- mria_mnesia:running_nodes()
+ ]).
lookup_client(Node, Key, {M, F}) ->
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
- {error, Err} -> {error, Err};
- L -> lists:map(fun({Chan, Info0, Stats}) ->
- Info = Info0#{node => Node},
- M:F({Chan, Info, Stats})
- end,
- L)
+ {error, Err} ->
+ {error, Err};
+ L ->
+ lists:map(
+ fun({Chan, Info0, Stats}) ->
+ Info = Info0#{node => Node},
+ M:F({Chan, Info, Stats})
+ end,
+ L
+ )
end.
kickout_client({ClientID, FormatFun}) ->
@@ -266,7 +284,7 @@ clean_authz_cache(Node, ClientId) ->
clean_authz_cache_all() ->
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
- [] -> ok;
+ [] -> ok;
BadNodes -> {error, BadNodes}
end.
@@ -287,9 +305,13 @@ set_keepalive(_ClientId, _Interval) ->
%% @private
call_client(ClientId, Req) ->
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
- Expected = lists:filter(fun({error, _}) -> false;
- (_) -> true
- end, Results),
+ Expected = lists:filter(
+ fun
+ ({error, _}) -> false;
+ (_) -> true
+ end,
+ Results
+ ),
case Expected of
[] -> {error, not_found};
[Result | _] -> Result
@@ -299,13 +321,15 @@ call_client(ClientId, Req) ->
-spec do_call_client(emqx_types:clientid(), term()) -> term().
do_call_client(ClientId, Req) ->
case emqx_cm:lookup_channels(ClientId) of
- [] -> {error, not_found};
+ [] ->
+ {error, not_found};
Pids when is_list(Pids) ->
Pid = lists:last(Pids),
case emqx_cm:get_chan_info(ClientId, Pid) of
#{conninfo := #{conn_mod := ConnMod}} ->
erlang:apply(ConnMod, call, [Pid, Req]);
- undefined -> {error, not_found}
+ undefined ->
+ {error, not_found}
end
end.
@@ -320,22 +344,28 @@ call_client(Node, ClientId, Req) ->
-spec do_list_subscriptions() -> [map()].
do_list_subscriptions() ->
case check_row_limit([mqtt_subproperty]) of
- false -> throw(max_row_limit);
- ok -> [#{topic => Topic, clientid => ClientId, options => Options}
- || {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)]
+ false ->
+ throw(max_row_limit);
+ ok ->
+ [
+ #{topic => Topic, clientid => ClientId, options => Options}
+ || {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)
+ ]
end.
list_subscriptions(Node) ->
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
list_subscriptions_via_topic(Topic, FormatFun) ->
- lists:append([list_subscriptions_via_topic(Node, Topic, FormatFun)
- || Node <- mria_mnesia:running_nodes()]).
+ lists:append([
+ list_subscriptions_via_topic(Node, Topic, FormatFun)
+ || Node <- mria_mnesia:running_nodes()
+ ]).
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
{error, Reason} -> {error, Reason};
- Result -> M:F(Result)
+ Result -> M:F(Result)
end.
lookup_subscriptions(ClientId) ->
@@ -354,20 +384,17 @@ subscribe(ClientId, TopicTables) ->
subscribe([Node | Nodes], ClientId, TopicTables) ->
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
- {subscribe, Res} ->
- {subscribe, Res, Node}
+ {subscribe, Res} -> {subscribe, Res, Node}
end;
-
subscribe([], _ClientId, _TopicTables) ->
{error, channel_not_found}.
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
- {subscribe, _} | {error, atom()}.
+ {subscribe, _} | {error, atom()}.
do_subscribe(ClientId, TopicTables) ->
case ets:lookup(emqx_channel, ClientId) of
[] -> {error, channel_not_found};
- [{_, Pid}] ->
- Pid ! {subscribe, TopicTables}
+ [{_, Pid}] -> Pid ! {subscribe, TopicTables}
end.
%%TODO: ???
@@ -376,12 +403,12 @@ publish(Msg) ->
emqx:publish(Msg).
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
- {unsubscribe, _} | {error, channel_not_found}.
+ {unsubscribe, _} | {error, channel_not_found}.
unsubscribe(ClientId, Topic) ->
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
- {unsubscribe, _} | {error, channel_not_found}.
+ {unsubscribe, _} | {error, channel_not_found}.
unsubscribe([Node | Nodes], ClientId, Topic) ->
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
@@ -391,12 +418,11 @@ unsubscribe([], _ClientId, _Topic) ->
{error, channel_not_found}.
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
- {unsubscribe, _} | {error, _}.
+ {unsubscribe, _} | {error, _}.
do_unsubscribe(ClientId, Topic) ->
case ets:lookup(emqx_channel, ClientId) of
[] -> {error, channel_not_found};
- [{_, Pid}] ->
- Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
+ [{_, Pid}] -> Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
end.
%%--------------------------------------------------------------------
@@ -426,11 +452,18 @@ add_duration_field([], _Now, Acc) ->
Acc;
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
-
-add_duration_field( [Alarm = #{ activated := false
- , activate_at := ActivateAt
- , deactivate_at := DeactivateAt} | Rest]
- , Now, Acc) ->
+add_duration_field(
+ [
+ Alarm = #{
+ activated := false,
+ activate_at := ActivateAt,
+ deactivate_at := DeactivateAt
+ }
+ | Rest
+ ],
+ Now,
+ Acc
+) ->
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
%%--------------------------------------------------------------------
@@ -462,13 +495,13 @@ check_row_limit([], _Limit) ->
ok;
check_row_limit([Tab | Tables], Limit) ->
case table_size(Tab) > Limit of
- true -> false;
+ true -> false;
false -> check_row_limit(Tables, Limit)
end.
check_results(Results) ->
case lists:any(fun(Item) -> Item =:= ok end, Results) of
- true -> ok;
+ true -> ok;
false -> wrap_rpc(lists:last(Results))
end.
diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl
index 36d647f57..342228b19 100644
--- a/apps/emqx_management/src/emqx_mgmt_api.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api.erl
@@ -22,16 +22,18 @@
-define(FRESH_SELECT, fresh_select).
--export([ paginate/3
- , paginate/4
- ]).
+-export([
+ paginate/3,
+ paginate/4
+]).
%% first_next query APIs
--export([ node_query/5
- , cluster_query/4
- , select_table_with_count/5
- , b2i/1
- ]).
+-export([
+ node_query/5,
+ cluster_query/4,
+ select_table_with_count/5,
+ b2i/1
+]).
-export([do_query/6]).
@@ -50,30 +52,30 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
Limit = b2i(limit(Params)),
Cursor = qlc:cursor(Qh),
case Page > 1 of
- true ->
+ true ->
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
ok;
- false -> ok
+ false ->
+ ok
end,
Rows = qlc:next_answers(Cursor, Limit),
qlc:delete_cursor(Cursor),
- #{meta => #{page => Page, limit => Limit, count => Count},
- data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]}.
+ #{
+ meta => #{page => Page, limit => Limit, count => Count},
+ data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
+ }.
query_handle(Table) when is_atom(Table) ->
qlc:q([R || R <- ets:table(Table)]);
-
query_handle({Table, Opts}) when is_atom(Table) ->
qlc:q([R || R <- ets:table(Table, Opts)]);
-
query_handle([Table]) when is_atom(Table) ->
qlc:q([R || R <- ets:table(Table)]);
-
query_handle([{Table, Opts}]) when is_atom(Table) ->
qlc:q([R || R <- ets:table(Table, Opts)]);
-
query_handle(Tables) ->
- qlc:append([query_handle(T) || T <- Tables]). %
+ %
+ qlc:append([query_handle(T) || T <- Tables]).
query_handle(Table, MatchSpec) when is_atom(Table) ->
Options = {traverse, {select, MatchSpec}},
@@ -87,16 +89,12 @@ query_handle(Tables, MatchSpec) ->
count(Table) when is_atom(Table) ->
ets:info(Table, size);
-
count({Table, _}) when is_atom(Table) ->
ets:info(Table, size);
-
count([Table]) when is_atom(Table) ->
ets:info(Table, size);
-
count([{Table, _}]) when is_atom(Table) ->
ets:info(Table, size);
-
count(Tables) ->
lists:sum([count(T) || T <- Tables]).
@@ -121,7 +119,7 @@ limit(Params) ->
init_meta(Params) ->
Limit = b2i(limit(Params)),
- Page = b2i(page(Params)),
+ Page = b2i(page(Params)),
#{
page => Page,
limit => Limit,
@@ -134,17 +132,24 @@ init_meta(Params) ->
node_query(Node, QString, Tab, QSchema, QueryFun) ->
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
- page_limit_check_query( init_meta(QString)
- , { fun do_node_query/5
- , [Node, Tab, NQString, QueryFun, init_meta(QString)]}).
+ page_limit_check_query(
+ init_meta(QString),
+ {fun do_node_query/5, [Node, Tab, NQString, QueryFun, init_meta(QString)]}
+ ).
%% @private
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
-do_node_query( Node, Tab, QString, QueryFun, Continuation
- , Meta = #{limit := Limit}
- , Results) ->
+do_node_query(
+ Node,
+ Tab,
+ QString,
+ QueryFun,
+ Continuation,
+ Meta = #{limit := Limit},
+ Results
+) ->
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
{error, {badrpc, R}} ->
{error, Node, {badrpc, R}};
@@ -164,18 +169,33 @@ cluster_query(QString, Tab, QSchema, QueryFun) ->
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
Nodes = mria_mnesia:running_nodes(),
page_limit_check_query(
- init_meta(QString)
- , {fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}).
+ init_meta(QString),
+ {fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}
+ ).
%% @private
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
- do_cluster_query( Nodes, Tab, QString, QueryFun
- , _Continuation = ?FRESH_SELECT, Meta, _Results = []).
+ do_cluster_query(
+ Nodes,
+ Tab,
+ QString,
+ QueryFun,
+ _Continuation = ?FRESH_SELECT,
+ Meta,
+ _Results = []
+ ).
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
#{meta => Meta, data => Results};
-do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
- Meta = #{limit := Limit}, Results) ->
+do_cluster_query(
+ [Node | Tail] = Nodes,
+ Tab,
+ QString,
+ QueryFun,
+ Continuation,
+ Meta = #{limit := Limit},
+ Results
+) ->
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
{error, {badrpc, R}} ->
{error, Node, {bar_rpc, R}};
@@ -192,11 +212,18 @@ do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
%%--------------------------------------------------------------------
%% @private This function is exempt from BPAPI
-do_query(Node, Tab, QString, {M,F}, Continuation, Limit) when Node =:= node() ->
+do_query(Node, Tab, QString, {M, F}, Continuation, Limit) when Node =:= node() ->
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
- case rpc:call(Node, ?MODULE, do_query,
- [Node, Tab, QString, QueryFun, Continuation, Limit], 50000) of
+ case
+ rpc:call(
+ Node,
+ ?MODULE,
+ do_query,
+ [Node, Tab, QString, QueryFun, Continuation, Limit],
+ 50000
+ )
+ of
{badrpc, _} = R -> {error, R};
Ret -> Ret
end.
@@ -220,8 +247,9 @@ sub_query_result(Len, Rows, Limit, Results, Meta) ->
%% Table Select
%%--------------------------------------------------------------------
-select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
- when is_function(FuzzyFilterFun) andalso Limit > 0 ->
+select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun) when
+ is_function(FuzzyFilterFun) andalso Limit > 0
+->
case ets:select(Tab, Ms, Limit) of
'$end_of_table' ->
{0, [], ?FRESH_SELECT};
@@ -229,8 +257,9 @@ select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
Rows = FuzzyFilterFun(RawResult),
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
end;
-select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun)
- when is_function(FuzzyFilterFun) ->
+select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun) when
+ is_function(FuzzyFilterFun)
+->
case ets:select(ets:repair_continuation(Continuation, Ms)) of
'$end_of_table' ->
{0, [], ?FRESH_SELECT};
@@ -238,8 +267,9 @@ select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun
Rows = FuzzyFilterFun(RawResult),
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
end;
-select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun)
- when Limit > 0 ->
+select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun) when
+ Limit > 0
+->
case ets:select(Tab, Ms, Limit) of
'$end_of_table' ->
{0, [], ?FRESH_SELECT};
@@ -267,36 +297,53 @@ parse_qstring(QString, QSchema) ->
do_parse_qstring([], _, Acc1, Acc2) ->
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
{lists:reverse(Acc1), lists:reverse(NAcc2)};
-
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
case proplists:get_value(Key, QSchema) of
- undefined -> do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
+ undefined ->
+ do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
Type ->
case Key of
- <>
- when Prefix =:= <<"gte_">>;
- Prefix =:= <<"lte_">> ->
- OpposeKey = case Prefix of
- <<"gte_">> -> <<"lte_", NKey/binary>>;
- <<"lte_">> -> <<"gte_", NKey/binary>>
- end,
+ <> when
+ Prefix =:= <<"gte_">>;
+ Prefix =:= <<"lte_">>
+ ->
+ OpposeKey =
+ case Prefix of
+ <<"gte_">> -> <<"lte_", NKey/binary>>;
+ <<"lte_">> -> <<"gte_", NKey/binary>>
+ end,
case lists:keytake(OpposeKey, 1, RestQString) of
false ->
- do_parse_qstring( RestQString, QSchema
- , [qs(Key, Value, Type) | Acc1], Acc2);
+ do_parse_qstring(
+ RestQString,
+ QSchema,
+ [qs(Key, Value, Type) | Acc1],
+ Acc2
+ );
{value, {K2, V2}, NParams} ->
- do_parse_qstring( NParams, QSchema
- , [qs(Key, Value, K2, V2, Type) | Acc1], Acc2)
+ do_parse_qstring(
+ NParams,
+ QSchema,
+ [qs(Key, Value, K2, V2, Type) | Acc1],
+ Acc2
+ )
end;
_ ->
case is_fuzzy_key(Key) of
true ->
- do_parse_qstring( RestQString, QSchema
- , Acc1, [qs(Key, Value, Type) | Acc2]);
+ do_parse_qstring(
+ RestQString,
+ QSchema,
+ Acc1,
+ [qs(Key, Value, Type) | Acc2]
+ );
_ ->
- do_parse_qstring( RestQString, QSchema
- , [qs(Key, Value, Type) | Acc1], Acc2)
-
+ do_parse_qstring(
+ RestQString,
+ QSchema,
+ [qs(Key, Value, Type) | Acc1],
+ Acc2
+ )
end
end
end.
@@ -310,7 +357,7 @@ qs(K, Value0, Type) ->
try
qs(K, to_type(Value0, Type))
catch
- throw : bad_value_type ->
+ throw:bad_value_type ->
throw({bad_value_type, {K, Type, Value0}})
end.
@@ -333,12 +380,11 @@ is_fuzzy_key(_) ->
false.
page_start(1, _) -> 1;
-page_start(Page, Limit) -> (Page-1) * Limit + 1.
-
+page_start(Page, Limit) -> (Page - 1) * Limit + 1.
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
PageStart = page_start(Page, Limit),
- PageEnd = Page * Limit,
+ PageEnd = Page * Limit,
case Count + Len of
NCount when NCount < PageStart ->
{more, Meta#{count => NCount}};
@@ -353,7 +399,7 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
case (Count - Len) < PageStart of
true ->
NeedNowNum = Count - PageStart + 1,
- SubStart = Len - NeedNowNum + 1,
+ SubStart = Len - NeedNowNum + 1,
{SubStart, NeedNowNum};
false ->
{_SubStart = 1, _NeedNowNum = Len}
@@ -361,8 +407,9 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
page_limit_check_query(Meta, {F, A}) ->
case Meta of
- #{page := Page, limit := Limit}
- when Page < 1; Limit < 1 ->
+ #{page := Page, limit := Limit} when
+ Page < 1; Limit < 1
+ ->
{error, page_limit_invalid};
_ ->
erlang:apply(F, A)
@@ -376,7 +423,7 @@ to_type(V, TargetType) ->
try
to_type_(V, TargetType)
catch
- _ : _ ->
+ _:_ ->
throw(bad_value_type)
end.
@@ -419,37 +466,43 @@ to_ip_port(IPAddress) ->
-include_lib("eunit/include/eunit.hrl").
params2qs_test() ->
- QSchema = [{<<"str">>, binary},
- {<<"int">>, integer},
- {<<"atom">>, atom},
- {<<"ts">>, timestamp},
- {<<"gte_range">>, integer},
- {<<"lte_range">>, integer},
- {<<"like_fuzzy">>, binary},
- {<<"match_topic">>, binary}],
- QString = [{<<"str">>, <<"abc">>},
- {<<"int">>, <<"123">>},
- {<<"atom">>, <<"connected">>},
- {<<"ts">>, <<"156000">>},
- {<<"gte_range">>, <<"1">>},
- {<<"lte_range">>, <<"5">>},
- {<<"like_fuzzy">>, <<"user">>},
- {<<"match_topic">>, <<"t/#">>}],
- ExpectedQs = [{str, '=:=', <<"abc">>},
- {int, '=:=', 123},
- {atom, '=:=', connected},
- {ts, '=:=', 156000},
- {range, '>=', 1, '=<', 5}
- ],
- FuzzyNQString = [{fuzzy, like, <<"user">>},
- {topic, match, <<"t/#">>}],
+ QSchema = [
+ {<<"str">>, binary},
+ {<<"int">>, integer},
+ {<<"atom">>, atom},
+ {<<"ts">>, timestamp},
+ {<<"gte_range">>, integer},
+ {<<"lte_range">>, integer},
+ {<<"like_fuzzy">>, binary},
+ {<<"match_topic">>, binary}
+ ],
+ QString = [
+ {<<"str">>, <<"abc">>},
+ {<<"int">>, <<"123">>},
+ {<<"atom">>, <<"connected">>},
+ {<<"ts">>, <<"156000">>},
+ {<<"gte_range">>, <<"1">>},
+ {<<"lte_range">>, <<"5">>},
+ {<<"like_fuzzy">>, <<"user">>},
+ {<<"match_topic">>, <<"t/#">>}
+ ],
+ ExpectedQs = [
+ {str, '=:=', <<"abc">>},
+ {int, '=:=', 123},
+ {atom, '=:=', connected},
+ {ts, '=:=', 156000},
+ {range, '>=', 1, '=<', 5}
+ ],
+ FuzzyNQString = [
+ {fuzzy, like, <<"user">>},
+ {topic, match, <<"t/#">>}
+ ],
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
-endif.
-
b2i(Bin) when is_binary(Bin) ->
binary_to_integer(Bin);
b2i(Any) ->
diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl
index 2a9704bfe..5c455b4e4 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl
@@ -18,6 +18,7 @@
-behaviour(minirest_api).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("typerefl/include/types.hrl").
@@ -38,13 +39,16 @@ schema("/alarms") ->
#{
'operationId' => alarms,
get => #{
- description => <<"EMQX alarms">>,
+ description => ?DESC(list_alarms_api),
parameters => [
hoconsc:ref(emqx_dashboard_swagger, page),
hoconsc:ref(emqx_dashboard_swagger, limit),
- {activated, hoconsc:mk(boolean(), #{in => query,
- desc => <<"All alarms, if not specified">>,
- required => false})}
+ {activated,
+ hoconsc:mk(boolean(), #{
+ in => query,
+ desc => ?DESC(get_alarms_qs_activated),
+ required => false
+ })}
],
responses => #{
200 => [
@@ -53,34 +57,48 @@ schema("/alarms") ->
]
}
},
- delete => #{
- description => <<"Remove all deactivated alarms">>,
+ delete => #{
+ description => ?DESC(delete_alarms_api),
responses => #{
- 204 => <<"Remove all deactivated alarms ok">>
+ 204 => ?DESC(delete_alarms_api_response204)
}
}
}.
fields(alarm) ->
[
- {node, hoconsc:mk(binary(),
- #{desc => <<"Alarm in node">>, example => atom_to_list(node())})},
- {name, hoconsc:mk(binary(),
- #{desc => <<"Alarm name">>, example => <<"high_system_memory_usage">>})},
- {message, hoconsc:mk(binary(), #{desc => <<"Alarm readable information">>,
- example => <<"System memory usage is higher than 70%">>})},
- {details, hoconsc:mk(map(), #{desc => <<"Alarm details information">>,
- example => #{<<"high_watermark">> => 70}})},
- {duration, hoconsc:mk(integer(),
- #{desc => <<"Alarms duration time; UNIX time stamp, millisecond">>,
- example => 297056})},
- {activate_at, hoconsc:mk(binary(), #{desc => <<"Alarms activate time, RFC 3339">>,
- example => <<"2021-10-25T11:52:52.548+08:00">>})},
- {deactivate_at, hoconsc:mk(binary(),
- #{desc => <<"Nullable, alarms deactivate time, RFC 3339">>,
- example => <<"2021-10-31T10:52:52.548+08:00">>})}
+ {node,
+ hoconsc:mk(
+ binary(),
+ #{desc => ?DESC(node), example => atom_to_list(node())}
+ )},
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{desc => ?DESC(node), example => <<"high_system_memory_usage">>}
+ )},
+ {message,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(message),
+ example => <<"System memory usage is higher than 70%">>
+ })},
+ {details,
+ hoconsc:mk(map(), #{
+ desc => ?DESC(details),
+ example => #{<<"high_watermark">> => 70}
+ })},
+ {duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
+ {activate_at,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(activate_at),
+ example => <<"2021-10-25T11:52:52.548+08:00">>
+ })},
+ {deactivate_at,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(deactivate_at),
+ example => <<"2021-10-31T10:52:52.548+08:00">>
+ })}
];
-
fields(meta) ->
emqx_dashboard_swagger:fields(page) ++
emqx_dashboard_swagger:fields(limit) ++
@@ -93,9 +111,15 @@ alarms(get, #{query_string := QString}) ->
true -> ?ACTIVATED_ALARM;
false -> ?DEACTIVATED_ALARM
end,
- Response = emqx_mgmt_api:cluster_query(QString, Table, [], {?MODULE, query}),
- emqx_mgmt_util:generate_response(Response);
-
+ case emqx_mgmt_api:cluster_query(QString, Table, [], {?MODULE, query}) of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Response ->
+ {200, Response}
+ end;
alarms(delete, _Params) ->
_ = emqx_mgmt:delete_all_deactivated_alarms(),
{204}.
@@ -104,11 +128,10 @@ alarms(delete, _Params) ->
%% internal
query(Table, _QsSpec, Continuation, Limit) ->
- Ms = [{'$1',[],['$1']}],
+ Ms = [{'$1', [], ['$1']}],
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
format_alarm(Alarms) when is_list(Alarms) ->
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
-
format_alarm(Alarm) ->
emqx_alarm:format(Alarm).
diff --git a/apps/emqx_management/src/emqx_mgmt_api_app.erl b/apps/emqx_management/src/emqx_mgmt_api_app.erl
index 239e9d3a6..3668a4aea 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_app.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_app.erl
@@ -31,7 +31,6 @@ api_spec() ->
paths() ->
["/api_key", "/api_key/:name"].
-
schema("/api_key") ->
#{
'operationId' => api_key,
@@ -82,41 +81,80 @@ schema("/api_key/:name") ->
fields(app) ->
[
- {name, hoconsc:mk(binary(),
- #{desc => "Unique and format by [a-zA-Z0-9-_]",
- validator => fun ?MODULE:validate_name/1,
- example => <<"EMQX-API-KEY-1">>})},
- {api_key, hoconsc:mk(binary(),
- #{desc => """TODO:uses HMAC-SHA256 for signing.""",
- example => <<"a4697a5c75a769f6">>})},
- {api_secret, hoconsc:mk(binary(),
- #{desc => """An API secret is a simple encrypted string that identifies"""
- """an application without any principal."""
- """They are useful for accessing public data anonymously,"""
- """and are used to associate API requests.""",
- example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>})},
- {expired_at, hoconsc:mk(hoconsc:union([undefined, emqx_datetime:epoch_second()]),
- #{desc => "No longer valid datetime",
- example => <<"2021-12-05T02:01:34.186Z">>,
- required => false,
- default => undefined
- })},
- {created_at, hoconsc:mk(emqx_datetime:epoch_second(),
- #{desc => "ApiKey create datetime",
- example => <<"2021-12-01T00:00:00.000Z">>
- })},
- {desc, hoconsc:mk(binary(),
- #{example => <<"Note">>, required => false})},
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Unique and format by [a-zA-Z0-9-_]",
+ validator => fun ?MODULE:validate_name/1,
+ example => <<"EMQX-API-KEY-1">>
+ }
+ )},
+ {api_key,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "" "TODO:uses HMAC-SHA256 for signing." "",
+ example => <<"a4697a5c75a769f6">>
+ }
+ )},
+ {api_secret,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc =>
+ ""
+ "An API secret is a simple encrypted string that identifies"
+ ""
+ ""
+ "an application without any principal."
+ ""
+ ""
+ "They are useful for accessing public data anonymously,"
+ ""
+ ""
+ "and are used to associate API requests."
+ "",
+ example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>
+ }
+ )},
+ {expired_at,
+ hoconsc:mk(
+ hoconsc:union([undefined, emqx_datetime:epoch_second()]),
+ #{
+ desc => "No longer valid datetime",
+ example => <<"2021-12-05T02:01:34.186Z">>,
+ required => false,
+ default => undefined
+ }
+ )},
+ {created_at,
+ hoconsc:mk(
+ emqx_datetime:epoch_second(),
+ #{
+ desc => "ApiKey create datetime",
+ example => <<"2021-12-01T00:00:00.000Z">>
+ }
+ )},
+ {desc,
+ hoconsc:mk(
+ binary(),
+ #{example => <<"Note">>, required => false}
+ )},
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
];
fields(name) ->
- [{name, hoconsc:mk(binary(),
- #{
- desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
- example => <<"EMQX-API-KEY-1">>,
- in => path,
- validator => fun ?MODULE:validate_name/1
- })}
+ [
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
+ example => <<"EMQX-API-KEY-1">>,
+ in => path,
+ validator => fun ?MODULE:validate_name/1
+ }
+ )}
].
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
@@ -129,7 +167,8 @@ validate_name(Name) ->
nomatch -> {error, "Name should be " ?NAME_RE};
_ -> ok
end;
- false -> {error, "Name Length must =< 256"}
+ false ->
+ {error, "Name Length must =< 256"}
end.
delete(Keys, Fields) ->
@@ -146,10 +185,13 @@ api_key(post, #{body := App}) ->
ExpiredAt = ensure_expired_at(App),
Desc = unicode:characters_to_binary(Desc0, unicode),
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
- {ok, NewApp} -> {200, format(NewApp)};
+ {ok, NewApp} ->
+ {200, format(NewApp)};
{error, Reason} ->
- {400, #{code => 'BAD_REQUEST',
- message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
+ {400, #{
+ code => 'BAD_REQUEST',
+ message => iolist_to_binary(io_lib:format("~p", [Reason]))
+ }}
end.
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
@@ -184,5 +226,5 @@ format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
}.
-ensure_expired_at(#{<<"expired_at">> := ExpiredAt})when is_integer(ExpiredAt) -> ExpiredAt;
+ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
ensure_expired_at(_) -> undefined.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl
index 65e0cd549..1354b9ac8 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl
@@ -16,6 +16,7 @@
-module(emqx_mgmt_api_banned).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/emqx.hrl").
-include_lib("typerefl/include/types.hrl").
@@ -23,16 +24,19 @@
-behaviour(minirest_api).
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
-export([format/1]).
--export([ banned/2
- , delete_banned/2
- ]).
+-export([
+ banned/2,
+ delete_banned/2
+]).
-define(TAB, emqx_banned).
@@ -48,28 +52,29 @@ paths() ->
schema("/banned") ->
#{
- 'operationId' => banned,
+ 'operationId' => banned,
get => #{
- description => <<"List banned">>,
+ description => ?DESC(list_banned_api),
parameters => [
hoconsc:ref(emqx_dashboard_swagger, page),
hoconsc:ref(emqx_dashboard_swagger, limit)
],
responses => #{
- 200 =>[
+ 200 => [
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
]
}
},
post => #{
- description => <<"Create banned">>,
+ description => ?DESC(create_banned_api),
'requestBody' => hoconsc:mk(hoconsc:ref(ban)),
responses => #{
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
400 => emqx_dashboard_swagger:error_codes(
- ['ALREADY_EXISTS', 'BAD_REQUEST'],
- <<"Banned already existed, or bad args">>)
+ ['ALREADY_EXISTS', 'BAD_REQUEST'],
+ ?DESC(create_banned_api_response400)
+ )
}
}
};
@@ -77,53 +82,71 @@ schema("/banned/:as/:who") ->
#{
'operationId' => delete_banned,
delete => #{
- description => <<"Delete banned">>,
+ description => ?DESC(delete_banned_api),
parameters => [
- {as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
- desc => <<"Banned type">>,
- in => path,
- example => username})},
- {who, hoconsc:mk(binary(), #{
- desc => <<"Client info as banned type">>,
- in => path,
- example => <<"Badass">>})}
- ],
+ {as,
+ hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
+ desc => ?DESC(as),
+ required => true,
+ in => path,
+ example => username
+ })},
+ {who,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(who),
+ required => true,
+ in => path,
+ example => <<"Badass">>
+ })}
+ ],
responses => #{
204 => <<"Delete banned success">>,
404 => emqx_dashboard_swagger:error_codes(
- ['NOT_FOUND'],
- <<"Banned not found. May be the banned time has been exceeded">>)
+ ['NOT_FOUND'],
+ ?DESC(delete_banned_api_response404)
+ )
}
}
}.
fields(ban) ->
[
- {as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
- desc => <<"Banned type clientid, username, peerhost">>,
- required => true,
- example => username})},
- {who, hoconsc:mk(binary(), #{
- desc => <<"Client info as banned type">>,
- required => true,
- example => <<"Banned name"/utf8>>})},
- {by, hoconsc:mk(binary(), #{
- desc => <<"Commander">>,
- required => false,
- example => <<"mgmt_api">>})},
- {reason, hoconsc:mk(binary(), #{
- desc => <<"Banned reason">>,
- required => false,
- example => <<"Too many requests">>})},
- {at, hoconsc:mk(emqx_datetime:epoch_second(), #{
- desc => <<"Create banned time, rfc3339, now if not specified">>,
- required => false,
- example => <<"2021-10-25T21:48:47+08:00">>})},
- {until, hoconsc:mk(emqx_datetime:epoch_second(), #{
- desc => <<"Cancel banned time, rfc3339, now + 5 minute if not specified">>,
- required => false,
- example => <<"2021-10-25T21:53:47+08:00">>})
- }
+ {as,
+ hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
+ desc => ?DESC(as),
+ required => true,
+ example => username
+ })},
+ {who,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(who),
+ required => true,
+ example => <<"Banned name"/utf8>>
+ })},
+ {by,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(by),
+ required => false,
+ example => <<"mgmt_api">>
+ })},
+ {reason,
+ hoconsc:mk(binary(), #{
+ desc => ?DESC(reason),
+ required => false,
+ example => <<"Too many requests">>
+ })},
+ {at,
+ hoconsc:mk(emqx_datetime:epoch_second(), #{
+ desc => ?DESC(at),
+ required => false,
+ example => <<"2021-10-25T21:48:47+08:00">>
+ })},
+ {until,
+ hoconsc:mk(emqx_datetime:epoch_second(), #{
+ desc => ?DESC(until),
+ required => false,
+ example => <<"2021-10-25T21:53:47+08:00">>
+ })}
];
fields(meta) ->
emqx_dashboard_swagger:fields(page) ++
@@ -140,8 +163,7 @@ banned(post, #{body := Body}) ->
Ban ->
case emqx_banned:create(Ban) of
{ok, Banned} -> {200, format(Banned)};
- {error, {already_exist, Old}} ->
- {400, 'ALREADY_EXISTS', format(Old)}
+ {error, {already_exist, Old}} -> {400, 'ALREADY_EXISTS', format(Old)}
end
end.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl
index d8c71e80c..393af6b5d 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl
@@ -26,63 +26,70 @@
-include("emqx_mgmt.hrl").
%% API
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
--export([ clients/2
- , client/2
- , subscriptions/2
- , authz_cache/2
- , subscribe/2
- , unsubscribe/2
- , subscribe_batch/2
- , set_keepalive/2
- ]).
+-export([
+ clients/2,
+ client/2,
+ subscriptions/2,
+ authz_cache/2,
+ subscribe/2,
+ unsubscribe/2,
+ subscribe_batch/2,
+ set_keepalive/2
+]).
--export([ query/4
- , format_channel_info/1
- ]).
+-export([
+ query/4,
+ format_channel_info/1
+]).
%% for batch operation
-export([do_subscribe/3]).
-define(CLIENT_QTAB, emqx_channel_info).
--define(CLIENT_QSCHEMA,
- [ {<<"node">>, atom}
- , {<<"username">>, binary}
- , {<<"zone">>, atom}
- , {<<"ip_address">>, ip}
- , {<<"conn_state">>, atom}
- , {<<"clean_start">>, atom}
- , {<<"proto_name">>, binary}
- , {<<"proto_ver">>, integer}
- , {<<"like_clientid">>, binary}
- , {<<"like_username">>, binary}
- , {<<"gte_created_at">>, timestamp}
- , {<<"lte_created_at">>, timestamp}
- , {<<"gte_connected_at">>, timestamp}
- , {<<"lte_connected_at">>, timestamp}]).
+-define(CLIENT_QSCHEMA, [
+ {<<"node">>, atom},
+ {<<"username">>, binary},
+ {<<"zone">>, atom},
+ {<<"ip_address">>, ip},
+ {<<"conn_state">>, atom},
+ {<<"clean_start">>, atom},
+ {<<"proto_name">>, binary},
+ {<<"proto_ver">>, integer},
+ {<<"like_clientid">>, binary},
+ {<<"like_username">>, binary},
+ {<<"gte_created_at">>, timestamp},
+ {<<"lte_created_at">>, timestamp},
+ {<<"gte_connected_at">>, timestamp},
+ {<<"lte_connected_at">>, timestamp}
+]).
-define(QUERY_FUN, {?MODULE, query}).
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
-define(CLIENT_ID_NOT_FOUND,
- <<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>).
+ <<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>
+).
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
paths() ->
- [ "/clients"
- , "/clients/:clientid"
- , "/clients/:clientid/authz_cache"
- , "/clients/:clientid/subscriptions"
- , "/clients/:clientid/subscribe"
- , "/clients/:clientid/unsubscribe"
- , "/clients/:clientid/keepalive"
+ [
+ "/clients",
+ "/clients/:clientid",
+ "/clients/:clientid/authorization/cache",
+ "/clients/:clientid/subscriptions",
+ "/clients/:clientid/subscribe",
+ "/clients/:clientid/unsubscribe",
+ "/clients/:clientid/keepalive"
].
schema("/clients") ->
@@ -93,69 +100,105 @@ schema("/clients") ->
parameters => [
hoconsc:ref(emqx_dashboard_swagger, page),
hoconsc:ref(emqx_dashboard_swagger, limit),
- {node, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Node name">>,
- example => atom_to_list(node())})},
- {username, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"User name">>})},
- {zone, hoconsc:mk(binary(), #{
- in => query,
- required => false})},
- {ip_address, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Client's IP address">>,
- example => <<"127.0.0.1">>})},
- {conn_state, hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
- in => query,
- required => false,
- desc => <<"The current connection status of the client, ",
- "the possible values are connected,idle,disconnected">>})},
- {clean_start, hoconsc:mk(boolean(), #{
- in => query,
- required => false,
- description => <<"Whether the client uses a new session">>})},
- {proto_name, hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
- in => query,
- required => false,
- description => <<"Client protocol name, ",
- "the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>})},
- {proto_ver, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Client protocol version">>})},
- {like_clientid, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Fuzzy search `clientid` as substring">>})},
- {like_username, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Fuzzy search `username` as substring">>})},
- {gte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
- in => query,
- required => false,
- desc => <<"Search client session creation time by greater",
- " than or equal method, rfc3339 or timestamp(millisecond)">>})},
- {lte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
- in => query,
- required => false,
- desc => <<"Search client session creation time by less",
- " than or equal method, rfc3339 or timestamp(millisecond)">>})},
- {gte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
- in => query,
- required => false,
- desc => <<"Search client connection creation time by greater"
- " than or equal method, rfc3339 or timestamp(epoch millisecond)">>})},
- {lte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
- in => query,
- required => false,
- desc => <<"Search client connection creation time by less"
- " than or equal method, rfc3339 or timestamp(millisecond)">>})}
+ {node,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Node name">>,
+ example => atom_to_list(node())
+ })},
+ {username,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"User name">>
+ })},
+ {zone,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false
+ })},
+ {ip_address,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Client's IP address">>,
+ example => <<"127.0.0.1">>
+ })},
+ {conn_state,
+ hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
+ in => query,
+ required => false,
+ desc =>
+ <<"The current connection status of the client, ",
+ "the possible values are connected,idle,disconnected">>
+ })},
+ {clean_start,
+ hoconsc:mk(boolean(), #{
+ in => query,
+ required => false,
+ description => <<"Whether the client uses a new session">>
+ })},
+ {proto_name,
+ hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
+ in => query,
+ required => false,
+ description =>
+ <<"Client protocol name, ",
+ "the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>
+ })},
+ {proto_ver,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Client protocol version">>
+ })},
+ {like_clientid,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Fuzzy search `clientid` as substring">>
+ })},
+ {like_username,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Fuzzy search `username` as substring">>
+ })},
+ {gte_created_at,
+ hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
+ in => query,
+ required => false,
+ desc =>
+ <<"Search client session creation time by greater",
+ " than or equal method, rfc3339 or timestamp(millisecond)">>
+ })},
+ {lte_created_at,
+ hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
+ in => query,
+ required => false,
+ desc =>
+ <<"Search client session creation time by less",
+ " than or equal method, rfc3339 or timestamp(millisecond)">>
+ })},
+ {gte_connected_at,
+ hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
+ in => query,
+ required => false,
+ desc => <<
+ "Search client connection creation time by greater"
+ " than or equal method, rfc3339 or timestamp(epoch millisecond)"
+ >>
+ })},
+ {lte_connected_at,
+ hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
+ in => query,
+ required => false,
+ desc => <<
+ "Search client connection creation time by less"
+ " than or equal method, rfc3339 or timestamp(millisecond)"
+ >>
+ })}
],
responses => #{
200 => [
@@ -164,10 +207,11 @@ schema("/clients") ->
],
400 =>
emqx_dashboard_swagger:error_codes(
- ['INVALID_PARAMETER'], <<"Invalid parameters">>)}
+ ['INVALID_PARAMETER'], <<"Invalid parameters">>
+ )
+ }
}
};
-
schema("/clients/:clientid") ->
#{
'operationId' => client,
@@ -177,42 +221,47 @@ schema("/clients/:clientid") ->
responses => #{
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)}},
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
+ }
+ },
delete => #{
description => <<"Kick out client by client ID">>,
parameters => [
- {clientid, hoconsc:mk(binary(), #{in => path})}],
+ {clientid, hoconsc:mk(binary(), #{in => path})}
+ ],
responses => #{
204 => <<"Kick out client successfully">>,
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
};
-
-schema("/clients/:clientid/authz_cache") ->
+schema("/clients/:clientid/authorization/cache") ->
#{
'operationId' => authz_cache,
get => #{
- description => <<"Get client authz cache">>,
+ description => <<"Get client authz cache in the cluster.">>,
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
responses => #{
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
},
delete => #{
- description => <<"Clean client authz cache">>,
+ description => <<"Clean client authz cache in the cluster.">>,
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
responses => #{
204 => <<"Kick out client successfully">>,
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
};
-
schema("/clients/:clientid/subscriptions") ->
#{
'operationId' => subscriptions,
@@ -220,13 +269,15 @@ schema("/clients/:clientid/subscriptions") ->
description => <<"Get client subscriptions">>,
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
responses => #{
- 200 => hoconsc:mk(hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}),
+ 200 => hoconsc:mk(
+ hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}
+ ),
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
};
-
schema("/clients/:clientid/subscribe") ->
#{
'operationId' => subscribe,
@@ -237,11 +288,11 @@ schema("/clients/:clientid/subscribe") ->
responses => #{
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
};
-
schema("/clients/:clientid/unsubscribe") ->
#{
'operationId' => unsubscribe,
@@ -252,11 +303,11 @@ schema("/clients/:clientid/unsubscribe") ->
responses => #{
204 => <<"Unsubscribe OK">>,
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
};
-
schema("/clients/:clientid/keepalive") ->
#{
'operationId' => set_keepalive,
@@ -267,96 +318,187 @@ schema("/clients/:clientid/keepalive") ->
responses => #{
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
404 => emqx_dashboard_swagger:error_codes(
- ['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
+ ['CLIENTID_NOT_FOUND'], <<"Client id not found">>
+ )
}
}
}.
fields(client) ->
[
- {awaiting_rel_cnt, hoconsc:mk(integer(), #{desc =>
- <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>})},
- {awaiting_rel_max, hoconsc:mk(integer(), #{desc =>
- <<"v4 api name [max_awaiting_rel]. "
- "Maximum allowed number of awaiting PUBREC packet">>})},
- {clean_start, hoconsc:mk(boolean(), #{desc =>
- <<"Indicate whether the client is using a brand new session">>})},
+ {awaiting_rel_cnt,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>
+ })},
+ {awaiting_rel_max,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<
+ "v4 api name [max_awaiting_rel]. "
+ "Maximum allowed number of awaiting PUBREC packet"
+ >>
+ })},
+ {clean_start,
+ hoconsc:mk(boolean(), #{
+ desc =>
+ <<"Indicate whether the client is using a brand new session">>
+ })},
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
- {connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
- #{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>})},
- {created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
- #{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>})},
- {disconnected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{desc =>
- <<"Client offline time."
- " It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)">>})},
- {expiry_interval, hoconsc:mk(integer(), #{desc =>
- <<"Session expiration interval, with the unit of second">>})},
- {heap_size, hoconsc:mk(integer(), #{desc =>
- <<"Process heap size with the unit of byte">>})},
+ {connected_at,
+ hoconsc:mk(
+ emqx_datetime:epoch_millisecond(),
+ #{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>}
+ )},
+ {created_at,
+ hoconsc:mk(
+ emqx_datetime:epoch_millisecond(),
+ #{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>}
+ )},
+ {disconnected_at,
+ hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
+ desc =>
+ <<
+ "Client offline time."
+ " It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)"
+ >>
+ })},
+ {expiry_interval,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Session expiration interval, with the unit of second">>
+ })},
+ {heap_size,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Process heap size with the unit of byte">>
+ })},
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
- {inflight_max, hoconsc:mk(integer(), #{desc =>
- <<"v4 api name [max_inflight]. Maximum length of inflight">>})},
+ {inflight_max,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"v4 api name [max_inflight]. Maximum length of inflight">>
+ })},
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
- {is_bridge, hoconsc:mk(boolean(), #{desc =>
- <<"Indicates whether the client is connectedvia bridge">>})},
- {keepalive, hoconsc:mk(integer(), #{desc =>
- <<"keepalive time, with the unit of second">>})},
+ {is_bridge,
+ hoconsc:mk(boolean(), #{
+ desc =>
+ <<"Indicates whether the client is connectedvia bridge">>
+ })},
+ {keepalive,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"keepalive time, with the unit of second">>
+ })},
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
- {mqueue_dropped, hoconsc:mk(integer(), #{desc =>
- <<"Number of messages dropped by the message queue due to exceeding the length">>})},
+ {mqueue_dropped,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of messages dropped by the message queue due to exceeding the length">>
+ })},
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
- {mqueue_max, hoconsc:mk(integer(), #{desc =>
- <<"v4 api name [max_mqueue]. Maximum length of message queue">>})},
- {node, hoconsc:mk(binary(), #{desc =>
- <<"Name of the node to which the client is connected">>})},
+ {mqueue_max,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"v4 api name [max_mqueue]. Maximum length of message queue">>
+ })},
+ {node,
+ hoconsc:mk(binary(), #{
+ desc =>
+ <<"Name of the node to which the client is connected">>
+ })},
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
- {'recv_msg.dropped', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets">>})},
- {'recv_msg.dropped.await_pubrel_timeout', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets due to expired">>})},
- {'recv_msg.qos0', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS0 packets received">>})},
- {'recv_msg.qos1', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS1 packets received">>})},
- {'recv_msg.qos2', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS2 packets received">>})},
+ {'recv_msg.dropped',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets">>
+ })},
+ {'recv_msg.dropped.await_pubrel_timeout',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets due to expired">>
+ })},
+ {'recv_msg.qos0',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS0 packets received">>
+ })},
+ {'recv_msg.qos1',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS1 packets received">>
+ })},
+ {'recv_msg.qos2',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS2 packets received">>
+ })},
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
- {'send_msg.dropped', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets">>})},
- {'send_msg.dropped.expired', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets due to expired">>})},
- {'send_msg.dropped.queue_full', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets due to queue full">>})},
- {'send_msg.dropped.too_large', hoconsc:mk(integer(), #{desc =>
- <<"Number of dropped PUBLISH packets due to packet length too large">>})},
- {'send_msg.qos0', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS0 packets sent">>})},
- {'send_msg.qos1', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS1 packets sent">>})},
- {'send_msg.qos2', hoconsc:mk(integer(), #{desc =>
- <<"Number of PUBLISH QoS2 packets sent">>})},
+ {'send_msg.dropped',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets">>
+ })},
+ {'send_msg.dropped.expired',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets due to expired">>
+ })},
+ {'send_msg.dropped.queue_full',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets due to queue full">>
+ })},
+ {'send_msg.dropped.too_large',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of dropped PUBLISH packets due to packet length too large">>
+ })},
+ {'send_msg.qos0',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS0 packets sent">>
+ })},
+ {'send_msg.qos1',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS1 packets sent">>
+ })},
+ {'send_msg.qos2',
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of PUBLISH QoS2 packets sent">>
+ })},
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
- {subscriptions_cnt, hoconsc:mk(integer(), #{desc =>
- <<"Number of subscriptions established by this client.">>})},
- {subscriptions_max, hoconsc:mk(integer(), #{desc =>
- <<"v4 api name [max_subscriptions]",
- " Maximum number of subscriptions allowed by this client">>})},
+ {subscriptions_cnt,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"Number of subscriptions established by this client.">>
+ })},
+ {subscriptions_max,
+ hoconsc:mk(integer(), #{
+ desc =>
+ <<"v4 api name [max_subscriptions]",
+ " Maximum number of subscriptions allowed by this client">>
+ })},
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
- {zone, hoconsc:mk(binary(), #{desc =>
- <<"Indicate the configuration group used by the client">>})}
+ {zone,
+ hoconsc:mk(binary(), #{
+ desc =>
+ <<"Indicate the configuration group used by the client">>
+ })}
];
-
fields(authz_cache) ->
[
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
@@ -364,23 +506,19 @@ fields(authz_cache) ->
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
];
-
fields(keepalive) ->
[
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
];
-
fields(subscribe) ->
[
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
];
-
fields(unsubscribe) ->
[
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
];
-
fields(meta) ->
emqx_dashboard_swagger:fields(page) ++
emqx_dashboard_swagger:fields(limit) ++
@@ -393,13 +531,11 @@ clients(get, #{query_string := QString}) ->
client(get, #{bindings := Bindings}) ->
lookup(Bindings);
-
client(delete, #{bindings := Bindings}) ->
kickout(Bindings).
authz_cache(get, #{bindings := Bindings}) ->
get_authz_cache(Bindings);
-
authz_cache(delete, #{bindings := Bindings}) ->
clean_authz_cache(Bindings).
@@ -415,11 +551,14 @@ unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) ->
%% TODO: batch
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
Topics =
- [begin
- Topic = maps:get(<<"topic">>, TopicInfo),
- Qos = maps:get(<<"qos">>, TopicInfo, 0),
- #{topic => Topic, qos => Qos}
- end || TopicInfo <- TopicInfos],
+ [
+ begin
+ Topic = maps:get(<<"topic">>, TopicInfo),
+ Qos = maps:get(<<"qos">>, TopicInfo, 0),
+ #{topic => Topic, qos => Qos}
+ end
+ || TopicInfo <- TopicInfos
+ ],
subscribe_batch(#{clientid => ClientID, topics => Topics}).
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
@@ -436,16 +575,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
qos => maps:get(qos, SubOpts)
}
end,
- {200, lists:map(Formatter, Subs)}
+ {200, lists:map(Formatter, Subs)}
end.
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
case maps:find(<<"interval">>, Body) of
- error -> {400, 'BAD_REQUEST',"Interval Not Found"};
+ error ->
+ {400, 'BAD_REQUEST', "Interval Not Found"};
{ok, Interval} ->
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
ok -> lookup(#{clientid => ClientID});
- {error, not_found} ->{404, ?CLIENT_ID_NOT_FOUND};
+ {error, not_found} -> {404, ?CLIENT_ID_NOT_FOUND};
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
end
end.
@@ -454,17 +594,34 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
%% api apply
list_clients(QString) ->
- case maps:get(<<"node">>, QString, undefined) of
- undefined ->
- Response = emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB,
- ?CLIENT_QSCHEMA, ?QUERY_FUN),
- emqx_mgmt_util:generate_response(Response);
- Node1 ->
- Node = binary_to_atom(Node1, utf8),
- QStringWithoutNode = maps:without([<<"node">>], QString),
- Response = emqx_mgmt_api:node_query(Node, QStringWithoutNode,
- ?CLIENT_QTAB, ?CLIENT_QSCHEMA, ?QUERY_FUN),
- emqx_mgmt_util:generate_response(Response)
+ Result =
+ case maps:get(<<"node">>, QString, undefined) of
+ undefined ->
+ emqx_mgmt_api:cluster_query(
+ QString,
+ ?CLIENT_QTAB,
+ ?CLIENT_QSCHEMA,
+ ?QUERY_FUN
+ );
+ Node0 ->
+ Node1 = binary_to_atom(Node0, utf8),
+ QStringWithoutNode = maps:without([<<"node">>], QString),
+ emqx_mgmt_api:node_query(
+ Node1,
+ QStringWithoutNode,
+ ?CLIENT_QTAB,
+ ?CLIENT_QSCHEMA,
+ ?QUERY_FUN
+ )
+ end,
+ case Result of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Response ->
+ {200, Response}
end.
lookup(#{clientid := ClientID}) ->
@@ -483,7 +640,7 @@ kickout(#{clientid := ClientID}) ->
{204}
end.
-get_authz_cache(#{clientid := ClientID})->
+get_authz_cache(#{clientid := ClientID}) ->
case emqx_mgmt:list_authz_cache(ClientID) of
{error, not_found} ->
{404, ?CLIENT_ID_NOT_FOUND};
@@ -517,9 +674,9 @@ subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) ->
Response =
#{
clientid => ClientID,
- topic => Topic,
- qos => Qos,
- node => Node
+ topic => Topic,
+ qos => Qos,
+ node => Node
},
{200, Response}
end.
@@ -555,7 +712,7 @@ do_subscribe(ClientID, Topic0, Qos) ->
end.
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
- {unsubscribe, _} | {error, channel_not_found}.
+ {unsubscribe, _} | {error, channel_not_found}.
do_unsubscribe(ClientID, Topic) ->
case emqx_mgmt:unsubscribe(ClientID, Topic) of
{error, Reason} ->
@@ -569,14 +726,23 @@ do_unsubscribe(ClientID, Topic) ->
query(Tab, {QString, []}, Continuation, Limit) ->
Ms = qs2ms(QString),
- emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
- fun format_channel_info/1);
-
+ emqx_mgmt_api:select_table_with_count(
+ Tab,
+ Ms,
+ Continuation,
+ Limit,
+ fun format_channel_info/1
+ );
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
Ms = qs2ms(QString),
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
- emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
- fun format_channel_info/1).
+ emqx_mgmt_api:select_table_with_count(
+ Tab,
+ {Ms, FuzzyFilterFun},
+ Continuation,
+ Limit,
+ fun format_channel_info/1
+ ).
%%--------------------------------------------------------------------
%% QueryString to Match Spec
@@ -588,7 +754,6 @@ qs2ms(Qs) ->
qs2ms([], _, {MtchHead, Conds}) ->
{MtchHead, lists:reverse(Conds)};
-
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
qs2ms(Rest, N, {NMtchHead, Conds});
@@ -596,13 +761,16 @@ qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
NConds = put_conds(Qs, Holder, Conds),
- qs2ms(Rest, N+1, {NMtchHead, NConds}).
+ qs2ms(Rest, N + 1, {NMtchHead, NConds}).
put_conds({_, Op, V}, Holder, Conds) ->
[{Op, Holder, V} | Conds];
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
- [{Op2, Holder, V2},
- {Op1, Holder, V1} | Conds].
+ [
+ {Op2, Holder, V2},
+ {Op1, Holder, V1}
+ | Conds
+ ].
ms(clientid, X) ->
#{clientinfo => #{clientid => X}};
@@ -630,68 +798,78 @@ ms(created_at, X) ->
fuzzy_filter_fun(Fuzzy) ->
fun(MsRaws) when is_list(MsRaws) ->
- lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
- , MsRaws)
+ lists:filter(
+ fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
+ MsRaws
+ )
end.
run_fuzzy_filter(_, []) ->
true;
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
- Val = case maps:get(Key, ClientInfo, <<>>) of
- undefined -> <<>>;
- V -> V
- end,
+ Val =
+ case maps:get(Key, ClientInfo, <<>>) of
+ undefined -> <<>>;
+ V -> V
+ end,
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
%%--------------------------------------------------------------------
%% format funcs
format_channel_info({_, ClientInfo, ClientStats}) ->
- Node = case ClientInfo of
- #{node := N} -> N;
- _ -> node()
- end,
- StatsMap = maps:without([memory, next_pkt_id, total_heap_size],
- maps:from_list(ClientStats)),
+ Node =
+ case ClientInfo of
+ #{node := N} -> N;
+ _ -> node()
+ end,
+ StatsMap = maps:without(
+ [memory, next_pkt_id, total_heap_size],
+ maps:from_list(ClientStats)
+ ),
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
- Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
+ Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
- ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
+ ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
RemoveList =
- [ auth_result
- , peername
- , sockname
- , peerhost
- , conn_state
- , send_pend
- , conn_props
- , peercert
- , sockstate
- , subscriptions
- , receive_maximum
- , protocol
- , is_superuser
- , sockport
- , anonymous
- , mountpoint
- , socktype
- , active_n
- , await_rel_timeout
- , conn_mod
- , sockname
- , retry_interval
- , upgrade_qos
- , id %% sessionID, defined in emqx_session.erl
- ],
+ [
+ auth_result,
+ peername,
+ sockname,
+ peerhost,
+ conn_state,
+ send_pend,
+ conn_props,
+ peercert,
+ sockstate,
+ subscriptions,
+ receive_maximum,
+ protocol,
+ is_superuser,
+ sockport,
+ anonymous,
+ mountpoint,
+ socktype,
+ active_n,
+ await_rel_timeout,
+ conn_mod,
+ sockname,
+ retry_interval,
+ upgrade_qos,
+ %% sessionID, defined in emqx_session.erl
+ id
+ ],
TimesKeys = [created_at, connected_at, disconnected_at],
%% format timestamp to rfc3339
- lists:foldl(fun result_format_time_fun/2
- , maps:without(RemoveList, ClientInfoMap)
- , TimesKeys).
+ lists:foldl(
+ fun result_format_time_fun/2,
+ maps:without(RemoveList, ClientInfoMap),
+ TimesKeys
+ ).
%% format func helpers
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
@@ -703,20 +881,22 @@ result_format_time_fun(Key, NClientInfoMap) ->
case NClientInfoMap of
#{Key := TimeStamp} ->
NClientInfoMap#{
- Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)};
+ Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)
+ };
#{} ->
NClientInfoMap
end.
--spec(peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}).
+-spec peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}.
peername_dispart({Addr, Port}) ->
AddrBinary = list_to_binary(inet:ntoa(Addr)),
%% PortBinary = integer_to_binary(Port),
{AddrBinary, Port}.
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
- #{ access => PubSub,
- topic => Topic,
- result => AuthzResult,
- updated_time => Timestamp
- }.
+ #{
+ access => PubSub,
+ topic => Topic,
+ result => AuthzResult,
+ updated_time => Timestamp
+ }.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_cluster.erl b/apps/emqx_management/src/emqx_mgmt_api_cluster.erl
index 0c6cd151d..25ec5c9a2 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_cluster.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_cluster.erl
@@ -125,7 +125,7 @@ force_leave(delete, #{bindings := #{node := Node0}}) ->
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
end.
--spec(join(node()) -> ok | ignore | {error, term()}).
+-spec join(node()) -> ok | ignore | {error, term()}.
join(Node) ->
ekka:join(Node).
diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl
index 21febd865..5143ff892 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl
@@ -23,11 +23,13 @@
-export([api_spec/0, namespace/0]).
-export([paths/0, schema/1, fields/1]).
--export([ config/3
- , config_reset/3
- , configs/3
- , get_full_config/0
- , global_zone_configs/3]).
+-export([
+ config/3,
+ config_reset/3,
+ configs/3,
+ get_full_config/0,
+ global_zone_configs/3
+]).
-export([gen_schema/1]).
@@ -36,29 +38,30 @@
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
--define(EXCLUDES, [
- <<"exhook">>,
- <<"gateway">>,
- <<"plugins">>,
- <<"bridges">>,
- <<"rule_engine">>,
- <<"authorization">>,
- <<"authentication">>,
- <<"rpc">>,
- <<"db">>,
- <<"connectors">>,
- <<"slow_subs">>,
- <<"psk_authentication">>,
- <<"topic_metrics">>,
- <<"rewrite">>,
- <<"auto_subscribe">>,
- <<"retainer">>,
- <<"statsd">>,
- <<"delayed">>,
- <<"event_message">>,
- <<"prometheus">>,
- <<"telemetry">>,
- <<"sys_topics">>
+-define(EXCLUDES,
+ [
+ <<"exhook">>,
+ <<"gateway">>,
+ <<"plugins">>,
+ <<"bridges">>,
+ <<"rule_engine">>,
+ <<"authorization">>,
+ <<"authentication">>,
+ <<"rpc">>,
+ <<"db">>,
+ <<"connectors">>,
+ <<"slow_subs">>,
+ <<"psk_authentication">>,
+ <<"topic_metrics">>,
+ <<"rewrite">>,
+ <<"auto_subscribe">>,
+ <<"retainer">>,
+ <<"statsd">>,
+ <<"delayed">>,
+ <<"event_message">>,
+ <<"prometheus">>,
+ <<"telemetry">>,
+ <<"sys_topics">>
] ++ global_zone_roots()
).
@@ -69,7 +72,7 @@ namespace() -> "configuration".
paths() ->
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
- lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
+ lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
schema("/configs") ->
#{
@@ -77,12 +80,20 @@ schema("/configs") ->
get => #{
tags => [conf],
description =>
-<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
+ <<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
parameters => [
- {node, hoconsc:mk(typerefl:atom(),
- #{in => query, required => false, example => <<"emqx@127.0.0.1">>,
- desc =>
- <<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
+ {node,
+ hoconsc:mk(
+ typerefl:atom(),
+ #{
+ in => query,
+ required => false,
+ example => <<"emqx@127.0.0.1">>,
+ desc =>
+ <<"Node's name: If you do not fill in the fields, this node will be used by default.">>
+ }
+ )}
+ ],
responses => #{
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
}
@@ -95,18 +106,29 @@ schema("/configs_reset/:rootname") ->
post => #{
tags => [conf],
description =>
-<<"Reset the config entry specified by the query string parameter `conf_path`.
-- For a config entry that has default value, this resets it to the default value;
-- For a config entry that has no default value, an error 400 will be returned">>,
+ <<"Reset the config entry specified by the query string parameter `conf_path`.
\n"
+ "- For a config entry that has default value, this resets it to the default value;\n"
+ "- For a config entry that has no default value, an error 400 will be returned">>,
%% We only return "200" rather than the new configs that has been changed, as
%% the schema of the changed configs is depends on the request parameter
%% `conf_path`, it cannot be defined here.
parameters => [
- {rootname, hoconsc:mk( hoconsc:enum(Paths)
- , #{in => path, example => <<"sysmon">>})},
- {conf_path, hoconsc:mk(typerefl:binary(),
- #{in => query, required => false, example => <<"os.sysmem_high_watermark">>,
- desc => <<"The config path separated by '.' character">>})}],
+ {rootname,
+ hoconsc:mk(
+ hoconsc:enum(Paths),
+ #{in => path, example => <<"sysmon">>}
+ )},
+ {conf_path,
+ hoconsc:mk(
+ typerefl:binary(),
+ #{
+ in => query,
+ required => false,
+ example => <<"os.sysmem_high_watermark">>,
+ desc => <<"The config path separated by '.' character">>
+ }
+ )}
+ ],
responses => #{
200 => <<"Rest config successfully">>,
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
@@ -138,9 +160,11 @@ schema(Path) ->
'operationId' => config,
get => #{
tags => [conf],
- description => iolist_to_binary([ <<"Get the sub-configurations under *">>
- , RootKey
- , <<"*">>]),
+ description => iolist_to_binary([
+ <<"Get the sub-configurations under *">>,
+ RootKey,
+ <<"*">>
+ ]),
responses => #{
200 => Schema,
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
@@ -148,9 +172,11 @@ schema(Path) ->
},
put => #{
tags => [conf],
- description => iolist_to_binary([ <<"Update the sub-configurations under *">>
- , RootKey
- , <<"*">>]),
+ description => iolist_to_binary([
+ <<"Update the sub-configurations under *">>,
+ RootKey,
+ <<"*">>
+ ]),
'requestBody' => Schema,
responses => #{
200 => Schema,
@@ -177,7 +203,6 @@ config(get, _Params, Req) ->
Path = conf_path(Req),
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
{200, Conf};
-
config(put, #{body := Body}, Req) ->
Path = conf_path(Req),
case emqx_conf:update(Path, Body, ?OPTS) of
@@ -189,21 +214,32 @@ config(put, #{body := Body}, Req) ->
global_zone_configs(get, _Params, _Req) ->
Paths = global_zone_roots(),
- Zones = lists:foldl(fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
- #{}, Paths),
+ Zones = lists:foldl(
+ fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
+ #{},
+ Paths
+ ),
{200, Zones};
global_zone_configs(put, #{body := Body}, _Req) ->
Res =
- maps:fold(fun(Path, Value, Acc) ->
- case emqx_conf:update([Path], Value, ?OPTS) of
- {ok, #{raw_config := RawConf}} ->
- Acc#{Path => RawConf};
- {error, Reason} ->
- ?SLOG(error, #{msg => "update global zone failed", reason => Reason,
- path => Path, value => Value}),
- Acc
- end
- end, #{}, Body),
+ maps:fold(
+ fun(Path, Value, Acc) ->
+ case emqx_conf:update([Path], Value, ?OPTS) of
+ {ok, #{raw_config := RawConf}} ->
+ Acc#{Path => RawConf};
+ {error, Reason} ->
+ ?SLOG(error, #{
+ msg => "update global zone failed",
+ reason => Reason,
+ path => Path,
+ value => Value
+ }),
+ Acc
+ end
+ end,
+ #{},
+ Body
+ ),
case maps:size(Res) =:= maps:size(Body) of
true -> {200, Res};
false -> {400, #{code => 'UPDATE_FAILED'}}
@@ -213,7 +249,8 @@ config_reset(post, _Params, Req) ->
%% reset the config specified by the query string param 'conf_path'
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
case emqx:reset_config(Path, #{}) of
- {ok, _} -> {200};
+ {ok, _} ->
+ {200};
{error, no_default_value} ->
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
{error, Reason} ->
@@ -223,9 +260,8 @@ config_reset(post, _Params, Req) ->
configs(get, Params, _Req) ->
Node = maps:get(node, Params, node()),
case
- lists:member(Node, mria_mnesia:running_nodes())
- andalso
- emqx_management_proto_v1:get_full_config(Node)
+ lists:member(Node, mria_mnesia:running_nodes()) andalso
+ emqx_management_proto_v1:get_full_config(Node)
of
false ->
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
@@ -243,8 +279,11 @@ conf_path_reset(Req) ->
get_full_config() ->
emqx_config:fill_defaults(
- maps:without(?EXCLUDES,
- emqx:get_raw_config([]))).
+ maps:without(
+ ?EXCLUDES,
+ emqx:get_raw_config([])
+ )
+ ).
get_config_with_default(Path) ->
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
@@ -279,8 +318,11 @@ gen_schema(Conf) when is_list(Conf) ->
#{type => array, items => gen_schema(hd(Conf))}
end;
gen_schema(Conf) when is_map(Conf) ->
- #{type => object, properties =>
- maps:map(fun(_K, V) -> gen_schema(V) end, Conf)};
+ #{
+ type => object,
+ properties =>
+ maps:map(fun(_K, V) -> gen_schema(V) end, Conf)
+ };
gen_schema(_Conf) ->
%% the conf is not of JSON supported type, it may have been converted
%% by the hocon schema
diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl
index a2dbce1aa..9cdb26ed6 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl
@@ -22,12 +22,10 @@
-import(emqx_dashboard_swagger, [error_codes/2, error_codes/1]).
-export([
+ listener_type_status/2,
list_listeners/2,
crud_listeners_by_id/2,
- list_listeners_on_node/2,
- crud_listener_by_id_on_node/2,
- action_listeners_by_id/2,
- action_listeners_by_id_on_node/2
+ action_listeners_by_id/2
]).
%% for rpc call
@@ -55,21 +53,35 @@ api_spec() ->
paths() ->
[
+ "/listeners_status",
"/listeners",
"/listeners/:id",
- "/listeners/:id/:action",
- "/nodes/:node/listeners",
- "/nodes/:node/listeners/:id",
- "/nodes/:node/listeners/:id/:action"
+ "/listeners/:id/:action"
].
+schema("/listeners_status") ->
+ #{
+ 'operationId' => listener_type_status,
+ get => #{
+ tags => [<<"listeners">>],
+ desc => <<"List all running node's listeners live status. group by listener type">>,
+ responses => #{200 => ?HOCON(?ARRAY(?R_REF(listener_type_status)))}
+ }
+ };
schema("/listeners") ->
#{
'operationId' => list_listeners,
get => #{
tags => [<<"listeners">>],
- desc => <<"List all running node's listeners.">>,
- responses => #{200 => ?HOCON(?ARRAY(?R_REF(listeners)))}
+ desc => <<"List all running node's listeners for the specified type.">>,
+ parameters => [
+ {type,
+ ?HOCON(
+ ?ENUM(listeners_type()),
+ #{desc => "Listener type", in => query, required => false}
+ )}
+ ],
+ responses => #{200 => ?HOCON(?ARRAY(?R_REF(listener_id_status)))}
}
};
schema("/listeners/:id") ->
@@ -80,17 +92,29 @@ schema("/listeners/:id") ->
desc => <<"List all running node's listeners for the specified id.">>,
parameters => [?R_REF(listener_id)],
responses => #{
- 200 => ?HOCON(?ARRAY(?R_REF(listeners)))
+ 200 => ?HOCON(listener_schema(#{bind => true})),
+ 404 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
}
},
put => #{
tags => [<<"listeners">>],
- desc => <<"Create or update the specified listener on all nodes.">>,
+ desc => <<"Update the specified listener on all nodes.">>,
parameters => [?R_REF(listener_id)],
- 'requestBody' => ?HOCON(listener_schema(), #{}),
+ 'requestBody' => ?HOCON(listener_schema(#{bind => false}), #{}),
responses => #{
- 200 => ?HOCON(listener_schema(), #{}),
- 400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
+ 200 => ?HOCON(listener_schema(#{bind => true}), #{}),
+ 400 => error_codes(['BAD_REQUEST']),
+ 404 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'], ?LISTENER_NOT_FOUND)
+ }
+ },
+ post => #{
+ tags => [<<"listeners">>],
+ desc => <<"Create the specified listener on all nodes.">>,
+ parameters => [?R_REF(listener_id)],
+ 'requestBody' => ?HOCON(listener_schema(#{bind => true}), #{}),
+ responses => #{
+ 200 => ?HOCON(listener_schema(#{bind => true}), #{}),
+ 400 => error_codes(['BAD_LISTENER_ID', 'BAD_REQUEST'])
}
},
delete => #{
@@ -118,90 +142,8 @@ schema("/listeners/:id/:action") ->
400 => error_codes(['BAD_REQUEST'])
}
}
- };
-schema("/nodes/:node/listeners") ->
- #{
- 'operationId' => list_listeners_on_node,
- get => #{
- tags => [<<"listeners">>],
- desc => <<"List all listeners on the specified node.">>,
- parameters => [?R_REF(node)],
- responses => #{
- 200 => ?HOCON(?ARRAY(listener_schema())),
- 400 => error_codes(['BAD_NODE', 'BAD_REQUEST'], ?NODE_NOT_FOUND_OR_DOWN)
- }
- }
- };
-schema("/nodes/:node/listeners/:id") ->
- #{
- 'operationId' => crud_listener_by_id_on_node,
- get => #{
- tags => [<<"listeners">>],
- desc => <<"Get the specified listener on the specified node.">>,
- parameters => [
- ?R_REF(listener_id),
- ?R_REF(node)
- ],
- responses => #{
- 200 => ?HOCON(listener_schema()),
- 400 => error_codes(['BAD_REQUEST']),
- 404 => error_codes(['BAD_LISTEN_ID'], ?NODE_LISTENER_NOT_FOUND)
- }
- },
- put => #{
- tags => [<<"listeners">>],
- desc => <<"Create or update the specified listener on the specified node.">>,
- parameters => [
- ?R_REF(listener_id),
- ?R_REF(node)
- ],
- 'requestBody' => ?HOCON(listener_schema()),
- responses => #{
- 200 => ?HOCON(listener_schema()),
- 400 => error_codes(['BAD_REQUEST'])
- }
- },
- delete => #{
- tags => [<<"listeners">>],
- desc => <<"Delete the specified listener on the specified node.">>,
- parameters => [
- ?R_REF(listener_id),
- ?R_REF(node)
- ],
- responses => #{
- 204 => <<"Listener deleted">>,
- 400 => error_codes(['BAD_REQUEST'])
- }
- }
- };
-schema("/nodes/:node/listeners/:id/:action") ->
- #{
- 'operationId' => action_listeners_by_id_on_node,
- post => #{
- tags => [<<"listeners">>],
- desc => <<"Start/stop/restart listeners on a specified node.">>,
- parameters => [
- ?R_REF(node),
- ?R_REF(listener_id),
- ?R_REF(action)
- ],
- responses => #{
- 200 => <<"Updated">>,
- 400 => error_codes(['BAD_REQUEST'])
- }
- }
}.
-fields(listeners) ->
- [
- {"node",
- ?HOCON(atom(), #{
- desc => "Node name",
- example => "emqx@127.0.0.1",
- required => true
- })},
- {"listeners", ?ARRAY(listener_schema())}
- ];
fields(listener_id) ->
[
{id,
@@ -230,23 +172,56 @@ fields(node) ->
in => path
})}
];
+fields(listener_type_status) ->
+ [
+ {type, ?HOCON(?ENUM(listeners_type()), #{desc => "Listener type", required => true})},
+ {enable, ?HOCON(boolean(), #{desc => "Listener enable", required => true})},
+ {ids, ?HOCON(?ARRAY(string()), #{desc => "Listener Ids", required => true})},
+ {status, ?HOCON(?R_REF(status))},
+ {node_status, ?HOCON(?ARRAY(?R_REF(node_status)))}
+ ];
+fields(listener_id_status) ->
+ fields(listener_id) ++
+ [
+ {enable, ?HOCON(boolean(), #{desc => "Listener enable", required => true})},
+ {number, ?HOCON(typerefl:pos_integer(), #{desc => "ListenerId number"})},
+ {status, ?HOCON(?R_REF(status))},
+ {node_status, ?HOCON(?ARRAY(?R_REF(node_status)))}
+ ];
+fields(status) ->
+ [
+ {max_connections,
+ ?HOCON(hoconsc:union([infinity, integer()]), #{desc => "Max connections"})},
+ {current_connections, ?HOCON(non_neg_integer(), #{desc => "Current connections"})}
+ ];
+fields(node_status) ->
+ fields(node) ++ fields(status);
fields(Type) ->
- Listeners = listeners_info(),
+ Listeners = listeners_info(#{bind => true}) ++ listeners_info(#{bind => false}),
[Schema] = [S || #{ref := ?R_REF(_, T), schema := S} <- Listeners, T =:= Type],
Schema.
-listener_schema() ->
- ?UNION(lists:map(fun(#{ref := Ref}) -> Ref end, listeners_info())).
+listener_schema(Opts) ->
+ ?UNION(lists:map(fun(#{ref := Ref}) -> Ref end, listeners_info(Opts))).
-listeners_info() ->
+listeners_type() ->
+ lists:map(
+ fun({Type, _}) -> list_to_existing_atom(Type) end,
+ hocon_schema:fields(emqx_schema, "listeners")
+ ).
+
+listeners_info(Opts) ->
Listeners = hocon_schema:fields(emqx_schema, "listeners"),
lists:map(
fun({Type, #{type := ?MAP(_Name, ?R_REF(Mod, Field))}}) ->
Fields0 = hocon_schema:fields(Mod, Field),
Fields1 = lists:keydelete("authentication", 1, Fields0),
+ Fields2 = lists:keydelete("limiter", 1, Fields1),
+ Fields3 = required_bind(Fields2, Opts),
+ Ref = listeners_ref(Type, Opts),
TypeAtom = list_to_existing_atom(Type),
#{
- ref => ?R_REF(TypeAtom),
+ ref => ?R_REF(Ref),
schema => [
{type, ?HOCON(?ENUM([TypeAtom]), #{desc => "Listener type", required => true})},
{running, ?HOCON(boolean(), #{desc => "Listener status", required => false})},
@@ -255,14 +230,33 @@ listeners_info() ->
desc => "Listener id",
required => true,
validator => fun validate_id/1
- })}
- | Fields1
+ })},
+ {current_connections,
+ ?HOCON(
+ non_neg_integer(),
+ #{desc => "Current connections", required => false}
+ )}
+ | Fields3
]
}
end,
Listeners
).
+required_bind(Fields, #{bind := true}) ->
+ Fields;
+required_bind(Fields, #{bind := false}) ->
+ {value, {_, Hocon}, Fields1} = lists:keytake("bind", 1, Fields),
+ [{"bind", Hocon#{required => false}} | Fields1].
+
+listeners_ref(Type, #{bind := Bind}) ->
+ Suffix =
+ case Bind of
+ true -> "_required_bind";
+ false -> "_not_required_bind"
+ end,
+ Type ++ Suffix.
+
validate_id(Id) ->
case emqx_listeners:parse_listener_id(Id) of
{error, Reason} -> {error, Reason};
@@ -270,19 +264,65 @@ validate_id(Id) ->
end.
%% api
-list_listeners(get, _Request) ->
- {200, list_listeners()}.
+listener_type_status(get, _Request) ->
+ Listeners = maps:to_list(listener_status_by_type(list_listeners(), #{})),
+ List = lists:map(fun({Type, L}) -> L#{type => Type} end, Listeners),
+ {200, List}.
-crud_listeners_by_id(get, #{bindings := #{id := Id}}) ->
- {200, list_listeners_by_id(Id)};
+list_listeners(get, #{query_string := Query}) ->
+ Listeners = list_listeners(),
+ NodeL =
+ case maps:find(<<"type">>, Query) of
+ {ok, Type} -> listener_type_filter(atom_to_binary(Type), Listeners);
+ error -> Listeners
+ end,
+ {200, listener_status_by_id(NodeL)}.
+
+crud_listeners_by_id(get, #{bindings := #{id := Id0}}) ->
+ Listeners = [
+ Conf#{<<"id">> => Id, <<"type">> => Type}
+ || {Id, Type, Conf} <- emqx_listeners:list_raw(),
+ Id =:= Id0
+ ],
+ case Listeners of
+ [] -> {404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
+ [L] -> {200, L}
+ end;
crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Body0}) ->
case parse_listener_conf(Body0) of
{Id, Type, Name, Conf} ->
- case emqx_conf:update([listeners, Type, Name], Conf, ?OPTS(cluster)) of
- {ok, #{raw_config := _RawConf}} ->
- crud_listeners_by_id(get, #{bindings => #{id => Id}});
- {error, Reason} ->
- {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
+ Key = [listeners, Type, Name],
+ case emqx_conf:get_raw(Key, undefined) of
+ undefined ->
+ {404, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_NOT_FOUND}};
+ PrevConf ->
+ MergeConf = emqx_map_lib:deep_merge(PrevConf, Conf),
+ case emqx_conf:update(Key, MergeConf, ?OPTS(cluster)) of
+ {ok, #{raw_config := _RawConf}} ->
+ crud_listeners_by_id(get, #{bindings => #{id => Id}});
+ {error, Reason} ->
+ {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
+ end
+ end;
+ {error, Reason} ->
+ {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
+ _ ->
+ {400, #{code => 'BAD_LISTENER_ID', message => ?LISTENER_ID_INCONSISTENT}}
+ end;
+crud_listeners_by_id(post, #{bindings := #{id := Id}, body := Body0}) ->
+ case parse_listener_conf(Body0) of
+ {Id, Type, Name, Conf} ->
+ Key = [listeners, Type, Name],
+ case emqx_conf:get(Key, undefined) of
+ undefined ->
+ case emqx_conf:update([listeners, Type, Name], Conf, ?OPTS(cluster)) of
+ {ok, #{raw_config := _RawConf}} ->
+ crud_listeners_by_id(get, #{bindings => #{id => Id}});
+ {error, Reason} ->
+ {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
+ end;
+ _ ->
+ {400, #{code => 'BAD_LISTENER_ID', message => <<"Already Exist">>}}
end;
{error, Reason} ->
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
@@ -298,64 +338,16 @@ crud_listeners_by_id(delete, #{bindings := #{id := Id}}) ->
parse_listener_conf(Conf0) ->
Conf1 = maps:remove(<<"running">>, Conf0),
- {IdBin, Conf2} = maps:take(<<"id">>, Conf1),
- {TypeBin, Conf3} = maps:take(<<"type">>, Conf2),
+ Conf2 = maps:remove(<<"current_connections">>, Conf1),
+ {IdBin, Conf3} = maps:take(<<"id">>, Conf2),
+ {TypeBin, Conf4} = maps:take(<<"type">>, Conf3),
{ok, #{type := Type, name := Name}} = emqx_listeners:parse_listener_id(IdBin),
TypeAtom = binary_to_existing_atom(TypeBin),
case Type =:= TypeAtom of
- true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf3};
+ true -> {binary_to_existing_atom(IdBin), TypeAtom, Name, Conf4};
false -> {error, listener_type_inconsistent}
end.
-list_listeners_on_node(get, #{bindings := #{node := Node}}) ->
- case list_listeners(Node) of
- {error, nodedown} ->
- {400, #{code => 'BAD_NODE', message => ?NODE_NOT_FOUND_OR_DOWN}};
- {error, Reason} ->
- {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
- #{<<"listeners">> := Listener} ->
- {200, Listener}
- end.
-
-crud_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) ->
- case get_listener(Node, Id) of
- {error, not_found} ->
- {404, #{code => 'BAD_LISTEN_ID', message => ?NODE_LISTENER_NOT_FOUND}};
- {error, Reason} ->
- {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
- Listener ->
- {200, Listener}
- end;
-crud_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node}, body := Body}) ->
- case parse_listener_conf(Body) of
- {error, Reason} ->
- {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
- {Id, Type, _Name, Conf} ->
- case update_listener(Node, Id, Conf) of
- {error, nodedown} ->
- {400, #{code => 'BAD_REQUEST', message => ?NODE_NOT_FOUND_OR_DOWN}};
- %% TODO
- {error, {eaddrinuse, _}} ->
- {400, #{code => 'BAD_REQUEST', message => ?ADDR_PORT_INUSE}};
- {error, Reason} ->
- {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
- {ok, Listener} ->
- {200, Listener#{<<"id">> => Id, <<"type">> => Type, <<"running">> => true}}
- end;
- _ ->
- {400, #{code => 'BAD_REQUEST', message => ?LISTENER_ID_INCONSISTENT}}
- end;
-crud_listener_by_id_on_node(delete, #{bindings := #{id := Id, node := Node}}) ->
- case remove_listener(Node, Id) of
- ok -> {204};
- {error, Reason} -> {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}
- end.
-
-action_listeners_by_id_on_node(post,
- #{bindings := #{id := Id, action := Action, node := Node}}) ->
- {_, Result} = action_listeners(Node, Id, Action),
- Result.
-
action_listeners_by_id(post, #{bindings := #{id := Id, action := Action}}) ->
Results = [action_listeners(Node, Id, Action) || Node <- mria_mnesia:running_nodes()],
case
@@ -418,31 +410,49 @@ list_listeners() ->
list_listeners(Node) ->
wrap_rpc(emqx_management_proto_v1:list_listeners(Node)).
-list_listeners_by_id(Id) ->
- listener_id_filter(Id, list_listeners()).
-
-get_listener(Node, Id) ->
- case listener_id_filter(Id, [list_listeners(Node)]) of
- [#{<<"listeners">> := []}] -> {error, not_found};
- [#{<<"listeners">> := [Listener]}] -> Listener
- end.
-
-listener_id_filter(Id, Listeners) ->
+listener_status_by_id(NodeL) ->
+ Listeners = maps:to_list(listener_status_by_id(NodeL, #{})),
lists:map(
- fun(Conf = #{<<"listeners">> := Listeners0}) ->
- Conf#{
- <<"listeners">> =>
- [C || C = #{<<"id">> := Id0} <- Listeners0, Id =:= Id0]
- }
+ fun({Id, L}) ->
+ L1 = maps:remove(ids, L),
+ #{node_status := Nodes} = L1,
+ L1#{number => maps:size(Nodes), id => Id}
end,
Listeners
).
-update_listener(Node, Id, Config) ->
- wrap_rpc(emqx_management_proto_v1:update_listener(Node, Id, Config)).
+listener_status_by_type([], Acc) ->
+ Acc;
+listener_status_by_type([NodeL | Rest], Acc) ->
+ #{<<"node">> := Node, <<"listeners">> := Listeners} = NodeL,
+ Acc1 = lists:foldl(
+ fun(L, Acc0) -> format_status(<<"type">>, Node, L, Acc0) end,
+ Acc,
+ Listeners
+ ),
+ listener_status_by_type(Rest, Acc1).
-remove_listener(Node, Id) ->
- wrap_rpc(emqx_management_proto_v1:remove_listener(Node, Id)).
+listener_status_by_id([], Acc) ->
+ Acc;
+listener_status_by_id([NodeL | Rest], Acc) ->
+ #{<<"node">> := Node, <<"listeners">> := Listeners} = NodeL,
+ Acc1 = lists:foldl(
+ fun(L, Acc0) -> format_status(<<"id">>, Node, L, Acc0) end,
+ Acc,
+ Listeners
+ ),
+ listener_status_by_id(Rest, Acc1).
+
+listener_type_filter(Type0, Listeners) ->
+ lists:map(
+ fun(Conf = #{<<"listeners">> := Listeners0}) ->
+ Conf#{
+ <<"listeners">> =>
+ [C || C = #{<<"type">> := Type} <- Listeners0, Type =:= Type0]
+ }
+ end,
+ Listeners
+ ).
-spec do_list_listeners() -> map().
do_list_listeners() ->
@@ -476,3 +486,75 @@ wrap_rpc({badrpc, Reason}) ->
{error, Reason};
wrap_rpc(Res) ->
Res.
+
+format_status(Key, Node, Listener, Acc) ->
+ #{
+ <<"id">> := Id,
+ <<"running">> := Running,
+ <<"max_connections">> := MaxConnections,
+ <<"current_connections">> := CurrentConnections
+ } = Listener,
+ GroupKey = maps:get(Key, Listener),
+ case maps:find(GroupKey, Acc) of
+ error ->
+ Acc#{
+ GroupKey => #{
+ enable => Running,
+ ids => [Id],
+ status => #{
+ max_connections => MaxConnections,
+ current_connections => CurrentConnections
+ },
+ node_status => #{
+ Node => #{
+ max_connections => MaxConnections,
+ current_connections => CurrentConnections
+ }
+ }
+ }
+ };
+ {ok, GroupValue} ->
+ #{
+ ids := Ids,
+ status := #{
+ max_connections := MaxConnections0,
+ current_connections := CurrentConnections0
+ },
+ node_status := NodeStatus0
+ } = GroupValue,
+ NodeStatus =
+ case maps:find(Node, NodeStatus0) of
+ error ->
+ #{
+ Node => #{
+ max_connections => MaxConnections,
+ current_connections => CurrentConnections
+ }
+ };
+ {ok, #{
+ max_connections := PrevMax,
+ current_connections := PrevCurr
+ }} ->
+ NodeStatus0#{
+ Node => #{
+ max_connections => max_conn(MaxConnections, PrevMax),
+ current_connections => CurrentConnections + PrevCurr
+ }
+ }
+ end,
+ Acc#{
+ GroupKey =>
+ GroupValue#{
+ ids => lists:usort([Id | Ids]),
+ status => #{
+ max_connections => max_conn(MaxConnections0, MaxConnections),
+ current_connections => CurrentConnections0 + CurrentConnections
+ },
+ node_status => NodeStatus
+ }
+ }
+ end.
+
+max_conn(_Int1, infinity) -> infinity;
+max_conn(infinity, _Int) -> infinity;
+max_conn(Int1, Int2) -> Int1 + Int2.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl
index ee087efa9..69ce7723c 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_metrics.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_metrics.erl
@@ -23,14 +23,16 @@
-import(hoconsc, [mk/2, ref/2]).
%% minirest/dashbaord_swagger behaviour callbacks
--export([ api_spec/0
- , paths/0
- , schema/1
- ]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1
+]).
--export([ roots/0
- , fields/1
- ]).
+-export([
+ roots/0,
+ fields/1
+]).
%% http handlers
-export([metrics/2]).
@@ -53,9 +55,12 @@ metrics(get, #{query_string := Qs}) ->
true ->
{200, emqx_mgmt:get_metrics()};
false ->
- Data = [maps:from_list(
- emqx_mgmt:get_metrics(Node) ++ [{node, Node}])
- || Node <- mria_mnesia:running_nodes()],
+ Data = [
+ maps:from_list(
+ emqx_mgmt:get_metrics(Node) ++ [{node, Node}]
+ )
+ || Node <- mria_mnesia:running_nodes()
+ ],
{200, Data}
end.
@@ -64,23 +69,34 @@ metrics(get, #{query_string := Qs}) ->
%%--------------------------------------------------------------------
schema("/metrics") ->
- #{ 'operationId' => metrics
- , get =>
- #{ description => <<"EMQX metrics">>
- , parameters =>
- [{ aggregate
- , mk( boolean()
- , #{ in => query
- , required => false
- , desc => <<"Whether to aggregate all nodes Metrics">>})
- }]
- , responses =>
- #{ 200 => hoconsc:union(
- [ref(?MODULE, aggregated_metrics),
- hoconsc:array(ref(?MODULE, node_metrics))])
- }
+ #{
+ 'operationId' => metrics,
+ get =>
+ #{
+ description => <<"EMQX metrics">>,
+ parameters =>
+ [
+ {aggregate,
+ mk(
+ boolean(),
+ #{
+ in => query,
+ required => false,
+ desc => <<"Whether to aggregate all nodes Metrics">>
+ }
+ )}
+ ],
+ responses =>
+ #{
+ 200 => hoconsc:union(
+ [
+ ref(?MODULE, aggregated_metrics),
+ hoconsc:array(ref(?MODULE, node_metrics))
+ ]
+ )
+ }
}
- }.
+ }.
roots() ->
[].
@@ -91,177 +107,354 @@ fields(node_metrics) ->
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
properties() ->
- [ m('actions.failure',
- <<"Number of failure executions of the rule engine action">>)
- , m('actions.success',
- <<"Number of successful executions of the rule engine action">>)
- , m('bytes.received',
- <<"Number of bytes received ">>)
- , m('bytes.sent',
- <<"Number of bytes sent on this connection">>)
- , m('client.auth.anonymous',
- <<"Number of clients who log in anonymously">>)
- , m('client.authenticate',
- <<"Number of client authentications">>)
- , m('client.check_authz',
- <<"Number of Authorization rule checks">>)
- , m('client.connack',
- <<"Number of CONNACK packet sent">>)
- , m('client.connect',
- <<"Number of client connections">>)
- , m('client.connected',
- <<"Number of successful client connections">>)
- , m('client.disconnected',
- <<"Number of client disconnects">>)
- , m('client.subscribe',
- <<"Number of client subscriptions">>)
- , m('client.unsubscribe',
- <<"Number of client unsubscriptions">>)
- , m('delivery.dropped',
- <<"Total number of discarded messages when sending">>)
- , m('delivery.dropped.expired',
- <<"Number of messages dropped due to message expiration on sending">>)
- , m('delivery.dropped.no_local',
- <<"Number of messages that were dropped due to the No Local subscription "
- "option when sending">>)
- , m('delivery.dropped.qos0_msg',
- <<"Number of messages with QoS 0 that were dropped because the message "
- "queue was full when sending">>)
- , m('delivery.dropped.queue_full',
- <<"Number of messages with a non-zero QoS that were dropped because the "
- "message queue was full when sending">>)
- , m('delivery.dropped.too_large',
- <<"The number of messages that were dropped because the length exceeded "
- "the limit when sending">>)
- , m('messages.acked',
- <<"Number of received PUBACK and PUBREC packet">>)
- , m('messages.delayed',
- <<"Number of delay-published messages">>)
- , m('messages.delivered',
- <<"Number of messages forwarded to the subscription process internally">>)
- , m('messages.dropped',
- <<"Total number of messages dropped before forwarding to the subscription process">>)
- , m('messages.dropped.await_pubrel_timeout',
- <<"Number of messages dropped due to waiting PUBREL timeout">>)
- , m('messages.dropped.no_subscribers',
- <<"Number of messages dropped due to no subscribers">>)
- , m('messages.forward',
- <<"Number of messages forwarded to other nodes">>)
- , m('messages.publish',
- <<"Number of messages published in addition to system messages">>)
- , m('messages.qos0.received',
- <<"Number of QoS 0 messages received from clients">>)
- , m('messages.qos0.sent',
- <<"Number of QoS 0 messages sent to clients">>)
- , m('messages.qos1.received',
- <<"Number of QoS 1 messages received from clients">>)
- , m('messages.qos1.sent',
- <<"Number of QoS 1 messages sent to clients">>)
- , m('messages.qos2.received',
- <<"Number of QoS 2 messages received from clients">>)
- , m('messages.qos2.sent',
- <<"Number of QoS 2 messages sent to clients">>)
- , m('messages.received',
- <<"Number of messages received from the client, equal to the sum of "
- "messages.qos0.received\fmessages.qos1.received and messages.qos2.received">>)
- , m('messages.retained',
- <<"Number of retained messages">>)
- , m('messages.sent',
- <<"Number of messages sent to the client, equal to the sum of "
- "messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent">>)
- , m('packets.auth.received',
- <<"Number of received AUTH packet">>)
- , m('packets.auth.sent',
- <<"Number of sent AUTH packet">>)
- , m('packets.connack.auth_error',
- <<"Number of received CONNECT packet with failed authentication">>)
- , m('packets.connack.error',
- <<"Number of received CONNECT packet with unsuccessful connections">>)
- , m('packets.connack.sent',
- <<"Number of sent CONNACK packet">>)
- , m('packets.connect.received',
- <<"Number of received CONNECT packet">>)
- , m('packets.disconnect.received',
- <<"Number of received DISCONNECT packet">>)
- , m('packets.disconnect.sent',
- <<"Number of sent DISCONNECT packet">>)
- , m('packets.pingreq.received',
- <<"Number of received PINGREQ packet">>)
- , m('packets.pingresp.sent',
- <<"Number of sent PUBRESP packet">>)
- , m('packets.puback.inuse',
- <<"Number of received PUBACK packet with occupied identifiers">>)
- , m('packets.puback.missed',
- <<"Number of received packet with identifiers.">>)
- , m('packets.puback.received',
- <<"Number of received PUBACK packet">>)
- , m('packets.puback.sent',
- <<"Number of sent PUBACK packet">>)
- , m('packets.pubcomp.inuse',
- <<"Number of received PUBCOMP packet with occupied identifiers">>)
- , m('packets.pubcomp.missed',
- <<"Number of missed PUBCOMP packet">>)
- , m('packets.pubcomp.received',
- <<"Number of received PUBCOMP packet">>)
- , m('packets.pubcomp.sent',
- <<"Number of sent PUBCOMP packet">>)
- , m('packets.publish.auth_error',
- <<"Number of received PUBLISH packets with failed the Authorization check">>)
- , m('packets.publish.dropped',
- <<"Number of messages discarded due to the receiving limit">>)
- , m('packets.publish.error',
- <<"Number of received PUBLISH packet that cannot be published">>)
- , m('packets.publish.inuse',
- <<"Number of received PUBLISH packet with occupied identifiers">>)
- , m('packets.publish.received',
- <<"Number of received PUBLISH packet">>)
- , m('packets.publish.sent',
- <<"Number of sent PUBLISH packet">>)
- , m('packets.pubrec.inuse',
- <<"Number of received PUBREC packet with occupied identifiers">>)
- , m('packets.pubrec.missed',
- <<"Number of received PUBREC packet with unknown identifiers">>)
- , m('packets.pubrec.received',
- <<"Number of received PUBREC packet">>)
- , m('packets.pubrec.sent',
- <<"Number of sent PUBREC packet">>)
- , m('packets.pubrel.missed',
- <<"Number of received PUBREC packet with unknown identifiers">>)
- , m('packets.pubrel.received',
- <<"Number of received PUBREL packet">>)
- , m('packets.pubrel.sent',
- <<"Number of sent PUBREL packet">>)
- , m('packets.received',
- <<"Number of received packet">>)
- , m('packets.sent',
- <<"Number of sent packet">>)
- , m('packets.suback.sent',
- <<"Number of sent SUBACK packet">>)
- , m('packets.subscribe.auth_error',
- <<"Number of received SUBACK packet with failed Authorization check">>)
- , m('packets.subscribe.error',
- <<"Number of received SUBSCRIBE packet with failed subscriptions">>)
- , m('packets.subscribe.received',
- <<"Number of received SUBSCRIBE packet">>)
- , m('packets.unsuback.sent',
- <<"Number of sent UNSUBACK packet">>)
- , m('packets.unsubscribe.error',
- <<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>)
- , m('packets.unsubscribe.received',
- <<"Number of received UNSUBSCRIBE packet">>)
- , m('rules.matched',
- <<"Number of rule matched">>)
- , m('session.created',
- <<"Number of sessions created">>)
- , m('session.discarded',
- <<"Number of sessions dropped because Clean Session or Clean Start is true">>)
- , m('session.resumed',
- <<"Number of sessions resumed because Clean Session or Clean Start is false">>)
- , m('session.takenover',
- <<"Number of sessions takenover because Clean Session or Clean Start is false">>)
- , m('session.terminated',
- <<"Number of terminated sessions">>)
+ [
+ m(
+ 'actions.failure',
+ <<"Number of failure executions of the rule engine action">>
+ ),
+ m(
+ 'actions.success',
+ <<"Number of successful executions of the rule engine action">>
+ ),
+ m(
+ 'bytes.received',
+ <<"Number of bytes received ">>
+ ),
+ m(
+ 'bytes.sent',
+ <<"Number of bytes sent on this connection">>
+ ),
+ m(
+ 'client.auth.anonymous',
+ <<"Number of clients who log in anonymously">>
+ ),
+ m(
+ 'client.authenticate',
+ <<"Number of client authentications">>
+ ),
+ m(
+ 'client.check_authz',
+ <<"Number of Authorization rule checks">>
+ ),
+ m(
+ 'client.connack',
+ <<"Number of CONNACK packet sent">>
+ ),
+ m(
+ 'client.connect',
+ <<"Number of client connections">>
+ ),
+ m(
+ 'client.connected',
+ <<"Number of successful client connections">>
+ ),
+ m(
+ 'client.disconnected',
+ <<"Number of client disconnects">>
+ ),
+ m(
+ 'client.subscribe',
+ <<"Number of client subscriptions">>
+ ),
+ m(
+ 'client.unsubscribe',
+ <<"Number of client unsubscriptions">>
+ ),
+ m(
+ 'delivery.dropped',
+ <<"Total number of discarded messages when sending">>
+ ),
+ m(
+ 'delivery.dropped.expired',
+ <<"Number of messages dropped due to message expiration on sending">>
+ ),
+ m(
+ 'delivery.dropped.no_local',
+ <<
+ "Number of messages that were dropped due to the No Local subscription "
+ "option when sending"
+ >>
+ ),
+ m(
+ 'delivery.dropped.qos0_msg',
+ <<
+ "Number of messages with QoS 0 that were dropped because the message "
+ "queue was full when sending"
+ >>
+ ),
+ m(
+ 'delivery.dropped.queue_full',
+ <<
+ "Number of messages with a non-zero QoS that were dropped because the "
+ "message queue was full when sending"
+ >>
+ ),
+ m(
+ 'delivery.dropped.too_large',
+ <<
+ "The number of messages that were dropped because the length exceeded "
+ "the limit when sending"
+ >>
+ ),
+ m(
+ 'messages.acked',
+ <<"Number of received PUBACK and PUBREC packet">>
+ ),
+ m(
+ 'messages.delayed',
+ <<"Number of delay-published messages">>
+ ),
+ m(
+ 'messages.delivered',
+ <<"Number of messages forwarded to the subscription process internally">>
+ ),
+ m(
+ 'messages.dropped',
+ <<"Total number of messages dropped before forwarding to the subscription process">>
+ ),
+ m(
+ 'messages.dropped.await_pubrel_timeout',
+ <<"Number of messages dropped due to waiting PUBREL timeout">>
+ ),
+ m(
+ 'messages.dropped.no_subscribers',
+ <<"Number of messages dropped due to no subscribers">>
+ ),
+ m(
+ 'messages.forward',
+ <<"Number of messages forwarded to other nodes">>
+ ),
+ m(
+ 'messages.publish',
+ <<"Number of messages published in addition to system messages">>
+ ),
+ m(
+ 'messages.qos0.received',
+ <<"Number of QoS 0 messages received from clients">>
+ ),
+ m(
+ 'messages.qos0.sent',
+ <<"Number of QoS 0 messages sent to clients">>
+ ),
+ m(
+ 'messages.qos1.received',
+ <<"Number of QoS 1 messages received from clients">>
+ ),
+ m(
+ 'messages.qos1.sent',
+ <<"Number of QoS 1 messages sent to clients">>
+ ),
+ m(
+ 'messages.qos2.received',
+ <<"Number of QoS 2 messages received from clients">>
+ ),
+ m(
+ 'messages.qos2.sent',
+ <<"Number of QoS 2 messages sent to clients">>
+ ),
+ m(
+ 'messages.received',
+ <<
+ "Number of messages received from the client, equal to the sum of "
+ "messages.qos0.received\fmessages.qos1.received and messages.qos2.received"
+ >>
+ ),
+ m(
+ 'messages.retained',
+ <<"Number of retained messages">>
+ ),
+ m(
+ 'messages.sent',
+ <<
+ "Number of messages sent to the client, equal to the sum of "
+ "messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent"
+ >>
+ ),
+ m(
+ 'packets.auth.received',
+ <<"Number of received AUTH packet">>
+ ),
+ m(
+ 'packets.auth.sent',
+ <<"Number of sent AUTH packet">>
+ ),
+ m(
+ 'packets.connack.auth_error',
+ <<"Number of received CONNECT packet with failed authentication">>
+ ),
+ m(
+ 'packets.connack.error',
+ <<"Number of received CONNECT packet with unsuccessful connections">>
+ ),
+ m(
+ 'packets.connack.sent',
+ <<"Number of sent CONNACK packet">>
+ ),
+ m(
+ 'packets.connect.received',
+ <<"Number of received CONNECT packet">>
+ ),
+ m(
+ 'packets.disconnect.received',
+ <<"Number of received DISCONNECT packet">>
+ ),
+ m(
+ 'packets.disconnect.sent',
+ <<"Number of sent DISCONNECT packet">>
+ ),
+ m(
+ 'packets.pingreq.received',
+ <<"Number of received PINGREQ packet">>
+ ),
+ m(
+ 'packets.pingresp.sent',
+ <<"Number of sent PUBRESP packet">>
+ ),
+ m(
+ 'packets.puback.inuse',
+ <<"Number of received PUBACK packet with occupied identifiers">>
+ ),
+ m(
+ 'packets.puback.missed',
+ <<"Number of received packet with identifiers.">>
+ ),
+ m(
+ 'packets.puback.received',
+ <<"Number of received PUBACK packet">>
+ ),
+ m(
+ 'packets.puback.sent',
+ <<"Number of sent PUBACK packet">>
+ ),
+ m(
+ 'packets.pubcomp.inuse',
+ <<"Number of received PUBCOMP packet with occupied identifiers">>
+ ),
+ m(
+ 'packets.pubcomp.missed',
+ <<"Number of missed PUBCOMP packet">>
+ ),
+ m(
+ 'packets.pubcomp.received',
+ <<"Number of received PUBCOMP packet">>
+ ),
+ m(
+ 'packets.pubcomp.sent',
+ <<"Number of sent PUBCOMP packet">>
+ ),
+ m(
+ 'packets.publish.auth_error',
+ <<"Number of received PUBLISH packets with failed the Authorization check">>
+ ),
+ m(
+ 'packets.publish.dropped',
+ <<"Number of messages discarded due to the receiving limit">>
+ ),
+ m(
+ 'packets.publish.error',
+ <<"Number of received PUBLISH packet that cannot be published">>
+ ),
+ m(
+ 'packets.publish.inuse',
+ <<"Number of received PUBLISH packet with occupied identifiers">>
+ ),
+ m(
+ 'packets.publish.received',
+ <<"Number of received PUBLISH packet">>
+ ),
+ m(
+ 'packets.publish.sent',
+ <<"Number of sent PUBLISH packet">>
+ ),
+ m(
+ 'packets.pubrec.inuse',
+ <<"Number of received PUBREC packet with occupied identifiers">>
+ ),
+ m(
+ 'packets.pubrec.missed',
+ <<"Number of received PUBREC packet with unknown identifiers">>
+ ),
+ m(
+ 'packets.pubrec.received',
+ <<"Number of received PUBREC packet">>
+ ),
+ m(
+ 'packets.pubrec.sent',
+ <<"Number of sent PUBREC packet">>
+ ),
+ m(
+ 'packets.pubrel.missed',
+ <<"Number of received PUBREC packet with unknown identifiers">>
+ ),
+ m(
+ 'packets.pubrel.received',
+ <<"Number of received PUBREL packet">>
+ ),
+ m(
+ 'packets.pubrel.sent',
+ <<"Number of sent PUBREL packet">>
+ ),
+ m(
+ 'packets.received',
+ <<"Number of received packet">>
+ ),
+ m(
+ 'packets.sent',
+ <<"Number of sent packet">>
+ ),
+ m(
+ 'packets.suback.sent',
+ <<"Number of sent SUBACK packet">>
+ ),
+ m(
+ 'packets.subscribe.auth_error',
+ <<"Number of received SUBACK packet with failed Authorization check">>
+ ),
+ m(
+ 'packets.subscribe.error',
+ <<"Number of received SUBSCRIBE packet with failed subscriptions">>
+ ),
+ m(
+ 'packets.subscribe.received',
+ <<"Number of received SUBSCRIBE packet">>
+ ),
+ m(
+ 'packets.unsuback.sent',
+ <<"Number of sent UNSUBACK packet">>
+ ),
+ m(
+ 'packets.unsubscribe.error',
+ <<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>
+ ),
+ m(
+ 'packets.unsubscribe.received',
+ <<"Number of received UNSUBSCRIBE packet">>
+ ),
+ m(
+ 'rules.matched',
+ <<"Number of rule matched">>
+ ),
+ m(
+ 'session.created',
+ <<"Number of sessions created">>
+ ),
+ m(
+ 'session.discarded',
+ <<"Number of sessions dropped because Clean Session or Clean Start is true">>
+ ),
+ m(
+ 'session.resumed',
+ <<"Number of sessions resumed because Clean Session or Clean Start is false">>
+ ),
+ m(
+ 'session.takenover',
+ <<"Number of sessions takenover because Clean Session or Clean Start is false">>
+ ),
+ m(
+ 'session.terminated',
+ <<"Number of terminated sessions">>
+ )
].
m(K, Desc) ->
- {K, mk(integer(), #{desc => Desc})}.
+ {K, mk(non_neg_integer(), #{desc => Desc})}.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl
index dba1cc5a8..5813e6522 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_nodes.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_nodes.erl
@@ -28,18 +28,20 @@
-define(SOURCE_ERROR, 'SOURCE_ERROR').
%% Swagger specs from hocon schema
--export([ api_spec/0
- , schema/1
- , paths/0
- , fields/1
- ]).
+-export([
+ api_spec/0,
+ schema/1,
+ paths/0,
+ fields/1
+]).
%% API callbacks
--export([ nodes/2
- , node/2
- , node_metrics/2
- , node_stats/2
- ]).
+-export([
+ nodes/2,
+ node/2,
+ node_metrics/2,
+ node_stats/2
+]).
%%--------------------------------------------------------------------
%% API spec funcs
@@ -49,123 +51,183 @@ api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
paths() ->
- [ "/nodes"
- , "/nodes/:node"
- , "/nodes/:node/metrics"
- , "/nodes/:node/stats"
+ [
+ "/nodes",
+ "/nodes/:node",
+ "/nodes/:node/metrics",
+ "/nodes/:node/stats"
].
schema("/nodes") ->
- #{ 'operationId' => nodes
- , get =>
- #{ description => <<"List EMQX nodes">>
- , responses =>
- #{200 => mk( array(ref(node_info))
- , #{desc => <<"List all EMQX nodes">>})}
+ #{
+ 'operationId' => nodes,
+ get =>
+ #{
+ description => <<"List EMQX nodes">>,
+ responses =>
+ #{
+ 200 => mk(
+ array(ref(node_info)),
+ #{desc => <<"List all EMQX nodes">>}
+ )
+ }
}
- };
+ };
schema("/nodes/:node") ->
- #{ 'operationId' => node
- , get =>
- #{ description => <<"Get node info">>
- , parameters => [ref(node_name)]
- , responses =>
- #{ 200 => mk( ref(node_info)
- , #{desc => <<"Get node info successfully">>})
- , 400 => node_error()
- }
+ #{
+ 'operationId' => node,
+ get =>
+ #{
+ description => <<"Get node info">>,
+ parameters => [ref(node_name)],
+ responses =>
+ #{
+ 200 => mk(
+ ref(node_info),
+ #{desc => <<"Get node info successfully">>}
+ ),
+ 400 => node_error()
+ }
}
- };
+ };
schema("/nodes/:node/metrics") ->
- #{ 'operationId' => node_metrics
- , get =>
- #{ description => <<"Get node metrics">>
- , parameters => [ref(node_name)]
- , responses =>
- #{ 200 => mk( ref(?NODE_METRICS_MODULE, node_metrics)
- , #{desc => <<"Get node metrics successfully">>})
- , 400 => node_error()
- }
+ #{
+ 'operationId' => node_metrics,
+ get =>
+ #{
+ description => <<"Get node metrics">>,
+ parameters => [ref(node_name)],
+ responses =>
+ #{
+ 200 => mk(
+ ref(?NODE_METRICS_MODULE, node_metrics),
+ #{desc => <<"Get node metrics successfully">>}
+ ),
+ 400 => node_error()
+ }
}
- };
+ };
schema("/nodes/:node/stats") ->
- #{ 'operationId' => node_stats
- , get =>
- #{ description => <<"Get node stats">>
- , parameters => [ref(node_name)]
- , responses =>
- #{ 200 => mk( ref(?NODE_STATS_MODULE, node_stats_data)
- , #{desc => <<"Get node stats successfully">>})
- , 400 => node_error()
- }
+ #{
+ 'operationId' => node_stats,
+ get =>
+ #{
+ description => <<"Get node stats">>,
+ parameters => [ref(node_name)],
+ responses =>
+ #{
+ 200 => mk(
+ ref(?NODE_STATS_MODULE, node_stats_data),
+ #{desc => <<"Get node stats successfully">>}
+ ),
+ 400 => node_error()
+ }
}
- }.
+ }.
%%--------------------------------------------------------------------
%% Fields
fields(node_name) ->
- [ { node
- , mk(atom()
- , #{ in => path
- , description => <<"Node name">>
- , required => true
- , example => <<"emqx@127.0.0.1">>
- })
- }
+ [
+ {node,
+ mk(
+ atom(),
+ #{
+ in => path,
+ description => <<"Node name">>,
+ required => true,
+ example => <<"emqx@127.0.0.1">>
+ }
+ )}
];
fields(node_info) ->
- [ { node
- , mk( atom()
- , #{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>})}
- , { connections
- , mk( non_neg_integer()
- , #{desc => <<"Number of clients currently connected to this node">>, example => 0})}
- , { load1
- , mk( string()
- , #{desc => <<"CPU average load in 1 minute">>, example => "2.66"})}
- , { load5
- , mk( string()
- , #{desc => <<"CPU average load in 5 minute">>, example => "2.66"})}
- , { load15
- , mk( string()
- , #{desc => <<"CPU average load in 15 minute">>, example => "2.66"})}
- , { max_fds
- , mk( non_neg_integer()
- , #{desc => <<"File descriptors limit">>, example => 1024})}
- , { memory_total
- , mk( emqx_schema:bytesize()
- , #{desc => <<"Allocated memory">>, example => "512.00M"})}
- , { memory_used
- , mk( emqx_schema:bytesize()
- , #{desc => <<"Used memory">>, example => "256.00M"})}
- , { node_status
- , mk( enum(['Running', 'Stopped'])
- , #{desc => <<"Node status">>, example => "Running"})}
- , { otp_release
- , mk( string()
- , #{ desc => <<"Erlang/OTP version">>, example => "24.2/12.2"})}
- , { process_available
- , mk( non_neg_integer()
- , #{desc => <<"Erlang processes limit">>, example => 2097152})}
- , { process_used
- , mk( non_neg_integer()
- , #{desc => <<"Running Erlang processes">>, example => 1024})}
- , { uptime
- , mk( non_neg_integer()
- , #{desc => <<"System uptime, milliseconds">>, example => 5120000})}
- , { version
- , mk( string()
- , #{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"})}
- , { sys_path
- , mk( string()
- , #{desc => <<"Path to system files">>, example => "path/to/emqx"})}
- , { log_path
- , mk( string()
- , #{desc => <<"Path to log files">>, example => "path/to/log | not found"})}
- , { role
- , mk( enum([core, replicant])
- , #{desc => <<"Node role">>, example => "core"})}
+ [
+ {node,
+ mk(
+ atom(),
+ #{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>}
+ )},
+ {connections,
+ mk(
+ non_neg_integer(),
+ #{desc => <<"Number of clients currently connected to this node">>, example => 0}
+ )},
+ {load1,
+ mk(
+ string(),
+ #{desc => <<"CPU average load in 1 minute">>, example => "2.66"}
+ )},
+ {load5,
+ mk(
+ string(),
+ #{desc => <<"CPU average load in 5 minute">>, example => "2.66"}
+ )},
+ {load15,
+ mk(
+ string(),
+ #{desc => <<"CPU average load in 15 minute">>, example => "2.66"}
+ )},
+ {max_fds,
+ mk(
+ non_neg_integer(),
+ #{desc => <<"File descriptors limit">>, example => 1024}
+ )},
+ {memory_total,
+ mk(
+ emqx_schema:bytesize(),
+ #{desc => <<"Allocated memory">>, example => "512.00M"}
+ )},
+ {memory_used,
+ mk(
+ emqx_schema:bytesize(),
+ #{desc => <<"Used memory">>, example => "256.00M"}
+ )},
+ {node_status,
+ mk(
+ enum(['Running', 'Stopped']),
+ #{desc => <<"Node status">>, example => "Running"}
+ )},
+ {otp_release,
+ mk(
+ string(),
+ #{desc => <<"Erlang/OTP version">>, example => "24.2/12.2"}
+ )},
+ {process_available,
+ mk(
+ non_neg_integer(),
+ #{desc => <<"Erlang processes limit">>, example => 2097152}
+ )},
+ {process_used,
+ mk(
+ non_neg_integer(),
+ #{desc => <<"Running Erlang processes">>, example => 1024}
+ )},
+ {uptime,
+ mk(
+ non_neg_integer(),
+ #{desc => <<"System uptime, milliseconds">>, example => 5120000}
+ )},
+ {version,
+ mk(
+ string(),
+ #{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"}
+ )},
+ {sys_path,
+ mk(
+ string(),
+ #{desc => <<"Path to system files">>, example => "path/to/emqx"}
+ )},
+ {log_path,
+ mk(
+ string(),
+ #{desc => <<"Path to log files">>, example => "path/to/log | not found"}
+ )},
+ {role,
+ mk(
+ enum([core, replicant]),
+ #{desc => <<"Node role">>, example => "core"}
+ )}
].
%%--------------------------------------------------------------------
@@ -221,17 +283,20 @@ get_stats(Node) ->
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
{ok, SysPathBinary} = file:get_cwd(),
SysPath = list_to_binary(SysPathBinary),
- LogPath = case log_path() of
- undefined ->
- <<"not found">>;
- Path0 ->
- Path = list_to_binary(Path0),
- <>
- end,
- Info#{ memory_total := emqx_mgmt_util:kmg(Total)
- , memory_used := emqx_mgmt_util:kmg(Used)
- , sys_path => SysPath
- , log_path => LogPath}.
+ LogPath =
+ case log_path() of
+ undefined ->
+ <<"not found">>;
+ Path0 ->
+ Path = list_to_binary(Path0),
+ <>
+ end,
+ Info#{
+ memory_total := emqx_mgmt_util:kmg(Total),
+ memory_used := emqx_mgmt_util:kmg(Used),
+ sys_path => SysPath,
+ log_path => LogPath
+ }.
log_path() ->
Configs = logger:get_handler_config(),
diff --git a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl
index b9415391e..ea9f1e0ad 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_plugins.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_plugins.erl
@@ -22,27 +22,30 @@
-include_lib("emqx/include/logger.hrl").
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
--export([ api_spec/0
- , fields/1
- , paths/0
- , schema/1
- , namespace/0
- ]).
+-export([
+ api_spec/0,
+ fields/1,
+ paths/0,
+ schema/1,
+ namespace/0
+]).
--export([ list_plugins/2
- , upload_install/2
- , plugin/2
- , update_plugin/2
- , update_boot_order/2
- ]).
+-export([
+ list_plugins/2,
+ upload_install/2,
+ plugin/2,
+ update_plugin/2,
+ update_boot_order/2
+]).
--export([ validate_name/1
- , get_plugins/0
- , install_package/2
- , delete_package/1
- , describe_package/1
- , ensure_action/2
- ]).
+-export([
+ validate_name/1,
+ get_plugins/0,
+ install_package/2,
+ delete_package/1,
+ describe_package/1,
+ ensure_action/2
+]).
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
@@ -65,9 +68,10 @@ schema("/plugins") ->
#{
'operationId' => list_plugins,
get => #{
- description => "List all install plugins.
"
- "Plugins are launched in top-down order.
"
- "Using `POST /plugins/{name}/move` to change the boot order.",
+ description =>
+ "List all install plugins.
"
+ "Plugins are launched in top-down order.
"
+ "Using `POST /plugins/{name}/move` to change the boot order.",
responses => #{
200 => hoconsc:array(hoconsc:ref(plugin))
}
@@ -77,20 +81,26 @@ schema("/plugins/install") ->
#{
'operationId' => upload_install,
post => #{
- description => "Install a plugin(plugin-vsn.tar.gz)."
- "Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
- "to develop plugin.",
+ description =>
+ "Install a plugin(plugin-vsn.tar.gz)."
+ "Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
+ "to develop plugin.",
'requestBody' => #{
content => #{
'multipart/form-data' => #{
schema => #{
type => object,
properties => #{
- plugin => #{type => string, format => binary}}},
- encoding => #{plugin => #{'contentType' => 'application/gzip'}}}}},
+ plugin => #{type => string, format => binary}
+ }
+ },
+ encoding => #{plugin => #{'contentType' => 'application/gzip'}}
+ }
+ }
+ },
responses => #{
200 => <<"OK">>,
- 400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR','ALREADY_INSTALLED'])
+ 400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR', 'ALREADY_INSTALLED'])
}
}
};
@@ -118,12 +128,14 @@ schema("/plugins/:name/:action") ->
#{
'operationId' => update_plugin,
put => #{
- description => "start/stop a installed plugin.
"
- "- **start**: start the plugin.
"
- "- **stop**: stop the plugin.
",
+ description =>
+ "start/stop a installed plugin.
"
+ "- **start**: start the plugin.
"
+ "- **stop**: stop the plugin.
",
parameters => [
hoconsc:ref(name),
- {action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}],
+ {action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}
+ ],
responses => #{
200 => <<"OK">>,
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
@@ -143,57 +155,83 @@ schema("/plugins/:name/move") ->
fields(plugin) ->
[
- {name, hoconsc:mk(binary(),
- #{
- desc => "Name-Vsn: without .tar.gz",
- validator => fun ?MODULE:validate_name/1,
- required => true,
- example => "emqx_plugin_template-5.0-rc.1"})
- },
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Name-Vsn: without .tar.gz",
+ validator => fun ?MODULE:validate_name/1,
+ required => true,
+ example => "emqx_plugin_template-5.0-rc.1"
+ }
+ )},
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
{builder, hoconsc:ref(?MODULE, builder)},
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
- {git_commit_or_build_date, hoconsc:mk(string(), #{
- example => "2021-12-25",
- desc => "Last git commit date by `git log -1 --pretty=format:'%cd' "
- "--date=format:'%Y-%m-%d`.\n"
- " If the last commit date is not available, the build date will be presented."
- })},
+ {git_commit_or_build_date,
+ hoconsc:mk(string(), #{
+ example => "2021-12-25",
+ desc =>
+ "Last git commit date by `git log -1 --pretty=format:'%cd' "
+ "--date=format:'%Y-%m-%d`.\n"
+ " If the last commit date is not available, the build date will be presented."
+ })},
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
- {rel_vsn, hoconsc:mk(binary(),
- #{desc => "Plugins release version",
- required => true,
- example => <<"5.0-rc.1">>})
- },
- {rel_apps, hoconsc:mk(hoconsc:array(binary()),
- #{desc => "Aplications in plugin.",
- required => true,
- example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]})
- },
+ {rel_vsn,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Plugins release version",
+ required => true,
+ example => <<"5.0-rc.1">>
+ }
+ )},
+ {rel_apps,
+ hoconsc:mk(
+ hoconsc:array(binary()),
+ #{
+ desc => "Aplications in plugin.",
+ required => true,
+ example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]
+ }
+ )},
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
- {description, hoconsc:mk(binary(),
- #{desc => "Plugin description.",
- required => true,
- example => "This is an demo plugin description"})
- },
- {running_status, hoconsc:mk(hoconsc:array(hoconsc:ref(running_status)),
- #{required => true})},
- {readme, hoconsc:mk(binary(), #{
- example => "This is an demo plugin.",
- desc => "only return when `GET /plugins/{name}`.",
- required => false})}
+ {description,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Plugin description.",
+ required => true,
+ example => "This is an demo plugin description"
+ }
+ )},
+ {running_status,
+ hoconsc:mk(
+ hoconsc:array(hoconsc:ref(running_status)),
+ #{required => true}
+ )},
+ {readme,
+ hoconsc:mk(binary(), #{
+ example => "This is an demo plugin.",
+ desc => "only return when `GET /plugins/{name}`.",
+ required => false
+ })}
];
fields(name) ->
- [{name, hoconsc:mk(binary(),
- #{
- desc => list_to_binary(?NAME_RE),
- example => "emqx_plugin_template-5.0-rc.1",
- in => path,
- validator => fun ?MODULE:validate_name/1
- })}
+ [
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => list_to_binary(?NAME_RE),
+ example => "emqx_plugin_template-5.0-rc.1",
+ in => path,
+ validator => fun ?MODULE:validate_name/1
+ }
+ )}
];
fields(builder) ->
[
@@ -202,27 +240,38 @@ fields(builder) ->
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
];
fields(position) ->
- [{position, hoconsc:mk(hoconsc:union([front, rear, binary()]),
- #{
- desc => """
- Enable auto-boot at position in the boot list, where Position could be
- 'front', 'rear', or 'before:other-vsn', 'after:other-vsn'
- to specify a relative position.
- """,
- required => false
- })}];
+ [
+ {position,
+ hoconsc:mk(
+ hoconsc:union([front, rear, binary()]),
+ #{
+ desc =>
+ ""
+ "\n"
+ " Enable auto-boot at position in the boot list, where Position could be\n"
+ " 'front', 'rear', or 'before:other-vsn', 'after:other-vsn'\n"
+ " to specify a relative position.\n"
+ " "
+ "",
+ required => false
+ }
+ )}
+ ];
fields(running_status) ->
[
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
- {status, hoconsc:mk(hoconsc:enum([running, stopped]), #{
- desc => "Install plugin status at runtime"
- "1. running: plugin is running.
"
- "2. stopped: plugin is stopped.
"
- })}
+ {status,
+ hoconsc:mk(hoconsc:enum([running, stopped]), #{
+ desc =>
+ "Install plugin status at runtime"
+ "1. running: plugin is running.
"
+ "2. stopped: plugin is stopped.
"
+ })}
].
move_request_body() ->
- emqx_dashboard_swagger:schema_with_examples(hoconsc:ref(?MODULE, position),
+ emqx_dashboard_swagger:schema_with_examples(
+ hoconsc:ref(?MODULE, position),
#{
move_to_front => #{
summary => <<"move plugin on the front">>,
@@ -240,7 +289,8 @@ move_request_body() ->
summary => <<"move plugin after other plugins">>,
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
}
- }).
+ }
+ ).
validate_name(Name) ->
NameLen = byte_size(Name),
@@ -250,7 +300,8 @@ validate_name(Name) ->
nomatch -> {error, "Name should be " ?NAME_RE};
_ -> ok
end;
- false -> {error, "Name Length must =< 256"}
+ false ->
+ {error, "Name Length must =< 256"}
end.
%% API CallBack Begin
@@ -271,29 +322,42 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) -
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
case filelib:wildcard(AppDir ++ "*.tar.gz") of
- [] -> do_install_package(FileName, Bin);
+ [] ->
+ do_install_package(FileName, Bin);
OtherVsn ->
- {400, #{code => 'ALREADY_INSTALLED',
- message => iolist_to_binary(io_lib:format("~p already installed",
- [OtherVsn]))}}
+ {400, #{
+ code => 'ALREADY_INSTALLED',
+ message => iolist_to_binary(
+ io_lib:format(
+ "~p already installed",
+ [OtherVsn]
+ )
+ )
+ }}
end;
{ok, _} ->
- {400, #{code => 'ALREADY_INSTALLED',
- message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))}}
+ {400, #{
+ code => 'ALREADY_INSTALLED',
+ message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))
+ }}
end;
upload_install(post, #{}) ->
- {400, #{code => 'BAD_FORM_DATA',
+ {400, #{
+ code => 'BAD_FORM_DATA',
message =>
- <<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>}
- }.
+ <<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>
+ }}.
do_install_package(FileName, Bin) ->
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
case lists:filter(fun(R) -> R =/= ok end, Res) of
- [] -> {200};
+ [] ->
+ {200};
[{error, Reason} | _] ->
- {400, #{code => 'UNEXPECTED_ERROR',
- message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
+ {400, #{
+ code => 'UNEXPECTED_ERROR',
+ message => iolist_to_binary(io_lib:format("~p", [Reason]))
+ }}
end.
plugin(get, #{bindings := #{name := Name}}) ->
@@ -302,7 +366,6 @@ plugin(get, #{bindings := #{name := Name}}) ->
[Plugin] -> {200, Plugin};
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
end;
-
plugin(delete, #{bindings := #{name := Name}}) ->
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
return(204, Res).
@@ -313,13 +376,17 @@ update_plugin(put, #{bindings := #{name := Name, action := Action}}) ->
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
case parse_position(Body, Name) of
- {error, Reason} -> {400, #{code => 'BAD_POSITION', message => Reason}};
+ {error, Reason} ->
+ {400, #{code => 'BAD_POSITION', message => Reason}};
Position ->
case emqx_plugins:ensure_enabled(Name, Position) of
- ok -> {200};
+ ok ->
+ {200};
{error, Reason} ->
- {400, #{code => 'MOVE_FAILED',
- message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
+ {400, #{
+ code => 'MOVE_FAILED',
+ message => iolist_to_binary(io_lib:format("~p", [Reason]))
+ }}
end
end.
@@ -347,7 +414,8 @@ delete_package(Name) ->
_ = emqx_plugins:ensure_disabled(Name),
_ = emqx_plugins:purge(Name),
_ = emqx_plugins:delete_package(Name);
- Error -> Error
+ Error ->
+ Error
end.
%% for RPC plugin update
@@ -361,15 +429,19 @@ ensure_action(Name, restart) ->
_ = emqx_plugins:ensure_enabled(Name),
_ = emqx_plugins:restart(Name).
-return(Code, ok) -> {Code};
-return(Code, {ok, Result}) -> {Code, Result};
+return(Code, ok) ->
+ {Code};
+return(Code, {ok, Result}) ->
+ {Code, Result};
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
{404, #{code => 'NOT_FOUND', message => Path}};
return(_, {error, Reason}) ->
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
-parse_position(#{<<"position">> := <<"front">>}, _) -> front;
-parse_position(#{<<"position">> := <<"rear">>}, _) -> rear;
+parse_position(#{<<"position">> := <<"front">>}, _) ->
+ front;
+parse_position(#{<<"position">> := <<"rear">>}, _) ->
+ rear;
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
{error, <<"Invalid parameter. Cannot be placed before itself">>};
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
@@ -382,7 +454,8 @@ parse_position(#{<<"position">> := <<"before:", Before/binary>>}, _Name) ->
{before, binary_to_list(Before)};
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
{behind, binary_to_list(After)};
-parse_position(Position, _) -> {error, iolist_to_binary(io_lib:format("~p", [Position]))}.
+parse_position(Position, _) ->
+ {error, iolist_to_binary(io_lib:format("~p", [Position]))}.
format_plugins(List) ->
StatusMap = aggregate_status(List),
@@ -392,13 +465,18 @@ format_plugins(List) ->
pack_status_in_order(List, StatusMap) ->
{Plugins, _} =
- lists:foldl(fun({_Node, PluginList}, {Acc, StatusAcc}) ->
- pack_plugin_in_order(PluginList, Acc, StatusAcc)
- end, {[], StatusMap}, List),
+ lists:foldl(
+ fun({_Node, PluginList}, {Acc, StatusAcc}) ->
+ pack_plugin_in_order(PluginList, Acc, StatusAcc)
+ end,
+ {[], StatusMap},
+ List
+ ),
lists:reverse(Plugins).
-pack_plugin_in_order([], Acc, StatusAcc) -> {Acc, StatusAcc};
-pack_plugin_in_order(_, Acc, StatusAcc)when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
+pack_plugin_in_order([], Acc, StatusAcc) ->
+ {Acc, StatusAcc};
+pack_plugin_in_order(_, Acc, StatusAcc) when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
case maps:find({Name, Vsn}, StatusAcc) of
@@ -413,15 +491,20 @@ pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
aggregate_status(List) -> aggregate_status(List, #{}).
-aggregate_status([], Acc) -> Acc;
+aggregate_status([], Acc) ->
+ Acc;
aggregate_status([{Node, Plugins} | List], Acc) ->
NewAcc =
- lists:foldl(fun(Plugin, SubAcc) ->
- #{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
- Key = {Name, Vsn},
- Value = #{node => Node, status => plugin_status(Plugin)},
- SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
- end, Acc, Plugins),
+ lists:foldl(
+ fun(Plugin, SubAcc) ->
+ #{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
+ Key = {Name, Vsn},
+ Value = #{node => Node, status => plugin_status(Plugin)},
+ SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
+ end,
+ Acc,
+ Plugins
+ ),
aggregate_status(List, NewAcc).
% running_status: running loaded, stopped
diff --git a/apps/emqx_management/src/emqx_mgmt_api_publish.erl b/apps/emqx_management/src/emqx_mgmt_api_publish.erl
index c0e208fbb..27280051c 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_publish.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_publish.erl
@@ -20,14 +20,17 @@
-behaviour(minirest_api).
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1
- ]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
--export([ publish/2
- , publish_batch/2]).
+-export([
+ publish/2,
+ publish_batch/2
+]).
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
@@ -46,7 +49,6 @@ schema("/publish") ->
}
}
};
-
schema("/publish/bulk") ->
#{
'operationId' => publish_batch,
@@ -61,32 +63,43 @@ schema("/publish/bulk") ->
fields(publish_message) ->
[
- {topic, hoconsc:mk(binary(), #{
- desc => <<"Topic Name">>,
- required => true,
- example => <<"api/example/topic">>})},
- {qos, hoconsc:mk(emqx_schema:qos(), #{
- desc => <<"MQTT QoS">>,
- required => false,
- default => 0})},
- {from, hoconsc:mk(binary(), #{
- desc => <<"From client ID">>,
- required => false,
- example => <<"api_example_client">>})},
- {payload, hoconsc:mk(binary(), #{
- desc => <<"MQTT Payload">>,
- required => true,
- example => <<"hello emqx api">>})},
- {retain, hoconsc:mk(boolean(), #{
- desc => <<"MQTT Retain Message">>,
- required => false,
- default => false})}
+ {topic,
+ hoconsc:mk(binary(), #{
+ desc => <<"Topic Name">>,
+ required => true,
+ example => <<"api/example/topic">>
+ })},
+ {qos,
+ hoconsc:mk(emqx_schema:qos(), #{
+ desc => <<"MQTT QoS">>,
+ required => false,
+ default => 0
+ })},
+ {from,
+ hoconsc:mk(binary(), #{
+ desc => <<"From client ID">>,
+ required => false,
+ example => <<"api_example_client">>
+ })},
+ {payload,
+ hoconsc:mk(binary(), #{
+ desc => <<"MQTT Payload">>,
+ required => true,
+ example => <<"hello emqx api">>
+ })},
+ {retain,
+ hoconsc:mk(boolean(), #{
+ desc => <<"MQTT Retain Message">>,
+ required => false,
+ default => false
+ })}
];
-
fields(publish_message_info) ->
[
- {id, hoconsc:mk(binary(), #{
- desc => <<"Internal Message ID">>})}
+ {id,
+ hoconsc:mk(binary(), #{
+ desc => <<"Internal Message ID">>
+ })}
] ++ fields(publish_message).
publish(post, #{body := Body}) ->
@@ -100,19 +113,21 @@ publish_batch(post, #{body := Body}) ->
{200, format_message(Messages)}.
message(Map) ->
- From = maps:get(<<"from">>, Map, http_api),
- QoS = maps:get(<<"qos">>, Map, 0),
- Topic = maps:get(<<"topic">>, Map),
+ From = maps:get(<<"from">>, Map, http_api),
+ QoS = maps:get(<<"qos">>, Map, 0),
+ Topic = maps:get(<<"topic">>, Map),
Payload = maps:get(<<"payload">>, Map),
- Retain = maps:get(<<"retain">>, Map, false),
+ Retain = maps:get(<<"retain">>, Map, false),
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
messages(List) ->
[message(MessageMap) || MessageMap <- List].
-format_message(Messages) when is_list(Messages)->
+format_message(Messages) when is_list(Messages) ->
[format_message(Message) || Message <- Messages];
-format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags}) ->
+format_message(#message{
+ id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags
+}) ->
#{
id => emqx_guid:to_hexstr(ID),
qos => Qos,
@@ -124,5 +139,5 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload
to_binary(Data) when is_binary(Data) ->
Data;
-to_binary(Data) ->
+to_binary(Data) ->
list_to_binary(io_lib:format("~p", [Data])).
diff --git a/apps/emqx_management/src/emqx_mgmt_api_stats.erl b/apps/emqx_management/src/emqx_mgmt_api_stats.erl
index af233ef17..6263380ad 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_stats.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_stats.erl
@@ -19,17 +19,22 @@
-include_lib("typerefl/include/types.hrl").
--import( hoconsc
- , [ mk/2
- , ref/1
- , ref/2
- , array/1]).
+-import(
+ hoconsc,
+ [
+ mk/2,
+ ref/1,
+ ref/2,
+ array/1
+ ]
+).
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1
- ]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
-export([list/2]).
@@ -40,102 +45,80 @@ paths() ->
["/stats"].
schema("/stats") ->
- #{ 'operationId' => list
- , get =>
- #{ description => <<"EMQX stats">>
- , tags => [<<"stats">>]
- , parameters => [ref(aggregate)]
- , responses =>
- #{ 200 => mk( hoconsc:union([ ref(?MODULE, node_stats_data)
- , array(ref(?MODULE, aggergate_data))
- ])
- , #{ desc => <<"List stats ok">> })
- }
+ #{
+ 'operationId' => list,
+ get =>
+ #{
+ description => <<"EMQX stats">>,
+ tags => [<<"stats">>],
+ parameters => [ref(aggregate)],
+ responses =>
+ #{
+ 200 => mk(
+ hoconsc:union([
+ ref(?MODULE, node_stats_data),
+ array(ref(?MODULE, aggergate_data))
+ ]),
+ #{desc => <<"List stats ok">>}
+ )
+ }
}
- }.
+ }.
fields(aggregate) ->
- [ { aggregate
- , mk( boolean()
- , #{ desc => <<"Calculation aggregate for all nodes">>
- , in => query
- , required => false
- , example => false})}
+ [
+ {aggregate,
+ mk(
+ boolean(),
+ #{
+ desc => <<"Calculation aggregate for all nodes">>,
+ in => query,
+ required => false,
+ example => false
+ }
+ )}
];
fields(node_stats_data) ->
- [ { 'channels.count'
- , mk( integer(), #{ desc => <<"sessions.count">>
- , example => 0})}
- , { 'channels.max'
- , mk( integer(), #{ desc => <<"session.max">>
- , example => 0})}
- , { 'connections.count'
- , mk( integer(), #{ desc => <<"Number of current connections">>
- , example => 0})}
- , { 'connections.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of connections">>
- , example => 0})}
- , { 'delayed.count'
- , mk( integer(), #{ desc => <<"Number of delayed messages">>
- , example => 0})}
- , { 'delayed.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of delayed messages">>
- , example => 0})}
- , { 'live_connections.count'
- , mk( integer(), #{ desc => <<"Number of current live connections">>
- , example => 0})}
- , { 'live_connections.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of live connections">>
- , example => 0})}
- , { 'retained.count'
- , mk( integer(), #{ desc => <<"Number of currently retained messages">>
- , example => 0})}
- , { 'retained.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of retained messages">>
- , example => 0})}
- , { 'sessions.count'
- , mk( integer(), #{ desc => <<"Number of current sessions">>
- , example => 0})}
- , { 'sessions.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of sessions">>
- , example => 0})}
- , { 'suboptions.count'
- , mk( integer(), #{ desc => <<"subscriptions.count">>
- , example => 0})}
- , { 'suboptions.max'
- , mk( integer(), #{ desc => <<"subscriptions.max">>
- , example => 0})}
- , { 'subscribers.count'
- , mk( integer(), #{ desc => <<"Number of current subscribers">>
- , example => 0})}
- , { 'subscribers.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of subscribers">>
- , example => 0})}
- , { 'subscriptions.count'
- , mk( integer(), #{ desc => <<"Number of current subscriptions, including shared subscriptions">>
- , example => 0})}
- , { 'subscriptions.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of subscriptions">>
- , example => 0})}
- , { 'subscriptions.shared.count'
- , mk( integer(), #{ desc => <<"Number of current shared subscriptions">>
- , example => 0})}
- , { 'subscriptions.shared.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of shared subscriptions">>
- , example => 0})}
- , { 'topics.count'
- , mk( integer(), #{ desc => <<"Number of current topics">>
- , example => 0})}
- , { 'topics.max'
- , mk( integer(), #{ desc => <<"Historical maximum number of topics">>
- , example => 0})}
+ [
+ stats_schema('channels.count', <<"sessions.count">>),
+ stats_schema('channels.max', <<"session.max">>),
+ stats_schema('connections.count', <<"Number of current connections">>),
+ stats_schema('connections.max', <<"Historical maximum number of connections">>),
+ stats_schema('delayed.count', <<"Number of delayed messages">>),
+ stats_schema('delayed.max', <<"Historical maximum number of delayed messages">>),
+ stats_schema('live_connections.count', <<"Number of current live connections">>),
+ stats_schema('live_connections.max', <<"Historical maximum number of live connections">>),
+ stats_schema('retained.count', <<"Number of currently retained messages">>),
+ stats_schema('retained.max', <<"Historical maximum number of retained messages">>),
+ stats_schema('sessions.count', <<"Number of current sessions">>),
+ stats_schema('sessions.max', <<"Historical maximum number of sessions">>),
+ stats_schema('suboptions.count', <<"subscriptions.count">>),
+ stats_schema('suboptions.max', <<"subscriptions.max">>),
+ stats_schema('subscribers.count', <<"Number of current subscribers">>),
+ stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
+ stats_schema(
+ 'subscriptions.count',
+ <<"Number of current subscriptions, including shared subscriptions">>
+ ),
+ stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
+ stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
+ stats_schema(
+ 'subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>
+ ),
+ stats_schema('topics.count', <<"Number of current topics">>),
+ stats_schema('topics.max', <<"Historical maximum number of topics">>)
];
fields(aggergate_data) ->
- [ { node
- , mk( string(), #{ desc => <<"Node name">>
- , example => <<"emqx@127.0.0.1">>})}
+ [
+ {node,
+ mk(string(), #{
+ desc => <<"Node name">>,
+ example => <<"emqx@127.0.0.1">>
+ })}
] ++ fields(node_stats_data).
+stats_schema(Name, Desc) ->
+ {Name, mk(non_neg_integer(), #{desc => Desc, example => 0})}.
%%%==============================================================================================
%% api apply
@@ -144,7 +127,9 @@ list(get, #{query_string := Qs}) ->
true ->
{200, emqx_mgmt:get_stats()};
_ ->
- Data = [maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}]) ||
- Node <- mria_mnesia:running_nodes()],
+ Data = [
+ maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}])
+ || Node <- mria_mnesia:running_nodes()
+ ],
{200, Data}
end.
diff --git a/apps/emqx_management/src/emqx_mgmt_api_status.erl b/apps/emqx_management/src/emqx_mgmt_api_status.erl
index 882fd271c..3a29d8567 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_status.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_status.erl
@@ -17,10 +17,11 @@
%% API
-behaviour(minirest_api).
--export([ api_spec/0
- , paths/0
- , schema/1
- ]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1
+]).
-export([running_status/2]).
@@ -31,22 +32,30 @@ paths() ->
["/status"].
schema("/status") ->
- #{ 'operationId' => running_status
- , get =>
- #{ description => <<"Node running status">>
- , security => []
- , responses =>
- #{200 =>
- #{ description => <<"Node is running">>
- , content =>
- #{ 'text/plain' =>
- #{ schema => #{type => string}
- , example => <<"Node emqx@127.0.0.1 is started\nemqx is running">>}
- }
- }
- }
+ #{
+ 'operationId' => running_status,
+ get =>
+ #{
+ description => <<"Node running status">>,
+ security => [],
+ responses =>
+ #{
+ 200 =>
+ #{
+ description => <<"Node is running">>,
+ content =>
+ #{
+ 'text/plain' =>
+ #{
+ schema => #{type => string},
+ example =>
+ <<"Node emqx@127.0.0.1 is started\nemqx is running">>
+ }
+ }
+ }
+ }
}
- }.
+ }.
%%--------------------------------------------------------------------
%% API Handler funcs
@@ -62,7 +71,7 @@ running_status(get, _Params) ->
end,
AppStatus =
case lists:keysearch(emqx, 1, application:which_applications()) of
- false -> not_running;
+ false -> not_running;
{value, _Val} -> running
end,
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl
index 92c702043..be3d3f293 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl
@@ -22,26 +22,30 @@
-include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/emqx_mqtt.hrl").
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
-export([subscriptions/2]).
--export([ query/4
- , format/1
- ]).
+-export([
+ query/4,
+ format/1
+]).
-define(SUBS_QTABLE, emqx_suboption).
--define(SUBS_QSCHEMA,
- [ {<<"clientid">>, binary}
- , {<<"topic">>, binary}
- , {<<"share">>, binary}
- , {<<"share_group">>, binary}
- , {<<"qos">>, integer}
- , {<<"match_topic">>, binary}]).
+-define(SUBS_QSCHEMA, [
+ {<<"clientid">>, binary},
+ {<<"topic">>, binary},
+ {<<"share">>, binary},
+ {<<"share_group">>, binary},
+ {<<"qos">>, integer},
+ {<<"match_topic">>, binary}
+]).
-define(QUERY_FUN, {?MODULE, query}).
@@ -58,7 +62,9 @@ schema("/subscriptions") ->
description => <<"List subscriptions">>,
parameters => parameters(),
responses => #{
- 200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})}}
+ 200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})
+ }
+ }
}.
fields(subscription) ->
@@ -74,62 +80,89 @@ parameters() ->
hoconsc:ref(emqx_dashboard_swagger, page),
hoconsc:ref(emqx_dashboard_swagger, limit),
{
- node, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Node name">>,
- example => atom_to_list(node())})
+ node,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Node name">>,
+ example => atom_to_list(node())
+ })
},
{
- clientid, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Client ID">>})
+ clientid,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Client ID">>
+ })
},
{
- qos, hoconsc:mk(emqx_schema:qos(), #{
- in => query,
- required => false,
- desc => <<"QoS">>})
+ qos,
+ hoconsc:mk(emqx_schema:qos(), #{
+ in => query,
+ required => false,
+ desc => <<"QoS">>
+ })
},
{
- topic, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Topic, url encoding">>})
+ topic,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Topic, url encoding">>
+ })
},
{
- match_topic, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Match topic string, url encoding">>})
+ match_topic,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Match topic string, url encoding">>
+ })
},
{
- share_group, hoconsc:mk(binary(), #{
- in => query,
- required => false,
- desc => <<"Shared subscription group name">>})
+ share_group,
+ hoconsc:mk(binary(), #{
+ in => query,
+ required => false,
+ desc => <<"Shared subscription group name">>
+ })
}
].
subscriptions(get, #{query_string := QString}) ->
- case maps:get(<<"node">>, QString, undefined) of
- undefined ->
- Response = emqx_mgmt_api:cluster_query(QString, ?SUBS_QTABLE,
- ?SUBS_QSCHEMA, ?QUERY_FUN),
- emqx_mgmt_util:generate_response(Response);
- Node ->
- Response = emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), QString,
- ?SUBS_QTABLE, ?SUBS_QSCHEMA, ?QUERY_FUN),
- emqx_mgmt_util:generate_response(Response)
+ Response =
+ case maps:get(<<"node">>, QString, undefined) of
+ undefined ->
+ emqx_mgmt_api:cluster_query(
+ QString,
+ ?SUBS_QTABLE,
+ ?SUBS_QSCHEMA,
+ ?QUERY_FUN
+ );
+ Node0 ->
+ emqx_mgmt_api:node_query(
+ binary_to_atom(Node0, utf8),
+ QString,
+ ?SUBS_QTABLE,
+ ?SUBS_QSCHEMA,
+ ?QUERY_FUN
+ )
+ end,
+ case Response of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Result ->
+ {200, Result}
end.
format(Items) when is_list(Items) ->
[format(Item) || Item <- Items];
-
format({{Subscriber, Topic}, Options}) ->
format({Subscriber, Topic, Options});
-
format({_Subscriber, Topic, Options = #{share := Group}}) ->
QoS = maps:get(qos, Options),
#{
@@ -153,19 +186,30 @@ format({_Subscriber, Topic, Options}) ->
query(Tab, {Qs, []}, Continuation, Limit) ->
Ms = qs2ms(Qs),
- emqx_mgmt_api:select_table_with_count( Tab, Ms
- , Continuation, Limit, fun format/1);
-
+ emqx_mgmt_api:select_table_with_count(
+ Tab,
+ Ms,
+ Continuation,
+ Limit,
+ fun format/1
+ );
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
Ms = qs2ms(Qs),
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
- emqx_mgmt_api:select_table_with_count( Tab, {Ms, FuzzyFilterFun}
- , Continuation, Limit, fun format/1).
+ emqx_mgmt_api:select_table_with_count(
+ Tab,
+ {Ms, FuzzyFilterFun},
+ Continuation,
+ Limit,
+ fun format/1
+ ).
fuzzy_filter_fun(Fuzzy) ->
fun(MsRaws) when is_list(MsRaws) ->
- lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
- , MsRaws)
+ lists:filter(
+ fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
+ MsRaws
+ )
end.
run_fuzzy_filter(_, []) ->
diff --git a/apps/emqx_management/src/emqx_mgmt_api_topics.erl b/apps/emqx_management/src/emqx_mgmt_api_topics.erl
index 249879c62..ef07cabe3 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_topics.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_topics.erl
@@ -22,23 +22,24 @@
%% API
-behaviour(minirest_api).
--export([ api_spec/0
- , paths/0
- , schema/1
- , fields/1
- ]).
+-export([
+ api_spec/0,
+ paths/0,
+ schema/1,
+ fields/1
+]).
--export([ topics/2
- , topic/2
- ]).
+-export([
+ topics/2,
+ topic/2
+]).
--export([ query/4]).
+-export([query/4]).
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
-
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
@@ -73,19 +74,23 @@ schema("/topics/:topic") ->
responses => #{
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
404 =>
- emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'],<<"Topic not found">>)
+ emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'], <<"Topic not found">>)
}
}
}.
fields(topic) ->
[
- {topic, hoconsc:mk(binary(), #{
- desc => <<"Topic Name">>,
- required => true})},
- {node, hoconsc:mk(binary(), #{
- desc => <<"Node">>,
- required => true})}
+ {topic,
+ hoconsc:mk(binary(), #{
+ desc => <<"Topic Name">>,
+ required => true
+ })},
+ {node,
+ hoconsc:mk(binary(), #{
+ desc => <<"Node">>,
+ required => true
+ })}
];
fields(meta) ->
emqx_dashboard_swagger:fields(page) ++
@@ -103,9 +108,19 @@ topic(get, #{bindings := Bindings}) ->
%%%==============================================================================================
%% api apply
do_list(Params) ->
- Response = emqx_mgmt_api:node_query(
- node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}),
- emqx_mgmt_util:generate_response(Response).
+ case
+ emqx_mgmt_api:node_query(
+ node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}
+ )
+ of
+ {error, page_limit_invalid} ->
+ {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
+ {error, Node, {badrpc, R}} ->
+ Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
+ {500, #{code => <<"NODE_DOWN">>, message => Message}};
+ Response ->
+ {200, Response}
+ end.
lookup(#{topic := Topic}) ->
case emqx_router:lookup_routes(Topic) of
@@ -121,16 +136,18 @@ generate_topic(Params = #{<<"topic">> := Topic}) ->
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
generate_topic(Params = #{topic := Topic}) ->
Params#{topic => uri_string:percent_decode(Topic)};
-generate_topic(Params) -> Params.
+generate_topic(Params) ->
+ Params.
query(Tab, {Qs, _}, Continuation, Limit) ->
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
-qs2ms([], Res) -> Res;
-qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
+qs2ms([], Res) ->
+ Res;
+qs2ms([{topic, '=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
-qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
+qs2ms([{node, '=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
format(#route{topic = Topic, dest = {_, Node}}) ->
@@ -140,7 +157,8 @@ format(#route{topic = Topic, dest = Node}) ->
topic_param(In) ->
{
- topic, hoconsc:mk(binary(), #{
+ topic,
+ hoconsc:mk(binary(), #{
desc => <<"Topic Name">>,
in => In,
required => (In == path),
@@ -148,9 +166,10 @@ topic_param(In) ->
})
}.
-node_param()->
+node_param() ->
{
- node, hoconsc:mk(binary(), #{
+ node,
+ hoconsc:mk(binary(), #{
desc => <<"Node Name">>,
in => query,
required => false,
diff --git a/apps/emqx_management/src/emqx_mgmt_api_trace.erl b/apps/emqx_management/src/emqx_mgmt_api_trace.erl
index 055a7f9e0..c1a6e196f 100644
--- a/apps/emqx_management/src/emqx_mgmt_api_trace.erl
+++ b/apps/emqx_management/src/emqx_mgmt_api_trace.erl
@@ -21,26 +21,29 @@
-include_lib("typerefl/include/types.hrl").
-include_lib("emqx/include/logger.hrl").
--export([ api_spec/0
- , fields/1
- , paths/0
- , schema/1
- , namespace/0
- ]).
+-export([
+ api_spec/0,
+ fields/1,
+ paths/0,
+ schema/1,
+ namespace/0
+]).
--export([ trace/2
- , delete_trace/2
- , update_trace/2
- , download_trace_log/2
- , stream_log_file/2
- ]).
+-export([
+ trace/2,
+ delete_trace/2,
+ update_trace/2,
+ download_trace_log/2,
+ stream_log_file/2
+]).
-export([validate_name/1]).
%% for rpc
--export([ read_trace_file/3
- , get_trace_size/0
- ]).
+-export([
+ read_trace_file/3,
+ get_trace_size/0
+]).
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
@@ -53,7 +56,6 @@ api_spec() ->
paths() ->
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
-
schema("/trace") ->
#{
'operationId' => trace,
@@ -68,9 +70,14 @@ schema("/trace") ->
'requestBody' => delete([status, log_size], fields(trace)),
responses => #{
200 => hoconsc:ref(trace),
- 400 => emqx_dashboard_swagger:error_codes(['ALREADY_EXISTS',
- 'DUPLICATE_CONDITION', 'INVALID_PARAMS'],
- <<"trace name already exists">>)
+ 400 => emqx_dashboard_swagger:error_codes(
+ [
+ 'ALREADY_EXISTS',
+ 'DUPLICATE_CONDITION',
+ 'INVALID_PARAMS'
+ ],
+ <<"trace name already exists">>
+ )
}
},
delete => #{
@@ -112,12 +119,13 @@ schema("/trace/:name/download") ->
parameters => [hoconsc:ref(name)],
responses => #{
200 =>
- #{description => "A trace zip file",
- content => #{
- 'application/octet-stream' =>
- #{schema => #{type => "string", format => "binary"}}
+ #{
+ description => "A trace zip file",
+ content => #{
+ 'application/octet-stream' =>
+ #{schema => #{type => "string", format => "binary"}}
+ }
}
- }
}
}
};
@@ -134,92 +142,151 @@ schema("/trace/:name/log") ->
],
responses => #{
200 =>
- [
- {items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
- | fields(bytes) ++ fields(position)
- ]
+ [
+ {items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
+ | fields(bytes) ++ fields(position)
+ ]
}
}
}.
fields(trace) ->
[
- {name, hoconsc:mk(binary(),
- #{desc => "Unique and format by [a-zA-Z0-9-_]",
- validator => fun ?MODULE:validate_name/1,
- required => true,
- example => <<"EMQX-TRACE-1">>})},
- {type, hoconsc:mk(hoconsc:enum([clientid, topic, ip_address]),
- #{desc => """Filter type""",
- required => true,
- example => <<"clientid">>})},
- {topic, hoconsc:mk(binary(),
- #{desc => """support mqtt wildcard topic.""",
- required => false,
- example => <<"/dev/#">>})},
- {clientid, hoconsc:mk(binary(),
- #{desc => """mqtt clientid.""",
- required => false,
- example => <<"dev-001">>})},
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Unique and format by [a-zA-Z0-9-_]",
+ validator => fun ?MODULE:validate_name/1,
+ required => true,
+ example => <<"EMQX-TRACE-1">>
+ }
+ )},
+ {type,
+ hoconsc:mk(
+ hoconsc:enum([clientid, topic, ip_address]),
+ #{
+ desc => "" "Filter type" "",
+ required => true,
+ example => <<"clientid">>
+ }
+ )},
+ {topic,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "" "support mqtt wildcard topic." "",
+ required => false,
+ example => <<"/dev/#">>
+ }
+ )},
+ {clientid,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "" "mqtt clientid." "",
+ required => false,
+ example => <<"dev-001">>
+ }
+ )},
%% TODO add ip_address type in emqx_schema.erl
- {ip_address, hoconsc:mk(binary(),
- #{desc => "client ip address",
- required => false,
- example => <<"127.0.0.1">>
- })},
- {status, hoconsc:mk(hoconsc:enum([running, stopped, waiting]),
- #{desc => "trace status",
- required => false,
- example => running
- })},
- {start_at, hoconsc:mk(emqx_datetime:epoch_second(),
- #{desc => "rfc3339 timestamp or epoch second",
- required => false,
- example => <<"2021-11-04T18:17:38+08:00">>
- })},
- {end_at, hoconsc:mk(emqx_datetime:epoch_second(),
- #{desc => "rfc3339 timestamp or epoch second",
- required => false,
- example => <<"2021-11-05T18:17:38+08:00">>
- })},
- {log_size, hoconsc:mk(hoconsc:array(map()),
- #{desc => "trace log size",
- example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
- required => false})}
+ {ip_address,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "client ip address",
+ required => false,
+ example => <<"127.0.0.1">>
+ }
+ )},
+ {status,
+ hoconsc:mk(
+ hoconsc:enum([running, stopped, waiting]),
+ #{
+ desc => "trace status",
+ required => false,
+ example => running
+ }
+ )},
+ {start_at,
+ hoconsc:mk(
+ emqx_datetime:epoch_second(),
+ #{
+ desc => "rfc3339 timestamp or epoch second",
+ required => false,
+ example => <<"2021-11-04T18:17:38+08:00">>
+ }
+ )},
+ {end_at,
+ hoconsc:mk(
+ emqx_datetime:epoch_second(),
+ #{
+ desc => "rfc3339 timestamp or epoch second",
+ required => false,
+ example => <<"2021-11-05T18:17:38+08:00">>
+ }
+ )},
+ {log_size,
+ hoconsc:mk(
+ hoconsc:array(map()),
+ #{
+ desc => "trace log size",
+ example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
+ required => false
+ }
+ )}
];
fields(name) ->
- [{name, hoconsc:mk(binary(),
- #{
- desc => <<"[a-zA-Z0-9-_]">>,
- example => <<"EMQX-TRACE-1">>,
- in => path,
- validator => fun ?MODULE:validate_name/1
- })}
+ [
+ {name,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => <<"[a-zA-Z0-9-_]">>,
+ example => <<"EMQX-TRACE-1">>,
+ in => path,
+ validator => fun ?MODULE:validate_name/1
+ }
+ )}
];
fields(node) ->
- [{node, hoconsc:mk(binary(),
- #{
- desc => "Node name",
- in => query,
- required => false
- })}];
+ [
+ {node,
+ hoconsc:mk(
+ binary(),
+ #{
+ desc => "Node name",
+ in => query,
+ required => false
+ }
+ )}
+ ];
fields(bytes) ->
- [{bytes, hoconsc:mk(integer(),
- #{
- desc => "Maximum number of bytes to store in request",
- in => query,
- required => false,
- default => 1000
- })}];
+ [
+ {bytes,
+ hoconsc:mk(
+ integer(),
+ #{
+ desc => "Maximum number of bytes to store in request",
+ in => query,
+ required => false,
+ default => 1000
+ }
+ )}
+ ];
fields(position) ->
- [{position, hoconsc:mk(integer(),
- #{
- desc => "Offset from the current trace position.",
- in => query,
- required => false,
- default => 0
- })}].
-
+ [
+ {position,
+ hoconsc:mk(
+ integer(),
+ #{
+ desc => "Offset from the current trace position.",
+ in => query,
+ required => false,
+ default => 0
+ }
+ )}
+ ].
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
@@ -231,7 +298,8 @@ validate_name(Name) ->
nomatch -> {error, "Name should be " ?NAME_RE};
_ -> ok
end;
- false -> {error, "Name Length must =< 256"}
+ false ->
+ {error, "Name Length must =< 256"}
end.
delete(Keys, Fields) ->
@@ -239,32 +307,48 @@ delete(Keys, Fields) ->
trace(get, _Params) ->
case emqx_trace:list() of
- [] -> {200, []};
+ [] ->
+ {200, []};
List0 ->
- List = lists:sort(fun(#{start_at := A}, #{start_at := B}) -> A > B end,
- emqx_trace:format(List0)),
+ List = lists:sort(
+ fun(#{start_at := A}, #{start_at := B}) -> A > B end,
+ emqx_trace:format(List0)
+ ),
Nodes = mria_mnesia:running_nodes(),
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
Now = erlang:system_time(second),
Traces =
- lists:map(fun(Trace = #{name := Name, start_at := Start,
- end_at := End, enable := Enable, type := Type, filter := Filter}) ->
- FileName = emqx_trace:filename(Name, Start),
- LogSize = collect_file_size(Nodes, FileName, AllFileSize),
- Trace0 = maps:without([enable, filter], Trace),
- Trace0#{log_size => LogSize
- , Type => iolist_to_binary(Filter)
- , start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
- , end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
- , status => status(Enable, Start, End, Now)
- }
- end, List),
+ lists:map(
+ fun(
+ Trace = #{
+ name := Name,
+ start_at := Start,
+ end_at := End,
+ enable := Enable,
+ type := Type,
+ filter := Filter
+ }
+ ) ->
+ FileName = emqx_trace:filename(Name, Start),
+ LogSize = collect_file_size(Nodes, FileName, AllFileSize),
+ Trace0 = maps:without([enable, filter], Trace),
+ Trace0#{
+ log_size => LogSize,
+ Type => iolist_to_binary(Filter),
+ start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
+ end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
+ status => status(Enable, Start, End, Now)
+ }
+ end,
+ List
+ ),
{200, Traces}
end;
trace(post, #{body := Param}) ->
case emqx_trace:create(Param) of
- {ok, Trace0} -> {200, format_trace(Trace0)};
+ {ok, Trace0} ->
+ {200, format_trace(Trace0)};
{error, {already_existed, Name}} ->
{400, #{
code => 'ALREADY_EXISTS',
@@ -287,18 +371,27 @@ trace(delete, _Param) ->
format_trace(Trace0) ->
[
- #{start_at := Start, end_at := End,
- enable := Enable, type := Type, filter := Filter} = Trace1
+ #{
+ start_at := Start,
+ end_at := End,
+ enable := Enable,
+ type := Type,
+ filter := Filter
+ } = Trace1
] = emqx_trace:format([Trace0]),
Now = erlang:system_time(second),
- LogSize = lists:foldl(fun(Node, Acc) -> Acc#{Node => 0} end, #{},
- mria_mnesia:running_nodes()),
+ LogSize = lists:foldl(
+ fun(Node, Acc) -> Acc#{Node => 0} end,
+ #{},
+ mria_mnesia:running_nodes()
+ ),
Trace2 = maps:without([enable, filter], Trace1),
- Trace2#{log_size => LogSize
- , Type => iolist_to_binary(Filter)
- , start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
- , end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
- , status => status(Enable, Start, End, Now)
+ Trace2#{
+ log_size => LogSize,
+ Type => iolist_to_binary(Filter),
+ start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
+ end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
+ status => status(Enable, Start, End, Now)
}.
delete_trace(delete, #{bindings := #{name := Name}}) ->
@@ -334,25 +427,34 @@ download_trace_log(get, #{bindings := #{name := Name}}) ->
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
},
{200, Headers, {file_binary, ZipName, Binary}};
- {error, not_found} -> ?NOT_FOUND(Name)
+ {error, not_found} ->
+ ?NOT_FOUND(Name)
end.
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
- lists:foldl(fun(Res, Acc) ->
- case Res of
- {ok, Node, Bin} ->
- FileName = Node ++ "-" ++ TraceLog,
- ZipName = filename:join([ZipDir, FileName]),
- case file:write_file(ZipName, Bin) of
- ok -> [FileName | Acc];
- _ -> Acc
- end;
- {error, Node, Reason} ->
- ?SLOG(error, #{msg => "download_trace_log_error", node => Node,
- log => TraceLog, reason => Reason}),
- Acc
- end
- end, [], TraceFiles).
+ lists:foldl(
+ fun(Res, Acc) ->
+ case Res of
+ {ok, Node, Bin} ->
+ FileName = Node ++ "-" ++ TraceLog,
+ ZipName = filename:join([ZipDir, FileName]),
+ case file:write_file(ZipName, Bin) of
+ ok -> [FileName | Acc];
+ _ -> Acc
+ end;
+ {error, Node, Reason} ->
+ ?SLOG(error, #{
+ msg => "download_trace_log_error",
+ node => Node,
+ log => TraceLog,
+ reason => Reason
+ }),
+ Acc
+ end
+ end,
+ [],
+ TraceFiles
+ ).
collect_trace_file(TraceLog) ->
Nodes = mria_mnesia:running_nodes(),
@@ -376,18 +478,25 @@ stream_log_file(get, #{bindings := #{name := Name}, query_string := Query}) ->
{eof, Size} ->
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
{200, #{meta => Meta, items => <<"">>}};
- {error, enoent} -> %% the waiting trace should return "" not error.
+ %% the waiting trace should return "" not error.
+ {error, enoent} ->
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
{200, #{meta => Meta, items => <<"">>}};
{error, Reason} ->
- ?SLOG(error, #{msg => "read_file_failed",
- node => Node, name => Name, reason => Reason,
- position => Position, bytes => Bytes}),
+ ?SLOG(error, #{
+ msg => "read_file_failed",
+ node => Node,
+ name => Name,
+ reason => Reason,
+ position => Position,
+ bytes => Bytes
+ }),
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
{badrpc, nodedown} ->
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
end;
- {error, not_found} -> {400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
+ {error, not_found} ->
+ {400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
end.
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
@@ -396,23 +505,31 @@ get_trace_size() ->
Node = node(),
case file:list_dir(TraceDir) of
{ok, AllFiles} ->
- lists:foldl(fun(File, Acc) ->
- FullFileName = filename:join(TraceDir, File),
- Acc#{{Node, File} => filelib:file_size(FullFileName)}
- end, #{}, lists:delete("zip", AllFiles));
- _ -> #{}
+ lists:foldl(
+ fun(File, Acc) ->
+ FullFileName = filename:join(TraceDir, File),
+ Acc#{{Node, File} => filelib:file_size(FullFileName)}
+ end,
+ #{},
+ lists:delete("zip", AllFiles)
+ );
+ _ ->
+ #{}
end.
%% this is an rpc call for stream_log_file/2
--spec read_trace_file( binary()
- , non_neg_integer()
- , non_neg_integer()
- ) -> {ok, binary()}
- | {error, _}
- | {eof, non_neg_integer()}.
+-spec read_trace_file(
+ binary(),
+ non_neg_integer(),
+ non_neg_integer()
+) ->
+ {ok, binary()}
+ | {error, _}
+ | {eof, non_neg_integer()}.
read_trace_file(Name, Position, Limit) ->
case emqx_trace:get_trace_filename(Name) of
- {error, _} = Error -> Error;
+ {error, _} = Error ->
+ Error;
{ok, TraceFile} ->
TraceDir = emqx_trace:trace_dir(),
TracePath = filename:join([TraceDir, TraceFile]),
@@ -423,13 +540,16 @@ read_file(Path, Offset, Bytes) ->
case file:open(Path, [read, raw, binary]) of
{ok, IoDevice} ->
try
- _ = case Offset of
+ _ =
+ case Offset of
0 -> ok;
_ -> file:position(IoDevice, {bof, Offset})
end,
case file:read(IoDevice, Bytes) of
- {ok, Bin} -> {ok, Bin};
- {error, Reason} -> {error, Reason};
+ {ok, Bin} ->
+ {ok, Bin};
+ {error, Reason} ->
+ {error, Reason};
eof ->
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
{eof, Size}
@@ -437,20 +557,27 @@ read_file(Path, Offset, Bytes) ->
after
file:close(IoDevice)
end;
- {error, Reason} -> {error, Reason}
+ {error, Reason} ->
+ {error, Reason}
end.
to_node(Node) ->
- try {ok, binary_to_existing_atom(Node)}
- catch _:_ ->
- {error, not_found}
+ try
+ {ok, binary_to_existing_atom(Node)}
+ catch
+ _:_ ->
+ {error, not_found}
end.
collect_file_size(Nodes, FileName, AllFiles) ->
- lists:foldl(fun(Node, Acc) ->
- Size = maps:get({Node, FileName}, AllFiles, 0),
- Acc#{Node => Size}
- end, #{}, Nodes).
+ lists:foldl(
+ fun(Node, Acc) ->
+ Size = maps:get({Node, FileName}, AllFiles, 0),
+ Acc#{Node => Size}
+ end,
+ #{},
+ Nodes
+ ).
status(false, _Start, _End, _Now) -> <<"stopped">>;
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
diff --git a/apps/emqx_management/src/emqx_mgmt_app.erl b/apps/emqx_management/src/emqx_mgmt_app.erl
index 372690cf4..9ad1930a0 100644
--- a/apps/emqx_management/src/emqx_mgmt_app.erl
+++ b/apps/emqx_management/src/emqx_mgmt_app.erl
@@ -20,9 +20,10 @@
-define(APP, emqx_management).
--export([ start/2
- , stop/1
- ]).
+-export([
+ start/2,
+ stop/1
+]).
-include("emqx_mgmt.hrl").
diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl
index e0aeab49c..fe1919632 100644
--- a/apps/emqx_management/src/emqx_mgmt_auth.erl
+++ b/apps/emqx_management/src/emqx_mgmt_auth.erl
@@ -20,14 +20,15 @@
-export([mnesia/1]).
-boot_mnesia({mnesia, [boot]}).
--export([ create/4
- , read/1
- , update/4
- , delete/1
- , list/0
- ]).
+-export([
+ create/4,
+ read/1,
+ update/4,
+ delete/1,
+ list/0
+]).
--export([ authorize/3 ]).
+-export([authorize/3]).
-define(APP, emqx_app).
@@ -39,7 +40,7 @@
desc = <<>> :: binary() | '_',
expired_at = 0 :: integer() | undefined | '_',
created_at = 0 :: integer() | '_'
- }).
+}).
mnesia(boot) ->
ok = mria:create_table(?APP, [
@@ -47,7 +48,8 @@ mnesia(boot) ->
{rlog_shard, ?COMMON_SHARD},
{storage, disc_copies},
{record_name, ?APP},
- {attributes, record_info(fields, ?APP)}]).
+ {attributes, record_info(fields, ?APP)}
+ ]).
create(Name, Enable, ExpiredAt, Desc) ->
case mnesia:table_info(?APP, size) < 30 of
@@ -61,13 +63,14 @@ read(Name) ->
[] -> mnesia:abort(not_found);
[App] -> to_map(App)
end
- end,
+ end,
trans(Fun).
update(Name, Enable, ExpiredAt, Desc) ->
Fun = fun() ->
case mnesia:read(?APP, Name, write) of
- [] -> mnesia:abort(not_found);
+ [] ->
+ mnesia:abort(not_found);
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
App =
App0#?APP{
@@ -78,22 +81,25 @@ update(Name, Enable, ExpiredAt, Desc) ->
ok = mnesia:write(App),
to_map(App)
end
- end,
+ end,
trans(Fun).
delete(Name) ->
Fun = fun() ->
case mnesia:read(?APP, Name) of
[] -> mnesia:abort(not_found);
- [_App] -> mnesia:delete({?APP, Name}) end
- end,
+ [_App] -> mnesia:delete({?APP, Name})
+ end
+ end,
trans(Fun).
list() ->
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
-authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
-authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
+authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
+ {error, <<"not_allowed">>};
+authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
+ {error, <<"not_allowed">>};
authorize(_Path, ApiKey, ApiSecret) ->
Now = erlang:system_time(second),
case find_by_api_key(ApiKey) of
@@ -102,28 +108,35 @@ authorize(_Path, ApiKey, ApiSecret) ->
ok -> ok;
error -> {error, "secret_error"}
end;
- {ok, true, _ExpiredAt, _SecretHash} -> {error, "secret_expired"};
- {ok, false, _ExpiredAt, _SecretHash} -> {error, "secret_disable"};
- {error, Reason} -> {error, Reason}
+ {ok, true, _ExpiredAt, _SecretHash} ->
+ {error, "secret_expired"};
+ {ok, false, _ExpiredAt, _SecretHash} ->
+ {error, "secret_disable"};
+ {error, Reason} ->
+ {error, Reason}
end.
find_by_api_key(ApiKey) ->
- Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
+ Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
case trans(Fun) of
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
{ok, Enable, ExpiredAt, SecretHash};
- _ -> {error, "not_found"}
+ _ ->
+ {error, "not_found"}
end.
ensure_not_undefined(undefined, Old) -> Old;
ensure_not_undefined(New, _Old) -> New.
-to_map(Apps)when is_list(Apps) ->
+to_map(Apps) when is_list(Apps) ->
Fields = record_info(fields, ?APP),
- lists:map(fun(Trace0 = #?APP{}) ->
- [_ | Values] = tuple_to_list(Trace0),
- maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
- end, Apps);
+ lists:map(
+ fun(Trace0 = #?APP{}) ->
+ [_ | Values] = tuple_to_list(Trace0),
+ maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
+ end,
+ Apps
+ );
to_map(App0) ->
[App] = to_map([App0]),
App.
@@ -149,16 +162,18 @@ create_app(Name, Enable, ExpiredAt, Desc) ->
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
trans(fun() ->
case mnesia:read(?APP, Name) of
- [_] -> mnesia:abort(name_already_existed);
+ [_] ->
+ mnesia:abort(name_already_existed);
[] ->
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
[] ->
ok = mnesia:write(App),
to_map(App);
- _ -> mnesia:abort(api_key_already_existed)
+ _ ->
+ mnesia:abort(api_key_already_existed)
end
end
- end).
+ end).
trans(Fun) ->
case mria:transaction(?COMMON_SHARD, Fun) of
diff --git a/apps/emqx_management/src/emqx_mgmt_sup.erl b/apps/emqx_management/src/emqx_mgmt_sup.erl
index ef1d05833..74f77dbd0 100644
--- a/apps/emqx_management/src/emqx_mgmt_sup.erl
+++ b/apps/emqx_management/src/emqx_mgmt_sup.erl
@@ -23,8 +23,7 @@
-export([init/1]).
start_link() ->
- supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
- {ok, {{one_for_one, 1, 5}, []}}.
-
+ {ok, {{one_for_one, 1, 5}, []}}.
diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl
index 7c7d2c316..f553be22d 100644
--- a/apps/emqx_management/src/emqx_mgmt_util.erl
+++ b/apps/emqx_management/src/emqx_mgmt_util.erl
@@ -16,41 +16,40 @@
-module(emqx_mgmt_util).
--export([ strftime/1
- , datetime/1
- , kmg/1
- , ntoa/1
- , merge_maps/2
- , batch_operation/3
- ]).
-
--export([ bad_request/0
- , bad_request/1
- , properties/1
- , page_params/0
- , schema/1
- , schema/2
- , object_schema/1
- , object_schema/2
- , array_schema/1
- , array_schema/2
- , object_array_schema/1
- , object_array_schema/2
- , page_schema/1
- , page_object_schema/1
- , error_schema/1
- , error_schema/2
- , batch_schema/1
- ]).
-
--export([generate_response/1]).
+-export([
+ strftime/1,
+ datetime/1,
+ kmg/1,
+ ntoa/1,
+ merge_maps/2,
+ batch_operation/3
+]).
+-export([
+ bad_request/0,
+ bad_request/1,
+ properties/1,
+ page_params/0,
+ schema/1,
+ schema/2,
+ object_schema/1,
+ object_schema/2,
+ array_schema/1,
+ array_schema/2,
+ object_array_schema/1,
+ object_array_schema/2,
+ page_schema/1,
+ page_object_schema/1,
+ error_schema/1,
+ error_schema/2,
+ batch_schema/1
+]).
-export([urldecode/1]).
-define(KB, 1024).
--define(MB, (1024*1024)).
--define(GB, (1024*1024*1024)).
+-define(MB, (1024 * 1024)).
+-define(GB, (1024 * 1024 * 1024)).
%%--------------------------------------------------------------------
%% Strftime
@@ -58,17 +57,17 @@
strftime({MegaSecs, Secs, _MicroSecs}) ->
strftime(datetime(MegaSecs * 1000000 + Secs));
-
strftime(Secs) when is_integer(Secs) ->
strftime(datetime(Secs));
-
-strftime({{Y,M,D}, {H,MM,S}}) ->
+strftime({{Y, M, D}, {H, MM, S}}) ->
lists:flatten(
io_lib:format(
- "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
+ "~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S]
+ )
+ ).
datetime(Timestamp) when is_integer(Timestamp) ->
- Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}),
+ Epoch = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
calendar:universal_time_to_local_time(Universal).
@@ -83,19 +82,27 @@ kmg(Byte) ->
kmg(F, S) ->
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
-ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
+ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
ntoa(IP) ->
inet_parse:ntoa(IP).
merge_maps(Default, New) ->
- maps:fold(fun(K, V, Acc) ->
- case maps:get(K, Acc, undefined) of
- OldV when is_map(OldV),
- is_map(V) -> Acc#{K => merge_maps(OldV, V)};
- _ -> Acc#{K => V}
- end
- end, Default, New).
+ maps:fold(
+ fun(K, V, Acc) ->
+ case maps:get(K, Acc, undefined) of
+ OldV when
+ is_map(OldV),
+ is_map(V)
+ ->
+ Acc#{K => merge_maps(OldV, V)};
+ _ ->
+ Acc#{K => V}
+ end
+ end,
+ Default,
+ New
+ ).
urldecode(S) ->
emqx_http_lib:uri_decode(S).
@@ -126,8 +133,13 @@ array_schema(Schema, Desc) ->
object_array_schema(Properties) when is_map(Properties) ->
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
object_array_schema(Properties, Desc) ->
- json_content_schema(#{type => array,
- items => #{type => object, properties => Properties}}, Desc).
+ json_content_schema(
+ #{
+ type => array,
+ items => #{type => object, properties => Properties}
+ },
+ Desc
+ ).
page_schema(Ref) when is_atom(Ref) ->
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
@@ -137,9 +149,11 @@ page_schema(Schema) ->
properties => #{
meta => #{
type => object,
- properties => properties([{page, integer},
- {limit, integer},
- {count, integer}])
+ properties => properties([
+ {page, integer},
+ {limit, integer},
+ {count, integer}
+ ])
},
data => #{
type => array,
@@ -158,8 +172,10 @@ error_schema(Description) ->
error_schema(Description, Enum) ->
Schema = #{
type => object,
- properties => properties([{code, string, <<>>, Enum},
- {message, string}])
+ properties => properties([
+ {code, string, <<>>, Enum},
+ {message, string}
+ ])
},
json_content_schema(Schema, Description).
@@ -171,20 +187,28 @@ batch_schema(DefName) when is_binary(DefName) ->
properties => #{
success => #{
type => integer,
- description => <<"Success count">>},
+ description => <<"Success count">>
+ },
failed => #{
type => integer,
- description => <<"Failed count">>},
+ description => <<"Failed count">>
+ },
detail => #{
type => array,
description => <<"Failed object & reason">>,
items => #{
type => object,
properties =>
- #{
- data => minirest:ref(DefName),
- reason => #{
- type => <<"string">>}}}}}},
+ #{
+ data => minirest:ref(DefName),
+ reason => #{
+ type => <<"string">>
+ }
+ }
+ }
+ }
+ }
+ },
json_content_schema(Schema).
json_content_schema(Schema) when is_map(Schema) ->
@@ -214,7 +238,7 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) ->
case erlang:apply(Module, Function, Args) of
ok ->
batch_operation(Module, Function, ArgsList, Failed);
- {error ,Reason} ->
+ {error, Reason} ->
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
end.
@@ -227,52 +251,77 @@ properties([Key | Props], Acc) when is_atom(Key) ->
properties([{Key, Type} | Props], Acc) ->
properties(Props, maps:put(Key, #{type => Type}, Acc));
properties([{Key, object, Props1} | Props], Acc) ->
- properties(Props, maps:put(Key, #{type => object,
- properties => properties(Props1)}, Acc));
+ properties(
+ Props,
+ maps:put(
+ Key,
+ #{
+ type => object,
+ properties => properties(Props1)
+ },
+ Acc
+ )
+ );
properties([{Key, {array, object}, Props1} | Props], Acc) ->
- properties(Props, maps:put(Key, #{type => array,
- items => #{type => object,
- properties => properties(Props1)
- }}, Acc));
+ properties(
+ Props,
+ maps:put(
+ Key,
+ #{
+ type => array,
+ items => #{
+ type => object,
+ properties => properties(Props1)
+ }
+ },
+ Acc
+ )
+ );
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
- properties(Props, maps:put(Key, #{type => array,
- items => #{type => Type},
- description => Desc}, Acc));
+ properties(
+ Props,
+ maps:put(
+ Key,
+ #{
+ type => array,
+ items => #{type => Type},
+ description => Desc
+ },
+ Acc
+ )
+ );
properties([{Key, Type, Desc} | Props], Acc) ->
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
- properties(Props, maps:put(Key, #{type => Type,
- description => Desc,
- enum => Enum}, Acc)).
+ properties(
+ Props,
+ maps:put(
+ Key,
+ #{
+ type => Type,
+ description => Desc,
+ enum => Enum
+ },
+ Acc
+ )
+ ).
page_params() ->
- [#{
- name => page,
- in => query,
- description => <<"Page">>,
- schema => #{type => integer, default => 1}
- },
- #{
- name => limit,
- in => query,
- description => <<"Page size">>,
- schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
- }].
+ [
+ #{
+ name => page,
+ in => query,
+ description => <<"Page">>,
+ schema => #{type => integer, default => 1}
+ },
+ #{
+ name => limit,
+ in => query,
+ description => <<"Page size">>,
+ schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
+ }
+ ].
bad_request() ->
bad_request(<<"Bad Request">>).
bad_request(Desc) ->
object_schema(properties([{message, string}, {code, string}]), Desc).
-
-%%%==============================================================================================
-%% Response util
-
-generate_response(QueryResult) ->
- case QueryResult of
- {error, page_limit_invalid} ->
- {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
- {error, Node, {badrpc, R}} ->
- Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, R])),
- {500, #{code => <<"NODE_DOWN">>, message => Message}};
- Response ->
- {200, Response}
- end.
diff --git a/apps/emqx_management/src/proto/emqx_mgmt_api_plugins_proto_v1.erl b/apps/emqx_management/src/proto/emqx_mgmt_api_plugins_proto_v1.erl
index 4a2d9e65d..dc7904d01 100644
--- a/apps/emqx_management/src/proto/emqx_mgmt_api_plugins_proto_v1.erl
+++ b/apps/emqx_management/src/proto/emqx_mgmt_api_plugins_proto_v1.erl
@@ -17,12 +17,13 @@
-behaviour(emqx_bpapi).
--export([ introduced_in/0
- , get_plugins/0
- , install_package/2
- , describe_package/1
- , delete_package/1
- , ensure_action/2
+-export([
+ introduced_in/0,
+ get_plugins/0,
+ install_package/2,
+ describe_package/1,
+ delete_package/1,
+ ensure_action/2
]).
-include_lib("emqx/include/bpapi.hrl").
diff --git a/apps/emqx_management/src/proto/emqx_mgmt_trace_proto_v1.erl b/apps/emqx_management/src/proto/emqx_mgmt_trace_proto_v1.erl
index 6579dcd02..4f847219e 100644
--- a/apps/emqx_management/src/proto/emqx_mgmt_trace_proto_v1.erl
+++ b/apps/emqx_management/src/proto/emqx_mgmt_trace_proto_v1.erl
@@ -18,12 +18,13 @@
-behaviour(emqx_bpapi).
--export([ introduced_in/0
+-export([
+ introduced_in/0,
- , trace_file/2
- , get_trace_size/1
- , read_trace_file/4
- ]).
+ trace_file/2,
+ get_trace_size/1,
+ read_trace_file/4
+]).
-include_lib("emqx/include/bpapi.hrl").
@@ -31,21 +32,22 @@ introduced_in() ->
"5.0.0".
-spec get_trace_size([node()]) ->
- emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
+ emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
get_trace_size(Nodes) ->
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
-spec trace_file([node()], file:name_all()) ->
- emqx_rpc:multicall_result(
- {ok, Node :: list(), Binary :: binary()} |
- {error, Node :: list(), Reason :: term()}).
+ emqx_rpc:multicall_result(
+ {ok, Node :: list(), Binary :: binary()}
+ | {error, Node :: list(), Reason :: term()}
+ ).
trace_file(Nodes, File) ->
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
- {ok, binary()}
- | {error, _}
- | {eof, non_neg_integer()}
- | {badrpc, _}.
+ {ok, binary()}
+ | {error, _}
+ | {eof, non_neg_integer()}
+ | {badrpc, _}.
read_trace_file(Node, Name, Position, Limit) ->
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
diff --git a/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl
index e87a8a6b0..a499e7cd2 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_alarms_SUITE.erl
@@ -15,7 +15,6 @@
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_alarms_SUITE).
-
-compile(export_all).
-compile(nowarn_export_all).
@@ -55,8 +54,8 @@ get_alarms(AssertCount, Activated) ->
Headers = emqx_mgmt_api_test_util:auth_header_(),
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
Data = emqx_json:decode(Response, [return_maps]),
- Meta = maps:get(<<"meta">>, Data),
- Page = maps:get(<<"page">>, Meta),
+ Meta = maps:get(<<"meta">>, Data),
+ Page = maps:get(<<"page">>, Meta),
Limit = maps:get(<<"limit">>, Meta),
Count = maps:get(<<"count">>, Meta),
?assertEqual(Page, 1),
diff --git a/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl
index a8319fa73..85f712fed 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_app_SUITE.erl
@@ -22,10 +22,11 @@
all() -> [{group, parallel}, {group, sequence}].
suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
- {parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
- {sequence, [], [t_create_failed]}
- ].
+groups() ->
+ [
+ {parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
+ {sequence, [], [t_create_failed]}
+ ].
init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite(),
@@ -37,15 +38,20 @@ end_per_suite(_) ->
t_create(_Config) ->
Name = <<"EMQX-API-KEY-1">>,
{ok, Create} = create_app(Name),
- ?assertMatch(#{<<"api_key">> := _,
- <<"api_secret">> := _,
- <<"created_at">> := _,
- <<"desc">> := _,
- <<"enable">> := true,
- <<"expired_at">> := _,
- <<"name">> := Name}, Create),
+ ?assertMatch(
+ #{
+ <<"api_key">> := _,
+ <<"api_secret">> := _,
+ <<"created_at">> := _,
+ <<"desc">> := _,
+ <<"enable">> := true,
+ <<"expired_at">> := _,
+ <<"name">> := Name
+ },
+ Create
+ ),
{ok, List} = list_app(),
- [App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
+ [App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
{ok, App1} = read_app(Name),
?assertEqual(Name, maps:get(<<"name">>, App1)),
@@ -64,9 +70,12 @@ t_create_failed(_Config) ->
{ok, List} = list_app(),
CreateNum = 30 - erlang:length(List),
- Names = lists:map(fun(Seq) ->
- <<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
- end, lists:seq(1, CreateNum)),
+ Names = lists:map(
+ fun(Seq) ->
+ <<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
+ end,
+ lists:seq(1, CreateNum)
+ ),
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
@@ -93,7 +102,8 @@ t_update(_Config) ->
?assertEqual(Name, maps:get(<<"name">>, Update1)),
?assertEqual(false, maps:get(<<"enable">>, Update1)),
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
- ?assertEqual(calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
+ ?assertEqual(
+ calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
),
Unexpired1 = maps:without([expired_at], Change),
@@ -117,10 +127,14 @@ t_delete(_Config) ->
t_authorize(_Config) ->
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
- BasicHeader = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
- binary_to_list(ApiSecret)),
- SecretError = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
- binary_to_list(ApiKey)),
+ BasicHeader = emqx_common_test_http:auth_header(
+ binary_to_list(ApiKey),
+ binary_to_list(ApiSecret)
+ ),
+ SecretError = emqx_common_test_http:auth_header(
+ binary_to_list(ApiKey),
+ binary_to_list(ApiKey)
+ ),
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
@@ -134,8 +148,10 @@ t_authorize(_Config) ->
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
- ?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := false}},
- update_app(Name, #{enable => false})),
+ ?assertMatch(
+ {ok, #{<<"api_key">> := _, <<"enable">> := false}},
+ update_app(Name, #{enable => false})
+ ),
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
Expired = #{
@@ -145,8 +161,10 @@ t_authorize(_Config) ->
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
UnExpired = #{expired_at => undefined},
- ?assertMatch({ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
- update_app(Name, UnExpired)),
+ ?assertMatch(
+ {ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
+ update_app(Name, UnExpired)
+ ),
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
ok.
@@ -159,7 +177,6 @@ t_create_unexpired_app(_Config) ->
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
ok.
-
list_app() ->
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
case emqx_mgmt_api_test_util:request_api(get, Path) of
diff --git a/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl
index 828312b9c..3e69dd851 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_banned_SUITE.erl
@@ -47,13 +47,17 @@ t_create(_Config) ->
until => Until
},
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
- ?assertEqual(#{<<"as">> => As,
- <<"at">> => At,
- <<"by">> => By,
- <<"reason">> => Reason,
- <<"until">> => Until,
- <<"who">> => ClientId
- }, ClientIdBannedRes),
+ ?assertEqual(
+ #{
+ <<"as">> => As,
+ <<"at">> => At,
+ <<"by">> => By,
+ <<"reason">> => Reason,
+ <<"until">> => Until,
+ <<"who">> => ClientId
+ },
+ ClientIdBannedRes
+ ),
PeerHost = <<"192.168.2.13">>,
PeerHostBanned = #{
as => <<"peerhost">>,
@@ -64,15 +68,19 @@ t_create(_Config) ->
until => Until
},
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
- ?assertEqual(#{<<"as">> => <<"peerhost">>,
- <<"at">> => At,
- <<"by">> => By,
- <<"reason">> => Reason,
- <<"until">> => Until,
- <<"who">> => PeerHost
- }, PeerHostBannedRes),
+ ?assertEqual(
+ #{
+ <<"as">> => <<"peerhost">>,
+ <<"at">> => At,
+ <<"by">> => By,
+ <<"reason">> => Reason,
+ <<"until">> => Until,
+ <<"who">> => PeerHost
+ },
+ PeerHostBannedRes
+ ),
{ok, #{<<"data">> := List}} = list_banned(),
- Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
+ Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
ok.
@@ -94,8 +102,10 @@ t_create_failed(_Config) ->
},
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
?assertEqual(BadRequest, create_banned(BadPeerHost)),
- Expired = BadPeerHost#{until => emqx_banned:to_rfc3339(Now - 1),
- who => <<"127.0.0.1">>},
+ Expired = BadPeerHost#{
+ until => emqx_banned:to_rfc3339(Now - 1),
+ who => <<"127.0.0.1">>
+ },
?assertEqual(BadRequest, create_banned(Expired)),
ok.
@@ -117,8 +127,10 @@ t_delete(_Config) ->
},
{ok, _} = create_banned(Banned),
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
- ?assertMatch({error,{"HTTP/1.1",404,"Not Found"}},
- delete_banned(binary_to_list(As), binary_to_list(Who))),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 404, "Not Found"}},
+ delete_banned(binary_to_list(As), binary_to_list(Who))
+ ),
ok.
list_banned() ->
diff --git a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl
index 80f236fed..897862b20 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl
@@ -44,20 +44,20 @@ t_clients(_) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
- {ok, _} = emqtt:connect(C1),
+ {ok, _} = emqtt:connect(C1),
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
- {ok, _} = emqtt:connect(C2),
+ {ok, _} = emqtt:connect(C2),
timer:sleep(300),
%% get /clients
- ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
- {ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
+ ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
+ {ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
- ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
- ClientsPage = maps:get(<<"page">>, ClientsMeta),
- ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
- ClientsCount = maps:get(<<"count">>, ClientsMeta),
+ ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
+ ClientsPage = maps:get(<<"page">>, ClientsMeta),
+ ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
+ ClientsCount = maps:get(<<"count">>, ClientsMeta),
?assertEqual(ClientsPage, 1),
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
?assertEqual(ClientsCount, 2),
@@ -76,29 +76,49 @@ t_clients(_) ->
AfterKickoutResponse2 = emqx_mgmt_api_test_util:request_api(get, Client2Path),
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
- %% get /clients/:clientid/authz_cache should has no authz cache
- Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path(["clients",
- binary_to_list(ClientId1), "authz_cache"]),
+ %% get /clients/:clientid/authorization/cache should has no authz cache
+ Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([
+ "clients",
+ binary_to_list(ClientId1),
+ "authorization",
+ "cache"
+ ]),
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
?assertEqual("[]", Client1AuthzCache),
%% post /clients/:clientid/subscribe
SubscribeBody = #{topic => Topic, qos => Qos},
- SubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
- binary_to_list(ClientId1), "subscribe"]),
- {ok, _} = emqx_mgmt_api_test_util:request_api(post, SubscribePath,
- "", AuthHeader, SubscribeBody),
+ SubscribePath = emqx_mgmt_api_test_util:api_path([
+ "clients",
+ binary_to_list(ClientId1),
+ "subscribe"
+ ]),
+ {ok, _} = emqx_mgmt_api_test_util:request_api(
+ post,
+ SubscribePath,
+ "",
+ AuthHeader,
+ SubscribeBody
+ ),
timer:sleep(100),
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
?assertEqual(AfterSubTopic, Topic),
?assertEqual(AfterSubQos, Qos),
%% post /clients/:clientid/unsubscribe
- UnSubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
- binary_to_list(ClientId1), "unsubscribe"]),
+ UnSubscribePath = emqx_mgmt_api_test_util:api_path([
+ "clients",
+ binary_to_list(ClientId1),
+ "unsubscribe"
+ ]),
UnSubscribeBody = #{topic => Topic},
- {ok, _} = emqx_mgmt_api_test_util:request_api(post, UnSubscribePath,
- "", AuthHeader, UnSubscribeBody),
+ {ok, _} = emqx_mgmt_api_test_util:request_api(
+ post,
+ UnSubscribePath,
+ "",
+ AuthHeader,
+ UnSubscribeBody
+ ),
timer:sleep(100),
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
@@ -118,44 +138,58 @@ t_query_clients_with_time(_) ->
ClientId2 = <<"client2">>,
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
- {ok, _} = emqtt:connect(C1),
+ {ok, _} = emqtt:connect(C1),
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
- {ok, _} = emqtt:connect(C2),
+ {ok, _} = emqtt:connect(C2),
timer:sleep(100),
- AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
- ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
+ AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
+ ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
%% get /clients with time(rfc3339)
NowTimeStampInt = erlang:system_time(millisecond),
%% Do not uri_encode `=` to `%3D`
- Rfc3339String = emqx_http_lib:uri_encode(binary:bin_to_list(
- emqx_datetime:epoch_to_rfc3339(NowTimeStampInt))),
+ Rfc3339String = emqx_http_lib:uri_encode(
+ binary:bin_to_list(
+ emqx_datetime:epoch_to_rfc3339(NowTimeStampInt)
+ )
+ ),
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
- LteKeys = ["lte_created_at=", "lte_connected_at="],
- GteKeys = ["gte_created_at=", "gte_connected_at="],
- LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
- LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
- GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
- GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
+ LteKeys = ["lte_created_at=", "lte_connected_at="],
+ GteKeys = ["gte_created_at=", "gte_connected_at="],
+ LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
+ LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
+ GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
+ GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
- RequestResults =
- [emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
- || Param <- LteParamRfc3339 ++ LteParamStamp
- ++ GteParamRfc3339 ++ GteParamStamp],
- DecodedResults = [emqx_json:decode(Response, [return_maps])
- || {ok, Response} <- RequestResults],
+ RequestResults =
+ [
+ emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
+ || Param <-
+ LteParamRfc3339 ++ LteParamStamp ++
+ GteParamRfc3339 ++ GteParamStamp
+ ],
+ DecodedResults = [
+ emqx_json:decode(Response, [return_maps])
+ || {ok, Response} <- RequestResults
+ ],
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
%% EachData :: list()
- [?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
+ [
+ ?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
- #{<<"created_at">> := CreatedAt} <- EachData],
- [?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
+ #{<<"created_at">> := CreatedAt} <- EachData
+ ],
+ [
+ ?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
- #{<<"connected_at">> := ConnectedAt} <- EachData],
- [?assertEqual(EachData, [])
- || #{<<"data">> := EachData} <- GteResponseDecodeds],
+ #{<<"connected_at">> := ConnectedAt} <- EachData
+ ],
+ [
+ ?assertEqual(EachData, [])
+ || #{<<"data">> := EachData} <- GteResponseDecodeds
+ ],
%% testcase cleanup, kickout client1 and client2
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
@@ -169,7 +203,7 @@ t_keepalive(_Config) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
Body = #{interval => 11},
- {error,{"HTTP/1.1",404,"Not Found"}} =
+ {error, {"HTTP/1.1", 404, "Not Found"}} =
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
{ok, _} = emqtt:connect(C1),
@@ -190,5 +224,6 @@ time_string_to_epoch(DateTime, Unit) when is_binary(DateTime) ->
catch
error:badarg ->
calendar:rfc3339_to_system_time(
- binary_to_list(DateTime), [{unit, Unit}])
+ binary_to_list(DateTime), [{unit, Unit}]
+ )
end.
diff --git a/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl
index 592c6ede2..b594751af 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_configs_SUITE.erl
@@ -32,10 +32,13 @@ end_per_suite(_) ->
t_get(_Config) ->
{ok, Configs} = get_configs(),
- maps:map(fun(Name, Value) ->
- {ok, Config} = get_config(Name),
- ?assertEqual(Value, Config)
- end, maps:remove(<<"license">>, Configs)),
+ maps:map(
+ fun(Name, Value) ->
+ {ok, Config} = get_config(Name),
+ ?assertEqual(Value, Config)
+ end,
+ maps:remove(<<"license">>, Configs)
+ ),
ok.
t_update(_Config) ->
@@ -50,8 +53,10 @@ t_update(_Config) ->
%% update failed
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
- ?assertMatch({error, {"HTTP/1.1", 400, _}},
- update_config(<<"sysmon">>, ErrorSysMon)),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 400, _}},
+ update_config(<<"sysmon">>, ErrorSysMon)
+ ),
{ok, SysMon2} = get_config(<<"sysmon">>),
?assertEqual(SysMon1, SysMon2),
@@ -101,8 +106,10 @@ t_global_zone(_Config) ->
{ok, Zones} = get_global_zone(),
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
- ?assertEqual(emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
- emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)),
+ ?assertEqual(
+ emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
+ emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)
+ ),
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
{ok, #{}} = update_global_zone(NewZones),
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
@@ -133,7 +140,8 @@ get_config(Name) ->
case emqx_mgmt_api_test_util:request_api(get, Path) of
{ok, Res} ->
{ok, emqx_json:decode(Res, [return_maps])};
- Error -> Error
+ Error ->
+ Error
end.
get_configs() ->
@@ -153,8 +161,11 @@ update_config(Name, Change) ->
reset_config(Name, Key) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
- Path = binary_to_list(iolist_to_binary(
- emqx_mgmt_api_test_util:api_path(["configs_reset", Name]))),
+ Path = binary_to_list(
+ iolist_to_binary(
+ emqx_mgmt_api_test_util:api_path(["configs_reset", Name])
+ )
+ ),
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
{ok, []} -> ok;
Error -> Error
diff --git a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl
index a146f48ef..e0c2586ec 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl
@@ -35,8 +35,8 @@ end_per_suite(_) ->
t_list_listeners(_) ->
Path = emqx_mgmt_api_test_util:api_path(["listeners"]),
Res = request(get, Path, [], []),
- Expect = emqx_mgmt_api_listeners:do_list_listeners(),
- ?assertEqual(emqx_json:encode([Expect]), emqx_json:encode(Res)),
+ #{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(),
+ ?assertEqual(length(Expect), length(Res)),
ok.
t_crud_listeners_by_id(_) ->
@@ -44,19 +44,18 @@ t_crud_listeners_by_id(_) ->
NewListenerId = <<"tcp:new">>,
TcpPath = emqx_mgmt_api_test_util:api_path(["listeners", TcpListenerId]),
NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]),
- [#{<<"listeners">> := [TcpListener], <<"node">> := Node}] = request(get, TcpPath, [], []),
- ?assertEqual(atom_to_binary(node()), Node),
+ TcpListener = request(get, TcpPath, [], []),
%% create
?assertEqual({error, not_found}, is_running(NewListenerId)),
- ?assertMatch([#{<<"listeners">> := []}], request(get, NewPath, [], [])),
+ ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])),
NewConf = TcpListener#{
<<"id">> => NewListenerId,
<<"bind">> => <<"0.0.0.0:2883">>
},
- [#{<<"listeners">> := [Create]}] = request(put, NewPath, [], NewConf),
+ Create = request(post, NewPath, [], NewConf),
?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))),
- [#{<<"listeners">> := [Get1]}] = request(get, NewPath, [], []),
+ Get1 = request(get, NewPath, [], []),
?assertMatch(Create, Get1),
?assert(is_running(NewListenerId)),
@@ -67,64 +66,21 @@ t_crud_listeners_by_id(_) ->
<<"id">> => BadId,
<<"bind">> => <<"0.0.0.0:2883">>
},
- ?assertEqual({error, {"HTTP/1.1", 400, "Bad Request"}}, request(put, BadPath, [], BadConf)),
+ ?assertMatch({error, {"HTTP/1.1", 400, _}}, request(post, BadPath, [], BadConf)),
%% update
#{<<"acceptors">> := Acceptors} = Create,
Acceptors1 = Acceptors + 10,
- [#{<<"listeners">> := [Update]}] =
+ Update =
request(put, NewPath, [], Create#{<<"acceptors">> => Acceptors1}),
?assertMatch(#{<<"acceptors">> := Acceptors1}, Update),
- [#{<<"listeners">> := [Get2]}] = request(get, NewPath, [], []),
- ?assertMatch(#{<<"acceptors">> := Acceptors1}, Get2),
-
- %% delete
- ?assertEqual([], delete(NewPath)),
- ?assertEqual({error, not_found}, is_running(NewListenerId)),
- ?assertMatch([#{<<"listeners">> := []}], request(get, NewPath, [], [])),
- ?assertEqual([], delete(NewPath)),
- ok.
-
-t_list_listeners_on_node(_) ->
- Node = atom_to_list(node()),
- Path = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners"]),
- Listeners = request(get, Path, [], []),
- #{<<"listeners">> := Expect} = emqx_mgmt_api_listeners:do_list_listeners(),
- ?assertEqual(emqx_json:encode(Expect), emqx_json:encode(Listeners)),
- ok.
-
-t_crud_listener_by_id_on_node(_) ->
- TcpListenerId = <<"tcp:default">>,
- NewListenerId = <<"tcp:new1">>,
- Node = atom_to_list(node()),
- TcpPath = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners", TcpListenerId]),
- NewPath = emqx_mgmt_api_test_util:api_path(["nodes", Node, "listeners", NewListenerId]),
- TcpListener = request(get, TcpPath, [], []),
-
- %% create
- ?assertEqual({error, not_found}, is_running(NewListenerId)),
- ?assertMatch({error,{"HTTP/1.1", 404, "Not Found"}}, request(get, NewPath, [], [])),
- Create = request(put, NewPath, [], TcpListener#{
- <<"id">> => NewListenerId,
- <<"bind">> => <<"0.0.0.0:3883">>
- }),
- ?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))),
- Get1 = request(get, NewPath, [], []),
- ?assertMatch(Create, Get1),
- ?assert(is_running(NewListenerId)),
-
- %% update
- #{<<"acceptors">> := Acceptors} = Create,
- Acceptors1 = Acceptors + 10,
- Update = request(put, NewPath, [], Create#{<<"acceptors">> => Acceptors1}),
- ?assertMatch(#{<<"acceptors">> := Acceptors1}, Update),
Get2 = request(get, NewPath, [], []),
?assertMatch(#{<<"acceptors">> := Acceptors1}, Get2),
%% delete
?assertEqual([], delete(NewPath)),
?assertEqual({error, not_found}, is_running(NewListenerId)),
- ?assertMatch({error, {"HTTP/1.1", 404, "Not Found"}}, request(get, NewPath, [], [])),
+ ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])),
?assertEqual([], delete(NewPath)),
ok.
@@ -139,8 +95,8 @@ action_listener(ID, Action, Running) ->
{ok, _} = emqx_mgmt_api_test_util:request_api(post, Path),
timer:sleep(500),
GetPath = emqx_mgmt_api_test_util:api_path(["listeners", ID]),
- [#{<<"listeners">> := Listeners}] = request(get, GetPath, [], []),
- [listener_stats(Listener, Running) || Listener <- Listeners].
+ Listener = request(get, GetPath, [], []),
+ listener_stats(Listener, Running).
request(Method, Url, QueryParams, Body) ->
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
diff --git a/apps/emqx_management/test/emqx_mgmt_api_metrics_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_metrics_SUITE.erl
index 5d6f9e80b..1166903d7 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_metrics_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_metrics_SUITE.erl
@@ -40,16 +40,17 @@ t_single_node_metrics_api(_) ->
{ok, MetricsResponse} = request_helper("metrics"),
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
LocalNodeMetrics = maps:from_list(
- emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]),
+ emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]
+ ),
match_helper(LocalNodeMetrics, MetricsFromAPI).
match_helper(SystemMetrics, MetricsFromAPI) ->
length_equal(SystemMetrics, MetricsFromAPI),
Fun =
- fun (Key, {SysMetrics, APIMetrics}) ->
- Value = maps:get(Key, SysMetrics),
- ?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
- {Value, {SysMetrics, APIMetrics}}
+ fun(Key, {SysMetrics, APIMetrics}) ->
+ Value = maps:get(Key, SysMetrics),
+ ?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
+ {Value, {SysMetrics, APIMetrics}}
end,
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
diff --git a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl
index 43e032991..0bb98aca6 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_nodes_SUITE.erl
@@ -67,19 +67,21 @@ t_nodes_api(_) ->
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
?assertMatch(
{error, {_, 400, _}},
- emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
+ emqx_mgmt_api_test_util:request_api(get, BadNodePath)
+ ).
t_log_path(_) ->
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
?assertEqual(
- <<"emqx-test.log">>,
- filename:basename(Path)).
+ <<"emqx-test.log">>,
+ filename:basename(Path)
+ ).
t_node_stats_api(_) ->
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
- SystemStats= emqx_mgmt:get_stats(),
+ SystemStats = emqx_mgmt:get_stats(),
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
Stats = emqx_json:decode(StatsResponse, [return_maps]),
Fun =
@@ -91,12 +93,13 @@ t_node_stats_api(_) ->
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
?assertMatch(
{error, {_, 400, _}},
- emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
+ emqx_mgmt_api_test_util:request_api(get, BadNodePath)
+ ).
t_node_metrics_api(_) ->
MetricsPath =
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
- SystemMetrics= emqx_mgmt:get_metrics(),
+ SystemMetrics = emqx_mgmt:get_metrics(),
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
Fun =
@@ -108,4 +111,5 @@ t_node_metrics_api(_) ->
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
?assertMatch(
{error, {_, 400, _}},
- emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
+ emqx_mgmt_api_test_util:request_api(get, BadNodePath)
+ ).
diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl
index a2612d176..ddf4de1a4 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl
@@ -56,17 +56,35 @@ todo_t_plugins(Config) ->
ok = emqx_plugins:delete_package(NameVsn),
ok = install_plugin(PackagePath),
{ok, StopRes} = describe_plugins(NameVsn),
- ?assertMatch(#{<<"running_status">> := [
- #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes),
+ ?assertMatch(
+ #{
+ <<"running_status">> := [
+ #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
+ ]
+ },
+ StopRes
+ ),
{ok, StopRes1} = update_plugin(NameVsn, "start"),
?assertEqual([], StopRes1),
{ok, StartRes} = describe_plugins(NameVsn),
- ?assertMatch(#{<<"running_status">> := [
- #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}]}, StartRes),
+ ?assertMatch(
+ #{
+ <<"running_status">> := [
+ #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}
+ ]
+ },
+ StartRes
+ ),
{ok, []} = update_plugin(NameVsn, "stop"),
{ok, StopRes2} = describe_plugins(NameVsn),
- ?assertMatch(#{<<"running_status">> := [
- #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes2),
+ ?assertMatch(
+ #{
+ <<"running_status">> := [
+ #{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
+ ]
+ },
+ StopRes2
+ ),
{ok, []} = uninstall_plugin(NameVsn),
ok.
@@ -87,8 +105,16 @@ describe_plugins(Name) ->
install_plugin(FilePath) ->
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
- case emqx_mgmt_api_test_util:upload_request(Path, FilePath, "plugin",
- <<"application/gzip">>, [], Token) of
+ case
+ emqx_mgmt_api_test_util:upload_request(
+ Path,
+ FilePath,
+ "plugin",
+ <<"application/gzip">>,
+ [],
+ Token
+ )
+ of
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
Error -> Error
end.
@@ -109,7 +135,6 @@ uninstall_plugin(Name) ->
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
-
build_demo_plugin_package(Dir) ->
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
diff --git a/apps/emqx_management/test/emqx_mgmt_api_publish_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_publish_SUITE.erl
index adf8e8456..4d17451c9 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_publish_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_publish_SUITE.erl
@@ -37,7 +37,9 @@ end_per_suite(_) ->
emqx_mgmt_api_test_util:end_suite().
t_publish_api(_) ->
- {ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
+ {ok, Client} = emqtt:start_link(#{
+ username => <<"api_username">>, clientid => <<"api_clientid">>
+ }),
{ok, _} = emqtt:connect(Client),
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
@@ -50,14 +52,16 @@ t_publish_api(_) ->
emqtt:disconnect(Client).
t_publish_bulk_api(_) ->
- {ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
+ {ok, Client} = emqtt:start_link(#{
+ username => <<"api_username">>, clientid => <<"api_clientid">>
+ }),
{ok, _} = emqtt:connect(Client),
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
Payload = <<"hello">>,
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
Auth = emqx_mgmt_api_test_util:auth_header_(),
- Body =[#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
+ Body = [#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
ResponseMap = emqx_json:decode(Response, [return_maps]),
?assertEqual(2, erlang:length(ResponseMap)),
@@ -68,12 +72,12 @@ t_publish_bulk_api(_) ->
receive_assert(Topic, Qos, Payload) ->
receive
{publish, Message} ->
- ReceiveTopic = maps:get(topic, Message),
- ReceiveQos = maps:get(qos, Message),
- ReceivePayload = maps:get(payload, Message),
- ?assertEqual(ReceiveTopic , Topic),
- ?assertEqual(ReceiveQos , Qos),
- ?assertEqual(ReceivePayload , Payload),
+ ReceiveTopic = maps:get(topic, Message),
+ ReceiveQos = maps:get(qos, Message),
+ ReceivePayload = maps:get(payload, Message),
+ ?assertEqual(ReceiveTopic, Topic),
+ ?assertEqual(ReceiveQos, Qos),
+ ?assertEqual(ReceivePayload, Payload),
ok
after 5000 ->
timeout
diff --git a/apps/emqx_management/test/emqx_mgmt_api_stats_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_stats_SUITE.erl
index e27a2d7f8..2b260aa9e 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_stats_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_stats_SUITE.erl
@@ -37,7 +37,7 @@ t_stats_api(_) ->
SystemStats1 = emqx_mgmt:get_stats(),
Fun1 =
fun(Key) ->
- ?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
+ ?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
end,
lists:foreach(Fun1, maps:keys(SystemStats1)),
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
diff --git a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl
index a668888a5..89c36d933 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl
@@ -58,13 +58,13 @@ t_subscription_api(_) ->
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
end,
- [Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
+ [Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
- QS = uri_string:compose_query([
+ QS = uri_string:compose_query([
{"clientid", ?CLIENTID},
{"topic", ?TOPIC2_TOPIC_ONLY},
{"node", atom_to_list(node())},
@@ -83,11 +83,11 @@ t_subscription_api(_) ->
?assertEqual(length(SubscriptionsList2), 1),
MatchQs = uri_string:compose_query([
- {"clientid", ?CLIENTID},
- {"node", atom_to_list(node())},
- {"qos", "0"},
- {"match_topic", "t/#"}
- ]),
+ {"clientid", ?CLIENTID},
+ {"node", atom_to_list(node())},
+ {"qos", "0"},
+ {"match_topic", "t/#"}
+ ]),
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
MatchData = emqx_json:decode(MatchRes, [return_maps]),
diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
index 9952686c5..1bdf584e5 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl
@@ -28,7 +28,6 @@ init_suite(Apps) ->
application:load(emqx_management),
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
-
end_suite() ->
end_suite([]).
@@ -39,15 +38,7 @@ end_suite(Apps) ->
ok.
set_special_configs(emqx_dashboard) ->
- Config = #{
- default_username => <<"admin">>,
- default_password => <<"public">>,
- listeners => [#{
- protocol => http,
- port => 18083
- }]
- },
- emqx_config:put([dashboard], Config),
+ emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_App) ->
ok.
@@ -61,36 +52,44 @@ request_api(Method, Url, AuthOrHeaders) ->
request_api(Method, Url, QueryParams, AuthOrHeaders) ->
request_api(Method, Url, QueryParams, AuthOrHeaders, []).
-request_api(Method, Url, QueryParams, AuthOrHeaders, [])
- when (Method =:= options) orelse
- (Method =:= get) orelse
- (Method =:= put) orelse
- (Method =:= head) orelse
- (Method =:= delete) orelse
- (Method =:= trace) ->
- NewUrl = case QueryParams of
- "" -> Url;
- _ -> Url ++ "?" ++ QueryParams
- end,
+request_api(Method, Url, QueryParams, AuthOrHeaders, []) when
+ (Method =:= options) orelse
+ (Method =:= get) orelse
+ (Method =:= put) orelse
+ (Method =:= head) orelse
+ (Method =:= delete) orelse
+ (Method =:= trace)
+->
+ NewUrl =
+ case QueryParams of
+ "" -> Url;
+ _ -> Url ++ "?" ++ QueryParams
+ end,
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
-request_api(Method, Url, QueryParams, AuthOrHeaders, Body)
- when (Method =:= post) orelse
- (Method =:= patch) orelse
- (Method =:= put) orelse
- (Method =:= delete) ->
- NewUrl = case QueryParams of
- "" -> Url;
- _ -> Url ++ "?" ++ QueryParams
- end,
- do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}).
+request_api(Method, Url, QueryParams, AuthOrHeaders, Body) when
+ (Method =:= post) orelse
+ (Method =:= patch) orelse
+ (Method =:= put) orelse
+ (Method =:= delete)
+->
+ NewUrl =
+ case QueryParams of
+ "" -> Url;
+ _ -> Url ++ "?" ++ QueryParams
+ end,
+ do_request_api(
+ Method,
+ {NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}
+ ).
-do_request_api(Method, Request)->
+do_request_api(Method, Request) ->
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], []) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
- {ok, {{"HTTP/1.1", Code, _}, _, Return} }
- when Code >= 200 andalso Code =< 299 ->
+ {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
+ Code >= 200 andalso Code =< 299
+ ->
{ok, Return};
{ok, {Reason, _, _} = Error} ->
ct:pal("error: ~p~n", [Error]),
@@ -105,11 +104,10 @@ auth_header_() ->
build_http_header(X) when is_list(X) ->
X;
-
build_http_header(X) ->
[X].
-api_path(Parts)->
+api_path(Parts) ->
?SERVER ++ filename:join([?BASE_PATH | Parts]).
%% Usage:
@@ -125,20 +123,27 @@ api_path(Parts)->
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
- {ok, binary()} | {error, list()} when
- URL:: binary(),
- FilePath:: binary(),
- Name:: binary(),
- MimeType:: binary(),
- RequestData:: list(),
- AuthorizationToken:: binary().
+ {ok, binary()} | {error, list()}
+when
+ URL :: binary(),
+ FilePath :: binary(),
+ Name :: binary(),
+ MimeType :: binary(),
+ RequestData :: list(),
+ AuthorizationToken :: binary().
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
Method = post,
Filename = filename:basename(FilePath),
{ok, Data} = file:read_file(FilePath),
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
- RequestBody = format_multipart_formdata(Data, RequestData, Name,
- [Filename], MimeType, Boundary),
+ RequestBody = format_multipart_formdata(
+ Data,
+ RequestData,
+ Name,
+ [Filename],
+ MimeType,
+ Boundary
+ ),
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
Headers = [
@@ -154,34 +159,56 @@ upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) -
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
- binary() when
- Data:: binary(),
- Params:: list(),
- Name:: binary(),
- FileNames:: list(),
- MimeType:: binary(),
- Boundary:: binary().
+ binary()
+when
+ Data :: binary(),
+ Params :: list(),
+ Name :: binary(),
+ FileNames :: list(),
+ MimeType :: binary(),
+ Boundary :: binary().
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
LineSeparator = <<"\r\n">>,
- WithParams = lists:foldl(fun({Key, Value}, Acc) ->
- erlang:iolist_to_binary([
- Acc,
- StartBoundary, LineSeparator,
- <<"Content-Disposition: form-data; name=\"">>, Key, <<"\"">>,
- LineSeparator, LineSeparator,
- Value, LineSeparator
- ])
- end, <<"">>, Params),
- WithPaths = lists:foldl(fun(FileName, Acc) ->
- erlang:iolist_to_binary([
- Acc,
- StartBoundary, LineSeparator,
- <<"Content-Disposition: form-data; name=\"">>, Name, <<"\"; filename=\"">>,
- FileName, <<"\"">>, LineSeparator,
- <<"Content-Type: ">>, MimeType, LineSeparator, LineSeparator,
- Data,
- LineSeparator
- ])
- end, WithParams, FileNames),
+ WithParams = lists:foldl(
+ fun({Key, Value}, Acc) ->
+ erlang:iolist_to_binary([
+ Acc,
+ StartBoundary,
+ LineSeparator,
+ <<"Content-Disposition: form-data; name=\"">>,
+ Key,
+ <<"\"">>,
+ LineSeparator,
+ LineSeparator,
+ Value,
+ LineSeparator
+ ])
+ end,
+ <<"">>,
+ Params
+ ),
+ WithPaths = lists:foldl(
+ fun(FileName, Acc) ->
+ erlang:iolist_to_binary([
+ Acc,
+ StartBoundary,
+ LineSeparator,
+ <<"Content-Disposition: form-data; name=\"">>,
+ Name,
+ <<"\"; filename=\"">>,
+ FileName,
+ <<"\"">>,
+ LineSeparator,
+ <<"Content-Type: ">>,
+ MimeType,
+ LineSeparator,
+ LineSeparator,
+ Data,
+ LineSeparator
+ ])
+ end,
+ WithParams,
+ FileNames
+ ),
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
diff --git a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl
index f6536c3b1..a06154400 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_topics_SUITE.erl
@@ -32,7 +32,9 @@ end_per_suite(_) ->
t_nodes_api(_) ->
Topic = <<"test_topic">>,
- {ok, Client} = emqtt:start_link(#{username => <<"routes_username">>, clientid => <<"routes_cid">>}),
+ {ok, Client} = emqtt:start_link(#{
+ username => <<"routes_username">>, clientid => <<"routes_cid">>
+ }),
{ok, _} = emqtt:connect(Client),
{ok, _, _} = emqtt:subscribe(Client, Topic),
diff --git a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl
index 8625a804c..255698b16 100644
--- a/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl
+++ b/apps/emqx_management/test/emqx_mgmt_api_trace_SUITE.erl
@@ -59,7 +59,9 @@ t_http_test(_Config) ->
#{
<<"code">> => <<"BAD_REQUEST">>,
<<"message">> => <<"name : mandatory_required_field">>
- }, json(Body)),
+ },
+ json(Body)
+ ),
Name = <<"test-name">>,
Trace = [
@@ -77,32 +79,47 @@ t_http_test(_Config) ->
%% update
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
- ?assertEqual(#{<<"enable">> => false,
- <<"name">> => <<"test-name">>}, json(Update)),
+ ?assertEqual(
+ #{
+ <<"enable">> => false,
+ <<"name">> => <<"test-name">>
+ },
+ json(Update)
+ ),
- ?assertMatch({error, {"HTTP/1.1", 404, _}, _},
- request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 404, _}, _},
+ request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})
+ ),
{ok, List1} = request_api(get, api_path("trace"), Header),
[Data1] = json(List1),
Node = atom_to_binary(node()),
- ?assertMatch(#{
- <<"status">> := <<"stopped">>,
- <<"name">> := <<"test-name">>,
- <<"log_size">> := #{Node := _},
- <<"start_at">> := _,
- <<"end_at">> := _,
- <<"type">> := <<"topic">>,
- <<"topic">> := <<"/x/y/z">>
- }, Data1),
+ ?assertMatch(
+ #{
+ <<"status">> := <<"stopped">>,
+ <<"name">> := <<"test-name">>,
+ <<"log_size">> := #{Node := _},
+ <<"start_at">> := _,
+ <<"end_at">> := _,
+ <<"type">> := <<"topic">>,
+ <<"topic">> := <<"/x/y/z">>
+ },
+ Data1
+ ),
%% delete
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
?assertEqual(<<>>, Delete),
- {error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound}
- = request_api(delete, api_path("trace/test-name"), Header),
- ?assertEqual(#{<<"code">> => <<"NOT_FOUND">>,
- <<"message">> => <<"test-name NOT FOUND">>}, json(DeleteNotFound)),
+ {error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound} =
+ request_api(delete, api_path("trace/test-name"), Header),
+ ?assertEqual(
+ #{
+ <<"code">> => <<"NOT_FOUND">>,
+ <<"message">> => <<"test-name NOT FOUND">>
+ },
+ json(DeleteNotFound)
+ ),
{ok, List2} = request_api(get, api_path("trace"), Header),
?assertEqual([], json(List2)),
@@ -123,29 +140,43 @@ t_create_failed(_Config) ->
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
BadName1 = {<<"name">>, <<"test/bad">>},
- ?assertMatch({error, {"HTTP/1.1", 400, _}, _},
- request_api(post, api_path("trace"), Header, [BadName1 | Trace])),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 400, _}, _},
+ request_api(post, api_path("trace"), Header, [BadName1 | Trace])
+ ),
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
- ?assertMatch({error, {"HTTP/1.1", 400, _}, _},
- request_api(post, api_path("trace"), Header, [BadName2 | Trace])),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 400, _}, _},
+ request_api(post, api_path("trace"), Header, [BadName2 | Trace])
+ ),
%% already_exist
GoodName = {<<"name">>, <<"test-name-0">>},
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
- ?assertMatch({error, {"HTTP/1.1", 400, _}, _},
- request_api(post, api_path("trace"), Header, [GoodName | Trace])),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 400, _}, _},
+ request_api(post, api_path("trace"), Header, [GoodName | Trace])
+ ),
%% MAX Limited
- lists:map(fun(Seq) ->
- Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
- Trace0 = [{name, Name0}, {type, topic},
- {topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}],
- {ok, _} = emqx_trace:create(Trace0)
- end, lists:seq(1, 30 - ets:info(emqx_trace, size))),
+ lists:map(
+ fun(Seq) ->
+ Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
+ Trace0 = [
+ {name, Name0},
+ {type, topic},
+ {topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}
+ ],
+ {ok, _} = emqx_trace:create(Trace0)
+ end,
+ lists:seq(1, 30 - ets:info(emqx_trace, size))
+ ),
GoodName1 = {<<"name">>, <<"test-name-1">>},
- ?assertMatch({error, {"HTTP/1.1", 400, _}, _},
- request_api(post, api_path("trace"), Header, [GoodName1 | Trace])),
+ ?assertMatch(
+ {error, {"HTTP/1.1", 400, _}, _},
+ request_api(post, api_path("trace"), Header, [GoodName1 | Trace])
+ ),
unload(),
emqx_trace:clear(),
ok.
@@ -158,14 +189,23 @@ t_download_log(_Config) ->
create_trace(Name, ClientId, Now),
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
{ok, _} = emqtt:connect(Client),
- [begin _ = emqtt:ping(Client) end ||_ <- lists:seq(1, 5)],
+ [
+ begin
+ _ = emqtt:ping(Client)
+ end
+ || _ <- lists:seq(1, 5)
+ ],
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
Header = auth_header_(),
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
- {ok, [_Comment,
- #zip_file{name = ZipName,
- info = #file_info{size = Size, type = regular, access = read_write}}]}
- = zip:table(Binary),
+ {ok, [
+ _Comment,
+ #zip_file{
+ name = ZipName,
+ info = #file_info{size = Size, type = regular, access = read_write}
+ }
+ ]} =
+ zip:table(Binary),
?assert(Size > 0),
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
@@ -176,13 +216,18 @@ create_trace(Name, ClientId, Start) ->
?check_trace(
#{timetrap => 900},
begin
- {ok, _} = emqx_trace:create([{<<"name">>, Name},
- {<<"type">>, clientid}, {<<"clientid">>, ClientId}, {<<"start_at">>, Start}]),
+ {ok, _} = emqx_trace:create([
+ {<<"name">>, Name},
+ {<<"type">>, clientid},
+ {<<"clientid">>, ClientId},
+ {<<"start_at">>, Start}
+ ]),
?block_until(#{?snk_kind := update_trace_done})
end,
fun(Trace) ->
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
- end).
+ end
+ ).
t_stream_log(_Config) ->
application:set_env(emqx, allow_anonymous, true),
@@ -194,7 +239,12 @@ t_stream_log(_Config) ->
create_trace(Name, ClientId, Now - 10),
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
{ok, _} = emqtt:connect(Client),
- [begin _ = emqtt:ping(Client) end || _ <- lists:seq(1, 5)],
+ [
+ begin
+ _ = emqtt:ping(Client)
+ end
+ || _ <- lists:seq(1, 5)
+ ],
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
ok = emqtt:disconnect(Client),
@@ -239,8 +289,9 @@ do_request_api(Method, Request) ->
{error, socket_closed_remotely};
{error, {shutdown, server_closed}} ->
{error, server_closed};
- {ok, {{"HTTP/1.1", Code, _}, _Headers, Return}}
- when Code =:= 200 orelse Code =:= 201 orelse Code =:= 204 ->
+ {ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when
+ Code =:= 200 orelse Code =:= 201 orelse Code =:= 204
+ ->
{ok, Return};
{ok, {Reason, _Header, Body}} ->
{error, Reason, Body}
@@ -250,7 +301,8 @@ api_path(Path) ->
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
json(Data) ->
- {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.
+ {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]),
+ Jsx.
load() ->
emqx_trace:start_link().
diff --git a/apps/emqx_modules/i18n/emqx_modules_schema_i18n.conf b/apps/emqx_modules/i18n/emqx_modules_schema_i18n.conf
new file mode 100644
index 000000000..128bb92b9
--- /dev/null
+++ b/apps/emqx_modules/i18n/emqx_modules_schema_i18n.conf
@@ -0,0 +1,74 @@
+emqx_modules_schema {
+
+ rewrite {
+ desc {
+ en: """The topic rewriting function of EMQX supports rewriting topic A to topic B when the client subscribes to topics, publishes messages, and cancels subscriptions according to user-configured rules.
+Each rewrite rule consists of three parts: subject filter, regular expression, and target expression.
+Under the premise that the subject rewriting function is enabled, when EMQX receives a subject-based MQTT message such as a PUBLISH message,
+it will use the subject of the message to sequentially match the subject filter part of the rule in the configuration file. If the match is successful,
+the regular expression is used to extract the information in the subject, and then replaced with the target expression to form a new subject.
+Variables in the format of $N can be used in the target expression to match the elements extracted from the regular expression.
+The value of $N is the Nth element extracted from the regular expression. For example, $1 is the regular expression. The first element extracted by the expression.
+It should be noted that EMQX uses reverse order to read the rewrite rules in the configuration file.
+When a topic can match the topic filter of multiple topic rewrite rules at the same time, EMQX will only use the first rule it matches. Rewrite.
+If the regular expression in this rule does not match the subject of the MQTT message, the rewriting will fail, and no other rules will be attempted for rewriting.
+Therefore, users need to carefully design MQTT message topics and topic rewriting rules when using them."""
+ zh: """EMQX 的主题重写功能支持根据用户配置的规则在客户端订阅主题、发布消息、取消订阅的时候将 A 主题重写为 B 主题。
+重写规则分为 Pub 规则和 Sub 规则,Pub 规则匹配 PUSHLISH 报文携带的主题,Sub 规则匹配 SUBSCRIBE、UNSUBSCRIBE 报文携带的主题。
+每条重写规则都由主题过滤器、正则表达式、目标表达式三部分组成。
+在主题重写功能开启的前提下,EMQX 在收到诸如 PUBLISH 报文等带有主题的 MQTT 报文时,将使用报文中的主题去依次匹配配置文件中规则的主题过滤器部分,一旦成功匹配,则使用正则表达式提取主题中的信息,然后替换至目标表达式以构成新的主题。
+目标表达式中可以使用 $N 这种格式的变量匹配正则表达中提取出来的元素,$N 的值为正则表达式中提取出来的第 N 个元素,比如 $1 即为正则表达式提取的第一个元素。
+需要注意的是,EMQX 使用倒序读取配置文件中的重写规则,当一条主题可以同时匹配多条主题重写规则的主题过滤器时,EMQX 仅会使用它匹配到的第一条规则进行重写,如果该条规则中的正则表达式与 MQTT 报文主题不匹配,则重写失败,不会再尝试使用其他的规则进行重写。
+因此用户在使用时需要谨慎的设计 MQTT 报文主题以及主题重写规则。"""
+ }
+ label {
+ en: """Topic Rewrite"""
+ zh: """主题重写"""
+ }
+ }
+
+ tr_source_topic {
+ desc {
+ en: """Source topic, specified by the client."""
+ zh: """源主题,客户端业务指定的主题"""
+ }
+ label {
+ en: """Source Topic"""
+ zh: """源主题"""
+ }
+ }
+
+ tr_action {
+ desc {
+ en: """subscribe: Rewrite topic when client do subscribe.
+publish: Rewrite topic when client do publish.
+all: Both"""
+ zh: """subscribe:订阅时重写主题;
+publish:发布时重写主题;
+all:全部重写主题"""
+ }
+ label {
+ en: """Action"""
+ zh: """Action"""
+ }
+ }
+
+ tr_re {
+ desc {
+ en: """Regular expressions"""
+ zh: """正则表达式"""
+ }
+ }
+
+ tr_dest_topic {
+ desc {
+ en: """Destination topic."""
+ zh: """目标主题。"""
+ }
+ label {
+ en: """Destination Topic"""
+ zh: """目标主题"""
+ }
+ }
+
+}
diff --git a/apps/emqx_modules/i18n/emqx_rewrite_api_i18n.conf b/apps/emqx_modules/i18n/emqx_rewrite_api_i18n.conf
new file mode 100644
index 000000000..91f15cb8c
--- /dev/null
+++ b/apps/emqx_modules/i18n/emqx_rewrite_api_i18n.conf
@@ -0,0 +1,25 @@
+emqx_rewrite_api {
+
+ list_topic_rewrite_api {
+ desc {
+ en: """List all rewrite rules"""
+ zh: """列出全部主题重写规则"""
+ }
+ }
+
+ update_topic_rewrite_api {
+ desc {
+ en: """Update all rewrite rules"""
+ zh: """更新全部主题重写规则"""
+ }
+ }
+
+
+ update_topic_rewrite_api_response413 {
+ desc {
+ en: """Rules count exceed max limit"""
+ zh: """超出主题重写规则数量上限"""
+ }
+ }
+
+}
diff --git a/apps/emqx_modules/src/emqx_delayed_api.erl b/apps/emqx_modules/src/emqx_delayed_api.erl
index 4029646af..c1347922c 100644
--- a/apps/emqx_modules/src/emqx_delayed_api.erl
+++ b/apps/emqx_modules/src/emqx_delayed_api.erl
@@ -149,9 +149,9 @@ schema("/mqtt/delayed/messages") ->
[
{data, mk(hoconsc:array(ref("message")), #{})},
{meta, [
- {page, mk(integer(), #{})},
- {limit, mk(integer(), #{})},
- {count, mk(integer(), #{})}
+ {page, mk(pos_integer(), #{})},
+ {limit, mk(pos_integer(), #{})},
+ {count, mk(non_neg_integer(), #{})}
]}
]
}
@@ -163,11 +163,11 @@ fields("message_without_payload") ->
{msgid, mk(integer(), #{desc => <<"Message Id (MQTT message id hash)">>})},
{node, mk(binary(), #{desc => <<"The node where message from">>})},
{publish_at, mk(binary(), #{desc => <<"Client publish message time, rfc 3339">>})},
- {delayed_interval, mk(integer(), #{desc => <<"Delayed interval, second">>})},
- {delayed_remaining, mk(integer(), #{desc => <<"Delayed remaining, second">>})},
+ {delayed_interval, mk(pos_integer(), #{desc => <<"Delayed interval, second">>})},
+ {delayed_remaining, mk(non_neg_integer(), #{desc => <<"Delayed remaining, second">>})},
{expected_at, mk(binary(), #{desc => <<"Expect publish time, rfc 3339">>})},
{topic, mk(binary(), #{desc => <<"Topic">>, example => <<"/sys/#">>})},
- {qos, mk(binary(), #{desc => <<"QoS">>})},
+ {qos, mk(emqx_schema:qos(), #{desc => <<"QoS">>})},
{from_clientid, mk(binary(), #{desc => <<"From ClientId">>})},
{from_username, mk(binary(), #{desc => <<"From Username">>})}
];
diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl
index a74406488..9c1e07dbb 100644
--- a/apps/emqx_modules/src/emqx_modules_schema.erl
+++ b/apps/emqx_modules/src/emqx_modules_schema.erl
@@ -16,6 +16,7 @@
-module(emqx_modules_schema).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
@@ -50,17 +51,17 @@ fields("rewrite") ->
{action,
sc(
hoconsc:enum([subscribe, publish, all]),
- #{desc => <<"Action">>, example => publish}
+ #{required => true, desc => ?DESC(tr_action), example => publish}
)},
{source_topic,
sc(
binary(),
- #{desc => <<"Origin Topic">>, example => "x/#"}
+ #{required => true, desc => ?DESC(tr_source_topic), example => "x/#"}
)},
{dest_topic,
sc(
binary(),
- #{desc => <<"Destination Topic">>, example => "z/y/$1"}
+ #{required => true, desc => ?DESC(tr_dest_topic), example => "z/y/$1"}
)},
{re, fun regular_expression/1}
];
@@ -72,14 +73,15 @@ desc("telemetry") ->
desc("delayed") ->
"Settings for the delayed module.";
desc("rewrite") ->
- "Rewrite rule.";
+ ?DESC(rewrite);
desc("topic_metrics") ->
"";
desc(_) ->
undefined.
regular_expression(type) -> binary();
-regular_expression(desc) -> "Regular expressions";
+regular_expression(required) -> true;
+regular_expression(desc) -> ?DESC(tr_re);
regular_expression(example) -> "^x/y/(.+)$";
regular_expression(validator) -> fun is_re/1;
regular_expression(_) -> undefined.
diff --git a/apps/emqx_modules/src/emqx_rewrite_api.erl b/apps/emqx_modules/src/emqx_rewrite_api.erl
index 264a36a06..a62b2a9b2 100644
--- a/apps/emqx_modules/src/emqx_rewrite_api.erl
+++ b/apps/emqx_modules/src/emqx_rewrite_api.erl
@@ -16,6 +16,8 @@
-module(emqx_rewrite_api).
-behaviour(minirest_api).
+
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-include("emqx_modules.hrl").
@@ -38,16 +40,16 @@ schema("/mqtt/topic_rewrite") ->
'operationId' => topic_rewrite,
get => #{
tags => ?API_TAG_MQTT,
- description => <<"List rewrite topic.">>,
+ description => ?DESC(list_topic_rewrite_api),
responses => #{
200 => hoconsc:mk(
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
- #{desc => <<"List all rewrite rules">>}
+ #{desc => ?DESC(list_topic_rewrite_api)}
)
}
},
put => #{
- description => <<"Update rewrite topic">>,
+ description => ?DESC(update_topic_rewrite_api),
tags => ?API_TAG_MQTT,
'requestBody' => hoconsc:mk(
hoconsc:array(
@@ -58,11 +60,11 @@ schema("/mqtt/topic_rewrite") ->
responses => #{
200 => hoconsc:mk(
hoconsc:array(hoconsc:ref(emqx_modules_schema, "rewrite")),
- #{desc => <<"Update rewrite topic success.">>}
+ #{desc => ?DESC(update_topic_rewrite_api)}
),
413 => emqx_dashboard_swagger:error_codes(
[?EXCEED_LIMIT],
- <<"Rules count exceed max limit">>
+ ?DESC(update_topic_rewrite_api_response413)
)
}
}
diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_metrics.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_metrics.erl
index 98c4490ee..da0d0ab4f 100644
--- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_metrics.erl
+++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_metrics.erl
@@ -307,17 +307,19 @@ calculate_rate(CurrVal, #rate{max = MaxRate0, last_v = LastVal,
%% calculate the max rate since the emqx startup
MaxRate =
- if MaxRate0 >= CurrRate -> MaxRate0;
- true -> CurrRate
+ case MaxRate0 >= CurrRate of
+ true -> MaxRate0;
+ false -> CurrRate
end,
%% calculate the average rate in last 5 mins
{Last5MinSamples, Acc5Min, Last5Min} =
- if Tick =< ?SAMPCOUNT_5M ->
+ case Tick =< ?SAMPCOUNT_5M of
+ true ->
Acc = AccRate5Min0 + CurrRate,
{lists:reverse([CurrRate | lists:reverse(Last5MinSamples0)]),
Acc, Acc / Tick};
- true ->
+ false ->
[FirstRate | Rates] = Last5MinSamples0,
Acc = AccRate5Min0 + CurrRate - FirstRate,
{lists:reverse([CurrRate | lists:reverse(Rates)]),
diff --git a/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf
new file mode 100644
index 000000000..f49d8d00c
--- /dev/null
+++ b/apps/emqx_prometheus/i18n/emqx_prometheus_schema_i18n.conf
@@ -0,0 +1,33 @@
+emqx_prometheus_schema {
+
+ prometheus {
+ desc {
+ en: """Settings for reporting metrics to Prometheus"""
+ zh: """Prometheus 监控数据推送"""
+ }
+ label {
+ en: """Prometheus"""
+ zh: """Prometheus"""
+ }
+ }
+
+ push_gateway_server {
+ desc {
+ en: """URL of Prometheus server"""
+ zh: """Prometheus 服务器地址"""
+ }
+ }
+
+ interval {
+ desc {
+ en: """Data reporting interval, in milliseconds."""
+ zh: """数据推送间隔,单位 毫秒"""
+ }
+ }
+ enable {
+ desc {
+ en: """Turn Prometheus data pushing on or off"""
+ zh: """开启或关闭 Prometheus 数据推送"""
+ }
+ }
+}
diff --git a/apps/emqx_prometheus/rebar.config b/apps/emqx_prometheus/rebar.config
index b05091515..36a53d1bb 100644
--- a/apps/emqx_prometheus/rebar.config
+++ b/apps/emqx_prometheus/rebar.config
@@ -4,7 +4,7 @@
[ {emqx, {path, "../emqx"}},
%% FIXME: tag this as v3.1.3
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
- {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}}
+ {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}}
]}.
{edoc_opts, [{preprocess, true}]}.
diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl
index 6e6ebd4b9..98f9a519b 100644
--- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl
+++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl
@@ -15,6 +15,7 @@
%%--------------------------------------------------------------------
-module(emqx_prometheus_schema).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
@@ -30,25 +31,25 @@ namespace() -> "prometheus".
roots() -> ["prometheus"].
fields("prometheus") ->
- [ {push_gateway_server, sc(string(),
+ [
+ {push_gateway_server, sc(string(),
#{ default => "http://127.0.0.1:9091"
, required => true
- , desc => "URL of Prometheus pushgateway."
- })}
- , {interval, sc(emqx_schema:duration_ms(),
+ , desc => ?DESC(push_gateway_server)
+ })},
+ {interval, sc(emqx_schema:duration_ms(),
#{ default => "15s"
, required => true
- , desc => "Data reporting interval in milliseconds."
- })}
- , {enable, sc(boolean(),
+ , desc => ?DESC(interval)
+ })},
+ {enable, sc(boolean(),
#{ default => false
, required => true
- , desc => "Enable reporting of metrics via Prometheus Pushgateway."
+ , desc => ?DESC(enable)
})}
].
-desc("prometheus") ->
- "Settings for reporting metrics to Prometheus pushgateway.";
+desc("prometheus") -> ?DESC(prometheus);
desc(_) ->
undefined.
diff --git a/apps/emqx_retainer/include/emqx_retainer.hrl b/apps/emqx_retainer/include/emqx_retainer.hrl
index 7a5d4d83f..95d5eb9fb 100644
--- a/apps/emqx_retainer/include/emqx_retainer.hrl
+++ b/apps/emqx_retainer/include/emqx_retainer.hrl
@@ -24,8 +24,10 @@
-type payload() :: binary().
-type message() :: #message{}.
--type context() :: #{context_id := pos_integer(),
- atom() => term()}.
+-type context() :: #{
+ context_id := pos_integer(),
+ atom() => term()
+}.
-define(DELIVER_SEMAPHORE, deliver_remained_quota).
-type semaphore() :: ?DELIVER_SEMAPHORE.
diff --git a/apps/emqx_retainer/rebar.config b/apps/emqx_retainer/rebar.config
index b3f60c616..7e791f90f 100644
--- a/apps/emqx_retainer/rebar.config
+++ b/apps/emqx_retainer/rebar.config
@@ -1,27 +1,35 @@
%% -*- mode: erlang -*-
-{deps, [ {emqx, {path, "../emqx"}}
- ]}.
+{deps, [{emqx, {path, "../emqx"}}]}.
{edoc_opts, [{preprocess, true}]}.
-{erl_opts, [warn_unused_vars,
- warn_shadow_vars,
- warn_unused_import,
- warn_obsolete_guard,
- debug_info,
- {parse_transform}]}.
+{erl_opts, [
+ warn_unused_vars,
+ warn_shadow_vars,
+ warn_unused_import,
+ warn_obsolete_guard,
+ debug_info,
+ {parse_transform}
+]}.
-{xref_checks, [undefined_function_calls, undefined_functions,
- locals_not_used, deprecated_function_calls,
- warnings_as_errors, deprecated_functions]}.
+{xref_checks, [
+ undefined_function_calls,
+ undefined_functions,
+ locals_not_used,
+ deprecated_function_calls,
+ warnings_as_errors,
+ deprecated_functions
+]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
-{profiles,
- [{test,
- [{deps,
- [
- {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}}]}
- ]}
- ]}.
+{profiles, [
+ {test, [
+ {deps, [
+ {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}}
+ ]}
+ ]}
+]}.
+
+{project_plugins, [erlfmt]}.
diff --git a/apps/emqx_retainer/src/emqx_retainer.app.src b/apps/emqx_retainer/src/emqx_retainer.app.src
index ab5f2e1cd..ea6b58317 100644
--- a/apps/emqx_retainer/src/emqx_retainer.app.src
+++ b/apps/emqx_retainer/src/emqx_retainer.app.src
@@ -1,15 +1,17 @@
%% -*- mode: erlang -*-
-{application, emqx_retainer,
- [{description, "EMQX Retainer"},
- {vsn, "5.0.0"}, % strict semver, bump manually!
- {modules, []},
- {registered, [emqx_retainer_sup]},
- {applications, [kernel,stdlib,emqx]},
- {mod, {emqx_retainer_app,[]}},
- {env, []},
- {licenses, ["Apache-2.0"]},
- {maintainers, ["EMQX Team "]},
- {links, [{"Homepage", "https://emqx.io/"},
- {"Github", "https://github.com/emqx/emqx-retainer"}
- ]}
- ]}.
+{application, emqx_retainer, [
+ {description, "EMQX Retainer"},
+ % strict semver, bump manually!
+ {vsn, "5.0.0"},
+ {modules, []},
+ {registered, [emqx_retainer_sup]},
+ {applications, [kernel, stdlib, emqx]},
+ {mod, {emqx_retainer_app, []}},
+ {env, []},
+ {licenses, ["Apache-2.0"]},
+ {maintainers, ["EMQX Team "]},
+ {links, [
+ {"Homepage", "https://emqx.io/"},
+ {"Github", "https://github.com/emqx/emqx-retainer"}
+ ]}
+]}.
diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl
index 247f597b0..51ffe950b 100644
--- a/apps/emqx_retainer/src/emqx_retainer.erl
+++ b/apps/emqx_retainer/src/emqx_retainer.erl
@@ -23,38 +23,43 @@
-export([start_link/0]).
--export([ on_session_subscribed/4
- , on_message_publish/2
- ]).
+-export([
+ on_session_subscribed/4,
+ on_message_publish/2
+]).
--export([ delete_message/2
- , store_retained/2
- , get_backend_module/0
- ]).
+-export([
+ delete_message/2,
+ store_retained/2,
+ get_backend_module/0
+]).
--export([ get_expiry_time/1
- , update_config/1
- , clean/0
- , delete/1
- , page_read/3
- , post_config_update/5
- , stats_fun/0
- ]).
+-export([
+ get_expiry_time/1,
+ update_config/1,
+ clean/0,
+ delete/1,
+ page_read/3,
+ post_config_update/5,
+ stats_fun/0
+]).
%% gen_server callbacks
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- , terminate/2
- , code_change/3
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3
+]).
--type state() :: #{ enable := boolean()
- , context_id := non_neg_integer()
- , context := undefined | context()
- , clear_timer := undefined | reference()
- }.
+-type state() :: #{
+ enable := boolean(),
+ context_id := non_neg_integer(),
+ context := undefined | context(),
+ clear_timer := undefined | reference()
+}.
-define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)).
-define(DEF_EXPIRY_INTERVAL, 0).
@@ -86,10 +91,14 @@ on_session_subscribed(_, Topic, #{rh := Rh} = Opts, Context) ->
end.
%% RETAIN flag set to 1 and payload containing zero bytes
-on_message_publish(Msg = #message{flags = #{retain := true},
- topic = Topic,
- payload = <<>>},
- Context) ->
+on_message_publish(
+ Msg = #message{
+ flags = #{retain := true},
+ topic = Topic,
+ payload = <<>>
+ },
+ Context
+) ->
delete_message(Context, Topic),
case get_stop_publish_clear_msg() of
true ->
@@ -97,7 +106,6 @@ on_message_publish(Msg = #message{flags = #{retain := true},
_ ->
{ok, Msg}
end;
-
on_message_publish(Msg = #message{flags = #{retain := true}}, Context) ->
Msg1 = emqx_message:set_header(retained, true, Msg),
store_retained(Context, Msg1),
@@ -110,14 +118,16 @@ on_message_publish(Msg, _) ->
%%--------------------------------------------------------------------
%% @doc Start the retainer
--spec(start_link() -> emqx_types:startlink_ret()).
+-spec start_link() -> emqx_types:startlink_ret().
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) ->
0;
-get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
- timestamp = Ts}) ->
+get_expiry_time(#message{
+ headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
+ timestamp = Ts
+}) ->
Ts + Interval * 1000;
get_expiry_time(#message{timestamp = Ts}) ->
Interval = emqx_conf:get([retainer, msg_expiry_interval], ?DEF_EXPIRY_INTERVAL),
@@ -161,30 +171,26 @@ init([]) ->
State = new_state(),
#{enable := Enable} = Cfg = emqx:get_config([retainer]),
{ok,
- case Enable of
- true ->
- enable_retainer(State, Cfg);
- _ ->
- State
- end}.
+ case Enable of
+ true ->
+ enable_retainer(State, Cfg);
+ _ ->
+ State
+ end}.
handle_call({update_config, NewConf, OldConf}, _, State) ->
State2 = update_config(State, NewConf, OldConf),
{reply, ok, State2};
-
handle_call(clean, _, #{context := Context} = State) ->
clean(Context),
{reply, ok, State};
-
handle_call({delete, Topic}, _, #{context := Context} = State) ->
delete_message(Context, Topic),
{reply, ok, State};
-
handle_call({page_read, Topic, Page, Limit}, _, #{context := Context} = State) ->
Mod = get_backend_module(),
Result = Mod:page_read(Context, Topic, Page, Limit),
{reply, Result, State};
-
handle_call(Req, _From, State) ->
?SLOG(error, #{msg => "unexpected_call", call => Req}),
{reply, ignored, State}.
@@ -194,7 +200,6 @@ handle_cast(stats_fun, #{context := Context} = State) ->
Size = Mod:size(Context),
emqx_stats:setstat('retained.count', 'retained.max', Size),
{noreply, State};
-
handle_cast(Msg, State) ->
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
{noreply, State}.
@@ -204,7 +209,6 @@ handle_info(clear_expired, #{context := Context} = State) ->
Mod:clear_expired(Context),
Interval = emqx_conf:get([retainer, msg_clear_interval], ?DEF_EXPIRY_INTERVAL),
{noreply, State#{clear_timer := add_timer(Interval, clear_expired)}, hibernate};
-
handle_info(Info, State) ->
?SLOG(error, #{msg => "unexpected_info", info => Info}),
{noreply, State}.
@@ -222,11 +226,12 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
-spec new_state() -> state().
new_state() ->
- #{enable => false,
- context_id => 0,
- context => undefined,
- clear_timer => undefined
- }.
+ #{
+ enable => false,
+ context_id => 0,
+ context => undefined,
+ clear_timer => undefined
+ }.
-spec new_context(pos_integer()) -> context().
new_context(Id) ->
@@ -249,11 +254,13 @@ store_retained(Context, #message{topic = Topic, payload = Payload} = Msg) ->
Size = iolist_size(Payload),
case payload_size_limit() of
Limit when Limit > 0 andalso Limit < Size ->
- ?SLOG(error, #{msg => "retain_failed_for_payload_size_exceeded_limit",
- topic => Topic,
- config => emqx_hocon:format_path(?MAX_PAYLOAD_SIZE_CONFIG_PATH),
- size => Size,
- limit => Limit});
+ ?SLOG(error, #{
+ msg => "retain_failed_for_payload_size_exceeded_limit",
+ topic => Topic,
+ config => emqx_hocon:format_path(?MAX_PAYLOAD_SIZE_CONFIG_PATH),
+ size => Size,
+ limit => Limit
+ });
_ ->
Mod = get_backend_module(),
Mod:store_retained(Context, Msg)
@@ -266,23 +273,30 @@ clean(Context) ->
-spec update_config(state(), hocons:config(), hocons:config()) -> state().
update_config(State, Conf, OldConf) ->
- update_config(maps:get(enable, Conf),
- maps:get(enable, OldConf),
- State,
- Conf,
- OldConf).
+ update_config(
+ maps:get(enable, Conf),
+ maps:get(enable, OldConf),
+ State,
+ Conf,
+ OldConf
+ ).
-spec update_config(boolean(), boolean(), state(), hocons:config(), hocons:config()) -> state().
update_config(false, _, State, _, _) ->
disable_retainer(State);
-
update_config(true, false, State, NewConf, _) ->
enable_retainer(State, NewConf);
-
-update_config(true, true,
- #{clear_timer := ClearTimer} = State, NewConf, OldConf) ->
- #{backend := BackendCfg,
- msg_clear_interval := ClearInterval} = NewConf,
+update_config(
+ true,
+ true,
+ #{clear_timer := ClearTimer} = State,
+ NewConf,
+ OldConf
+) ->
+ #{
+ backend := BackendCfg,
+ msg_clear_interval := ClearInterval
+ } = NewConf,
#{backend := OldBackendCfg} = OldConf,
@@ -290,34 +304,49 @@ update_config(true, true,
OldStrorageType = maps:get(type, OldBackendCfg),
case OldStrorageType of
StorageType ->
- State#{clear_timer := check_timer(ClearTimer,
- ClearInterval,
- clear_expired)};
+ State#{
+ clear_timer := check_timer(
+ ClearTimer,
+ ClearInterval,
+ clear_expired
+ )
+ };
_ ->
State2 = disable_retainer(State),
enable_retainer(State2, NewConf)
end.
-spec enable_retainer(state(), hocon:config()) -> state().
-enable_retainer(#{context_id := ContextId} = State,
- #{msg_clear_interval := ClearInterval,
- backend := BackendCfg}) ->
+enable_retainer(
+ #{context_id := ContextId} = State,
+ #{
+ msg_clear_interval := ClearInterval,
+ backend := BackendCfg
+ }
+) ->
NewContextId = ContextId + 1,
Context = create_resource(new_context(NewContextId), BackendCfg),
load(Context),
- State#{enable := true,
- context_id := NewContextId,
- context := Context,
- clear_timer := add_timer(ClearInterval, clear_expired)}.
+ State#{
+ enable := true,
+ context_id := NewContextId,
+ context := Context,
+ clear_timer := add_timer(ClearInterval, clear_expired)
+ }.
-spec disable_retainer(state()) -> state().
-disable_retainer(#{clear_timer := ClearTimer,
- context := Context} = State) ->
+disable_retainer(
+ #{
+ clear_timer := ClearTimer,
+ context := Context
+ } = State
+) ->
unload(),
ok = close_resource(Context),
- State#{enable := false,
- clear_timer := stop_timer(ClearTimer)
- }.
+ State#{
+ enable := false,
+ clear_timer := stop_timer(ClearTimer)
+ }.
-spec stop_timer(undefined | reference()) -> undefined.
stop_timer(undefined) ->
@@ -344,24 +373,27 @@ check_timer(Timer, _, _) ->
-spec get_backend_module() -> backend().
get_backend_module() ->
- ModName = case emqx:get_config([retainer, backend]) of
- #{type := built_in_database} -> mnesia;
- #{type := Backend} -> Backend
- end,
+ ModName =
+ case emqx:get_config([retainer, backend]) of
+ #{type := built_in_database} -> mnesia;
+ #{type := Backend} -> Backend
+ end,
erlang:list_to_existing_atom(io_lib:format("~ts_~ts", [?APP, ModName])).
create_resource(Context, #{type := built_in_database} = Cfg) ->
emqx_retainer_mnesia:create_resource(Cfg),
Context;
-
create_resource(Context, #{type := DB} = Config) ->
ResourceID = erlang:iolist_to_binary([io_lib:format("~ts_~ts", [?APP, DB])]),
- case emqx_resource:create(
- ResourceID,
- <<"emqx_retainer">>,
- list_to_existing_atom(io_lib:format("~ts_~ts", [emqx_connector, DB])),
- Config,
- #{}) of
+ case
+ emqx_resource:create(
+ ResourceID,
+ <<"emqx_retainer">>,
+ list_to_existing_atom(io_lib:format("~ts_~ts", [emqx_connector, DB])),
+ Config,
+ #{}
+ )
+ of
{ok, already_created} ->
Context#{resource_id => ResourceID};
{ok, _} ->
diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl
index db7c25cbb..d85db4ae6 100644
--- a/apps/emqx_retainer/src/emqx_retainer_api.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_api.erl
@@ -24,14 +24,17 @@
%% API
-export([api_spec/0, paths/0, schema/1, namespace/0, fields/1]).
--export([ lookup_retained_warp/2
- , with_topic_warp/2
- , config/2]).
+-export([
+ lookup_retained_warp/2,
+ with_topic_warp/2,
+ config/2
+]).
-import(hoconsc, [mk/2, ref/1, ref/2, array/1]).
-import(emqx_dashboard_swagger, [error_codes/2]).
--define(MAX_PAYLOAD_SIZE, 1048576). %% 1MB = 1024 x 1024
+%% 1MB = 1024 x 1024
+-define(MAX_PAYLOAD_SIZE, 1048576).
-define(PREFIX, "/mqtt/retainer").
-define(TAGS, [<<"retainer">>]).
@@ -44,52 +47,65 @@ paths() ->
[?PREFIX, ?PREFIX ++ "/messages", ?PREFIX ++ "/message/:topic"].
schema(?PREFIX) ->
- #{'operationId' => config,
- get => #{tags => ?TAGS,
- description => <<"Get retainer config">>,
- responses => #{200 => mk(conf_schema(), #{desc => "The config content"}),
- 404 => error_codes(['NOT_FOUND'], <<"Config not found">>)
- }
- },
- put => #{tags => ?TAGS,
- description => <<"Update retainer config">>,
- 'requestBody' => mk(conf_schema(), #{desc => "The config content"}),
- responses => #{200 => mk(conf_schema(), #{desc => "Update configs successfully"}),
- 400 => error_codes(['UPDATE_FAILED'], <<"Update config failed">>)
- }
- }
- };
-
+ #{
+ 'operationId' => config,
+ get => #{
+ tags => ?TAGS,
+ description => <<"Get retainer config">>,
+ responses => #{
+ 200 => mk(conf_schema(), #{desc => "The config content"}),
+ 404 => error_codes(['NOT_FOUND'], <<"Config not found">>)
+ }
+ },
+ put => #{
+ tags => ?TAGS,
+ description => <<"Update retainer config">>,
+ 'requestBody' => mk(conf_schema(), #{desc => "The config content"}),
+ responses => #{
+ 200 => mk(conf_schema(), #{desc => "Update configs successfully"}),
+ 400 => error_codes(['UPDATE_FAILED'], <<"Update config failed">>)
+ }
+ }
+ };
schema(?PREFIX ++ "/messages") ->
- #{'operationId' => lookup_retained_warp,
- get => #{tags => ?TAGS,
- description => <<"List retained messages">>,
- parameters => page_params(),
- responses => #{200 => mk(array(ref(message_summary)), #{desc => "The result list"}),
- 400 => error_codes(['BAD_REQUEST'], <<"Unsupported backend">>)
- }
- }
- };
-
+ #{
+ 'operationId' => lookup_retained_warp,
+ get => #{
+ tags => ?TAGS,
+ description => <<"List retained messages">>,
+ parameters => page_params(),
+ responses => #{
+ 200 => mk(array(ref(message_summary)), #{desc => "The result list"}),
+ 400 => error_codes(['BAD_REQUEST'], <<"Unsupported backend">>)
+ }
+ }
+ };
schema(?PREFIX ++ "/message/:topic") ->
- #{'operationId' => with_topic_warp,
- get => #{tags => ?TAGS,
- description => <<"Lookup a message by a topic without wildcards">>,
- parameters => parameters(),
- responses => #{200 => mk(ref(message), #{desc => "Details of the message"}),
- 404 => error_codes(['NOT_FOUND'], <<"Viewed message doesn't exist">>),
- 400 => error_codes(['BAD_REQUEST'], <<"Unsupported backend">>)
- }
- },
- delete => #{tags => ?TAGS,
- description => <<"Delete matching messages">>,
- parameters => parameters(),
- responses => #{204 => <<>>,
- 400 => error_codes(['BAD_REQUEST'],
- <<"Unsupported backend">>)
- }
- }
- }.
+ #{
+ 'operationId' => with_topic_warp,
+ get => #{
+ tags => ?TAGS,
+ description => <<"Lookup a message by a topic without wildcards">>,
+ parameters => parameters(),
+ responses => #{
+ 200 => mk(ref(message), #{desc => "Details of the message"}),
+ 404 => error_codes(['NOT_FOUND'], <<"Viewed message doesn't exist">>),
+ 400 => error_codes(['BAD_REQUEST'], <<"Unsupported backend">>)
+ }
+ },
+ delete => #{
+ tags => ?TAGS,
+ description => <<"Delete matching messages">>,
+ parameters => parameters(),
+ responses => #{
+ 204 => <<>>,
+ 400 => error_codes(
+ ['BAD_REQUEST'],
+ <<"Unsupported backend">>
+ )
+ }
+ }
+ }.
page_params() ->
emqx_dashboard_swagger:fields(page) ++ emqx_dashboard_swagger:fields(limit).
@@ -98,23 +114,29 @@ conf_schema() ->
ref(emqx_retainer_schema, "retainer").
parameters() ->
- [{topic, mk(binary(), #{in => path,
- required => true,
- desc => "The target topic"
- })}].
+ [
+ {topic,
+ mk(binary(), #{
+ in => path,
+ required => true,
+ desc => "The target topic"
+ })}
+ ].
fields(message_summary) ->
- [ {msgid, mk(binary(), #{desc => <<"Message ID">>})}
- , {topic, mk(binary(), #{desc => "The topic"})}
- , {qos, mk(emqx_schema:qos(), #{desc => "The QoS"})}
- , {publish_at, mk(string(), #{desc => "Publish datetime, in RFC 3339 format"})}
- , {from_clientid, mk(binary(), #{desc => "Publisher ClientId"})}
- , {from_username, mk(binary(), #{desc => "Publisher Username"})}
+ [
+ {msgid, mk(binary(), #{desc => <<"Message ID">>})},
+ {topic, mk(binary(), #{desc => "The topic"})},
+ {qos, mk(emqx_schema:qos(), #{desc => "The QoS"})},
+ {publish_at, mk(string(), #{desc => "Publish datetime, in RFC 3339 format"})},
+ {from_clientid, mk(binary(), #{desc => "Publisher ClientId"})},
+ {from_username, mk(binary(), #{desc => "Publisher Username"})}
];
-
fields(message) ->
- [{payload, mk(binary(), #{desc => "The payload content"})} |
- fields(message_summary)].
+ [
+ {payload, mk(binary(), #{desc => "The payload content"})}
+ | fields(message_summary)
+ ].
lookup_retained_warp(Type, Params) ->
check_backend(Type, Params, fun lookup_retained/2).
@@ -124,15 +146,16 @@ with_topic_warp(Type, Params) ->
config(get, _) ->
{200, emqx:get_raw_config([retainer])};
-
config(put, #{body := Body}) ->
try
{ok, _} = emqx_retainer:update_config(Body),
{200, emqx:get_raw_config([retainer])}
- catch _:Reason:_ ->
- {400,
- #{code => <<"UPDATE_FAILED">>,
- message => iolist_to_binary(io_lib:format("~p~n", [Reason]))}}
+ catch
+ _:Reason:_ ->
+ {400, #{
+ code => <<"UPDATE_FAILED">>,
+ message => iolist_to_binary(io_lib:format("~p~n", [Reason]))
+ }}
end.
%%------------------------------------------------------------------------------
@@ -151,26 +174,36 @@ with_topic(get, #{bindings := Bindings}) ->
[H | _] ->
{200, format_detail_message(H)};
_ ->
- {404, #{code => <<"NOT_FOUND">>,
- message => <<"Viewed message doesn't exist">>
- }}
+ {404, #{
+ code => <<"NOT_FOUND">>,
+ message => <<"Viewed message doesn't exist">>
+ }}
end;
-
with_topic(delete, #{bindings := Bindings}) ->
Topic = maps:get(topic, Bindings),
emqx_retainer_mnesia:delete_message(undefined, Topic),
{204}.
-format_message(#message{ id = ID, qos = Qos, topic = Topic, from = From
- , timestamp = Timestamp, headers = Headers}) ->
- #{msgid => emqx_guid:to_hexstr(ID),
- qos => Qos,
- topic => Topic,
- publish_at => list_to_binary(calendar:system_time_to_rfc3339(
- Timestamp, [{unit, millisecond}])),
- from_clientid => to_bin_string(From),
- from_username => maps:get(username, Headers, <<>>)
- }.
+format_message(#message{
+ id = ID,
+ qos = Qos,
+ topic = Topic,
+ from = From,
+ timestamp = Timestamp,
+ headers = Headers
+}) ->
+ #{
+ msgid => emqx_guid:to_hexstr(ID),
+ qos => Qos,
+ topic => Topic,
+ publish_at => list_to_binary(
+ calendar:system_time_to_rfc3339(
+ Timestamp, [{unit, millisecond}]
+ )
+ ),
+ from_clientid => to_bin_string(From),
+ from_username => maps:get(username, Headers, <<>>)
+ }.
format_detail_message(#message{payload = Payload} = Msg) ->
Base = format_message(Msg),
@@ -183,7 +216,7 @@ format_detail_message(#message{payload = Payload} = Msg) ->
to_bin_string(Data) when is_binary(Data) ->
Data;
-to_bin_string(Data) ->
+to_bin_string(Data) ->
list_to_binary(io_lib:format("~p", [Data])).
check_backend(Type, Params, Cont) ->
diff --git a/apps/emqx_retainer/src/emqx_retainer_app.erl b/apps/emqx_retainer/src/emqx_retainer_app.erl
index a63739cd9..2a7620664 100644
--- a/apps/emqx_retainer/src/emqx_retainer_app.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_app.erl
@@ -18,13 +18,13 @@
-behaviour(application).
--export([ start/2
- , stop/1
- ]).
+-export([
+ start/2,
+ stop/1
+]).
start(_Type, _Args) ->
emqx_retainer_sup:start_link().
-
stop(_State) ->
ok.
diff --git a/apps/emqx_retainer/src/emqx_retainer_dispatcher.erl b/apps/emqx_retainer/src/emqx_retainer_dispatcher.erl
index 6eb5457b7..1bdf11432 100644
--- a/apps/emqx_retainer/src/emqx_retainer_dispatcher.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_dispatcher.erl
@@ -22,15 +22,23 @@
-include_lib("emqx/include/logger.hrl").
%% API
--export([ start_link/2
- , dispatch/2
- , refresh_limiter/0
- , worker/0
- ]).
+-export([
+ start_link/2,
+ dispatch/2,
+ refresh_limiter/0,
+ worker/0
+]).
%% gen_server callbacks
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3, format_status/2]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3,
+ format_status/2
+]).
-type limiter() :: emqx_htb_limiter:limiter().
@@ -46,10 +54,12 @@ dispatch(Context, Topic) ->
%% an limiter update handler maybe added later, now this is a workaround
refresh_limiter() ->
Workers = gproc_pool:active_workers(?POOL),
- lists:foreach(fun({_, Pid}) ->
- gen_server:cast(Pid, ?FUNCTION_NAME)
- end,
- Workers).
+ lists:foreach(
+ fun({_, Pid}) ->
+ gen_server:cast(Pid, ?FUNCTION_NAME)
+ end,
+ Workers
+ ).
worker() ->
gproc_pool:pick_worker(?POOL, self()).
@@ -59,13 +69,18 @@ worker() ->
%% Starts the server
%% @end
%%--------------------------------------------------------------------
--spec start_link(atom(), pos_integer()) -> {ok, Pid :: pid()} |
- {error, Error :: {already_started, pid()}} |
- {error, Error :: term()} |
- ignore.
+-spec start_link(atom(), pos_integer()) ->
+ {ok, Pid :: pid()}
+ | {error, Error :: {already_started, pid()}}
+ | {error, Error :: term()}
+ | ignore.
start_link(Pool, Id) ->
- gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)},
- ?MODULE, [Pool, Id], [{hibernate_after, 1000}]).
+ gen_server:start_link(
+ {local, emqx_misc:proc_name(?MODULE, Id)},
+ ?MODULE,
+ [Pool, Id],
+ [{hibernate_after, 1000}]
+ ).
%%%===================================================================
%%% gen_server callbacks
@@ -77,11 +92,12 @@ start_link(Pool, Id) ->
%% Initializes the server
%% @end
%%--------------------------------------------------------------------
--spec init(Args :: term()) -> {ok, State :: term()} |
- {ok, State :: term(), Timeout :: timeout()} |
- {ok, State :: term(), hibernate} |
- {stop, Reason :: term()} |
- ignore.
+-spec init(Args :: term()) ->
+ {ok, State :: term()}
+ | {ok, State :: term(), Timeout :: timeout()}
+ | {ok, State :: term(), hibernate}
+ | {stop, Reason :: term()}
+ | ignore.
init([Pool, Id]) ->
erlang:process_flag(trap_exit, true),
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
@@ -96,14 +112,14 @@ init([Pool, Id]) ->
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
- {reply, Reply :: term(), NewState :: term()} |
- {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()} |
- {reply, Reply :: term(), NewState :: term(), hibernate} |
- {noreply, NewState :: term()} |
- {noreply, NewState :: term(), Timeout :: timeout()} |
- {noreply, NewState :: term(), hibernate} |
- {stop, Reason :: term(), Reply :: term(), NewState :: term()} |
- {stop, Reason :: term(), NewState :: term()}.
+ {reply, Reply :: term(), NewState :: term()}
+ | {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()}
+ | {reply, Reply :: term(), NewState :: term(), hibernate}
+ | {noreply, NewState :: term()}
+ | {noreply, NewState :: term(), Timeout :: timeout()}
+ | {noreply, NewState :: term(), hibernate}
+ | {stop, Reason :: term(), Reply :: term(), NewState :: term()}
+ | {stop, Reason :: term(), NewState :: term()}.
handle_call(Req, _From, State) ->
?SLOG(error, #{msg => "unexpected_call", call => Req}),
{reply, ignored, State}.
@@ -115,19 +131,17 @@ handle_call(Req, _From, State) ->
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: term()) ->
- {noreply, NewState :: term()} |
- {noreply, NewState :: term(), Timeout :: timeout()} |
- {noreply, NewState :: term(), hibernate} |
- {stop, Reason :: term(), NewState :: term()}.
+ {noreply, NewState :: term()}
+ | {noreply, NewState :: term(), Timeout :: timeout()}
+ | {noreply, NewState :: term(), hibernate}
+ | {stop, Reason :: term(), NewState :: term()}.
handle_cast({dispatch, Context, Pid, Topic}, #{limiter := Limiter} = State) ->
{ok, Limiter2} = dispatch(Context, Pid, Topic, undefined, Limiter),
{noreply, State#{limiter := Limiter2}};
-
handle_cast(refresh_limiter, State) ->
BucketName = emqx:get_config([retainer, flow_control, batch_deliver_limiter]),
Limiter = emqx_limiter_server:connect(batch, BucketName),
{noreply, State#{limiter := Limiter}};
-
handle_cast(Msg, State) ->
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
{noreply, State}.
@@ -139,10 +153,10 @@ handle_cast(Msg, State) ->
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
- {noreply, NewState :: term()} |
- {noreply, NewState :: term(), Timeout :: timeout()} |
- {noreply, NewState :: term(), hibernate} |
- {stop, Reason :: normal | term(), NewState :: term()}.
+ {noreply, NewState :: term()}
+ | {noreply, NewState :: term(), Timeout :: timeout()}
+ | {noreply, NewState :: term(), hibernate}
+ | {stop, Reason :: normal | term(), NewState :: term()}.
handle_info(Info, State) ->
?SLOG(error, #{msg => "unexpected_info", info => Info}),
{noreply, State}.
@@ -156,8 +170,10 @@ handle_info(Info, State) ->
%% with Reason. The return value is ignored.
%% @end
%%--------------------------------------------------------------------
--spec terminate(Reason :: normal | shutdown | {shutdown, term()} | term(),
- State :: term()) -> any().
+-spec terminate(
+ Reason :: normal | shutdown | {shutdown, term()} | term(),
+ State :: term()
+) -> any().
terminate(_Reason, #{pool := Pool, id := Id}) ->
gproc_pool:disconnect_worker(Pool, {Pool, Id}).
%%--------------------------------------------------------------------
@@ -166,10 +182,13 @@ terminate(_Reason, #{pool := Pool, id := Id}) ->
%% Convert process state when code is changed
%% @end
%%--------------------------------------------------------------------
--spec code_change(OldVsn :: term() | {down, term()},
- State :: term(),
- Extra :: term()) -> {ok, NewState :: term()} |
- {error, Reason :: term()}.
+-spec code_change(
+ OldVsn :: term() | {down, term()},
+ State :: term(),
+ Extra :: term()
+) ->
+ {ok, NewState :: term()}
+ | {error, Reason :: term()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
@@ -181,8 +200,10 @@ code_change(_OldVsn, State, _Extra) ->
%% or when it appears in termination error logs.
%% @end
%%--------------------------------------------------------------------
--spec format_status(Opt :: normal | terminate,
- Status :: list()) -> Status :: term().
+-spec format_status(
+ Opt :: normal | terminate,
+ Status :: list()
+) -> Status :: term().
format_status(_Opt, Status) ->
Status.
@@ -200,19 +221,17 @@ dispatch(Context, Pid, Topic, Cursor, Limiter) ->
false ->
{ok, Result} = erlang:apply(Mod, read_message, [Context, Topic]),
deliver(Result, Context, Pid, Topic, undefined, Limiter);
- true ->
+ true ->
{ok, Result, NewCursor} = erlang:apply(Mod, match_messages, [Context, Topic, Cursor]),
deliver(Result, Context, Pid, Topic, NewCursor, Limiter)
end.
-spec deliver(list(emqx_types:message()), context(), pid(), topic(), cursor(), limiter()) ->
- {ok, limiter()}.
+ {ok, limiter()}.
deliver([], _Context, _Pid, _Topic, undefined, Limiter) ->
{ok, Limiter};
-
deliver([], Context, Pid, Topic, Cursor, Limiter) ->
dispatch(Context, Pid, Topic, Cursor, Limiter);
-
deliver(Result, Context, Pid, Topic, Cursor, Limiter) ->
case erlang:is_process_alive(Pid) of
false ->
@@ -235,7 +254,6 @@ deliver(Result, Context, Pid, Topic, Cursor, Limiter) ->
do_deliver([], _DeliverNum, _Pid, _Topic, Limiter) ->
{ok, Limiter};
-
do_deliver(Msgs, DeliverNum, Pid, Topic, Limiter) ->
{Num, ToDelivers, Msgs2} = safe_split(DeliverNum, Msgs),
case emqx_htb_limiter:consume(Num, Limiter) of
@@ -243,17 +261,17 @@ do_deliver(Msgs, DeliverNum, Pid, Topic, Limiter) ->
do_deliver(ToDelivers, Pid, Topic),
do_deliver(Msgs2, DeliverNum, Pid, Topic, Limiter2);
{drop, _} = Drop ->
- ?SLOG(error, #{msg => "retained_message_dropped",
- reason => "reached_ratelimit",
- dropped_count => length(ToDelivers)
- }),
+ ?SLOG(error, #{
+ msg => "retained_message_dropped",
+ reason => "reached_ratelimit",
+ dropped_count => length(ToDelivers)
+ }),
Drop
end.
do_deliver([Msg | T], Pid, Topic) ->
Pid ! {deliver, Topic, Msg},
do_deliver(T, Pid, Topic);
-
do_deliver([], _, _) ->
ok.
@@ -262,9 +280,7 @@ safe_split(N, List) ->
safe_split(0, List, Count, Acc) ->
{Count, lists:reverse(Acc), List};
-
safe_split(_N, [], Count, Acc) ->
{Count, lists:reverse(Acc), []};
-
safe_split(N, [H | T], Count, Acc) ->
safe_split(N - 1, T, Count + 1, [H | Acc]).
diff --git a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl
index 93f5251a6..fd76442b9 100644
--- a/apps/emqx_retainer/src/emqx_retainer_mnesia.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_mnesia.erl
@@ -23,83 +23,99 @@
-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("stdlib/include/qlc.hrl").
-
--export([ delete_message/2
- , store_retained/2
- , read_message/2
- , page_read/4
- , match_messages/3
- , clear_expired/1
- , clean/1
- , size/1
- ]).
+-export([
+ delete_message/2,
+ store_retained/2,
+ read_message/2,
+ page_read/4,
+ match_messages/3,
+ clear_expired/1,
+ clean/1,
+ size/1
+]).
-export([create_resource/1]).
-record(retained, {topic, msg, expiry_time}).
-type batch_read_result() ::
- {ok, list(emqx:message()), cursor()}.
+ {ok, list(emqx:message()), cursor()}.
%%--------------------------------------------------------------------
%% emqx_retainer_storage callbacks
%%--------------------------------------------------------------------
create_resource(#{storage_type := StorageType}) ->
- Copies = case StorageType of
- ram -> ram_copies;
- disc -> disc_copies
- end,
+ Copies =
+ case StorageType of
+ ram -> ram_copies;
+ disc -> disc_copies
+ end,
- StoreProps = [{ets, [compressed,
- {read_concurrency, true},
- {write_concurrency, true}]},
- {dets, [{auto_save, 1000}]}],
+ StoreProps = [
+ {ets, [
+ compressed,
+ {read_concurrency, true},
+ {write_concurrency, true}
+ ]},
+ {dets, [{auto_save, 1000}]}
+ ],
ok = mria:create_table(?TAB, [
- {type, ordered_set},
- {rlog_shard, ?RETAINER_SHARD},
- {storage, Copies},
- {record_name, retained},
- {attributes, record_info(fields, retained)},
- {storage_properties, StoreProps}
- ]),
+ {type, ordered_set},
+ {rlog_shard, ?RETAINER_SHARD},
+ {storage, Copies},
+ {record_name, retained},
+ {attributes, record_info(fields, retained)},
+ {storage_properties, StoreProps}
+ ]),
ok = mria_rlog:wait_for_shards([?RETAINER_SHARD], infinity),
case mnesia:table_info(?TAB, storage_type) of
- Copies -> ok;
+ Copies ->
+ ok;
_Other ->
{atomic, ok} = mnesia:change_table_copy_type(?TAB, node(), Copies),
ok
end.
-store_retained(_, Msg =#message{topic = Topic}) ->
+store_retained(_, Msg = #message{topic = Topic}) ->
ExpiryTime = emqx_retainer:get_expiry_time(Msg),
case is_table_full() of
false ->
- mria:dirty_write(?TAB,
- #retained{topic = topic2tokens(Topic),
- msg = Msg,
- expiry_time = ExpiryTime});
+ mria:dirty_write(
+ ?TAB,
+ #retained{
+ topic = topic2tokens(Topic),
+ msg = Msg,
+ expiry_time = ExpiryTime
+ }
+ );
_ ->
Tokens = topic2tokens(Topic),
Fun = fun() ->
- case mnesia:read(?TAB, Tokens) of
- [_] ->
- mnesia:write(?TAB,
- #retained{topic = Tokens,
- msg = Msg,
- expiry_time = ExpiryTime},
- write);
- [] ->
- mnesia:abort(table_is_full)
- end
+ case mnesia:read(?TAB, Tokens) of
+ [_] ->
+ mnesia:write(
+ ?TAB,
+ #retained{
+ topic = Tokens,
+ msg = Msg,
+ expiry_time = ExpiryTime
+ },
+ write
+ );
+ [] ->
+ mnesia:abort(table_is_full)
+ end
end,
case mria:transaction(?RETAINER_SHARD, Fun) of
- {atomic, ok} -> ok;
+ {atomic, ok} ->
+ ok;
{aborted, Reason} ->
- ?SLOG(error, #{ msg => "failed_to_retain_message"
- , topic => Topic
- , reason => Reason
- })
+ ?SLOG(error, #{
+ msg => "failed_to_retain_message",
+ topic => Topic,
+ reason => Reason
+ })
end
end.
@@ -108,20 +124,21 @@ clear_expired(_) ->
MsHd = #retained{topic = '$1', msg = '_', expiry_time = '$3'},
Ms = [{MsHd, [{'=/=', '$3', 0}, {'<', '$3', NowMs}], ['$1']}],
Fun = fun() ->
- Keys = mnesia:select(?TAB, Ms, write),
- lists:foreach(fun(Key) -> mnesia:delete({?TAB, Key}) end, Keys)
- end,
+ Keys = mnesia:select(?TAB, Ms, write),
+ lists:foreach(fun(Key) -> mnesia:delete({?TAB, Key}) end, Keys)
+ end,
{atomic, _} = mria:transaction(?RETAINER_SHARD, Fun),
ok.
delete_message(_, Topic) ->
case emqx_topic:wildcard(Topic) of
- true -> match_delete_messages(Topic);
+ true ->
+ match_delete_messages(Topic);
false ->
Tokens = topic2tokens(Topic),
Fun = fun() ->
- mnesia:delete({?TAB, Tokens})
- end,
+ mnesia:delete({?TAB, Tokens})
+ end,
_ = mria:transaction(?RETAINER_SHARD, Fun),
ok
end,
@@ -169,8 +186,7 @@ size(_) ->
%%--------------------------------------------------------------------
sort_retained([]) -> [];
sort_retained([Msg]) -> [Msg];
-sort_retained(Msgs) ->
- lists:sort(fun compare_message/2, Msgs).
+sort_retained(Msgs) -> lists:sort(fun compare_message/2, Msgs).
compare_message(M1, M2) ->
M1#message.timestamp =< M2#message.timestamp.
@@ -194,12 +210,13 @@ batch_read_messages(Cursor, MaxReadNum) ->
{ok, Answers, Cursor}
end.
--spec(read_messages(emqx_types:topic())
- -> [emqx_types:message()]).
+-spec read_messages(emqx_types:topic()) ->
+ [emqx_types:message()].
read_messages(Topic) ->
Tokens = topic2tokens(Topic),
case mnesia:dirty_read(?TAB, Tokens) of
- [] -> [];
+ [] ->
+ [];
[#retained{msg = Msg, expiry_time = Et}] ->
case Et =:= 0 orelse Et >= erlang:system_time(millisecond) of
true -> [Msg];
@@ -207,13 +224,13 @@ read_messages(Topic) ->
end
end.
--spec(match_messages(emqx_types:topic())
- -> [emqx_types:message()]).
+-spec match_messages(emqx_types:topic()) ->
+ [emqx_types:message()].
match_messages(Filter) ->
Ms = make_match_spec(Filter),
mnesia:dirty_select(?TAB, Ms).
--spec(match_delete_messages(emqx_types:topic()) -> ok).
+-spec match_delete_messages(emqx_types:topic()) -> ok.
match_delete_messages(Filter) ->
Cond = condition(emqx_topic:words(Filter)),
MsHd = #retained{topic = Cond, msg = '_', expiry_time = '_'},
@@ -223,7 +240,13 @@ match_delete_messages(Filter) ->
%% @private
condition(Ws) ->
- Ws1 = [case W =:= '+' of true -> '_'; _ -> W end || W <- Ws],
+ Ws1 = [
+ case W =:= '+' of
+ true -> '_';
+ _ -> W
+ end
+ || W <- Ws
+ ],
case lists:last(Ws1) =:= '#' of
false -> Ws1;
_ -> (Ws1 -- ['#']) ++ '_'
@@ -240,8 +263,10 @@ make_match_spec(Topic) ->
condition(emqx_topic:words(Topic))
end,
MsHd = #retained{topic = Cond, msg = '$2', expiry_time = '$3'},
- [{MsHd, [{'=:=', '$3', 0}], ['$2']},
- {MsHd, [{'>', '$3', NowMs}], ['$2']}].
+ [
+ {MsHd, [{'=:=', '$3', 0}], ['$2']},
+ {MsHd, [{'>', '$3', NowMs}], ['$2']}
+ ].
-spec make_cursor(undefined | topic()) -> qlc:query_cursor().
make_cursor(Topic) ->
diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl
index 74732bf9b..4940a5c41 100644
--- a/apps/emqx_retainer/src/emqx_retainer_schema.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl
@@ -11,56 +11,84 @@ namespace() -> "retainer".
roots() -> ["retainer"].
fields("retainer") ->
- [ {enable, sc(boolean(), "Enable retainer feature.", false)}
- , {msg_expiry_interval, sc(emqx_schema:duration_ms(),
- "Message retention time. 0 means message will never be expired.",
- "0s")}
- , {msg_clear_interval, sc(emqx_schema:duration_ms(),
- "Periodic interval for cleaning up expired messages. "
- "Never clear if the value is 0.",
- "0s")}
- , {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))}
- , {max_payload_size, sc(emqx_schema:bytesize(),
- "Maximum retained message size.",
- "1MB")}
- , {stop_publish_clear_msg, sc(boolean(),
- "When the retained flag of the `PUBLISH` message is set and Payload is empty, "
- "whether to continue to publish the message.
"
- "See: "
- "http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718038",
- false)}
- , {backend, backend_config()}
+ [
+ {enable, sc(boolean(), "Enable retainer feature.", false)},
+ {msg_expiry_interval,
+ sc(
+ emqx_schema:duration_ms(),
+ "Message retention time. 0 means message will never be expired.",
+ "0s"
+ )},
+ {msg_clear_interval,
+ sc(
+ emqx_schema:duration_ms(),
+ "Periodic interval for cleaning up expired messages. "
+ "Never clear if the value is 0.",
+ "0s"
+ )},
+ {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))},
+ {max_payload_size,
+ sc(
+ emqx_schema:bytesize(),
+ "Maximum retained message size.",
+ "1MB"
+ )},
+ {stop_publish_clear_msg,
+ sc(
+ boolean(),
+ "When the retained flag of the `PUBLISH` message is set and Payload is empty, "
+ "whether to continue to publish the message.
"
+ "See: "
+ "http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718038",
+ false
+ )},
+ {backend, backend_config()}
];
-
fields(mnesia_config) ->
- [ {type, hoconsc:mk(hoconsc:union([built_in_database]), #{desc => "Backend type."})}
- , {storage_type, sc(hoconsc:union([ram, disc]),
- "Specifies whether the messages are stored in RAM or persisted on disc.",
- ram)}
- , {max_retained_messages, sc(integer(),
- "Maximum number of retained messages. 0 means no limit.",
- 0,
- fun is_pos_integer/1)}
+ [
+ {type, hoconsc:mk(hoconsc:union([built_in_database]), #{desc => "Backend type."})},
+ {storage_type,
+ sc(
+ hoconsc:union([ram, disc]),
+ "Specifies whether the messages are stored in RAM or persisted on disc.",
+ ram
+ )},
+ {max_retained_messages,
+ sc(
+ integer(),
+ "Maximum number of retained messages. 0 means no limit.",
+ 0,
+ fun is_pos_integer/1
+ )}
];
-
fields(flow_control) ->
- [ {batch_read_number, sc(integer(),
- "Size of the batch when reading messages from storage. 0 means no limit.",
- 0,
- fun is_pos_integer/1)}
- , {batch_deliver_number, sc(range(0, 1000),
- "The number of retained messages can be delivered per batch.",
- 0)}
- , {batch_deliver_limiter, sc(emqx_limiter_schema:bucket_name(),
- "The rate limiter name for retained messages' delivery.
"
- "Limiter helps to avoid delivering too many messages to the client at once, "
- "which may cause the client "
- "to block or crash, or drop messages due to exceeding the size of the message"
- " queue.
"
- "The names of the available rate limiters are taken from the existing rate "
- "limiters under `limiter.batch`.
"
- "If this field is empty, limiter is not used.",
- undefined)}
+ [
+ {batch_read_number,
+ sc(
+ integer(),
+ "Size of the batch when reading messages from storage. 0 means no limit.",
+ 0,
+ fun is_pos_integer/1
+ )},
+ {batch_deliver_number,
+ sc(
+ range(0, 1000),
+ "The number of retained messages can be delivered per batch.",
+ 0
+ )},
+ {batch_deliver_limiter,
+ sc(
+ emqx_limiter_schema:bucket_name(),
+ "The rate limiter name for retained messages' delivery.
"
+ "Limiter helps to avoid delivering too many messages to the client at once, "
+ "which may cause the client "
+ "to block or crash, or drop messages due to exceeding the size of the message"
+ " queue.
"
+ "The names of the available rate limiters are taken from the existing rate "
+ "limiters under `limiter.batch`.
"
+ "If this field is empty, limiter is not used.",
+ undefined
+ )}
].
desc("retainer") ->
@@ -79,15 +107,17 @@ sc(Type, Desc, Default) ->
hoconsc:mk(Type, #{default => Default, desc => Desc}).
sc(Type, Desc, Default, Validator) ->
- hoconsc:mk(Type, #{default => Default,
- desc => Desc,
- validator => Validator}).
+ hoconsc:mk(Type, #{
+ default => Default,
+ desc => Desc,
+ validator => Validator
+ }).
is_pos_integer(V) ->
V >= 0.
backend_config() ->
#{
- type => hoconsc:union([hoconsc:ref(?MODULE, mnesia_config)]),
- desc => "Settings for the database storing the retained messages."
- }.
+ type => hoconsc:union([hoconsc:ref(?MODULE, mnesia_config)]),
+ desc => "Settings for the database storing the retained messages."
+ }.
diff --git a/apps/emqx_retainer/src/emqx_retainer_sup.erl b/apps/emqx_retainer/src/emqx_retainer_sup.erl
index 5073bb987..9938f9881 100644
--- a/apps/emqx_retainer/src/emqx_retainer_sup.erl
+++ b/apps/emqx_retainer/src/emqx_retainer_sup.erl
@@ -26,13 +26,21 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
- PoolSpec = emqx_pool_sup:spec([emqx_retainer_dispatcher, hash, emqx_vm:schedulers(),
- {emqx_retainer_dispatcher, start_link, []}]),
- {ok, {{one_for_one, 10, 3600},
- [#{id => retainer,
- start => {emqx_retainer, start_link, []},
- restart => permanent,
- shutdown => 5000,
- type => worker,
- modules => [emqx_retainer]},
- PoolSpec]}}.
+ PoolSpec = emqx_pool_sup:spec([
+ emqx_retainer_dispatcher,
+ hash,
+ emqx_vm:schedulers(),
+ {emqx_retainer_dispatcher, start_link, []}
+ ]),
+ {ok,
+ {{one_for_one, 10, 3600}, [
+ #{
+ id => retainer,
+ start => {emqx_retainer, start_link, []},
+ restart => permanent,
+ shutdown => 5000,
+ type => worker,
+ modules => [emqx_retainer]
+ },
+ PoolSpec
+ ]}}.
diff --git a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl
index 9e957f214..30201bdef 100644
--- a/apps/emqx_retainer/test/emqx_retainer_SUITE.erl
+++ b/apps/emqx_retainer/test/emqx_retainer_SUITE.erl
@@ -27,23 +27,27 @@
all() -> emqx_common_test_helpers:all(?MODULE).
--define(BASE_CONF, <<"""
-retainer {
- enable = true
- msg_clear_interval = 0s
- msg_expiry_interval = 0s
- max_payload_size = 1MB
- flow_control {
- batch_read_number = 0
- batch_deliver_number = 0
- batch_deliver_limiter = retainer
- }
- backend {
- type = built_in_database
- storage_type = ram
- max_retained_messages = 0
- }
-}""">>).
+-define(BASE_CONF, <<
+ ""
+ "\n"
+ "retainer {\n"
+ " enable = true\n"
+ " msg_clear_interval = 0s\n"
+ " msg_expiry_interval = 0s\n"
+ " max_payload_size = 1MB\n"
+ " flow_control {\n"
+ " batch_read_number = 0\n"
+ " batch_deliver_number = 0\n"
+ " batch_deliver_limiter = retainer\n"
+ " }\n"
+ " backend {\n"
+ " type = built_in_database\n"
+ " storage_type = ram\n"
+ " max_retained_messages = 0\n"
+ " }\n"
+ "}"
+ ""
+>>).
%%--------------------------------------------------------------------
%% Setups
@@ -82,9 +86,11 @@ t_store_and_clean(_) ->
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained">>,
- <<"this is a retained message">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained">>,
+ <<"this is a retained message">>,
+ [{qos, 0}, {retain, true}]
+ ),
timer:sleep(100),
{ok, List} = emqx_retainer:page_read(<<"retained">>, 1, 10),
@@ -121,9 +127,11 @@ t_retain_handling(_) ->
{ok, #{}, [0]} = emqtt:unsubscribe(C1, <<"retained/#">>),
emqtt:publish(
- C1, <<"retained">>,
- <<"this is a retained message">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained">>,
+ <<"this is a retained message">>,
+ [{qos, 0}, {retain, true}]
+ ),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained">>, [{qos, 0}, {rh, 0}]),
?assertEqual(1, length(receive_messages(1))),
@@ -154,20 +162,26 @@ t_wildcard_subscription(_) ->
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained/0">>,
- <<"this is a retained message 0">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/0">>,
+ <<"this is a retained message 0">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/1">>,
- <<"this is a retained message 1">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/1">>,
+ <<"this is a retained message 1">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/a/b/c">>,
- <<"this is a retained message 2">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/a/b/c">>,
+ <<"this is a retained message 2">>,
+ [{qos, 0}, {retain, true}]
+ ),
- {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+">>, 0),
- {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+/b/#">>, 0),
+ {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+">>, 0),
+ {ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+/b/#">>, 0),
?assertEqual(3, length(receive_messages(3))),
emqtt:publish(C1, <<"retained/0">>, <<"">>, [{qos, 0}, {retain, true}]),
@@ -180,25 +194,38 @@ t_message_expiry(_) ->
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained/0">>, #{'Message-Expiry-Interval' => 0},
- <<"don't expire">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/0">>,
+ #{'Message-Expiry-Interval' => 0},
+ <<"don't expire">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/1">>, #{'Message-Expiry-Interval' => 2},
- <<"expire">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/1">>,
+ #{'Message-Expiry-Interval' => 2},
+ <<"expire">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/2">>, #{'Message-Expiry-Interval' => 5},
- <<"don't expire">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/2">>,
+ #{'Message-Expiry-Interval' => 5},
+ <<"don't expire">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/3">>,
- <<"don't expire">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/3">>,
+ <<"don't expire">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"$SYS/retained/4">>,
- <<"don't expire">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"$SYS/retained/4">>,
+ <<"don't expire">>,
+ [{qos, 0}, {retain, true}]
+ ),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/+">>, 0),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"$SYS/retained/+">>, 0),
@@ -240,17 +267,23 @@ t_clean(_) ->
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained/0">>,
- <<"this is a retained message 0">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/0">>,
+ <<"this is a retained message 0">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/1">>,
- <<"this is a retained message 1">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/1">>,
+ <<"this is a retained message 1">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/test/0">>,
- <<"this is a retained message 2">>,
- [{qos, 0}, {retain, true}]),
+ C1,
+ <<"retained/test/0">>,
+ <<"this is a retained message 2">>,
+ [{qos, 0}, {retain, true}]
+ ),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/#">>, [{qos, 0}, {rh, 0}]),
?assertEqual(3, length(receive_messages(3))),
@@ -266,10 +299,11 @@ t_stop_publish_clear_msg(_) ->
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained/0">>,
- <<"this is a retained message 0">>,
- [{qos, 0}, {retain, true}]
- ),
+ C1,
+ <<"retained/0">>,
+ <<"this is a retained message 0">>,
+ [{qos, 0}, {retain, true}]
+ ),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/#">>, [{qos, 0}, {rh, 0}]),
?assertEqual(1, length(receive_messages(1))),
@@ -282,9 +316,13 @@ t_stop_publish_clear_msg(_) ->
t_flow_control(_) ->
#{per_client := PerClient} = RetainerCfg = emqx_config:get([limiter, batch, bucket, retainer]),
- RetainerCfg2 = RetainerCfg#{per_client :=
- PerClient#{rate := emqx_ratelimiter_SUITE:to_rate("1/1s"),
- capacity := 1}},
+ RetainerCfg2 = RetainerCfg#{
+ per_client :=
+ PerClient#{
+ rate := emqx_ratelimiter_SUITE:to_rate("1/1s"),
+ capacity := 1
+ }
+ },
emqx_config:put([limiter, batch, bucket, retainer], RetainerCfg2),
emqx_limiter_manager:restart_server(shared),
timer:sleep(500),
@@ -292,35 +330,44 @@ t_flow_control(_) ->
emqx_retainer_dispatcher:refresh_limiter(),
timer:sleep(500),
- emqx_retainer:update_config(#{<<"flow_control">> =>
- #{<<"batch_read_number">> => 1,
- <<"batch_deliver_number">> => 1,
- <<"batch_deliver_limiter">> => retainer}}),
+ emqx_retainer:update_config(#{
+ <<"flow_control">> =>
+ #{
+ <<"batch_read_number">> => 1,
+ <<"batch_deliver_number">> => 1,
+ <<"batch_deliver_limiter">> => retainer
+ }
+ }),
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
{ok, _} = emqtt:connect(C1),
emqtt:publish(
- C1, <<"retained/0">>,
- <<"this is a retained message 0">>,
- [{qos, 0}, {retain, true}]
- ),
+ C1,
+ <<"retained/0">>,
+ <<"this is a retained message 0">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/1">>,
- <<"this is a retained message 1">>,
- [{qos, 0}, {retain, true}]
- ),
+ C1,
+ <<"retained/1">>,
+ <<"this is a retained message 1">>,
+ [{qos, 0}, {retain, true}]
+ ),
emqtt:publish(
- C1, <<"retained/3">>,
- <<"this is a retained message 3">>,
- [{qos, 0}, {retain, true}]
- ),
+ C1,
+ <<"retained/3">>,
+ <<"this is a retained message 3">>,
+ [{qos, 0}, {retain, true}]
+ ),
Begin = erlang:system_time(millisecond),
{ok, #{}, [0]} = emqtt:subscribe(C1, <<"retained/#">>, [{qos, 0}, {rh, 0}]),
?assertEqual(3, length(receive_messages(3))),
End = erlang:system_time(millisecond),
Diff = End - Begin,
- ?assert(Diff > timer:seconds(2.5) andalso Diff < timer:seconds(3.9),
- lists:flatten(io_lib:format("Diff is :~p~n", [Diff]))),
+ ?assert(
+ Diff > timer:seconds(2.5) andalso Diff < timer:seconds(3.9),
+ lists:flatten(io_lib:format("Diff is :~p~n", [Diff]))
+ ),
ok = emqtt:disconnect(C1),
@@ -335,56 +382,72 @@ t_flow_control(_) ->
t_clear_expired(_) ->
ConfMod = fun(Conf) ->
- Conf#{<<"msg_clear_interval">> := <<"1s">>,
- <<"msg_expiry_interval">> := <<"3s">>}
- end,
+ Conf#{
+ <<"msg_clear_interval">> := <<"1s">>,
+ <<"msg_expiry_interval">> := <<"3s">>
+ }
+ end,
Case = fun() ->
- {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
- {ok, _} = emqtt:connect(C1),
+ {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
+ {ok, _} = emqtt:connect(C1),
- lists:foreach(fun(I) ->
- emqtt:publish(C1,
- <<"retained/", (I + 60):8/unsigned-integer>>,
- #{'Message-Expiry-Interval' => 3},
- <<"retained">>,
- [{qos, 0}, {retain, true}])
- end,
- lists:seq(1, 5)),
- timer:sleep(1000),
+ lists:foreach(
+ fun(I) ->
+ emqtt:publish(
+ C1,
+ <<"retained/", (I + 60):8/unsigned-integer>>,
+ #{'Message-Expiry-Interval' => 3},
+ <<"retained">>,
+ [{qos, 0}, {retain, true}]
+ )
+ end,
+ lists:seq(1, 5)
+ ),
+ timer:sleep(1000),
- {ok, List} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
- ?assertEqual(5, erlang:length(List)),
+ {ok, List} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
+ ?assertEqual(5, erlang:length(List)),
- timer:sleep(4500),
+ timer:sleep(4500),
- {ok, List2} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
- ?assertEqual(0, erlang:length(List2)),
+ {ok, List2} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
+ ?assertEqual(0, erlang:length(List2)),
- ok = emqtt:disconnect(C1)
- end,
+ ok = emqtt:disconnect(C1)
+ end,
with_conf(ConfMod, Case).
t_max_payload_size(_) ->
ConfMod = fun(Conf) -> Conf#{<<"max_payload_size">> := 6} end,
Case = fun() ->
- emqx_retainer:clean(),
- timer:sleep(500),
- {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
- {ok, _} = emqtt:connect(C1),
+ emqx_retainer:clean(),
+ timer:sleep(500),
+ {ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
+ {ok, _} = emqtt:connect(C1),
- emqtt:publish(C1,
- <<"retained/1">>, #{}, <<"1234">>, [{qos, 0}, {retain, true}]),
+ emqtt:publish(
+ C1,
+ <<"retained/1">>,
+ #{},
+ <<"1234">>,
+ [{qos, 0}, {retain, true}]
+ ),
- emqtt:publish(C1,
- <<"retained/2">>, #{}, <<"1234567">>, [{qos, 0}, {retain, true}]),
+ emqtt:publish(
+ C1,
+ <<"retained/2">>,
+ #{},
+ <<"1234567">>,
+ [{qos, 0}, {retain, true}]
+ ),
- timer:sleep(500),
- {ok, List} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
- ?assertEqual(1, erlang:length(List)),
+ timer:sleep(500),
+ {ok, List} = emqx_retainer:page_read(<<"retained/+">>, 1, 10),
+ ?assertEqual(1, erlang:length(List)),
- ok = emqtt:disconnect(C1)
- end,
+ ok = emqtt:disconnect(C1)
+ end,
with_conf(ConfMod, Case).
t_page_read(_) ->
@@ -394,12 +457,13 @@ t_page_read(_) ->
timer:sleep(500),
Fun = fun(I) ->
- emqtt:publish(C1,
- <<"retained/", (I + 60)>>,
- <<"this is a retained message">>,
- [{qos, 0}, {retain, true}]
- )
- end,
+ emqtt:publish(
+ C1,
+ <<"retained/", (I + 60)>>,
+ <<"this is a retained message">>,
+ [{qos, 0}, {retain, true}]
+ )
+ end,
lists:foreach(Fun, lists:seq(1, 9)),
timer:sleep(200),
@@ -436,12 +500,12 @@ receive_messages(Count, Msgs) ->
receive
{publish, Msg} ->
ct:log("Msg: ~p ~n", [Msg]),
- receive_messages(Count-1, [Msg|Msgs]);
+ receive_messages(Count - 1, [Msg | Msgs]);
Other ->
- ct:log("Other Msg: ~p~n",[Other]),
+ ct:log("Other Msg: ~p~n", [Other]),
receive_messages(Count, Msgs)
after 2000 ->
- Msgs
+ Msgs
end.
with_conf(ConfMod, Case) ->
@@ -451,7 +515,8 @@ with_conf(ConfMod, Case) ->
try
Case(),
emqx_retainer:update_config(Conf)
- catch Type:Error:Strace ->
+ catch
+ Type:Error:Strace ->
emqx_retainer:update_config(Conf),
erlang:raise(Type, Error, Strace)
end.
diff --git a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl
index 0eec1b1f1..ba062a883 100644
--- a/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl
+++ b/apps/emqx_retainer/test/emqx_retainer_api_SUITE.erl
@@ -56,19 +56,31 @@ t_config(_Config) ->
Path = api_path(["mqtt", "retainer"]),
{ok, ConfJson} = request_api(get, Path),
ReturnConf = decode_json(ConfJson),
- ?assertMatch(#{backend := _, enable := _, flow_control := _,
- max_payload_size := _, msg_clear_interval := _,
- msg_expiry_interval := _},
- ReturnConf),
+ ?assertMatch(
+ #{
+ backend := _,
+ enable := _,
+ flow_control := _,
+ max_payload_size := _,
+ msg_clear_interval := _,
+ msg_expiry_interval := _
+ },
+ ReturnConf
+ ),
UpdateConf = fun(Enable) ->
- RawConf = emqx_json:decode(ConfJson, [return_maps]),
- UpdateJson = RawConf#{<<"enable">> := Enable},
- {ok, UpdateResJson} = request_api(put,
- Path, [], auth_header_(), UpdateJson),
- UpdateRawConf = emqx_json:decode(UpdateResJson, [return_maps]),
- ?assertEqual(Enable, maps:get(<<"enable">>, UpdateRawConf))
- end,
+ RawConf = emqx_json:decode(ConfJson, [return_maps]),
+ UpdateJson = RawConf#{<<"enable">> := Enable},
+ {ok, UpdateResJson} = request_api(
+ put,
+ Path,
+ [],
+ auth_header_(),
+ UpdateJson
+ ),
+ UpdateRawConf = emqx_json:decode(UpdateResJson, [return_maps]),
+ ?assertEqual(Enable, maps:get(<<"enable">>, UpdateRawConf))
+ end,
UpdateConf(false),
UpdateConf(true).
@@ -80,10 +92,13 @@ t_messages(_) ->
timer:sleep(500),
Each = fun(I) ->
- emqtt:publish(C1, <<"retained/", (I + 60)>>,
- <<"retained">>,
- [{qos, 0}, {retain, true}])
- end,
+ emqtt:publish(
+ C1,
+ <<"retained/", (I + 60)>>,
+ <<"retained">>,
+ [{qos, 0}, {retain, true}]
+ )
+ end,
lists:foreach(Each, lists:seq(1, 5)),
timer:sleep(500),
@@ -91,19 +106,28 @@ t_messages(_) ->
{ok, MsgsJson} = request_api(get, api_path(["mqtt", "retainer", "messages"])),
Msgs = decode_json(MsgsJson),
MsgLen = erlang:length(Msgs),
- ?assert(MsgLen >= 5,
- io_lib:format("message length is:~p~n", [MsgLen])), %% maybe has $SYS messages
+ ?assert(
+ MsgLen >= 5,
+ %% maybe has $SYS messages
+ io_lib:format("message length is:~p~n", [MsgLen])
+ ),
[First | _] = Msgs,
- ?assertMatch(#{msgid := _, topic := _, qos := _,
- publish_at := _, from_clientid := _, from_username := _
- },
- First),
+ ?assertMatch(
+ #{
+ msgid := _,
+ topic := _,
+ qos := _,
+ publish_at := _,
+ from_clientid := _,
+ from_username := _
+ },
+ First
+ ),
ok = emqtt:disconnect(C1).
t_lookup_and_delete(_) ->
-
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
{ok, _} = emqtt:connect(C1),
emqx_retainer:clean(),
@@ -116,10 +140,18 @@ t_lookup_and_delete(_) ->
{ok, LookupJson} = request_api(get, API),
LookupResult = decode_json(LookupJson),
- ?assertMatch(#{msgid := _, topic := _, qos := _, payload := _,
- publish_at := _, from_clientid := _, from_username := _
- },
- LookupResult),
+ ?assertMatch(
+ #{
+ msgid := _,
+ topic := _,
+ qos := _,
+ payload := _,
+ publish_at := _,
+ from_clientid := _,
+ from_username := _
+ },
+ LookupResult
+ ),
{ok, []} = request_api(delete, API),
diff --git a/apps/emqx_retainer/test/emqx_retainer_cli_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_cli_SUITE.erl
index 8a3158aac..69c4c6801 100644
--- a/apps/emqx_retainer/test/emqx_retainer_cli_SUITE.erl
+++ b/apps/emqx_retainer/test/emqx_retainer_cli_SUITE.erl
@@ -37,4 +37,3 @@ end_per_testcase(_TestCase, Config) ->
% t_load(_) ->
% error('TODO').
-
diff --git a/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl
index a29347440..459948902 100644
--- a/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl
+++ b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl
@@ -43,7 +43,7 @@ receive_messages(0, Msgs) ->
receive_messages(Count, Msgs) ->
receive
{publish, Msg} ->
- receive_messages(Count-1, [Msg|Msgs]);
+ receive_messages(Count - 1, [Msg | Msgs]);
_Other ->
receive_messages(Count, Msgs)
after 300 ->
@@ -74,17 +74,26 @@ t_publish_retain_message(_) ->
{ok, Client1} = emqtt:start_link([{proto_ver, v5}]),
{ok, _} = emqtt:connect(Client1),
{ok, _} = emqtt:publish(
- Client1, Topic, #{},
- <<"retained message">>,
- [{qos, 2}, {retain, true}]),
+ Client1,
+ Topic,
+ #{},
+ <<"retained message">>,
+ [{qos, 2}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, Topic, #{},
- <<"new retained message">>,
- [{qos, 2}, {retain, true}]),
+ Client1,
+ Topic,
+ #{},
+ <<"new retained message">>,
+ [{qos, 2}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, Topic, #{},
- <<"not retained message">>,
- [{qos, 2}, {retain, false}]),
+ Client1,
+ Topic,
+ #{},
+ <<"not retained message">>,
+ [{qos, 2}, {retain, false}]
+ ),
{ok, _, [2]} = emqtt:subscribe(Client1, Topic, 2),
[Msg] = receive_messages(3),
@@ -95,7 +104,8 @@ t_publish_retain_message(_) ->
{ok, _} = emqtt:publish(Client1, Topic, #{}, <<"">>, [{qos, 2}, {retain, true}]),
{ok, _, [2]} = emqtt:subscribe(Client1, Topic, 2),
- ?assertEqual(0, length(receive_messages(1))), %% [MQTT-3.3.1-6] [MQTT-3.3.1-7]
+ %% [MQTT-3.3.1-6] [MQTT-3.3.1-7]
+ ?assertEqual(0, length(receive_messages(1))),
ok = emqtt:disconnect(Client1).
@@ -103,38 +113,55 @@ t_publish_message_expiry_interval(_) ->
{ok, Client1} = emqtt:start_link([{proto_ver, v5}]),
{ok, _} = emqtt:connect(Client1),
{ok, _} = emqtt:publish(
- Client1, <<"topic/A">>, #{'Message-Expiry-Interval' => 1},
- <<"retained message">>,
- [{qos, 1}, {retain, true}]),
+ Client1,
+ <<"topic/A">>,
+ #{'Message-Expiry-Interval' => 1},
+ <<"retained message">>,
+ [{qos, 1}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, <<"topic/B">>, #{'Message-Expiry-Interval' => 1},
- <<"retained message">>,
- [{qos, 2}, {retain, true}]),
+ Client1,
+ <<"topic/B">>,
+ #{'Message-Expiry-Interval' => 1},
+ <<"retained message">>,
+ [{qos, 2}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, <<"topic/C">>, #{'Message-Expiry-Interval' => 10},
- <<"retained message">>,
- [{qos, 1}, {retain, true}]),
+ Client1,
+ <<"topic/C">>,
+ #{'Message-Expiry-Interval' => 10},
+ <<"retained message">>,
+ [{qos, 1}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, <<"topic/D">>, #{'Message-Expiry-Interval' => 10},
- <<"retained message">>,
- [{qos, 2}, {retain, true}]),
+ Client1,
+ <<"topic/D">>,
+ #{'Message-Expiry-Interval' => 10},
+ <<"retained message">>,
+ [{qos, 2}, {retain, true}]
+ ),
timer:sleep(1500),
{ok, _, [2]} = emqtt:subscribe(Client1, <<"topic/+">>, 2),
Msgs = receive_messages(6),
- ?assertEqual(2, length(Msgs)), %% [MQTT-3.3.2-5]
+ %% [MQTT-3.3.2-5]
+ ?assertEqual(2, length(Msgs)),
L = lists:map(
- fun(Msg) ->
- MessageExpiryInterval = maps:get('Message-Expiry-Interval',
- maps:get(properties, Msg)),
+ fun(Msg) ->
+ MessageExpiryInterval = maps:get(
+ 'Message-Expiry-Interval',
+ maps:get(properties, Msg)
+ ),
MessageExpiryInterval < 10
- end, Msgs),
- ?assertEqual(2, length(L)), %% [MQTT-3.3.2-6]
+ end,
+ Msgs
+ ),
+ %% [MQTT-3.3.2-6]
+ ?assertEqual(2, length(L)),
ok = emqtt:disconnect(Client1),
- clean_retained( <<"topic/C">>),
- clean_retained( <<"topic/D">>).
-
+ clean_retained(<<"topic/C">>),
+ clean_retained(<<"topic/D">>).
%%--------------------------------------------------------------------
%% Subsctibe
@@ -144,39 +171,50 @@ t_subscribe_retain_handing(_) ->
{ok, Client1} = emqtt:start_link([{proto_ver, v5}]),
{ok, _} = emqtt:connect(Client1),
ok = emqtt:publish(
- Client1, <<"topic/A">>, #{},
- <<"retained message">>,
- [{qos, 0}, {retain, true}]
- ),
+ Client1,
+ <<"topic/A">>,
+ #{},
+ <<"retained message">>,
+ [{qos, 0}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, <<"topic/B">>, #{},
- <<"retained message">>,
- [{qos, 1}, {retain, true}]
- ),
+ Client1,
+ <<"topic/B">>,
+ #{},
+ <<"retained message">>,
+ [{qos, 1}, {retain, true}]
+ ),
{ok, _} = emqtt:publish(
- Client1, <<"topic/C">>, #{},
- <<"retained message">>,
- [{qos, 2}, {retain, true}]
- ),
+ Client1,
+ <<"topic/C">>,
+ #{},
+ <<"retained message">>,
+ [{qos, 2}, {retain, true}]
+ ),
timer:sleep(200),
{ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{<<"topic/+">>, [{rh, 1}, {qos, 2}]}]),
- ?assertEqual(3, length(receive_messages(3))), %% [MQTT-3.3.1-10]
+ %% [MQTT-3.3.1-10]
+ ?assertEqual(3, length(receive_messages(3))),
{ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{<<"topic/+">>, [{rh, 2}, {qos, 2}]}]),
- ?assertEqual(0, length(receive_messages(3))), %% [MQTT-3.3.1-11]
+ %% [MQTT-3.3.1-11]
+ ?assertEqual(0, length(receive_messages(3))),
{ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{<<"topic/+">>, [{rh, 0}, {qos, 2}]}]),
- ?assertEqual(3, length(receive_messages(3))), %% [MQTT-3.3.1-9]
+ %% [MQTT-3.3.1-9]
+ ?assertEqual(3, length(receive_messages(3))),
{ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{<<"topic/+">>, [{rh, 1}, {qos, 2}]}]),
- ?assertEqual(0, length(receive_messages(3))), %% [MQTT-3.3.1-10]
+ %% [MQTT-3.3.1-10]
+ ?assertEqual(0, length(receive_messages(3))),
{ok, _, [2]} = emqtt:subscribe(Client1, #{}, [{<<"topic/+">>, [{rh, 0}, {qos, 2}]}]),
- ?assertEqual(3, length(receive_messages(3))), %% [MQTT-3.8.4-4]
+ %% [MQTT-3.8.4-4]
+ ?assertEqual(3, length(receive_messages(3))),
ok = emqtt:disconnect(Client1),
- clean_retained( <<"topic/A">>),
- clean_retained( <<"topic/B">>),
- clean_retained( <<"topic/C">>).
+ clean_retained(<<"topic/A">>),
+ clean_retained(<<"topic/B">>),
+ clean_retained(<<"topic/C">>).
diff --git a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl
index 32c53d5cd..02a1610fb 100644
--- a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl
+++ b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl
@@ -53,7 +53,7 @@ fields("rule_info") ->
})}
] ++ fields("rule_creation");
-%% TODO: we can delete this API if the Dashboard not denpends on it
+%% TODO: we can delete this API if the Dashboard not depends on it
fields("rule_events") ->
ETopics = [binary_to_atom(emqx_rule_events:event_topic(E)) || E <- emqx_rule_events:event_names()],
[ {"event", sc(hoconsc:enum(ETopics), #{desc => "The event topics", required => true})}
@@ -83,39 +83,39 @@ fields("rule_test") ->
];
fields("metrics") ->
- [ {"sql.matched", sc(integer(), #{
+ [ {"sql.matched", sc(non_neg_integer(), #{
desc => "How much times the FROM clause of the SQL is matched."
})}
, {"sql.matched.rate", sc(float(), #{desc => "The rate of matched, times/second"})}
, {"sql.matched.rate.max", sc(float(), #{desc => "The max rate of matched, times/second"})}
, {"sql.matched.rate.last5m", sc(float(),
#{desc => "The average rate of matched in last 5 minutes, times/second"})}
- , {"sql.passed", sc(integer(), #{desc => "How much times the SQL is passed"})}
- , {"sql.failed", sc(integer(), #{desc => "How much times the SQL is failed"})}
- , {"sql.failed.exception", sc(integer(), #{
+ , {"sql.passed", sc(non_neg_integer(), #{desc => "How much times the SQL is passed"})}
+ , {"sql.failed", sc(non_neg_integer(), #{desc => "How much times the SQL is failed"})}
+ , {"sql.failed.exception", sc(non_neg_integer(), #{
desc => "How much times the SQL is failed due to exceptions. "
"This may because of a crash when calling a SQL function, or "
"trying to do arithmetic operation on undefined variables"
})}
- , {"sql.failed.unknown", sc(integer(), #{
+ , {"sql.failed.unknown", sc(non_neg_integer(), #{
desc => "How much times the SQL is failed due to an unknown error."
})}
- , {"outputs.total", sc(integer(), #{
+ , {"outputs.total", sc(non_neg_integer(), #{
desc => "How much times the outputs are called by the rule. "
"This value may several times of 'sql.matched', depending on the "
"number of the outputs of the rule."
})}
- , {"outputs.success", sc(integer(), #{
+ , {"outputs.success", sc(non_neg_integer(), #{
desc => "How much times the rule success to call the outputs."
})}
- , {"outputs.failed", sc(integer(), #{
+ , {"outputs.failed", sc(non_neg_integer(), #{
desc => "How much times the rule failed to call the outputs."
})}
- , {"outputs.failed.out_of_service", sc(integer(), #{
+ , {"outputs.failed.out_of_service", sc(non_neg_integer(), #{
desc => "How much times the rule failed to call outputs due to the output is "
"out of service. For example, a bridge is disabled or stopped."
})}
- , {"outputs.failed.unknown", sc(integer(), #{
+ , {"outputs.failed.unknown", sc(non_neg_integer(), #{
desc => "How much times the rule failed to call outputs due to to an unknown error."
})}
];
diff --git a/apps/emqx_rule_engine/src/emqx_rule_date.erl b/apps/emqx_rule_engine/src/emqx_rule_date.erl
new file mode 100644
index 000000000..a06571b64
--- /dev/null
+++ b/apps/emqx_rule_engine/src/emqx_rule_date.erl
@@ -0,0 +1,210 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+
+-module(emqx_rule_date).
+
+-export([date/3, date/4, parse_date/4]).
+
+-export([ is_int_char/1
+ , is_symbol_char/1
+ , is_m_char/1
+ ]).
+
+-record(result, {
+ year = "1970" :: string() %%year()
+ , month = "1" :: string() %%month()
+ , day = "1" :: string() %%day()
+ , hour = "0" :: string() %%hour()
+ , minute = "0" :: string() %%minute() %% epoch in millisecond precision
+ , second = "0" :: string() %%second() %% epoch in millisecond precision
+ , zone = "+00:00" :: string() %%integer() %% zone maybe some value
+}).
+
+%% -type time_unit() :: 'microsecond'
+%% | 'millisecond'
+%% | 'nanosecond'
+%% | 'second'.
+%% -type offset() :: [byte()] | (Time :: integer()).
+date(TimeUnit, Offset, FormatString) ->
+ date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)).
+
+date(TimeUnit, Offset, FormatString, TimeEpoch) ->
+ [Head|Other] = string:split(FormatString, "%", all),
+ R = create_tag([{st, Head}], Other),
+ Res = lists:map(fun(Expr) ->
+ eval_tag(rmap(make_time(TimeUnit, Offset, TimeEpoch)), Expr) end, R),
+ lists:concat(Res).
+
+parse_date(TimeUnit, Offset, FormatString, InputString) ->
+ [Head|Other] = string:split(FormatString, "%", all),
+ R = create_tag([{st, Head}], Other),
+ IsZ = fun(V) -> case V of
+ {tag, $Z} -> true;
+ _ -> false
+ end end,
+ R1 = lists:filter(IsZ, R),
+ IfFun = fun(Con, A, B) ->
+ case Con of
+ [] -> A;
+ _ -> B
+ end end,
+ Res = parse_input(FormatString, InputString),
+ Str = Res#result.year ++ "-"
+ ++ Res#result.month ++ "-"
+ ++ Res#result.day ++ "T"
+ ++ Res#result.hour ++ ":"
+ ++ Res#result.minute ++ ":"
+ ++ Res#result.second ++
+ IfFun(R1, Offset, Res#result.zone),
+ calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]).
+
+mlist(R)->
+ [ {$H, R#result.hour} %% %H Shows hour in 24-hour format [15]
+ , {$M, R#result.minute} %% %M Displays minutes [00-59]
+ , {$S, R#result.second} %% %S Displays seconds [00-59]
+ , {$y, R#result.year} %% %y Displays year YYYY [2021]
+ , {$m, R#result.month} %% %m Displays the number of the month [01-12]
+ , {$d, R#result.day} %% %d Displays the number of the month [01-12]
+ , {$Z, R#result.zone} %% %Z Displays Time zone
+ ].
+
+rmap(Result) ->
+ maps:from_list(mlist(Result)).
+
+support_char() -> "HMSymdZ".
+
+create_tag(Head, []) ->
+ Head;
+create_tag(Head, [Val1|RVal]) ->
+ case Val1 of
+ [] -> create_tag(Head ++ [{st, [$%]}], RVal);
+ [H| Other] ->
+ case lists:member(H, support_char()) of
+ true -> create_tag(Head ++ [{tag, H}, {st, Other}], RVal);
+ false -> create_tag(Head ++ [{st, [$%|Val1]}], RVal)
+ end
+ end.
+
+eval_tag(_,{st, Str}) ->
+ Str;
+eval_tag(Map,{tag, Char}) ->
+ maps:get(Char, Map, "undefined").
+
+%% make_time(TimeUnit, Offset) ->
+%% make_time(TimeUnit, Offset, erlang:system_time(TimeUnit)).
+make_time(TimeUnit, Offset, TimeEpoch) ->
+ Res = calendar:system_time_to_rfc3339(TimeEpoch,
+ [{unit, TimeUnit}, {offset, Offset}]),
+ [Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, _T,
+ H1, H2, $:, Min1, Min2, $:, S1, S2 | TimeStr] = Res,
+ IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
+ {FractionStr, UtcOffset} = lists:splitwith(IsFractionChar, TimeStr),
+ #result{
+ year = [Y1, Y2, Y3, Y4]
+ , month = [Mon1, Mon2]
+ , day = [D1, D2]
+ , hour = [H1, H2]
+ , minute = [Min1, Min2]
+ , second = [S1, S2] ++ FractionStr
+ , zone = UtcOffset
+ }.
+
+
+is_int_char(C) ->
+ C >= $0 andalso C =< $9 .
+is_symbol_char(C) ->
+ C =:= $- orelse C =:= $+ .
+is_m_char(C) ->
+ C =:= $:.
+
+parse_char_with_fun(_, []) -> error(null_input);
+parse_char_with_fun(ValidFun, [C|Other]) ->
+ Res = case erlang:is_function(ValidFun) of
+ true -> ValidFun(C);
+ false -> erlang:apply(emqx_rule_date, ValidFun, [C])
+ end,
+ case Res of
+ true -> {C, Other};
+ false -> error({unexpected,[C|Other]})
+ end.
+parse_string([], Input) -> {[], Input};
+parse_string([C|Other], Input) ->
+ {C1, Input1} = parse_char_with_fun(fun(V) -> V =:= C end, Input),
+ {Res, Input2} = parse_string(Other, Input1),
+ {[C1|Res], Input2}.
+
+parse_times(0, _, Input) -> {[], Input};
+parse_times(Times, Fun, Input) ->
+ {C1, Input1} = parse_char_with_fun(Fun, Input),
+ {Res, Input2} = parse_times((Times - 1), Fun, Input1),
+ {[C1|Res], Input2}.
+
+parse_int_times(Times, Input) ->
+ parse_times(Times, is_int_char, Input).
+
+parse_fraction(Input) ->
+ IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
+ lists:splitwith(IsFractionChar, Input).
+
+parse_second(Input) ->
+ {M, Input1} = parse_int_times(2, Input),
+ {M1, Input2} = parse_fraction(Input1),
+ {M++M1, Input2}.
+
+parse_zone(Input) ->
+ {S, Input1} = parse_char_with_fun(is_symbol_char, Input),
+ {M, Input2} = parse_int_times(2, Input1),
+ {C, Input3} = parse_char_with_fun(is_m_char, Input2),
+ {V, Input4} = parse_int_times(2, Input3),
+ {[S|M++[C|V]], Input4}.
+
+mlist1()->
+ maps:from_list(
+ [ {$H, fun(Input) -> parse_int_times(2, Input) end} %% %H Shows hour in 24-hour format [15]
+ , {$M, fun(Input) -> parse_int_times(2, Input) end} %% %M Displays minutes [00-59]
+ , {$S, fun(Input) -> parse_second(Input) end} %% %S Displays seconds [00-59]
+ , {$y, fun(Input) -> parse_int_times(4, Input) end} %% %y Displays year YYYY [2021]
+ , {$m, fun(Input) -> parse_int_times(2, Input) end} %% %m Displays the number of the month [01-12]
+ , {$d, fun(Input) -> parse_int_times(2, Input) end} %% %d Displays the number of the month [01-12]
+ , {$Z, fun(Input) -> parse_zone(Input) end} %% %Z Displays Time zone
+ ]).
+
+update_result($H, Res, Str) -> Res#result{hour=Str};
+update_result($M, Res, Str) -> Res#result{minute=Str};
+update_result($S, Res, Str) -> Res#result{second=Str};
+update_result($y, Res, Str) -> Res#result{year=Str};
+update_result($m, Res, Str) -> Res#result{month=Str};
+update_result($d, Res, Str) -> Res#result{day=Str};
+update_result($Z, Res, Str) -> Res#result{zone=Str}.
+
+parse_tag(Res, {st, St}, InputString) ->
+ {_A, B} = parse_string(St, InputString),
+ {Res, B};
+parse_tag(Res, {tag, St}, InputString) ->
+ Fun = maps:get(St, mlist1()),
+ {A, B} = Fun(InputString),
+ NRes = update_result(St, Res, A),
+ {NRes, B}.
+
+parse_tags(Res, [], _) -> Res;
+parse_tags(Res, [Tag|Others], InputString) ->
+ {NRes, B} = parse_tag(Res, Tag, InputString),
+ parse_tags(NRes, Others, B).
+
+parse_input(FormatString, InputString) ->
+ [Head|Other] = string:split(FormatString, "%", all),
+ R = create_tag([{st, Head}], Other),
+ parse_tags(#result{}, R, InputString).
diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl
index 0c83c8ccd..ff2415d5c 100644
--- a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl
+++ b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl
@@ -99,7 +99,7 @@ rule_info_schema() ->
schema("/rules") ->
#{
- operationId => '/rules',
+ 'operationId' => '/rules',
get => #{
tags => [<<"rules">>],
description => <<"List all rules">>,
@@ -111,7 +111,7 @@ schema("/rules") ->
tags => [<<"rules">>],
description => <<"Create a new rule using given Id">>,
summary => <<"Create a Rule">>,
- requestBody => rule_creation_schema(),
+ 'requestBody' => rule_creation_schema(),
responses => #{
400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
201 => rule_info_schema()
@@ -120,7 +120,7 @@ schema("/rules") ->
schema("/rule_events") ->
#{
- operationId => '/rule_events',
+ 'operationId' => '/rule_events',
get => #{
tags => [<<"rules">>],
description => <<"List all events can be used in rules">>,
@@ -133,7 +133,7 @@ schema("/rule_events") ->
schema("/rules/:id") ->
#{
- operationId => '/rules/:id',
+ 'operationId' => '/rules/:id',
get => #{
tags => [<<"rules">>],
description => <<"Get a rule by given Id">>,
@@ -149,7 +149,7 @@ schema("/rules/:id") ->
description => <<"Update a rule by given Id to all nodes in the cluster">>,
summary => <<"Update a Rule">>,
parameters => param_path_id(),
- requestBody => rule_creation_schema(),
+ 'requestBody' => rule_creation_schema(),
responses => #{
400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
200 => rule_info_schema()
@@ -168,7 +168,7 @@ schema("/rules/:id") ->
schema("/rules/:id/reset_metrics") ->
#{
- operationId => '/rules/:id/reset_metrics',
+ 'operationId' => '/rules/:id/reset_metrics',
put => #{
tags => [<<"rules">>],
description => <<"Reset a rule metrics">>,
@@ -183,12 +183,12 @@ schema("/rules/:id/reset_metrics") ->
schema("/rule_test") ->
#{
- operationId => '/rule_test',
+ 'operationId' => '/rule_test',
post => #{
tags => [<<"rules">>],
description => <<"Test a rule">>,
summary => <<"Test a Rule">>,
- requestBody => rule_test_schema(),
+ 'requestBody' => rule_test_schema(),
responses => #{
400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
412 => error_schema('NOT_MATCH', "SQL Not Match"),
diff --git a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl
index c06ba1130..e7f5da52f 100644
--- a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl
+++ b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl
@@ -190,6 +190,9 @@
, rfc3339_to_unix_ts/2
, now_timestamp/0
, now_timestamp/1
+ , format_date/3
+ , format_date/4
+ , date_to_unix_ts/4
]).
%% Proc Dict Func
@@ -880,6 +883,25 @@ time_unit(<<"millisecond">>) -> millisecond;
time_unit(<<"microsecond">>) -> microsecond;
time_unit(<<"nanosecond">>) -> nanosecond.
+format_date(TimeUnit, Offset, FormatString) ->
+ emqx_plugin_libs_rule:bin(
+ emqx_rule_date:date(time_unit(TimeUnit),
+ emqx_plugin_libs_rule:str(Offset),
+ emqx_plugin_libs_rule:str(FormatString))).
+
+format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
+ emqx_plugin_libs_rule:bin(
+ emqx_rule_date:date(time_unit(TimeUnit),
+ emqx_plugin_libs_rule:str(Offset),
+ emqx_plugin_libs_rule:str(FormatString),
+ TimeEpoch)).
+
+date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
+ emqx_rule_date:parse_date(time_unit(TimeUnit),
+ emqx_plugin_libs_rule:str(Offset),
+ emqx_plugin_libs_rule:str(FormatString),
+ emqx_plugin_libs_rule:str(InputString)).
+
%% @doc This is for sql funcs that should be handled in the specific modules.
%% Here the emqx_rule_funcs module acts as a proxy, forwarding
%% the function handling to the worker module.
diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl
index 906a219b2..8211426ab 100644
--- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl
+++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl
@@ -48,9 +48,9 @@
-spec(apply_rules(list(rule()), input()) -> ok).
apply_rules([], _Input) ->
ok;
-apply_rules([#{enable := false}|More], Input) ->
+apply_rules([#{enable := false} | More], Input) ->
apply_rules(More, Input);
-apply_rules([Rule|More], Input) ->
+apply_rules([Rule | More], Input) ->
apply_rule_discard_result(Rule, Input),
apply_rules(More, Input).
@@ -150,14 +150,14 @@ select_and_transform(Fields, Input) ->
select_and_transform([], _Input, Output) ->
Output;
-select_and_transform(['*'|More], Input, Output) ->
+select_and_transform(['*' | More], Input, Output) ->
select_and_transform(More, Input, maps:merge(Output, Input));
-select_and_transform([{as, Field, Alias}|More], Input, Output) ->
+select_and_transform([{as, Field, Alias} | More], Input, Output) ->
Val = eval(Field, Input),
select_and_transform(More,
nested_put(Alias, Val, Input),
nested_put(Alias, Val, Output));
-select_and_transform([Field|More], Input, Output) ->
+select_and_transform([Field | More], Input, Output) ->
Val = eval(Field, Input),
Key = alias(Field),
select_and_transform(More,
@@ -172,7 +172,7 @@ select_and_collect(Fields, Input) ->
select_and_collect([{as, Field, {_, A} = Alias}], Input, {Output, _}) ->
Val = eval(Field, Input),
{nested_put(Alias, Val, Output), {A, ensure_list(Val)}};
-select_and_collect([{as, Field, Alias}|More], Input, {Output, LastKV}) ->
+select_and_collect([{as, Field, Alias} | More], Input, {Output, LastKV}) ->
Val = eval(Field, Input),
select_and_collect(More,
nested_put(Alias, Val, Input),
@@ -181,7 +181,7 @@ select_and_collect([Field], Input, {Output, _}) ->
Val = eval(Field, Input),
Key = alias(Field),
{nested_put(Key, Val, Output), {'item', ensure_list(Val)}};
-select_and_collect([Field|More], Input, {Output, LastKV}) ->
+select_and_collect([Field | More], Input, {Output, LastKV}) ->
Val = eval(Field, Input),
Key = alias(Field),
select_and_collect(More,
diff --git a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl
index 846191098..a795e2cda 100644
--- a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl
+++ b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl
@@ -664,6 +664,26 @@ t_rfc3339_to_unix_ts(_) ->
?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
end || Unit <- [second,millisecond,microsecond,nanosecond]].
+t_format_date_funcs(_) ->
+ ?PROPTEST(prop_format_date_fun).
+
+prop_format_date_fun() ->
+ Args1 = [<<"second">>, <<"+07:00">>, <<"%m--%d--%y---%H:%M:%S%Z">>],
+ ?FORALL(S, erlang:system_time(second),
+ S == apply_func(date_to_unix_ts,
+ Args1 ++ [apply_func(format_date,
+ Args1 ++ [S])])),
+ Args2 = [<<"millisecond">>, <<"+04:00">>, <<"--%m--%d--%y---%H:%M:%S%Z">>],
+ ?FORALL(S, erlang:system_time(millisecond),
+ S == apply_func(date_to_unix_ts,
+ Args2 ++ [apply_func(format_date,
+ Args2 ++ [S])])),
+ Args = [<<"second">>, <<"+08:00">>, <<"%y-%m-%d-%H:%M:%S%Z">>],
+ ?FORALL(S, erlang:system_time(second),
+ S == apply_func(date_to_unix_ts,
+ Args ++ [apply_func(format_date,
+ Args ++ [S])])).
+
%%------------------------------------------------------------------------------
%% Utility functions
%%------------------------------------------------------------------------------
@@ -822,4 +842,3 @@ all() ->
suite() ->
[{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}].
-
diff --git a/apps/emqx_slow_subs/include/emqx_slow_subs.hrl b/apps/emqx_slow_subs/include/emqx_slow_subs.hrl
index f282037dc..4c6b1cc8c 100644
--- a/apps/emqx_slow_subs/include/emqx_slow_subs.hrl
+++ b/apps/emqx_slow_subs/include/emqx_slow_subs.hrl
@@ -23,12 +23,13 @@
-define(MAX_SIZE, 1000).
--record(top_k, { index :: topk_index()
- , last_update_time :: pos_integer()
- , extra = []
- }).
+-record(top_k, {
+ index :: topk_index(),
+ last_update_time :: pos_integer(),
+ extra = []
+}).
--record(index_tab, { index :: index()}).
+-record(index_tab, {index :: index()}).
-type top_k() :: #top_k{}.
-type index_tab() :: #index_tab{}.
diff --git a/apps/emqx_slow_subs/rebar.config b/apps/emqx_slow_subs/rebar.config
index 528efecb6..9f17b7657 100644
--- a/apps/emqx_slow_subs/rebar.config
+++ b/apps/emqx_slow_subs/rebar.config
@@ -1,4 +1,5 @@
%% -*- mode: erlang -*-
-{deps, [ {emqx, {path, "../emqx"}}
- ]}.
+{deps, [{emqx, {path, "../emqx"}}]}.
+
+{project_plugins, [erlfmt]}.
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs.app.src b/apps/emqx_slow_subs/src/emqx_slow_subs.app.src
index 8f5a85efc..5a46ff92a 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs.app.src
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs.app.src
@@ -1,12 +1,13 @@
-{application, emqx_slow_subs,
- [{description, "EMQX Slow Subscribers Statistics"},
- {vsn, "1.0.0"}, % strict semver, bump manually!
- {modules, []},
- {registered, [emqx_slow_subs_sup]},
- {applications, [kernel,stdlib]},
- {mod, {emqx_slow_subs_app,[]}},
- {env, []},
- {licenses, ["Apache-2.0"]},
- {maintainers, ["EMQX Team "]},
- {links, []}
- ]}.
+{application, emqx_slow_subs, [
+ {description, "EMQX Slow Subscribers Statistics"},
+ % strict semver, bump manually!
+ {vsn, "1.0.0"},
+ {modules, []},
+ {registered, [emqx_slow_subs_sup]},
+ {applications, [kernel, stdlib, emqx]},
+ {mod, {emqx_slow_subs_app, []}},
+ {env, []},
+ {licenses, ["Apache-2.0"]},
+ {maintainers, ["EMQX Team "]},
+ {links, []}
+]}.
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs.erl b/apps/emqx_slow_subs/src/emqx_slow_subs.erl
index a29dea123..f0605baca 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs.erl
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs.erl
@@ -22,37 +22,50 @@
-include_lib("emqx/include/logger.hrl").
-include_lib("emqx_slow_subs/include/emqx_slow_subs.hrl").
--export([ start_link/0, on_delivery_completed/3, update_settings/1
- , clear_history/0, init_tab/0, post_config_update/5
- ]).
+-export([
+ start_link/0,
+ on_delivery_completed/3,
+ update_settings/1,
+ clear_history/0,
+ init_tab/0,
+ post_config_update/5
+]).
%% gen_server callbacks
--export([ init/1
- , handle_call/3
- , handle_cast/2
- , handle_info/2
- , terminate/2
- , code_change/3
- ]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3
+]).
-compile(nowarn_unused_type).
--type state() :: #{ enable := boolean()
- , last_tick_at := pos_integer()
- , expire_timer := undefined | reference()
- }.
+-type state() :: #{
+ enable := boolean(),
+ last_tick_at := pos_integer(),
+ expire_timer := undefined | reference()
+}.
-type message() :: #message{}.
--type stats_type() :: whole %% whole = internal + response
- | internal %% timespan from message in to deliver
- | response. %% timespan from delivery to client response
+%% whole = internal + response
+-type stats_type() ::
+ whole
+ %% timespan from message in to deliver
+ | internal
+ %% timespan from delivery to client response
+ | response.
-type stats_update_args() :: #{session_birth_time := pos_integer()}.
--type stats_update_env() :: #{ threshold := non_neg_integer()
- , stats_type := stats_type()
- , max_size := pos_integer()}.
+-type stats_update_env() :: #{
+ threshold := non_neg_integer(),
+ stats_type := stats_type(),
+ max_size := pos_integer()
+}.
-ifdef(TEST).
-define(EXPIRE_CHECK_INTERVAL, timer:seconds(1)).
@@ -73,33 +86,39 @@
%% APIs
%%--------------------------------------------------------------------
%% @doc Start the st_statistics
--spec(start_link() -> emqx_types:startlink_ret()).
+-spec start_link() -> emqx_types:startlink_ret().
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-on_delivery_completed(#message{timestamp = Ts},
- #{session_birth_time := BirthTime}, _Cfg) when Ts =< BirthTime ->
+on_delivery_completed(
+ #message{timestamp = Ts},
+ #{session_birth_time := BirthTime},
+ _Cfg
+) when Ts =< BirthTime ->
ok;
-
on_delivery_completed(Msg, Env, Cfg) ->
on_delivery_completed(Msg, Env, erlang:system_time(millisecond), Cfg).
-on_delivery_completed(#message{topic = Topic} = Msg,
- #{clientid := ClientId},
- Now,
- #{threshold := Threshold,
- stats_type := StatsType,
- max_size := MaxSize}) ->
+on_delivery_completed(
+ #message{topic = Topic} = Msg,
+ #{clientid := ClientId},
+ Now,
+ #{
+ threshold := Threshold,
+ stats_type := StatsType,
+ max_size := MaxSize
+ }
+) ->
TimeSpan = calc_timespan(StatsType, Msg, Now),
case TimeSpan =< Threshold of
- true -> ok;
+ true ->
+ ok;
_ ->
Id = ?ID(ClientId, Topic),
LastUpdateValue = find_last_update_value(Id),
case TimeSpan =< LastUpdateValue of
true -> ok;
- _ ->
- try_insert_to_topk(MaxSize, Now, LastUpdateValue, TimeSpan, Id)
+ _ -> try_insert_to_topk(MaxSize, Now, LastUpdateValue, TimeSpan, Id)
end
end.
@@ -113,15 +132,23 @@ post_config_update(_KeyPath, _UpdateReq, NewConf, _OldConf, _AppEnvs) ->
gen_server:call(?MODULE, {update_settings, NewConf}, ?DEF_CALL_TIMEOUT).
init_tab() ->
- safe_create_tab(?TOPK_TAB, [ ordered_set, public, named_table
- , {keypos, #top_k.index}, {write_concurrency, true}
- , {read_concurrency, true}
- ]),
+ safe_create_tab(?TOPK_TAB, [
+ ordered_set,
+ public,
+ named_table,
+ {keypos, #top_k.index},
+ {write_concurrency, true},
+ {read_concurrency, true}
+ ]),
- safe_create_tab(?INDEX_TAB, [ ordered_set, public, named_table
- , {keypos, #index_tab.index}, {write_concurrency, true}
- , {read_concurrency, true}
- ]).
+ safe_create_tab(?INDEX_TAB, [
+ ordered_set,
+ public,
+ named_table,
+ {keypos, #index_tab.index},
+ {write_concurrency, true},
+ {read_concurrency, true}
+ ]).
%%--------------------------------------------------------------------
%% gen_server callbacks
@@ -130,12 +157,13 @@ init_tab() ->
init([]) ->
erlang:process_flag(trap_exit, true),
- emqx_conf:add_handler([slow_subs], ?MODULE),
+ ok = emqx_conf:add_handler([slow_subs], ?MODULE),
- InitState = #{enable => false,
- last_tick_at => 0,
- expire_timer => undefined
- },
+ InitState = #{
+ enable => false,
+ last_tick_at => 0,
+ expire_timer => undefined
+ },
Enable = emqx:get_config([slow_subs, enable]),
{ok, check_enable(Enable, InitState)}.
@@ -143,11 +171,9 @@ init([]) ->
handle_call({update_settings, #{enable := Enable}}, _From, State) ->
State2 = check_enable(Enable, State),
{reply, ok, State2};
-
handle_call(clear_history, _, State) ->
do_clear_history(),
{reply, ok, State};
-
handle_call(Req, _From, State) ->
?SLOG(error, #{msg => "unexpected_call", call => Req}),
{reply, ignored, State}.
@@ -161,12 +187,12 @@ handle_info(expire_tick, State) ->
do_clear(Logs),
State1 = start_timer(expire_timer, fun expire_tick/0, State),
{noreply, State1};
-
handle_info(Info, State) ->
?SLOG(error, #{msg => "unexpected_info", info => Info}),
{noreply, State}.
terminate(_Reason, State) ->
+ ok = emqx_conf:remove_handler([slow_subs]),
_ = unload(State),
ok.
@@ -180,46 +206,52 @@ expire_tick() ->
erlang:send_after(?EXPIRE_CHECK_INTERVAL, self(), ?FUNCTION_NAME).
load(State) ->
- #{top_k_num := MaxSizeT,
- stats_type := StatsType,
- threshold := Threshold} = emqx:get_config([slow_subs]),
+ #{
+ top_k_num := MaxSizeT,
+ stats_type := StatsType,
+ threshold := Threshold
+ } = emqx:get_config([slow_subs]),
MaxSize = erlang:min(MaxSizeT, ?MAX_SIZE),
- _ = emqx:hook('delivery.completed',
- {?MODULE, on_delivery_completed,
- [#{max_size => MaxSize,
- stats_type => StatsType,
- threshold => Threshold
- }]}),
+ _ = emqx:hook(
+ 'delivery.completed',
+ {?MODULE, on_delivery_completed, [
+ #{
+ max_size => MaxSize,
+ stats_type => StatsType,
+ threshold => Threshold
+ }
+ ]}
+ ),
State1 = start_timer(expire_timer, fun expire_tick/0, State),
State1#{enable := true, last_tick_at => ?NOW}.
unload(#{expire_timer := ExpireTimer} = State) ->
emqx:unhook('delivery.completed', {?MODULE, on_delivery_completed}),
- State#{enable := false,
- expire_timer := cancel_timer(ExpireTimer)}.
+ State#{
+ enable := false,
+ expire_timer := cancel_timer(ExpireTimer)
+ }.
do_clear(Logs) ->
Now = ?NOW,
Interval = emqx:get_config([slow_subs, expire_interval]),
Each = fun(#top_k{index = ?TOPK_INDEX(TimeSpan, Id), last_update_time = Ts}) ->
- case Now - Ts >= Interval of
- true ->
- delete_with_index(TimeSpan, Id);
- _ ->
- true
- end
- end,
+ case Now - Ts >= Interval of
+ true ->
+ delete_with_index(TimeSpan, Id);
+ _ ->
+ true
+ end
+ end,
lists:foreach(Each, Logs).
-spec calc_timespan(stats_type(), emqx_types:message(), non_neg_integer()) -> non_neg_integer().
calc_timespan(whole, #message{timestamp = Ts}, Now) ->
Now - Ts;
-
calc_timespan(internal, #message{timestamp = Ts} = Msg, Now) ->
End = emqx_message:get_header(deliver_begin_at, Msg, Now),
End - Ts;
-
calc_timespan(response, Msg, Now) ->
Begin = emqx_message:get_header(deliver_begin_at, Msg, Now),
Now - Begin.
@@ -248,7 +280,8 @@ try_insert_to_topk(MaxSize, Now, LastUpdateValue, TimeSpan, Id) ->
update_topk(Now, LastUpdateValue, TimeSpan, Id);
?TOPK_INDEX(Min, MinId) ->
case TimeSpan =< Min of
- true -> false;
+ true ->
+ false;
_ ->
update_topk(Now, LastUpdateValue, TimeSpan, Id),
delete_with_index(Min, MinId)
@@ -256,10 +289,9 @@ try_insert_to_topk(MaxSize, Now, LastUpdateValue, TimeSpan, Id) ->
end
end.
-
-spec find_last_update_value(id()) -> non_neg_integer().
find_last_update_value(Id) ->
- case ets:next(?INDEX_TAB, ?INDEX(0, Id)) of
+ case ets:next(?INDEX_TAB, ?INDEX(0, Id)) of
?INDEX(LastUpdateValue, Id) ->
LastUpdateValue;
_ ->
@@ -269,10 +301,11 @@ find_last_update_value(Id) ->
-spec update_topk(pos_integer(), non_neg_integer(), non_neg_integer(), id()) -> true.
update_topk(Now, LastUpdateValue, TimeSpan, Id) ->
%% update record
- ets:insert(?TOPK_TAB, #top_k{index = ?TOPK_INDEX(TimeSpan, Id),
- last_update_time = Now,
- extra = []
- }),
+ ets:insert(?TOPK_TAB, #top_k{
+ index = ?TOPK_INDEX(TimeSpan, Id),
+ last_update_time = Now,
+ extra = []
+ }),
%% update index
ets:insert(?INDEX_TAB, #index_tab{index = ?INDEX(TimeSpan, Id)}),
@@ -283,7 +316,6 @@ update_topk(Now, LastUpdateValue, TimeSpan, Id) ->
-spec delete_with_index(non_neg_integer(), id()) -> true.
delete_with_index(0, _) ->
true;
-
delete_with_index(TimeSpan, Id) ->
ets:delete(?INDEX_TAB, ?INDEX(TimeSpan, Id)),
ets:delete(?TOPK_TAB, ?TOPK_INDEX(TimeSpan, Id)).
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs_api.erl b/apps/emqx_slow_subs/src/emqx_slow_subs_api.erl
index 98072ba9c..74adfde1c 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs_api.erl
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs_api.erl
@@ -41,48 +41,52 @@ paths() -> ["/slow_subscriptions", "/slow_subscriptions/settings"].
schema(("/slow_subscriptions")) ->
#{
- 'operationId' => slow_subs,
- delete => #{tags => [<<"slow subs">>],
- description => <<"Clear current data and re count slow topic">>,
- parameters => [],
- 'requestBody' => [],
- responses => #{204 => <<"No Content">>}
- },
- get => #{tags => [<<"slow subs">>],
- description => <<"Get slow topics statistics record data">>,
- parameters => [ {page, mk(integer(), #{in => query})}
- , {limit, mk(integer(), #{in => query})}
- ],
- 'requestBody' => [],
- responses => #{200 => [{data, mk(hoconsc:array(ref(record)), #{})}]}
- }
- };
-
+ 'operationId' => slow_subs,
+ delete => #{
+ tags => [<<"slow subs">>],
+ description => <<"Clear current data and re count slow topic">>,
+ parameters => [],
+ 'requestBody' => [],
+ responses => #{204 => <<"No Content">>}
+ },
+ get => #{
+ tags => [<<"slow subs">>],
+ description => <<"Get slow topics statistics record data">>,
+ parameters => [
+ {page, mk(pos_integer(), #{in => query})},
+ {limit, mk(pos_integer(), #{in => query})}
+ ],
+ 'requestBody' => [],
+ responses => #{200 => [{data, mk(hoconsc:array(ref(record)), #{})}]}
+ }
+ };
schema("/slow_subscriptions/settings") ->
- #{'operationId' => settings,
- get => #{tags => [<<"slow subs">>],
- description => <<"Get slow subs settings">>,
- responses => #{200 => conf_schema()}
- },
- put => #{tags => [<<"slow subs">>],
- description => <<"Update slow subs settings">>,
- 'requestBody' => conf_schema(),
- responses => #{200 => conf_schema()}
- }
- }.
+ #{
+ 'operationId' => settings,
+ get => #{
+ tags => [<<"slow subs">>],
+ description => <<"Get slow subs settings">>,
+ responses => #{200 => conf_schema()}
+ },
+ put => #{
+ tags => [<<"slow subs">>],
+ description => <<"Update slow subs settings">>,
+ 'requestBody' => conf_schema(),
+ responses => #{200 => conf_schema()}
+ }
+ }.
fields(record) ->
- [ {clientid,
- mk(string(), #{desc => <<"the clientid">>})},
- {node,
- mk(string(), #{desc => <<"the node">>})},
- {topic,
- mk(string(), #{desc => <<"the topic">>})},
- {timespan,
- mk(integer(),
- #{desc => <<"timespan for message transmission">>})},
- {last_update_time,
- mk(integer(), #{desc => <<"the timestamp of last update">>})}
+ [
+ {clientid, mk(string(), #{desc => <<"the clientid">>})},
+ {node, mk(string(), #{desc => <<"the node">>})},
+ {topic, mk(string(), #{desc => <<"the topic">>})},
+ {timespan,
+ mk(
+ integer(),
+ #{desc => <<"timespan for message transmission">>}
+ )},
+ {last_update_time, mk(integer(), #{desc => <<"the timestamp of last update">>})}
].
conf_schema() ->
@@ -92,17 +96,17 @@ conf_schema() ->
slow_subs(delete, _) ->
_ = rpc_call(fun(Nodes) -> emqx_slow_subs_proto_v1:clear_history(Nodes) end),
{204};
-
slow_subs(get, _) ->
NodeRankL = rpc_call(fun(Nodes) -> emqx_slow_subs_proto_v1:get_history(Nodes) end),
- Fun = fun({ok, L}, Acc) -> L ++ Acc;
- (_, Acc) -> Acc
- end,
+ Fun = fun
+ ({ok, L}, Acc) -> L ++ Acc;
+ (_, Acc) -> Acc
+ end,
RankL = lists:foldl(Fun, [], NodeRankL),
SortFun = fun(#{timespan := A}, #{timespan := B}) ->
- A > B
- end,
+ A > B
+ end,
SortedL = lists:sort(SortFun, RankL),
SortedL2 = lists:sublist(SortedL, ?MAX_SIZE),
@@ -112,22 +116,25 @@ slow_subs(get, _) ->
get_history() ->
Node = node(),
RankL = ets:tab2list(?TOPK_TAB),
- ConvFun = fun(#top_k{index = ?TOPK_INDEX(TimeSpan, ?ID(ClientId, Topic)),
- last_update_time = LastUpdateTime
- }) ->
- #{ clientid => ClientId
- , node => Node
- , topic => Topic
- , timespan => TimeSpan
- , last_update_time => LastUpdateTime
- }
- end,
+ ConvFun = fun(
+ #top_k{
+ index = ?TOPK_INDEX(TimeSpan, ?ID(ClientId, Topic)),
+ last_update_time = LastUpdateTime
+ }
+ ) ->
+ #{
+ clientid => ClientId,
+ node => Node,
+ topic => Topic,
+ timespan => TimeSpan,
+ last_update_time => LastUpdateTime
+ }
+ end,
lists:map(ConvFun, RankL).
settings(get, _) ->
{200, emqx:get_raw_config([slow_subs], #{})};
-
settings(put, #{body := Body}) ->
case emqx_slow_subs:update_settings(Body) of
{ok, #{config := NewConf}} ->
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs_app.erl b/apps/emqx_slow_subs/src/emqx_slow_subs_app.erl
index ba2b84fba..f1de12088 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs_app.erl
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs_app.erl
@@ -18,9 +18,10 @@
-behaviour(application).
--export([ start/2
- , stop/1
- ]).
+-export([
+ start/2,
+ stop/1
+]).
start(_Type, _Args) ->
{ok, Sup} = emqx_slow_subs_sup:start_link(),
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl b/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl
index 1d54edd6b..fe6a5aa0c 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs_schema.erl
@@ -9,23 +9,32 @@ namespace() -> "slow_subs".
roots() -> ["slow_subs"].
fields("slow_subs") ->
- [ {enable, sc(boolean(), false, "Enable this feature.")}
- , {threshold,
- sc(emqx_schema:duration_ms(),
- "500ms",
- "The latency threshold for statistics, the minimum value is 100ms.")}
- , {expire_interval,
- sc(emqx_schema:duration_ms(),
- "300s",
- "The eviction time of the record, which in the statistics record table.")}
- , {top_k_num,
- sc(integer(),
- 10,
- "The maximum number of records in the slow subscription statistics record table.")}
- , {stats_type,
- sc(hoconsc:union([whole, internal, response]),
- whole,
- "The method to calculate the latency.")}
+ [
+ {enable, sc(boolean(), false, "Enable this feature.")},
+ {threshold,
+ sc(
+ emqx_schema:duration_ms(),
+ "500ms",
+ "The latency threshold for statistics, the minimum value is 100ms."
+ )},
+ {expire_interval,
+ sc(
+ emqx_schema:duration_ms(),
+ "300s",
+ "The eviction time of the record, which in the statistics record table."
+ )},
+ {top_k_num,
+ sc(
+ pos_integer(),
+ 10,
+ "The maximum number of records in the slow subscription statistics record table."
+ )},
+ {stats_type,
+ sc(
+ hoconsc:union([whole, internal, response]),
+ whole,
+ "The method to calculate the latency."
+ )}
].
desc("slow_subs") ->
diff --git a/apps/emqx_slow_subs/src/emqx_slow_subs_sup.erl b/apps/emqx_slow_subs/src/emqx_slow_subs_sup.erl
index c4c5625e0..5c94f594b 100644
--- a/apps/emqx_slow_subs/src/emqx_slow_subs_sup.erl
+++ b/apps/emqx_slow_subs/src/emqx_slow_subs_sup.erl
@@ -27,10 +27,14 @@ start_link() ->
init([]) ->
emqx_slow_subs:init_tab(),
- {ok, {{one_for_one, 10, 3600},
- [#{id => st_statistics,
- start => {emqx_slow_subs, start_link, []},
- restart => permanent,
- shutdown => 5000,
- type => worker,
- modules => [emqx_slow_subs]}]}}.
+ {ok,
+ {{one_for_one, 10, 3600}, [
+ #{
+ id => st_statistics,
+ start => {emqx_slow_subs, start_link, []},
+ restart => permanent,
+ shutdown => 5000,
+ type => worker,
+ modules => [emqx_slow_subs]
+ }
+ ]}}.
diff --git a/apps/emqx_slow_subs/test/emqx_slow_subs_SUITE.erl b/apps/emqx_slow_subs/test/emqx_slow_subs_SUITE.erl
index 94f17acd5..0547eb1f8 100644
--- a/apps/emqx_slow_subs/test/emqx_slow_subs_SUITE.erl
+++ b/apps/emqx_slow_subs/test/emqx_slow_subs_SUITE.erl
@@ -27,13 +27,17 @@
-define(NOW, erlang:system_time(millisecond)).
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
--define(BASE_CONF, <<"""
-slow_subs {
- enable = true
- top_k_num = 5,
- expire_interval = 5m
- stats_type = whole
- }""">>).
+-define(BASE_CONF, <<
+ ""
+ "\n"
+ "slow_subs {\n"
+ " enable = true\n"
+ " top_k_num = 5,\n"
+ " expire_interval = 5m\n"
+ " stats_type = whole\n"
+ " }"
+ ""
+>>).
all() ->
emqx_common_test_helpers:all(?MODULE).
@@ -46,7 +50,6 @@ init_per_suite(Config) ->
meck:expect(emqx_alarm, activate, 3, ok),
meck:expect(emqx_alarm, deactivate, 3, ok),
-
ok = emqx_common_test_helpers:load_config(emqx_slow_subs_schema, ?BASE_CONF),
emqx_common_test_helpers:start_apps([emqx_slow_subs]),
Config.
@@ -64,13 +67,13 @@ init_per_testcase(t_expire, Config) ->
Cfg = emqx_config:get([slow_subs]),
emqx_slow_subs:update_settings(Cfg#{expire_interval := 1500}),
Config;
-
init_per_testcase(_, Config) ->
Config.
end_per_testcase(_, _) ->
case erlang:whereis(node()) of
- undefined -> ok;
+ undefined ->
+ ok;
P ->
erlang:unlink(P),
erlang:exit(P, kill)
@@ -88,21 +91,25 @@ t_pub(_) ->
Now = ?NOW,
%% publish
- lists:foreach(fun(I) ->
- Topic = list_to_binary(io_lib:format("/test1/~p", [I])),
- Msg = emqx_message:make(undefined, ?QOS_1, Topic, <<"Hello">>),
- emqx:publish(Msg#message{timestamp = Now - 500}),
- timer:sleep(100)
- end,
- lists:seq(1, 10)),
+ lists:foreach(
+ fun(I) ->
+ Topic = list_to_binary(io_lib:format("/test1/~p", [I])),
+ Msg = emqx_message:make(undefined, ?QOS_1, Topic, <<"Hello">>),
+ emqx:publish(Msg#message{timestamp = Now - 500}),
+ timer:sleep(100)
+ end,
+ lists:seq(1, 10)
+ ),
- lists:foreach(fun(I) ->
- Topic = list_to_binary(io_lib:format("/test2/~p", [I])),
- Msg = emqx_message:make(undefined, ?QOS_2, Topic, <<"Hello">>),
- emqx:publish(Msg#message{timestamp = Now - 500}),
- timer:sleep(100)
- end,
- lists:seq(1, 10)),
+ lists:foreach(
+ fun(I) ->
+ Topic = list_to_binary(io_lib:format("/test2/~p", [I])),
+ Msg = emqx_message:make(undefined, ?QOS_2, Topic, <<"Hello">>),
+ emqx:publish(Msg#message{timestamp = Now - 500}),
+ timer:sleep(100)
+ end,
+ lists:seq(1, 10)
+ ),
timer:sleep(1000),
Size = ets:info(?TOPK_TAB, size),
@@ -114,10 +121,12 @@ t_pub(_) ->
t_expire(_) ->
Now = ?NOW,
Each = fun(I) ->
- ClientId = erlang:list_to_binary(io_lib:format("test_~p", [I])),
- ets:insert(?TOPK_TAB, #top_k{index = ?TOPK_INDEX(1, ?ID(ClientId, <<"topic">>)),
- last_update_time = Now - timer:minutes(5)})
- end,
+ ClientId = erlang:list_to_binary(io_lib:format("test_~p", [I])),
+ ets:insert(?TOPK_TAB, #top_k{
+ index = ?TOPK_INDEX(1, ?ID(ClientId, <<"topic">>)),
+ last_update_time = Now - timer:minutes(5)
+ })
+ end,
lists:foreach(Each, lists:seq(1, 5)),
@@ -130,10 +139,12 @@ start_client(Subs) ->
[spawn(fun() -> client(I, Subs) end) || I <- lists:seq(1, 10)].
client(I, Subs) ->
- {ok, C} = emqtt:start_link([{host, "localhost"},
- {clientid, io_lib:format("slow_subs_~p", [I])},
- {username, <<"plain">>},
- {password, <<"plain">>}]),
+ {ok, C} = emqtt:start_link([
+ {host, "localhost"},
+ {clientid, io_lib:format("slow_subs_~p", [I])},
+ {username, <<"plain">>},
+ {password, <<"plain">>}
+ ]),
{ok, _} = emqtt:connect(C),
Len = erlang:length(Subs),
diff --git a/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl b/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl
index 7cefbac21..3f5f63f29 100644
--- a/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl
+++ b/apps/emqx_slow_subs/test/emqx_slow_subs_api_SUITE.erl
@@ -34,15 +34,18 @@
-define(NOW, erlang:system_time(millisecond)).
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
--define(CONF_DEFAULT, <<"""
-slow_subs
-{
- enable = true
- top_k_num = 5,
- expire_interval = 60000
- stats_type = whole
-}""">>).
-
+-define(CONF_DEFAULT, <<
+ ""
+ "\n"
+ "slow_subs\n"
+ "{\n"
+ " enable = true\n"
+ " top_k_num = 5,\n"
+ " expire_interval = 60000\n"
+ " stats_type = whole\n"
+ "}"
+ ""
+>>).
all() ->
emqx_common_test_helpers:all(?MODULE).
@@ -79,7 +82,8 @@ init_per_testcase(_, Config) ->
end_per_testcase(_, Config) ->
application:stop(emqx_slow_subs),
case erlang:whereis(node()) of
- undefined -> ok;
+ undefined ->
+ ok;
P ->
erlang:unlink(P),
erlang:exit(P, kill)
@@ -89,50 +93,70 @@ end_per_testcase(_, Config) ->
t_get_history(_) ->
Now = ?NOW,
Each = fun(I) ->
- ClientId = erlang:list_to_binary(io_lib:format("test_~p", [I])),
- ets:insert(?TOPK_TAB, #top_k{index = ?TOPK_INDEX(1, ?ID(ClientId, <<"topic">>)),
- last_update_time = Now})
- end,
+ ClientId = erlang:list_to_binary(io_lib:format("test_~p", [I])),
+ ets:insert(?TOPK_TAB, #top_k{
+ index = ?TOPK_INDEX(1, ?ID(ClientId, <<"topic">>)),
+ last_update_time = Now
+ })
+ end,
lists:foreach(Each, lists:seq(1, 5)),
- {ok, Data} = request_api(get, api_path(["slow_subscriptions"]), "_page=1&_limit=10",
- auth_header_()),
+ {ok, Data} = request_api(
+ get,
+ api_path(["slow_subscriptions"]),
+ "page=1&limit=10",
+ auth_header_()
+ ),
#{<<"data">> := [First | _]} = emqx_json:decode(Data, [return_maps]),
- ?assertMatch(#{<<"clientid">> := <<"test_5">>,
- <<"topic">> := <<"topic">>,
- <<"last_update_time">> := Now,
- <<"node">> := _,
- <<"timespan">> := _}, First).
+ ?assertMatch(
+ #{
+ <<"clientid">> := <<"test_5">>,
+ <<"topic">> := <<"topic">>,
+ <<"last_update_time">> := Now,
+ <<"node">> := _,
+ <<"timespan">> := _
+ },
+ First
+ ).
t_clear(_) ->
- ets:insert(?TOPK_TAB, #top_k{index = ?TOPK_INDEX(1, ?ID(<<"clientid">>, <<"topic">>)),
- last_update_time = ?NOW}),
+ ets:insert(?TOPK_TAB, #top_k{
+ index = ?TOPK_INDEX(1, ?ID(<<"clientid">>, <<"topic">>)),
+ last_update_time = ?NOW
+ }),
- {ok, _} = request_api(delete, api_path(["slow_subscriptions"]), [],
- auth_header_()),
+ {ok, _} = request_api(
+ delete,
+ api_path(["slow_subscriptions"]),
+ [],
+ auth_header_()
+ ),
?assertEqual(0, ets:info(?TOPK_TAB, size)).
t_settting(_) ->
Conf = emqx:get_config([slow_subs]),
Conf2 = Conf#{stats_type => internal},
- {ok, Data} = request_api(put,
- api_path(["slow_subscriptions", "settings"]),
- [],
- auth_header_(),
- Conf2),
+ {ok, Data} = request_api(
+ put,
+ api_path(["slow_subscriptions", "settings"]),
+ [],
+ auth_header_(),
+ Conf2
+ ),
Return = decode_json(Data),
?assertEqual(Conf2#{stats_type := <<"internal">>}, Return),
- {ok, GetData} = request_api(get,
- api_path(["slow_subscriptions", "settings"]),
- [],
- auth_header_()
- ),
+ {ok, GetData} = request_api(
+ get,
+ api_path(["slow_subscriptions", "settings"]),
+ [],
+ auth_header_()
+ ),
timer:sleep(1000),
@@ -151,25 +175,28 @@ request_api(Method, Url, QueryParams, Auth) ->
request_api(Method, Url, QueryParams, Auth, []).
request_api(Method, Url, QueryParams, Auth, []) ->
- NewUrl = case QueryParams of
- "" -> Url;
- _ -> Url ++ "?" ++ QueryParams
- end,
+ NewUrl =
+ case QueryParams of
+ "" -> Url;
+ _ -> Url ++ "?" ++ QueryParams
+ end,
do_request_api(Method, {NewUrl, [Auth]});
request_api(Method, Url, QueryParams, Auth, Body) ->
- NewUrl = case QueryParams of
- "" -> Url;
- _ -> Url ++ "?" ++ QueryParams
- end,
+ NewUrl =
+ case QueryParams of
+ "" -> Url;
+ _ -> Url ++ "?" ++ QueryParams
+ end,
do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}).
-do_request_api(Method, Request)->
+do_request_api(Method, Request) ->
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], [{body_format, binary}]) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
- {ok, {{"HTTP/1.1", Code, _}, _, Return} }
- when Code =:= 200 orelse Code =:= 204 ->
+ {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
+ Code =:= 200 orelse Code =:= 204
+ ->
{ok, Return};
{ok, {Reason, _, _}} ->
{error, Reason}
@@ -181,8 +208,8 @@ auth_header_() ->
auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)).
auth_header_(User, Pass) ->
- Encoded = base64:encode_to_string(lists:append([User,":",Pass])),
- {"Authorization","Basic " ++ Encoded}.
+ Encoded = base64:encode_to_string(lists:append([User, ":", Pass])),
+ {"Authorization", "Basic " ++ Encoded}.
-api_path(Parts)->
+api_path(Parts) ->
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts).
diff --git a/apps/emqx_statsd/i18n/emqx_statsd_schema_i18n.conf b/apps/emqx_statsd/i18n/emqx_statsd_schema_i18n.conf
new file mode 100644
index 000000000..bc0e1d3b0
--- /dev/null
+++ b/apps/emqx_statsd/i18n/emqx_statsd_schema_i18n.conf
@@ -0,0 +1,41 @@
+emqx_statsd_schema {
+
+ statsd {
+ desc {
+ en: """Settings for reporting metrics to Statsd"""
+ zh: """Statsd 监控数据推送"""
+ }
+ label {
+ en: """Statsd"""
+ zh: """Statsd"""
+ }
+ }
+
+ server {
+ desc {
+ en: """URL of Statsd server"""
+ zh: """Statsd 服务器地址"""
+ }
+ }
+
+ sample_interval {
+ desc {
+ en: """Data collection interval in second."""
+ zh: """数据收集间隔,单位 毫秒"""
+ }
+ }
+
+ flush_interval {
+ desc {
+ en: """Data reporting interval, in second."""
+ zh: """数据推送间隔,单位 毫秒"""
+ }
+ }
+
+ enable {
+ desc {
+ en: """Turn Statsd data pushing on or off"""
+ zh: """开启或关闭 Statsd 数据推送"""
+ }
+ }
+}
diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl
index 7566e6255..df98e4df1 100644
--- a/apps/emqx_statsd/src/emqx_statsd_schema.erl
+++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl
@@ -16,6 +16,7 @@
-module(emqx_statsd_schema).
+-include_lib("hocon/include/hoconsc.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
@@ -37,34 +38,33 @@ fields("statsd") ->
[ {enable, hoconsc:mk(boolean(),
#{ default => false
, required => true
- , desc => "Enable statsd"
+ , desc => ?DESC(enable)
})}
, {server, fun server/1}
, {sample_time_interval, fun sample_interval/1}
, {flush_time_interval, fun flush_interval/1}
].
-desc("statsd") ->
- "Configuration related to reporting metrics to statsd.";
+desc("statsd") -> ?DESC(statsd);
desc(_) ->
undefined.
server(type) -> emqx_schema:ip_port();
server(required) -> true;
server(default) -> "127.0.0.1:8125";
-server(desc) -> "URL of the statsd gateway.";
+server(desc) -> ?DESC(?FUNCTION_NAME);
server(_) -> undefined.
sample_interval(type) -> emqx_schema:duration_ms();
sample_interval(required) -> true;
sample_interval(default) -> "10s";
-sample_interval(desc) -> "Data collection interval in milliseconds.";
+sample_interval(desc) -> ?DESC(?FUNCTION_NAME);
sample_interval(_) -> undefined.
flush_interval(type) -> emqx_schema:duration_ms();
flush_interval(required) -> true;
flush_interval(default) -> "10s";
-flush_interval(desc) -> "Flush interval in milliseconds.";
+flush_interval(desc) -> ?DESC(?FUNCTION_NAME);
flush_interval(_) -> undefined.
to_ip_port(Str) ->
diff --git a/build b/build
index 421fdd6bd..ac09f95fe 100755
--- a/build
+++ b/build
@@ -82,7 +82,8 @@ make_doc() {
# shellcheck disable=SC2086
erl -noshell -pa $libs_dir1 $libs_dir2 $libs_dir3 -eval \
"Dir = filename:join(['_build', '${PROFILE}', lib, emqx_dashboard, priv, www, static]), \
- ok = emqx_conf:dump_schema(Dir, $SCHEMA_MODULE), \
+ I18nFile = filename:join(['_build', '${PROFILE}', lib, emqx_dashboard, etc, 'i18n.conf.all']), \
+ ok = emqx_conf:dump_schema(Dir, $SCHEMA_MODULE, I18nFile), \
halt(0)."
}
diff --git a/git-blame-ignore-revs b/git-blame-ignore-revs
index 339c5398c..d76cf9165 100644
--- a/git-blame-ignore-revs
+++ b/git-blame-ignore-revs
@@ -19,3 +19,9 @@ acb3544d4b112121b5d9414237d2af7860ccc2a3
3f6d78dda03fd0d8e968a352e134f11a7f16bfe8
# reformat apps/emqx_exhook
1a4afabe9fed06bb0038f95c898be69c72b94bf3
+# reformat apps/emqx_retainer
+f1acfece6b79ed69b491da03783a7adaa7627b96
+# reformat apps/emqx_management
+aa7807baebfa5d8678025e43f386bcd9b3259d6a
+# reformat apps/emqx_slow_subs
+83511f8a4c1570a2c89d9c6c5b6f462520199ed8
diff --git a/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl b/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
index a24f3c3f7..10c5998db 100644
--- a/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
+++ b/lib-ee/emqx_enterprise_conf/test/emqx_enterprise_conf_schema_tests.erl
@@ -9,5 +9,6 @@
doc_gen_test() ->
Dir = "tmp",
ok = filelib:ensure_dir(filename:join("tmp", foo)),
- _ = emqx_conf:dump_schema(Dir, emqx_enterprise_conf_schema),
+ I18nFile = filename:join(["_build", "test", "lib", "emqx_dashboard", "etc", "i18n.conf.all"]),
+ _ = emqx_conf:dump_schema(Dir, emqx_enterprise_conf_schema, I18nFile),
ok.
diff --git a/mix.exs b/mix.exs
index e5d149ffa..95b74c11c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -48,7 +48,7 @@ defmodule EMQXUmbrella.MixProject do
[
{:lc, github: "emqx/lc", tag: "0.2.1"},
{:redbug, "2.0.7"},
- {:typerefl, github: "ieQu1/typerefl", tag: "0.8.6", override: true},
+ {:typerefl, github: "ieQu1/typerefl", tag: "0.9.0", override: true},
{:ehttpc, github: "emqx/ehttpc", tag: "0.1.12"},
{:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true},
{:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true},
@@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do
# in conflict by emqtt and hocon
{:getopt, "1.0.2", override: true},
{:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.18.0", override: true},
- {:hocon, github: "emqx/hocon", tag: "0.26.6", override: true},
+ {:hocon, github: "emqx/hocon", tag: "0.26.7", override: true},
{:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.4.1", override: true},
{:esasl, github: "emqx/esasl", tag: "0.2.0"},
{:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
@@ -145,6 +145,8 @@ defmodule EMQXUmbrella.MixProject do
:emqx_statsd,
:emqx_retainer,
:emqx_prometheus,
+ :emqx_auto_subscribe,
+ :emqx_slow_subs,
:emqx_plugins
],
steps: steps,
@@ -230,7 +232,9 @@ defmodule EMQXUmbrella.MixProject do
:emqx_exhook,
:emqx_authn,
:emqx_authz,
- :emqx_plugin
+ :emqx_auto_subscribe,
+ :emqx_slow_subs,
+ :emqx_plugins
]
end
@@ -342,6 +346,13 @@ defmodule EMQXUmbrella.MixProject do
Path.join(etc, "certs")
)
+ # required by emqx_dashboard
+ Mix.Generator.copy_file(
+ "apps/emqx_dashboard/etc/i18n.conf.all",
+ Path.join(etc, "i18n.conf"),
+ force: overwrite?
+ )
+
# this is required by the produced escript / nodetool
Mix.Generator.copy_file(
Path.join(release.version_path, "start_clean.boot"),
diff --git a/rebar.config b/rebar.config
index ef0af3580..75d156e01 100644
--- a/rebar.config
+++ b/rebar.config
@@ -47,7 +47,7 @@
[ {lc, {git, "https://github.com/emqx/lc.git", {tag, "0.2.1"}}}
, {redbug, "2.0.7"}
, {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
- , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.8.6"}}}
+ , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.0"}}}
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.12"}}}
, {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
@@ -66,7 +66,7 @@
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.2"}}}
, {getopt, "1.0.2"}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
- , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.6"}}}
+ , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.26.7"}}}
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.1"}}}
, {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}
diff --git a/rebar.config.erl b/rebar.config.erl
index 1cc18b434..8e022b44f 100644
--- a/rebar.config.erl
+++ b/rebar.config.erl
@@ -385,7 +385,9 @@ emqx_machine_boot_apps(ce) ->
, emqx_exhook
, emqx_authn
, emqx_authz
- , emqx_plugin
+ , emqx_slow_subs
+ , emqx_auto_subscribe
+ , emqx_plugins
];
emqx_machine_boot_apps(ee) ->
@@ -450,10 +452,12 @@ emqx_etc_overlay_common() ->
emqx_etc_overlay_per_edition(ce) ->
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
+ , {"{{base_dir}}/lib/emqx_dashboard/etc/i18n.conf.all", "etc/i18n.conf"}
];
emqx_etc_overlay_per_edition(ee) ->
[ {"{{base_dir}}/lib/emqx_conf/etc/emqx_enterprise.conf.all", "etc/emqx_enterprise.conf"}
, {"{{base_dir}}/lib/emqx_conf/etc/emqx.conf.all", "etc/emqx.conf"}
+ , {"{{base_dir}}/lib/emqx_dashboard/etc/i18n.conf.all", "etc/i18n.conf"}
].
get_vsn(Profile) ->
diff --git a/scripts/check-format.sh b/scripts/check-format.sh
index 8f22c648a..9b5ada731 100755
--- a/scripts/check-format.sh
+++ b/scripts/check-format.sh
@@ -12,6 +12,7 @@ APPS+=( 'apps/emqx' 'apps/emqx_modules' 'apps/emqx_gateway')
APPS+=( 'apps/emqx_authn' 'apps/emqx_authz' )
APPS+=( 'lib-ee/emqx_enterprise_conf' 'lib-ee/emqx_license' )
APPS+=( 'apps/emqx_exhook')
+APPS+=( 'apps/emqx_retainer' 'apps/emqx_slow_subs')
for app in "${APPS[@]}"; do
echo "$app ..."
diff --git a/scripts/merge-i18n.escript b/scripts/merge-i18n.escript
new file mode 100755
index 000000000..13a7df27b
--- /dev/null
+++ b/scripts/merge-i18n.escript
@@ -0,0 +1,60 @@
+#!/usr/bin/env escript
+
+-mode(compile).
+
+main(_) ->
+ {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf"),
+
+ Cfgs = get_all_cfgs("apps/"),
+ Conf = [merge(BaseConf, Cfgs),
+ io_lib:nl()
+ ],
+ ok = file:write_file("apps/emqx_dashboard/etc/i18n.conf.all", Conf).
+
+merge(BaseConf, Cfgs) ->
+ lists:foldl(
+ fun(CfgFile, Acc) ->
+ case filelib:is_regular(CfgFile) of
+ true ->
+ {ok, Bin1} = file:read_file(CfgFile),
+ [Acc, io_lib:nl(), Bin1];
+ false -> Acc
+ end
+ end, BaseConf, Cfgs).
+
+get_all_cfgs(Root) ->
+ Apps = filelib:wildcard("*", Root) -- ["emqx_machine", "emqx_dashboard"],
+ Dirs = [filename:join([Root, App]) || App <- Apps],
+ lists:foldl(fun get_cfgs/2, [], Dirs).
+
+get_all_cfgs(Dir, Cfgs) ->
+ Fun = fun(E, Acc) ->
+ Path = filename:join([Dir, E]),
+ get_cfgs(Path, Acc)
+ end,
+ lists:foldl(Fun, Cfgs, filelib:wildcard("*", Dir)).
+
+get_cfgs(Dir, Cfgs) ->
+ case filelib:is_dir(Dir) of
+ false ->
+ Cfgs;
+ _ ->
+ Files = filelib:wildcard("*", Dir),
+ case lists:member("i18n", Files) of
+ false ->
+ try_enter_child(Dir, Files, Cfgs);
+ true ->
+ EtcDir = filename:join([Dir, "i18n"]),
+ Confs = filelib:wildcard("*.conf", EtcDir),
+ NewCfgs = [filename:join([EtcDir, Name]) || Name <- Confs],
+ try_enter_child(Dir, Files, NewCfgs ++ Cfgs)
+ end
+ end.
+
+try_enter_child(Dir, Files, Cfgs) ->
+ case lists:member("src", Files) of
+ false ->
+ Cfgs;
+ true ->
+ get_all_cfgs(filename:join([Dir, "src"]), Cfgs)
+ end.