From fc391c7b9eda5de1eef40bf3c77b4f01a30a8245 Mon Sep 17 00:00:00 2001 From: firest Date: Fri, 22 Apr 2022 11:03:06 +0800 Subject: [PATCH 01/43] fix(delayed): unify and optimize the enable/disable codes --- apps/emqx_modules/src/emqx_delayed.erl | 62 ++++++++++--------- apps/emqx_modules/test/emqx_delayed_SUITE.erl | 2 +- .../test/emqx_delayed_api_SUITE.erl | 17 +++-- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index 8adf86e5d..479786eba 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -143,10 +143,10 @@ store(DelayedMsg) -> gen_server:call(?SERVER, {store, DelayedMsg}, infinity). enable() -> - gen_server:call(?SERVER, enable). + enable(true). disable() -> - gen_server:call(?SERVER, disable). + enable(false). set_max_delayed_messages(Max) -> gen_server:call(?SERVER, {set_max_delayed_messages, Max}). @@ -238,21 +238,7 @@ update_config(Config) -> emqx_conf:update([delayed], Config, #{rawconf_with_defaults => true, override_to => cluster}). post_config_update(_KeyPath, Config, _NewConf, _OldConf, _AppEnvs) -> - case maps:get(<<"enable">>, Config, undefined) of - undefined -> - ignore; - true -> - emqx_delayed:enable(); - false -> - emqx_delayed:disable() - end, - case maps:get(<<"max_delayed_messages">>, Config, undefined) of - undefined -> - ignore; - Max -> - ok = emqx_delayed:set_max_delayed_messages(Max) - end, - ok. + gen_server:call(?SERVER, {update_config, Config}). %%-------------------------------------------------------------------- %% gen_server callback @@ -262,7 +248,7 @@ init([Opts]) -> erlang:process_flag(trap_exit, true), emqx_conf:add_handler([delayed], ?MODULE), MaxDelayedMessages = maps:get(max_delayed_messages, Opts, 0), - {ok, + State = ensure_stats_event( ensure_publish_timer(#{ publish_timer => undefined, @@ -271,7 +257,8 @@ init([Opts]) -> stats_fun => undefined, max_delayed_messages => MaxDelayedMessages }) - )}. + ), + {ok, ensure_enable(emqx:get_config([delayed, enable]), State)}. handle_call({set_max_delayed_messages, Max}, _From, State) -> {reply, ok, State#{max_delayed_messages => Max}}; @@ -293,12 +280,11 @@ handle_call( emqx_metrics:inc('messages.delayed'), {reply, ok, ensure_publish_timer(Key, State)} end; -handle_call(enable, _From, State) -> - emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}), - {reply, ok, State}; -handle_call(disable, _From, State) -> - emqx_hooks:del('message.publish', {?MODULE, on_message_publish}), - {reply, ok, State}; +handle_call({update_config, Config}, _From, #{max_delayed_messages := Max} = State) -> + Max2 = maps:get(<<"max_delayed_messages">>, Config, Max), + State2 = State#{max_delayed_messages := Max2}, + State3 = ensure_enable(maps:get(<<"enable">>, Config, undefined), State2), + {reply, ok, State3}; handle_call(Req, _From, State) -> ?tp(error, emqx_delayed_unexpected_call, #{call => Req}), {reply, ignored, State}. @@ -320,10 +306,10 @@ handle_info(Info, State) -> ?tp(error, emqx_delayed_unexpected_info, #{info => Info}), {noreply, State}. -terminate(_Reason, #{publish_timer := PublishTimer, stats_timer := StatsTimer}) -> +terminate(_Reason, #{stats_timer := StatsTimer} = State) -> emqx_conf:remove_handler([delayed]), - emqx_misc:cancel_timer(PublishTimer), - emqx_misc:cancel_timer(StatsTimer). + emqx_misc:cancel_timer(StatsTimer), + ensure_enable(false, State). code_change(_Vsn, State, _Extra) -> {ok, State}. @@ -378,3 +364,23 @@ do_publish(Key = {Ts, _Id}, Now, Acc) when Ts =< Now -> -spec delayed_count() -> non_neg_integer(). delayed_count() -> mnesia:table_info(?TAB, size). + +enable(Enable) -> + case emqx:get_raw_config([delayed]) of + #{<<"enable">> := Enable} -> + ok; + Cfg -> + {ok, _} = update_config(Cfg#{<<"enable">> := Enable}), + ok + end. + +ensure_enable(true, State) -> + emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}), + State; +ensure_enable(false, #{publish_timer := PubTimer} = State) -> + emqx_hooks:del('message.publish', {?MODULE, on_message_publish}), + emqx_misc:cancel_timer(PubTimer), + ets:delete_all_objects(?TAB), + State#{publish_timer := undefined, publish_at := 0}; +ensure_enable(_, State) -> + State. diff --git a/apps/emqx_modules/test/emqx_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_SUITE.erl index c582a9722..a6b4d85b5 100644 --- a/apps/emqx_modules/test/emqx_delayed_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_SUITE.erl @@ -76,7 +76,7 @@ t_delayed_message(_) -> [#delayed_message{msg = #message{payload = Payload}}] = ets:tab2list(emqx_delayed), ?assertEqual(<<"delayed_m">>, Payload), - ct:sleep(2000), + ct:sleep(2500), EmptyKey = mnesia:dirty_all_keys(emqx_delayed), ?assertEqual([], EmptyKey). diff --git a/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl index 41c1e10b9..590fe24e6 100644 --- a/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_api_SUITE.erl @@ -98,6 +98,7 @@ t_status(_Config) -> t_messages(_) -> clear_all_record(), + emqx_delayed:enable(), {ok, C1} = emqtt:start_link([{clean_start, true}]), {ok, _} = emqtt:connect(C1), @@ -114,7 +115,7 @@ t_messages(_) -> end, lists:foreach(Each, lists:seq(1, 5)), - timer:sleep(500), + timer:sleep(1000), Msgs = get_messages(5), [First | _] = Msgs, @@ -197,6 +198,7 @@ t_messages(_) -> t_large_payload(_) -> clear_all_record(), + emqx_delayed:enable(), {ok, C1} = emqtt:start_link([{clean_start, true}]), {ok, _} = emqtt:connect(C1), @@ -209,7 +211,7 @@ t_large_payload(_) -> [{qos, 0}, {retain, true}] ), - timer:sleep(500), + timer:sleep(1000), [#{msgid := MsgId}] = get_messages(1), @@ -241,8 +243,13 @@ get_messages(Len) -> {ok, 200, MsgsJson} = request(get, uri(["mqtt", "delayed", "messages"])), #{data := Msgs} = decode_json(MsgsJson), MsgLen = erlang:length(Msgs), - ?assert( - MsgLen =:= Len, - lists:flatten(io_lib:format("message length is:~p~n", [MsgLen])) + ?assertEqual( + Len, + MsgLen, + lists:flatten( + io_lib:format("message length is:~p~nWhere:~p~nHooks:~p~n", [ + MsgLen, erlang:whereis(emqx_delayed), ets:tab2list(emqx_hooks) + ]) + ) ), Msgs. From 3858c2353ae48ce8afaf7665e9ccb0e847720b70 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 26 Apr 2022 14:21:52 +0800 Subject: [PATCH 02/43] chore(i18n): translate jwt fields to zh --- apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf index b08563267..5495dde49 100644 --- a/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf +++ b/apps/emqx_authz/i18n/emqx_authz_schema_i18n.conf @@ -343,7 +343,7 @@ Commands can support following wildcards:\n jwt { desc { en: """Authorization using ACL rules from authentication JWT.""" - zh: """Authorization using ACL rules from authentication JWT.""" + zh: """使用 JWT 登录认证中携带的 ACL 规则来进行发布和订阅的授权。""" } label { en: """jwt""" From 6b969c5c84ea223d33d1a63a681e5bb014418c99 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 25 Apr 2022 18:22:07 -0300 Subject: [PATCH 03/43] fix(emqx_conf): avoid crash/deadlock depending on node startup order Depending on the order that a cluster of core nodes might be stopped and then restarted, there might be a deadlock or crash during the configuration loading. The nodes try to check with each other which has the latest cluster overrides and copy that info. However, in some cases, Mnesia on node A might still think that it needs to consult another node C that is still down, so that this node proceeds with its boot sequence but `mnesia:wait_for_tables` in `emqx_cluster_rpc` is stuck. Meanwhile, a node B can come up, try to copy from the sole online node A, and fail because it's not ready yet. --- apps/emqx_conf/src/emqx_cluster_rpc.erl | 25 +++ apps/emqx_conf/src/emqx_conf_app.erl | 166 ++++++++++----- apps/emqx_conf/test/emqx_conf_app_SUITE.erl | 222 ++++++++++++++++++++ 3 files changed, 355 insertions(+), 58 deletions(-) create mode 100644 apps/emqx_conf/test/emqx_conf_app_SUITE.erl diff --git a/apps/emqx_conf/src/emqx_cluster_rpc.erl b/apps/emqx_conf/src/emqx_cluster_rpc.erl index e3b824d75..5be474601 100644 --- a/apps/emqx_conf/src/emqx_cluster_rpc.erl +++ b/apps/emqx_conf/src/emqx_cluster_rpc.erl @@ -42,6 +42,8 @@ code_change/3 ]). +-export([get_tables_status/0]). + -export_type([txn_id/0, succeed_num/0, multicall_return/1, multicall_return/0]). -ifdef(TEST). @@ -172,6 +174,29 @@ get_node_tnx_id(Node) -> [#cluster_rpc_commit{tnx_id = TnxId}] -> TnxId end. +%% Checks whether the Mnesia tables used by this module are waiting to +%% be loaded and from where. +-spec get_tables_status() -> #{atom() => {waiting, [node()]} | {disc | network, node()}}. +get_tables_status() -> + maps:from_list([ + {Tab, do_get_tables_status(Tab)} + || Tab <- [?CLUSTER_COMMIT, ?CLUSTER_MFA] + ]). + +do_get_tables_status(Tab) -> + Props = mnesia:table_info(Tab, all), + TabNodes = proplists:get_value(all_nodes, Props), + KnownDown = mnesia_recover:get_mnesia_downs(), + LocalNode = node(), + case proplists:get_value(load_node, Props) of + unknown -> + {waiting, TabNodes -- [LocalNode | KnownDown]}; + LocalNode -> + {disc, LocalNode}; + Node -> + {network, Node} + end. + %% Regardless of what MFA is returned, consider it a success), %% then move to the next tnxId. %% if the next TnxId failed, need call the function again to skip. diff --git a/apps/emqx_conf/src/emqx_conf_app.erl b/apps/emqx_conf/src/emqx_conf_app.erl index 81d0481df..2115bc617 100644 --- a/apps/emqx_conf/src/emqx_conf_app.erl +++ b/apps/emqx_conf/src/emqx_conf_app.erl @@ -31,64 +31,6 @@ start(_StartType, _StartArgs) -> stop(_State) -> ok. -%% internal functions -init_conf() -> - {ok, TnxId} = copy_override_conf_from_core_node(), - emqx_app:set_init_tnx_id(TnxId), - emqx_config:init_load(emqx_conf:schema_module()), - emqx_app:set_init_config_load_done(). - -copy_override_conf_from_core_node() -> - case mria_mnesia:running_nodes() -- [node()] of - %% The first core nodes is self. - [] -> - ?SLOG(debug, #{msg => "skip_copy_overide_conf_from_core_node"}), - {ok, -1}; - Nodes -> - {Results, Failed} = emqx_conf_proto_v1:get_override_config_file(Nodes), - {Ready, NotReady0} = lists:partition(fun(Res) -> element(1, Res) =:= ok end, Results), - NotReady = lists:filter(fun(Res) -> element(1, Res) =:= error end, NotReady0), - case (Failed =/= [] orelse NotReady =/= []) andalso Ready =/= [] of - true -> - Warning = #{ - nodes => Nodes, - failed => Failed, - not_ready => NotReady, - msg => "ignored_bad_nodes_when_copy_init_config" - }, - ?SLOG(warning, Warning); - false -> - ok - end, - case Ready of - [] -> - %% Other core nodes running but no one replicated it successfully. - ?SLOG(error, #{ - msg => "copy_overide_conf_from_core_node_failed", - nodes => Nodes, - failed => Failed, - not_ready => NotReady - }), - {error, "core node not ready"}; - _ -> - SortFun = fun( - {ok, #{wall_clock := W1}}, - {ok, #{wall_clock := W2}} - ) -> - W1 > W2 - end, - [{ok, Info} | _] = lists:sort(SortFun, Ready), - #{node := Node, conf := RawOverrideConf, tnx_id := TnxId} = Info, - Msg = #{msg => "copy_overide_conf_from_core_node_success", node => Node}, - ?SLOG(debug, Msg), - ok = emqx_config:save_to_override_conf( - RawOverrideConf, - #{override_to => cluster} - ), - {ok, TnxId} - end - end. - get_override_config_file() -> Node = node(), Role = mria_rlog:role(), @@ -114,3 +56,111 @@ get_override_config_file() -> true when Role =:= replicant -> {ignore, #{node => Node}} end. + +%% ------------------------------------------------------------------------------ +%% Internal functions +%% ------------------------------------------------------------------------------ + +init_conf() -> + {ok, TnxId} = copy_override_conf_from_core_node(), + emqx_app:set_init_tnx_id(TnxId), + emqx_config:init_load(emqx_conf:schema_module()), + emqx_app:set_init_config_load_done(). + +cluster_nodes() -> + maps:get(running_nodes, ekka_cluster:info()) -- [node()]. + +copy_override_conf_from_core_node() -> + case cluster_nodes() of + %% The first core nodes is self. + [] -> + ?SLOG(debug, #{msg => "skip_copy_overide_conf_from_core_node"}), + {ok, ?DEFAULT_INIT_TXN_ID}; + Nodes -> + {Results, Failed} = emqx_conf_proto_v1:get_override_config_file(Nodes), + {Ready, NotReady0} = lists:partition(fun(Res) -> element(1, Res) =:= ok end, Results), + NotReady = lists:filter(fun(Res) -> element(1, Res) =:= error end, NotReady0), + case (Failed =/= [] orelse NotReady =/= []) andalso Ready =/= [] of + true -> + Warning = #{ + nodes => Nodes, + failed => Failed, + not_ready => NotReady, + msg => "ignored_bad_nodes_when_copy_init_config" + }, + ?SLOG(warning, Warning); + false -> + ok + end, + case Ready of + [] -> + %% Other core nodes running but no one replicated it successfully. + ?SLOG(error, #{ + msg => "copy_override_conf_from_core_node_failed", + nodes => Nodes, + failed => Failed, + not_ready => NotReady + }), + + case should_proceed_with_boot() of + true -> + %% Act as if this node is alone, so it can + %% finish the boot sequence and load the + %% config for other nodes to copy it. + ?SLOG(info, #{ + msg => "skip_copy_overide_conf_from_core_node", + loading_from_disk => true, + nodes => Nodes, + failed => Failed, + not_ready => NotReady + }), + {ok, ?DEFAULT_INIT_TXN_ID}; + false -> + %% retry in some time + Jitter = rand:uniform(2_000), + Timeout = 10_000 + Jitter, + ?SLOG(info, #{ + msg => "copy_overide_conf_from_core_node_retry", + timeout => Timeout, + nodes => Nodes, + failed => Failed, + not_ready => NotReady + }), + timer:sleep(Timeout), + copy_override_conf_from_core_node() + end; + _ -> + SortFun = fun( + {ok, #{wall_clock := W1}}, + {ok, #{wall_clock := W2}} + ) -> + W1 > W2 + end, + [{ok, Info} | _] = lists:sort(SortFun, Ready), + #{node := Node, conf := RawOverrideConf, tnx_id := TnxId} = Info, + Msg = #{ + msg => "copy_overide_conf_from_core_node_success", + node => Node + }, + ?SLOG(debug, Msg), + ok = emqx_config:save_to_override_conf( + RawOverrideConf, + #{override_to => cluster} + ), + {ok, TnxId} + end + end. + +should_proceed_with_boot() -> + TablesStatus = emqx_cluster_rpc:get_tables_status(), + LocalNode = node(), + case maps:get(?CLUSTER_COMMIT, TablesStatus) of + {disc, LocalNode} -> + %% Loading locally; let this node finish its boot sequence + %% so others can copy the config from this one. + true; + _ -> + %% Loading from another node or still waiting for nodes to + %% be up. Try again. + false + end. diff --git a/apps/emqx_conf/test/emqx_conf_app_SUITE.erl b/apps/emqx_conf/test/emqx_conf_app_SUITE.erl new file mode 100644 index 000000000..29b1404e8 --- /dev/null +++ b/apps/emqx_conf/test/emqx_conf_app_SUITE.erl @@ -0,0 +1,222 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 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_conf_app_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_conf.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("snabbkaffe/include/snabbkaffe.hrl"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +t_copy_conf_override_on_restarts(_Config) -> + ct:timetrap({seconds, 120}), + snabbkaffe:fix_ct_logging(), + Cluster = cluster([core, core, core]), + try + %% 1. Start all nodes + Nodes = start_cluster(Cluster), + [join_cluster(Spec) || Spec <- Cluster], + assert_config_load_done(Nodes), + + %% 2. Stop each in order. + lists:foreach(fun stop_slave/1, Nodes), + + %% 3. Restart nodes in the same order. This should not + %% crash and eventually all nodes should be ready. + start_cluster_async(Cluster), + + timer:sleep(15_000), + + assert_config_load_done(Nodes), + + ok + after + teardown_cluster(Cluster) + end. + +%%------------------------------------------------------------------------------ +%% Helper functions +%%------------------------------------------------------------------------------ + +assert_config_load_done(Nodes) -> + lists:foreach( + fun(Node) -> + Done = rpc:call(Node, emqx_app, get_init_config_load_done, []), + ?assert(Done, #{node => Node}) + end, + Nodes + ). + +start_cluster(Specs) -> + [start_slave(I) || I <- Specs]. + +start_cluster_async(Specs) -> + [ + begin + spawn_link(fun() -> start_slave(I) end), + timer:sleep(7_000) + end + || I <- Specs + ]. + +cluster(Specs) -> + cluster(Specs, []). + +cluster(Specs0, CommonEnv) -> + Specs1 = lists:zip(Specs0, lists:seq(1, length(Specs0))), + Specs = expand_node_specs(Specs1, CommonEnv), + CoreNodes = [node_id(Name) || {{core, Name, _}, _} <- Specs], + %% Assign grpc ports: + BaseGenRpcPort = 9000, + GenRpcPorts = maps:from_list([ + {node_id(Name), {tcp, BaseGenRpcPort + Num}} + || {{_, Name, _}, Num} <- Specs + ]), + %% Set the default node of the cluster: + JoinTo = + case CoreNodes of + [First | _] -> #{join_to => First}; + _ -> #{} + end, + [ + JoinTo#{ + name => Name, + node => node_id(Name), + env => [ + {mria, core_nodes, CoreNodes}, + {mria, node_role, Role}, + {gen_rpc, tcp_server_port, BaseGenRpcPort + Number}, + {gen_rpc, client_config_per_node, {internal, GenRpcPorts}} + | Env + ], + number => Number, + role => Role + } + || {{Role, Name, Env}, Number} <- Specs + ]. + +start_apps(Node) -> + Handler = fun + (emqx) -> + application:set_env(emqx, boot_modules, []), + ok; + (_) -> + ok + end, + {Node, ok} = + {Node, rpc:call(Node, emqx_common_test_helpers, start_apps, [[emqx_conf], Handler])}, + ok. + +stop_apps(Node) -> + ok = rpc:call(Node, emqx_common_test_helpers, stop_apps, [[emqx_conf]]). + +join_cluster(#{node := Node, join_to := JoinTo}) -> + case rpc:call(Node, ekka, join, [JoinTo]) of + ok -> ok; + ignore -> ok; + Err -> error({failed_to_join_cluster, #{node => Node, error => Err}}) + end. + +start_slave(#{node := Node, env := Env}) -> + %% We want VMs to only occupy a single core + CommonBeamOpts = + "+S 1:1 " ++ + %% redirect logs to the master test node + " -master " ++ atom_to_list(node()) ++ " ", + %% We use `ct_slave' instead of `slave' because, in + %% `t_copy_conf_override_on_restarts', the nodes might be stuck + %% some time during boot up, and `slave' has a hard-coded boot + %% timeout. + {ok, Node} = ct_slave:start( + Node, + [ + {erl_flags, CommonBeamOpts ++ ebin_path()}, + {kill_if_fail, true}, + {monitor_master, true}, + {init_timeout, 30_000}, + {startup_timeout, 30_000} + ] + ), + + %% Load apps before setting the enviroment variables to avoid + %% overriding the environment during app start: + [rpc:call(Node, application, load, [App]) || App <- [gen_rpc]], + %% Disable gen_rpc listener by default: + Env1 = [{gen_rpc, tcp_server_port, false} | Env], + setenv(Node, Env1), + ok = start_apps(Node), + Node. + +expand_node_specs(Specs, CommonEnv) -> + lists:map( + fun({Spec, Num}) -> + { + case Spec of + core -> + {core, gen_node_name(Num), CommonEnv}; + replicant -> + {replicant, gen_node_name(Num), CommonEnv}; + {Role, Name} when is_atom(Name) -> + {Role, Name, CommonEnv}; + {Role, Env} when is_list(Env) -> + {Role, gen_node_name(Num), CommonEnv ++ Env}; + {Role, Name, Env} -> + {Role, Name, CommonEnv ++ Env} + end, + Num + } + end, + Specs + ). + +setenv(Node, Env) -> + [rpc:call(Node, application, set_env, [App, Key, Val]) || {App, Key, Val} <- Env]. + +teardown_cluster(Specs) -> + Nodes = [I || #{node := I} <- Specs], + [rpc:call(I, emqx_common_test_helpers, stop_apps, [emqx_conf]) || I <- Nodes], + [stop_slave(I) || I <- Nodes], + ok. + +stop_slave(Node) -> + ct_slave:stop(Node). + +host() -> + [_, Host] = string:tokens(atom_to_list(node()), "@"), + Host. + +node_id(Name) -> + list_to_atom(lists:concat([Name, "@", host()])). + +gen_node_name(N) -> + list_to_atom("n" ++ integer_to_list(N)). + +ebin_path() -> + string:join(["-pa" | paths()], " "). + +paths() -> + [ + Path + || Path <- code:get_path(), + string:prefix(Path, code:lib_dir()) =:= nomatch, + string:str(Path, "_build/default/plugins") =:= 0 + ]. From 8519948742f7d3af5b571389531441bc39d54499 Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Mon, 25 Apr 2022 09:09:45 -0300 Subject: [PATCH 04/43] refactor: use macro to denote initial transaction id --- apps/emqx_conf/src/emqx_conf_app.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/emqx_conf/src/emqx_conf_app.erl b/apps/emqx_conf/src/emqx_conf_app.erl index 2115bc617..8b2b81cb4 100644 --- a/apps/emqx_conf/src/emqx_conf_app.erl +++ b/apps/emqx_conf/src/emqx_conf_app.erl @@ -24,6 +24,8 @@ -include_lib("emqx/include/logger.hrl"). -include("emqx_conf.hrl"). +-define(DEFAULT_INIT_TXN_ID, -1). + start(_StartType, _StartArgs) -> init_conf(), emqx_conf_sup:start_link(). From 67ed7ba7b8f7e0ce0437e9ad2d44e591c3ddf27b Mon Sep 17 00:00:00 2001 From: Thales Macedo Garitezi Date: Tue, 26 Apr 2022 12:14:38 -0300 Subject: [PATCH 05/43] refactor: do not differentiate node roles --- apps/emqx_conf/src/emqx_conf_app.erl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/emqx_conf/src/emqx_conf_app.erl b/apps/emqx_conf/src/emqx_conf_app.erl index 8b2b81cb4..1ad460043 100644 --- a/apps/emqx_conf/src/emqx_conf_app.erl +++ b/apps/emqx_conf/src/emqx_conf_app.erl @@ -35,11 +35,10 @@ stop(_State) -> get_override_config_file() -> Node = node(), - Role = mria_rlog:role(), case emqx_app:get_init_config_load_done() of false -> {error, #{node => Node, msg => "init_conf_load_not_done"}}; - true when Role =:= core -> + true -> case erlang:whereis(emqx_config_handler) of undefined -> {error, #{node => Node, msg => "emqx_config_handler_not_ready"}}; @@ -54,9 +53,7 @@ get_override_config_file() -> {atomic, Res} -> {ok, Res}; {aborted, Reason} -> {error, #{node => Node, msg => Reason}} end - end; - true when Role =:= replicant -> - {ignore, #{node => Node}} + end end. %% ------------------------------------------------------------------------------ From 1fac70d2bb8b67567ac9b51a07e058191a8f6e44 Mon Sep 17 00:00:00 2001 From: EMQ-YangM Date: Wed, 27 Apr 2022 09:48:57 +0800 Subject: [PATCH 06/43] fix: remove error field --- .../emqx_rule_engine/src/emqx_rule_events.erl | 4 +-- .../test/emqx_rule_engine_SUITE.erl | 36 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 1a4562c3e..91c90f3e2 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -258,8 +258,7 @@ eventmsg_connack(ConnInfo = #{ peername := PeerName, sockname := SockName, proto_name := ProtoName, - proto_ver := ProtoVer, - connected_at := ConnectedAt + proto_ver := ProtoVer }, Reason) -> Keepalive = maps:get(keepalive, ConnInfo, 0), ConnProps = maps:get(conn_props, ConnInfo, #{}), @@ -275,7 +274,6 @@ eventmsg_connack(ConnInfo = #{ proto_ver => ProtoVer, keepalive => Keepalive, expiry_interval => ExpiryInterval, - connected_at => ConnectedAt, conn_props => printable_maps(ConnProps) }). diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index 1007bc2d2..0abd47a59 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -366,6 +366,28 @@ t_events(_Config) -> session_unsubscribed(Client2), ct:pal("====== verify $events/client_disconnected"), client_disconnected(Client, Client2), + ct:pal("====== verify $events/client_connack"), + client_connack_failed(), + ok. + +client_connack_failed() -> + {ok, Client} = emqtt:start_link( + [ {username, <<"u_event3">>} + , {clientid, <<"c_event3">>} + , {proto_ver, v5} + , {properties, #{'Session-Expiry-Interval' => 60}} + ]), + try + meck:new(emqx_access_control, [non_strict, passthrough]), + meck:expect(emqx_access_control, authenticate, + fun(_) -> {error, bad_username_or_password} end), + process_flag(trap_exit, true), + ?assertMatch({error, _}, emqtt:connect(Client)), + timer:sleep(300), + verify_event('client.connack') + after + meck:unload(emqx_access_control) + end, ok. message_publish(Client) -> @@ -1745,14 +1767,14 @@ verify_event_fields('client.connack', Fields) -> keepalive := Keepalive, expiry_interval := ExpiryInterval, conn_props := Properties, - timestamp := Timestamp, - connected_at := EventAt + reason_code := Reason, + timestamp := Timestamp } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, - RcvdAtElapse = Now - EventAt, - ?assert(lists:member(ClientId, [<<"c_event">>, <<"c_event2">>])), - ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>])), + ?assert(lists:member(Reason, [success, bad_username_or_password])), + ?assert(lists:member(ClientId, [<<"c_event">>, <<"c_event2">>, <<"c_event3">>])), + ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>, <<"u_event3">>])), verify_peername(PeerName), verify_peername(SockName), ?assertEqual(<<"MQTT">>, ProtoName), @@ -1761,9 +1783,7 @@ verify_event_fields('client.connack', Fields) -> ?assert(is_boolean(CleanStart)), ?assertEqual(60000, ExpiryInterval), ?assertMatch(#{'Session-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), - ?assert(EventAt =< Timestamp); + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000); verify_event_fields('client.check_authz_complete', Fields) -> #{clientid := ClientId, From 0d335456a910d3c3b661e21614b9a062487852a9 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:04:28 +0800 Subject: [PATCH 07/43] feat: app api support show expired --- .../emqx_management/src/emqx_mgmt_api_app.erl | 3 ++- apps/emqx_management/src/emqx_mgmt_auth.erl | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_app.erl b/apps/emqx_management/src/emqx_mgmt_api_app.erl index 3668a4aea..5649b1ca0 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_app.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_app.erl @@ -141,7 +141,8 @@ fields(app) -> binary(), #{example => <<"Note">>, required => false} )}, - {enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})} + {enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}, + {expired, hoconsc:mk(boolean(), #{desc => "Expired", required => false})} ]; fields(name) -> [ diff --git a/apps/emqx_management/src/emqx_mgmt_auth.erl b/apps/emqx_management/src/emqx_mgmt_auth.erl index cb9c4e7ab..6a8ca561f 100644 --- a/apps/emqx_management/src/emqx_mgmt_auth.erl +++ b/apps/emqx_management/src/emqx_mgmt_auth.erl @@ -129,17 +129,20 @@ ensure_not_undefined(undefined, Old) -> Old; ensure_not_undefined(New, _Old) -> New. 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 - ); -to_map(App0) -> - [App] = to_map([App0]), - App. + [to_map(App) || App <- Apps]; +to_map(#?APP{name = N, api_key = K, enable = E, expired_at = ET, created_at = CT, desc = D}) -> + #{ + name => N, + api_key => K, + enable => E, + expired_at => ET, + created_at => CT, + desc => D, + expired => is_expired(ET) + }. + +is_expired(undefined) -> false; +is_expired(ExpiredTime) -> ExpiredTime < erlang:system_time(second). create_app(Name, Enable, ExpiredAt, Desc) -> ApiSecret = generate_api_secret(), From ba800d853d37d8b4c054d95abfdf036f59bcccea Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 26 Apr 2022 18:19:40 +0800 Subject: [PATCH 08/43] fix(rule): republish all available fields if payload template empty --- apps/emqx_rule_engine/src/emqx_rule_outputs.erl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/emqx_rule_engine/src/emqx_rule_outputs.erl b/apps/emqx_rule_engine/src/emqx_rule_outputs.erl index c0f685d4e..6a858a73b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_outputs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_outputs.erl @@ -82,7 +82,7 @@ republish(Selected, #{flags := Flags, metadata := #{rule_id := RuleId}}, topic := TopicTks, payload := PayloadTks}}) -> Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected), - Payload = emqx_plugin_libs_rule:proc_tmpl(PayloadTks, Selected), + Payload = format_msg(PayloadTks, Selected), QoS = replace_simple_var(QoSTks, Selected, 0), Retain = replace_simple_var(RetainTks, Selected, false), ?TRACE("RULE", "republish_message", #{topic => Topic, payload => Payload}), @@ -96,7 +96,7 @@ republish(Selected, #{metadata := #{rule_id := RuleId}}, topic := TopicTks, payload := PayloadTks}}) -> Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected), - Payload = emqx_plugin_libs_rule:proc_tmpl(PayloadTks, Selected), + Payload = format_msg(PayloadTks, Selected), QoS = replace_simple_var(QoSTks, Selected, 0), Retain = replace_simple_var(RetainTks, Selected, false), ?TRACE("RULE", "republish_message_with_flags", #{topic => Topic, payload => Payload}), @@ -163,3 +163,8 @@ replace_simple_var(Tokens, Data, Default) when is_list(Tokens) -> end; replace_simple_var(Val, _Data, _Default) -> Val. + +format_msg([], Selected) -> + emqx_json:encode(Selected); +format_msg(Tokens, Selected) -> + emqx_plugin_libs_rule:proc_tmpl(Tokens, Selected). From 94e24c262177d0a0d8636aeb8630b1937608090d Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 27 Apr 2022 01:02:57 +0800 Subject: [PATCH 09/43] refactor: move ssl file handling from resources to bridges --- apps/emqx_bridge/src/emqx_bridge_app.erl | 15 ++++++-- apps/emqx_connector/src/emqx_connector.erl | 21 +++++++---- .../src/emqx_connector_ssl.erl} | 35 +++++++++---------- .../src/emqx_resource_instance.erl | 29 +++------------ 4 files changed, 50 insertions(+), 50 deletions(-) rename apps/{emqx_resource/src/emqx_resource_ssl.erl => emqx_connector/src/emqx_connector_ssl.erl} (60%) diff --git a/apps/emqx_bridge/src/emqx_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl index b02fe2a9c..99b2c4a84 100644 --- a/apps/emqx_bridge/src/emqx_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -20,6 +20,7 @@ -export([start/2, stop/1]). -export([ pre_config_update/3 + , post_config_update/5 ]). -define(TOP_LELVE_HDLR_PATH, (emqx_bridge:config_key_path())). @@ -46,8 +47,18 @@ pre_config_update(_, {_Oper, _, _}, undefined) -> pre_config_update(_, {Oper, _Type, _Name}, OldConfig) -> %% to save the 'enable' to the config files {ok, OldConfig#{<<"enable">> => operation_to_enable(Oper)}}; -pre_config_update(_, Conf, _OldConfig) when is_map(Conf) -> - {ok, Conf}. +pre_config_update(Path, Conf, _OldConfig) when is_map(Conf) -> + case emqx_connector_ssl:convert_certs(filename:join(Path), Conf) of + {error, Reason} -> + {error, Reason}; + {ok, ConfNew} -> + {ok, ConfNew} + end. + +post_config_update(Path, '$remove', _, OldConf, _AppEnvs) -> + _ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf); +post_config_update(_Path, _Req, _, _OldConf, _AppEnvs) -> + ok. %% internal functions operation_to_enable(disable) -> false; diff --git a/apps/emqx_connector/src/emqx_connector.erl b/apps/emqx_connector/src/emqx_connector.erl index 16684466f..0e17971e1 100644 --- a/apps/emqx_connector/src/emqx_connector.erl +++ b/apps/emqx_connector/src/emqx_connector.erl @@ -15,7 +15,10 @@ %%-------------------------------------------------------------------- -module(emqx_connector). --export([config_key_path/0]). +-export([ config_key_path/0 + , pre_config_update/3 + , post_config_update/5 + ]). -export([ parse_connector_id/1 , connector_id/2 @@ -31,20 +34,26 @@ , delete/2 ]). --export([ post_config_update/5 - ]). - config_key_path() -> [connectors]. +pre_config_update(Path, Conf, _OldConfig) when is_map(Conf) -> + case emqx_connector_ssl:convert_certs(filename:join(Path), Conf) of + {error, Reason} -> + {error, Reason}; + {ok, ConfNew} -> + {ok, ConfNew} + end. + -dialyzer([{nowarn_function, [post_config_update/5]}, error_handling]). -post_config_update([connectors, Type, Name], '$remove', _, _OldConf, _AppEnvs) -> +post_config_update([connectors, Type, Name] = Path, '$remove', _, OldConf, _AppEnvs) -> ConnId = connector_id(Type, Name), try foreach_linked_bridges(ConnId, fun(#{type := BType, name := BName}) -> throw({dependency_bridges_exist, emqx_bridge:bridge_id(BType, BName)}) end) catch throw:Error -> {error, Error} - end; + end, + _ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf); post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) -> ConnId = connector_id(Type, Name), foreach_linked_bridges(ConnId, diff --git a/apps/emqx_resource/src/emqx_resource_ssl.erl b/apps/emqx_connector/src/emqx_connector_ssl.erl similarity index 60% rename from apps/emqx_resource/src/emqx_resource_ssl.erl rename to apps/emqx_connector/src/emqx_connector_ssl.erl index 9e3fe0456..07b12eea1 100644 --- a/apps/emqx_resource/src/emqx_resource_ssl.erl +++ b/apps/emqx_connector/src/emqx_connector_ssl.erl @@ -15,37 +15,36 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_resource_ssl). +-module(emqx_connector_ssl). -export([ convert_certs/2 - , convert_certs/3 , clear_certs/2 ]). -convert_certs(ResId, NewConfig) -> - convert_certs(ResId, NewConfig, #{}). - -convert_certs(ResId, NewConfig, OldConfig) -> - OldSSL = drop_invalid_certs(maps:get(ssl, OldConfig, undefined)), - NewSSL = drop_invalid_certs(maps:get(ssl, NewConfig, undefined)), - CertsDir = cert_dir(ResId), - case emqx_tls_lib:ensure_ssl_files(CertsDir, NewSSL) of +convert_certs(RltvDir, NewConfig) -> + NewSSL = drop_invalid_certs(maps:get(<<"ssl">>, NewConfig, undefined)), + case emqx_tls_lib:ensure_ssl_files(RltvDir, NewSSL) of {ok, NewSSL1} -> - ok = emqx_tls_lib:delete_ssl_files(CertsDir, NewSSL1, OldSSL), {ok, new_ssl_config(NewConfig, NewSSL1)}; {error, Reason} -> {error, {bad_ssl_config, Reason}} end. -clear_certs(ResId, Config) -> - OldSSL = drop_invalid_certs(maps:get(ssl, Config, undefined)), - ok = emqx_tls_lib:delete_ssl_files(cert_dir(ResId), undefined, OldSSL). - -cert_dir(ResId) -> - filename:join(["resources", ResId]). +clear_certs(RltvDir, Config) -> + OldSSL = drop_invalid_certs(map_get_oneof([<<"ssl">>, ssl], Config, undefined)), + ok = emqx_tls_lib:delete_ssl_files(RltvDir, undefined, OldSSL). new_ssl_config(Config, undefined) -> Config; -new_ssl_config(Config, SSL) -> Config#{ssl => SSL}. +new_ssl_config(Config, SSL) -> Config#{<<"ssl">> => SSL}. drop_invalid_certs(undefined) -> undefined; drop_invalid_certs(SSL) -> emqx_tls_lib:drop_invalid_certs(SSL). + +map_get_oneof([], _Map, Default) -> Default; +map_get_oneof([Key | Keys], Map, Default) -> + case maps:find(Key, Map) of + error -> + map_get_oneof(Keys, Map, Default); + {ok, Value} -> + Value + end. \ No newline at end of file diff --git a/apps/emqx_resource/src/emqx_resource_instance.erl b/apps/emqx_resource/src/emqx_resource_instance.erl index 352ddf247..60b2babe5 100644 --- a/apps/emqx_resource/src/emqx_resource_instance.erl +++ b/apps/emqx_resource/src/emqx_resource_instance.erl @@ -196,32 +196,14 @@ do_create(InstId, Group, ResourceType, Config, Opts) -> {ok, _, _} -> {ok, already_created}; {error, not_found} -> - case emqx_resource_ssl:convert_certs(InstId, Config) of - {error, Reason} -> - {error, Reason}; - {ok, Config1} -> - do_create2(InstId, Group, ResourceType, Config1, Opts) - end + ok = do_start(InstId, Group, ResourceType, Config, Opts), + ok = emqx_plugin_libs_metrics:create_metrics(resource_metrics, InstId, + [matched, success, failed, exception], [matched]), + {ok, force_lookup(InstId)} end. -do_create2(InstId, Group, ResourceType, Config, Opts) -> - ok = do_start(InstId, Group, ResourceType, Config, Opts), - ok = emqx_plugin_libs_metrics:create_metrics(resource_metrics, InstId, - [matched, success, failed, exception], [matched]), - {ok, force_lookup(InstId)}. - do_create_dry_run(ResourceType, Config) -> InstId = make_test_id(), - case emqx_resource_ssl:convert_certs(InstId, Config) of - {error, Reason} -> - {error, Reason}; - {ok, Config1} -> - Result = do_create_dry_run2(InstId, ResourceType, Config1), - _ = emqx_resource_ssl:clear_certs(InstId, Config1), - Result - end. - -do_create_dry_run2(InstId, ResourceType, Config) -> case emqx_resource:call_start(InstId, ResourceType, Config) of {ok, ResourceState} -> case emqx_resource:call_health_check(InstId, ResourceType, ResourceState) of @@ -245,9 +227,8 @@ do_remove(Instance) -> do_remove(InstId, ClearMetrics) when is_binary(InstId) -> do_with_group_and_instance_data(InstId, fun do_remove/3, [ClearMetrics]). -do_remove(Group, #{id := InstId, config := Config} = Data, ClearMetrics) -> +do_remove(Group, #{id := InstId} = Data, ClearMetrics) -> _ = do_stop(Group, Data), - _ = emqx_resource_ssl:clear_certs(InstId, Config), ets:delete(emqx_resource_instance, InstId), case ClearMetrics of true -> ok = emqx_plugin_libs_metrics:clear_metrics(resource_metrics, InstId); From 318d0df4194c0c0332d0a963d4aca60f347c8775 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 27 Apr 2022 11:58:41 +0800 Subject: [PATCH 10/43] fix: return value of post_config_update --- apps/emqx_connector/src/emqx_connector.erl | 9 +++++---- apps/emqx_connector/src/emqx_connector_ssl.erl | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector.erl b/apps/emqx_connector/src/emqx_connector.erl index 0e17971e1..fbb89e8e7 100644 --- a/apps/emqx_connector/src/emqx_connector.erl +++ b/apps/emqx_connector/src/emqx_connector.erl @@ -48,12 +48,13 @@ pre_config_update(Path, Conf, _OldConfig) when is_map(Conf) -> -dialyzer([{nowarn_function, [post_config_update/5]}, error_handling]). post_config_update([connectors, Type, Name] = Path, '$remove', _, OldConf, _AppEnvs) -> ConnId = connector_id(Type, Name), - try foreach_linked_bridges(ConnId, fun(#{type := BType, name := BName}) -> + try + foreach_linked_bridges(ConnId, fun(#{type := BType, name := BName}) -> throw({dependency_bridges_exist, emqx_bridge:bridge_id(BType, BName)}) - end) + end), + _ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf) catch throw:Error -> {error, Error} - end, - _ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf); + end; post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) -> ConnId = connector_id(Type, Name), foreach_linked_bridges(ConnId, diff --git a/apps/emqx_connector/src/emqx_connector_ssl.erl b/apps/emqx_connector/src/emqx_connector_ssl.erl index 07b12eea1..02d9a4070 100644 --- a/apps/emqx_connector/src/emqx_connector_ssl.erl +++ b/apps/emqx_connector/src/emqx_connector_ssl.erl @@ -47,4 +47,4 @@ map_get_oneof([Key | Keys], Map, Default) -> map_get_oneof(Keys, Map, Default); {ok, Value} -> Value - end. \ No newline at end of file + end. From 6b8401625991dacdc539bf07781d9f7e36479793 Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 27 Apr 2022 11:34:57 +0800 Subject: [PATCH 11/43] fix(mgmt): add subscribe options into the result of the /subscriptions API --- .../src/emqx_mgmt_api_subscriptions.erl | 33 ++++++++++--------- .../test/emqx_mgmt_api_subscription_SUITE.erl | 32 ++++++++++++++---- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index be3d3f293..8bd418d43 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -72,7 +72,10 @@ fields(subscription) -> {node, hoconsc:mk(binary(), #{desc => <<"Access type">>})}, {topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})}, {clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})}, - {qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})} + {qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}, + {nl, hoconsc:mk(integer(), #{desc => <<"No Local">>})}, + {rap, hoconsc:mk(integer(), #{desc => <<"Retain as Published">>})}, + {rh, hoconsc:mk(integer(), #{desc => <<"Retain Handling">>})} ]. parameters() -> @@ -163,22 +166,20 @@ 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), - #{ - topic => filename:join([<<"$share">>, Group, Topic]), - clientid => maps:get(subid, Options), - qos => QoS, - node => node() - }; format({_Subscriber, Topic, Options}) -> - QoS = maps:get(qos, Options), - #{ - topic => Topic, - clientid => maps:get(subid, Options), - qos => QoS, - node => node() - }. + maps:merge( + #{ + topic => get_topic(Topic, Options), + clientid => maps:get(subid, Options), + node => node() + }, + maps:with([qos, nl, rap, rh], Options) + ). + +get_topic(Topic, #{share := Group}) -> + filename:join([<<"$share">>, Group, Topic]); +get_topic(Topic, _) -> + Topic. %%-------------------------------------------------------------------- %% Query Function 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 89c36d933..d1cf4e418 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_subscription_SUITE.erl @@ -25,6 +25,10 @@ %% notice: integer topic for sort response -define(TOPIC1, <<"t/0000">>). +-define(TOPIC1RH, 1). +-define(TOPIC1RAP, false). +-define(TOPIC1NL, false). +-define(TOPIC1QOS, 1). -define(TOPIC2, <<"$share/test_group/t/0001">>). -define(TOPIC2_TOPIC_ONLY, <<"t/0001">>). @@ -41,9 +45,13 @@ end_per_suite(_) -> emqx_mgmt_api_test_util:end_suite(). t_subscription_api(_) -> - {ok, Client} = emqtt:start_link(#{username => ?USERNAME, clientid => ?CLIENTID}), + {ok, Client} = emqtt:start_link(#{username => ?USERNAME, clientid => ?CLIENTID, proto_ver => v5}), {ok, _} = emqtt:connect(Client), - {ok, _, _} = emqtt:subscribe(Client, ?TOPIC1), + {ok, _, _} = emqtt:subscribe( + Client, [ + {?TOPIC1, [{rh, ?TOPIC1RH}, {rap, ?TOPIC1RAP}, {nl, ?TOPIC1NL}, {qos, ?TOPIC1QOS}]} + ] + ), {ok, _, _} = emqtt:subscribe(Client, ?TOPIC2), Path = emqx_mgmt_api_test_util:api_path(["subscriptions"]), {ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path), @@ -59,9 +67,21 @@ t_subscription_api(_) -> maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT) end, [Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions), - ?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1), + + ?assertMatch( + #{ + <<"topic">> := ?TOPIC1, + <<"qos">> := ?TOPIC1QOS, + <<"nl">> := _, + <<"rap">> := _, + <<"rh">> := ?TOPIC1RH, + <<"clientid">> := ?CLIENTID, + <<"node">> := _ + }, + Subscriptions1 + ), + ?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2), - ?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID), ?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID), QS = uri_string:compose_query([ @@ -94,8 +114,8 @@ t_subscription_api(_) -> MatchMeta = maps:get(<<"meta">>, MatchData), ?assertEqual(1, maps:get(<<"page">>, MatchMeta)), ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, MatchMeta)), - ?assertEqual(2, maps:get(<<"count">>, MatchMeta)), + ?assertEqual(1, maps:get(<<"count">>, MatchMeta)), MatchSubs = maps:get(<<"data">>, MatchData), - ?assertEqual(length(MatchSubs), 2), + ?assertEqual(1, length(MatchSubs)), emqtt:disconnect(Client). From 8d01e8a6973813cd44b1cf33cc011c0f7f5f7b71 Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 27 Apr 2022 11:37:58 +0800 Subject: [PATCH 12/43] fix(mgmt): add subscribe options into the result of the client subscriptions API --- .../src/emqx_mgmt_api_clients.erl | 14 +++++----- .../test/emqx_mgmt_api_clients_SUITE.erl | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index 8330c11b4..d5d5381cc 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -564,12 +564,14 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) -> {Node, Subs} -> Formatter = fun({Topic, SubOpts}) -> - #{ - node => Node, - clientid => ClientID, - topic => Topic, - qos => maps:get(qos, SubOpts) - } + maps:merge( + #{ + node => Node, + clientid => ClientID, + topic => Topic + }, + maps:with([qos, nl, rap, rh], SubOpts) + ) end, {200, lists:map(Formatter, Subs)} end. 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 897862b20..57bf25268 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl @@ -105,6 +105,32 @@ t_clients(_) -> ?assertEqual(AfterSubTopic, Topic), ?assertEqual(AfterSubQos, Qos), + %% get /clients/:clientid/subscriptions + SubscriptionsPath = emqx_mgmt_api_test_util:api_path([ + "clients", + binary_to_list(ClientId1), + "subscriptions" + ]), + {ok, SubscriptionsRes} = emqx_mgmt_api_test_util:request_api( + get, + SubscriptionsPath, + "", + AuthHeader + ), + [SubscriptionsData] = emqx_json:decode(SubscriptionsRes, [return_maps]), + ?assertMatch( + #{ + <<"clientid">> := ClientId1, + <<"nl">> := _, + <<"rap">> := _, + <<"rh">> := _, + <<"node">> := _, + <<"qos">> := Qos, + <<"topic">> := Topic + }, + SubscriptionsData + ), + %% post /clients/:clientid/unsubscribe UnSubscribePath = emqx_mgmt_api_test_util:api_path([ "clients", From d9c3cf5c97c9038aa34e53d1e3211ce24fe406c7 Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 27 Apr 2022 12:42:22 +0800 Subject: [PATCH 13/43] fix(mgmt): add subscribe options in client subscribe API --- .../src/emqx_mgmt_api_clients.erl | 40 +++++++++---------- .../test/emqx_mgmt_api_clients_SUITE.erl | 12 +++--- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index d5d5381cc..997f84227 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -513,7 +513,10 @@ fields(keepalive) -> fields(subscribe) -> [ {topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}, - {qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})} + {qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}, + {nl, hoconsc:mk(integer(), #{default => 0, desc => <<"No Local">>})}, + {rap, hoconsc:mk(integer(), #{default => 0, desc => <<"Retain as Published">>})}, + {rh, hoconsc:mk(integer(), #{default => 0, desc => <<"Retain Handling">>})} ]; fields(unsubscribe) -> [ @@ -536,9 +539,8 @@ authz_cache(delete, #{bindings := Bindings}) -> clean_authz_cache(Bindings). subscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) -> - Topic = maps:get(<<"topic">>, TopicInfo), - Qos = maps:get(<<"qos">>, TopicInfo, 0), - subscribe(#{clientid => ClientID, topic => Topic, qos => Qos}). + Opts = emqx_map_lib:unsafe_atom_key_map(TopicInfo), + subscribe(Opts#{clientid => ClientID}). unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) -> Topic = maps:get(<<"topic">>, TopicInfo), @@ -548,11 +550,7 @@ unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) -> 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 + emqx_map_lib:unsafe_atom_key_map(TopicInfo) || TopicInfo <- TopicInfos ], subscribe_batch(#{clientid => ClientID, topics => Topics}). @@ -661,21 +659,16 @@ clean_authz_cache(#{clientid := ClientID}) -> {500, #{code => <<"UNKNOW_ERROR">>, message => Message}} end. -subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) -> - case do_subscribe(ClientID, Topic, Qos) of +subscribe(#{clientid := ClientID, topic := Topic} = Sub) -> + Opts = maps:with([qos, nl, rap, rh], Sub), + case do_subscribe(ClientID, Topic, Opts) of {error, channel_not_found} -> {404, ?CLIENT_ID_NOT_FOUND}; {error, Reason} -> Message = list_to_binary(io_lib:format("~p", [Reason])), {500, #{code => <<"UNKNOW_ERROR">>, message => Message}}; {ok, Node} -> - Response = - #{ - clientid => ClientID, - topic => Topic, - qos => Qos, - node => Node - }, + Response = Sub#{node => Node}, {200, Response} end. @@ -688,15 +681,18 @@ unsubscribe(#{clientid := ClientID, topic := Topic}) -> end. subscribe_batch(#{clientid := ClientID, topics := Topics}) -> - ArgList = [[ClientID, Topic, Qos] || #{topic := Topic, qos := Qos} <- Topics], + ArgList = [ + [ClientID, Topic, maps:with([qos, nl, rap, rh], Sub)] + || #{topic := Topic} = Sub <- Topics + ], emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList). %%-------------------------------------------------------------------- %% internal function -do_subscribe(ClientID, Topic0, Qos) -> - {Topic, Opts} = emqx_topic:parse(Topic0), - TopicTable = [{Topic, Opts#{qos => Qos}}], +do_subscribe(ClientID, Topic0, Options) -> + {Topic, Opts} = emqx_topic:parse(Topic0, Options), + TopicTable = [{Topic, Opts}], case emqx_mgmt:subscribe(ClientID, TopicTable) of {error, Reason} -> {error, Reason}; 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 57bf25268..24c857288 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_clients_SUITE.erl @@ -43,7 +43,9 @@ t_clients(_) -> AuthHeader = emqx_mgmt_api_test_util:auth_header_(), - {ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}), + {ok, C1} = emqtt:start_link(#{ + username => Username1, clientid => ClientId1, proto_ver => v5 + }), {ok, _} = emqtt:connect(C1), {ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}), {ok, _} = emqtt:connect(C2), @@ -87,7 +89,7 @@ t_clients(_) -> ?assertEqual("[]", Client1AuthzCache), %% post /clients/:clientid/subscribe - SubscribeBody = #{topic => Topic, qos => Qos}, + SubscribeBody = #{topic => Topic, qos => Qos, nl => 1, rh => 1}, SubscribePath = emqx_mgmt_api_test_util:api_path([ "clients", binary_to_list(ClientId1), @@ -121,9 +123,9 @@ t_clients(_) -> ?assertMatch( #{ <<"clientid">> := ClientId1, - <<"nl">> := _, - <<"rap">> := _, - <<"rh">> := _, + <<"nl">> := 1, + <<"rap">> := 0, + <<"rh">> := 1, <<"node">> := _, <<"qos">> := Qos, <<"topic">> := Topic From 3429eaabcd0a76a844b1d8c2a9b468f0c3b3ce8a Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 27 Apr 2022 13:52:42 +0800 Subject: [PATCH 14/43] test(delayed): add enable/disable test for delayed --- apps/emqx_modules/test/emqx_delayed_SUITE.erl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/emqx_modules/test/emqx_delayed_SUITE.erl b/apps/emqx_modules/test/emqx_delayed_SUITE.erl index a6b4d85b5..2f11c9ba2 100644 --- a/apps/emqx_modules/test/emqx_delayed_SUITE.erl +++ b/apps/emqx_modules/test/emqx_delayed_SUITE.erl @@ -55,13 +55,26 @@ end_per_testcase(_Case, _Config) -> %% Test cases %%-------------------------------------------------------------------- -t_load_case(_) -> +t_enable_disable_case(_) -> + emqx_delayed:disable(), Hooks = emqx_hooks:lookup('message.publish'), MFA = {emqx_delayed, on_message_publish, []}, ?assertEqual(false, lists:keyfind(MFA, 2, Hooks)), + ok = emqx_delayed:enable(), Hooks1 = emqx_hooks:lookup('message.publish'), ?assertNotEqual(false, lists:keyfind(MFA, 2, Hooks1)), + + Ts0 = integer_to_binary(erlang:system_time(second) + 10), + DelayedMsg0 = emqx_message:make( + ?MODULE, 1, <<"$delayed/", Ts0/binary, "/publish">>, <<"delayed_abs">> + ), + _ = on_message_publish(DelayedMsg0), + ?assertMatch(#{data := Datas} when Datas =/= [], emqx_delayed:list(#{})), + + emqx_delayed:disable(), + ?assertEqual(false, lists:keyfind(MFA, 2, Hooks)), + ?assertMatch(#{data := []}, emqx_delayed:list(#{})), ok. t_delayed_message(_) -> From f5e09c9f2f68b9bbab8061009e02450467a495e6 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 26 Apr 2022 15:15:39 +0800 Subject: [PATCH 15/43] fix: bump minirest to 1.2.13 to fix crash when upload large form data --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 4684c15c0..176406e4f 100644 --- a/mix.exs +++ b/mix.exs @@ -57,7 +57,7 @@ defmodule EMQXUmbrella.MixProject do {:mria, github: "emqx/mria", tag: "0.2.4", override: true}, {:ekka, github: "emqx/ekka", tag: "0.12.4", override: true}, {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true}, - {:minirest, github: "emqx/minirest", tag: "1.2.12", override: true}, + {:minirest, github: "emqx/minirest", tag: "1.2.13", override: true}, {:ecpool, github: "emqx/ecpool", tag: "0.5.2"}, {:replayq, "0.3.4", override: true}, {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true}, diff --git a/rebar.config b/rebar.config index 5b08853dd..6145e398f 100644 --- a/rebar.config +++ b/rebar.config @@ -56,7 +56,7 @@ , {mria, {git, "https://github.com/emqx/mria", {tag, "0.2.4"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.4"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.12"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.13"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.2"}}} , {replayq, "0.3.4"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From 8cfcb10c7eee4576c6e9ba722c11ccdfd0d09bf3 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 26 Apr 2022 21:53:34 +0800 Subject: [PATCH 16/43] fix: make logger config options more specific --- apps/emqx_conf/i18n/emqx_conf_schema.conf | 236 +++++++--------------- apps/emqx_conf/src/emqx_conf_schema.erl | 40 ++-- 2 files changed, 88 insertions(+), 188 deletions(-) diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index 82913d061..a5e480753 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -941,26 +941,15 @@ until the RPC connection is considered lost.""" log_file_handlers { desc { en: """Key-value list of file-based log handlers.""" - zh: """基于文件的日志处理进程的键值列表。""" + zh: """需要持久化到文件的日志处理进程列表。默认只有 default 一个处理进程。""" } label { en: "Log Handlers Key Val List" - zh: "日志处理进程键值列表" + zh: "日志 Handler 列表" } } - log_error_logger { - desc { - en: """Deprecated.""" - zh: """该配置已弃用。""" - } - label { - en: "Deprecate" - zh: "配置已弃用" - } - } - - console_handler_enable { + common_handler_enable { desc { en: """Enable this log handler.""" zh: """启用此日志处理进程。""" @@ -971,21 +960,23 @@ until the RPC connection is considered lost.""" } } - console_handler_level { + common_handler_level { desc { en: """Global log level. This includes the primary log level and all log handlers.""" - zh: """全局日志级别。 这包括主日志级别和所有日志处理进程。""" + zh: """设置日志级别。 默认为warning。""" } label { en: "Global Log Level" - zh: "全局日志级别" + zh: "日志级别" } } - console_handler_time_offset { + common_handler_time_offset { desc { en: """The time offset to be used when formatting the timestamp.""" - zh: """格式化时间戳时,使用的时间偏移量。""" + zh: """日志格式中的时间戳,使用的时间偏移量。默认使用系统时区system,当为utc为无时间偏移量 +为具体的N(1-24)数字时,则代表时间偏移量+N。 + """ } label { en: "Time Offset" @@ -993,10 +984,10 @@ until the RPC connection is considered lost.""" } } - console_handler_chars_limit { + common_handler_chars_limit { desc { en: """Set the maximum length of a single log message. If this length is exceeded, the log message will be truncated.""" - zh: """设置单个日志消息的最大长度。 如果超过此长度,则日志消息将被截断。""" + zh: """设置单个日志消息的最大长度。 如果超过此长度,则日志消息将被截断。最小可设置的长度为100。""" } label { en: "Single Log Max Length" @@ -1004,10 +995,10 @@ until the RPC connection is considered lost.""" } } - console_handler_formatter { + common_handler_formatter { desc { en: """Choose log format. text for free text, and json for structured logging.""" - zh: """选择日志格式。 text 用于自由文本,json 用于结构化日志记录。""" + zh: """选择日志格式。 text 用于纯文本,json 用于结构化日志记录。""" } label { en: "Log Format" @@ -1015,10 +1006,10 @@ until the RPC connection is considered lost.""" } } - console_handler_single_line { + common_handler_single_line { desc { en: """Print logs in a single line if set to true. Otherwise, log messages may span multiple lines.""" - zh: """如果设置为 true,则在一行中打印日志。 否则,日志消息可能跨越多行。""" + zh: """如果设置为 true,则单行打印日志。 否则,日志消息可能跨越多行。""" } label { en: "Single Line Mode" @@ -1026,10 +1017,24 @@ until the RPC connection is considered lost.""" } } - console_handler_sync_mode_qlen { + common_handler_sync_mode_qlen { desc { - en: """As long as the number of buffered log events is lower than this value, all log events are handled asynchronously.""" - zh: """只要缓冲的日志事件的数量低于这个值,所有的日志事件都会被异步处理。""" + en: """As long as the number of buffered log events is lower than this value, +all log events are handled asynchronously. This means that the client process sending the log event, +by calling a log function in the Logger API, does not wait for a response from the handler +but continues executing immediately after the event is sent. +It is not affected by the time it takes the handler to print the event to the log device. +If the message queue grows larger than this value, t +he handler starts handling log events synchronously instead, +meaning that the client process sending the event must wait for a response. +When the handler reduces the message queue to a level below the sync_mode_qlen threshold, +asynchronous operation is resumed. +""" + zh: """只要缓冲的日志事件的数量低于这个值,所有的日志事件都会被异步处理。 +这意味着,日志落地速度不会影响正常的业务进程,因为它们不需要等待日志处理进程的响应。 +如果消息队列的增长超过了这个值,处理程序开始同步处理日志事件。也就是说,发送事件的客户进程必须等待响应。 +当处理程序将消息队列减少到低于sync_mode_qlen阈值的水平时,异步操作就会恢复。 +默认为100条信息,当等待的日志事件大于100条时,就开始同步处理日志。""" } label { en: "Sync Mode Max Log Events" @@ -1037,10 +1042,17 @@ until the RPC connection is considered lost.""" } } - console_handler_drop_mode_qlen { + common_handler_drop_mode_qlen { desc { - en: """When the number of buffered log events is larger than this value, the new log events are dropped.
When drop mode is activated or deactivated, a message is printed in the logs.""" - zh: """当缓冲的日志事件数大于此值时,新的日志事件将被丢弃。
启用或停用丢弃模式时,会在日志中打印一条消息。""" + en: """When the number of buffered log events is larger than this value, the new log events are dropped. +When drop mode is activated or deactivated, a message is printed in the logs.""" + zh: """当缓冲的日志事件数大于此值时,新的日志事件将被丢弃。起到过载保护的功能。 +为了使过载保护算法正常工作必须要: sync_mode_qlen =< drop_mode_qlen =< flush_qlen <\code> 且 drop_mode_qlen > 1 +要禁用某些模式,请执行以下操作。 +- 如果sync_mode_qlen被设置为0,所有的日志事件都被同步处理。也就是说,异步日志被禁用。 +- 如果sync_mode_qlen被设置为与drop_mode_qlen相同的值,同步模式被禁用。也就是说,处理程序总是以异步模式运行,除非调用drop或flushing。 +- 如果drop_mode_qlen被设置为与flush_qlen相同的值,则drop模式被禁用,永远不会发生。 +""" } label { en: "Drop Mode Max Log Events" @@ -1048,10 +1060,11 @@ until the RPC connection is considered lost.""" } } - console_handler_flush_qlen { + common_handler_flush_qlen { desc { en: """If the number of buffered log events grows larger than this threshold, a flush (delete) operation takes place. To flush events, the handler discards the buffered log messages without logging.""" - zh: """如果缓冲日志事件的数量增长大于此阈值,则会发生刷新(删除)操作。 为了完成刷新事件,处理进程丢弃缓冲的日志消息。""" + zh: """如果缓冲日志事件的数量增长大于此阈值,则会发生刷新(删除)操作。 日志处理进程会丢弃缓冲的日志消息。 +来缓解自身不会由于内存瀑涨而影响其它业务进程。日志内容会提醒有多少事件被删除。""" } label { en: "Flush Threshold" @@ -1059,14 +1072,14 @@ until the RPC connection is considered lost.""" } } - console_handler_supervisor_reports { + common_handler_supervisor_reports { desc { en: """Type of supervisor reports that are logged. - `error`: only log errors in the Erlang processes. - `progress`: log process startup.""" - zh: """ supervisor 报告的类型。 + zh: """ supervisor 报告的类型。默认为 error 类型。 - `error`:仅记录 Erlang 进程中的错误。 - - `progress`:记录进程启动。""" + - `progress`:除了 error 信息外,还需要记录进程启动的详细信息。""" } label { en: "Report Type" @@ -1074,7 +1087,7 @@ until the RPC connection is considered lost.""" } } - console_handler_max_depth { + common_handler_max_depth { desc { en: """Maximum depth for Erlang term log formatting and Erlang process message queue inspection.""" zh: """Erlang 内部格式日志格式化和 Erlang 进程消息队列检查的最大深度。""" @@ -1088,7 +1101,7 @@ until the RPC connection is considered lost.""" log_file_handler_file { desc { en: """Name the log file.""" - zh: """日志文件名字。""" + zh: """日志文件路径及名字。""" } label { en: "Log File Name" @@ -1099,7 +1112,9 @@ until the RPC connection is considered lost.""" log_file_handler_max_size { desc { en: """This parameter controls log file rotation. The value `infinity` means the log file will grow indefinitely, otherwise the log file will be rotated once it reaches `max_size` in bytes.""" - zh: """此参数控制日志文件轮换。 `infinity` 意味着日志文件将无限增长,否则日志文件将在达到 `max_size`(以字节为单位)时进行轮换。""" + zh: """此参数控制日志文件轮换。 `infinity` 意味着日志文件将无限增长,否则日志文件将在达到 `max_size`(以字节为单位)时进行轮换。 +与 rotation count配合使用。如果 counter 为 10,则是10个文件轮换。 +""" } label { en: "Rotation Size" @@ -1107,128 +1122,14 @@ until the RPC connection is considered lost.""" } } - log_file_handler_enable { + log_error_logger { desc { - en: """Enable this log handler.""" - zh: """启用此日志处理进程。""" + en: """Keep error_logger silent.""" + zh: """让 error_logger 日志处理进程关闭,防止一条异常信息被记录多次。""" } label { - en: "Enable Log Handler" - zh: "启用此日志处理进程" - } - } - - log_file_handler_level { - desc { - en: """Global log level. This includes the primary log level and all log handlers.""" - zh: """全局日志级别。 这包括主日志级别和所有日志处理进程。""" - } - label { - en: "Global Level" - zh: "全局日志级别" - } - } - - log_file_handler_time_offset { - desc { - en: """The time offset to be used when formatting the timestamp.""" - zh: """格式化时间戳时要使用的时间偏移量。""" - } - label { - en: "Time Offset" - zh: "时间偏移" - } - } - - log_file_handler_chars_limit { - desc { - en: """Set the maximum length of a single log message. If this length is exceeded, the log message will be truncated.""" - zh: """设置单个日志消息的最大长度。 如果超过此长度,则日志消息将被截断。""" - } - label { - en: "Single Log Max Length" - zh: "单个日志消息最大长度" - } - } - - log_file_handler_formatter { - desc { - en: """Choose log format. text for free text, and json for structured logging.""" - zh: """选择日志格式。 text 用于自由文本,json 用于结构化日志记录。""" - } - label { - en: "Log Format" - zh: "日志格式" - } - } - - log_file_handler_single_line { - desc { - en: """Print logs in a single line if set to true. Otherwise, log messages may span multiple lines.""" - zh: """如果设置为 true,则在一行中打印日志。 否则,日志消息可能跨越多行。""" - } - label { - en: "Single Line Mode" - zh: "单行模式" - } - } - - log_file_handler_sync_mode_qlen { - desc { - en: """As long as the number of buffered log events is lower than this value, all log events are handled asynchronously.""" - zh: """只要缓冲的日志事件的数量低于这个值,所有的日志事件都会被异步处理。""" - } - label { - en: "Sync Mode Max Log Events" - zh: "异步模式最大事件数" - } - } - - log_file_handler_drop_mode_qlen { - desc { - en: """When the number of buffered log events is larger than this value, the new log events are dropped.
When drop mode is activated or deactivated, a message is printed in the logs.""" - zh: """当缓冲的日志事件数大于此值时,新的日志事件将被丢弃。
启用或停用丢弃模式时,会在日志中打印一条消息。""" - } - label { - en: "Drop Mode Max Log Events" - zh: "缓存最大日志事件数" - } - } - - log_file_handler_flush_qlen { - desc { - en: """If the number of buffered log events grows larger than this threshold, a flush (delete) operation takes place. To flush events, the handler discards the buffered log messages without logging.""" - zh: """如果缓冲日志事件的数量增长大于此阈值,则会发生刷新(删除)操作。 为了完成刷新事件,处理进程丢弃缓冲的日志消息。""" - } - label { - en: "Flush Threshold" - zh: "刷新阈值" - } - } - - log_file_handler_supervisor_reports { - desc { - en: """Type of supervisor reports that are logged. - - `error`: only log errors in the Erlang processes. - - `progress`: log process startup.""" - zh: """ supervisor 报告的类型。 - - `error`:仅记录 Erlang 进程中的错误。 - - `progress`:记录进程启动。""" - } - label { - en: "Report Type" - zh: "报告类型" - } - } - - log_file_handler_max_depth { - desc { - en: """Maximum depth for Erlang term log formatting and Erlang process message queue inspection.""" - zh: """Erlang 内部格式日志格式化和 Erlang 进程消息队列检查的最大深度。""" - } - label { - en: "Max Depth" - zh: "最大深度" + en: "error_logger" + zh: "error_logger" } } @@ -1290,22 +1191,22 @@ until the RPC connection is considered lost.""" log_overload_kill_restart_after { desc { en: """If the handler is terminated, it restarts automatically after a delay specified in milliseconds. The value `infinity` prevents restarts.""" - zh: """如果处理进程终止,它会在以毫秒为单位指定的延迟后自动重新启动。 `infinity` 防止重新启动。""" + zh: """如果处理进程终止,它会在以指定的时间后后自动重新启动。 `infinity` 不自动重启。""" } label { - en: "Handler Restart Delay" - zh: "处理进程重启延迟" + en: "Handler Restart Timer" + zh: "处理进程重启机制" } } log_burst_limit_enable { desc { en: """Enable log burst control feature.""" - zh: """启用日志突发控制功能。""" + zh: """启用日志限流保护机制。""" } label { en: "Enable Burst" - zh: "启用日志突发控制" + zh: "日志限流保护" } } @@ -1509,10 +1410,10 @@ By default, the logs are stored in `./log` directory (for installation from zip This section of the configuration controls the number of files kept for each log handler. """ zh: -""" -默认情况下,日志存储在 `./log` 目录(用于从 zip 文件安装)或 `/var/log/emqx`(用于二进制安装)。
-这部分配置,控制每个日志处理进程保留的文件数量。 -""" + """ + 默认情况下,日志存储在 `./log` 目录(用于从 zip 文件安装)或 `/var/log/emqx`(用于二进制安装)。
+ 这部分配置,控制每个日志处理进程保留的文件数量。 + """ } label { en: "Log Rotation" @@ -1568,5 +1469,4 @@ Log burst limit feature can temporarily disable logging to avoid these issues."" zh: "授权" } } - } diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index f882855e2..969710787 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -801,8 +801,8 @@ fields("log") -> mapping => "kernel.error_logger", default => silent, desc => ?DESC("log_error_logger") - } - )} + }) + } ]; fields("console_handler") -> log_handler_common_confs(); @@ -866,7 +866,7 @@ fields("log_overload_kill") -> )}, {"qlen", sc( - integer(), + pos_integer(), #{ default => 20000, desc => ?DESC("log_overload_kill_qlen") @@ -874,7 +874,7 @@ fields("log_overload_kill") -> )}, {"restart_after", sc( - hoconsc:union([emqx_schema:duration(), infinity]), + hoconsc:union([emqx_schema:duration_ms(), infinity]), #{ default => "5s", desc => ?DESC("log_overload_kill_restart_after") @@ -893,7 +893,7 @@ fields("log_burst_limit") -> )}, {"max_count", sc( - integer(), + pos_integer(), #{ default => 10000, desc => ?DESC("log_burst_limit_max_count") @@ -1073,7 +1073,7 @@ log_handler_common_confs() -> boolean(), #{ default => false, - desc => ?DESC("log_file_handler_enable") + desc => ?DESC("common_handler_enable") } )}, {"level", @@ -1081,7 +1081,7 @@ log_handler_common_confs() -> log_level(), #{ default => warning, - desc => ?DESC("log_file_handler_level") + desc => ?DESC("common_handler_level") } )}, {"time_offset", @@ -1089,15 +1089,15 @@ log_handler_common_confs() -> string(), #{ default => "system", - desc => ?DESC("log_file_handler_time_offset") + desc => ?DESC("common_handler_time_offset") } )}, {"chars_limit", sc( - hoconsc:union([unlimited, range(1, inf)]), + hoconsc:union([unlimited, range(100, inf)]), #{ default => unlimited, - desc => ?DESC("log_file_handler_chars_limit") + desc => ?DESC("common_handler_chars_limit") } )}, {"formatter", @@ -1105,7 +1105,7 @@ log_handler_common_confs() -> hoconsc:enum([text, json]), #{ default => text, - desc => ?DESC("log_file_handler_formatter") + desc => ?DESC("common_handler_formatter") } )}, {"single_line", @@ -1113,31 +1113,31 @@ log_handler_common_confs() -> boolean(), #{ default => true, - desc => ?DESC("log_file_handler_single_line") + desc => ?DESC("common_handler_single_line") } )}, {"sync_mode_qlen", sc( - integer(), + non_neg_integer(), #{ default => 100, - desc => ?DESC("log_file_handler_sync_mode_qlen") + desc => ?DESC("common_handler_sync_mode_qlen") } )}, {"drop_mode_qlen", sc( - integer(), + pos_integer(), #{ default => 3000, - desc => ?DESC("log_file_handler_drop_mode_qlen") + desc => ?DESC("common_handler_drop_mode_qlen") } )}, {"flush_qlen", sc( - integer(), + pos_integer(), #{ default => 8000, - desc => ?DESC("log_file_handler_flush_qlen") + desc => ?DESC("common_handler_flush_qlen") } )}, {"overload_kill", sc(ref("log_overload_kill"), #{})}, @@ -1147,7 +1147,7 @@ log_handler_common_confs() -> hoconsc:enum([error, progress]), #{ default => error, - desc => ?DESC("log_file_handler_supervisor_reports") + desc => ?DESC("common_handler_supervisor_reports") } )}, {"max_depth", @@ -1155,7 +1155,7 @@ log_handler_common_confs() -> hoconsc:union([unlimited, non_neg_integer()]), #{ default => 100, - desc => ?DESC("log_file_handler_max_depth") + desc => ?DESC("common_handler_max_depth") } )} ]. From a99c49e75f9f66adbf58235d50f4f3bf2c2fb650 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Tue, 26 Apr 2022 22:17:25 +0800 Subject: [PATCH 17/43] fix: limit file ^[/\_a-zA-Z0-9\.\-]*$ --- apps/emqx_conf/i18n/emqx_conf_schema.conf | 8 ++++---- apps/emqx_conf/src/emqx_conf_schema.erl | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/emqx_conf/i18n/emqx_conf_schema.conf b/apps/emqx_conf/i18n/emqx_conf_schema.conf index a5e480753..03ed344fc 100644 --- a/apps/emqx_conf/i18n/emqx_conf_schema.conf +++ b/apps/emqx_conf/i18n/emqx_conf_schema.conf @@ -1024,8 +1024,8 @@ all log events are handled asynchronously. This means that the client process se by calling a log function in the Logger API, does not wait for a response from the handler but continues executing immediately after the event is sent. It is not affected by the time it takes the handler to print the event to the log device. -If the message queue grows larger than this value, t -he handler starts handling log events synchronously instead, +If the message queue grows larger than this value, +the handler starts handling log events synchronously instead, meaning that the client process sending the event must wait for a response. When the handler reduces the message queue to a level below the sync_mode_qlen threshold, asynchronous operation is resumed. @@ -1158,11 +1158,11 @@ When drop mode is activated or deactivated, a message is printed in the logs.""" log_overload_kill_enable { desc { en: """Enable log handler overload kill feature.""" - zh: """启用日志处理进程过载终止功能。""" + zh: """日志处理进程过载时为保护自己节点其它的业务能正常,强制杀死日志处理进程。""" } label { en: "Log Handler Overload Kill" - zh: "日志处理进程过载终止" + zh: "日志处理进程过载保护" } } diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 969710787..1a3270ab8 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -800,6 +800,7 @@ fields("log") -> #{ mapping => "kernel.error_logger", default => silent, + readOnly => true, desc => ?DESC("log_error_logger") }) } @@ -811,7 +812,8 @@ fields("log_file_handler") -> {"file", sc( file(), - #{desc => ?DESC("log_file_handler_file")} + #{desc => ?DESC("log_file_handler_file"), + validator => fun file_location/1 } )}, {"rotation", sc( @@ -822,7 +824,7 @@ fields("log_file_handler") -> sc( hoconsc:union([infinity, emqx_schema:bytesize()]), #{ - default => "10MB", + default => "50MB", desc => ?DESC("log_file_handler_max_size") } )} @@ -1328,3 +1330,14 @@ emqx_schema_high_prio_roots() -> #{desc => ?DESC(authorization)} )}, lists:keyreplace("authorization", 1, Roots, Authz). + +-define(VALID_FILE, "^[/\_a-zA-Z0-9\.\-]*$"). +file_location(File) -> + Error = {error, "Invalid file name: " ++ ?VALID_FILE}, + try + case re:run(File, ?VALID_FILE) of + nomatch -> Error; + _ -> ok + end + catch _:_ -> Error + end. From 08cad804bf5e286981ad374e537ed56872a3f48f Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 09:19:47 +0800 Subject: [PATCH 18/43] fix: bump hocon to 0.27.4 to obfuscate sensitive as binary --- apps/emqx/rebar.config | 2 +- apps/emqx_prometheus/rebar.config | 2 +- mix.exs | 2 +- rebar.config | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 4ade86dc6..458326b81 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -29,7 +29,7 @@ {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.4"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, - {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.3"}}}, + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.4"}}}, {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, "1.0.0"}}} diff --git a/apps/emqx_prometheus/rebar.config b/apps/emqx_prometheus/rebar.config index a51d56b99..974192a41 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.27.3"}}} + {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.4"}}} ]}. {edoc_opts, [{preprocess, true}]}. diff --git a/mix.exs b/mix.exs index 176406e4f..b999480ab 100644 --- a/mix.exs +++ b/mix.exs @@ -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: "1.0.0", override: true}, - {:hocon, github: "emqx/hocon", tag: "0.27.3", override: true}, + {:hocon, github: "emqx/hocon", tag: "0.27.4", override: true}, {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true}, {:esasl, github: "emqx/esasl", tag: "0.2.0"}, {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"}, diff --git a/rebar.config b/rebar.config index 6145e398f..95e3f705c 100644 --- a/rebar.config +++ b/rebar.config @@ -66,7 +66,7 @@ , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.3"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.4"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.1"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} From 55be66a5ebdd89c4bf324580e0c2effc386d0e8c Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 10:31:39 +0800 Subject: [PATCH 19/43] feat: add self node to /cluster --- apps/emqx_conf/src/emqx_conf_schema.erl | 15 +++++++++------ .../emqx_management/src/emqx_mgmt_api_cluster.erl | 6 ++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 1a3270ab8..7ab815d9a 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -800,10 +800,10 @@ fields("log") -> #{ mapping => "kernel.error_logger", default => silent, - readOnly => true, + 'readOnly' => true, desc => ?DESC("log_error_logger") - }) - } + } + )} ]; fields("console_handler") -> log_handler_common_confs(); @@ -812,8 +812,10 @@ fields("log_file_handler") -> {"file", sc( file(), - #{desc => ?DESC("log_file_handler_file"), - validator => fun file_location/1 } + #{ + desc => ?DESC("log_file_handler_file"), + validator => fun file_location/1 + } )}, {"rotation", sc( @@ -1339,5 +1341,6 @@ file_location(File) -> nomatch -> Error; _ -> ok end - catch _:_ -> Error + catch + _:_ -> Error end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_cluster.erl b/apps/emqx_management/src/emqx_mgmt_api_cluster.erl index 25ec5c9a2..e082fa745 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_cluster.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_cluster.erl @@ -43,7 +43,8 @@ schema("/cluster") -> responses => #{ 200 => [ {name, ?HOCON(string(), #{desc => "Cluster name"})}, - {nodes, ?HOCON(?ARRAY(string()), #{desc => "Node name"})} + {nodes, ?HOCON(?ARRAY(string()), #{desc => "Node name"})}, + {self, ?HOCON(string(), #{desc => "Self node name"})} ] } } @@ -97,7 +98,8 @@ cluster_info(get, _) -> ClusterName = application:get_env(ekka, cluster_name, emqxcl), Info = #{ name => ClusterName, - nodes => mria_mnesia:running_nodes() + nodes => mria_mnesia:running_nodes(), + self => node() }, {200, Info}. From af69899619a8746ad8c3d79dd3c68122220b09dc Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 26 Apr 2022 23:57:40 +0200 Subject: [PATCH 20/43] chore: add a pre-commit hook to auto format erlang code --- .gitattributes | 1 + scripts/erlfmt | Bin 0 -> 137271 bytes scripts/git-hook-pre-commit.sh | 11 +++++++++++ scripts/git-hooks-init.sh | 4 ++++ 4 files changed, 16 insertions(+) create mode 100755 scripts/erlfmt create mode 100755 scripts/git-hook-pre-commit.sh diff --git a/.gitattributes b/.gitattributes index fdca880b3..f12364de3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,4 @@ scripts/* text eol=lf *.jpg -text *.png -text *.pdf -text +scripts/erlfmt -text diff --git a/scripts/erlfmt b/scripts/erlfmt new file mode 100755 index 0000000000000000000000000000000000000000..0c42bdf924414935bd3dc9049d44429646a8a474 GIT binary patch literal 137271 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�chQsSTQDR$ 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 Xyf>*KKQ5#r??U7ZR23`v=>Yr}((0R_ literal 0 HcmV?d00001 diff --git a/scripts/git-hook-pre-commit.sh b/scripts/git-hook-pre-commit.sh new file mode 100755 index 000000000..7b9e06385 --- /dev/null +++ b/scripts/git-hook-pre-commit.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +files="$(git diff --cached --name-only | grep -E '.*\.erl' || true)" +if [[ "${files}" == '' ]]; then + exit 0 +fi +files="$(echo -e "$files" | xargs)" +# shellcheck disable=SC2086 +./scripts/erlfmt -c $files diff --git a/scripts/git-hooks-init.sh b/scripts/git-hooks-init.sh index fd47dd46f..6b85e23e3 100755 --- a/scripts/git-hooks-init.sh +++ b/scripts/git-hooks-init.sh @@ -11,3 +11,7 @@ mkdir -p ".git/hooks" if [ ! -L '.git/hooks/pre-push' ]; then ln -sf '../../scripts/git-hook-pre-push.sh' '.git/hooks/pre-push' fi + +if [ ! -L '.git/hooks/pre-commit' ]; then + ln -sf '../../scripts/git-hook-pre-commit.sh' '.git/hooks/pre-commit' +fi From 46550d5a6ffa5ed8af5c8adbb89c3aa4befa0eaf Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 27 Apr 2022 14:07:33 +0800 Subject: [PATCH 21/43] fix: don't remote the cert files when updating authz --- apps/emqx_authz/src/emqx_authz.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 953dad27c..e394f46f8 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -180,7 +180,6 @@ do_post_config_update({{?CMD_REPLACE, Type}, RawNewSource}, Sources) -> {OldSource, Front, Rear} = take(Type, OldSources), NewSource = get_source_by_type(type(RawNewSource), Sources), ok = ensure_resource_deleted(OldSource), - clear_certs(OldSource), InitedSources = init_source(NewSource), Front ++ [InitedSources] ++ Rear; do_post_config_update({{?CMD_DELETE, Type}, _RawNewSource}, _Sources) -> From 0635918d16575dd9d34c85be5cac79e6caeee34c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 27 Apr 2022 14:17:22 +0800 Subject: [PATCH 22/43] fix: define ssl SNI field as a non-empty-string --- apps/emqx/src/emqx_schema.erl | 7 +++++++ .../src/mqtt/emqx_connector_mqtt_schema.erl | 15 ++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 4a23bfeda..b4cb63fc4 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -57,6 +57,7 @@ validate_heap_size/1, parse_user_lookup_fun/1, validate_alarm_actions/1, + non_empty_string/1, validations/0 ]). @@ -1898,6 +1899,7 @@ client_ssl_opts_schema(Defaults1) -> hoconsc:union([disable, string()]), #{ required => false, + validator => fun emqx_schema:non_empty_string/1, desc => ?DESC(client_ssl_opts_schema_server_name_indication) } )} @@ -2177,3 +2179,8 @@ authentication(Type) -> -spec qos() -> typerefl:type(). qos() -> typerefl:alias("qos", typerefl:union([0, 1, 2])). + +non_empty_string(<<>>) -> {error, empty_string_not_allowed}; +non_empty_string("") -> {error, empty_string_not_allowed}; +non_empty_string(S) when is_binary(S); is_list(S) -> ok; +non_empty_string(_) -> {error, invalid_string}. 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 2a0fcc3fa..d913e1ecf 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -31,8 +31,6 @@ , egress_desc/0 ]). --export([non_empty_string/1]). - -import(emqx_schema, [mk_duration/2]). namespace() -> "connector-mqtt". @@ -98,7 +96,7 @@ fields("ingress") -> [ {remote_topic, sc(binary(), #{ required => true - , validator => fun ?MODULE:non_empty_string/1 + , validator => fun emqx_schema:non_empty_string/1 , desc => ?DESC("ingress_remote_topic") })} , {remote_qos, @@ -108,7 +106,7 @@ fields("ingress") -> })} , {local_topic, sc(binary(), - #{ validator => fun ?MODULE:non_empty_string/1 + #{ validator => fun emqx_schema:non_empty_string/1 , desc => ?DESC("ingress_local_topic") })} , {local_qos, @@ -140,12 +138,12 @@ fields("egress") -> [ {local_topic, sc(binary(), #{ desc => ?DESC("egress_local_topic") - , validator => fun ?MODULE:non_empty_string/1 + , validator => fun emqx_schema:non_empty_string/1 })} , {remote_topic, sc(binary(), #{ required => true - , validator => fun ?MODULE:non_empty_string/1 + , validator => fun emqx_schema:non_empty_string/1 , desc => ?DESC("egress_remote_topic") })} , {remote_qos, @@ -228,10 +226,5 @@ local_topic will be forwarded. qos() -> hoconsc:union([emqx_schema:qos(), binary()]). -non_empty_string(<<>>) -> {error, empty_string_not_allowed}; -non_empty_string("") -> {error, empty_string_not_allowed}; -non_empty_string(S) when is_binary(S); is_list(S) -> ok; -non_empty_string(_) -> {error, invalid_string}. - sc(Type, Meta) -> hoconsc:mk(Type, Meta). ref(Field) -> hoconsc:ref(?MODULE, Field). From ab9e93e8a76573342b58614e3a84af5bdc5c9e46 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 13:58:38 +0800 Subject: [PATCH 23/43] fix: websocket's max_connection not work --- apps/emqx/src/emqx_schema.erl | 4 +- apps/emqx/src/emqx_ws_connection.erl | 224 +++++++++++++++------------ 2 files changed, 131 insertions(+), 97 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 4a23bfeda..d1ba42eea 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1515,7 +1515,7 @@ base_listener() -> )}, {"acceptors", sc( - integer(), + pos_integer(), #{ default => 16, desc => ?DESC(base_listener_acceptors) @@ -1523,7 +1523,7 @@ base_listener() -> )}, {"max_connections", sc( - hoconsc:union([infinity, integer()]), + hoconsc:union([infinity, pos_integer()]), #{ default => infinity, desc => ?DESC(base_listener_max_connections) diff --git a/apps/emqx/src/emqx_ws_connection.erl b/apps/emqx/src/emqx_ws_connection.erl index 37bd8fdcf..52f998acd 100644 --- a/apps/emqx/src/emqx_ws_connection.erl +++ b/apps/emqx/src/emqx_ws_connection.erl @@ -272,78 +272,65 @@ check_origin_header(Req, #{listener := {Type, Listener}} = Opts) -> false -> ok end. -websocket_init([ - Req, - #{zone := Zone, limiter := LimiterCfg, listener := {Type, Listener}} = Opts -]) -> - {Peername, Peercert} = - case - emqx_config:get_listener_conf(Type, Listener, [proxy_protocol]) andalso - maps:get(proxy_header, Req) - of - #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> - SourceName = {SrcAddr, SrcPort}, - %% Notice: Only CN is available in Proxy Protocol V2 additional info - SourceSSL = - case maps:get(cn, SSL, undefined) of - undeined -> nossl; - CN -> [{pp2_ssl_cn, CN}] - end, - {SourceName, SourceSSL}; - #{src_address := SrcAddr, src_port := SrcPort} -> - SourceName = {SrcAddr, SrcPort}, - {SourceName, nossl}; - _ -> - {get_peer(Req, Opts), cowboy_req:cert(Req)} - end, - Sockname = cowboy_req:sock(Req), - WsCookie = - try - cowboy_req:parse_cookies(Req) - catch - error:badarg -> - ?SLOG(error, #{msg => "bad_cookie"}), - undefined; - Error:Reason -> - ?SLOG(error, #{ - msg => "failed_to_parse_cookie", - exception => Error, - reason => Reason - }), - undefined - end, - ConnInfo = #{ - socktype => ws, - peername => Peername, - sockname => Sockname, - peercert => Peercert, - ws_cookie => WsCookie, - conn_mod => ?MODULE - }, - Limiter = emqx_limiter_container:get_limiter_by_names( - [?LIMITER_BYTES_IN, ?LIMITER_MESSAGE_IN], LimiterCfg - ), - MQTTPiggyback = get_ws_opts(Type, Listener, mqtt_piggyback), - FrameOpts = #{ - strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]), - max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size]) - }, - ParseState = emqx_frame:initial_parse_state(FrameOpts), - Serialize = emqx_frame:serialize_opts(), - Channel = emqx_channel:init(ConnInfo, Opts), - GcState = - case emqx_config:get_zone_conf(Zone, [force_gc]) of - #{enable := false} -> undefined; - GcPolicy -> emqx_gc:init(GcPolicy) - end, - StatsTimer = - case emqx_config:get_zone_conf(Zone, [stats, enable]) of - true -> undefined; - false -> disabled - end, - %% MQTT Idle Timeout - IdleTimeout = emqx_channel:get_mqtt_conf(Zone, idle_timeout), - IdleTimer = start_timer(IdleTimeout, idle_timeout), +websocket_init([Req, Opts]) -> + #{zone := Zone, limiter := LimiterCfg, listener := {Type, Listener}} = Opts, + case check_max_connection(Type, Listener) of + allow -> + {Peername, PeerCert} = get_peer_info(Type, Listener, Req, Opts), + Sockname = cowboy_req:sock(Req), + WsCookie = get_ws_cookie(Req), + ConnInfo = #{ + socktype => ws, + peername => Peername, + sockname => Sockname, + peercert => PeerCert, + ws_cookie => WsCookie, + conn_mod => ?MODULE + }, + Limiter = emqx_limiter_container:get_limiter_by_names( + [?LIMITER_BYTES_IN, ?LIMITER_MESSAGE_IN], LimiterCfg + ), + MQTTPiggyback = get_ws_opts(Type, Listener, mqtt_piggyback), + FrameOpts = #{ + strict_mode => emqx_config:get_zone_conf(Zone, [mqtt, strict_mode]), + max_size => emqx_config:get_zone_conf(Zone, [mqtt, max_packet_size]) + }, + ParseState = emqx_frame:initial_parse_state(FrameOpts), + Serialize = emqx_frame:serialize_opts(), + Channel = emqx_channel:init(ConnInfo, Opts), + GcState = get_force_gc(Zone), + StatsTimer = get_stats_enable(Zone), + %% MQTT Idle Timeout + IdleTimeout = emqx_channel:get_mqtt_conf(Zone, idle_timeout), + IdleTimer = start_timer(IdleTimeout, idle_timeout), + tune_heap_size(Channel), + emqx_logger:set_metadata_peername(esockd:format(Peername)), + {ok, + #state{ + peername = Peername, + sockname = Sockname, + sockstate = running, + mqtt_piggyback = MQTTPiggyback, + limiter = Limiter, + parse_state = ParseState, + serialize = Serialize, + channel = Channel, + gc_state = GcState, + postponed = [], + stats_timer = StatsTimer, + idle_timeout = IdleTimeout, + idle_timer = IdleTimer, + zone = Zone, + listener = {Type, Listener}, + limiter_timer = undefined, + limiter_cache = queue:new() + }, + hibernate}; + {denny, Reason} -> + {stop, Reason} + end. + +tune_heap_size(Channel) -> case emqx_config:get_zone_conf( emqx_channel:info(zone, Channel), @@ -352,29 +339,56 @@ websocket_init([ of #{enable := false} -> ok; ShutdownPolicy -> emqx_misc:tune_heap_size(ShutdownPolicy) - end, - emqx_logger:set_metadata_peername(esockd:format(Peername)), - {ok, - #state{ - peername = Peername, - sockname = Sockname, - sockstate = running, - mqtt_piggyback = MQTTPiggyback, - limiter = Limiter, - parse_state = ParseState, - serialize = Serialize, - channel = Channel, - gc_state = GcState, - postponed = [], - stats_timer = StatsTimer, - idle_timeout = IdleTimeout, - idle_timer = IdleTimer, - zone = Zone, - listener = {Type, Listener}, - limiter_timer = undefined, - limiter_cache = queue:new() - }, - hibernate}. + end. + +get_stats_enable(Zone) -> + case emqx_config:get_zone_conf(Zone, [stats, enable]) of + true -> undefined; + false -> disabled + end. + +get_force_gc(Zone) -> + case emqx_config:get_zone_conf(Zone, [force_gc]) of + #{enable := false} -> undefined; + GcPolicy -> emqx_gc:init(GcPolicy) + end. + +get_ws_cookie(Req) -> + try + cowboy_req:parse_cookies(Req) + catch + error:badarg -> + ?SLOG(error, #{msg => "bad_cookie"}), + undefined; + Error:Reason -> + ?SLOG(error, #{ + msg => "failed_to_parse_cookie", + exception => Error, + reason => Reason + }), + undefined + end. + +get_peer_info(Type, Listener, Req, Opts) -> + case + emqx_config:get_listener_conf(Type, Listener, [proxy_protocol]) andalso + maps:get(proxy_header, Req) + of + #{src_address := SrcAddr, src_port := SrcPort, ssl := SSL} -> + SourceName = {SrcAddr, SrcPort}, + %% Notice: Only CN is available in Proxy Protocol V2 additional info + SourceSSL = + case maps:get(cn, SSL, undefined) of + undeined -> nossl; + CN -> [{pp2_ssl_cn, CN}] + end, + {SourceName, SourceSSL}; + #{src_address := SrcAddr, src_port := SrcPort} -> + SourceName = {SrcAddr, SrcPort}, + {SourceName, nossl}; + _ -> + {get_peer(Req, Opts), cowboy_req:cert(Req)} + end. websocket_handle({binary, Data}, State) when is_list(Data) -> websocket_handle({binary, iolist_to_binary(Data)}, State); @@ -1000,6 +1014,26 @@ get_peer(Req, #{listener := {Type, Listener}}) -> _:_ -> {Addr, PeerPort} end. +check_max_connection(Type, Listener) -> + case emqx_config:get_listener_conf(Type, Listener, [max_connections]) of + infinity -> + allow; + Max -> + MatchSpec = [{{'_', emqx_ws_connection}, [], [true]}], + Curr = ets:select_count(emqx_channel_conn, MatchSpec), + case Curr >= Max of + false -> + allow; + true -> + Reason = #{ + max => Max, + current => Curr, + msg => "websocket_max_connections_limited" + }, + ?SLOG(warning, Reason), + {denny, Reason} + end + end. %%-------------------------------------------------------------------- %% For CT tests %%-------------------------------------------------------------------- From fd0f32418ef64e2e50c7fcdaa794e123968e28e2 Mon Sep 17 00:00:00 2001 From: firest Date: Wed, 27 Apr 2022 15:41:14 +0800 Subject: [PATCH 24/43] fix(gateway): Add support for query by is_superuser --- .../i18n/emqx_gateway_api_authn_i18n.conf | 8 ++++ .../src/emqx_gateway_api_authn.erl | 12 ++++- .../test/emqx_gateway_api_SUITE.erl | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf b/apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf index 617474adb..db7d0ce65 100644 --- a/apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf +++ b/apps/emqx_gateway/i18n/emqx_gateway_api_authn_i18n.conf @@ -90,4 +90,12 @@ emqx_gateway_api_authn { zh: """Client ID 模糊搜索""" } } + + is_superuser { + desc { + en: """Is superuser""" + zh: """是否是超级用户""" + } + } + } diff --git a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl index 68941e516..ac071f8ff 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_authn.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_authn.erl @@ -195,7 +195,8 @@ parse_qstring(Qs) -> <<"page">>, <<"limit">>, <<"like_username">>, - <<"like_clientid">> + <<"like_clientid">>, + <<"is_superuser">> ], Qs ). @@ -397,6 +398,15 @@ params_fuzzy_in_qs() -> desc => ?DESC(like_clientid), example => <<"clientid">> } + )}, + {is_superuser, + mk( + boolean(), + #{ + in => query, + required => false, + desc => ?DESC(is_superuser) + } )} ]. diff --git a/apps/emqx_gateway/test/emqx_gateway_api_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_api_SUITE.erl index 260cff115..6bb111e60 100644 --- a/apps/emqx_gateway/test/emqx_gateway_api_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_api_SUITE.erl @@ -415,6 +415,53 @@ t_listeners_authn_data_mgmt(_) -> ), {204, _} = request(delete, "/gateway/stomp"). +t_authn_fuzzy_search(_) -> + GwConf = #{name => <<"stomp">>}, + {201, _} = request(post, "/gateway", GwConf), + {204, _} = request(get, "/gateway/stomp/authentication"), + + AuthConf = #{ + mechanism => <<"password_based">>, + backend => <<"built_in_database">>, + user_id_type => <<"clientid">> + }, + {201, _} = request(post, "/gateway/stomp/authentication", AuthConf), + {200, ConfResp} = request(get, "/gateway/stomp/authentication"), + assert_confs(AuthConf, ConfResp), + + Checker = fun({User, Fuzzy}) -> + {200, #{data := [UserRespd]}} = request( + get, "/gateway/stomp/authentication/users", Fuzzy + ), + assert_confs(UserRespd, User) + end, + + Create = fun(User) -> + {201, _} = request(post, "/gateway/stomp/authentication/users", User) + end, + + UserDatas = [ + #{ + user_id => <<"test">>, + password => <<"123456">>, + is_superuser => false + }, + #{ + user_id => <<"foo">>, + password => <<"123456">>, + is_superuser => true + } + ], + + FuzzyDatas = [[{<<"like_username">>, <<"test">>}], [{<<"is_superuser">>, <<"true">>}]], + + lists:foreach(Create, UserDatas), + lists:foreach(Checker, lists:zip(UserDatas, FuzzyDatas)), + + {204, _} = request(delete, "/gateway/stomp/authentication"), + {204, _} = request(get, "/gateway/stomp/authentication"), + {204, _} = request(delete, "/gateway/stomp"). + %%-------------------------------------------------------------------- %% Asserts From be67e27aa48d5aa904fa1ce5d814d57fce842f3e Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 27 Apr 2022 10:46:54 +0200 Subject: [PATCH 25/43] ci: build slim packages on self-hosted runners --- .github/workflows/build_slim_packages.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 7fd9eaa9b..6f374dda7 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -23,7 +23,7 @@ on: jobs: linux: - runs-on: ubuntu-20.04 + runs-on: aws-amd64 strategy: fail-fast: false From 106d45ca533c992c72bf0eca4278b4fce5f1c5e2 Mon Sep 17 00:00:00 2001 From: William Yang Date: Wed, 27 Apr 2022 11:48:20 +0200 Subject: [PATCH 26/43] ci(self-hosted): clean before run --- .github/workflows/build_slim_packages.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index 6f374dda7..a05424169 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -43,6 +43,7 @@ jobs: container: "ghcr.io/emqx/emqx-builder/5.0-10:${{ matrix.elixir }}-${{ matrix.otp }}-${{ matrix.os }}" steps: + - uses: AutoModality/action-clean@v1 - uses: actions/checkout@v1 - name: prepare run: | From bad3adbee617c8006566c544c4dd8ecfe0cdfe1b Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 27 Apr 2022 18:26:40 +0800 Subject: [PATCH 27/43] docs: improve i18n for zone --- apps/emqx/i18n/emqx_schema_i18n.conf | 233 +++++++++++++++++++++------ 1 file changed, 187 insertions(+), 46 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index bd507a5bf..ecb143d71 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -604,8 +604,12 @@ mqtt 下所有的配置作为全局的默认值存在,它可以被 zone< mqtt_idle_timeout { desc { - en: """Close TCP connections from the clients that have not sent MQTT CONNECT message within this interval.""" - zh: """关闭在此时间间隔内未发送 MQTT CONNECT 消息的客户端的 TCP 连接。""" + en: """After the TCP connection is established, if the MQTT CONNECT packet from the client is not received within the time specified by idle_timeout, the connection will be disconnected.""" + zh: """TCP 连接建立后,如果在 idle_timeout 指定的时间内未收到客户端的 MQTT CONNECT 报文,则连接将被断开。""" + } + label: { + en: """Idle Timeout""" + zh: """空闲超时""" } } @@ -614,19 +618,31 @@ mqtt 下所有的配置作为全局的默认值存在,它可以被 zone< en: """Maximum MQTT packet size allowed.""" zh: """允许的最大 MQTT 报文大小。""" } + label: { + en: """Max Packet Size""" + zh: """最大报文大小""" + } } mqtt_max_clientid_len { desc { - en: """Maximum allowed length of MQTT clientId.""" - zh: """允许的最大 MQTT Client ID 长度""" + en: """Maximum allowed length of MQTT Client ID.""" + zh: """允许的最大 MQTT Client ID 长度。""" + } + label: { + en: """Max Client ID Length""" + zh: """最大 Client ID 长度""" } } mqtt_max_topic_levels { desc { en: """Maximum topic levels allowed.""" - zh: """允许的 Topic 最大层级数""" + zh: """允许的最大主题层级。""" + } + label: { + en: """Max Topic Levels""" + zh: """最大主题层级""" } } @@ -635,40 +651,64 @@ mqtt 下所有的配置作为全局的默认值存在,它可以被 zone< en: """Maximum QoS allowed.""" zh: """允许的最大 QoS 等级。""" } + label: { + en: """Max QoS""" + zh: """最大 QoS""" + } } mqtt_max_topic_alias { desc { - en: """Maximum Topic Alias, 0 means no topic alias supported.""" + en: """Maximum topic alias, 0 means no topic alias supported.""" zh: """允许的最大主题别名数,0 表示不支持主题别名。""" } + label: { + en: """Max Topic Alias""" + zh: """最大主题别名""" + } } mqtt_retain_available { desc { - en: """Support MQTT retained messages.""" - zh: """是否支持 retained 消息。""" + en: """Whether to enable support for MQTT retained message.""" + zh: """是否启用对 MQTT 保留消息的支持。""" + } + label: { + en: """Retain Available""" + zh: """保留消息可用""" } } mqtt_wildcard_subscription { desc { - en: """Support MQTT Wildcard Subscriptions.""" - zh: """是否支持主题的通配符订阅。""" + en: """Whether to enable support for MQTT wildcard subscription.""" + zh: """是否启用对 MQTT 通配符订阅的支持。""" + } + label: { + en: """Wildcard Subscription Available""" + zh: """通配符订阅可用""" } } mqtt_shared_subscription { desc { - en: """Support MQTT Shared Subscriptions.""" - zh: """是否支持 MQTT 共享订阅""" + en: """Whether to enable support for MQTT shared subscription.""" + zh: """是否启用对 MQTT 共享订阅的支持。""" + } + label: { + en: """Shared Subscription Available""" + zh: """共享订阅可用""" } } mqtt_ignore_loop_deliver { desc { - en: """Ignore loop delivery of messages for MQTT v3.1.1/v3.1.0.""" - zh: """是否为 MQTT v3.1.1/v3.1.0 客户端忽略接收自己发布出消息""" + en: """Ignore loop delivery of messages for MQTT v3.1.1/v3.1.0, similar to No Local subscription option in MQTT 5.0""" + zh: """是否为 MQTT v3.1.1/v3.1.0 客户端忽略投递自己发布的消息,类似于 MQTT 5.0 中的 No Local 订阅选项""" + } + label: { + en: """Ignore Loop Deliver""" + zh: """忽略循环投递""" } } @@ -679,35 +719,53 @@ When set to true, invalid utf8 strings in for example client ID, topic name, etc zh: """是否以严格模式解析 MQTT 消息。 当设置为 true 时,例如客户端 ID、主题名称等中的无效 utf8 字符串将导致客户端断开连接。""" } + label: { + en: """Strict Mode""" + zh: """严格模式""" + } } mqtt_response_information { desc { - en: """Specify the response information returned to the client. This feature is disabled if is set to \"\".""" - zh: """指定返回给客户端的响应信息。如果设置为 \"\",则禁用此功能。""" + en: """Specify the response information returned to the client. This feature is disabled if is set to \"\". Applies only to clients using MQTT 5.0.""" + zh: """指定返回给客户端的响应信息。如果设置为 \"\",则禁用此功能。仅适用于使用 MQTT 5.0 协议的客户端。""" + } + label: { + en: """Response Information""" + zh: """响应信息""" } } mqtt_server_keepalive { desc { - en: """'Server Keep Alive' of MQTT 5.0. -If the server returns a 'Server Keep Alive' in the CONNACK packet, the client MUST use that value instead of the value it sent as the 'Keep Alive'.""" - zh: """MQTT 5.0 的 'Server Keep Alive' 属性。 -如果服务器在 CONNACK 数据包中返回'Server Keep Alive',则客户端必须使用该值作为实际的 'Keep Alive' 值。""" + en: """The keep alive that EMQX requires the client to use. If configured as disabled, it means that the keep alive specified by the client will be used. Requires Server Keep Alive in MQTT 5.0, so it is only applicable to clients using MQTT 5.0 protocol.""" + zh: """EMQX 要求客户端使用的保活时间,配置为 disabled 表示将使用客户端指定的保活时间。需要用到 MQTT 5.0 中的 Server Keep Alive,因此仅适用于使用 MQTT 5.0 协议的客户端。""" + } + label: { + en: """Server Keep Alive""" + zh: """服务端保持连接""" } } mqtt_keepalive_backoff { desc { - en: """The backoff for MQTT keepalive timeout. The broker will close the connection after idling for 'Keepalive * backoff * 2'.""" - zh: """Broker 判定客户端 Keep Alive 超时的退避乘数。EMQX 将在'Keepalive * backoff * 2' 空闲后关闭连接。""" + en: """The backoff multiplier used by the broker to determine the client keep alive timeout. If EMQX doesn't receive any packet in Keep Alive * Backoff * 2 seconds, EMQX will close the current connection.""" + zh: """Broker 判定客户端保活超时使用的退避乘数。如果 EMQX 在 Keep Alive * Backoff * 2 秒内未收到任何报文,EMQX 将关闭当前连接。""" + } + label: { + en: """Keep Alive Backoff""" + zh: """保持连接退避乘数""" } } mqtt_max_subscriptions { desc { - en: """Maximum number of subscriptions allowed.""" - zh: """允许的每个客户端最大订阅数""" + en: """Maximum number of subscriptions allowed per client.""" + zh: """允许每个客户端建立的最大订阅数量。""" + } + label: { + en: """Max Subscriptions""" + zh: """最大订阅数量""" } } @@ -716,40 +774,65 @@ If the server returns a 'Server Keep Alive' in the CONNACK packet, the client MU en: """Force upgrade of QoS level according to subscription.""" zh: """投递消息时,是否根据订阅主题时的 QoS 等级来强制提升派发的消息的 QoS 等级。""" } + label: { + en: """Upgrade QoS""" + zh: """升级 QoS""" + } } mqtt_max_inflight { desc { - en: """Maximum size of the Inflight Window storing QoS1/2 messages delivered but un-acked.""" - zh: """飞行窗口的最大值。""" + en: """Maximum number of QoS 1 and QoS 2 messages that are allowed to be delivered simultaneously before completing the acknowledgment.""" + zh: """允许在完成应答前同时投递的 QoS 1 和 QoS 2 消息的最大数量。""" } + label: { + en: """Max Inflight""" + zh: """最大飞行窗口""" + } + } mqtt_retry_interval { desc { - en: """Retry interval for QoS1/2 message delivering.""" - zh: """QoS1/2 消息的重新投递间隔。""" + en: """Retry interval for QoS 1/2 message delivering.""" + zh: """QoS 1/2 消息的重新投递间隔。""" + } + label: { + en: """Retry Interval""" + zh: """重试间隔""" } } mqtt_max_awaiting_rel { desc { - en: """Maximum QoS2 packets (Client -> Broker) awaiting PUBREL.""" + en: """Maximum QoS 2 packets (Client -> Broker) awaiting PUBREL.""" zh: """PUBREL (Client -> Broker) 最大等待队列长度。""" } + label: { + en: """Max Awaiting PUBREL""" + zh: """Max Awaiting PUBREL""" + } } mqtt_await_rel_timeout { desc { - en: """The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout.""" + en: """The QoS 2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout.""" zh: """PUBREL (Client -> Broker) 最大等待时间,超时则会被丢弃。""" } + label: { + en: """Max Awaiting PUBREL TIMEOUT""" + zh: """Max Awaiting PUBREL TIMEOUT""" + } } mqtt_session_expiry_interval { desc { - en: """Default session expiry interval for MQTT V3.1.1 connections.""" - zh: """Session 默认超时时间。""" + en: """Specifies how long the session will expire after the connection is disconnected, only for non-MQTT 5.0 connections.""" + zh: """指定会话将在连接断开后多久过期,仅适用于非 MQTT 5.0 的连接。""" + } + label: { + en: """Session Expiry Interval""" + zh: """会话过期间隔""" } } @@ -758,6 +841,10 @@ If the server returns a 'Server Keep Alive' in the CONNACK packet, the client MU en: """Maximum queue length. Enqueued messages when persistent client disconnected, or inflight window is full.""" zh: """消息队列最大长度。持久客户端断开连接或飞行窗口已满时排队的消息长度。""" } + label: { + en: """Max Message Queue Length""" + zh: """最大消息队列长度""" + } } mqtt_mqueue_priorities { @@ -783,42 +870,96 @@ To configure \"topic/1\" > \"topic/2\": mqueue_priorities: {\"topic/1\": 10, \"topic/2\": 8} """ } + label: { + en: """Topic Priorities""" + zh: """主题优先级""" + } } mqtt_mqueue_default_priority { desc { - en: """Default to the highest priority for topics not matching priority table.""" - zh: """主题默认的优先级,不在 mqtt.mqueue_priorities 中的主题将会使用该优先级。""" + en: """Default topic priority, which will be used by topics not in Topic Priorities (Configuration item is mqtt.mqueue_priorities).""" + zh: """默认的主题优先级,不在 主题优先级(配置项为 mqtt.mqueue_priorities) 中的主题将会使用该优先级。""" + } + label: { + en: """Default Topic Priorities""" + zh: """默认主题优先级""" } } mqtt_mqueue_store_qos0 { desc { - en: """Support enqueue QoS0 messages.""" - zh: """消息队列是否存储 QoS0 消息。""" + en: """Specifies whether to store QoS 0 messages in the message queue while the connection is down but the session remains.""" + zh: """指定在连接断开但会话保持期间,是否需要在消息队列中存储 QoS 0 消息。""" + } + label: { + en: """Store QoS 0 Message""" + zh: """存储 QoS 0 消息""" } } mqtt_use_username_as_clientid { desc { - en: """Replace client ID with the username.""" - zh: """是否使用 Username 替换 Client ID。""" + en: """Whether to user Client ID as Username. +This setting takes effect later than Use Peer Certificate as Username (Configuration item is peer_cert_as_username) and Use peer certificate as Client ID (Configuration item is peer_cert_as_clientid). +""" + zh: """是否使用用户名作为客户端 ID。 +此设置的作用时间晚于 使用对端证书作为用户名(配置项为 peer_cert_as_username) 和 使用对端证书作为客户端 ID(配置项为 peer_cert_as_clientid)。 +""" + } + label: { + en: """Use Username as Client ID""" + zh: """使用用户名作为客户端 ID""" } } mqtt_peer_cert_as_username { desc { - en: """Use the CN, DN or CRT field from the client certificate as a username. -Only works for the TLS connection.""" - zh: """使用客户端证书中的 CN, DN 字段或整个证书来作为客户端用户名。""" + en: """Use the CN, DN field in the peer certificate or the entire certificate content as Username. Only works for the TLS connection. +Supported configurations are the following: +- cn: Take the CN field of the certificate as Username +- dn: Take the DN field of the certificate as Username +- crt: Take the content of the DER or PEM certificate as Username +- pem: Convert DER certificate content to PEM format as Username +- md5: Take the MD5 value of the content of the DER or PEM certificate as Username +""" + zh: """使用对端证书中的 CN, DN 字段或整个证书内容来作为用户名。仅适用于 TLS 连接。 +目前支持配置为以下内容: +- cn: 取证书的 CN 字段作为 Username +- dn: 取证书的 DN 字段作为 Username +- crt: 取 DERPEM 证书的内容作为 Username +- pem: 将 DER 证书内容转换为 PEM 格式后作为 Username +- md5: 取 DERPEM 证书的内容的 MD5 值作为 Username +""" + } + label: { + en: """Use Peer Certificate as Username""" + zh: """使用对端证书作为用户名""" } } mqtt_peer_cert_as_clientid { desc { - en: """Use the CN, DN or CRT field from the client certificate as a clientid. -Only works for the TLS connection.""" - zh: """使用客户端证书中的 CN, DN 字段或整个证书来作为客户端 ID。""" + en: """Use the CN, DN field in the peer certificate or the entire certificate content as Client ID. Only works for the TLS connection. +Supported configurations are the following: +- cn: Take the CN field of the certificate as Client ID +- dn: Take the DN field of the certificate as Client ID +- crt: Take the content of the DER or PEM certificate as Client ID +- pem: Convert DER certificate content to PEM format as Client ID +- md5: Take the MD5 value of the content of the DER or PEM certificate as Client ID +""" + zh: """使用对端证书中的 CN, DN 字段或整个证书内容来作为客户端 ID。仅适用于 TLS 连接。 +目前支持配置为以下内容: +- cn: 取证书的 CN 字段作为 Client ID +- dn: 取证书的 DN 字段作为 Client ID +- crt: 取 DERPEM 证书的内容作为 Client ID +- pem: 将 DER 证书内容转换为 PEM 格式后作为 Client ID +- md5: 取 DERPEM 证书的内容的 MD5 值作为 Client ID +""" + } + label: { + en: """Use Peer Certificate as Client ID""" + zh: """使用对端证书作为客户端 ID""" } } @@ -874,11 +1015,11 @@ Only works for the TLS connection.""" broker_shared_dispatch_ack_enabled { desc { - en: """Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages. + en: """Enable/disable shared dispatch acknowledgement for QoS 1 and QoS 2 messages. This should allow messages to be dispatched to a different subscriber in the group in case the picked (based on `shared_subscription_strategy`) subscriber is offline. """ - zh: """启用/禁用 QoS1 和 QoS2 消息的共享派发确认。 + zh: """启用/禁用 QoS 1 和 QoS 2 消息的共享派发确认。 开启后,允许将消息从未及时回复 ACK 的订阅者 (例如,客户端离线)重新派发给另外一个订阅者。 """ } From a83268a0f12d0d1f570e557529508e2ff552c4fb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 27 Apr 2022 18:35:34 +0800 Subject: [PATCH 28/43] docs: improve descs that mention configuration items --- apps/emqx/i18n/emqx_schema_i18n.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx/i18n/emqx_schema_i18n.conf b/apps/emqx/i18n/emqx_schema_i18n.conf index ecb143d71..5765e18a4 100644 --- a/apps/emqx/i18n/emqx_schema_i18n.conf +++ b/apps/emqx/i18n/emqx_schema_i18n.conf @@ -878,8 +878,8 @@ To configure \"topic/1\" > \"topic/2\": mqtt_mqueue_default_priority { desc { - en: """Default topic priority, which will be used by topics not in Topic Priorities (Configuration item is mqtt.mqueue_priorities).""" - zh: """默认的主题优先级,不在 主题优先级(配置项为 mqtt.mqueue_priorities) 中的主题将会使用该优先级。""" + en: """Default topic priority, which will be used by topics not in Topic Priorities (mqueue_priorities).""" + zh: """默认的主题优先级,不在 主题优先级mqueue_priorities) 中的主题将会使用该优先级。""" } label: { en: """Default Topic Priorities""" @@ -901,10 +901,10 @@ To configure \"topic/1\" > \"topic/2\": mqtt_use_username_as_clientid { desc { en: """Whether to user Client ID as Username. -This setting takes effect later than Use Peer Certificate as Username (Configuration item is peer_cert_as_username) and Use peer certificate as Client ID (Configuration item is peer_cert_as_clientid). +This setting takes effect later than Use Peer Certificate as Username (peer_cert_as_username) and Use peer certificate as Client ID (peer_cert_as_clientid). """ zh: """是否使用用户名作为客户端 ID。 -此设置的作用时间晚于 使用对端证书作为用户名(配置项为 peer_cert_as_username) 和 使用对端证书作为客户端 ID(配置项为 peer_cert_as_clientid)。 +此设置的作用时间晚于 使用对端证书作为用户名peer_cert_as_username) 和 使用对端证书作为客户端 IDpeer_cert_as_clientid)。 """ } label: { From 37be7a4977d75342954411d70c90d3f279dcf12b Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 27 Apr 2022 08:00:05 +0200 Subject: [PATCH 29/43] chore: update check-format.sh to reformat all apps --- scripts/check-format.sh | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/scripts/check-format.sh b/scripts/check-format.sh index d97863eec..66165833f 100755 --- a/scripts/check-format.sh +++ b/scripts/check-format.sh @@ -7,19 +7,8 @@ set -euo pipefail cd -P -- "$(dirname -- "$0")/.." -APPS=() -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') -APPS+=( 'apps/emqx_management') -APPS+=( 'apps/emqx_psk') -APPS+=( 'apps/emqx_plugin_libs' 'apps/emqx_machine' 'apps/emqx_statsd' ) -APPS+=( 'apps/emqx_auto_subscribe' 'apps/emqx_conf') -APPS+=( 'apps/emqx_dashboard') - -for app in "${APPS[@]}"; do +APPS="$(./scripts/find-apps.sh | xargs)" +for app in ${APPS}; do echo "$app ..." ./scripts/format_app.py -a "$app" -f done From 02c3f87b316e8370287d5cd46de4f103ffe48433 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 27 Apr 2022 15:51:18 +0200 Subject: [PATCH 30/43] style: reformat all remaining apps --- apps/emqx_bridge/rebar.config | 7 +- apps/emqx_bridge/src/emqx_bridge.app.src | 32 +- apps/emqx_bridge/src/emqx_bridge.erl | 429 ++- apps/emqx_bridge/src/emqx_bridge_api.erl | 458 ++- apps/emqx_bridge/src/emqx_bridge_app.erl | 7 +- .../src/emqx_bridge_http_schema.erl | 146 +- apps/emqx_bridge/src/emqx_bridge_monitor.erl | 49 +- .../src/emqx_bridge_mqtt_schema.erl | 52 +- apps/emqx_bridge/src/emqx_bridge_schema.erl | 159 +- apps/emqx_bridge/src/emqx_bridge_sup.erl | 20 +- .../src/proto/emqx_bridge_proto_v1.erl | 46 +- apps/emqx_bridge/test/emqx_bridge_SUITE.erl | 115 +- .../test/emqx_bridge_api_SUITE.erl | 271 +- .../emqx_connector/include/emqx_connector.hrl | 15 +- apps/emqx_connector/rebar.config | 40 +- .../emqx_connector/src/emqx_connector.app.src | 50 +- apps/emqx_connector/src/emqx_connector.erl | 94 +- .../emqx_connector/src/emqx_connector_api.erl | 124 +- .../src/emqx_connector_http.erl | 423 ++- .../src/emqx_connector_ldap.erl | 188 +- .../src/emqx_connector_mongo.erl | 389 ++- .../src/emqx_connector_mqtt.erl | 153 +- .../src/emqx_connector_mysql.erl | 149 +- .../src/emqx_connector_pgsql.erl | 121 +- .../src/emqx_connector_redis.erl | 235 +- .../src/emqx_connector_schema.erl | 34 +- .../src/emqx_connector_schema_lib.erl | 73 +- .../emqx_connector/src/emqx_connector_ssl.erl | 11 +- .../emqx_connector/src/emqx_connector_sup.erl | 22 +- .../src/mqtt/emqx_connector_mqtt_mod.erl | 181 +- .../src/mqtt/emqx_connector_mqtt_msg.erl | 75 +- .../src/mqtt/emqx_connector_mqtt_schema.erl | 389 ++- .../src/mqtt/emqx_connector_mqtt_worker.erl | 256 +- .../test/emqx_connector_api_SUITE.erl | 641 ++-- .../test/emqx_connector_mongo_SUITE.erl | 61 +- .../test/emqx_connector_mqtt_tests.erl | 39 +- .../test/emqx_connector_mqtt_worker_tests.erl | 16 +- .../test/emqx_connector_mysql_SUITE.erl | 67 +- .../test/emqx_connector_pgsql_SUITE.erl | 65 +- .../test/emqx_connector_redis_SUITE.erl | 51 +- .../test/emqx_connector_test_helpers.erl | 17 +- apps/emqx_plugins/rebar.config | 5 +- apps/emqx_plugins/src/emqx_plugins.app.src | 16 +- apps/emqx_plugins/src/emqx_plugins.erl | 442 ++- apps/emqx_plugins/src/emqx_plugins_app.erl | 10 +- apps/emqx_plugins/src/emqx_plugins_cli.erl | 44 +- apps/emqx_plugins/src/emqx_plugins_schema.erl | 60 +- apps/emqx_plugins/src/emqx_plugins_sup.erl | 3 +- apps/emqx_plugins/test/emqx_plugins_SUITE.erl | 263 +- apps/emqx_plugins/test/emqx_plugins_tests.erl | 46 +- apps/emqx_prometheus/rebar.config | 39 +- .../src/emqx_prometheus.app.src | 30 +- apps/emqx_prometheus/src/emqx_prometheus.erl | 362 +- .../src/emqx_prometheus_api.erl | 93 +- .../src/emqx_prometheus_app.erl | 7 +- .../src/emqx_prometheus_mria.erl | 61 +- .../src/emqx_prometheus_schema.erl | 56 +- .../src/emqx_prometheus_sup.erl | 27 +- .../src/proto/emqx_prometheus_proto_v1.erl | 9 +- .../test/emqx_prometheus_SUITE.erl | 15 +- .../test/emqx_prometheus_api_SUITE.erl | 11 +- apps/emqx_resource/include/emqx_resource.hrl | 13 +- .../include/emqx_resource_utils.hrl | 16 +- apps/emqx_resource/rebar.config | 21 +- apps/emqx_resource/src/emqx_resource.app.src | 34 +- apps/emqx_resource/src/emqx_resource.erl | 279 +- .../src/emqx_resource_health_check.erl | 55 +- .../src/emqx_resource_instance.erl | 136 +- apps/emqx_resource/src/emqx_resource_sup.erl | 39 +- .../src/emqx_resource_validator.erl | 23 +- .../src/proto/emqx_resource_proto_v1.erl | 60 +- .../test/emqx_resource_SUITE.erl | 236 +- .../emqx_resource/test/emqx_test_resource.erl | 68 +- apps/emqx_rule_engine/include/rule_engine.hrl | 93 +- apps/emqx_rule_engine/rebar.config | 35 +- .../src/emqx_rule_api_schema.erl | 431 +-- apps/emqx_rule_engine/src/emqx_rule_date.erl | 244 +- .../src/emqx_rule_engine.app.src | 30 +- .../emqx_rule_engine/src/emqx_rule_engine.erl | 269 +- .../src/emqx_rule_engine_api.erl | 310 +- .../src/emqx_rule_engine_schema.erl | 215 +- .../src/emqx_rule_engine_sup.erl | 14 +- .../emqx_rule_engine/src/emqx_rule_events.erl | 1215 ++++--- apps/emqx_rule_engine/src/emqx_rule_funcs.erl | 637 ++-- apps/emqx_rule_engine/src/emqx_rule_maps.erl | 112 +- .../src/emqx_rule_outputs.erl | 101 +- .../src/emqx_rule_runtime.erl | 225 +- .../src/emqx_rule_sqlparser.erl | 83 +- .../src/emqx_rule_sqltester.erl | 23 +- .../src/proto/emqx_rule_engine_proto_v1.erl | 9 +- .../test/emqx_rule_engine_SUITE.erl | 2954 +++++++++++------ .../test/emqx_rule_engine_api_SUITE.erl | 31 +- .../test/emqx_rule_events_SUITE.erl | 41 +- .../test/emqx_rule_funcs_SUITE.erl | 632 ++-- .../test/emqx_rule_maps_SUITE.erl | 279 +- apps/emqx_rule_engine/test/prop_rule_maps.erl | 16 +- 96 files changed, 9988 insertions(+), 6360 deletions(-) diff --git a/apps/emqx_bridge/rebar.config b/apps/emqx_bridge/rebar.config index d24d23f8c..0a1cbc29b 100644 --- a/apps/emqx_bridge/rebar.config +++ b/apps/emqx_bridge/rebar.config @@ -1,8 +1,9 @@ {erl_opts, [debug_info]}. -{deps, [ {emqx, {path, "../emqx"}} - ]}. +{deps, [{emqx, {path, "../emqx"}}]}. {shell, [ - % {config, "config/sys.config"}, + % {config, "config/sys.config"}, {apps, [emqx_bridge]} ]}. + +{project_plugins, [erlfmt]}. diff --git a/apps/emqx_bridge/src/emqx_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src index 2a2f11603..70550efe4 100644 --- a/apps/emqx_bridge/src/emqx_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,18 +1,18 @@ %% -*- mode: erlang -*- -{application, emqx_bridge, - [{description, "An OTP application"}, - {vsn, "0.1.0"}, - {registered, []}, - {mod, {emqx_bridge_app, []}}, - {applications, - [kernel, - stdlib, - emqx, - emqx_connector - ]}, - {env,[]}, - {modules, []}, +{application, emqx_bridge, [ + {description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_bridge_app, []}}, + {applications, [ + kernel, + stdlib, + emqx, + emqx_connector + ]}, + {env, []}, + {modules, []}, - {licenses, ["Apache 2.0"]}, - {links, []} - ]}. + {licenses, ["Apache 2.0"]}, + {links, []} +]}. diff --git a/apps/emqx_bridge/src/emqx_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl index b2939f9a9..88f12c1f5 100644 --- a/apps/emqx_bridge/src/emqx_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -18,48 +18,48 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ post_config_update/5 - ]). +-export([post_config_update/5]). --export([ load_hook/0 - , unload_hook/0 - ]). +-export([ + load_hook/0, + unload_hook/0 +]). -export([on_message_publish/1]). --export([ resource_type/1 - , bridge_type/1 - , resource_id/1 - , resource_id/2 - , bridge_id/2 - , parse_bridge_id/1 - ]). +-export([ + resource_type/1, + bridge_type/1, + resource_id/1, + resource_id/2, + bridge_id/2, + parse_bridge_id/1 +]). --export([ load/0 - , lookup/1 - , lookup/2 - , lookup/3 - , list/0 - , list_bridges_by_connector/1 - , create/2 - , create/3 - , recreate/2 - , recreate/3 - , create_dry_run/2 - , remove/1 - , remove/2 - , update/2 - , update/3 - , stop/2 - , restart/2 - , reset_metrics/1 - ]). +-export([ + load/0, + lookup/1, + lookup/2, + lookup/3, + list/0, + list_bridges_by_connector/1, + create/2, + create/3, + recreate/2, + recreate/3, + create_dry_run/2, + remove/1, + remove/2, + update/2, + update/3, + stop/2, + restart/2, + reset_metrics/1 +]). --export([ send_message/2 - ]). +-export([send_message/2]). --export([ config_key_path/0 - ]). +-export([config_key_path/0]). %% exported for `emqx_telemetry' -export([get_basic_usage_info/0]). @@ -69,18 +69,25 @@ load_hook() -> load_hook(Bridges). load_hook(Bridges) -> - lists:foreach(fun({_Type, Bridge}) -> - lists:foreach(fun({_Name, BridgeConf}) -> + lists:foreach( + fun({_Type, Bridge}) -> + lists:foreach( + fun({_Name, BridgeConf}) -> do_load_hook(BridgeConf) - end, maps:to_list(Bridge)) - end, maps:to_list(Bridges)). + end, + maps:to_list(Bridge) + ) + end, + maps:to_list(Bridges) + ). do_load_hook(#{local_topic := _} = Conf) -> case maps:get(direction, Conf, egress) of egress -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}); ingress -> ok end; -do_load_hook(_Conf) -> ok. +do_load_hook(_Conf) -> + ok. unload_hook() -> ok = emqx_hooks:del('message.publish', {?MODULE, on_message_publish}). @@ -90,23 +97,36 @@ on_message_publish(Message = #message{topic = Topic, flags = Flags}) -> false -> Msg = emqx_rule_events:eventmsg_publish(Message), send_to_matched_egress_bridges(Topic, Msg); - true -> ok + true -> + ok end, {ok, Message}. send_to_matched_egress_bridges(Topic, Msg) -> - lists:foreach(fun (Id) -> - try send_message(Id, Msg) of - {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_exception", - bridge => Id, error => Err, reason => Reason, - stacktrace => ST}) - end - end, get_matched_bridges(Topic)). + lists:foreach( + fun(Id) -> + try send_message(Id, Msg) of + {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_exception", + bridge => Id, + error => Err, + reason => Reason, + stacktrace => ST + }) + end + end, + get_matched_bridges(Topic) + ). send_message(BridgeId, Message) -> {BridgeType, BridgeName} = parse_bridge_id(BridgeId), @@ -132,8 +152,8 @@ bridge_type(emqx_connector_mqtt) -> mqtt; bridge_type(emqx_connector_http) -> http. post_config_update(_, _Req, NewConf, OldConf, _AppEnv) -> - #{added := Added, removed := Removed, changed := Updated} - = diff_confs(NewConf, OldConf), + #{added := Added, removed := Removed, changed := Updated} = + diff_confs(NewConf, OldConf), %% The config update will be failed if any task in `perform_bridge_changes` failed. Result = perform_bridge_changes([ {fun remove/3, Removed}, @@ -150,15 +170,19 @@ perform_bridge_changes(Tasks) -> perform_bridge_changes([], Result) -> Result; perform_bridge_changes([{Action, MapConfs} | Tasks], Result0) -> - Result = maps:fold(fun - ({_Type, _Name}, _Conf, {error, Reason}) -> - {error, Reason}; - ({Type, Name}, Conf, _) -> - case Action(Type, Name, Conf) of - {error, Reason} -> {error, Reason}; - Return -> Return - end - end, Result0, MapConfs), + Result = maps:fold( + fun + ({_Type, _Name}, _Conf, {error, Reason}) -> + {error, Reason}; + ({Type, Name}, Conf, _) -> + case Action(Type, Name, Conf) of + {error, Reason} -> {error, Reason}; + Return -> Return + end + end, + Result0, + MapConfs + ), perform_bridge_changes(Tasks, Result). load() -> @@ -184,18 +208,29 @@ parse_bridge_id(BridgeId) -> end. list() -> - lists:foldl(fun({Type, NameAndConf}, Bridges) -> - lists:foldl(fun({Name, RawConf}, Acc) -> + lists:foldl( + fun({Type, NameAndConf}, Bridges) -> + lists:foldl( + fun({Name, RawConf}, Acc) -> case lookup(Type, Name, RawConf) of {error, not_found} -> Acc; {ok, Res} -> [Res | Acc] end - end, Bridges, maps:to_list(NameAndConf)) - end, [], maps:to_list(emqx:get_raw_config([bridges], #{}))). + end, + Bridges, + maps:to_list(NameAndConf) + ) + end, + [], + maps:to_list(emqx:get_raw_config([bridges], #{})) + ). list_bridges_by_connector(ConnectorId) -> - [B || B = #{raw_config := #{<<"connector">> := Id}} <- list(), - ConnectorId =:= Id]. + [ + B + || B = #{raw_config := #{<<"connector">> := Id}} <- list(), + ConnectorId =:= Id + ]. lookup(Id) -> {Type, Name} = parse_bridge_id(Id), @@ -206,10 +241,15 @@ lookup(Type, Name) -> lookup(Type, Name, RawConf). lookup(Type, Name, RawConf) -> case emqx_resource:get_instance(resource_id(Type, Name)) of - {error, not_found} -> {error, not_found}; + {error, not_found} -> + {error, not_found}; {ok, _, Data} -> - {ok, #{type => Type, name => Name, resource_data => Data, - raw_config => RawConf}} + {ok, #{ + type => Type, + name => Name, + resource_data => Data, + raw_config => RawConf + }} end. reset_metrics(ResourceId) -> @@ -227,13 +267,21 @@ create(BridgeId, Conf) -> create(BridgeType, BridgeName, Conf). create(Type, Name, Conf) -> - ?SLOG(info, #{msg => "create bridge", type => Type, name => Name, - config => Conf}), - case emqx_resource:create_local(resource_id(Type, Name), - <<"emqx_bridge">>, - emqx_bridge:resource_type(Type), - parse_confs(Type, Name, Conf), - #{}) of + ?SLOG(info, #{ + msg => "create bridge", + type => Type, + name => Name, + config => Conf + }), + case + emqx_resource:create_local( + resource_id(Type, Name), + <<"emqx_bridge">>, + emqx_bridge:resource_type(Type), + parse_confs(Type, Name, Conf), + #{} + ) + of {ok, already_created} -> maybe_disable_bridge(Type, Name, Conf); {ok, _} -> maybe_disable_bridge(Type, Name, Conf); {error, Reason} -> {error, Reason} @@ -254,15 +302,25 @@ update(Type, Name, {OldConf, Conf}) -> %% case if_only_to_toggle_enable(OldConf, Conf) of false -> - ?SLOG(info, #{msg => "update bridge", type => Type, name => Name, - config => Conf}), + ?SLOG(info, #{ + msg => "update bridge", + type => Type, + name => Name, + config => Conf + }), case recreate(Type, Name, Conf) of - {ok, _} -> maybe_disable_bridge(Type, Name, Conf); + {ok, _} -> + maybe_disable_bridge(Type, Name, Conf); {error, not_found} -> - ?SLOG(warning, #{ msg => "updating_a_non-exist_bridge_need_create_a_new_one" - , type => Type, name => Name, config => Conf}), + ?SLOG(warning, #{ + msg => "updating_a_non-exist_bridge_need_create_a_new_one", + type => Type, + name => Name, + config => Conf + }), create(Type, Name, Conf); - {error, Reason} -> {error, {update_bridge_failed, Reason}} + {error, Reason} -> + {error, {update_bridge_failed, Reason}} end; true -> %% we don't need to recreate the bridge if this config change is only to @@ -277,22 +335,25 @@ recreate(Type, Name) -> recreate(Type, Name, emqx:get_config([bridges, Type, Name])). recreate(Type, Name, Conf) -> - emqx_resource:recreate_local(resource_id(Type, Name), + emqx_resource:recreate_local( + resource_id(Type, Name), emqx_bridge:resource_type(Type), parse_confs(Type, Name, Conf), - #{}). + #{} + ). create_dry_run(Type, Conf) -> - - Conf0 = Conf#{<<"egress">> => - #{ <<"remote_topic">> => <<"t">> - , <<"remote_qos">> => 0 - , <<"retain">> => true - , <<"payload">> => <<"val">> - }, - <<"ingress">> => - #{ <<"remote_topic">> => <<"t">> - }}, + Conf0 = Conf#{ + <<"egress">> => + #{ + <<"remote_topic">> => <<"t">>, + <<"remote_qos">> => 0, + <<"retain">> => true, + <<"payload">> => <<"val">> + }, + <<"ingress">> => + #{<<"remote_topic">> => <<"t">>} + }, case emqx_resource:check_config(emqx_bridge:resource_type(Type), Conf0) of {ok, Conf1} -> emqx_resource:create_dry_run_local(emqx_bridge:resource_type(Type), Conf1); @@ -313,35 +374,48 @@ remove(Type, Name, _Conf) -> case emqx_resource:remove_local(resource_id(Type, Name)) of ok -> ok; {error, not_found} -> ok; - {error, Reason} -> - {error, Reason} + {error, Reason} -> {error, Reason} end. diff_confs(NewConfs, OldConfs) -> - emqx_map_lib:diff_maps(flatten_confs(NewConfs), - flatten_confs(OldConfs)). + emqx_map_lib:diff_maps( + flatten_confs(NewConfs), + flatten_confs(OldConfs) + ). flatten_confs(Conf0) -> maps:from_list( - lists:flatmap(fun({Type, Conf}) -> + lists:flatmap( + fun({Type, Conf}) -> do_flatten_confs(Type, Conf) - end, maps:to_list(Conf0))). + end, + maps:to_list(Conf0) + ) + ). do_flatten_confs(Type, Conf0) -> [{{Type, Name}, Conf} || {Name, Conf} <- maps:to_list(Conf0)]. get_matched_bridges(Topic) -> Bridges = emqx:get_config([bridges], #{}), - maps:fold(fun (BType, Conf, Acc0) -> - maps:fold(fun - %% Confs for MQTT, Kafka bridges have the `direction` flag - (_BName, #{direction := ingress}, Acc1) -> - Acc1; - (BName, #{direction := egress} = Egress, Acc1) -> - %% HTTP, MySQL bridges only have egress direction - get_matched_bridge_id(Egress, Topic, BType, BName, Acc1) - end, Acc0, Conf) - end, [], Bridges). + maps:fold( + fun(BType, Conf, Acc0) -> + maps:fold( + fun + %% Confs for MQTT, Kafka bridges have the `direction` flag + (_BName, #{direction := ingress}, Acc1) -> + Acc1; + (BName, #{direction := egress} = Egress, Acc1) -> + %% HTTP, MySQL bridges only have egress direction + get_matched_bridge_id(Egress, Topic, BType, BName, Acc1) + end, + Acc0, + Conf + ) + end, + [], + Bridges + ). get_matched_bridge_id(#{enable := false}, _Topic, _BType, _BName, Acc) -> Acc; @@ -351,38 +425,56 @@ get_matched_bridge_id(#{local_topic := Filter}, Topic, BType, BName, Acc) -> false -> Acc end. -parse_confs(http, _Name, - #{ url := Url - , method := Method - , body := Body - , headers := Headers - , request_timeout := ReqTimeout - } = Conf) -> +parse_confs( + http, + _Name, + #{ + url := Url, + method := Method, + body := Body, + headers := Headers, + request_timeout := ReqTimeout + } = Conf +) -> {BaseUrl, Path} = parse_url(Url), {ok, BaseUrl2} = emqx_http_lib:uri_parse(BaseUrl), - Conf#{ base_url => BaseUrl2 - , request => - #{ path => Path - , method => Method - , body => Body - , headers => Headers - , request_timeout => ReqTimeout - } - }; -parse_confs(Type, Name, #{connector := ConnId, direction := Direction} = Conf) - when is_binary(ConnId) -> + Conf#{ + base_url => BaseUrl2, + request => + #{ + path => Path, + method => Method, + body => Body, + headers => Headers, + request_timeout => ReqTimeout + } + }; +parse_confs(Type, Name, #{connector := ConnId, direction := Direction} = Conf) when + is_binary(ConnId) +-> case emqx_connector:parse_connector_id(ConnId) of {Type, ConnName} -> ConnectorConfs = emqx:get_config([connectors, Type, ConnName]), - make_resource_confs(Direction, ConnectorConfs, - maps:without([connector, direction], Conf), Type, Name); + make_resource_confs( + Direction, + ConnectorConfs, + maps:without([connector, direction], Conf), + Type, + Name + ); {_ConnType, _ConnName} -> error({cannot_use_connector_with_different_type, ConnId}) end; -parse_confs(Type, Name, #{connector := ConnectorConfs, direction := Direction} = Conf) - when is_map(ConnectorConfs) -> - make_resource_confs(Direction, ConnectorConfs, - maps:without([connector, direction], Conf), Type, Name). +parse_confs(Type, Name, #{connector := ConnectorConfs, direction := Direction} = Conf) when + is_map(ConnectorConfs) +-> + make_resource_confs( + Direction, + ConnectorConfs, + maps:without([connector, direction], Conf), + Type, + Name + ). make_resource_confs(ingress, ConnectorConfs, BridgeConf, Type, Name) -> BName = bridge_id(Type, Name), @@ -417,39 +509,48 @@ if_only_to_toggle_enable(OldConf, Conf) -> #{added := Added, removed := Removed, changed := Updated} = emqx_map_lib:diff_maps(OldConf, Conf), case {Added, Removed, Updated} of - {Added, Removed, #{enable := _}= Updated} - when map_size(Added) =:= 0, - map_size(Removed) =:= 0, - map_size(Updated) =:= 1 -> true; - {_, _, _} -> false + {Added, Removed, #{enable := _} = Updated} when + map_size(Added) =:= 0, + map_size(Removed) =:= 0, + map_size(Updated) =:= 1 + -> + true; + {_, _, _} -> + false end. -spec get_basic_usage_info() -> - #{ num_bridges => non_neg_integer() - , count_by_type => - #{ BridgeType => non_neg_integer() - } - } when BridgeType :: atom(). + #{ + num_bridges => non_neg_integer(), + count_by_type => + #{BridgeType => non_neg_integer()} + } +when + BridgeType :: atom(). get_basic_usage_info() -> InitialAcc = #{num_bridges => 0, count_by_type => #{}}, try lists:foldl( - fun(#{resource_data := #{config := #{enable := false}}}, Acc) -> - Acc; - (#{type := BridgeType}, Acc) -> - NumBridges = maps:get(num_bridges, Acc), - CountByType0 = maps:get(count_by_type, Acc), - CountByType = maps:update_with( - binary_to_atom(BridgeType, utf8), - fun(X) -> X + 1 end, - 1, - CountByType0), - Acc#{ num_bridges => NumBridges + 1 - , count_by_type => CountByType - } - end, - InitialAcc, - list()) + fun + (#{resource_data := #{config := #{enable := false}}}, Acc) -> + Acc; + (#{type := BridgeType}, Acc) -> + NumBridges = maps:get(num_bridges, Acc), + CountByType0 = maps:get(count_by_type, Acc), + CountByType = maps:update_with( + binary_to_atom(BridgeType, utf8), + fun(X) -> X + 1 end, + 1, + CountByType0 + ), + Acc#{ + num_bridges => NumBridges + 1, + count_by_type => CountByType + } + end, + InitialAcc, + list() + ) catch %% for instance, when the bridge app is not ready yet. _:_ -> diff --git a/apps/emqx_bridge/src/emqx_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl index ff2250844..9870830d0 100644 --- a/apps/emqx_bridge/src/emqx_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -24,22 +24,23 @@ -import(hoconsc, [mk/2, array/1, enum/1]). %% Swagger specs from hocon schema --export([ api_spec/0 - , paths/0 - , schema/1 - , namespace/0 - ]). +-export([ + api_spec/0, + paths/0, + schema/1, + namespace/0 +]). %% API callbacks --export([ '/bridges'/2 - , '/bridges/:id'/2 - , '/bridges/:id/operation/:operation'/2 - , '/nodes/:node/bridges/:id/operation/:operation'/2 - , '/bridges/:id/reset_metrics'/2 - ]). +-export([ + '/bridges'/2, + '/bridges/:id'/2, + '/bridges/:id/operation/:operation'/2, + '/nodes/:node/bridges/:id/operation/:operation'/2, + '/bridges/:id/reset_metrics'/2 +]). --export([ lookup_from_local_node/2 - ]). +-export([lookup_from_local_node/2]). -define(TYPES, [mqtt, http]). @@ -51,35 +52,45 @@ EXPR catch error:{invalid_bridge_id, Id0} -> - {400, error_msg('INVALID_ID', <<"invalid_bridge_id: ", Id0/binary, - ". Bridge Ids must be of format {type}:{name}">>)} - end). + {400, + error_msg( + 'INVALID_ID', + <<"invalid_bridge_id: ", Id0/binary, + ". Bridge Ids must be of format {type}:{name}">> + )} + end +). --define(METRICS(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), - #{ matched => MATCH, - success => SUCC, - failed => FAILED, - rate => RATE, - rate_last5m => RATE_5, - rate_max => RATE_MAX - }). --define(metrics(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), - #{ matched := MATCH, - success := SUCC, - failed := FAILED, - rate := RATE, - rate_last5m := RATE_5, - rate_max := RATE_MAX - }). +-define(METRICS(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{ + matched => MATCH, + success => SUCC, + failed => FAILED, + rate => RATE, + rate_last5m => RATE_5, + rate_max => RATE_MAX +}). +-define(metrics(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{ + matched := MATCH, + success := SUCC, + failed := FAILED, + rate := RATE, + rate_last5m := RATE_5, + rate_max := RATE_MAX +}). namespace() -> "bridge". api_spec() -> emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}). -paths() -> ["/bridges", "/bridges/:id", "/bridges/:id/operation/:operation", - "/nodes/:node/bridges/:id/operation/:operation", - "/bridges/:id/reset_metrics"]. +paths() -> + [ + "/bridges", + "/bridges/:id", + "/bridges/:id/operation/:operation", + "/nodes/:node/bridges/:id/operation/:operation", + "/bridges/:id/reset_metrics" + ]. error_schema(Code, Message) when is_atom(Code) -> error_schema([Code], Message); @@ -89,40 +100,58 @@ error_schema(Codes, Message) when is_list(Codes) andalso is_binary(Message) -> emqx_dashboard_swagger:error_codes(Codes, Message). get_response_body_schema() -> - emqx_dashboard_swagger:schema_with_examples(emqx_bridge_schema:get_response(), - bridge_info_examples(get)). + emqx_dashboard_swagger:schema_with_examples( + emqx_bridge_schema:get_response(), + bridge_info_examples(get) + ). param_path_operation_cluster() -> - {operation, mk(enum([enable, disable, stop, restart]), - #{ in => path - , required => true - , example => <<"start">> - , desc => ?DESC("desc_param_path_operation_cluster") - })}. + {operation, + mk( + enum([enable, disable, stop, restart]), + #{ + in => path, + required => true, + example => <<"start">>, + desc => ?DESC("desc_param_path_operation_cluster") + } + )}. param_path_operation_on_node() -> - {operation, mk(enum([stop, restart]), - #{ in => path - , required => true - , example => <<"start">> - , desc => ?DESC("desc_param_path_operation_on_node") - })}. + {operation, + mk( + enum([stop, restart]), + #{ + in => path, + required => true, + example => <<"start">>, + desc => ?DESC("desc_param_path_operation_on_node") + } + )}. param_path_node() -> - {node, mk(binary(), - #{ in => path - , required => true - , example => <<"emqx@127.0.0.1">> - , desc => ?DESC("desc_param_path_node") - })}. + {node, + mk( + binary(), + #{ + in => path, + required => true, + example => <<"emqx@127.0.0.1">>, + desc => ?DESC("desc_param_path_node") + } + )}. param_path_id() -> - {id, mk(binary(), - #{ in => path - , required => true - , example => <<"http:my_http_bridge">> - , desc => ?DESC("desc_param_path_id") - })}. + {id, + mk( + binary(), + #{ + in => path, + required => true, + example => <<"http:my_http_bridge">>, + desc => ?DESC("desc_param_path_id") + } + )}. bridge_info_array_example(Method) -> [Config || #{value := Config} <- maps:values(bridge_info_examples(Method))]. @@ -136,7 +165,8 @@ bridge_info_examples(Method) -> }). conn_bridge_examples(Method) -> - lists:foldl(fun(Type, Acc) -> + lists:foldl( + fun(Type, Acc) -> SType = atom_to_list(Type), KeyIngress = bin(SType ++ "_ingress"), KeyEgress = bin(SType ++ "_egress"), @@ -150,19 +180,25 @@ conn_bridge_examples(Method) -> value => info_example(Type, egress, Method) } }) - end, #{}, ?CONN_TYPES). + end, + #{}, + ?CONN_TYPES + ). info_example(Type, Direction, Method) -> - maps:merge(info_example_basic(Type, Direction), - method_example(Type, Direction, Method)). + maps:merge( + info_example_basic(Type, Direction), + method_example(Type, Direction, Method) + ). method_example(Type, Direction, Method) when Method == get; Method == post -> SType = atom_to_list(Type), SDir = atom_to_list(Direction), - SName = case Type of - http -> "my_" ++ SType ++ "_bridge"; - _ -> "my_" ++ SDir ++ "_" ++ SType ++ "_bridge" - end, + SName = + case Type of + http -> "my_" ++ SType ++ "_bridge"; + _ -> "my_" ++ SDir ++ "_" ++ SType ++ "_bridge" + end, TypeNameExamp = #{ type => bin(SType), name => bin(SName) @@ -175,8 +211,10 @@ maybe_with_metrics_example(TypeNameExamp, get) -> TypeNameExamp#{ metrics => ?METRICS(0, 0, 0, 0, 0, 0), node_metrics => [ - #{node => node(), - metrics => ?METRICS(0, 0, 0, 0, 0, 0)} + #{ + node => node(), + metrics => ?METRICS(0, 0, 0, 0, 0, 0) + } ] }; maybe_with_metrics_example(TypeNameExamp, _) -> @@ -231,8 +269,9 @@ schema("/bridges") -> description => ?DESC("desc_api1"), responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( - array(emqx_bridge_schema:get_response()), - bridge_info_array_example(get)) + array(emqx_bridge_schema:get_response()), + bridge_info_array_example(get) + ) } }, post => #{ @@ -240,15 +279,15 @@ schema("/bridges") -> summary => <<"Create Bridge">>, description => ?DESC("desc_api2"), 'requestBody' => emqx_dashboard_swagger:schema_with_examples( - emqx_bridge_schema:post_request(), - bridge_info_examples(post)), + emqx_bridge_schema:post_request(), + bridge_info_examples(post) + ), responses => #{ 201 => get_response_body_schema(), 400 => error_schema('ALREADY_EXISTS', "Bridge already exists") } } }; - schema("/bridges/:id") -> #{ 'operationId' => '/bridges/:id', @@ -268,8 +307,9 @@ schema("/bridges/:id") -> description => ?DESC("desc_api4"), parameters => [param_path_id()], 'requestBody' => emqx_dashboard_swagger:schema_with_examples( - emqx_bridge_schema:put_request(), - bridge_info_examples(put)), + emqx_bridge_schema:put_request(), + bridge_info_examples(put) + ), responses => #{ 200 => get_response_body_schema(), 404 => error_schema('NOT_FOUND', "Bridge not found"), @@ -287,7 +327,6 @@ schema("/bridges/:id") -> } } }; - schema("/bridges/:id/reset_metrics") -> #{ 'operationId' => '/bridges/:id/reset_metrics', @@ -319,7 +358,6 @@ schema("/bridges/:id/operation/:operation") -> } } }; - schema("/nodes/:node/bridges/:id/operation/:operation") -> #{ 'operationId' => '/nodes/:node/bridges/:id/operation/:operation', @@ -336,7 +374,6 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> 200 => <<"Operation success">>, 400 => error_schema('INVALID_ID', "Bad bridge ID"), 403 => error_schema('FORBIDDEN_REQUEST', "forbidden operation") - } } }. @@ -353,15 +390,18 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> end end; '/bridges'(get, _Params) -> - {200, zip_bridges([[format_resp(Data) || Data <- emqx_bridge_proto_v1:list_bridges(Node)] - || Node <- mria_mnesia:running_nodes()])}. + {200, + zip_bridges([ + [format_resp(Data) || Data <- emqx_bridge_proto_v1:list_bridges(Node)] + || Node <- mria_mnesia:running_nodes() + ])}. '/bridges/:id'(get, #{bindings := #{id := Id}}) -> ?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200)); - '/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) -> Conf = filter_out_request_body(Conf0), - ?TRY_PARSE_ID(Id, + ?TRY_PARSE_ID( + Id, case emqx_bridge:lookup(BridgeType, BridgeName) of {ok, _} -> case ensure_bridge_created(BridgeType, BridgeName, Conf) of @@ -371,24 +411,31 @@ schema("/nodes/:node/bridges/:id/operation/:operation") -> {400, Error} end; {error, not_found} -> - {404, error_msg('NOT_FOUND',<<"bridge not found">>)} - end); - + {404, error_msg('NOT_FOUND', <<"bridge not found">>)} + end + ); '/bridges/:id'(delete, #{bindings := #{id := Id}}) -> - ?TRY_PARSE_ID(Id, - case emqx_conf:remove(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], - #{override_to => cluster}) of + ?TRY_PARSE_ID( + Id, + case + emqx_conf:remove( + emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], + #{override_to => cluster} + ) + of {ok, _} -> {204}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} - end). + {error, Reason} -> {500, error_msg('INTERNAL_ERROR', Reason)} + end + ). '/bridges/:id/reset_metrics'(put, #{bindings := #{id := Id}}) -> - ?TRY_PARSE_ID(Id, + ?TRY_PARSE_ID( + Id, case emqx_bridge:reset_metrics(emqx_bridge:resource_id(BridgeType, BridgeName)) of ok -> {200, <<"Reset success">>}; Reason -> {400, error_msg('BAD_REQUEST', Reason)} - end). + end + ). lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) -> Nodes = mria_mnesia:running_nodes(), @@ -407,40 +454,58 @@ lookup_from_local_node(BridgeType, BridgeName) -> Error -> Error end. -'/bridges/:id/operation/:operation'(post, #{bindings := - #{id := Id, operation := Op}}) -> - ?TRY_PARSE_ID(Id, case operation_func(Op) of - invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; - OperFunc when OperFunc == enable; OperFunc == disable -> - case emqx_conf:update(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], - {OperFunc, BridgeType, BridgeName}, #{override_to => cluster}) of - {ok, _} -> {200}; - {error, {pre_config_update, _, bridge_not_found}} -> - {404, error_msg('NOT_FOUND', <<"bridge not found">>)}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} - end; - OperFunc -> - Nodes = mria_mnesia:running_nodes(), - operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) - end). +'/bridges/:id/operation/:operation'(post, #{ + bindings := + #{id := Id, operation := Op} +}) -> + ?TRY_PARSE_ID( + Id, + case operation_func(Op) of + invalid -> + {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; + OperFunc when OperFunc == enable; OperFunc == disable -> + case + emqx_conf:update( + emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], + {OperFunc, BridgeType, BridgeName}, + #{override_to => cluster} + ) + of + {ok, _} -> + {200}; + {error, {pre_config_update, _, bridge_not_found}} -> + {404, error_msg('NOT_FOUND', <<"bridge not found">>)}; + {error, Reason} -> + {500, error_msg('INTERNAL_ERROR', Reason)} + end; + OperFunc -> + Nodes = mria_mnesia:running_nodes(), + operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) + end + ). -'/nodes/:node/bridges/:id/operation/:operation'(post, #{bindings := - #{id := Id, operation := Op}}) -> - ?TRY_PARSE_ID(Id, case operation_func(Op) of - invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; - OperFunc when OperFunc == restart; OperFunc == stop -> - ConfMap = emqx:get_config([bridges, BridgeType, BridgeName]), - case maps:get(enable, ConfMap, false) of - false -> {403, error_msg('FORBIDDEN_REQUEST', <<"forbidden operation">>)}; - true -> - case emqx_bridge:OperFunc(BridgeType, BridgeName) of - ok -> {200}; - {error, Reason} -> - {500, error_msg('INTERNAL_ERROR', Reason)} - end - end - end). +'/nodes/:node/bridges/:id/operation/:operation'(post, #{ + bindings := + #{id := Id, operation := Op} +}) -> + ?TRY_PARSE_ID( + Id, + case operation_func(Op) of + invalid -> + {400, error_msg('BAD_REQUEST', <<"invalid operation">>)}; + OperFunc when OperFunc == restart; OperFunc == stop -> + ConfMap = emqx:get_config([bridges, BridgeType, BridgeName]), + case maps:get(enable, ConfMap, false) of + false -> + {403, error_msg('FORBIDDEN_REQUEST', <<"forbidden operation">>)}; + true -> + case emqx_bridge:OperFunc(BridgeType, BridgeName) of + ok -> {200}; + {error, Reason} -> {500, error_msg('INTERNAL_ERROR', Reason)} + end + end + end + ). operation_func(<<"stop">>) -> stop; operation_func(<<"restart">>) -> restart; @@ -449,10 +514,11 @@ operation_func(<<"disable">>) -> disable; operation_func(_) -> invalid. operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> - RpcFunc = case OperFunc of - restart -> restart_bridges_to_all_nodes; - stop -> stop_bridges_to_all_nodes - end, + RpcFunc = + case OperFunc of + restart -> restart_bridges_to_all_nodes; + stop -> stop_bridges_to_all_nodes + end, case is_ok(emqx_bridge_proto_v1:RpcFunc(Nodes, BridgeType, BridgeName)) of {ok, _} -> {200}; @@ -461,48 +527,70 @@ operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) -> end. ensure_bridge_created(BridgeType, BridgeName, Conf) -> - case emqx_conf:update(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], - Conf, #{override_to => cluster}) of + case + emqx_conf:update( + emqx_bridge:config_key_path() ++ [BridgeType, BridgeName], + Conf, + #{override_to => cluster} + ) + of {ok, _} -> ok; - {error, Reason} -> - {error, error_msg('BAD_REQUEST', Reason)} + {error, Reason} -> {error, error_msg('BAD_REQUEST', Reason)} end. zip_bridges([BridgesFirstNode | _] = BridgesAllNodes) -> - lists:foldl(fun(#{type := Type, name := Name}, Acc) -> + lists:foldl( + fun(#{type := Type, name := Name}, Acc) -> Bridges = pick_bridges_by_id(Type, Name, BridgesAllNodes), [format_bridge_info(Bridges) | Acc] - end, [], BridgesFirstNode). + end, + [], + BridgesFirstNode + ). pick_bridges_by_id(Type, Name, BridgesAllNodes) -> - lists:foldl(fun(BridgesOneNode, Acc) -> - case [Bridge || Bridge = #{type := Type0, name := Name0} <- BridgesOneNode, - Type0 == Type, Name0 == Name] of - [BridgeInfo] -> [BridgeInfo | Acc]; + lists:foldl( + fun(BridgesOneNode, Acc) -> + case + [ + Bridge + || Bridge = #{type := Type0, name := Name0} <- BridgesOneNode, + Type0 == Type, + Name0 == Name + ] + of + [BridgeInfo] -> + [BridgeInfo | Acc]; [] -> - ?SLOG(warning, #{msg => "bridge_inconsistent_in_cluster", - bridge => emqx_bridge:bridge_id(Type, Name)}), + ?SLOG(warning, #{ + msg => "bridge_inconsistent_in_cluster", + bridge => emqx_bridge:bridge_id(Type, Name) + }), Acc end - end, [], BridgesAllNodes). + end, + [], + BridgesAllNodes + ). format_bridge_info([FirstBridge | _] = Bridges) -> Res = maps:remove(node, FirstBridge), NodeStatus = collect_status(Bridges), NodeMetrics = collect_metrics(Bridges), - Res#{ status => aggregate_status(NodeStatus) - , node_status => NodeStatus - , metrics => aggregate_metrics(NodeMetrics) - , node_metrics => NodeMetrics - }. + Res#{ + status => aggregate_status(NodeStatus), + node_status => NodeStatus, + metrics => aggregate_metrics(NodeMetrics), + node_metrics => NodeMetrics + }. collect_status(Bridges) -> [maps:with([node, status], B) || B <- Bridges]. aggregate_status(AllStatus) -> - Head = fun ([A | _]) -> A end, + Head = fun([A | _]) -> A end, HeadVal = maps:get(status, Head(AllStatus), connecting), - AllRes = lists:all(fun (#{status := Val}) -> Val == HeadVal end, AllStatus), + AllRes = lists:all(fun(#{status := Val}) -> Val == HeadVal end, AllStatus), case AllRes of true -> HeadVal; false -> inconsistent @@ -512,15 +600,31 @@ collect_metrics(Bridges) -> [maps:with([node, metrics], B) || B <- Bridges]. aggregate_metrics(AllMetrics) -> - InitMetrics = ?METRICS(0,0,0,0,0,0), - lists:foldl(fun(#{metrics := ?metrics(Match1, Succ1, Failed1, Rate1, Rate5m1, RateMax1)}, - ?metrics(Match0, Succ0, Failed0, Rate0, Rate5m0, RateMax0)) -> - ?METRICS(Match1 + Match0, Succ1 + Succ0, Failed1 + Failed0, - Rate1 + Rate0, Rate5m1 + Rate5m0, RateMax1 + RateMax0) - end, InitMetrics, AllMetrics). + InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0), + lists:foldl( + fun( + #{metrics := ?metrics(Match1, Succ1, Failed1, Rate1, Rate5m1, RateMax1)}, + ?metrics(Match0, Succ0, Failed0, Rate0, Rate5m0, RateMax0) + ) -> + ?METRICS( + Match1 + Match0, + Succ1 + Succ0, + Failed1 + Failed0, + Rate1 + Rate0, + Rate5m1 + Rate5m0, + RateMax1 + RateMax0 + ) + end, + InitMetrics, + AllMetrics + ). -format_resp(#{type := Type, name := BridgeName, raw_config := RawConf, - resource_data := #{status := Status, metrics := Metrics}}) -> +format_resp(#{ + type := Type, + name := BridgeName, + raw_config := RawConf, + resource_data := #{status := Status, metrics := Metrics} +}) -> RawConfFull = fill_defaults(Type, RawConf), RawConfFull#{ type => Type, @@ -531,10 +635,11 @@ format_resp(#{type := Type, name := BridgeName, raw_config := RawConf, }. format_metrics(#{ - counters := #{failed := Failed, exception := Ex, matched := Match, success := Succ}, - rate := #{ - matched := #{current := Rate, last5m := Rate5m, max := RateMax} - } }) -> + counters := #{failed := Failed, exception := Ex, matched := Match, success := Succ}, + rate := #{ + matched := #{current := Rate, last5m := Rate5m, max := RateMax} + } +}) -> ?METRICS(Match, Succ, Failed + Ex, Rate, Rate5m, RateMax). fill_defaults(Type, RawConf) -> @@ -551,14 +656,31 @@ unpack_bridge_conf(Type, PackedConf) -> RawConf. is_ok(ResL) -> - case lists:filter(fun({ok, _}) -> false; (ok) -> false; (_) -> true end, ResL) of + case + lists:filter( + fun + ({ok, _}) -> false; + (ok) -> false; + (_) -> true + end, + ResL + ) + of [] -> {ok, [Res || {ok, Res} <- ResL]}; ErrL -> {error, ErrL} end. filter_out_request_body(Conf) -> - ExtraConfs = [<<"id">>, <<"type">>, <<"name">>, <<"status">>, <<"node_status">>, - <<"node_metrics">>, <<"metrics">>, <<"node">>], + ExtraConfs = [ + <<"id">>, + <<"type">>, + <<"name">>, + <<"status">>, + <<"node_status">>, + <<"node_metrics">>, + <<"metrics">>, + <<"node">> + ], maps:without(ExtraConfs, Conf). error_msg(Code, Msg) when is_binary(Msg) -> diff --git a/apps/emqx_bridge/src/emqx_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl index 99b2c4a84..3fc4d57ba 100644 --- a/apps/emqx_bridge/src/emqx_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -19,9 +19,10 @@ -export([start/2, stop/1]). --export([ pre_config_update/3 - , post_config_update/5 - ]). +-export([ + pre_config_update/3, + post_config_update/5 +]). -define(TOP_LELVE_HDLR_PATH, (emqx_bridge:config_key_path())). -define(LEAF_NODE_HDLR_PATH, (emqx_bridge:config_key_path() ++ ['?', '?'])). diff --git a/apps/emqx_bridge/src/emqx_bridge_http_schema.erl b/apps/emqx_bridge/src/emqx_bridge_http_schema.erl index 3bf4e8160..ff1ab2c05 100644 --- a/apps/emqx_bridge/src/emqx_bridge_http_schema.erl +++ b/apps/emqx_bridge/src/emqx_bridge_http_schema.erl @@ -15,45 +15,66 @@ roots() -> []. fields("config") -> basic_config() ++ - [ {url, mk(binary(), - #{ required => true - , desc => ?DESC("config_url") - })} - , {local_topic, mk(binary(), - #{ desc => ?DESC("config_local_topic") - })} - , {method, mk(method(), - #{ default => post - , desc => ?DESC("config_method") - })} - , {headers, mk(map(), - #{ default => #{ - <<"accept">> => <<"application/json">>, - <<"cache-control">> => <<"no-cache">>, - <<"connection">> => <<"keep-alive">>, - <<"content-type">> => <<"application/json">>, - <<"keep-alive">> => <<"timeout=5">>} - , desc => ?DESC("config_headers") - }) - } - , {body, mk(binary(), - #{ default => <<"${payload}">> - , desc => ?DESC("config_body") - })} - , {request_timeout, mk(emqx_schema:duration_ms(), - #{ default => <<"15s">> - , desc => ?DESC("config_request_timeout") - })} - ]; - + [ + {url, + mk( + binary(), + #{ + required => true, + desc => ?DESC("config_url") + } + )}, + {local_topic, + mk( + binary(), + #{desc => ?DESC("config_local_topic")} + )}, + {method, + mk( + method(), + #{ + default => post, + desc => ?DESC("config_method") + } + )}, + {headers, + mk( + map(), + #{ + default => #{ + <<"accept">> => <<"application/json">>, + <<"cache-control">> => <<"no-cache">>, + <<"connection">> => <<"keep-alive">>, + <<"content-type">> => <<"application/json">>, + <<"keep-alive">> => <<"timeout=5">> + }, + desc => ?DESC("config_headers") + } + )}, + {body, + mk( + binary(), + #{ + default => <<"${payload}">>, + desc => ?DESC("config_body") + } + )}, + {request_timeout, + mk( + emqx_schema:duration_ms(), + #{ + default => <<"15s">>, + desc => ?DESC("config_request_timeout") + } + )} + ]; fields("post") -> - [ type_field() - , name_field() + [ + type_field(), + name_field() ] ++ fields("config"); - fields("put") -> fields("config"); - fields("get") -> emqx_bridge_schema:metrics_status_fields() ++ fields("post"). @@ -65,32 +86,47 @@ desc(_) -> undefined. basic_config() -> - [ {enable, - mk(boolean(), - #{ desc => ?DESC("config_enable") - , default => true - })} - , {direction, - mk(egress, - #{ desc => ?DESC("config_direction") - , default => egress - })} - ] - ++ proplists:delete(base_url, emqx_connector_http:fields(config)). + [ + {enable, + mk( + boolean(), + #{ + desc => ?DESC("config_enable"), + default => true + } + )}, + {direction, + mk( + egress, + #{ + desc => ?DESC("config_direction"), + default => egress + } + )} + ] ++ + proplists:delete(base_url, emqx_connector_http:fields(config)). %%====================================================================================== type_field() -> - {type, mk(http, - #{ required => true - , desc => ?DESC("desc_type") - })}. + {type, + mk( + http, + #{ + required => true, + desc => ?DESC("desc_type") + } + )}. name_field() -> - {name, mk(binary(), - #{ required => true - , desc => ?DESC("desc_name") - })}. + {name, + mk( + binary(), + #{ + required => true, + desc => ?DESC("desc_name") + } + )}. method() -> enum([post, put, get, delete]). diff --git a/apps/emqx_bridge/src/emqx_bridge_monitor.erl b/apps/emqx_bridge/src/emqx_bridge_monitor.erl index 8de216974..b9a22bb8c 100644 --- a/apps/emqx_bridge/src/emqx_bridge_monitor.erl +++ b/apps/emqx_bridge/src/emqx_bridge_monitor.erl @@ -22,17 +22,20 @@ -include_lib("snabbkaffe/include/snabbkaffe.hrl"). %% API functions --export([ start_link/0 - , ensure_all_started/1 - ]). +-export([ + start_link/0, + ensure_all_started/1 +]). %% 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 +]). -record(state, {}). @@ -52,7 +55,6 @@ handle_call(_Request, _From, State) -> handle_cast({start_and_monitor, Configs}, State) -> ok = load_bridges(Configs), {noreply, State}; - handle_cast(_Msg, State) -> {noreply, State}. @@ -67,13 +69,22 @@ code_change(_OldVsn, State, _Extra) -> %%============================================================================ load_bridges(Configs) -> - lists:foreach(fun({Type, NamedConf}) -> - lists:foreach(fun({Name, Conf}) -> + lists:foreach( + fun({Type, NamedConf}) -> + lists:foreach( + fun({Name, Conf}) -> _Res = emqx_bridge:create(Type, Name, Conf), - ?tp(emqx_bridge_monitor_loaded_bridge, - #{ type => Type - , name => Name - , res => _Res - }) - end, maps:to_list(NamedConf)) - end, maps:to_list(Configs)). + ?tp( + emqx_bridge_monitor_loaded_bridge, + #{ + type => Type, + name => Name, + res => _Res + } + ) + end, + maps:to_list(NamedConf) + ) + end, + maps:to_list(Configs) + ). diff --git a/apps/emqx_bridge/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge/src/emqx_bridge_mqtt_schema.erl index 304df779f..15e24024a 100644 --- a/apps/emqx_bridge/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge/src/emqx_bridge_mqtt_schema.erl @@ -12,31 +12,27 @@ roots() -> []. fields("ingress") -> - [ emqx_bridge_schema:direction_field(ingress, emqx_connector_mqtt_schema:ingress_desc()) - ] - ++ emqx_bridge_schema:common_bridge_fields() - ++ proplists:delete(hookpoint, emqx_connector_mqtt_schema:fields("ingress")); - + [emqx_bridge_schema:direction_field(ingress, emqx_connector_mqtt_schema:ingress_desc())] ++ + emqx_bridge_schema:common_bridge_fields() ++ + proplists:delete(hookpoint, emqx_connector_mqtt_schema:fields("ingress")); fields("egress") -> - [ emqx_bridge_schema:direction_field(egress, emqx_connector_mqtt_schema:egress_desc()) - ] - ++ emqx_bridge_schema:common_bridge_fields() - ++ emqx_connector_mqtt_schema:fields("egress"); - + [emqx_bridge_schema:direction_field(egress, emqx_connector_mqtt_schema:egress_desc())] ++ + emqx_bridge_schema:common_bridge_fields() ++ + emqx_connector_mqtt_schema:fields("egress"); fields("post_ingress") -> - [ type_field() - , name_field() + [ + type_field(), + name_field() ] ++ proplists:delete(enable, fields("ingress")); fields("post_egress") -> - [ type_field() - , name_field() + [ + type_field(), + name_field() ] ++ proplists:delete(enable, fields("egress")); - fields("put_ingress") -> proplists:delete(enable, fields("ingress")); fields("put_egress") -> proplists:delete(enable, fields("egress")); - fields("get_ingress") -> emqx_bridge_schema:metrics_status_fields() ++ fields("post_ingress"); fields("get_egress") -> @@ -49,13 +45,21 @@ desc(_) -> %%====================================================================================== type_field() -> - {type, mk(mqtt, - #{ required => true - , desc => ?DESC("desc_type") - })}. + {type, + mk( + mqtt, + #{ + required => true, + desc => ?DESC("desc_type") + } + )}. name_field() -> - {name, mk(binary(), - #{ required => true - , desc => ?DESC("desc_name") - })}. + {name, + mk( + binary(), + #{ + required => true, + desc => ?DESC("desc_name") + } + )}. diff --git a/apps/emqx_bridge/src/emqx_bridge_schema.erl b/apps/emqx_bridge/src/emqx_bridge_schema.erl index fd5d68aa1..7c54652ef 100644 --- a/apps/emqx_bridge/src/emqx_bridge_schema.erl +++ b/apps/emqx_bridge/src/emqx_bridge_schema.erl @@ -7,15 +7,17 @@ -export([roots/0, fields/1, desc/1, namespace/0]). --export([ get_response/0 - , put_request/0 - , post_request/0 - ]). +-export([ + get_response/0, + put_request/0, + post_request/0 +]). --export([ common_bridge_fields/0 - , metrics_status_fields/0 - , direction_field/2 - ]). +-export([ + common_bridge_fields/0, + metrics_status_fields/0, + direction_field/2 +]). %%====================================================================================== %% Hocon Schema Definitions @@ -34,43 +36,68 @@ post_request() -> http_schema("post"). http_schema(Method) -> - Schemas = lists:flatmap(fun(Type) -> - [ref(schema_mod(Type), Method ++ "_ingress"), - ref(schema_mod(Type), Method ++ "_egress")] - end, ?CONN_TYPES), - hoconsc:union([ref(emqx_bridge_http_schema, Method) - | Schemas]). + Schemas = lists:flatmap( + fun(Type) -> + [ + ref(schema_mod(Type), Method ++ "_ingress"), + ref(schema_mod(Type), Method ++ "_egress") + ] + end, + ?CONN_TYPES + ), + hoconsc:union([ + ref(emqx_bridge_http_schema, Method) + | Schemas + ]). common_bridge_fields() -> - [ {enable, - mk(boolean(), - #{ desc => ?DESC("desc_enable") - , default => true - })} - , {connector, - mk(binary(), - #{ required => true - , example => <<"mqtt:my_mqtt_connector">> - , desc => ?DESC("desc_connector") - })} + [ + {enable, + mk( + boolean(), + #{ + desc => ?DESC("desc_enable"), + default => true + } + )}, + {connector, + mk( + binary(), + #{ + required => true, + example => <<"mqtt:my_mqtt_connector">>, + desc => ?DESC("desc_connector") + } + )} ]. metrics_status_fields() -> - [ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})} - , {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")), - #{ desc => ?DESC("desc_node_metrics")})} - , {"status", mk(status(), #{desc => ?DESC("desc_status")})} - , {"node_status", mk(hoconsc:array(ref(?MODULE, "node_status")), - #{ desc => ?DESC("desc_node_status")})} + [ + {"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})}, + {"node_metrics", + mk( + hoconsc:array(ref(?MODULE, "node_metrics")), + #{desc => ?DESC("desc_node_metrics")} + )}, + {"status", mk(status(), #{desc => ?DESC("desc_status")})}, + {"node_status", + mk( + hoconsc:array(ref(?MODULE, "node_status")), + #{desc => ?DESC("desc_node_status")} + )} ]. direction_field(Dir, Desc) -> - {direction, mk(Dir, - #{ required => true - , default => egress - , desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.
" - ++ Desc - })}. + {direction, + mk( + Dir, + #{ + required => true, + default => egress, + desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.
" ++ + Desc + } + )}. %%====================================================================================== %% For config files @@ -80,31 +107,49 @@ namespace() -> "bridge". roots() -> [bridges]. fields(bridges) -> - [{http, mk(hoconsc:map(name, ref(emqx_bridge_http_schema, "config")), - #{desc => ?DESC("bridges_http")})}] - ++ [{T, mk(hoconsc:map(name, hoconsc:union([ ref(schema_mod(T), "ingress") - , ref(schema_mod(T), "egress") - ])), - #{desc => ?DESC("bridges_name")})} || T <- ?CONN_TYPES]; - + [ + {http, + mk( + hoconsc:map(name, ref(emqx_bridge_http_schema, "config")), + #{desc => ?DESC("bridges_http")} + )} + ] ++ + [ + {T, + mk( + hoconsc:map( + name, + hoconsc:union([ + ref(schema_mod(T), "ingress"), + ref(schema_mod(T), "egress") + ]) + ), + #{desc => ?DESC("bridges_name")} + )} + || T <- ?CONN_TYPES + ]; fields("metrics") -> - [ {"matched", mk(integer(), #{desc => ?DESC("metric_matched")})} - , {"success", mk(integer(), #{desc => ?DESC("metric_success")})} - , {"failed", mk(integer(), #{desc => ?DESC("metric_failed")})} - , {"rate", mk(float(), #{desc => ?DESC("metric_rate")})} - , {"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})} - , {"rate_last5m", mk(float(), - #{desc => ?DESC("metric_rate_last5m")})} + [ + {"matched", mk(integer(), #{desc => ?DESC("metric_matched")})}, + {"success", mk(integer(), #{desc => ?DESC("metric_success")})}, + {"failed", mk(integer(), #{desc => ?DESC("metric_failed")})}, + {"rate", mk(float(), #{desc => ?DESC("metric_rate")})}, + {"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})}, + {"rate_last5m", + mk( + float(), + #{desc => ?DESC("metric_rate_last5m")} + )} ]; - fields("node_metrics") -> - [ node_name() - , {"metrics", mk(ref(?MODULE, "metrics"), #{})} + [ + node_name(), + {"metrics", mk(ref(?MODULE, "metrics"), #{})} ]; - fields("node_status") -> - [ node_name() - , {"status", mk(status(), #{})} + [ + node_name(), + {"status", mk(status(), #{})} ]. desc(bridges) -> diff --git a/apps/emqx_bridge/src/emqx_bridge_sup.erl b/apps/emqx_bridge/src/emqx_bridge_sup.erl index 0c73ac585..cce3a066b 100644 --- a/apps/emqx_bridge/src/emqx_bridge_sup.erl +++ b/apps/emqx_bridge/src/emqx_bridge_sup.erl @@ -27,15 +27,19 @@ start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). init([]) -> - SupFlags = #{strategy => one_for_one, - intensity => 10, - period => 10}, + SupFlags = #{ + strategy => one_for_one, + intensity => 10, + period => 10 + }, ChildSpecs = [ - #{id => emqx_bridge_monitor, - start => {emqx_bridge_monitor, start_link, []}, - restart => permanent, - type => worker, - modules => [emqx_bridge_monitor]} + #{ + id => emqx_bridge_monitor, + start => {emqx_bridge_monitor, start_link, []}, + restart => permanent, + type => worker, + modules => [emqx_bridge_monitor] + } ], {ok, {SupFlags, ChildSpecs}}. diff --git a/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl index 021074a1c..75060c7c1 100644 --- a/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl +++ b/apps/emqx_bridge/src/proto/emqx_bridge_proto_v1.erl @@ -18,13 +18,14 @@ -behaviour(emqx_bpapi). --export([ introduced_in/0 +-export([ + introduced_in/0, - , list_bridges/1 - , lookup_from_all_nodes/3 - , restart_bridges_to_all_nodes/3 - , stop_bridges_to_all_nodes/3 - ]). + list_bridges/1, + lookup_from_all_nodes/3, + restart_bridges_to_all_nodes/3, + stop_bridges_to_all_nodes/3 +]). -include_lib("emqx/include/bpapi.hrl"). @@ -40,19 +41,34 @@ list_bridges(Node) -> -type key() :: atom() | binary() | [byte()]. -spec restart_bridges_to_all_nodes([node()], key(), key()) -> - emqx_rpc:erpc_multicall(). + emqx_rpc:erpc_multicall(). restart_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) -> - erpc:multicall(Nodes, emqx_bridge, restart, - [BridgeType, BridgeName], ?TIMEOUT). + erpc:multicall( + Nodes, + emqx_bridge, + restart, + [BridgeType, BridgeName], + ?TIMEOUT + ). -spec stop_bridges_to_all_nodes([node()], key(), key()) -> - emqx_rpc:erpc_multicall(). + emqx_rpc:erpc_multicall(). stop_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) -> - erpc:multicall(Nodes, emqx_bridge, stop, - [BridgeType, BridgeName], ?TIMEOUT). + erpc:multicall( + Nodes, + emqx_bridge, + stop, + [BridgeType, BridgeName], + ?TIMEOUT + ). -spec lookup_from_all_nodes([node()], key(), key()) -> - emqx_rpc:erpc_multicall(). + emqx_rpc:erpc_multicall(). lookup_from_all_nodes(Nodes, BridgeType, BridgeName) -> - erpc:multicall(Nodes, emqx_bridge_api, lookup_from_local_node, - [BridgeType, BridgeName], ?TIMEOUT). + erpc:multicall( + Nodes, + emqx_bridge_api, + lookup_from_local_node, + [BridgeType, BridgeName], + ?TIMEOUT + ). diff --git a/apps/emqx_bridge/test/emqx_bridge_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_SUITE.erl index 9aa98de7c..d49c907b7 100644 --- a/apps/emqx_bridge/test/emqx_bridge_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_SUITE.erl @@ -23,7 +23,7 @@ -include_lib("snabbkaffe/include/snabbkaffe.hrl"). all() -> - emqx_common_test_helpers:all(?MODULE). + emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> %% to avoid inter-suite dependencies @@ -32,8 +32,12 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx, emqx_bridge, - emqx_resource, emqx_connector]). + emqx_common_test_helpers:stop_apps([ + emqx, + emqx_bridge, + emqx_resource, + emqx_connector + ]). init_per_testcase(t_get_basic_usage_info_1, Config) -> setup_fake_telemetry_data(), @@ -43,13 +47,15 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(t_get_basic_usage_info_1, _Config) -> lists:foreach( - fun({BridgeType, BridgeName}) -> - ok = emqx_bridge:remove(BridgeType, BridgeName) - end, - [ {http, <<"basic_usage_info_http">>} - , {http, <<"basic_usage_info_http_disabled">>} - , {mqtt, <<"basic_usage_info_mqtt">>} - ]), + fun({BridgeType, BridgeName}) -> + ok = emqx_bridge:remove(BridgeType, BridgeName) + end, + [ + {http, <<"basic_usage_info_http">>}, + {http, <<"basic_usage_info_http_disabled">>}, + {mqtt, <<"basic_usage_info_mqtt">>} + ] + ), ok = emqx_config:delete_override_conf_files(), ok = emqx_config:put([bridges], #{}), ok = emqx_config:put_raw([bridges], #{}), @@ -59,53 +65,68 @@ end_per_testcase(_TestCase, _Config) -> t_get_basic_usage_info_0(_Config) -> ?assertEqual( - #{ num_bridges => 0 - , count_by_type => #{} + #{ + num_bridges => 0, + count_by_type => #{} }, - emqx_bridge:get_basic_usage_info()). + emqx_bridge:get_basic_usage_info() + ). t_get_basic_usage_info_1(_Config) -> BasicUsageInfo = emqx_bridge:get_basic_usage_info(), ?assertEqual( - #{ num_bridges => 2 - , count_by_type => #{ http => 1 - , mqtt => 1 - } + #{ + num_bridges => 2, + count_by_type => #{ + http => 1, + mqtt => 1 + } }, - BasicUsageInfo). + BasicUsageInfo + ). setup_fake_telemetry_data() -> ConnectorConf = - #{<<"connectors">> => - #{<<"mqtt">> => #{<<"my_mqtt_connector">> => - #{ server => "127.0.0.1:1883" }}}}, - MQTTConfig = #{ connector => <<"mqtt:my_mqtt_connector">> - , enable => true - , direction => ingress - , remote_topic => <<"aws/#">> - , remote_qos => 1 - }, - HTTPConfig = #{ url => <<"http://localhost:9901/messages/${topic}">> - , enable => true - , direction => egress - , local_topic => "emqx_http/#" - , method => post - , body => <<"${payload}">> - , headers => #{} - , request_timeout => "15s" - }, - Conf = - #{ <<"bridges">> => - #{ <<"http">> => - #{ <<"basic_usage_info_http">> => HTTPConfig - , <<"basic_usage_info_http_disabled">> => - HTTPConfig#{enable => false} - } - , <<"mqtt">> => - #{ <<"basic_usage_info_mqtt">> => MQTTConfig - } + #{ + <<"connectors">> => + #{ + <<"mqtt">> => #{ + <<"my_mqtt_connector">> => + #{server => "127.0.0.1:1883"} + } } - }, + }, + MQTTConfig = #{ + connector => <<"mqtt:my_mqtt_connector">>, + enable => true, + direction => ingress, + remote_topic => <<"aws/#">>, + remote_qos => 1 + }, + HTTPConfig = #{ + url => <<"http://localhost:9901/messages/${topic}">>, + enable => true, + direction => egress, + local_topic => "emqx_http/#", + method => post, + body => <<"${payload}">>, + headers => #{}, + request_timeout => "15s" + }, + Conf = + #{ + <<"bridges">> => + #{ + <<"http">> => + #{ + <<"basic_usage_info_http">> => HTTPConfig, + <<"basic_usage_info_http_disabled">> => + HTTPConfig#{enable => false} + }, + <<"mqtt">> => + #{<<"basic_usage_info_mqtt">> => MQTTConfig} + } + }, ok = emqx_common_test_helpers:load_config(emqx_connector_schema, ConnectorConf), ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, Conf), diff --git a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl index 5fea93a94..5142ee5c1 100644 --- a/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl +++ b/apps/emqx_bridge/test/emqx_bridge_api_SUITE.erl @@ -25,11 +25,15 @@ -define(CONF_DEFAULT, <<"bridges: {}">>). -define(BRIDGE_TYPE, <<"http">>). -define(BRIDGE_NAME, <<"test_bridge">>). --define(URL(PORT, PATH), list_to_binary( - io_lib:format("http://localhost:~s/~s", - [integer_to_list(PORT), PATH]))). --define(HTTP_BRIDGE(URL, TYPE, NAME), -#{ +-define(URL(PORT, PATH), + list_to_binary( + io_lib:format( + "http://localhost:~s/~s", + [integer_to_list(PORT), PATH] + ) + ) +). +-define(HTTP_BRIDGE(URL, TYPE, NAME), #{ <<"type">> => TYPE, <<"name">> => NAME, <<"url">> => URL, @@ -40,7 +44,6 @@ <<"headers">> => #{ <<"content-type">> => <<"application/json">> } - }). all() -> @@ -50,15 +53,17 @@ groups() -> []. suite() -> - [{timetrap,{seconds,60}}]. + [{timetrap, {seconds, 60}}]. init_per_suite(Config) -> _ = application:load(emqx_conf), %% some testcases (may from other app) already get emqx_connector started _ = application:stop(emqx_resource), _ = application:stop(emqx_connector), - ok = emqx_common_test_helpers:start_apps([emqx_bridge, emqx_dashboard], - fun set_special_configs/1), + ok = emqx_common_test_helpers:start_apps( + [emqx_bridge, emqx_dashboard], + fun set_special_configs/1 + ), ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT), Config. @@ -79,9 +84,12 @@ end_per_testcase(_, _Config) -> ok. clear_resources() -> - lists:foreach(fun(#{type := Type, name := Name}) -> + lists:foreach( + fun(#{type := Type, name := Name}) -> ok = emqx_bridge:remove(Type, Name) - end, emqx_bridge:list()). + end, + emqx_bridge:list() + ). %%------------------------------------------------------------------------------ %% HTTP server for testing @@ -95,12 +103,12 @@ start_http_server(HandleFun) -> end), receive {port, Port} -> Port - after - 2000 -> error({timeout, start_http_server}) + after 2000 -> error({timeout, start_http_server}) end. listen_on_random_port() -> - Min = 1024, Max = 65000, + Min = 1024, + Max = 65000, Port = rand:uniform(Max - Min) + Min, case gen_tcp:listen(Port, [{active, false}, {reuseaddr, true}, binary]) of {ok, Sock} -> {Port, Sock}; @@ -109,16 +117,18 @@ listen_on_random_port() -> loop(Sock, HandleFun, Parent) -> {ok, Conn} = gen_tcp:accept(Sock), - Handler = spawn(fun () -> HandleFun(Conn, Parent) end), + Handler = spawn(fun() -> HandleFun(Conn, Parent) end), gen_tcp:controlling_process(Conn, Handler), loop(Sock, HandleFun, Parent). make_response(CodeStr, Str) -> B = iolist_to_binary(Str), iolist_to_binary( - io_lib:fwrite( - "HTTP/1.0 ~s\r\nContent-Type: text/html\r\nContent-Length: ~p\r\n\r\n~s", - [CodeStr, size(B), B])). + io_lib:fwrite( + "HTTP/1.0 ~s\r\nContent-Type: text/html\r\nContent-Length: ~p\r\n\r\n~s", + [CodeStr, size(B), B] + ) + ). handle_fun_200_ok(Conn, Parent) -> case gen_tcp:recv(Conn, 0) of @@ -151,18 +161,22 @@ t_http_crud_apis(_) -> %% then we add a http bridge, using POST %% POST /bridges/ will create a bridge URL1 = ?URL(Port, "path1"), - {ok, 201, Bridge} = request(post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), %ct:pal("---bridge: ~p", [Bridge]), - #{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := _ - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL1 - } = jsx:decode(Bridge), + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL1 + } = jsx:decode(Bridge), BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), %% send an message to emqx and the message should be forwarded to the HTTP server @@ -170,49 +184,70 @@ t_http_crud_apis(_) -> emqx:publish(emqx_message:make(<<"emqx_http/1">>, Body)), ?assert( receive - {http_server, received, #{method := <<"POST">>, path := <<"/path1">>, - body := Body}} -> + {http_server, received, #{ + method := <<"POST">>, + path := <<"/path1">>, + body := Body + }} -> true; Msg -> ct:pal("error: http got unexpected request: ~p", [Msg]), false after 100 -> false - end), + end + ), %% update the request-path of the bridge URL2 = ?URL(Port, "path2"), - {ok, 200, Bridge2} = request(put, uri(["bridges", BridgeID]), - ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)), - ?assertMatch(#{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := _ - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL2 - }, jsx:decode(Bridge2)), + {ok, 200, Bridge2} = request( + put, + uri(["bridges", BridgeID]), + ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), + ?assertMatch( + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL2 + }, + jsx:decode(Bridge2) + ), %% list all bridges again, assert Bridge2 is in it {ok, 200, Bridge2Str} = request(get, uri(["bridges"]), []), - ?assertMatch([#{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := _ - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL2 - }], jsx:decode(Bridge2Str)), + ?assertMatch( + [ + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL2 + } + ], + jsx:decode(Bridge2Str) + ), %% get the bridge by id {ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := _ - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL2 - }, jsx:decode(Bridge3Str)), + ?assertMatch( + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := _, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL2 + }, + jsx:decode(Bridge3Str) + ), %% send an message to emqx again, check the path has been changed emqx:publish(emqx_message:make(<<"emqx_http/1">>, Body)), @@ -225,25 +260,35 @@ t_http_crud_apis(_) -> false after 100 -> false - end), + end + ), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), %% update a deleted bridge returns an error - {ok, 404, ErrMsg2} = request(put, uri(["bridges", BridgeID]), - ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)), + {ok, 404, ErrMsg2} = request( + put, + uri(["bridges", BridgeID]), + ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), ?assertMatch( - #{ <<"code">> := _ - , <<"message">> := <<"bridge not found">> - }, jsx:decode(ErrMsg2)), + #{ + <<"code">> := _, + <<"message">> := <<"bridge not found">> + }, + jsx:decode(ErrMsg2) + ), ok. t_start_stop_bridges(_) -> - lists:foreach(fun(Type) -> + lists:foreach( + fun(Type) -> do_start_stop_bridges(Type) - end, [node, cluster]). + end, + [node, cluster] + ). do_start_stop_bridges(Type) -> %% assert we there's no bridges at first @@ -251,40 +296,40 @@ do_start_stop_bridges(Type) -> Port = start_http_server(fun handle_fun_200_ok/2), URL1 = ?URL(Port, "abc"), - {ok, 201, Bridge} = request(post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), %ct:pal("the bridge ==== ~p", [Bridge]), - #{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := <<"connected">> - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL1 - } = jsx:decode(Bridge), + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL1 + } = jsx:decode(Bridge), BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), %% stop it {ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"disconnected">> - }, jsx:decode(Bridge2)), + ?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)), %% start again {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge3)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% restart an already started bridge {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge3)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% stop it again {ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), %% restart a stopped bridge {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge4)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). @@ -295,33 +340,34 @@ t_enable_disable_bridges(_) -> Port = start_http_server(fun handle_fun_200_ok/2), URL1 = ?URL(Port, "abc"), - {ok, 201, Bridge} = request(post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), %ct:pal("the bridge ==== ~p", [Bridge]), - #{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := <<"connected">> - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL1 - } = jsx:decode(Bridge), + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL1 + } = jsx:decode(Bridge), BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), %% disable it {ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"disconnected">> - }, jsx:decode(Bridge2)), + ?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)), %% enable again {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge3)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% enable an already started bridge {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge3)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)), %% disable it again {ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>), @@ -331,8 +377,7 @@ t_enable_disable_bridges(_) -> %% enable a stopped bridge {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), {ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(Bridge4)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). @@ -343,17 +388,21 @@ t_reset_bridges(_) -> Port = start_http_server(fun handle_fun_200_ok/2), URL1 = ?URL(Port, "abc"), - {ok, 201, Bridge} = request(post, uri(["bridges"]), - ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), + ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) + ), %ct:pal("the bridge ==== ~p", [Bridge]), - #{ <<"type">> := ?BRIDGE_TYPE - , <<"name">> := ?BRIDGE_NAME - , <<"status">> := <<"connected">> - , <<"node_status">> := [_|_] - , <<"metrics">> := _ - , <<"node_metrics">> := [_|_] - , <<"url">> := URL1 - } = jsx:decode(Bridge), + #{ + <<"type">> := ?BRIDGE_TYPE, + <<"name">> := ?BRIDGE_NAME, + <<"status">> := <<"connected">>, + <<"node_status">> := [_ | _], + <<"metrics">> := _, + <<"node_metrics">> := [_ | _], + <<"url">> := URL1 + } = jsx:decode(Bridge), BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), {ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []), diff --git a/apps/emqx_connector/include/emqx_connector.hrl b/apps/emqx_connector/include/emqx_connector.hrl index f0e07166d..ec7c2cd5e 100644 --- a/apps/emqx_connector/include/emqx_connector.hrl +++ b/apps/emqx_connector/include/emqx_connector.hrl @@ -24,13 +24,16 @@ -define(REDIS_DEFAULT_PORT, 6379). -define(PGSQL_DEFAULT_PORT, 5432). --define(SERVERS_DESC, "A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].` -For each Node should be: "). +-define(SERVERS_DESC, + "A Node list for Cluster to connect to. The nodes should be separated with commas, such as: `Node[,Node].`\n" + "For each Node should be: " +). --define(SERVER_DESC(TYPE, DEFAULT_PORT), " -The IPv4 or IPv6 address or the hostname to connect to.
-A host entry has the following form: `Host[:Port]`.
-The " ++ TYPE ++ " default port " ++ DEFAULT_PORT ++ " is used if `[:Port]` is not specified." +-define(SERVER_DESC(TYPE, DEFAULT_PORT), + "\n" + "The IPv4 or IPv6 address or the hostname to connect to.
\n" + "A host entry has the following form: `Host[:Port]`.
\n" + "The " ++ TYPE ++ " default port " ++ DEFAULT_PORT ++ " is used if `[:Port]` is not specified." ). -define(THROW_ERROR(Str), erlang:throw({error, Str})). diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index 5467c5261..a6fd0c77e 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -1,30 +1,32 @@ %% -*- mode: erlang -*- {erl_opts, [ - nowarn_unused_import, - debug_info + nowarn_unused_import, + debug_info ]}. {deps, [ - {emqx, {path, "../emqx"}}, - {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, - {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, - {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}}, - %% NOTE: mind poolboy version when updating mongodb-erlang version - {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}}, - %% NOTE: mind poolboy version when updating eredis_cluster version - {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.1"}}}, - %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git - %% (which has overflow_ttl feature added). - %% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07). - %% By accident, We have always been using the upstream fork due to - %% eredis_cluster's dependency getting resolved earlier. - %% Here we pin 1.5.2 to avoid surprises in the future. - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, - {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}} + {emqx, {path, "../emqx"}}, + {eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}, + {mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}, + {epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}}, + %% NOTE: mind poolboy version when updating mongodb-erlang version + {mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}}, + %% NOTE: mind poolboy version when updating eredis_cluster version + {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.1"}}}, + %% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git + %% (which has overflow_ttl feature added). + %% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07). + %% By accident, We have always been using the upstream fork due to + %% eredis_cluster's dependency getting resolved earlier. + %% Here we pin 1.5.2 to avoid surprises in the future. + {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}} ]}. {shell, [ - % {config, "config/sys.config"}, + % {config, "config/sys.config"}, {apps, [emqx_connector]} ]}. + +{project_plugins, [erlfmt]}. diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index d83d16764..fd804f6c5 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -1,27 +1,27 @@ %% -*- mode: erlang -*- -{application, emqx_connector, - [{description, "An OTP application"}, - {vsn, "0.1.1"}, - {registered, []}, - {mod, {emqx_connector_app, []}}, - {applications, - [kernel, - stdlib, - ecpool, - emqx_resource, - eredis_cluster, - eredis, - epgsql, - eldap2, - mysql, - mongodb, - ehttpc, - emqx, - emqtt - ]}, - {env,[]}, - {modules, []}, +{application, emqx_connector, [ + {description, "An OTP application"}, + {vsn, "0.1.1"}, + {registered, []}, + {mod, {emqx_connector_app, []}}, + {applications, [ + kernel, + stdlib, + ecpool, + emqx_resource, + eredis_cluster, + eredis, + epgsql, + eldap2, + mysql, + mongodb, + ehttpc, + emqx, + emqtt + ]}, + {env, []}, + {modules, []}, - {licenses, ["Apache 2.0"]}, - {links, []} - ]}. + {licenses, ["Apache 2.0"]}, + {links, []} +]}. diff --git a/apps/emqx_connector/src/emqx_connector.erl b/apps/emqx_connector/src/emqx_connector.erl index fbb89e8e7..aefff666a 100644 --- a/apps/emqx_connector/src/emqx_connector.erl +++ b/apps/emqx_connector/src/emqx_connector.erl @@ -15,24 +15,27 @@ %%-------------------------------------------------------------------- -module(emqx_connector). --export([ config_key_path/0 - , pre_config_update/3 - , post_config_update/5 - ]). +-export([ + config_key_path/0, + pre_config_update/3, + post_config_update/5 +]). --export([ parse_connector_id/1 - , connector_id/2 - ]). +-export([ + parse_connector_id/1, + connector_id/2 +]). --export([ list_raw/0 - , lookup_raw/1 - , lookup_raw/2 - , create_dry_run/2 - , update/2 - , update/3 - , delete/1 - , delete/2 - ]). +-export([ + list_raw/0, + lookup_raw/1, + lookup_raw/2, + create_dry_run/2, + update/2, + update/3, + delete/1, + delete/2 +]). config_key_path() -> [connectors]. @@ -53,19 +56,27 @@ post_config_update([connectors, Type, Name] = Path, '$remove', _, OldConf, _AppE throw({dependency_bridges_exist, emqx_bridge:bridge_id(BType, BName)}) end), _ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf) - catch throw:Error -> {error, Error} + catch + throw:Error -> {error, Error} end; post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) -> ConnId = connector_id(Type, Name), - foreach_linked_bridges(ConnId, + foreach_linked_bridges( + ConnId, fun(#{type := BType, name := BName}) -> BridgeConf = emqx:get_config([bridges, BType, BName]), - case emqx_bridge:update(BType, BName, {BridgeConf#{connector => OldConf}, - BridgeConf#{connector => NewConf}}) of + case + emqx_bridge:update( + BType, + BName, + {BridgeConf#{connector => OldConf}, BridgeConf#{connector => NewConf}} + ) + of ok -> ok; {error, Reason} -> error({update_bridge_error, Reason}) end - end). + end + ). connector_id(Type0, Name0) -> Type = bin(Type0), @@ -80,13 +91,22 @@ parse_connector_id(ConnectorId) -> list_raw() -> case get_raw_connector_conf() of - not_found -> []; + not_found -> + []; Config -> - lists:foldl(fun({Type, NameAndConf}, Connectors) -> - lists:foldl(fun({Name, RawConf}, Acc) -> - [RawConf#{<<"type">> => Type, <<"name">> => Name} | Acc] - end, Connectors, maps:to_list(NameAndConf)) - end, [], maps:to_list(Config)) + lists:foldl( + fun({Type, NameAndConf}, Connectors) -> + lists:foldl( + fun({Name, RawConf}, Acc) -> + [RawConf#{<<"type">> => Type, <<"name">> => Name} | Acc] + end, + Connectors, + maps:to_list(NameAndConf) + ) + end, + [], + maps:to_list(Config) + ) end. lookup_raw(Id) when is_binary(Id) -> @@ -96,7 +116,8 @@ lookup_raw(Id) when is_binary(Id) -> lookup_raw(Type, Name) -> Path = [bin(P) || P <- [Type, Name]], case get_raw_connector_conf() of - not_found -> {error, not_found}; + not_found -> + {error, not_found}; Conf -> case emqx_map_lib:deep_get(Path, Conf, not_found) of not_found -> {error, not_found}; @@ -123,7 +144,8 @@ delete(Type, Name) -> get_raw_connector_conf() -> case emqx:get_raw_config(config_key_path(), not_found) of - not_found -> not_found; + not_found -> + not_found; RawConf -> #{<<"connectors">> := Conf} = emqx_config:fill_defaults(#{<<"connectors">> => RawConf}), @@ -135,8 +157,12 @@ bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). foreach_linked_bridges(ConnId, Do) -> - lists:foreach(fun - (#{raw_config := #{<<"connector">> := ConnId0}} = Bridge) when ConnId0 == ConnId -> - Do(Bridge); - (_) -> ok - end, emqx_bridge:list()). + lists:foreach( + fun + (#{raw_config := #{<<"connector">> := ConnId0}} = Bridge) when ConnId0 == ConnId -> + Do(Bridge); + (_) -> + ok + end, + emqx_bridge:list() + ). diff --git a/apps/emqx_connector/src/emqx_connector_api.erl b/apps/emqx_connector/src/emqx_connector_api.erl index 4aa710c79..8fb4f596d 100644 --- a/apps/emqx_connector/src/emqx_connector_api.erl +++ b/apps/emqx_connector/src/emqx_connector_api.erl @@ -40,9 +40,14 @@ EXPR catch error:{invalid_connector_id, Id0} -> - {400, #{code => 'INVALID_ID', message => <<"invalid_connector_id: ", Id0/binary, - ". Connector Ids must be of format {type}:{name}">>}} - end). + {400, #{ + code => 'INVALID_ID', + message => + <<"invalid_connector_id: ", Id0/binary, + ". Connector Ids must be of format {type}:{name}">> + }} + end +). namespace() -> "connector". @@ -58,21 +63,25 @@ error_schema(Codes, Message) when is_binary(Message) -> put_request_body_schema() -> emqx_dashboard_swagger:schema_with_examples( - emqx_connector_schema:put_request(), connector_info_examples(put)). + emqx_connector_schema:put_request(), connector_info_examples(put) + ). post_request_body_schema() -> emqx_dashboard_swagger:schema_with_examples( - emqx_connector_schema:post_request(), connector_info_examples(post)). + emqx_connector_schema:post_request(), connector_info_examples(post) + ). get_response_body_schema() -> emqx_dashboard_swagger:schema_with_examples( - emqx_connector_schema:get_response(), connector_info_examples(get)). + emqx_connector_schema:get_response(), connector_info_examples(get) + ). connector_info_array_example(Method) -> [Config || #{value := Config} <- maps:values(connector_info_examples(Method))]. connector_info_examples(Method) -> - lists:foldl(fun(Type, Acc) -> + lists:foldl( + fun(Type, Acc) -> SType = atom_to_list(Type), maps:merge(Acc, #{ Type => #{ @@ -80,11 +89,16 @@ connector_info_examples(Method) -> value => info_example(Type, Method) } }) - end, #{}, ?CONN_TYPES). + end, + #{}, + ?CONN_TYPES + ). info_example(Type, Method) -> - maps:merge(info_example_basic(Type), - method_example(Type, Method)). + maps:merge( + info_example_basic(Type), + method_example(Type, Method) + ). method_example(Type, Method) when Method == get; Method == post -> SType = atom_to_list(Type), @@ -115,11 +129,17 @@ info_example_basic(mqtt) -> }. param_path_id() -> - [{id, mk(binary(), - #{ in => path - , example => <<"mqtt:my_mqtt_connector">> - , desc => ?DESC("id") - })}]. + [ + {id, + mk( + binary(), + #{ + in => path, + example => <<"mqtt:my_mqtt_connector">>, + desc => ?DESC("id") + } + )} + ]. schema("/connectors_test") -> #{ @@ -135,7 +155,6 @@ schema("/connectors_test") -> } } }; - schema("/connectors") -> #{ 'operationId' => '/connectors', @@ -145,8 +164,9 @@ schema("/connectors") -> summary => <<"List connectors">>, responses => #{ 200 => emqx_dashboard_swagger:schema_with_example( - array(emqx_connector_schema:get_response()), - connector_info_array_example(get)) + array(emqx_connector_schema:get_response()), + connector_info_array_example(get) + ) } }, post => #{ @@ -160,7 +180,6 @@ schema("/connectors") -> } } }; - schema("/connectors/:id") -> #{ 'operationId' => '/connectors/:id', @@ -185,7 +204,8 @@ schema("/connectors/:id") -> 200 => get_response_body_schema(), 404 => error_schema(['NOT_FOUND'], "Connector not found"), 400 => error_schema(['INVALID_ID'], "Bad connector ID") - }}, + } + }, delete => #{ tags => [<<"connectors">>], desc => ?DESC("conn_id_delete"), @@ -196,7 +216,8 @@ schema("/connectors/:id") -> 403 => error_schema(['DEPENDENCY_EXISTS'], "Cannot remove dependent connector"), 404 => error_schema(['NOT_FOUND'], "Delete failed, not found"), 400 => error_schema(['INVALID_ID'], "Bad connector ID") - }} + } + } }. '/connectors_test'(post, #{body := #{<<"type">> := ConnType} = Params}) -> @@ -209,67 +230,83 @@ schema("/connectors/:id") -> '/connectors'(get, _Request) -> {200, [format_resp(Conn) || Conn <- emqx_connector:list_raw()]}; - '/connectors'(post, #{body := #{<<"type">> := ConnType, <<"name">> := ConnName} = Params}) -> case emqx_connector:lookup_raw(ConnType, ConnName) of {ok, _} -> {400, error_msg('ALREADY_EXISTS', <<"connector already exists">>)}; {error, not_found} -> - case emqx_connector:update(ConnType, ConnName, - filter_out_request_body(Params)) of + case + emqx_connector:update( + ConnType, + ConnName, + filter_out_request_body(Params) + ) + of {ok, #{raw_config := RawConf}} -> - {201, format_resp(RawConf#{<<"type">> => ConnType, - <<"name">> => ConnName})}; + {201, + format_resp(RawConf#{ + <<"type">> => ConnType, + <<"name">> => ConnName + })}; {error, Error} -> {400, error_msg('BAD_REQUEST', Error)} end end; - '/connectors'(post, _) -> {400, error_msg('BAD_REQUEST', <<"missing some required fields: [name, type]">>)}. '/connectors/:id'(get, #{bindings := #{id := Id}}) -> - ?TRY_PARSE_ID(Id, + ?TRY_PARSE_ID( + Id, case emqx_connector:lookup_raw(ConnType, ConnName) of {ok, Conf} -> {200, format_resp(Conf)}; {error, not_found} -> {404, error_msg('NOT_FOUND', <<"connector not found">>)} - end); - + end + ); '/connectors/:id'(put, #{bindings := #{id := Id}, body := Params0}) -> Params = filter_out_request_body(Params0), - ?TRY_PARSE_ID(Id, + ?TRY_PARSE_ID( + Id, case emqx_connector:lookup_raw(ConnType, ConnName) of {ok, _} -> case emqx_connector:update(ConnType, ConnName, Params) of {ok, #{raw_config := RawConf}} -> - {200, format_resp(RawConf#{<<"type">> => ConnType, - <<"name">> => ConnName})}; + {200, + format_resp(RawConf#{ + <<"type">> => ConnType, + <<"name">> => ConnName + })}; {error, Error} -> {500, error_msg('INTERNAL_ERROR', Error)} end; {error, not_found} -> {404, error_msg('NOT_FOUND', <<"connector not found">>)} - end); - + end + ); '/connectors/:id'(delete, #{bindings := #{id := Id}}) -> - ?TRY_PARSE_ID(Id, + ?TRY_PARSE_ID( + Id, case emqx_connector:lookup_raw(ConnType, ConnName) of {ok, _} -> case emqx_connector:delete(ConnType, ConnName) of {ok, _} -> {204}; {error, {post_config_update, _, {dependency_bridges_exist, BridgeID}}} -> - {403, error_msg('DEPENDENCY_EXISTS', - <<"Cannot remove the connector as it's in use by a bridge: ", - BridgeID/binary>>)}; + {403, + error_msg( + 'DEPENDENCY_EXISTS', + <<"Cannot remove the connector as it's in use by a bridge: ", + BridgeID/binary>> + )}; {error, Error} -> {500, error_msg('INTERNAL_ERROR', Error)} end; {error, not_found} -> {404, error_msg('NOT_FOUND', <<"connector not found">>)} - end). + end + ). error_msg(Code, Msg) when is_binary(Msg) -> #{code => Code, message => Msg}; @@ -277,8 +314,11 @@ error_msg(Code, Msg) -> #{code => Code, message => bin(io_lib:format("~p", [Msg]))}. format_resp(#{<<"type">> := ConnType, <<"name">> := ConnName} = RawConf) -> - NumOfBridges = length(emqx_bridge:list_bridges_by_connector( - emqx_connector:connector_id(ConnType, ConnName))), + NumOfBridges = length( + emqx_bridge:list_bridges_by_connector( + emqx_connector:connector_id(ConnType, ConnName) + ) + ), RawConf#{ <<"type">> => ConnType, <<"name">> => ConnName, diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 641e70959..38ddd2c23 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -25,32 +25,34 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). -type url() :: emqx_http_lib:uri_map(). -reflect_type([url/0]). -typerefl_from_string({url/0, emqx_http_lib, uri_parse}). --export([ roots/0 - , fields/1 - , desc/1 - , validations/0 - , namespace/0 - ]). +-export([ + roots/0, + fields/1, + desc/1, + validations/0, + namespace/0 +]). --export([ check_ssl_opts/2 - ]). +-export([check_ssl_opts/2]). -type connect_timeout() :: emqx_schema:duration() | infinity. -type pool_type() :: random | hash. --reflect_type([ connect_timeout/0 - , pool_type/0 - ]). +-reflect_type([ + connect_timeout/0, + pool_type/0 +]). %%===================================================================== %% Hocon schema @@ -61,63 +63,96 @@ roots() -> fields(config). fields(config) -> - [ {base_url, - sc(url(), - #{ required => true - , validator => fun(#{query := _Query}) -> + [ + {base_url, + sc( + url(), + #{ + required => true, + validator => fun + (#{query := _Query}) -> {error, "There must be no query in the base_url"}; - (_) -> ok - end - , desc => ?DESC("base_url") - })} - , {connect_timeout, - sc(emqx_schema:duration_ms(), - #{ default => "15s" - , desc => ?DESC("connect_timeout") - })} - , {max_retries, - sc(non_neg_integer(), - #{ default => 5 - , desc => ?DESC("max_retries") - })} - , {retry_interval, - sc(emqx_schema:duration(), - #{ default => "1s" - , desc => ?DESC("retry_interval") - })} - , {pool_type, - sc(pool_type(), - #{ default => random - , desc => ?DESC("pool_type") - })} - , {pool_size, - sc(pos_integer(), - #{ default => 8 - , desc => ?DESC("pool_size") - })} - , {enable_pipelining, - sc(boolean(), - #{ default => true - , desc => ?DESC("enable_pipelining") - })} - , {request, hoconsc:mk( - ref("request"), - #{ default => undefined - , required => false - , desc => ?DESC("request") - })} + (_) -> + ok + end, + desc => ?DESC("base_url") + } + )}, + {connect_timeout, + sc( + emqx_schema:duration_ms(), + #{ + default => "15s", + desc => ?DESC("connect_timeout") + } + )}, + {max_retries, + sc( + non_neg_integer(), + #{ + default => 5, + desc => ?DESC("max_retries") + } + )}, + {retry_interval, + sc( + emqx_schema:duration(), + #{ + default => "1s", + desc => ?DESC("retry_interval") + } + )}, + {pool_type, + sc( + pool_type(), + #{ + default => random, + desc => ?DESC("pool_type") + } + )}, + {pool_size, + sc( + pos_integer(), + #{ + default => 8, + desc => ?DESC("pool_size") + } + )}, + {enable_pipelining, + sc( + boolean(), + #{ + default => true, + desc => ?DESC("enable_pipelining") + } + )}, + {request, + hoconsc:mk( + ref("request"), + #{ + default => undefined, + required => false, + desc => ?DESC("request") + } + )} ] ++ emqx_connector_schema_lib:ssl_fields(); - fields("request") -> - [ {method, hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{required => false, desc => ?DESC("method")})} - , {path, hoconsc:mk(binary(), #{required => false, desc => ?DESC("path")})} - , {body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})} - , {headers, hoconsc:mk(map(), #{required => false, desc => ?DESC("headers")})} - , {request_timeout, - sc(emqx_schema:duration_ms(), - #{ required => false - , desc => ?DESC("request_timeout") - })} + [ + {method, + hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{ + required => false, desc => ?DESC("method") + })}, + {path, hoconsc:mk(binary(), #{required => false, desc => ?DESC("path")})}, + {body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})}, + {headers, hoconsc:mk(map(), #{required => false, desc => ?DESC("headers")})}, + {request_timeout, + sc( + emqx_schema:duration_ms(), + #{ + required => false, + desc => ?DESC("request_timeout") + } + )} ]. desc(config) -> @@ -128,24 +163,34 @@ desc(_) -> undefined. validations() -> - [ {check_ssl_opts, fun check_ssl_opts/1} ]. + [{check_ssl_opts, fun check_ssl_opts/1}]. sc(Type, Meta) -> hoconsc:mk(Type, Meta). ref(Field) -> hoconsc:ref(?MODULE, Field). %% =================================================================== -on_start(InstId, #{base_url := #{scheme := Scheme, - host := Host, - port := Port, - path := BasePath}, - connect_timeout := ConnectTimeout, - max_retries := MaxRetries, - retry_interval := RetryInterval, - pool_type := PoolType, - pool_size := PoolSize} = Config) -> - ?SLOG(info, #{msg => "starting_http_connector", - connector => InstId, config => Config}), +on_start( + InstId, + #{ + base_url := #{ + scheme := Scheme, + host := Host, + port := Port, + path := BasePath + }, + connect_timeout := ConnectTimeout, + max_retries := MaxRetries, + retry_interval := RetryInterval, + pool_type := PoolType, + pool_size := PoolSize + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_http_connector", + connector => InstId, + config => Config + }), {Transport, TransportOpts} = case Scheme of http -> @@ -155,16 +200,18 @@ on_start(InstId, #{base_url := #{scheme := Scheme, {tls, SSLOpts} end, NTransportOpts = emqx_misc:ipv6_probe(TransportOpts), - PoolOpts = [ {host, Host} - , {port, Port} - , {connect_timeout, ConnectTimeout} - , {retry, MaxRetries} - , {retry_timeout, RetryInterval} - , {keepalive, 30000} - , {pool_type, PoolType} - , {pool_size, PoolSize} - , {transport, Transport} - , {transport_opts, NTransportOpts}], + PoolOpts = [ + {host, Host}, + {port, Port}, + {connect_timeout, ConnectTimeout}, + {retry, MaxRetries}, + {retry_timeout, RetryInterval}, + {keepalive, 30000}, + {pool_type, PoolType}, + {pool_size, PoolSize}, + {transport, Transport}, + {transport_opts, NTransportOpts} + ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), State = #{ pool_name => PoolName, @@ -177,54 +224,84 @@ on_start(InstId, #{base_url := #{scheme := Scheme, case ehttpc_sup:start_pool(PoolName, PoolOpts) of {ok, _} -> {ok, State}; {error, {already_started, _}} -> {ok, State}; - {error, Reason} -> - {error, Reason} + {error, Reason} -> {error, Reason} end. on_stop(InstId, #{pool_name := PoolName}) -> - ?SLOG(info, #{msg => "stopping_http_connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping_http_connector", + connector => InstId + }), ehttpc_sup:stop_pool(PoolName). on_query(InstId, {send_message, Msg}, AfterQuery, State) -> case maps:get(request, State, undefined) of - undefined -> ?SLOG(error, #{msg => "request_not_found", connector => InstId}); + undefined -> + ?SLOG(error, #{msg => "request_not_found", connector => InstId}); Request -> - #{method := Method, path := Path, body := Body, headers := Headers, - request_timeout := Timeout} = process_request(Request, Msg), + #{ + method := Method, + path := Path, + body := Body, + headers := Headers, + request_timeout := Timeout + } = process_request(Request, Msg), on_query(InstId, {Method, {Path, Headers, Body}, Timeout}, AfterQuery, State) end; on_query(InstId, {Method, Request}, AfterQuery, State) -> on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State); on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) -> on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State); -on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, - #{pool_name := PoolName, base_path := BasePath} = State) -> - ?TRACE("QUERY", "http_connector_received", - #{request => Request, connector => InstId, state => State}), +on_query( + InstId, + {KeyOrNum, Method, Request, Timeout}, + AfterQuery, + #{pool_name := PoolName, base_path := BasePath} = State +) -> + ?TRACE( + "QUERY", + "http_connector_received", + #{request => Request, connector => InstId, state => State} + ), NRequest = formalize_request(Method, BasePath, Request), - case Result = ehttpc:request(case KeyOrNum of - undefined -> PoolName; - _ -> {PoolName, KeyOrNum} - end, Method, NRequest, Timeout) of + case + Result = ehttpc:request( + case KeyOrNum of + undefined -> PoolName; + _ -> {PoolName, KeyOrNum} + end, + Method, + NRequest, + Timeout + ) + of {error, Reason} -> - ?SLOG(error, #{msg => "http_connector_do_reqeust_failed", - request => NRequest, reason => Reason, - connector => InstId}), + ?SLOG(error, #{ + msg => "http_connector_do_reqeust_failed", + request => NRequest, + reason => Reason, + connector => InstId + }), emqx_resource:query_failed(AfterQuery); {ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 -> emqx_resource:query_success(AfterQuery); {ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 -> emqx_resource:query_success(AfterQuery); {ok, StatusCode, _} -> - ?SLOG(error, #{msg => "http connector do request, received error response", - request => NRequest, connector => InstId, - status_code => StatusCode}), + ?SLOG(error, #{ + msg => "http connector do request, received error response", + request => NRequest, + connector => InstId, + status_code => StatusCode + }), emqx_resource:query_failed(AfterQuery); {ok, StatusCode, _, _} -> - ?SLOG(error, #{msg => "http connector do request, received error response", - request => NRequest, connector => InstId, - status_code => StatusCode}), + ?SLOG(error, #{ + msg => "http connector do request, received error response", + request => NRequest, + connector => InstId, + status_code => StatusCode + }), emqx_resource:query_failed(AfterQuery) end, Result. @@ -232,14 +309,16 @@ on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery, on_health_check(_InstId, #{host := Host, port := Port, connect_timeout := Timeout} = State) -> case do_health_check(Host, Port, Timeout) of ok -> {ok, State}; - {error, Reason} -> - {error, {http_health_check_failed, Reason}, State} + {error, Reason} -> {error, {http_health_check_failed, Reason}, State} end. do_health_check(Host, Port, Timeout) -> case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), Timeout) of - {ok, Sock} -> gen_tcp:close(Sock), ok; - {error, Reason} -> {error, Reason} + {ok, Sock} -> + gen_tcp:close(Sock), + ok; + {error, Reason} -> + {error, Reason} end. %%-------------------------------------------------------------------- @@ -250,47 +329,64 @@ preprocess_request(undefined) -> undefined; preprocess_request(Req) when map_size(Req) == 0 -> undefined; -preprocess_request(#{ - method := Method, - path := Path, - body := Body, - headers := Headers - } = Req) -> - #{ method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method)) - , path => emqx_plugin_libs_rule:preproc_tmpl(Path) - , body => emqx_plugin_libs_rule:preproc_tmpl(Body) - , headers => preproc_headers(Headers) - , request_timeout => maps:get(request_timeout, Req, 30000) - }. +preprocess_request( + #{ + method := Method, + path := Path, + body := Body, + headers := Headers + } = Req +) -> + #{ + method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method)), + path => emqx_plugin_libs_rule:preproc_tmpl(Path), + body => emqx_plugin_libs_rule:preproc_tmpl(Body), + headers => preproc_headers(Headers), + request_timeout => maps:get(request_timeout, Req, 30000) + }. preproc_headers(Headers) when is_map(Headers) -> - maps:fold(fun(K, V, Acc) -> - [{ + maps:fold( + fun(K, V, Acc) -> + [ + { + emqx_plugin_libs_rule:preproc_tmpl(bin(K)), + emqx_plugin_libs_rule:preproc_tmpl(bin(V)) + } + | Acc + ] + end, + [], + Headers + ); +preproc_headers(Headers) when is_list(Headers) -> + lists:map( + fun({K, V}) -> + { emqx_plugin_libs_rule:preproc_tmpl(bin(K)), emqx_plugin_libs_rule:preproc_tmpl(bin(V)) - } | Acc] - end, [], Headers); -preproc_headers(Headers) when is_list(Headers) -> - lists:map(fun({K, V}) -> - { - emqx_plugin_libs_rule:preproc_tmpl(bin(K)), - emqx_plugin_libs_rule:preproc_tmpl(bin(V)) - } - end, Headers). + } + end, + Headers + ). -process_request(#{ - method := MethodTks, - path := PathTks, - body := BodyTks, - headers := HeadersTks, - request_timeout := ReqTimeout - } = Conf, Msg) -> - Conf#{ method => make_method(emqx_plugin_libs_rule:proc_tmpl(MethodTks, Msg)) - , path => emqx_plugin_libs_rule:proc_tmpl(PathTks, Msg) - , body => process_request_body(BodyTks, Msg) - , headers => proc_headers(HeadersTks, Msg) - , request_timeout => ReqTimeout - }. +process_request( + #{ + method := MethodTks, + path := PathTks, + body := BodyTks, + headers := HeadersTks, + request_timeout := ReqTimeout + } = Conf, + Msg +) -> + Conf#{ + method => make_method(emqx_plugin_libs_rule:proc_tmpl(MethodTks, Msg)), + path => emqx_plugin_libs_rule:proc_tmpl(PathTks, Msg), + body => process_request_body(BodyTks, Msg), + headers => proc_headers(HeadersTks, Msg), + request_timeout => ReqTimeout + }. process_request_body([], Msg) -> emqx_json:encode(Msg); @@ -298,12 +394,15 @@ process_request_body(BodyTks, Msg) -> emqx_plugin_libs_rule:proc_tmpl(BodyTks, Msg). proc_headers(HeaderTks, Msg) -> - lists:map(fun({K, V}) -> + lists:map( + fun({K, V}) -> { emqx_plugin_libs_rule:proc_tmpl(K, Msg), emqx_plugin_libs_rule:proc_tmpl(V, Msg) } - end, HeaderTks). + end, + HeaderTks + ). make_method(M) when M == <<"POST">>; M == <<"post">> -> post; make_method(M) when M == <<"PUT">>; M == <<"put">> -> put; @@ -315,19 +414,19 @@ check_ssl_opts(Conf) -> check_ssl_opts(URLFrom, Conf) -> #{scheme := Scheme} = hocon_maps:get(URLFrom, Conf), - SSL= hocon_maps:get("ssl", Conf), + SSL = hocon_maps:get("ssl", Conf), case {Scheme, maps:get(enable, SSL, false)} of {http, false} -> true; {https, true} -> true; {_, _} -> false end. -formalize_request(Method, BasePath, {Path, Headers, _Body}) - when Method =:= get; Method =:= delete -> +formalize_request(Method, BasePath, {Path, Headers, _Body}) when + Method =:= get; Method =:= delete +-> formalize_request(Method, BasePath, {Path, Headers}); formalize_request(_Method, BasePath, {Path, Headers, Body}) -> {filename:join(BasePath, Path), Headers, Body}; - formalize_request(_Method, BasePath, {Path, Headers}) -> {filename:join(BasePath, Path), Headers}. diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 918338ea1..25798fae5 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -24,11 +24,12 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). -export([do_health_check/1]). @@ -43,54 +44,84 @@ roots() -> fields(_) -> []. %% =================================================================== -on_start(InstId, #{servers := Servers0, - port := Port, - bind_dn := BindDn, - bind_password := BindPassword, - timeout := Timeout, - pool_size := PoolSize, - auto_reconnect := AutoReconn, - ssl := SSL} = Config) -> - ?SLOG(info, #{msg => "starting_ldap_connector", - connector => InstId, config => Config}), - Servers = [begin proplists:get_value(host, S) end || S <- Servers0], - SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, true}, - {sslopts, emqx_tls_lib:to_client_opts(SSL)} - ]; - false -> [{ssl, false}] - end, - Opts = [{servers, Servers}, - {port, Port}, - {bind_dn, BindDn}, - {bind_password, BindPassword}, - {timeout, Timeout}, - {pool_size, PoolSize}, - {auto_reconnect, reconn_interval(AutoReconn)}, - {servers, Servers}], +on_start( + InstId, + #{ + servers := Servers0, + port := Port, + bind_dn := BindDn, + bind_password := BindPassword, + timeout := Timeout, + pool_size := PoolSize, + auto_reconnect := AutoReconn, + ssl := SSL + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_ldap_connector", + connector => InstId, + config => Config + }), + Servers = [ + begin + proplists:get_value(host, S) + end + || S <- Servers0 + ], + SslOpts = + case maps:get(enable, SSL) of + true -> + [ + {ssl, true}, + {sslopts, emqx_tls_lib:to_client_opts(SSL)} + ]; + false -> + [{ssl, false}] + end, + Opts = [ + {servers, Servers}, + {port, Port}, + {bind_dn, BindDn}, + {bind_password, BindPassword}, + {timeout, Timeout}, + {pool_size, PoolSize}, + {auto_reconnect, reconn_interval(AutoReconn)}, + {servers, Servers} + ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts) of - ok -> {ok, #{poolname => PoolName}}; + ok -> {ok, #{poolname => PoolName}}; {error, Reason} -> {error, Reason} end. on_stop(InstId, #{poolname := PoolName}) -> - ?SLOG(info, #{msg => "stopping_ldap_connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping_ldap_connector", + connector => InstId + }), emqx_plugin_libs_pool:stop_pool(PoolName). on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) -> Request = {Base, Filter, Attributes}, - ?TRACE("QUERY", "ldap_connector_received", - #{request => Request, connector => InstId, state => State}), - case Result = ecpool:pick_and_do( - PoolName, - {?MODULE, search, [Base, Filter, Attributes]}, - no_handover) of + ?TRACE( + "QUERY", + "ldap_connector_received", + #{request => Request, connector => InstId, state => State} + ), + case + Result = ecpool:pick_and_do( + PoolName, + {?MODULE, search, [Base, Filter, Attributes]}, + no_handover + ) + of {error, Reason} -> - ?SLOG(error, #{msg => "ldap_connector_do_request_failed", - request => Request, connector => InstId, reason => Reason}), + ?SLOG(error, #{ + msg => "ldap_connector_do_request_failed", + request => Request, + connector => InstId, + reason => Reason + }), emqx_resource:query_failed(AfterQuery); _ -> emqx_resource:query_success(AfterQuery) @@ -107,38 +138,45 @@ reconn_interval(true) -> 15; reconn_interval(false) -> false. search(Conn, Base, Filter, Attributes) -> - eldap2:search(Conn, [{base, Base}, - {filter, Filter}, - {attributes, Attributes}, - {deref, eldap2:'derefFindingBaseObj'()}]). + eldap2:search(Conn, [ + {base, Base}, + {filter, Filter}, + {attributes, Attributes}, + {deref, eldap2:'derefFindingBaseObj'()} + ]). %% =================================================================== connect(Opts) -> - Servers = proplists:get_value(servers, Opts, ["localhost"]), - Port = proplists:get_value(port, Opts, 389), - Timeout = proplists:get_value(timeout, Opts, 30), - BindDn = proplists:get_value(bind_dn, Opts), + Servers = proplists:get_value(servers, Opts, ["localhost"]), + Port = proplists:get_value(port, Opts, 389), + Timeout = proplists:get_value(timeout, Opts, 30), + BindDn = proplists:get_value(bind_dn, Opts), BindPassword = proplists:get_value(bind_password, Opts), - SslOpts = case proplists:get_value(ssl, Opts, false) of - true -> - [{sslopts, proplists:get_value(sslopts, Opts, [])}, {ssl, true}]; - false -> - [{ssl, false}] - end, - LdapOpts = [{port, Port}, - {timeout, Timeout}] ++ SslOpts, + SslOpts = + case proplists:get_value(ssl, Opts, false) of + true -> + [{sslopts, proplists:get_value(sslopts, Opts, [])}, {ssl, true}]; + false -> + [{ssl, false}] + end, + LdapOpts = + [ + {port, Port}, + {timeout, Timeout} + ] ++ SslOpts, {ok, LDAP} = eldap2:open(Servers, LdapOpts), ok = eldap2:simple_bind(LDAP, BindDn, BindPassword), {ok, LDAP}. ldap_fields() -> - [ {servers, fun servers/1} - , {port, fun port/1} - , {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {bind_dn, fun bind_dn/1} - , {bind_password, fun emqx_connector_schema_lib:password/1} - , {timeout, fun duration/1} - , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} + [ + {servers, fun servers/1}, + {port, fun port/1}, + {pool_size, fun emqx_connector_schema_lib:pool_size/1}, + {bind_dn, fun bind_dn/1}, + {bind_password, fun emqx_connector_schema_lib:password/1}, + {timeout, fun duration/1}, + {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} ]. servers(type) -> list(); @@ -159,14 +197,18 @@ duration(type) -> emqx_schema:duration_ms(); duration(_) -> undefined. to_servers_raw(Servers) -> - {ok, lists:map( fun(Server) -> - case string:tokens(Server, ": ") of - [Ip] -> - [{host, Ip}]; - [Ip, Port] -> - [{host, Ip}, {port, list_to_integer(Port)}] - end - end, string:tokens(str(Servers), ", "))}. + {ok, + lists:map( + fun(Server) -> + case string:tokens(Server, ": ") of + [Ip] -> + [{host, Ip}]; + [Ip, Port] -> + [{host, Ip}, {port, list_to_integer(Port)}] + end + end, + string:tokens(str(Servers), ", ") + )}. str(A) when is_atom(A) -> atom_to_list(A); diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index d81aa04a4..170fa096b 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -24,11 +24,12 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). %% ecpool callback -export([connect/1]). @@ -40,57 +41,73 @@ -define(HEALTH_CHECK_TIMEOUT, 10000). %% mongo servers don't need parse --define( MONGO_HOST_OPTIONS - , #{ host_type => hostname - , default_port => ?MONGO_DEFAULT_PORT}). +-define(MONGO_HOST_OPTIONS, #{ + host_type => hostname, + default_port => ?MONGO_DEFAULT_PORT +}). %%===================================================================== roots() -> - [ {config, #{type => hoconsc:union( - [ hoconsc:ref(?MODULE, single) - , hoconsc:ref(?MODULE, rs) - , hoconsc:ref(?MODULE, sharded) - ])}} + [ + {config, #{ + type => hoconsc:union( + [ + hoconsc:ref(?MODULE, single), + hoconsc:ref(?MODULE, rs), + hoconsc:ref(?MODULE, sharded) + ] + ) + }} ]. fields(single) -> - [ {mongo_type, #{type => single, - default => single, - required => true, - desc => ?DESC("single_mongo_type")}} - , {server, fun server/1} - , {w_mode, fun w_mode/1} + [ + {mongo_type, #{ + type => single, + default => single, + required => true, + desc => ?DESC("single_mongo_type") + }}, + {server, fun server/1}, + {w_mode, fun w_mode/1} ] ++ mongo_fields(); fields(rs) -> - [ {mongo_type, #{type => rs, - default => rs, - required => true, - desc => ?DESC("rs_mongo_type")}} - , {servers, fun servers/1} - , {w_mode, fun w_mode/1} - , {r_mode, fun r_mode/1} - , {replica_set_name, fun replica_set_name/1} + [ + {mongo_type, #{ + type => rs, + default => rs, + required => true, + desc => ?DESC("rs_mongo_type") + }}, + {servers, fun servers/1}, + {w_mode, fun w_mode/1}, + {r_mode, fun r_mode/1}, + {replica_set_name, fun replica_set_name/1} ] ++ mongo_fields(); fields(sharded) -> - [ {mongo_type, #{type => sharded, - default => sharded, - required => true, - desc => ?DESC("sharded_mongo_type")}} - , {servers, fun servers/1} - , {w_mode, fun w_mode/1} + [ + {mongo_type, #{ + type => sharded, + default => sharded, + required => true, + desc => ?DESC("sharded_mongo_type") + }}, + {servers, fun servers/1}, + {w_mode, fun w_mode/1} ] ++ mongo_fields(); fields(topology) -> - [ {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} - , {connect_timeout_ms, fun duration/1} - , {socket_timeout_ms, fun duration/1} - , {server_selection_timeout_ms, fun duration/1} - , {wait_queue_timeout_ms, fun duration/1} - , {heartbeat_frequency_ms, fun duration/1} - , {min_heartbeat_frequency_ms, fun duration/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}, + {connect_timeout_ms, fun duration/1}, + {socket_timeout_ms, fun duration/1}, + {server_selection_timeout_ms, fun duration/1}, + {wait_queue_timeout_ms, fun duration/1}, + {heartbeat_frequency_ms, fun duration/1}, + {min_heartbeat_frequency_ms, fun duration/1} ]. desc(single) -> @@ -105,69 +122,96 @@ desc(_) -> undefined. mongo_fields() -> - [ {srv_record, fun srv_record/1} - , {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {username, fun emqx_connector_schema_lib:username/1} - , {password, fun emqx_connector_schema_lib:password/1} - , {auth_source, #{ type => binary() - , required => false - , desc => ?DESC("auth_source") - }} - , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology), required => false}} + [ + {srv_record, fun srv_record/1}, + {pool_size, fun emqx_connector_schema_lib:pool_size/1}, + {username, fun emqx_connector_schema_lib:username/1}, + {password, fun emqx_connector_schema_lib:password/1}, + {auth_source, #{ + type => binary(), + required => false, + desc => ?DESC("auth_source") + }}, + {database, fun emqx_connector_schema_lib:database/1}, + {topology, #{type => hoconsc:ref(?MODULE, topology), required => false}} ] ++ - emqx_connector_schema_lib:ssl_fields(). + emqx_connector_schema_lib:ssl_fields(). %% =================================================================== -on_start(InstId, Config = #{mongo_type := Type, - pool_size := PoolSize, - ssl := SSL}) -> - Msg = case Type of - single -> "starting_mongodb_single_connector"; - rs -> "starting_mongodb_replica_set_connector"; - sharded -> "starting_mongodb_sharded_connector" - end, +on_start( + InstId, + Config = #{ + mongo_type := Type, + pool_size := PoolSize, + ssl := SSL + } +) -> + Msg = + case Type of + single -> "starting_mongodb_single_connector"; + rs -> "starting_mongodb_replica_set_connector"; + sharded -> "starting_mongodb_sharded_connector" + end, ?SLOG(info, #{msg => Msg, connector => InstId, config => Config}), NConfig = #{hosts := Hosts} = may_parse_srv_and_txt_records(Config), - SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, true}, - {ssl_opts, emqx_tls_lib:to_client_opts(SSL)} - ]; - false -> [{ssl, false}] - end, + SslOpts = + case maps:get(enable, SSL) of + true -> + [ + {ssl, true}, + {ssl_opts, emqx_tls_lib:to_client_opts(SSL)} + ]; + false -> + [{ssl, false}] + end, Topology = maps:get(topology, NConfig, #{}), - Opts = [{mongo_type, init_type(NConfig)}, - {hosts, Hosts}, - {pool_size, PoolSize}, - {options, init_topology_options(maps:to_list(Topology), [])}, - {worker_options, init_worker_options(maps:to_list(NConfig), SslOpts)}], + Opts = [ + {mongo_type, init_type(NConfig)}, + {hosts, Hosts}, + {pool_size, PoolSize}, + {options, init_topology_options(maps:to_list(Topology), [])}, + {worker_options, init_worker_options(maps:to_list(NConfig), SslOpts)} + ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts) of - ok -> {ok, #{poolname => PoolName, type => Type}}; + ok -> {ok, #{poolname => PoolName, type => Type}}; {error, Reason} -> {error, Reason} end. on_stop(InstId, #{poolname := PoolName}) -> - ?SLOG(info, #{msg => "stopping_mongodb_connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping_mongodb_connector", + connector => InstId + }), emqx_plugin_libs_pool:stop_pool(PoolName). -on_query(InstId, - {Action, Collection, Selector, Projector}, - AfterQuery, - #{poolname := PoolName} = State) -> +on_query( + InstId, + {Action, Collection, Selector, Projector}, + AfterQuery, + #{poolname := PoolName} = State +) -> Request = {Action, Collection, Selector, Projector}, - ?TRACE("QUERY", "mongodb_connector_received", - #{request => Request, connector => InstId, state => State}), - case ecpool:pick_and_do(PoolName, - {?MODULE, mongo_query, [Action, Collection, Selector, Projector]}, - no_handover) of + ?TRACE( + "QUERY", + "mongodb_connector_received", + #{request => Request, connector => InstId, state => State} + ), + case + ecpool:pick_and_do( + PoolName, + {?MODULE, mongo_query, [Action, Collection, Selector, Projector]}, + no_handover + ) + of {error, Reason} -> - ?SLOG(error, #{msg => "mongodb_connector_do_query_failed", - request => Request, reason => Reason, - connector => InstId}), + ?SLOG(error, #{ + msg => "mongodb_connector_do_query_failed", + request => Request, + reason => Reason, + connector => InstId + }), emqx_resource:query_failed(AfterQuery), {error, Reason}; {ok, Cursor} when is_pid(Cursor) -> @@ -182,12 +226,16 @@ on_query(InstId, on_health_check(InstId, #{poolname := PoolName} = State) -> case health_check(PoolName) of true -> - ?tp(debug, emqx_connector_mongo_health_check, #{instance_id => InstId, - status => ok}), + ?tp(debug, emqx_connector_mongo_health_check, #{ + instance_id => InstId, + status => ok + }), {ok, State}; false -> - ?tp(warning, emqx_connector_mongo_health_check, #{instance_id => InstId, - status => failed}), + ?tp(warning, emqx_connector_mongo_health_check, #{ + instance_id => InstId, + status => failed + }), {error, health_check_failed, State} end. @@ -204,36 +252,43 @@ check_worker_health(Worker) -> %% we don't care if this returns something or not, we just to test the connection try do_test_query(Conn) of {error, Reason} -> - ?SLOG(warning, #{msg => "mongo_connection_health_check_error", - worker => Worker, - reason => Reason}), + ?SLOG(warning, #{ + msg => "mongo_connection_health_check_error", + worker => Worker, + reason => Reason + }), false; _ -> true catch Class:Error -> - ?SLOG(warning, #{msg => "mongo_connection_health_check_exception", - worker => Worker, - class => Class, - error => Error}), + ?SLOG(warning, #{ + msg => "mongo_connection_health_check_exception", + worker => Worker, + class => Class, + error => Error + }), false end; _ -> - ?SLOG(warning, #{msg => "mongo_connection_health_check_error", - worker => Worker, - reason => worker_not_found}), + ?SLOG(warning, #{ + msg => "mongo_connection_health_check_error", + worker => Worker, + reason => worker_not_found + }), false end. do_test_query(Conn) -> mongoc:transaction_query( - Conn, - fun(Conf = #{pool := Worker}) -> - Query = mongoc:find_one_query(Conf, <<"foo">>, #{}, #{}, 0), - mc_worker_api:find_one(Worker, Query) - end, - #{}, - ?HEALTH_CHECK_TIMEOUT). + Conn, + fun(Conf = #{pool := Worker}) -> + Query = mongoc:find_one_query(Conf, <<"foo">>, #{}, #{}, 0), + mc_worker_api:find_one(Worker, Query) + end, + #{}, + ?HEALTH_CHECK_TIMEOUT + ). connect(Opts) -> Type = proplists:get_value(mongo_type, Opts, single), @@ -244,10 +299,8 @@ connect(Opts) -> mongo_query(Conn, find, Collection, Selector, Projector) -> mongo_api:find(Conn, Collection, Selector, Projector); - mongo_query(Conn, find_one, Collection, Selector, Projector) -> mongo_api:find_one(Conn, Collection, Selector, Projector); - %% Todo xxx mongo_query(_Conn, _Action, _Collection, _Selector, _Projector) -> ok. @@ -298,7 +351,8 @@ init_worker_options([{r_mode, V} | R], Acc) -> init_worker_options(R, [{r_mode, V} | Acc]); init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); -init_worker_options([], Acc) -> Acc. +init_worker_options([], Acc) -> + Acc. %% =================================================================== %% Schema funcs @@ -356,59 +410,76 @@ may_parse_srv_and_txt_records(#{server := Server} = Config) -> may_parse_srv_and_txt_records(Config) -> may_parse_srv_and_txt_records_(Config). -may_parse_srv_and_txt_records_(#{mongo_type := Type, - srv_record := false, - servers := Servers} = Config) -> +may_parse_srv_and_txt_records_( + #{ + mongo_type := Type, + srv_record := false, + servers := Servers + } = Config +) -> case Type =:= rs andalso maps:is_key(replica_set_name, Config) =:= false of true -> error({missing_parameter, replica_set_name}); false -> Config#{hosts => servers_to_bin(Servers)} end; -may_parse_srv_and_txt_records_(#{mongo_type := Type, - srv_record := true, - servers := Servers} = Config) -> +may_parse_srv_and_txt_records_( + #{ + mongo_type := Type, + srv_record := true, + servers := Servers + } = Config +) -> Hosts = parse_srv_records(Type, Servers), ExtraOpts = parse_txt_records(Type, Servers), maps:merge(Config#{hosts => Hosts}, ExtraOpts). parse_srv_records(Type, Servers) -> Fun = fun(AccIn, {IpOrHost, _Port}) -> - case inet_res:lookup("_mongodb._tcp." - ++ ip_or_host_to_string(IpOrHost), in, srv) of - [] -> - error(service_not_found); - Services -> - [ [server_to_bin({Host, Port}) || {_, _, Port, Host} <- Services] - | AccIn] - end - end, + case + inet_res:lookup( + "_mongodb._tcp." ++ + ip_or_host_to_string(IpOrHost), + in, + srv + ) + of + [] -> + error(service_not_found); + Services -> + [ + [server_to_bin({Host, Port}) || {_, _, Port, Host} <- Services] + | AccIn + ] + end + end, Res = lists:foldl(Fun, [], Servers), case Type of single -> lists:nth(1, Res); - _ -> Res + _ -> Res end. parse_txt_records(Type, Servers) -> - Fields = case Type of - rs -> ["authSource", "replicaSet"]; - _ -> ["authSource"] - end, + Fields = + case Type of + rs -> ["authSource", "replicaSet"]; + _ -> ["authSource"] + end, Fun = fun(AccIn, {IpOrHost, _Port}) -> - case inet_res:lookup(IpOrHost, in, txt) of - [] -> - #{}; - [[QueryString]] -> - case uri_string:dissect_query(QueryString) of - {error, _, _} -> - error({invalid_txt_record, invalid_query_string}); - Options -> - maps:merge(AccIn, take_and_convert(Fields, Options)) - end; - _ -> - error({invalid_txt_record, multiple_records}) - end - end, + case inet_res:lookup(IpOrHost, in, txt) of + [] -> + #{}; + [[QueryString]] -> + case uri_string:dissect_query(QueryString) of + {error, _, _} -> + error({invalid_txt_record, invalid_query_string}); + Options -> + maps:merge(AccIn, take_and_convert(Fields, Options)) + end; + _ -> + error({invalid_txt_record, multiple_records}) + end + end, lists:foldl(Fun, #{}, Servers). take_and_convert(Fields, Options) -> @@ -430,8 +501,8 @@ take_and_convert([Field | More], Options, Acc) -> take_and_convert(More, Options, Acc) end. --spec ip_or_host_to_string(binary() | string() | tuple()) - -> string(). +-spec ip_or_host_to_string(binary() | string() | tuple()) -> + string(). ip_or_host_to_string(Ip) when is_tuple(Ip) -> inet:ntoa(Ip); ip_or_host_to_string(Host) -> @@ -448,18 +519,20 @@ server_to_bin({IpOrHost, Port}) -> %% =================================================================== %% typereflt funcs --spec to_server_raw(string()) - -> {string(), pos_integer()}. +-spec to_server_raw(string()) -> + {string(), pos_integer()}. to_server_raw(Server) -> emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS). --spec to_servers_raw(string()) - -> [{string(), pos_integer()}]. +-spec to_servers_raw(string()) -> + [{string(), pos_integer()}]. to_servers_raw(Servers) -> - lists:map( fun(Server) -> - emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS) - end - , string:tokens(str(Servers), ", ")). + lists:map( + fun(Server) -> + emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS) + end, + string:tokens(str(Servers), ", ") + ). str(A) when is_atom(A) -> atom_to_list(A); diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index d59cdf239..5f228027f 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -23,28 +23,32 @@ -behaviour(emqx_resource). %% API and callbacks for supervisor --export([ start_link/0 - , init/1 - , create_bridge/1 - , drop_bridge/1 - , bridges/0 - ]). +-export([ + start_link/0, + init/1, + create_bridge/1, + drop_bridge/1, + bridges/0 +]). -export([on_message_received/3]). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). -behaviour(hocon_schema). -import(hoconsc, [mk/2]). --export([ roots/0 - , fields/1]). +-export([ + roots/0, + fields/1 +]). %%===================================================================== %% Hocon schema @@ -53,25 +57,34 @@ roots() -> fields("config") -> emqx_connector_mqtt_schema:fields("config"); - fields("get") -> - [ {num_of_bridges, mk(integer(), - #{ desc => ?DESC("num_of_bridges") - })} + [ + {num_of_bridges, + mk( + integer(), + #{desc => ?DESC("num_of_bridges")} + )} ] ++ fields("post"); - fields("put") -> emqx_connector_mqtt_schema:fields("connector"); - fields("post") -> - [ {type, mk(mqtt, - #{ required => true - , desc => ?DESC("type") - })} - , {name, mk(binary(), - #{ required => true - , desc => ?DESC("name") - })} + [ + {type, + mk( + mqtt, + #{ + required => true, + desc => ?DESC("type") + } + )}, + {name, + mk( + binary(), + #{ + required => true, + desc => ?DESC("name") + } + )} ] ++ fields("put"). %% =================================================================== @@ -80,23 +93,29 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - SupFlag = #{strategy => one_for_one, - intensity => 100, - period => 10}, + SupFlag = #{ + strategy => one_for_one, + intensity => 100, + period => 10 + }, {ok, {SupFlag, []}}. bridge_spec(Config) -> - #{id => maps:get(name, Config), - start => {emqx_connector_mqtt_worker, start_link, [Config]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_connector_mqtt_worker]}. + #{ + id => maps:get(name, Config), + start => {emqx_connector_mqtt_worker, start_link, [Config]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_connector_mqtt_worker] + }. --spec(bridges() -> [{node(), map()}]). +-spec bridges() -> [{node(), map()}]. bridges() -> - [{Name, emqx_connector_mqtt_worker:status(Name)} - || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. + [ + {Name, emqx_connector_mqtt_worker:status(Name)} + || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE) + ]. create_bridge(Config) -> supervisor:start_child(?MODULE, bridge_spec(Config)). @@ -121,8 +140,11 @@ on_message_received(Msg, HookPoint, InstId) -> %% =================================================================== on_start(InstId, Conf) -> InstanceId = binary_to_atom(InstId, utf8), - ?SLOG(info, #{msg => "starting_mqtt_connector", - connector => InstanceId, config => Conf}), + ?SLOG(info, #{ + msg => "starting_mqtt_connector", + connector => InstanceId, + config => Conf + }), BasicConf = basic_config(Conf), BridgeConf = BasicConf#{ name => InstanceId, @@ -142,19 +164,25 @@ on_start(InstId, Conf) -> end. on_stop(_InstId, #{name := InstanceId}) -> - ?SLOG(info, #{msg => "stopping_mqtt_connector", - connector => InstanceId}), + ?SLOG(info, #{ + msg => "stopping_mqtt_connector", + connector => InstanceId + }), case ?MODULE:drop_bridge(InstanceId) of - ok -> ok; - {error, not_found} -> ok; + ok -> + ok; + {error, not_found} -> + ok; {error, Reason} -> - ?SLOG(error, #{msg => "stop_mqtt_connector", - connector => InstanceId, reason => Reason}) + ?SLOG(error, #{ + msg => "stop_mqtt_connector", + connector => InstanceId, + reason => Reason + }) end. on_query(_InstId, {message_received, _Msg}, AfterQuery, _State) -> emqx_resource:query_success(AfterQuery); - on_query(_InstId, {send_message, Msg}, AfterQuery, #{name := InstanceId}) -> ?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg), @@ -178,7 +206,8 @@ make_sub_confs(undefined, _) -> undefined; make_sub_confs(SubRemoteConf, InstId) -> case maps:take(hookpoint, SubRemoteConf) of - error -> SubRemoteConf; + error -> + SubRemoteConf; {HookPoint, SubConf} -> MFA = {?MODULE, on_message_received, [HookPoint, InstId]}, SubConf#{on_message_received => MFA} @@ -192,22 +221,24 @@ make_forward_confs(FrowardConf) -> FrowardConf. basic_config(#{ - server := Server, - reconnect_interval := ReconnIntv, - proto_ver := ProtoVer, - username := User, - password := Password, - clean_start := CleanStart, - keepalive := KeepAlive, - retry_interval := RetryIntv, - max_inflight := MaxInflight, - replayq := ReplayQ, - ssl := #{enable := EnableSsl} = Ssl}) -> + server := Server, + reconnect_interval := ReconnIntv, + proto_ver := ProtoVer, + username := User, + password := Password, + clean_start := CleanStart, + keepalive := KeepAlive, + retry_interval := RetryIntv, + max_inflight := MaxInflight, + replayq := ReplayQ, + ssl := #{enable := EnableSsl} = Ssl +}) -> #{ replayq => ReplayQ, %% connection opts server => Server, - connect_timeout => 30, %% 30s + %% 30s + connect_timeout => 30, reconnect_interval => ReconnIntv, proto_ver => ProtoVer, bridge_mode => true, diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 7a5ff9130..5e42a2ee2 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -23,11 +23,12 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). %% ecpool connect & reconnect -export([connect/1, prepare_sql_to_conn/2]). @@ -38,9 +39,10 @@ -export([do_health_check/1]). --define( MYSQL_HOST_OPTIONS - , #{ host_type => inet_addr - , default_port => ?MYSQL_DEFAULT_PORT}). +-define(MYSQL_HOST_OPTIONS, #{ + host_type => inet_addr, + default_port => ?MYSQL_DEFAULT_PORT +}). %%===================================================================== %% Hocon schema @@ -48,11 +50,10 @@ roots() -> [{config, #{type => hoconsc:ref(?MODULE, config)}}]. fields(config) -> - [ {server, fun server/1} - ] ++ - emqx_connector_schema_lib:relational_db_fields() ++ - emqx_connector_schema_lib:ssl_fields() ++ - emqx_connector_schema_lib:prepare_statement_fields(). + [{server, fun server/1}] ++ + emqx_connector_schema_lib:relational_db_fields() ++ + emqx_connector_schema_lib:ssl_fields() ++ + emqx_connector_schema_lib:prepare_statement_fields(). server(type) -> emqx_schema:ip_port(); server(required) -> true; @@ -62,47 +63,64 @@ server(desc) -> ?DESC("server"); server(_) -> undefined. %% =================================================================== -on_start(InstId, #{server := {Host, Port}, - database := DB, - username := User, - password := Password, - auto_reconnect := AutoReconn, - pool_size := PoolSize, - ssl := SSL } = Config) -> - ?SLOG(info, #{msg => "starting_mysql_connector", - connector => InstId, config => Config}), - SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, emqx_tls_lib:to_client_opts(SSL)}]; - false -> - [] - end, - Options = [{host, Host}, - {port, Port}, - {user, User}, - {password, Password}, - {database, DB}, - {auto_reconnect, reconn_interval(AutoReconn)}, - {pool_size, PoolSize}], +on_start( + InstId, + #{ + server := {Host, Port}, + database := DB, + username := User, + password := Password, + auto_reconnect := AutoReconn, + pool_size := PoolSize, + ssl := SSL + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_mysql_connector", + connector => InstId, + config => Config + }), + SslOpts = + case maps:get(enable, SSL) of + true -> + [{ssl, emqx_tls_lib:to_client_opts(SSL)}]; + false -> + [] + end, + Options = [ + {host, Host}, + {port, Port}, + {user, User}, + {password, Password}, + {database, DB}, + {auto_reconnect, reconn_interval(AutoReconn)}, + {pool_size, PoolSize} + ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), Prepares = maps:get(prepare_statement, Config, #{}), State = init_prepare(#{poolname => PoolName, prepare_statement => Prepares}), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of - ok -> {ok, State}; + ok -> {ok, State}; {error, Reason} -> {error, Reason} end. on_stop(InstId, #{poolname := PoolName}) -> - ?SLOG(info, #{msg => "stopping_mysql_connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping_mysql_connector", + connector => InstId + }), emqx_plugin_libs_pool:stop_pool(PoolName). on_query(InstId, {Type, SQLOrKey}, AfterQuery, State) -> on_query(InstId, {Type, SQLOrKey, [], default_timeout}, AfterQuery, State); on_query(InstId, {Type, SQLOrKey, Params}, AfterQuery, State) -> on_query(InstId, {Type, SQLOrKey, Params, default_timeout}, AfterQuery, State); -on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, - #{poolname := PoolName, prepare_statement := Prepares} = State) -> +on_query( + InstId, + {Type, SQLOrKey, Params, Timeout}, + AfterQuery, + #{poolname := PoolName, prepare_statement := Prepares} = State +) -> LogMeta = #{connector => InstId, sql => SQLOrKey, state => State}, ?TRACE("QUERY", "mysql_connector_received", LogMeta), Worker = ecpool:get_client(PoolName), @@ -111,28 +129,36 @@ on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, Result = erlang:apply(mysql, MySqlFunction, [Conn, SQLOrKey, Params, Timeout]), case Result of {error, disconnected} -> - ?SLOG(error, - LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected}), + ?SLOG( + error, + LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected} + ), %% kill the poll worker to trigger reconnection _ = exit(Conn, restart), emqx_resource:query_failed(AfterQuery), Result; {error, not_prepared} -> - ?SLOG(warning, - LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared}), + ?SLOG( + warning, + LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared} + ), case prepare_sql(Prepares, PoolName) of ok -> %% not return result, next loop will try again on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, State); {error, Reason} -> - ?SLOG(error, - LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason}), + ?SLOG( + error, + LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason} + ), emqx_resource:query_failed(AfterQuery), {error, Reason} end; {error, Reason} -> - ?SLOG(error, - LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}), + ?SLOG( + error, + LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason} + ), emqx_resource:query_failed(AfterQuery), Result; _ -> @@ -147,7 +173,7 @@ on_health_check(_InstId, #{poolname := PoolName} = State) -> case emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, State) of {ok, State} -> case do_health_check_prepares(State) of - ok-> + ok -> {ok, State}; {ok, NState} -> {ok, NState}; @@ -161,7 +187,7 @@ on_health_check(_InstId, #{poolname := PoolName} = State) -> do_health_check(Conn) -> ok == element(1, mysql:query(Conn, <<"SELECT count(1) AS T">>)). -do_health_check_prepares(#{prepare_statement := Prepares})when is_map(Prepares) -> +do_health_check_prepares(#{prepare_statement := Prepares}) when is_map(Prepares) -> ok; do_health_check_prepares(State = #{poolname := PoolName, prepare_statement := {error, Prepares}}) -> %% retry to prepare @@ -180,8 +206,8 @@ reconn_interval(false) -> false. connect(Options) -> mysql:start_link(Options). --spec to_server(string()) - -> {inet:ip_address() | inet:hostname(), pos_integer()}. +-spec to_server(string()) -> + {inet:ip_address() | inet:hostname(), pos_integer()}. to_server(Str) -> emqx_connector_schema_lib:parse_server(Str, ?MYSQL_HOST_OPTIONS). @@ -215,20 +241,27 @@ prepare_sql(Prepares, PoolName) -> do_prepare_sql(Prepares, PoolName) -> Conns = - [begin - {ok, Conn} = ecpool_worker:client(Worker), - Conn - end || {_Name, Worker} <- ecpool:workers(PoolName)], + [ + begin + {ok, Conn} = ecpool_worker:client(Worker), + Conn + end + || {_Name, Worker} <- ecpool:workers(PoolName) + ], prepare_sql_to_conn_list(Conns, Prepares). -prepare_sql_to_conn_list([], _PrepareList) -> ok; +prepare_sql_to_conn_list([], _PrepareList) -> + ok; prepare_sql_to_conn_list([Conn | ConnList], PrepareList) -> case prepare_sql_to_conn(Conn, PrepareList) of ok -> prepare_sql_to_conn_list(ConnList, PrepareList); {error, R} -> %% rollback - Fun = fun({Key, _}) -> _ = unprepare_sql_to_conn(Conn, Key), ok end, + Fun = fun({Key, _}) -> + _ = unprepare_sql_to_conn(Conn, Key), + ok + end, lists:foreach(Fun, PrepareList), {error, R} end. diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index d7b6c1b14..00ec31849 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -26,24 +26,26 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). -export([connect/1]). --export([ query/3 - , prepared_query/3 - ]). +-export([ + query/3, + prepared_query/3 +]). -export([do_health_check/1]). --define( PGSQL_HOST_OPTIONS - , #{ host_type => inet_addr - , default_port => ?PGSQL_DEFAULT_PORT}). - +-define(PGSQL_HOST_OPTIONS, #{ + host_type => inet_addr, + default_port => ?PGSQL_DEFAULT_PORT +}). %%===================================================================== @@ -52,9 +54,9 @@ roots() -> fields(config) -> [{server, fun server/1}] ++ - emqx_connector_schema_lib:relational_db_fields() ++ - emqx_connector_schema_lib:ssl_fields() ++ - emqx_connector_schema_lib:prepare_statement_fields(). + emqx_connector_schema_lib:relational_db_fields() ++ + emqx_connector_schema_lib:ssl_fields() ++ + emqx_connector_schema_lib:prepare_statement_fields(). server(type) -> emqx_schema:ip_port(); server(required) -> true; @@ -64,52 +66,73 @@ server(desc) -> ?DESC("server"); server(_) -> undefined. %% =================================================================== -on_start(InstId, #{server := {Host, Port}, - database := DB, - username := User, - password := Password, - auto_reconnect := AutoReconn, - pool_size := PoolSize, - ssl := SSL} = Config) -> - ?SLOG(info, #{msg => "starting_postgresql_connector", - connector => InstId, config => Config}), - SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, true}, - {ssl_opts, emqx_tls_lib:to_client_opts(SSL)}]; - false -> - [{ssl, false}] - end, - Options = [{host, Host}, - {port, Port}, - {username, User}, - {password, Password}, - {database, DB}, - {auto_reconnect, reconn_interval(AutoReconn)}, - {pool_size, PoolSize}, - {prepare_statement, maps:to_list(maps:get(prepare_statement, Config, #{}))}], +on_start( + InstId, + #{ + server := {Host, Port}, + database := DB, + username := User, + password := Password, + auto_reconnect := AutoReconn, + pool_size := PoolSize, + ssl := SSL + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_postgresql_connector", + connector => InstId, + config => Config + }), + SslOpts = + case maps:get(enable, SSL) of + true -> + [ + {ssl, true}, + {ssl_opts, emqx_tls_lib:to_client_opts(SSL)} + ]; + false -> + [{ssl, false}] + end, + Options = [ + {host, Host}, + {port, Port}, + {username, User}, + {password, Password}, + {database, DB}, + {auto_reconnect, reconn_interval(AutoReconn)}, + {pool_size, PoolSize}, + {prepare_statement, maps:to_list(maps:get(prepare_statement, Config, #{}))} + ], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of - ok -> {ok, #{poolname => PoolName}}; + ok -> {ok, #{poolname => PoolName}}; {error, Reason} -> {error, Reason} end. on_stop(InstId, #{poolname := PoolName}) -> - ?SLOG(info, #{msg => "stopping postgresql connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping postgresql connector", + connector => InstId + }), emqx_plugin_libs_pool:stop_pool(PoolName). on_query(InstId, {Type, NameOrSQL}, AfterQuery, #{poolname := _PoolName} = State) -> on_query(InstId, {Type, NameOrSQL, []}, AfterQuery, State); - on_query(InstId, {Type, NameOrSQL, Params}, AfterQuery, #{poolname := PoolName} = State) -> - ?SLOG(debug, #{msg => "postgresql connector received sql query", - connector => InstId, sql => NameOrSQL, state => State}), + ?SLOG(debug, #{ + msg => "postgresql connector received sql query", + connector => InstId, + sql => NameOrSQL, + state => State + }), case Result = ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Params]}, no_handover) of {error, Reason} -> ?SLOG(error, #{ msg => "postgresql connector do sql query failed", - connector => InstId, sql => NameOrSQL, reason => Reason}), + connector => InstId, + sql => NameOrSQL, + reason => Reason + }), emqx_resource:query_failed(AfterQuery); _ -> emqx_resource:query_success(AfterQuery) @@ -127,7 +150,7 @@ reconn_interval(true) -> 15; reconn_interval(false) -> false. connect(Opts) -> - Host = proplists:get_value(host, Opts), + Host = proplists:get_value(host, Opts), Username = proplists:get_value(username, Opts), Password = proplists:get_value(password, Opts), PrepareStatement = proplists:get_value(prepare_statement, Opts), @@ -177,7 +200,7 @@ conn_opts([_Opt | Opts], Acc) -> %% =================================================================== %% typereflt funcs --spec to_server(string()) - -> {inet:ip_address() | inet:hostname(), pos_integer()}. +-spec to_server(string()) -> + {inet:ip_address() | inet:hostname(), pos_integer()}. to_server(Str) -> emqx_connector_schema_lib:parse_server(Str, ?PGSQL_HOST_OPTIONS). diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 78607aac0..189d5e8c2 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -25,11 +25,12 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). -export([do_health_check/1]). @@ -38,50 +39,59 @@ -export([cmd/3]). %% redis host don't need parse --define( REDIS_HOST_OPTIONS - , #{ host_type => hostname - , default_port => ?REDIS_DEFAULT_PORT}). - +-define(REDIS_HOST_OPTIONS, #{ + host_type => hostname, + default_port => ?REDIS_DEFAULT_PORT +}). %%===================================================================== roots() -> - [ {config, #{type => hoconsc:union( - [ hoconsc:ref(?MODULE, cluster) - , hoconsc:ref(?MODULE, single) - , hoconsc:ref(?MODULE, sentinel) - ])} - } + [ + {config, #{ + type => hoconsc:union( + [ + hoconsc:ref(?MODULE, cluster), + hoconsc:ref(?MODULE, single), + hoconsc:ref(?MODULE, sentinel) + ] + ) + }} ]. fields(single) -> - [ {server, fun server/1} - , {redis_type, #{type => hoconsc:enum([single]), - required => true, - desc => ?DESC("single") - }} + [ + {server, fun server/1}, + {redis_type, #{ + type => hoconsc:enum([single]), + required => true, + desc => ?DESC("single") + }} ] ++ - redis_fields() ++ - emqx_connector_schema_lib:ssl_fields(); + redis_fields() ++ + emqx_connector_schema_lib:ssl_fields(); fields(cluster) -> - [ {servers, fun servers/1} - , {redis_type, #{type => hoconsc:enum([cluster]), - required => true, - desc => ?DESC("cluster") - }} + [ + {servers, fun servers/1}, + {redis_type, #{ + type => hoconsc:enum([cluster]), + required => true, + desc => ?DESC("cluster") + }} ] ++ - redis_fields() ++ - emqx_connector_schema_lib:ssl_fields(); + redis_fields() ++ + emqx_connector_schema_lib:ssl_fields(); fields(sentinel) -> - [ {servers, fun servers/1} - , {redis_type, #{type => hoconsc:enum([sentinel]), - required => true, - desc => ?DESC("sentinel") - }} - , {sentinel, #{type => string(), desc => ?DESC("sentinel_desc") - }} + [ + {servers, fun servers/1}, + {redis_type, #{ + type => hoconsc:enum([sentinel]), + required => true, + desc => ?DESC("sentinel") + }}, + {sentinel, #{type => string(), desc => ?DESC("sentinel_desc")}} ] ++ - redis_fields() ++ - emqx_connector_schema_lib:ssl_fields(). + redis_fields() ++ + emqx_connector_schema_lib:ssl_fields(). server(type) -> emqx_schema:ip_port(); server(required) -> true; @@ -98,62 +108,89 @@ servers(desc) -> ?DESC("servers"); servers(_) -> undefined. %% =================================================================== -on_start(InstId, #{redis_type := Type, - database := Database, - pool_size := PoolSize, - auto_reconnect := AutoReconn, - ssl := SSL } = Config) -> - ?SLOG(info, #{msg => "starting_redis_connector", - connector => InstId, config => Config}), - Servers = case Type of - single -> [{servers, [maps:get(server, Config)]}]; - _ ->[{servers, maps:get(servers, Config)}] - end, - Opts = [{pool_size, PoolSize}, +on_start( + InstId, + #{ + redis_type := Type, + database := Database, + pool_size := PoolSize, + auto_reconnect := AutoReconn, + ssl := SSL + } = Config +) -> + ?SLOG(info, #{ + msg => "starting_redis_connector", + connector => InstId, + config => Config + }), + Servers = + case Type of + single -> [{servers, [maps:get(server, Config)]}]; + _ -> [{servers, maps:get(servers, Config)}] + end, + Opts = + [ + {pool_size, PoolSize}, {database, Database}, {password, maps:get(password, Config, "")}, {auto_reconnect, reconn_interval(AutoReconn)} - ] ++ Servers, - Options = case maps:get(enable, SSL) of - true -> - [{ssl, true}, - {ssl_options, emqx_tls_lib:to_client_opts(SSL)}]; - false -> [{ssl, false}] - end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], + ] ++ Servers, + Options = + case maps:get(enable, SSL) of + true -> + [ + {ssl, true}, + {ssl_options, emqx_tls_lib:to_client_opts(SSL)} + ]; + false -> + [{ssl, false}] + end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), case Type of cluster -> case eredis_cluster:start_pool(PoolName, Opts ++ [{options, Options}]) of - {ok, _} -> {ok, #{poolname => PoolName, type => Type}}; - {ok, _, _} -> {ok, #{poolname => PoolName, type => Type}}; + {ok, _} -> {ok, #{poolname => PoolName, type => Type}}; + {ok, _, _} -> {ok, #{poolname => PoolName, type => Type}}; {error, Reason} -> {error, Reason} end; _ -> - case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ [{options, Options}]) of - ok -> {ok, #{poolname => PoolName, type => Type}}; + case + emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ [{options, Options}]) + of + ok -> {ok, #{poolname => PoolName, type => Type}}; {error, Reason} -> {error, Reason} end end. on_stop(InstId, #{poolname := PoolName, type := Type}) -> - ?SLOG(info, #{msg => "stopping_redis_connector", - connector => InstId}), + ?SLOG(info, #{ + msg => "stopping_redis_connector", + connector => InstId + }), case Type of cluster -> eredis_cluster:stop_pool(PoolName); _ -> emqx_plugin_libs_pool:stop_pool(PoolName) end. on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) -> - ?TRACE("QUERY", "redis_connector_received", - #{connector => InstId, sql => Command, state => State}), - Result = case Type of - cluster -> eredis_cluster:q(PoolName, Command); - _ -> ecpool:pick_and_do(PoolName, {?MODULE, cmd, [Type, Command]}, no_handover) - end, + ?TRACE( + "QUERY", + "redis_connector_received", + #{connector => InstId, sql => Command, state => State} + ), + Result = + case Type of + cluster -> eredis_cluster:q(PoolName, Command); + _ -> ecpool:pick_and_do(PoolName, {?MODULE, cmd, [Type, Command]}, no_handover) + end, case Result of {error, Reason} -> - ?SLOG(error, #{msg => "redis_connector_do_cmd_query_failed", - connector => InstId, sql => Command, reason => Reason}), + ?SLOG(error, #{ + msg => "redis_connector_do_cmd_query_failed", + connector => InstId, + sql => Command, + reason => Reason + }), emqx_resource:query_failed(AfterCommand); _ -> emqx_resource:query_success(AfterCommand) @@ -161,14 +198,19 @@ on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := T Result. extract_eredis_cluster_workers(PoolName) -> - lists:flatten([gen_server:call(PoolPid, get_all_workers) || - PoolPid <- eredis_cluster_monitor:get_all_pools(PoolName)]). + lists:flatten([ + gen_server:call(PoolPid, get_all_workers) + || PoolPid <- eredis_cluster_monitor:get_all_pools(PoolName) + ]). eredis_cluster_workers_exist_and_are_connected(Workers) -> - length(Workers) > 0 andalso lists:all( - fun({_, Pid, _, _}) -> - eredis_cluster_pool_worker:is_connected(Pid) =:= true - end, Workers). + length(Workers) > 0 andalso + lists:all( + fun({_, Pid, _, _}) -> + eredis_cluster_pool_worker:is_connected(Pid) =:= true + end, + Workers + ). on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) -> case eredis_cluster:pool_exists(PoolName) of @@ -178,12 +220,9 @@ on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) -> true -> {ok, State}; false -> {error, health_check_failed, State} end; - false -> {error, health_check_failed, State} end; - - on_health_check(_InstId, #{poolname := PoolName} = State) -> emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, State). @@ -206,28 +245,32 @@ connect(Opts) -> eredis:start_link(Opts). redis_fields() -> - [ {pool_size, fun emqx_connector_schema_lib:pool_size/1} - , {password, fun emqx_connector_schema_lib:password/1} - , {database, #{type => integer(), - default => 0, - required => true, - desc => ?DESC("database") - }} - , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} + [ + {pool_size, fun emqx_connector_schema_lib:pool_size/1}, + {password, fun emqx_connector_schema_lib:password/1}, + {database, #{ + type => integer(), + default => 0, + required => true, + desc => ?DESC("database") + }}, + {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} ]. --spec to_server_raw(string()) - -> {string(), pos_integer()}. +-spec to_server_raw(string()) -> + {string(), pos_integer()}. to_server_raw(Server) -> emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS). --spec to_servers_raw(string()) - -> [{string(), pos_integer()}]. +-spec to_servers_raw(string()) -> + [{string(), pos_integer()}]. to_servers_raw(Servers) -> - lists:map( fun(Server) -> - emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS) - end - , string:tokens(str(Servers), ", ")). + lists:map( + fun(Server) -> + emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS) + end, + string:tokens(str(Servers), ", ") + ). str(A) when is_atom(A) -> atom_to_list(A); diff --git a/apps/emqx_connector/src/emqx_connector_schema.erl b/apps/emqx_connector/src/emqx_connector_schema.erl index 2b1d026b1..27f982e74 100644 --- a/apps/emqx_connector/src/emqx_connector_schema.erl +++ b/apps/emqx_connector/src/emqx_connector_schema.erl @@ -24,10 +24,11 @@ -export([namespace/0, roots/0, fields/1, desc/1]). --export([ get_response/0 - , put_request/0 - , post_request/0 - ]). +-export([ + get_response/0, + put_request/0, + post_request/0 +]). %% the config for http bridges do not need connectors -define(CONN_TYPES, [mqtt]). @@ -55,18 +56,25 @@ namespace() -> connector. roots() -> ["connectors"]. -fields(connectors) -> fields("connectors"); +fields(connectors) -> + fields("connectors"); fields("connectors") -> - [ {mqtt, - mk(hoconsc:map(name, - hoconsc:union([ ref(emqx_connector_mqtt_schema, "connector") - ])), - #{ desc => ?DESC("mqtt") - })} + [ + {mqtt, + mk( + hoconsc:map( + name, + hoconsc:union([ref(emqx_connector_mqtt_schema, "connector")]) + ), + #{desc => ?DESC("mqtt")} + )} ]. -desc(Record) when Record =:= connectors; - Record =:= "connectors" -> ?DESC("desc_connector"); +desc(Record) when + Record =:= connectors; + Record =:= "connectors" +-> + ?DESC("desc_connector"); desc(_) -> undefined. diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 8600253c6..86b12dcf3 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -19,32 +19,36 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("hocon/include/hoconsc.hrl"). --export([ relational_db_fields/0 - , ssl_fields/0 - , prepare_statement_fields/0 - ]). +-export([ + relational_db_fields/0, + ssl_fields/0, + prepare_statement_fields/0 +]). --export([ ip_port_to_string/1 - , parse_server/2 - ]). +-export([ + ip_port_to_string/1, + parse_server/2 +]). --export([ pool_size/1 - , database/1 - , username/1 - , password/1 - , auto_reconnect/1 - ]). +-export([ + pool_size/1, + database/1, + username/1, + password/1, + auto_reconnect/1 +]). -type database() :: binary(). -type pool_size() :: pos_integer(). -type username() :: binary(). -type password() :: binary(). --reflect_type([ database/0 - , pool_size/0 - , username/0 - , password/0 - ]). +-reflect_type([ + database/0, + pool_size/0, + username/0, + password/0 +]). -export([roots/0, fields/1]). @@ -53,24 +57,25 @@ roots() -> []. fields(_) -> []. ssl_fields() -> - [ {ssl, #{type => hoconsc:ref(emqx_schema, "ssl_client_opts"), - default => #{<<"enable">> => false}, - desc => ?DESC("ssl") - } - } + [ + {ssl, #{ + type => hoconsc:ref(emqx_schema, "ssl_client_opts"), + default => #{<<"enable">> => false}, + desc => ?DESC("ssl") + }} ]. relational_db_fields() -> - [ {database, fun database/1} - , {pool_size, fun pool_size/1} - , {username, fun username/1} - , {password, fun password/1} - , {auto_reconnect, fun auto_reconnect/1} + [ + {database, fun database/1}, + {pool_size, fun pool_size/1}, + {username, fun username/1}, + {password, fun password/1}, + {auto_reconnect, fun auto_reconnect/1} ]. prepare_statement_fields() -> - [ {prepare_statement, fun prepare_statement/1} - ]. + [{prepare_statement, fun prepare_statement/1}]. prepare_statement(type) -> map(); prepare_statement(desc) -> ?DESC("prepare_statement"); @@ -113,16 +118,16 @@ parse_server(Str, #{host_type := inet_addr, default_port := DefaultPort}) -> try string:tokens(str(Str), ": ") of [Ip, Port] -> case parse_ip(Ip) of - {ok, R} -> {R, list_to_integer(Port)} + {ok, R} -> {R, list_to_integer(Port)} end; [Ip] -> case parse_ip(Ip) of - {ok, R} -> {R, DefaultPort} + {ok, R} -> {R, DefaultPort} end; _ -> ?THROW_ERROR("Bad server schema.") catch - error : Reason -> + error:Reason -> ?THROW_ERROR(Reason) end; parse_server(Str, #{host_type := hostname, default_port := DefaultPort}) -> @@ -134,7 +139,7 @@ parse_server(Str, #{host_type := hostname, default_port := DefaultPort}) -> _ -> ?THROW_ERROR("Bad server schema.") catch - error : Reason -> + error:Reason -> ?THROW_ERROR(Reason) end; parse_server(_, _) -> diff --git a/apps/emqx_connector/src/emqx_connector_ssl.erl b/apps/emqx_connector/src/emqx_connector_ssl.erl index 02d9a4070..131b6cbd8 100644 --- a/apps/emqx_connector/src/emqx_connector_ssl.erl +++ b/apps/emqx_connector/src/emqx_connector_ssl.erl @@ -1,4 +1,3 @@ - %%-------------------------------------------------------------------- %% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved. %% @@ -17,9 +16,10 @@ -module(emqx_connector_ssl). --export([ convert_certs/2 - , clear_certs/2 - ]). +-export([ + convert_certs/2, + clear_certs/2 +]). convert_certs(RltvDir, NewConfig) -> NewSSL = drop_invalid_certs(maps:get(<<"ssl">>, NewConfig, undefined)), @@ -40,7 +40,8 @@ new_ssl_config(Config, SSL) -> Config#{<<"ssl">> => SSL}. drop_invalid_certs(undefined) -> undefined; drop_invalid_certs(SSL) -> emqx_tls_lib:drop_invalid_certs(SSL). -map_get_oneof([], _Map, Default) -> Default; +map_get_oneof([], _Map, Default) -> + Default; map_get_oneof([Key | Keys], Map, Default) -> case maps:find(Key, Map) of error -> diff --git a/apps/emqx_connector/src/emqx_connector_sup.erl b/apps/emqx_connector/src/emqx_connector_sup.erl index 0824a68bf..29fc5b4b5 100644 --- a/apps/emqx_connector/src/emqx_connector_sup.erl +++ b/apps/emqx_connector/src/emqx_connector_sup.erl @@ -27,20 +27,24 @@ start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). init([]) -> - SupFlags = #{strategy => one_for_all, - intensity => 5, - period => 20}, + SupFlags = #{ + strategy => one_for_all, + intensity => 5, + period => 20 + }, ChildSpecs = [ child_spec(emqx_connector_mqtt) ], {ok, {SupFlags, ChildSpecs}}. child_spec(Mod) -> - #{id => Mod, - start => {Mod, start_link, []}, - restart => permanent, - shutdown => 3000, - type => supervisor, - modules => [Mod]}. + #{ + id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 3000, + type => supervisor, + modules => [Mod] + }. %% internal functions diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index 1a9c55ced..2f4f61043 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -18,21 +18,24 @@ -module(emqx_connector_mqtt_mod). --export([ start/1 - , send/2 - , stop/1 - , ping/1 - ]). +-export([ + start/1, + send/2, + stop/1, + ping/1 +]). --export([ ensure_subscribed/3 - , ensure_unsubscribed/2 - ]). +-export([ + ensure_subscribed/3, + ensure_unsubscribed/2 +]). %% callbacks for emqtt --export([ handle_puback/2 - , handle_publish/3 - , handle_disconnected/2 - ]). +-export([ + handle_puback/2, + handle_publish/3, + handle_disconnected/2 +]). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -69,7 +72,7 @@ start(Config) -> ok = sub_remote_topics(Pid, Subscriptions), {ok, #{client_pid => Pid, subscriptions => Subscriptions}} catch - throw : Reason -> + throw:Reason -> ok = stop(#{client_pid => Pid}), {error, error_reason(Reason, ServerStr)} end; @@ -90,13 +93,14 @@ stop(#{client_pid := Pid}) -> ping(undefined) -> pang; - ping(#{client_pid := Pid}) -> emqtt:ping(Pid). -ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when is_pid(Pid) -> +ensure_subscribed(#{client_pid := Pid, subscriptions := Subs} = Conn, Topic, QoS) when + is_pid(Pid) +-> case emqtt:subscribe(Pid, Topic, QoS) of - {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS}|Subs]}; + {ok, _, _} -> Conn#{subscriptions => [{Topic, QoS} | Subs]}; Error -> {error, Error} end; ensure_subscribed(_Conn, _Topic, _QoS) -> @@ -120,15 +124,14 @@ safe_stop(Pid, StopF, Timeout) -> try StopF() catch - _ : _ -> + _:_ -> ok end, receive {'DOWN', MRef, _, _, _} -> ok - after - Timeout -> - exit(Pid, kill) + after Timeout -> + exit(Pid, kill) end. send(Conn, Msgs) -> @@ -157,26 +160,38 @@ send(#{client_pid := ClientPid} = Conn, [Msg | Rest], PktIds) -> {error, Reason} end. -handle_puback(#{packet_id := PktId, reason_code := RC}, Parent) - when RC =:= ?RC_SUCCESS; - RC =:= ?RC_NO_MATCHING_SUBSCRIBERS -> - Parent ! {batch_ack, PktId}, ok; +handle_puback(#{packet_id := PktId, reason_code := RC}, Parent) when + RC =:= ?RC_SUCCESS; + RC =:= ?RC_NO_MATCHING_SUBSCRIBERS +-> + Parent ! {batch_ack, PktId}, + ok; handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) -> - ?SLOG(warning, #{msg => "publish_to_remote_node_falied", - packet_id => PktId, reason_code => RC}). + ?SLOG(warning, #{ + msg => "publish_to_remote_node_falied", + packet_id => PktId, + reason_code => RC + }). handle_publish(Msg, undefined, _Opts) -> - ?SLOG(error, #{msg => "cannot_publish_to_local_broker_as" - "_'ingress'_is_not_configured", - message => Msg}); + ?SLOG(error, #{ + msg => + "cannot_publish_to_local_broker_as" + "_'ingress'_is_not_configured", + message => Msg + }); handle_publish(#{properties := Props} = Msg0, Vars, Opts) -> Msg = format_msg_received(Msg0, Opts), - ?SLOG(debug, #{msg => "publish_to_local_broker", - message => Msg, vars => Vars}), + ?SLOG(debug, #{ + msg => "publish_to_local_broker", + message => Msg, + vars => Vars + }), case Vars of #{on_message_received := {Mod, Func, Args}} -> _ = erlang:apply(Mod, Func, [Msg | Args]); - _ -> ok + _ -> + ok end, maybe_publish_to_local_broker(Msg, Vars, Props). @@ -184,12 +199,14 @@ handle_disconnected(Reason, Parent) -> Parent ! {disconnected, self(), Reason}. make_hdlr(Parent, Vars, Opts) -> - #{puback => {fun ?MODULE:handle_puback/2, [Parent]}, - publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]}, - disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]} - }. + #{ + puback => {fun ?MODULE:handle_puback/2, [Parent]}, + publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]}, + disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]} + }. -sub_remote_topics(_ClientPid, undefined) -> ok; +sub_remote_topics(_ClientPid, undefined) -> + ok; sub_remote_topics(ClientPid, #{remote_topic := FromTopic, remote_qos := QoS}) -> case emqtt:subscribe(ClientPid, FromTopic, QoS) of {ok, _, _} -> ok; @@ -199,52 +216,82 @@ sub_remote_topics(ClientPid, #{remote_topic := FromTopic, remote_qos := QoS}) -> process_config(Config) -> maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config). -maybe_publish_to_local_broker(#{topic := Topic} = Msg, #{remote_topic := SubTopic} = Vars, - Props) -> +maybe_publish_to_local_broker( + #{topic := Topic} = Msg, + #{remote_topic := SubTopic} = Vars, + Props +) -> case maps:get(local_topic, Vars, undefined) of undefined -> - ok; %% local topic is not set, discard it + %% local topic is not set, discard it + ok; _ -> case emqx_topic:match(Topic, SubTopic) of true -> - _ = emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props)), + _ = emqx_broker:publish( + emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props) + ), ok; false -> - ?SLOG(warning, #{msg => "discard_message_as_topic_not_matched", - message => Msg, subscribed => SubTopic, got_topic => Topic}) + ?SLOG(warning, #{ + msg => "discard_message_as_topic_not_matched", + message => Msg, + subscribed => SubTopic, + got_topic => Topic + }) end end. -format_msg_received(#{dup := Dup, payload := Payload, properties := Props, - qos := QoS, retain := Retain, topic := Topic}, #{server := Server}) -> - #{ id => emqx_guid:to_hexstr(emqx_guid:gen()) - , server => Server - , payload => Payload - , topic => Topic - , qos => QoS - , dup => Dup - , retain => Retain - , pub_props => printable_maps(Props) - , message_received_at => erlang:system_time(millisecond) - }. +format_msg_received( + #{ + dup := Dup, + payload := Payload, + properties := Props, + qos := QoS, + retain := Retain, + topic := Topic + }, + #{server := Server} +) -> + #{ + id => emqx_guid:to_hexstr(emqx_guid:gen()), + server => Server, + payload => Payload, + topic => Topic, + qos => QoS, + dup => Dup, + retain => Retain, + pub_props => printable_maps(Props), + message_received_at => erlang:system_time(millisecond) + }. -printable_maps(undefined) -> #{}; +printable_maps(undefined) -> + #{}; printable_maps(Headers) -> maps:fold( - fun ('User-Property', V0, AccIn) when is_list(V0) -> + fun + ('User-Property', V0, AccIn) when is_list(V0) -> AccIn#{ 'User-Property' => maps:from_list(V0), - 'User-Property-Pairs' => [#{ - key => Key, - value => Value - } || {Key, Value} <- V0] + 'User-Property-Pairs' => [ + #{ + key => Key, + value => Value + } + || {Key, Value} <- V0 + ] }; - (K, V0, AccIn) -> AccIn#{K => V0} - end, #{}, Headers). + (K, V0, AccIn) -> + AccIn#{K => V0} + end, + #{}, + Headers + ). ip_port_to_server_str(Host, Port) -> - HostStr = case inet:ntoa(Host) of - {error, einval} -> Host; - IPStr -> IPStr - end, + HostStr = + case inet:ntoa(Host) of + {error, einval} -> Host; + IPStr -> IPStr + end, list_to_binary(io_lib:format("~s:~w", [HostStr, Port])). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index e8e4580f4..8cc582512 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -16,17 +16,19 @@ -module(emqx_connector_mqtt_msg). --export([ to_binary/1 - , from_binary/1 - , make_pub_vars/2 - , to_remote_msg/2 - , to_broker_msg/3 - , estimate_size/1 - ]). +-export([ + to_binary/1, + from_binary/1, + make_pub_vars/2, + to_remote_msg/2, + to_broker_msg/3, + estimate_size/1 +]). --export([ replace_vars_in_str/2 - , replace_simple_var/2 - ]). +-export([ + replace_vars_in_str/2, + replace_simple_var/2 +]). -export_type([msg/0]). @@ -34,7 +36,6 @@ -include_lib("emqtt/include/emqtt.hrl"). - -type msg() :: emqx_types:message(). -type exp_msg() :: emqx_types:message() | #mqtt_msg{}. @@ -46,7 +47,8 @@ payload := binary() }. -make_pub_vars(_, undefined) -> undefined; +make_pub_vars(_, undefined) -> + undefined; make_pub_vars(Mountpoint, Conf) when is_map(Conf) -> Conf#{mountpoint => Mountpoint}. @@ -57,37 +59,56 @@ make_pub_vars(Mountpoint, Conf) when is_map(Conf) -> %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(msg() | map(), variables()) - -> exp_msg(). +-spec to_remote_msg(msg() | map(), variables()) -> + exp_msg(). to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), MapMsg = maps:put(retain, Retain0, emqx_rule_events:eventmsg_publish(Msg)), to_remote_msg(MapMsg, Vars); -to_remote_msg(MapMsg, #{remote_topic := TopicToken, payload := PayloadToken, - remote_qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> +to_remote_msg(MapMsg, #{ + remote_topic := TopicToken, + payload := PayloadToken, + remote_qos := QoSToken, + retain := RetainToken, + mountpoint := Mountpoint +}) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = process_payload(PayloadToken, MapMsg), QoS = replace_simple_var(QoSToken, MapMsg), Retain = replace_simple_var(RetainToken, MapMsg), - #mqtt_msg{qos = QoS, - retain = Retain, - topic = topic(Mountpoint, Topic), - props = #{}, - payload = Payload}; + #mqtt_msg{ + qos = QoS, + retain = Retain, + topic = topic(Mountpoint, Topic), + props = #{}, + payload = Payload + }; to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> Msg#message{topic = topic(Mountpoint, Topic)}. %% published from remote node over a MQTT connection -to_broker_msg(#{dup := Dup} = MapMsg, - #{local_topic := TopicToken, payload := PayloadToken, - local_qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}, Props) -> +to_broker_msg( + #{dup := Dup} = MapMsg, + #{ + local_topic := TopicToken, + payload := PayloadToken, + local_qos := QoSToken, + retain := RetainToken, + mountpoint := Mountpoint + }, + Props +) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = process_payload(PayloadToken, MapMsg), QoS = replace_simple_var(QoSToken, MapMsg), Retain = replace_simple_var(RetainToken, MapMsg), - set_headers(Props, - emqx_message:set_flags(#{dup => Dup, retain => Retain}, - emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload))). + set_headers( + Props, + emqx_message:set_flags( + #{dup => Dup, retain => Retain}, + emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload) + ) + ). process_payload([], Msg) -> emqx_json:encode(Msg); 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 d913e1ecf..25dc4a50f 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -21,15 +21,17 @@ -behaviour(hocon_schema). --export([ namespace/0 - , roots/0 - , fields/1 - , desc/1 - ]). +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). --export([ ingress_desc/0 - , egress_desc/0 - ]). +-export([ + ingress_desc/0, + egress_desc/0 +]). -import(emqx_schema, [mk_duration/2]). @@ -40,146 +42,210 @@ roots() -> fields("config") -> fields("connector") ++ - topic_mappings(); - + topic_mappings(); fields("connector") -> - [ {mode, - sc(hoconsc:enum([cluster_shareload]), - #{ default => cluster_shareload - , desc => ?DESC("mode") - })} - , {server, - sc(emqx_schema:ip_port(), - #{ required => true - , desc => ?DESC("server") - })} - , {reconnect_interval, mk_duration( - "Reconnect interval. Delay for the MQTT bridge to retry establishing the connection " - "in case of transportation failure.", - #{default => "15s"})} - , {proto_ver, - sc(hoconsc:enum([v3, v4, v5]), - #{ default => v4 - , desc => ?DESC("proto_ver") - })} - , {username, - sc(binary(), - #{ default => "emqx" - , desc => ?DESC("username") - })} - , {password, - sc(binary(), - #{ default => "emqx" - , desc => ?DESC("password") - })} - , {clean_start, - sc(boolean(), - #{ default => true - , desc => ?DESC("clean_start") - })} - , {keepalive, mk_duration("MQTT Keepalive.", #{default => "300s"})} - , {retry_interval, mk_duration( - "Message retry interval. Delay for the MQTT bridge to retry sending the QoS1/QoS2 " - "messages in case of ACK not received.", - #{default => "15s"})} - , {max_inflight, - sc(non_neg_integer(), - #{ default => 32 - , desc => ?DESC("max_inflight") - })} - , {replayq, - sc(ref("replayq"), #{})} + [ + {mode, + sc( + hoconsc:enum([cluster_shareload]), + #{ + default => cluster_shareload, + desc => ?DESC("mode") + } + )}, + {server, + sc( + emqx_schema:ip_port(), + #{ + required => true, + desc => ?DESC("server") + } + )}, + {reconnect_interval, + mk_duration( + "Reconnect interval. Delay for the MQTT bridge to retry establishing the connection " + "in case of transportation failure.", + #{default => "15s"} + )}, + {proto_ver, + sc( + hoconsc:enum([v3, v4, v5]), + #{ + default => v4, + desc => ?DESC("proto_ver") + } + )}, + {username, + sc( + binary(), + #{ + default => "emqx", + desc => ?DESC("username") + } + )}, + {password, + sc( + binary(), + #{ + default => "emqx", + desc => ?DESC("password") + } + )}, + {clean_start, + sc( + boolean(), + #{ + default => true, + desc => ?DESC("clean_start") + } + )}, + {keepalive, mk_duration("MQTT Keepalive.", #{default => "300s"})}, + {retry_interval, + mk_duration( + "Message retry interval. Delay for the MQTT bridge to retry sending the QoS1/QoS2 " + "messages in case of ACK not received.", + #{default => "15s"} + )}, + {max_inflight, + sc( + non_neg_integer(), + #{ + default => 32, + desc => ?DESC("max_inflight") + } + )}, + {replayq, sc(ref("replayq"), #{})} ] ++ emqx_connector_schema_lib:ssl_fields(); - fields("ingress") -> %% the message maybe subscribed by rules, in this case 'local_topic' is not necessary - [ {remote_topic, - sc(binary(), - #{ required => true - , validator => fun emqx_schema:non_empty_string/1 - , desc => ?DESC("ingress_remote_topic") - })} - , {remote_qos, - sc(qos(), - #{ default => 1 - , desc => ?DESC("ingress_remote_qos") - })} - , {local_topic, - sc(binary(), - #{ validator => fun emqx_schema:non_empty_string/1 - , desc => ?DESC("ingress_local_topic") - })} - , {local_qos, - sc(qos(), - #{ default => <<"${qos}">> - , desc => ?DESC("ingress_local_qos") - })} - , {hookpoint, - sc(binary(), - #{ desc => ?DESC("ingress_hookpoint") - })} + [ + {remote_topic, + sc( + binary(), + #{ + required => true, + validator => fun emqx_schema:non_empty_string/1, + desc => ?DESC("ingress_remote_topic") + } + )}, + {remote_qos, + sc( + qos(), + #{ + default => 1, + desc => ?DESC("ingress_remote_qos") + } + )}, + {local_topic, + sc( + binary(), + #{ + validator => fun emqx_schema:non_empty_string/1, + desc => ?DESC("ingress_local_topic") + } + )}, + {local_qos, + sc( + qos(), + #{ + default => <<"${qos}">>, + desc => ?DESC("ingress_local_qos") + } + )}, + {hookpoint, + sc( + binary(), + #{desc => ?DESC("ingress_hookpoint")} + )}, - , {retain, - sc(hoconsc:union([boolean(), binary()]), - #{ default => <<"${retain}">> - , desc => ?DESC("retain") - })} + {retain, + sc( + hoconsc:union([boolean(), binary()]), + #{ + default => <<"${retain}">>, + desc => ?DESC("retain") + } + )}, - , {payload, - sc(binary(), - #{ default => <<"${payload}">> - , desc => ?DESC("payload") - })} + {payload, + sc( + binary(), + #{ + default => <<"${payload}">>, + desc => ?DESC("payload") + } + )} ]; - - fields("egress") -> %% the message maybe sent from rules, in this case 'local_topic' is not necessary - [ {local_topic, - sc(binary(), - #{ desc => ?DESC("egress_local_topic") - , validator => fun emqx_schema:non_empty_string/1 - })} - , {remote_topic, - sc(binary(), - #{ required => true - , validator => fun emqx_schema:non_empty_string/1 - , desc => ?DESC("egress_remote_topic") - })} - , {remote_qos, - sc(qos(), - #{ required => true - , desc => ?DESC("egress_remote_qos") - })} + [ + {local_topic, + sc( + binary(), + #{ + desc => ?DESC("egress_local_topic"), + validator => fun emqx_schema:non_empty_string/1 + } + )}, + {remote_topic, + sc( + binary(), + #{ + required => true, + validator => fun emqx_schema:non_empty_string/1, + desc => ?DESC("egress_remote_topic") + } + )}, + {remote_qos, + sc( + qos(), + #{ + required => true, + desc => ?DESC("egress_remote_qos") + } + )}, - , {retain, - sc(hoconsc:union([boolean(), binary()]), - #{ required => true - , desc => ?DESC("retain") - })} + {retain, + sc( + hoconsc:union([boolean(), binary()]), + #{ + required => true, + desc => ?DESC("retain") + } + )}, - , {payload, - sc(binary(), - #{ required => true - , desc => ?DESC("payload") - })} + {payload, + sc( + binary(), + #{ + required => true, + desc => ?DESC("payload") + } + )} ]; - fields("replayq") -> - [ {dir, - sc(hoconsc:union([boolean(), string()]), - #{ desc => ?DESC("dir") - })} - , {seg_bytes, - sc(emqx_schema:bytesize(), - #{ default => "100MB" - , desc => ?DESC("seg_bytes") - })} - , {offload, - sc(boolean(), - #{ default => false - , desc => ?DESC("offload") - })} + [ + {dir, + sc( + hoconsc:union([boolean(), string()]), + #{desc => ?DESC("dir")} + )}, + {seg_bytes, + sc( + emqx_schema:bytesize(), + #{ + default => "100MB", + desc => ?DESC("seg_bytes") + } + )}, + {offload, + sc( + boolean(), + #{ + default => false, + desc => ?DESC("offload") + } + )} ]. desc("connector") -> @@ -194,34 +260,37 @@ desc(_) -> undefined. topic_mappings() -> - [ {ingress, - sc(ref("ingress"), - #{ default => #{} - })} - , {egress, - sc(ref("egress"), - #{ default => #{} - })} + [ + {ingress, + sc( + ref("ingress"), + #{default => #{}} + )}, + {egress, + sc( + ref("egress"), + #{default => #{}} + )} ]. -ingress_desc() -> " -The ingress config defines how this bridge receive messages from the remote MQTT broker, and then -send them to the local broker.
-Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain', -'payload'.
-NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is -configured, then messages got from the remote broker will be sent to both the 'local_topic' and -the rule. -". +ingress_desc() -> + "\n" + "The ingress config defines how this bridge receive messages from the remote MQTT broker, and then\n" + "send them to the local broker.
\n" + "Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain',\n" + "'payload'.
\n" + "NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is\n" + "configured, then messages got from the remote broker will be sent to both the 'local_topic' and\n" + "the rule.\n". -egress_desc() -> " -The egress config defines how this bridge forwards messages from the local broker to the remote -broker.
-Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.
-NOTE: if this bridge is used as the output of a rule (emqx rule engine), and also local_topic -is configured, then both the data got from the rule and the MQTT messages that matches -local_topic will be forwarded. -". +egress_desc() -> + "\n" + "The egress config defines how this bridge forwards messages from the local broker to the remote\n" + "broker.
\n" + "Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.
\n" + "NOTE: if this bridge is used as the output of a rule (emqx rule engine), and also local_topic\n" + "is configured, then both the data got from the rule and the MQTT messages that matches\n" + "local_topic will be forwarded.\n". qos() -> hoconsc:union([emqx_schema:qos(), binary()]). diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 184c112ed..a434dd762 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -66,43 +66,46 @@ -include_lib("emqx/include/logger.hrl"). %% APIs --export([ start_link/1 - , register_metrics/0 - , stop/1 - ]). +-export([ + start_link/1, + register_metrics/0, + stop/1 +]). %% gen_statem callbacks --export([ terminate/3 - , code_change/4 - , init/1 - , callback_mode/0 - ]). +-export([ + terminate/3, + code_change/4, + init/1, + callback_mode/0 +]). %% state functions --export([ idle/3 - , connected/3 - ]). +-export([ + idle/3, + connected/3 +]). %% management APIs --export([ ensure_started/1 - , ensure_stopped/1 - , status/1 - , ping/1 - , send_to_remote/2 - ]). +-export([ + ensure_started/1, + ensure_stopped/1, + status/1, + ping/1, + send_to_remote/2 +]). --export([ get_forwards/1 - ]). +-export([get_forwards/1]). --export([ get_subscriptions/1 - ]). +-export([get_subscriptions/1]). %% Internal -export([msg_marshaller/1]). --export_type([ config/0 - , ack_ref/0 - ]). +-export_type([ + config/0, + ack_ref/0 +]). -type id() :: atom() | string() | pid(). -type qos() :: emqx_types:qos(). @@ -113,7 +116,6 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). - %% same as default in-flight limit for emqtt -define(DEFAULT_INFLIGHT_SIZE, 32). -define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)). @@ -188,8 +190,10 @@ callback_mode() -> [state_functions]. %% @doc Config should be a map(). init(#{name := Name} = ConnectOpts) -> - ?SLOG(debug, #{msg => "starting_bridge_worker", - name => Name}), + ?SLOG(debug, #{ + msg => "starting_bridge_worker", + name => Name + }), erlang:process_flag(trap_exit, true), Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})), State = init_state(ConnectOpts), @@ -205,31 +209,44 @@ init_state(Opts) -> Mountpoint = maps:get(forward_mountpoint, Opts, undefined), MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_INFLIGHT_SIZE), Name = maps:get(name, Opts, undefined), - #{start_type => StartType, - reconnect_interval => ReconnDelayMs, - mountpoint => format_mountpoint(Mountpoint), - inflight => [], - max_inflight => MaxInflightSize, - connection => undefined, - name => Name}. + #{ + start_type => StartType, + reconnect_interval => ReconnDelayMs, + mountpoint => format_mountpoint(Mountpoint), + inflight => [], + max_inflight => MaxInflightSize, + connection => undefined, + name => Name + }. open_replayq(Name, QCfg) -> Dir = maps:get(dir, QCfg, undefined), SegBytes = maps:get(seg_bytes, QCfg, ?DEFAULT_SEG_BYTES), MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE), - QueueConfig = case Dir =:= undefined orelse Dir =:= "" of - true -> #{mem_only => true}; - false -> #{dir => filename:join([Dir, node(), Name]), - seg_bytes => SegBytes, max_total_size => MaxTotalSize} - end, - replayq:open(QueueConfig#{sizer => fun emqx_connector_mqtt_msg:estimate_size/1, - marshaller => fun ?MODULE:msg_marshaller/1}). + QueueConfig = + case Dir =:= undefined orelse Dir =:= "" of + true -> + #{mem_only => true}; + false -> + #{ + dir => filename:join([Dir, node(), Name]), + seg_bytes => SegBytes, + max_total_size => MaxTotalSize + } + end, + replayq:open(QueueConfig#{ + sizer => fun emqx_connector_mqtt_msg:estimate_size/1, + marshaller => fun ?MODULE:msg_marshaller/1 + }). pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> - ConnectOpts#{subscriptions => pre_process_in_out(in, InConf), - forwards => pre_process_in_out(out, OutConf)}. + ConnectOpts#{ + subscriptions => pre_process_in_out(in, InConf), + forwards => pre_process_in_out(out, OutConf) + }. -pre_process_in_out(_, undefined) -> undefined; +pre_process_in_out(_, undefined) -> + undefined; pre_process_in_out(in, Conf) when is_map(Conf) -> Conf1 = pre_process_conf(local_topic, Conf), Conf2 = pre_process_conf(local_qos, Conf1), @@ -245,7 +262,8 @@ pre_process_in_out_common(Conf) -> pre_process_conf(Key, Conf) -> case maps:find(Key, Conf) of - error -> Conf; + error -> + Conf; {ok, Val} when is_binary(Val) -> Conf#{Key => emqx_plugin_libs_rule:preproc_tmpl(Val)}; {ok, Val} -> @@ -276,7 +294,6 @@ idle(info, idle, #{start_type := auto} = State) -> connecting(State); idle(state_timeout, reconnect, State) -> connecting(State); - idle(Type, Content, State) -> common(idle, Type, Content, State). @@ -298,13 +315,16 @@ connected(state_timeout, connected, #{inflight := Inflight} = State) -> connected(internal, maybe_send, State) -> {_, NewState} = pop_and_send(State), {keep_state, NewState}; - -connected(info, {disconnected, Conn, Reason}, - #{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State) -> +connected( + info, + {disconnected, Conn, Reason}, + #{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State +) -> ?tp(info, disconnected, #{name => Name, reason => Reason}), - case Conn =:= maps:get(client_pid, Connection, undefined) of + case Conn =:= maps:get(client_pid, Connection, undefined) of true -> - {next_state, idle, State#{connection => undefined}, {state_timeout, ReconnectDelayMs, reconnect}}; + {next_state, idle, State#{connection => undefined}, + {state_timeout, ReconnectDelayMs, reconnect}}; false -> keep_state_and_data end; @@ -317,7 +337,7 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; -common(_StateName, {call, From}, ping, #{connection := Conn} =_State) -> +common(_StateName, {call, From}, ping, #{connection := Conn} = _State) -> Reply = emqx_connector_mqtt_mod:ping(Conn), {keep_state_and_data, [{reply, From, Reply}]}; common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> @@ -335,27 +355,39 @@ common(_StateName, cast, {send_to_remote, Msg}, #{replayq := Q} = State) -> NewQ = replayq:append(Q, [Msg]), {keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}}; common(StateName, Type, Content, #{name := Name} = State) -> - ?SLOG(notice, #{msg => "bridge_discarded_event", - name => Name, type => Type, state_name => StateName, - content => Content}), + ?SLOG(notice, #{ + msg => "bridge_discarded_event", + name => Name, + type => Type, + state_name => StateName, + content => Content + }), {keep_state, State}. -do_connect(#{connect_opts := ConnectOpts, - inflight := Inflight, - name := Name} = State) -> +do_connect( + #{ + connect_opts := ConnectOpts, + inflight := Inflight, + name := Name + } = State +) -> case emqx_connector_mqtt_mod:start(ConnectOpts) of {ok, Conn} -> ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), {ok, State#{connection => Conn}}; {error, Reason} -> ConnectOpts1 = obfuscate(ConnectOpts), - ?SLOG(error, #{msg => "failed_to_connect", - config => ConnectOpts1, reason => Reason}), + ?SLOG(error, #{ + msg => "failed_to_connect", + config => ConnectOpts1, + reason => Reason + }), {error, Reason, State} end. %% Retry all inflight (previously sent but not acked) batches. -retry_inflight(State, []) -> {ok, State}; +retry_inflight(State, []) -> + {ok, State}; retry_inflight(State, [#{q_ack_ref := QAckRef, msg := Msg} | Rest] = OldInf) -> case do_send(State, QAckRef, Msg) of {ok, State1} -> @@ -386,28 +418,49 @@ pop_and_send_loop(#{replayq := Q} = State, N) -> end. do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) -> - ?SLOG(error, #{msg => "cannot_forward_messages_to_remote_broker" - "_as_'egress'_is_not_configured", - messages => Msg}); -do_send(#{inflight := Inflight, - connection := Connection, - mountpoint := Mountpoint, - connect_opts := #{forwards := Forwards}} = State, QAckRef, Msg) -> + ?SLOG(error, #{ + msg => + "cannot_forward_messages_to_remote_broker" + "_as_'egress'_is_not_configured", + messages => Msg + }); +do_send( + #{ + inflight := Inflight, + connection := Connection, + mountpoint := Mountpoint, + connect_opts := #{forwards := Forwards} + } = State, + QAckRef, + Msg +) -> Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> - emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'), - emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) - end, - ?SLOG(debug, #{msg => "publish_to_remote_broker", - message => Msg, vars => Vars}), + emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'), + emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) + end, + ?SLOG(debug, #{ + msg => "publish_to_remote_broker", + message => Msg, + vars => Vars + }), case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(Msg)]) of {ok, Refs} -> - {ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef, - send_ack_ref => map_set(Refs), - msg => Msg}]}}; + {ok, State#{ + inflight := Inflight ++ + [ + #{ + q_ack_ref => QAckRef, + send_ack_ref => map_set(Refs), + msg => Msg + } + ] + }}; {error, Reason} -> - ?SLOG(info, #{msg => "mqtt_bridge_produce_failed", - reason => Reason}), + ?SLOG(info, #{ + msg => "mqtt_bridge_produce_failed", + reason => Reason + }), {error, State} end. @@ -427,8 +480,10 @@ handle_batch_ack(#{inflight := Inflight0, replayq := Q} = State, Ref) -> State#{inflight := Inflight}. do_ack([], Ref) -> - ?SLOG(debug, #{msg => "stale_batch_ack_reference", - ref => Ref}), + ?SLOG(debug, #{ + msg => "stale_batch_ack_reference", + ref => Ref + }), []; do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) -> case maps:is_key(Ref, Refs) of @@ -443,8 +498,16 @@ do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) -> drop_acked_batches(_Q, []) -> ?tp(debug, inflight_drained, #{}), []; -drop_acked_batches(Q, [#{send_ack_ref := Refs, - q_ack_ref := QAckRef} | Rest] = All) -> +drop_acked_batches( + Q, + [ + #{ + send_ack_ref := Refs, + q_ack_ref := QAckRef + } + | Rest + ] = All +) -> case maps:size(Refs) of 0 -> %% all messages are acked by bridge target @@ -475,18 +538,25 @@ format_mountpoint(Prefix) -> name(Id) -> list_to_atom(str(Id)). register_metrics() -> - lists:foreach(fun emqx_metrics:ensure/1, - ['bridge.mqtt.message_sent_to_remote', - 'bridge.mqtt.message_received_from_remote' - ]). + lists:foreach( + fun emqx_metrics:ensure/1, + [ + 'bridge.mqtt.message_sent_to_remote', + 'bridge.mqtt.message_received_from_remote' + ] + ). obfuscate(Map) -> - maps:fold(fun(K, V, Acc) -> - case is_sensitive(K) of - true -> [{K, '***'} | Acc]; - false -> [{K, V} | Acc] - end - end, [], Map). + maps:fold( + fun(K, V, Acc) -> + case is_sensitive(K) of + true -> [{K, '***'} | Acc]; + false -> [{K, V} | Acc] + end + end, + [], + Map + ). is_sensitive(password) -> true; is_sensitive(_) -> false. diff --git a/apps/emqx_connector/test/emqx_connector_api_SUITE.erl b/apps/emqx_connector/test/emqx_connector_api_SUITE.erl index 329bfe059..65a965d60 100644 --- a/apps/emqx_connector/test/emqx_connector_api_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_api_SUITE.erl @@ -26,27 +26,23 @@ -include("emqx_dashboard/include/emqx_dashboard.hrl"). %% output functions --export([ inspect/3 - ]). +-export([inspect/3]). -define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>). -define(CONNECTR_TYPE, <<"mqtt">>). -define(CONNECTR_NAME, <<"test_connector">>). -define(BRIDGE_NAME_INGRESS, <<"ingress_test_bridge">>). -define(BRIDGE_NAME_EGRESS, <<"egress_test_bridge">>). --define(MQTT_CONNECTOR(Username), -#{ +-define(MQTT_CONNECTOR(Username), #{ <<"server">> => <<"127.0.0.1:1883">>, <<"username">> => Username, <<"password">> => <<"">>, <<"proto_ver">> => <<"v4">>, <<"ssl">> => #{<<"enable">> => false} }). --define(MQTT_CONNECTOR2(Server), - ?MQTT_CONNECTOR(<<"user1">>)#{<<"server">> => Server}). +-define(MQTT_CONNECTOR2(Server), ?MQTT_CONNECTOR(<<"user1">>)#{<<"server">> => Server}). --define(MQTT_BRIDGE_INGRESS(ID), -#{ +-define(MQTT_BRIDGE_INGRESS(ID), #{ <<"connector">> => ID, <<"direction">> => <<"ingress">>, <<"remote_topic">> => <<"remote_topic/#">>, @@ -57,8 +53,7 @@ <<"retain">> => <<"${retain}">> }). --define(MQTT_BRIDGE_EGRESS(ID), -#{ +-define(MQTT_BRIDGE_EGRESS(ID), #{ <<"connector">> => ID, <<"direction">> => <<"egress">>, <<"local_topic">> => <<"local_topic/#">>, @@ -68,10 +63,14 @@ <<"retain">> => <<"${retain}">> }). --define(metrics(MATCH, SUCC, FAILED, SPEED, SPEED5M, SPEEDMAX), - #{<<"matched">> := MATCH, <<"success">> := SUCC, - <<"failed">> := FAILED, <<"rate">> := SPEED, - <<"rate_last5m">> := SPEED5M, <<"rate_max">> := SPEEDMAX}). +-define(metrics(MATCH, SUCC, FAILED, SPEED, SPEED5M, SPEEDMAX), #{ + <<"matched">> := MATCH, + <<"success">> := SUCC, + <<"failed">> := FAILED, + <<"rate">> := SPEED, + <<"rate_last5m">> := SPEED5M, + <<"rate_max">> := SPEEDMAX +}). inspect(Selected, _Envs, _Args) -> persistent_term:put(?MODULE, #{inspect => Selected}). @@ -83,24 +82,37 @@ groups() -> []. suite() -> - [{timetrap,{seconds,30}}]. + [{timetrap, {seconds, 30}}]. init_per_suite(Config) -> _ = application:load(emqx_conf), %% some testcases (may from other app) already get emqx_connector started _ = application:stop(emqx_resource), _ = application:stop(emqx_connector), - ok = emqx_common_test_helpers:start_apps([emqx_rule_engine, emqx_connector, - emqx_bridge, emqx_dashboard], fun set_special_configs/1), + ok = emqx_common_test_helpers:start_apps( + [ + emqx_rule_engine, + emqx_connector, + emqx_bridge, + emqx_dashboard + ], + fun set_special_configs/1 + ), ok = emqx_common_test_helpers:load_config(emqx_connector_schema, <<"connectors: {}">>), - ok = emqx_common_test_helpers:load_config(emqx_rule_engine_schema, - <<"rule_engine {rules {}}">>), + ok = emqx_common_test_helpers:load_config( + emqx_rule_engine_schema, + <<"rule_engine {rules {}}">> + ), ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?BRIDGE_CONF_DEFAULT), Config. end_per_suite(_Config) -> - emqx_common_test_helpers:stop_apps([emqx_rule_engine, emqx_connector, emqx_bridge, - emqx_dashboard]), + emqx_common_test_helpers:stop_apps([ + emqx_rule_engine, + emqx_connector, + emqx_bridge, + emqx_dashboard + ]), ok. set_special_configs(emqx_dashboard) -> @@ -116,15 +128,24 @@ end_per_testcase(_, _Config) -> ok. clear_resources() -> - lists:foreach(fun(#{id := Id}) -> + lists:foreach( + fun(#{id := Id}) -> ok = emqx_rule_engine:delete_rule(Id) - end, emqx_rule_engine:get_rules()), - lists:foreach(fun(#{type := Type, name := Name}) -> + end, + emqx_rule_engine:get_rules() + ), + lists:foreach( + fun(#{type := Type, name := Name}) -> ok = emqx_bridge:remove(Type, Name) - end, emqx_bridge:list()), - lists:foreach(fun(#{<<"type">> := Type, <<"name">> := Name}) -> + end, + emqx_bridge:list() + ), + lists:foreach( + fun(#{<<"type">> := Type, <<"name">> := Name}) -> ok = emqx_connector:delete(Type, Name) - end, emqx_connector:list_raw()). + end, + emqx_connector:list_raw() + ). %%------------------------------------------------------------------------------ %% Testcases @@ -137,103 +158,144 @@ t_mqtt_crud_apis(_) -> %% then we add a mqtt connector, using POST %% POST /connectors/ will create a connector User1 = <<"user1">>, - {ok, 400, <<"{\"code\":\"BAD_REQUEST\",\"message\"" - ":\"missing some required fields: [name, type]\"}">>} - = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE - }), - {ok, 201, Connector} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 400, << + "{\"code\":\"BAD_REQUEST\",\"message\"" + ":\"missing some required fields: [name, type]\"}" + >>} = + request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(User1)#{<<"type">> => ?CONNECTR_TYPE} + ), + {ok, 201, Connector} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(User1)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?CONNECTR_NAME - , <<"server">> := <<"127.0.0.1:1883">> - , <<"username">> := User1 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - } = jsx:decode(Connector), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?CONNECTR_NAME, + <<"server">> := <<"127.0.0.1:1883">>, + <<"username">> := User1, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + } = jsx:decode(Connector), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% update the request-path of the connector User2 = <<"user2">>, - {ok, 200, Connector2} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR(User2)), - ?assertMatch(#{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?CONNECTR_NAME - , <<"server">> := <<"127.0.0.1:1883">> - , <<"username">> := User2 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - }, jsx:decode(Connector2)), + {ok, 200, Connector2} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR(User2) + ), + ?assertMatch( + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?CONNECTR_NAME, + <<"server">> := <<"127.0.0.1:1883">>, + <<"username">> := User2, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + }, + jsx:decode(Connector2) + ), %% list all connectors again, assert Connector2 is in it {ok, 200, Connector2Str} = request(get, uri(["connectors"]), []), - ?assertMatch([#{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?CONNECTR_NAME - , <<"server">> := <<"127.0.0.1:1883">> - , <<"username">> := User2 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - }], jsx:decode(Connector2Str)), + ?assertMatch( + [ + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?CONNECTR_NAME, + <<"server">> := <<"127.0.0.1:1883">>, + <<"username">> := User2, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + } + ], + jsx:decode(Connector2Str) + ), %% get the connector by id {ok, 200, Connector3Str} = request(get, uri(["connectors", ConnctorID]), []), - ?assertMatch(#{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?CONNECTR_NAME - , <<"server">> := <<"127.0.0.1:1883">> - , <<"username">> := User2 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - }, jsx:decode(Connector3Str)), + ?assertMatch( + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?CONNECTR_NAME, + <<"server">> := <<"127.0.0.1:1883">>, + <<"username">> := User2, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + }, + jsx:decode(Connector3Str) + ), %% delete the connector {ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []), {ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []), %% update a deleted connector returns an error - {ok, 404, ErrMsg2} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR(User2)), + {ok, 404, ErrMsg2} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR(User2) + ), ?assertMatch( - #{ <<"code">> := _ - , <<"message">> := <<"connector not found">> - }, jsx:decode(ErrMsg2)), + #{ + <<"code">> := _, + <<"message">> := <<"connector not found">> + }, + jsx:decode(ErrMsg2) + ), ok. t_mqtt_conn_bridge_ingress(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, - {ok, 201, Connector} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, Connector} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(User1)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?CONNECTR_NAME - , <<"server">> := <<"127.0.0.1:1883">> - , <<"num_of_bridges">> := 0 - , <<"username">> := User1 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - } = jsx:decode(Connector), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?CONNECTR_NAME, + <<"server">> := <<"127.0.0.1:1883">>, + <<"num_of_bridges">> := 0, + <<"username">> := User1, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + } = jsx:decode(Connector), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% ... and a MQTT bridge, using POST %% we bind this bridge to the connector created just now timer:sleep(50), - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_INGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_INGRESS - }), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?BRIDGE_NAME_INGRESS - , <<"connector">> := ConnctorID - } = jsx:decode(Bridge), + } + ), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?BRIDGE_NAME_INGRESS, + <<"connector">> := ConnctorID + } = jsx:decode(Bridge), BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS), wait_for_resource_ready(BridgeIDIngress, 5), @@ -257,12 +319,12 @@ t_mqtt_conn_bridge_ingress(_) -> false after 100 -> false - end), + end + ), %% get the connector by id, verify the num_of_bridges now is 1 {ok, 200, Connector1Str} = request(get, uri(["connectors", ConnctorID]), []), - ?assertMatch(#{ <<"num_of_bridges">> := 1 - }, jsx:decode(Connector1Str)), + ?assertMatch(#{<<"num_of_bridges">> := 1}, jsx:decode(Connector1Str)), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []), @@ -276,30 +338,39 @@ t_mqtt_conn_bridge_ingress(_) -> t_mqtt_conn_bridge_egress(_) -> %% then we add a mqtt connector, using POST User1 = <<"user1">>, - {ok, 201, Connector} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, Connector} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(User1)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), %ct:pal("---connector: ~p", [Connector]), - #{ <<"server">> := <<"127.0.0.1:1883">> - , <<"username">> := User1 - , <<"password">> := <<"">> - , <<"proto_ver">> := <<"v4">> - , <<"ssl">> := #{<<"enable">> := false} - } = jsx:decode(Connector), + #{ + <<"server">> := <<"127.0.0.1:1883">>, + <<"username">> := User1, + <<"password">> := <<"">>, + <<"proto_ver">> := <<"v4">>, + <<"ssl">> := #{<<"enable">> := false} + } = jsx:decode(Connector), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% ... and a MQTT bridge, using POST %% we bind this bridge to the connector created just now - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_EGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?BRIDGE_NAME_EGRESS - , <<"connector">> := ConnctorID - } = jsx:decode(Bridge), + } + ), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?BRIDGE_NAME_EGRESS, + <<"connector">> := ConnctorID + } = jsx:decode(Bridge), BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS), wait_for_resource_ready(BridgeIDEgress, 5), @@ -324,14 +395,19 @@ t_mqtt_conn_bridge_egress(_) -> false after 100 -> false - end), + end + ), %% verify the metrics of the bridge {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), - ?assertMatch(#{ <<"metrics">> := ?metrics(1, 1, 0, _, _, _) - , <<"node_metrics">> := - [#{<<"node">> := _, <<"metrics">> := ?metrics(1, 1, 0, _, _, _)}] - }, jsx:decode(BridgeStr)), + ?assertMatch( + #{ + <<"metrics">> := ?metrics(1, 1, 0, _, _, _), + <<"node_metrics">> := + [#{<<"node">> := _, <<"metrics">> := ?metrics(1, 1, 0, _, _, _)}] + }, + jsx:decode(BridgeStr) + ), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), @@ -347,38 +423,50 @@ t_mqtt_conn_bridge_egress(_) -> %% - cannot delete a connector that is used by at least one bridge t_mqtt_conn_update(_) -> %% then we add a mqtt connector, using POST - {ok, 201, Connector} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>) - #{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, Connector} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), %ct:pal("---connector: ~p", [Connector]), - #{ <<"server">> := <<"127.0.0.1:1883">> - } = jsx:decode(Connector), + #{<<"server">> := <<"127.0.0.1:1883">>} = jsx:decode(Connector), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% ... and a MQTT bridge, using POST %% we bind this bridge to the connector created just now - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_EGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?BRIDGE_NAME_EGRESS - , <<"connector">> := ConnctorID - } = jsx:decode(Bridge), + } + ), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?BRIDGE_NAME_EGRESS, + <<"connector">> := ConnctorID + } = jsx:decode(Bridge), BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS), wait_for_resource_ready(BridgeIDEgress, 5), %% Then we try to update 'server' of the connector, to an unavailable IP address %% The update OK, we recreate the resource even if the resource is current connected, %% and the target resource we're going to update is unavailable. - {ok, 200, _} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)), + {ok, 200, _} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>) + ), %% we fix the 'server' parameter to a normal one, it should work - {ok, 200, _} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR2(<<"127.0.0.1 : 1883">>)), + {ok, 200, _} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR2(<<"127.0.0.1 : 1883">>) + ), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), @@ -390,40 +478,51 @@ t_mqtt_conn_update(_) -> t_mqtt_conn_update2(_) -> %% then we add a mqtt connector, using POST %% but this connector is point to a unreachable server "2603" - {ok, 201, Connector} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>) - #{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, Connector} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), - #{ <<"server">> := <<"127.0.0.1:2603">> - } = jsx:decode(Connector), + #{<<"server">> := <<"127.0.0.1:2603">>} = jsx:decode(Connector), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% ... and a MQTT bridge, using POST %% we bind this bridge to the connector created just now - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_EGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - #{ <<"type">> := ?CONNECTR_TYPE - , <<"name">> := ?BRIDGE_NAME_EGRESS - , <<"status">> := <<"disconnected">> - , <<"connector">> := ConnctorID - } = jsx:decode(Bridge), + } + ), + #{ + <<"type">> := ?CONNECTR_TYPE, + <<"name">> := ?BRIDGE_NAME_EGRESS, + <<"status">> := <<"disconnected">>, + <<"connector">> := ConnctorID + } = jsx:decode(Bridge), BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS), %% We try to fix the 'server' parameter, to another unavailable server.. %% The update should success: we don't check the connectivity of the new config %% if the resource is now disconnected. - {ok, 200, _} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:2604">>)), + {ok, 200, _} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:2604">>) + ), %% we fix the 'server' parameter to a normal one, it should work - {ok, 200, _} = request(put, uri(["connectors", ConnctorID]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)), + {ok, 200, _} = request( + put, + uri(["connectors", ConnctorID]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>) + ), wait_for_resource_ready(BridgeIDEgress, 5), {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), - ?assertMatch(#{ <<"status">> := <<"connected">> - }, jsx:decode(BridgeStr)), + ?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(BridgeStr)), %% delete the bridge {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), @@ -434,21 +533,26 @@ t_mqtt_conn_update2(_) -> t_mqtt_conn_update3(_) -> %% we add a mqtt connector, using POST - {ok, 201, _} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>) - #{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, _} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), %% ... and a MQTT bridge, using POST %% we bind this bridge to the connector created just now - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_EGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - #{ <<"connector">> := ConnctorID - } = jsx:decode(Bridge), + } + ), + #{<<"connector">> := ConnctorID} = jsx:decode(Bridge), BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS), wait_for_resource_ready(BridgeIDEgress, 5), @@ -462,37 +566,54 @@ t_mqtt_conn_update3(_) -> t_mqtt_conn_testing(_) -> %% APIs for testing the connectivity %% then we add a mqtt connector, using POST - {ok, 204, <<>>} = request(post, uri(["connectors_test"]), + {ok, 204, <<>>} = request( + post, + uri(["connectors_test"]), ?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - {ok, 400, _} = request(post, uri(["connectors_test"]), + } + ), + {ok, 400, _} = request( + post, + uri(["connectors_test"]), ?MQTT_CONNECTOR2(<<"127.0.0.1:2883">>)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }). + } + ). t_ingress_mqtt_bridge_with_rules(_) -> - {ok, 201, _} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(<<"user1">>)#{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, _} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(<<"user1">>)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), - {ok, 201, _} = request(post, uri(["bridges"]), + {ok, 201, _} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_INGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_INGRESS - }), + } + ), BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS), - {ok, 201, Rule} = request(post, uri(["rules"]), - #{<<"name">> => <<"A rule get messages from a source mqtt bridge">>, - <<"enable">> => true, - <<"outputs">> => [#{<<"function">> => "emqx_connector_api_SUITE:inspect"}], - <<"sql">> => <<"SELECT * from \"$bridges/", BridgeIDIngress/binary, "\"">> - }), + {ok, 201, Rule} = request( + post, + uri(["rules"]), + #{ + <<"name">> => <<"A rule get messages from a source mqtt bridge">>, + <<"enable">> => true, + <<"outputs">> => [#{<<"function">> => "emqx_connector_api_SUITE:inspect"}], + <<"sql">> => <<"SELECT * from \"$bridges/", BridgeIDIngress/binary, "\"">> + } + ), #{<<"id">> := RuleId} = jsx:decode(Rule), %% we now test if the bridge works as expected @@ -517,63 +638,81 @@ t_ingress_mqtt_bridge_with_rules(_) -> false after 100 -> false - end), + end + ), %% and also the rule should be matched, with matched + 1: {ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []), - #{ <<"id">> := RuleId - , <<"metrics">> := #{ - <<"sql.matched">> := 1, - <<"sql.passed">> := 1, - <<"sql.failed">> := 0, - <<"sql.failed.exception">> := 0, - <<"sql.failed.no_result">> := 0, - <<"sql.matched.rate">> := _, - <<"sql.matched.rate.max">> := _, - <<"sql.matched.rate.last5m">> := _, - <<"outputs.total">> := 1, - <<"outputs.success">> := 1, - <<"outputs.failed">> := 0, - <<"outputs.failed.out_of_service">> := 0, - <<"outputs.failed.unknown">> := 0 - } - } = jsx:decode(Rule1), + #{ + <<"id">> := RuleId, + <<"metrics">> := #{ + <<"sql.matched">> := 1, + <<"sql.passed">> := 1, + <<"sql.failed">> := 0, + <<"sql.failed.exception">> := 0, + <<"sql.failed.no_result">> := 0, + <<"sql.matched.rate">> := _, + <<"sql.matched.rate.max">> := _, + <<"sql.matched.rate.last5m">> := _, + <<"outputs.total">> := 1, + <<"outputs.success">> := 1, + <<"outputs.failed">> := 0, + <<"outputs.failed.out_of_service">> := 0, + <<"outputs.failed.unknown">> := 0 + } + } = jsx:decode(Rule1), %% we also check if the outputs of the rule is triggered - ?assertMatch(#{inspect := #{ - event := <<"$bridges/mqtt", _/binary>>, - id := MsgId, - payload := Payload, - topic := RemoteTopic, - qos := 0, - dup := false, - retain := false, - pub_props := #{}, - timestamp := _ - }} when is_binary(MsgId), persistent_term:get(?MODULE)), + ?assertMatch( + #{ + inspect := #{ + event := <<"$bridges/mqtt", _/binary>>, + id := MsgId, + payload := Payload, + topic := RemoteTopic, + qos := 0, + dup := false, + retain := false, + pub_props := #{}, + timestamp := _ + } + } when is_binary(MsgId), + persistent_term:get(?MODULE) + ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []), {ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []). t_egress_mqtt_bridge_with_rules(_) -> - {ok, 201, _} = request(post, uri(["connectors"]), - ?MQTT_CONNECTOR(<<"user1">>)#{ <<"type">> => ?CONNECTR_TYPE - , <<"name">> => ?CONNECTR_NAME - }), + {ok, 201, _} = request( + post, + uri(["connectors"]), + ?MQTT_CONNECTOR(<<"user1">>)#{ + <<"type">> => ?CONNECTR_TYPE, + <<"name">> => ?CONNECTR_NAME + } + ), ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME), - {ok, 201, Bridge} = request(post, uri(["bridges"]), + {ok, 201, Bridge} = request( + post, + uri(["bridges"]), ?MQTT_BRIDGE_EGRESS(ConnctorID)#{ <<"type">> => ?CONNECTR_TYPE, <<"name">> => ?BRIDGE_NAME_EGRESS - }), - #{ <<"type">> := ?CONNECTR_TYPE, <<"name">> := ?BRIDGE_NAME_EGRESS } = jsx:decode(Bridge), + } + ), + #{<<"type">> := ?CONNECTR_TYPE, <<"name">> := ?BRIDGE_NAME_EGRESS} = jsx:decode(Bridge), BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS), - {ok, 201, Rule} = request(post, uri(["rules"]), - #{<<"name">> => <<"A rule send messages to a sink mqtt bridge">>, - <<"enable">> => true, - <<"outputs">> => [BridgeIDEgress], - <<"sql">> => <<"SELECT * from \"t/1\"">> - }), + {ok, 201, Rule} = request( + post, + uri(["rules"]), + #{ + <<"name">> => <<"A rule send messages to a sink mqtt bridge">>, + <<"enable">> => true, + <<"outputs">> => [BridgeIDEgress], + <<"sql">> => <<"SELECT * from \"t/1\"">> + } + ), #{<<"id">> := RuleId} = jsx:decode(Rule), %% we now test if the bridge works as expected @@ -597,7 +736,8 @@ t_egress_mqtt_bridge_with_rules(_) -> false after 100 -> false - end), + end + ), emqx:unsubscribe(RemoteTopic), %% PUBLISH a message to the rule. @@ -609,23 +749,24 @@ t_egress_mqtt_bridge_with_rules(_) -> wait_for_resource_ready(BridgeIDEgress, 5), emqx:publish(emqx_message:make(RuleTopic, Payload2)), {ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []), - #{ <<"id">> := RuleId - , <<"metrics">> := #{ - <<"sql.matched">> := 1, - <<"sql.passed">> := 1, - <<"sql.failed">> := 0, - <<"sql.failed.exception">> := 0, - <<"sql.failed.no_result">> := 0, - <<"sql.matched.rate">> := _, - <<"sql.matched.rate.max">> := _, - <<"sql.matched.rate.last5m">> := _, - <<"outputs.total">> := 1, - <<"outputs.success">> := 1, - <<"outputs.failed">> := 0, - <<"outputs.failed.out_of_service">> := 0, - <<"outputs.failed.unknown">> := 0 - } - } = jsx:decode(Rule1), + #{ + <<"id">> := RuleId, + <<"metrics">> := #{ + <<"sql.matched">> := 1, + <<"sql.passed">> := 1, + <<"sql.failed">> := 0, + <<"sql.failed.exception">> := 0, + <<"sql.failed.no_result">> := 0, + <<"sql.matched.rate">> := _, + <<"sql.matched.rate.max">> := _, + <<"sql.matched.rate.last5m">> := _, + <<"outputs.total">> := 1, + <<"outputs.success">> := 1, + <<"outputs.failed">> := 0, + <<"outputs.failed.out_of_service">> := 0, + <<"outputs.failed.unknown">> := 0 + } + } = jsx:decode(Rule1), %% we should receive a message on the "remote" broker, with specified topic ?assert( receive @@ -637,14 +778,19 @@ t_egress_mqtt_bridge_with_rules(_) -> false after 100 -> false - end), + end + ), %% verify the metrics of the bridge {ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []), - ?assertMatch(#{ <<"metrics">> := ?metrics(2, 2, 0, _, _, _) - , <<"node_metrics">> := - [#{<<"node">> := _, <<"metrics">> := ?metrics(2, 2, 0, _, _, _)}] - }, jsx:decode(BridgeStr)), + ?assertMatch( + #{ + <<"metrics">> := ?metrics(2, 2, 0, _, _, _), + <<"node_metrics">> := + [#{<<"node">> := _, <<"metrics">> := ?metrics(2, 2, 0, _, _, _)}] + }, + jsx:decode(BridgeStr) + ), {ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []), @@ -658,8 +804,9 @@ wait_for_resource_ready(InstId, 0) -> ct:fail(wait_resource_timeout); wait_for_resource_ready(InstId, Retry) -> case emqx_bridge:lookup(InstId) of - {ok, #{resource_data := #{status := connected}}} -> ok; + {ok, #{resource_data := #{status := connected}}} -> + ok; _ -> timer:sleep(100), - wait_for_resource_ready(InstId, Retry-1) + wait_for_resource_ready(InstId, Retry - 1) end. diff --git a/apps/emqx_connector/test/emqx_connector_mongo_SUITE.erl b/apps/emqx_connector/test/emqx_connector_mongo_SUITE.erl index fa24ae724..33a0397de 100644 --- a/apps/emqx_connector/test/emqx_connector_mongo_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_mongo_SUITE.erl @@ -65,20 +65,24 @@ t_lifecycle(_Config) -> perform_lifecycle_check(PoolName, InitialConfig) -> {ok, #{config := CheckedConfig}} = emqx_resource:check_config(?MONGO_RESOURCE_MOD, InitialConfig), - {ok, #{state := #{poolname := ReturnedPoolName} = State, - status := InitialStatus}} - = emqx_resource:create_local( - PoolName, - ?CONNECTOR_RESOURCE_GROUP, - ?MONGO_RESOURCE_MOD, - CheckedConfig, - #{} - ), + {ok, #{ + state := #{poolname := ReturnedPoolName} = State, + status := InitialStatus + }} = + emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?MONGO_RESOURCE_MOD, + CheckedConfig, + #{} + ), ?assertEqual(InitialStatus, connected), % Instance should match the state and status of the just started resource - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := InitialStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), % % Perform query as further check that the resource is working as expected ?assertMatch([], emqx_resource:query(PoolName, test_query_find())), @@ -86,11 +90,13 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail % as the worker no longer exists. - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := StoppedStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := StoppedStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(StoppedStatus, disconnected), - ?assertEqual({error,health_check_failed}, emqx_resource:health_check(PoolName)), + ?assertEqual({error, health_check_failed}, emqx_resource:health_check(PoolName)), % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), % Can call stop/1 again on an already stopped instance @@ -99,8 +105,8 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ?assertEqual(ok, emqx_resource:restart(PoolName)), % async restart, need to wait resource timer:sleep(500), - {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), ?assertMatch([], emqx_resource:query(PoolName, test_query_find())), ?assertMatch(undefined, emqx_resource:query(PoolName, test_query_find_one())), @@ -115,12 +121,19 @@ perform_lifecycle_check(PoolName, InitialConfig) -> % %%------------------------------------------------------------------------------ mongo_config() -> - RawConfig = list_to_binary(io_lib:format(""" - mongo_type = single - database = mqtt - pool_size = 8 - server = \"~s:~b\" - """, [?MONGO_HOST, ?MONGO_DEFAULT_PORT])), + RawConfig = list_to_binary( + io_lib:format( + "" + "\n" + " mongo_type = single\n" + " database = mqtt\n" + " pool_size = 8\n" + " server = \"~s:~b\"\n" + " " + "", + [?MONGO_HOST, ?MONGO_DEFAULT_PORT] + ) + ), {ok, Config} = hocon:binary(RawConfig), #{<<"config">> => Config}. diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl index 550f264d3..2bb9abd84 100644 --- a/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl @@ -22,23 +22,36 @@ send_and_ack_test() -> %% delegate from gen_rpc to rpc for unit test meck:new(emqtt, [passthrough, no_history]), - meck:expect(emqtt, start_link, 1, - fun(_) -> - {ok, spawn_link(fun() -> ok end)} - end), + meck:expect( + emqtt, + start_link, + 1, + fun(_) -> + {ok, spawn_link(fun() -> ok end)} + end + ), meck:expect(emqtt, connect, 1, {ok, dummy}), - meck:expect(emqtt, stop, 1, - fun(Pid) -> Pid ! stop end), - meck:expect(emqtt, publish, 2, - fun(Client, Msg) -> - Client ! {publish, Msg}, - {ok, Msg} %% as packet id - end), + meck:expect( + emqtt, + stop, + 1, + fun(Pid) -> Pid ! stop end + ), + meck:expect( + emqtt, + publish, + 2, + fun(Client, Msg) -> + Client ! {publish, Msg}, + %% as packet id + {ok, Msg} + end + ), try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127,0,0,1}, 1883}}), - % %% return last packet id as batch reference + {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127, 0, 0, 1}, 1883}}), + % %% return last packet id as batch reference {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), ok = emqx_connector_mqtt_mod:stop(Conn) diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl index cf1a190d4..aff1a92a6 100644 --- a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl @@ -23,13 +23,13 @@ -define(BRIDGE_NAME, test). -define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test). -define(WAIT(PATTERN, TIMEOUT), - receive - PATTERN -> - ok - after - TIMEOUT -> - error(timeout) - end). + receive + PATTERN -> + ok + after TIMEOUT -> + error(timeout) + end +). -export([start/1, send/2, stop/1]). @@ -125,7 +125,7 @@ manual_start_stop_test() -> Ref = make_ref(), TestPid = self(), BridgeName = manual_start_stop, - Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), Config = Config0#{start_type := manual}, {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}), %% call ensure_started again should yield the same result diff --git a/apps/emqx_connector/test/emqx_connector_mysql_SUITE.erl b/apps/emqx_connector/test/emqx_connector_mysql_SUITE.erl index 29ba2c181..c039da168 100644 --- a/apps/emqx_connector/test/emqx_connector_mysql_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_mysql_SUITE.erl @@ -64,9 +64,11 @@ t_lifecycle(_Config) -> perform_lifecycle_check(PoolName, InitialConfig) -> {ok, #{config := CheckedConfig}} = - emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig), - {ok, #{state := #{poolname := ReturnedPoolName} = State, - status := InitialStatus}} = emqx_resource:create_local( + emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig), + {ok, #{ + state := #{poolname := ReturnedPoolName} = State, + status := InitialStatus + }} = emqx_resource:create_local( PoolName, ?CONNECTOR_RESOURCE_GROUP, ?MYSQL_RESOURCE_MOD, @@ -75,23 +77,32 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ), ?assertEqual(InitialStatus, connected), % Instance should match the state and status of the just started resource - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := InitialStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), % % Perform query as further check that the resource is working as expected ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_no_params())), ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())), - ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, - test_query_with_params_and_timeout())), + ?assertMatch( + {ok, _, [[1]]}, + emqx_resource:query( + PoolName, + test_query_with_params_and_timeout() + ) + ), ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail % as the worker no longer exists. - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := StoppedStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := StoppedStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(StoppedStatus, disconnected), - ?assertEqual({error,health_check_failed}, emqx_resource:health_check(PoolName)), + ?assertEqual({error, health_check_failed}, emqx_resource:health_check(PoolName)), % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), % Can call stop/1 again on an already stopped instance @@ -105,8 +116,13 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ?assertEqual(ok, emqx_resource:health_check(PoolName)), ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_no_params())), ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())), - ?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, - test_query_with_params_and_timeout())), + ?assertMatch( + {ok, _, [[1]]}, + emqx_resource:query( + PoolName, + test_query_with_params_and_timeout() + ) + ), % Stop and remove the resource in one go. ?assertEqual(ok, emqx_resource:remove_local(PoolName)), ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), @@ -118,14 +134,21 @@ perform_lifecycle_check(PoolName, InitialConfig) -> % %%------------------------------------------------------------------------------ mysql_config() -> - RawConfig = list_to_binary(io_lib:format(""" - auto_reconnect = true - database = mqtt - username= root - password = public - pool_size = 8 - server = \"~s:~b\" - """, [?MYSQL_HOST, ?MYSQL_DEFAULT_PORT])), + RawConfig = list_to_binary( + io_lib:format( + "" + "\n" + " auto_reconnect = true\n" + " database = mqtt\n" + " username= root\n" + " password = public\n" + " pool_size = 8\n" + " server = \"~s:~b\"\n" + " " + "", + [?MYSQL_HOST, ?MYSQL_DEFAULT_PORT] + ) + ), {ok, Config} = hocon:binary(RawConfig), #{<<"config">> => Config}. diff --git a/apps/emqx_connector/test/emqx_connector_pgsql_SUITE.erl b/apps/emqx_connector/test/emqx_connector_pgsql_SUITE.erl index 0252e0816..b7044ea38 100644 --- a/apps/emqx_connector/test/emqx_connector_pgsql_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_pgsql_SUITE.erl @@ -65,20 +65,24 @@ t_lifecycle(_Config) -> perform_lifecycle_check(PoolName, InitialConfig) -> {ok, #{config := CheckedConfig}} = emqx_resource:check_config(?PGSQL_RESOURCE_MOD, InitialConfig), - {ok, #{state := #{poolname := ReturnedPoolName} = State, - status := InitialStatus}} - = emqx_resource:create_local( - PoolName, - ?CONNECTOR_RESOURCE_GROUP, - ?PGSQL_RESOURCE_MOD, - CheckedConfig, - #{} - ), + {ok, #{ + state := #{poolname := ReturnedPoolName} = State, + status := InitialStatus + }} = + emqx_resource:create_local( + PoolName, + ?CONNECTOR_RESOURCE_GROUP, + ?PGSQL_RESOURCE_MOD, + CheckedConfig, + #{} + ), ?assertEqual(InitialStatus, connected), % Instance should match the state and status of the just started resource - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := InitialStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), % % Perform query as further check that the resource is working as expected ?assertMatch({ok, _, [{1}]}, emqx_resource:query(PoolName, test_query_no_params())), @@ -86,11 +90,13 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail % as the worker no longer exists. - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := StoppedStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := StoppedStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(StoppedStatus, disconnected), - ?assertEqual({error,health_check_failed}, emqx_resource:health_check(PoolName)), + ?assertEqual({error, health_check_failed}, emqx_resource:health_check(PoolName)), % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), % Can call stop/1 again on an already stopped instance @@ -99,8 +105,8 @@ perform_lifecycle_check(PoolName, InitialConfig) -> ?assertEqual(ok, emqx_resource:restart(PoolName)), % async restart, need to wait resource timer:sleep(500), - {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), ?assertMatch({ok, _, [{1}]}, emqx_resource:query(PoolName, test_query_no_params())), ?assertMatch({ok, _, [{1}]}, emqx_resource:query(PoolName, test_query_with_params())), @@ -115,14 +121,21 @@ perform_lifecycle_check(PoolName, InitialConfig) -> % %%------------------------------------------------------------------------------ pgsql_config() -> - RawConfig = list_to_binary(io_lib:format(""" - auto_reconnect = true - database = mqtt - username= root - password = public - pool_size = 8 - server = \"~s:~b\" - """, [?PGSQL_HOST, ?PGSQL_DEFAULT_PORT])), + RawConfig = list_to_binary( + io_lib:format( + "" + "\n" + " auto_reconnect = true\n" + " database = mqtt\n" + " username= root\n" + " password = public\n" + " pool_size = 8\n" + " server = \"~s:~b\"\n" + " " + "", + [?PGSQL_HOST, ?PGSQL_DEFAULT_PORT] + ) + ), {ok, Config} = hocon:binary(RawConfig), #{<<"config">> => Config}. diff --git a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl index 8e473c397..64dd9e683 100644 --- a/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl +++ b/apps/emqx_connector/test/emqx_connector_redis_SUITE.erl @@ -80,8 +80,10 @@ t_sentinel_lifecycle(_Config) -> perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) -> {ok, #{config := CheckedConfig}} = emqx_resource:check_config(?REDIS_RESOURCE_MOD, InitialConfig), - {ok, #{state := #{poolname := ReturnedPoolName} = State, - status := InitialStatus}} = emqx_resource:create_local( + {ok, #{ + state := #{poolname := ReturnedPoolName} = State, + status := InitialStatus + }} = emqx_resource:create_local( PoolName, ?CONNECTOR_RESOURCE_GROUP, ?REDIS_RESOURCE_MOD, @@ -90,20 +92,24 @@ perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) -> ), ?assertEqual(InitialStatus, connected), % Instance should match the state and status of the just started resource - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := InitialStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), % Perform query as further check that the resource is working as expected ?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})), ?assertEqual(ok, emqx_resource:stop(PoolName)), % Resource will be listed still, but state will be changed and healthcheck will fail % as the worker no longer exists. - {ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State, - status := StoppedStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{ + state := State, + status := StoppedStatus + }} = + emqx_resource:get_instance(PoolName), ?assertEqual(StoppedStatus, disconnected), - ?assertEqual({error,health_check_failed}, emqx_resource:health_check(PoolName)), + ?assertEqual({error, health_check_failed}, emqx_resource:health_check(PoolName)), % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), % Can call stop/1 again on an already stopped instance @@ -112,8 +118,8 @@ perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) -> ?assertEqual(ok, emqx_resource:restart(PoolName)), % async restart, need to wait resource timer:sleep(500), - {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} - = emqx_resource:get_instance(PoolName), + {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = + emqx_resource:get_instance(PoolName), ?assertEqual(ok, emqx_resource:health_check(PoolName)), ?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})), % Stop and remove the resource in one go. @@ -136,14 +142,21 @@ redis_config_sentinel() -> redis_config_base("sentinel", "servers"). redis_config_base(Type, ServerKey) -> - RawConfig = list_to_binary(io_lib:format(""" - auto_reconnect = true - database = 1 - pool_size = 8 - redis_type = ~s - password = public - ~s = \"~s:~b\" - """, [Type, ServerKey, ?REDIS_HOST, ?REDIS_PORT])), + RawConfig = list_to_binary( + io_lib:format( + "" + "\n" + " auto_reconnect = true\n" + " database = 1\n" + " pool_size = 8\n" + " redis_type = ~s\n" + " password = public\n" + " ~s = \"~s:~b\"\n" + " " + "", + [Type, ServerKey, ?REDIS_HOST, ?REDIS_PORT] + ) + ), {ok, Config} = hocon:binary(RawConfig), #{<<"config">> => Config}. diff --git a/apps/emqx_connector/test/emqx_connector_test_helpers.erl b/apps/emqx_connector/test/emqx_connector_test_helpers.erl index 9e3fc5257..ea3380e85 100644 --- a/apps/emqx_connector/test/emqx_connector_test_helpers.erl +++ b/apps/emqx_connector/test/emqx_connector_test_helpers.erl @@ -19,10 +19,11 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). --export([ check_fields/1 - , start_apps/1 - , stop_apps/1 - ]). +-export([ + check_fields/1, + start_apps/1, + stop_apps/1 +]). check_fields({FieldName, FieldValue}) -> ?assert(is_atom(FieldName)), @@ -30,10 +31,10 @@ check_fields({FieldName, FieldValue}) -> is_map(FieldValue) -> ct:pal("~p~n", [{FieldName, FieldValue}]), ?assert( - (maps:is_key(type, FieldValue) - andalso maps:is_key(default, FieldValue)) - orelse ((maps:is_key(required, FieldValue) - andalso maps:get(required, FieldValue) =:= false)) + (maps:is_key(type, FieldValue) andalso + maps:is_key(default, FieldValue)) orelse + (maps:is_key(required, FieldValue) andalso + maps:get(required, FieldValue) =:= false) ); true -> ?assert(is_function(FieldValue)) diff --git a/apps/emqx_plugins/rebar.config b/apps/emqx_plugins/rebar.config index 528efecb6..9f17b7657 100644 --- a/apps/emqx_plugins/rebar.config +++ b/apps/emqx_plugins/rebar.config @@ -1,4 +1,5 @@ %% -*- mode: erlang -*- -{deps, [ {emqx, {path, "../emqx"}} - ]}. +{deps, [{emqx, {path, "../emqx"}}]}. + +{project_plugins, [erlfmt]}. diff --git a/apps/emqx_plugins/src/emqx_plugins.app.src b/apps/emqx_plugins/src/emqx_plugins.app.src index cc5b846b1..1635bb516 100644 --- a/apps/emqx_plugins/src/emqx_plugins.app.src +++ b/apps/emqx_plugins/src/emqx_plugins.app.src @@ -1,9 +1,9 @@ %% -*- mode: erlang -*- -{application, emqx_plugins, - [{description, "EMQX Plugin Management"}, - {vsn, "0.1.0"}, - {modules, []}, - {mod, {emqx_plugins_app,[]}}, - {applications, [kernel,stdlib,emqx]}, - {env, []} - ]}. +{application, emqx_plugins, [ + {description, "EMQX Plugin Management"}, + {vsn, "0.1.0"}, + {modules, []}, + {mod, {emqx_plugins_app, []}}, + {applications, [kernel, stdlib, emqx]}, + {env, []} +]}. diff --git a/apps/emqx_plugins/src/emqx_plugins.erl b/apps/emqx_plugins/src/emqx_plugins.erl index 09b7a735e..0376eaf27 100644 --- a/apps/emqx_plugins/src/emqx_plugins.erl +++ b/apps/emqx_plugins/src/emqx_plugins.erl @@ -19,35 +19,37 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ ensure_installed/1 - , ensure_uninstalled/1 - , ensure_enabled/1 - , ensure_enabled/2 - , ensure_disabled/1 - , purge/1 - , delete_package/1 - ]). +-export([ + ensure_installed/1, + ensure_uninstalled/1, + ensure_enabled/1, + ensure_enabled/2, + ensure_disabled/1, + purge/1, + delete_package/1 +]). --export([ ensure_started/0 - , ensure_started/1 - , ensure_stopped/0 - , ensure_stopped/1 - , restart/1 - , list/0 - , describe/1 - , parse_name_vsn/1 - ]). +-export([ + ensure_started/0, + ensure_started/1, + ensure_stopped/0, + ensure_stopped/1, + restart/1, + list/0, + describe/1, + parse_name_vsn/1 +]). --export([ get_config/2 - , put_config/2 - ]). +-export([ + get_config/2, + put_config/2 +]). %% internal --export([ do_ensure_started/1 - ]). +-export([do_ensure_started/1]). -export([ - install_dir/0 - ]). + install_dir/0 +]). -ifdef(TEST). -compile(export_all). @@ -58,8 +60,10 @@ -include_lib("emqx/include/logger.hrl"). -include("emqx_plugins.hrl"). --type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0" --type plugin() :: map(). %% the parse result of the JSON info file +%% "my_plugin-0.1.0" +-type name_vsn() :: binary() | string(). +%% the parse result of the JSON info file +-type plugin() :: map(). -type position() :: no_move | front | rear | {before, name_vsn()} | {behind, name_vsn()}. %%-------------------------------------------------------------------- @@ -86,22 +90,25 @@ do_ensure_installed(NameVsn) -> case erl_tar:extract(TarGz, [{cwd, install_dir()}, compressed]) of ok -> case read_plugin(NameVsn, #{}) of - {ok, _} -> ok; + {ok, _} -> + ok; {error, Reason} -> ?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}), _ = ensure_uninstalled(NameVsn), {error, Reason} end; {error, {_, enoent}} -> - {error, #{ reason => "failed_to_extract_plugin_package" - , path => TarGz - , return => not_found - }}; + {error, #{ + reason => "failed_to_extract_plugin_package", + path => TarGz, + return => not_found + }}; {error, Reason} -> - {error, #{ reason => "bad_plugin_package" - , path => TarGz - , return => Reason - }} + {error, #{ + reason => "bad_plugin_package", + path => TarGz, + return => Reason + }} end. %% @doc Ensure files and directories for the given plugin are delete. @@ -110,13 +117,15 @@ do_ensure_installed(NameVsn) -> ensure_uninstalled(NameVsn) -> case read_plugin(NameVsn, #{}) of {ok, #{running_status := RunningSt}} when RunningSt =/= stopped -> - {error, #{reason => "bad_plugin_running_status", - hint => "stop_the_plugin_first" - }}; + {error, #{ + reason => "bad_plugin_running_status", + hint => "stop_the_plugin_first" + }}; {ok, #{config_status := enabled}} -> - {error, #{reason => "bad_plugin_config_status", - hint => "disable_the_plugin_first" - }}; + {error, #{ + reason => "bad_plugin_config_status", + hint => "disable_the_plugin_first" + }}; _ -> purge(NameVsn) end. @@ -141,9 +150,10 @@ ensure_state(NameVsn, Position, State) when is_binary(NameVsn) -> ensure_state(NameVsn, Position, State) -> case read_plugin(NameVsn, #{}) of {ok, _} -> - Item = #{ name_vsn => NameVsn - , enable => State - }, + Item = #{ + name_vsn => NameVsn, + enable => State + }, tryit("ensure_state", fun() -> ensure_configured(Item, Position) end); {error, Reason} -> {error, Reason} @@ -175,18 +185,19 @@ add_new_configured(Configured, {Action, NameVsn}, Item) -> SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end, {Front, Rear} = lists:splitwith(SplitFun, Configured), Rear =:= [] andalso - throw(#{error => "position_anchor_plugin_not_configured", - hint => "maybe_install_and_configure", - name_vsn => NameVsn - }), + throw(#{ + error => "position_anchor_plugin_not_configured", + hint => "maybe_install_and_configure", + name_vsn => NameVsn + }), case Action of - before -> Front ++ [Item | Rear]; + before -> + Front ++ [Item | Rear]; behind -> [Anchor | Rear0] = Rear, Front ++ [Anchor, Item | Rear0] end. - %% @doc Delete the package file. -spec delete_package(name_vsn()) -> ok. delete_package(NameVsn) -> @@ -198,9 +209,11 @@ delete_package(NameVsn) -> {error, enoent} -> ok; {error, Reason} -> - ?SLOG(error, #{msg => "failed_to_delete_package_file", - path => File, - reason => Reason}), + ?SLOG(error, #{ + msg => "failed_to_delete_package_file", + path => File, + reason => Reason + }), {error, Reason} end. @@ -219,9 +232,11 @@ purge(NameVsn) -> {error, enoent} -> ok; {error, Reason} -> - ?SLOG(error, #{msg => "failed_to_purge_plugin_dir", - dir => Dir, - reason => Reason}), + ?SLOG(error, #{ + msg => "failed_to_purge_plugin_dir", + dir => Dir, + reason => Reason + }), {error, Reason} end. @@ -235,10 +250,13 @@ ensure_started() -> -spec ensure_started(name_vsn()) -> ok | {error, term()}. ensure_started(NameVsn) -> case do_ensure_started(NameVsn) of - ok -> ok; + ok -> + ok; {error, Reason} -> - ?SLOG(alert, #{msg => "failed_to_start_plugin", - reason => Reason}), + ?SLOG(alert, #{ + msg => "failed_to_start_plugin", + reason => Reason + }), {error, Reason} end. @@ -250,11 +268,13 @@ ensure_stopped() -> %% @doc Stop a plugin from Management API or CLI. -spec ensure_stopped(name_vsn()) -> ok | {error, term()}. ensure_stopped(NameVsn) -> - tryit("stop_plugin", - fun() -> - Plugin = do_read_plugin(NameVsn), - ensure_apps_stopped(Plugin) - end). + tryit( + "stop_plugin", + fun() -> + Plugin = do_read_plugin(NameVsn), + ensure_apps_stopped(Plugin) + end + ). %% @doc Stop and then start the plugin. restart(NameVsn) -> @@ -269,39 +289,45 @@ restart(NameVsn) -> list() -> Pattern = filename:join([install_dir(), "*", "release.json"]), All = lists:filtermap( - fun(JsonFile) -> - case read_plugin({file, JsonFile}, #{}) of - {ok, Info} -> - {true, Info}; - {error, Reason} -> - ?SLOG(warning, Reason), - false - end - end, filelib:wildcard(Pattern)), + fun(JsonFile) -> + case read_plugin({file, JsonFile}, #{}) of + {ok, Info} -> + {true, Info}; + {error, Reason} -> + ?SLOG(warning, Reason), + false + end + end, + filelib:wildcard(Pattern) + ), list(configured(), All). %% Make sure configured ones are ordered in front. -list([], All) -> All; +list([], All) -> + All; list([#{name_vsn := NameVsn} | Rest], All) -> SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> - bin([Name, "-", Vsn]) =/= bin(NameVsn) - end, + bin([Name, "-", Vsn]) =/= bin(NameVsn) + end, case lists:splitwith(SplitF, All) of {_, []} -> - ?SLOG(warning, #{msg => "configured_plugin_not_installed", - name_vsn => NameVsn - }), + ?SLOG(warning, #{ + msg => "configured_plugin_not_installed", + name_vsn => NameVsn + }), list(Rest, All); {Front, [I | Rear]} -> [I | list(Rest, Front ++ Rear)] end. do_ensure_started(NameVsn) -> - tryit("start_plugins", - fun() -> - Plugin = do_read_plugin(NameVsn), - ok = load_code_start_apps(NameVsn, Plugin) - end). + tryit( + "start_plugins", + fun() -> + Plugin = do_read_plugin(NameVsn), + ok = load_code_start_apps(NameVsn, Plugin) + end + ). %% try the function, catch 'throw' exceptions as normal 'error' return %% other exceptions with stacktrace returned. @@ -309,25 +335,28 @@ tryit(WhichOp, F) -> try F() catch - throw : Reason -> + throw:Reason -> %% thrown exceptions are known errors %% translate to a return value without stacktrace {error, Reason}; - error : Reason : Stacktrace -> + error:Reason:Stacktrace -> %% unexpected errors, log stacktrace - ?SLOG(warning, #{ msg => "plugin_op_failed" - , which_op => WhichOp - , exception => Reason - , stacktrace => Stacktrace - }), + ?SLOG(warning, #{ + msg => "plugin_op_failed", + which_op => WhichOp, + exception => Reason, + stacktrace => Stacktrace + }), {error, {failed, WhichOp}} end. %% read plugin info from the JSON file %% returns {ok, Info} or {error, Reason} read_plugin(NameVsn, Options) -> - tryit("read_plugin_info", - fun() -> {ok, do_read_plugin(NameVsn, Options)} end). + tryit( + "read_plugin_info", + fun() -> {ok, do_read_plugin(NameVsn, Options)} end + ). do_read_plugin(Plugin) -> do_read_plugin(Plugin, #{}). @@ -339,10 +368,11 @@ do_read_plugin({file, InfoFile}, Options) -> Info1 = plugins_readme(NameVsn, Options, Info0), plugin_status(NameVsn, Info1); {error, Reason} -> - throw(#{error => "bad_info_file", - path => InfoFile, - return => Reason - }) + throw(#{ + error => "bad_info_file", + path => InfoFile, + return => Reason + }) end; do_read_plugin(NameVsn, Options) -> do_read_plugin({file, info_file(NameVsn)}, Options). @@ -352,7 +382,8 @@ plugins_readme(NameVsn, #{fill_readme := true}, Info) -> {ok, Bin} -> Info#{readme => Bin}; _ -> Info#{readme => <<>>} end; -plugins_readme(_NameVsn, _Options, Info) -> Info. +plugins_readme(_NameVsn, _Options, Info) -> + Info. plugin_status(NameVsn, Info) -> {AppName, _AppVsn} = parse_name_vsn(NameVsn), @@ -368,74 +399,91 @@ plugin_status(NameVsn, Info) -> end, Configured = lists:filtermap( fun(#{name_vsn := Nv, enable := St}) -> - case bin(Nv) =:= bin(NameVsn) of - true -> {true, St}; - false -> false - end - end, configured()), - ConfSt = case Configured of - [] -> not_configured; - [true] -> enabled; - [false] -> disabled - end, - Info#{ running_status => RunningSt - , config_status => ConfSt + case bin(Nv) =:= bin(NameVsn) of + true -> {true, St}; + false -> false + end + end, + configured() + ), + ConfSt = + case Configured of + [] -> not_configured; + [true] -> enabled; + [false] -> disabled + end, + Info#{ + running_status => RunningSt, + config_status => ConfSt }. bin(A) when is_atom(A) -> atom_to_binary(A, utf8); bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8); bin(B) when is_binary(B) -> B. -check_plugin(#{ <<"name">> := Name - , <<"rel_vsn">> := Vsn - , <<"rel_apps">> := Apps - , <<"description">> := _ - } = Info, NameVsn, File) -> +check_plugin( + #{ + <<"name">> := Name, + <<"rel_vsn">> := Vsn, + <<"rel_apps">> := Apps, + <<"description">> := _ + } = Info, + NameVsn, + File +) -> case bin(NameVsn) =:= bin([Name, "-", Vsn]) of true -> try - [_ | _ ] = Apps, %% assert + %% assert + [_ | _] = Apps, %% validate if the list is all - strings lists:foreach(fun parse_name_vsn/1, Apps) catch - _ : _ -> - throw(#{ error => "bad_rel_apps" - , rel_apps => Apps - , hint => "A non-empty string list of app_name-app_vsn format" - }) + _:_ -> + throw(#{ + error => "bad_rel_apps", + rel_apps => Apps, + hint => "A non-empty string list of app_name-app_vsn format" + }) end, Info; false -> - throw(#{ error => "name_vsn_mismatch" - , name_vsn => NameVsn - , path => File - , name => Name - , rel_vsn => Vsn - }) + throw(#{ + error => "name_vsn_mismatch", + name_vsn => NameVsn, + path => File, + name => Name, + rel_vsn => Vsn + }) end; check_plugin(_What, NameVsn, File) -> - throw(#{ error => "bad_info_file_content" - , mandatory_fields => [rel_vsn, name, rel_apps, description] - , name_vsn => NameVsn - , path => File - }). + throw(#{ + error => "bad_info_file_content", + mandatory_fields => [rel_vsn, name, rel_apps, description], + name_vsn => NameVsn, + path => File + }). load_code_start_apps(RelNameVsn, #{<<"rel_apps">> := Apps}) -> LibDir = filename:join([install_dir(), RelNameVsn]), RunningApps = running_apps(), %% load plugin apps and beam code AppNames = - lists:map(fun(AppNameVsn) -> - {AppName, AppVsn} = parse_name_vsn(AppNameVsn), - EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]), - ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps), - AppName - end, Apps), + lists:map( + fun(AppNameVsn) -> + {AppName, AppVsn} = parse_name_vsn(AppNameVsn), + EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]), + ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps), + AppName + end, + Apps + ), lists:foreach(fun start_app/1, AppNames). load_plugin_app(AppName, AppVsn, Ebin, RunningApps) -> case lists:keyfind(AppName, 1, RunningApps) of - false -> do_load_plugin_app(AppName, Ebin); + false -> + do_load_plugin_app(AppName, Ebin); {_, Vsn} -> case bin(Vsn) =:= bin(AppVsn) of true -> @@ -443,10 +491,12 @@ load_plugin_app(AppName, AppVsn, Ebin, RunningApps) -> ok; false -> %% running but a different version - ?SLOG(warning, #{msg => "plugin_app_already_running", name => AppName, - running_vsn => Vsn, - loading_vsn => AppVsn - }) + ?SLOG(warning, #{ + msg => "plugin_app_already_running", + name => AppName, + running_vsn => Vsn, + loading_vsn => AppVsn + }) end end. @@ -457,21 +507,31 @@ do_load_plugin_app(AppName, Ebin) -> Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])), lists:foreach( fun(BeamFile) -> - Module = list_to_atom(filename:basename(BeamFile, ".beam")), - case code:load_file(Module) of - {module, _} -> ok; - {error, Reason} -> throw(#{error => "failed_to_load_plugin_beam", - path => BeamFile, - reason => Reason - }) - end - end, Modules), + Module = list_to_atom(filename:basename(BeamFile, ".beam")), + case code:load_file(Module) of + {module, _} -> + ok; + {error, Reason} -> + throw(#{ + error => "failed_to_load_plugin_beam", + path => BeamFile, + reason => Reason + }) + end + end, + Modules + ), case application:load(AppName) of - ok -> ok; - {error, {already_loaded, _}} -> ok; - {error, Reason} -> throw(#{error => "failed_to_load_plugin_app", - name => AppName, - reason => Reason}) + ok -> + ok; + {error, {already_loaded, _}} -> + ok; + {error, Reason} -> + throw(#{ + error => "failed_to_load_plugin_app", + name => AppName, + reason => Reason + }) end. start_app(App) -> @@ -484,11 +544,12 @@ start_app(App) -> ?SLOG(debug, #{msg => "started_plugin_app", app => App}), ok; {error, {ErrApp, Reason}} -> - throw(#{error => "failed_to_start_plugin_app", - app => App, - err_app => ErrApp, - reason => Reason - }) + throw(#{ + error => "failed_to_start_plugin_app", + app => App, + err_app => ErrApp, + reason => Reason + }) end. %% Stop all apps installed by the plugin package, @@ -496,18 +557,22 @@ start_app(App) -> ensure_apps_stopped(#{<<"rel_apps">> := Apps}) -> %% load plugin apps and beam code AppsToStop = - lists:map(fun(NameVsn) -> - {AppName, _AppVsn} = parse_name_vsn(NameVsn), - AppName - end, Apps), + lists:map( + fun(NameVsn) -> + {AppName, _AppVsn} = parse_name_vsn(NameVsn), + AppName + end, + Apps + ), case tryit("stop_apps", fun() -> stop_apps(AppsToStop) end) of {ok, []} -> %% all apps stopped ok; {ok, Left} -> - ?SLOG(warning, #{msg => "unabled_to_stop_plugin_apps", - apps => Left - }), + ?SLOG(warning, #{ + msg => "unabled_to_stop_plugin_apps", + apps => Left + }), ok; {error, Reason} -> {error, Reason} @@ -516,9 +581,12 @@ ensure_apps_stopped(#{<<"rel_apps">> := Apps}) -> stop_apps(Apps) -> RunningApps = running_apps(), case do_stop_apps(Apps, [], RunningApps) of - {ok, []} -> {ok, []}; %% all stopped - {ok, Remain} when Remain =:= Apps -> {ok, Apps}; %% no progress - {ok, Remain} -> stop_apps(Remain) %% try again + %% all stopped + {ok, []} -> {ok, []}; + %% no progress + {ok, Remain} when Remain =:= Apps -> {ok, Apps}; + %% try again + {ok, Remain} -> stop_apps(Remain) end. do_stop_apps([], Remain, _AllApps) -> @@ -553,11 +621,15 @@ unload_moudle_and_app(App) -> ok. is_needed_by_any(AppToStop, RunningApps) -> - lists:any(fun({RunningApp, _RunningAppVsn}) -> - is_needed_by(AppToStop, RunningApp) - end, RunningApps). + lists:any( + fun({RunningApp, _RunningAppVsn}) -> + is_needed_by(AppToStop, RunningApp) + end, + RunningApps + ). -is_needed_by(AppToStop, AppToStop) -> false; +is_needed_by(AppToStop, AppToStop) -> + false; is_needed_by(AppToStop, RunningApp) -> case application:get_key(RunningApp, applications) of {ok, Deps} -> lists:member(AppToStop, Deps); @@ -577,7 +649,8 @@ bin_key(Map) when is_map(Map) -> maps:fold(fun(K, V, Acc) -> Acc#{bin(K) => V} end, #{}, Map); bin_key(List = [#{} | _]) -> lists:map(fun(M) -> bin_key(M) end, List); -bin_key(Term) -> Term. +bin_key(Term) -> + Term. get_config(Key, Default) when is_atom(Key) -> get_config([Key], Default); @@ -604,8 +677,10 @@ for_plugin(#{name_vsn := NameVsn, enable := true}, Fun) -> {error, Reason} -> [{NameVsn, Reason}] end; for_plugin(#{name_vsn := NameVsn, enable := false}, _Fun) -> - ?SLOG(debug, #{msg => "plugin_disabled", - name_vsn => NameVsn}), + ?SLOG(debug, #{ + msg => "plugin_disabled", + name_vsn => NameVsn + }), []. parse_name_vsn(NameVsn) when is_binary(NameVsn) -> @@ -627,6 +702,9 @@ readme_file(NameVsn) -> filename:join([dir(NameVsn), "README.md"]). running_apps() -> - lists:map(fun({N, _, V}) -> - {N, V} - end, application:which_applications(infinity)). + lists:map( + fun({N, _, V}) -> + {N, V} + end, + application:which_applications(infinity) + ). diff --git a/apps/emqx_plugins/src/emqx_plugins_app.erl b/apps/emqx_plugins/src/emqx_plugins_app.erl index 70fab549f..5d3828fb5 100644 --- a/apps/emqx_plugins/src/emqx_plugins_app.erl +++ b/apps/emqx_plugins/src/emqx_plugins_app.erl @@ -18,12 +18,14 @@ -behaviour(application). --export([ start/2 - , stop/1 - ]). +-export([ + start/2, + stop/1 +]). start(_Type, _Args) -> - ok = emqx_plugins:ensure_started(), %% load all pre-configured + %% load all pre-configured + ok = emqx_plugins:ensure_started(), {ok, Sup} = emqx_plugins_sup:start_link(), {ok, Sup}. diff --git a/apps/emqx_plugins/src/emqx_plugins_cli.erl b/apps/emqx_plugins/src/emqx_plugins_cli.erl index 6e9f1dca2..2ea965ce7 100644 --- a/apps/emqx_plugins/src/emqx_plugins_cli.erl +++ b/apps/emqx_plugins/src/emqx_plugins_cli.erl @@ -16,21 +16,23 @@ -module(emqx_plugins_cli). --export([ list/1 - , describe/2 - , ensure_installed/2 - , ensure_uninstalled/2 - , ensure_started/2 - , ensure_stopped/2 - , restart/2 - , ensure_disabled/2 - , ensure_enabled/3 - ]). +-export([ + list/1, + describe/2, + ensure_installed/2, + ensure_uninstalled/2, + ensure_started/2, + ensure_stopped/2, + restart/2, + ensure_disabled/2, + ensure_enabled/3 +]). -include_lib("emqx/include/logger.hrl"). -define(PRINT(EXPR, LOG_FUN), - print(NameVsn, fun()-> EXPR end(), LOG_FUN, ?FUNCTION_NAME)). + print(NameVsn, fun() -> EXPR end(), LOG_FUN, ?FUNCTION_NAME) +). list(LogFun) -> LogFun("~ts~n", [to_json(emqx_plugins:list())]). @@ -43,9 +45,11 @@ describe(NameVsn, LogFun) -> %% this should not happen unless the package is manually installed %% corrupted packages installed from emqx_plugins:ensure_installed %% should not leave behind corrupted files - ?SLOG(error, #{msg => "failed_to_describe_plugin", - name_vsn => NameVsn, - cause => Reason}), + ?SLOG(error, #{ + msg => "failed_to_describe_plugin", + name_vsn => NameVsn, + cause => Reason + }), %% do nothing to the CLI console ok end. @@ -75,14 +79,18 @@ to_json(Input) -> emqx_logger_jsonfmt:best_effort_json(Input). print(NameVsn, Res, LogFun, Action) -> - Obj = #{action => Action, - name_vsn => NameVsn}, + Obj = #{ + action => Action, + name_vsn => NameVsn + }, JsonReady = case Res of ok -> Obj#{result => ok}; {error, Reason} -> - Obj#{result => not_ok, - cause => Reason} + Obj#{ + result => not_ok, + cause => Reason + } end, LogFun("~ts~n", [to_json(JsonReady)]). diff --git a/apps/emqx_plugins/src/emqx_plugins_schema.erl b/apps/emqx_plugins/src/emqx_plugins_schema.erl index eed85558c..ceb8b992f 100644 --- a/apps/emqx_plugins/src/emqx_plugins_schema.erl +++ b/apps/emqx_plugins/src/emqx_plugins_schema.erl @@ -18,10 +18,11 @@ -behaviour(hocon_schema). --export([ roots/0 - , fields/1 - , namespace/0 - ]). +-export([ + roots/0, + fields/1, + namespace/0 +]). -include_lib("hocon/include/hoconsc.hrl"). -include("emqx_plugins.hrl"). @@ -31,31 +32,41 @@ namespace() -> "plugin". roots() -> [?CONF_ROOT]. fields(?CONF_ROOT) -> - #{fields => root_fields(), - desc => ?DESC(?CONF_ROOT) - }; + #{ + fields => root_fields(), + desc => ?DESC(?CONF_ROOT) + }; fields(state) -> - #{ fields => state_fields(), - desc => ?DESC(state) - }. + #{ + fields => state_fields(), + desc => ?DESC(state) + }. state_fields() -> - [ {name_vsn, - hoconsc:mk(string(), - #{ desc => ?DESC(name_vsn) - , required => true - })} - , {enable, - hoconsc:mk(boolean(), - #{ desc => ?DESC(enable) - , required => true - })} + [ + {name_vsn, + hoconsc:mk( + string(), + #{ + desc => ?DESC(name_vsn), + required => true + } + )}, + {enable, + hoconsc:mk( + boolean(), + #{ + desc => ?DESC(enable), + required => true + } + )} ]. root_fields() -> - [ {states, fun states/1} - , {install_dir, fun install_dir/1} - , {check_interval, fun check_interval/1} + [ + {states, fun states/1}, + {install_dir, fun install_dir/1}, + {check_interval, fun check_interval/1} ]. states(type) -> hoconsc:array(hoconsc:ref(?MODULE, state)); @@ -66,7 +77,8 @@ states(_) -> undefined. install_dir(type) -> string(); install_dir(required) -> false; -install_dir(default) -> "plugins"; %% runner's root dir +%% runner's root dir +install_dir(default) -> "plugins"; install_dir(T) when T =/= desc -> undefined; install_dir(desc) -> ?DESC(install_dir). diff --git a/apps/emqx_plugins/src/emqx_plugins_sup.erl b/apps/emqx_plugins/src/emqx_plugins_sup.erl index 488372cc6..687e5d39e 100644 --- a/apps/emqx_plugins/src/emqx_plugins_sup.erl +++ b/apps/emqx_plugins/src/emqx_plugins_sup.erl @@ -29,7 +29,8 @@ init([]) -> %% TODO: Add monitor plugins change. Monitor = emqx_plugins_monitor, _Children = [ - #{id => Monitor, + #{ + id => Monitor, start => {Monitor, start_link, []}, restart => permanent, shutdown => brutal_kill, diff --git a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl index 00c8b4226..317519124 100644 --- a/apps/emqx_plugins/test/emqx_plugins_SUITE.erl +++ b/apps/emqx_plugins/test/emqx_plugins_SUITE.erl @@ -48,9 +48,12 @@ end_per_suite(Config) -> init_per_testcase(TestCase, Config) -> emqx_plugins:put_configured([]), - lists:foreach(fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> - emqx_plugins:purge(bin([Name, "-", Vsn])) - end, emqx_plugins:list()), + lists:foreach( + fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> + emqx_plugins:purge(bin([Name, "-", Vsn])) + end, + emqx_plugins:list() + ), ?MODULE:TestCase({init, Config}). end_per_testcase(TestCase, Config) -> @@ -59,35 +62,46 @@ end_per_testcase(TestCase, Config) -> build_demo_plugin_package() -> build_demo_plugin_package( - #{ target_path => "_build/default/emqx_plugrel" - , release_name => "emqx_plugin_template" - , git_url => "https://github.com/emqx/emqx-plugin-template.git" - , vsn => ?EMQX_PLUGIN_TEMPLATE_VSN - , workdir => "demo_src" - , shdir => emqx_plugins:install_dir() - }). + #{ + target_path => "_build/default/emqx_plugrel", + release_name => "emqx_plugin_template", + git_url => "https://github.com/emqx/emqx-plugin-template.git", + vsn => ?EMQX_PLUGIN_TEMPLATE_VSN, + workdir => "demo_src", + shdir => emqx_plugins:install_dir() + } + ). -build_demo_plugin_package(#{ target_path := TargetPath - , release_name := ReleaseName - , git_url := GitUrl - , vsn := PluginVsn - , workdir := DemoWorkDir - , shdir := WorkDir - } = Opts) -> +build_demo_plugin_package( + #{ + target_path := TargetPath, + release_name := ReleaseName, + git_url := GitUrl, + vsn := PluginVsn, + workdir := DemoWorkDir, + shdir := WorkDir + } = Opts +) -> BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]), - Cmd = string:join([ BuildSh - , PluginVsn - , TargetPath - , ReleaseName - , GitUrl - , DemoWorkDir - ], - " "), + Cmd = string:join( + [ + BuildSh, + PluginVsn, + TargetPath, + ReleaseName, + GitUrl, + DemoWorkDir + ], + " " + ), case emqx_run_sh:do(Cmd, [{cd, WorkDir}]) of {ok, _} -> - Pkg = filename:join([WorkDir, ReleaseName ++ "-" ++ - PluginVsn ++ - ?PACKAGE_SUFFIX]), + Pkg = filename:join([ + WorkDir, + ReleaseName ++ "-" ++ + PluginVsn ++ + ?PACKAGE_SUFFIX + ]), case filelib:is_regular(Pkg) of true -> Opts#{package => Pkg}; false -> error(#{reason => unexpected_build_result, not_found => Pkg}) @@ -104,16 +118,19 @@ bin(B) when is_binary(B) -> B. t_demo_install_start_stop_uninstall({init, Config}) -> Opts = #{package := Package} = build_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), - [ {name_vsn, NameVsn} - , {plugin_opts, Opts} - | Config + [ + {name_vsn, NameVsn}, + {plugin_opts, Opts} + | Config ]; -t_demo_install_start_stop_uninstall({'end', _Config}) -> ok; +t_demo_install_start_stop_uninstall({'end', _Config}) -> + ok; t_demo_install_start_stop_uninstall(Config) -> NameVsn = proplists:get_value(name_vsn, Config), - #{ release_name := ReleaseName - , vsn := PluginVsn - } = proplists:get_value(plugin_opts, Config), + #{ + release_name := ReleaseName, + vsn := PluginVsn + } = proplists:get_value(plugin_opts, Config), ok = emqx_plugins:ensure_installed(NameVsn), %% idempotent ok = emqx_plugins:ensure_installed(NameVsn), @@ -129,8 +146,10 @@ t_demo_install_start_stop_uninstall(Config) -> ok = assert_app_running(map_sets, true), %% running app can not be un-installed - ?assertMatch({error, _}, - emqx_plugins:ensure_uninstalled(NameVsn)), + ?assertMatch( + {error, _}, + emqx_plugins:ensure_uninstalled(NameVsn) + ), %% stop ok = emqx_plugins:ensure_stopped(NameVsn), @@ -143,9 +162,15 @@ t_demo_install_start_stop_uninstall(Config) -> %% still listed after stopped ReleaseNameBin = list_to_binary(ReleaseName), PluginVsnBin = list_to_binary(PluginVsn), - ?assertMatch([#{<<"name">> := ReleaseNameBin, - <<"rel_vsn">> := PluginVsnBin - }], emqx_plugins:list()), + ?assertMatch( + [ + #{ + <<"name">> := ReleaseNameBin, + <<"rel_vsn">> := PluginVsnBin + } + ], + emqx_plugins:list() + ), ok = emqx_plugins:ensure_uninstalled(NameVsn), ?assertEqual([], emqx_plugins:list()), ok. @@ -164,23 +189,29 @@ t_position({init, Config}) -> #{package := Package} = build_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [{name_vsn, NameVsn} | Config]; -t_position({'end', _Config}) -> ok; +t_position({'end', _Config}) -> + ok; t_position(Config) -> NameVsn = proplists:get_value(name_vsn, Config), ok = emqx_plugins:ensure_installed(NameVsn), ok = emqx_plugins:ensure_enabled(NameVsn), - FakeInfo = "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"]," - "description=\"desc fake position app\"", + FakeInfo = + "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"]," + "description=\"desc fake position app\"", PosApp2 = <<"position-2">>, ok = write_info_file(Config, PosApp2, FakeInfo), %% fake a disabled plugin in config ok = emqx_plugins:ensure_state(PosApp2, {before, NameVsn}, false), ListFun = fun() -> - lists:map(fun( - #{<<"name">> := Name, <<"rel_vsn">> := Vsn}) -> - <> - end, emqx_plugins:list()) - end, + lists:map( + fun( + #{<<"name">> := Name, <<"rel_vsn">> := Vsn} + ) -> + <> + end, + emqx_plugins:list() + ) + end, ?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()), emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}), ?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()), @@ -197,13 +228,15 @@ t_start_restart_and_stop({init, Config}) -> #{package := Package} = build_demo_plugin_package(), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), [{name_vsn, NameVsn} | Config]; -t_start_restart_and_stop({'end', _Config}) -> ok; +t_start_restart_and_stop({'end', _Config}) -> + ok; t_start_restart_and_stop(Config) -> NameVsn = proplists:get_value(name_vsn, Config), ok = emqx_plugins:ensure_installed(NameVsn), ok = emqx_plugins:ensure_enabled(NameVsn), - FakeInfo = "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"]," - "description=\"desc bar\"", + FakeInfo = + "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"]," + "description=\"desc bar\"", Bar2 = <<"bar-2">>, ok = write_info_file(Config, Bar2, FakeInfo), %% fake a disabled plugin in config @@ -216,8 +249,10 @@ t_start_restart_and_stop(Config) -> %% fake enable bar-2 ok = emqx_plugins:ensure_state(Bar2, rear, true), %% should cause an error - ?assertError(#{function := _, errors := [_ | _]}, - emqx_plugins:ensure_started()), + ?assertError( + #{function := _, errors := [_ | _]}, + emqx_plugins:ensure_started() + ), %% but demo plugin should still be running assert_app_running(emqx_plugin_template, true), @@ -255,9 +290,13 @@ t_enable_disable(Config) -> ?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()), ok = emqx_plugins:ensure_enabled(bin(NameVsn)), ?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()), - ?assertMatch({error, #{reason := "bad_plugin_config_status", - hint := "disable_the_plugin_first" - }}, emqx_plugins:ensure_uninstalled(NameVsn)), + ?assertMatch( + {error, #{ + reason := "bad_plugin_config_status", + hint := "disable_the_plugin_first" + }}, + emqx_plugins:ensure_uninstalled(NameVsn) + ), ok = emqx_plugins:ensure_disabled(bin(NameVsn)), ok = emqx_plugins:ensure_uninstalled(NameVsn), ?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)), @@ -271,20 +310,28 @@ assert_app_running(Name, false) -> AllApps = application:which_applications(), ?assertEqual(false, lists:keyfind(Name, 1, AllApps)). -t_bad_tar_gz({init, Config}) -> Config; -t_bad_tar_gz({'end', _Config}) -> ok; +t_bad_tar_gz({init, Config}) -> + Config; +t_bad_tar_gz({'end', _Config}) -> + ok; t_bad_tar_gz(Config) -> WorkDir = proplists:get_value(data_dir, Config), FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]), ok = file:write_file(FakeTarTz, "a\n"), - ?assertMatch({error, #{reason := "bad_plugin_package", - return := eof - }}, - emqx_plugins:ensure_installed("fake-vsn")), - ?assertMatch({error, #{reason := "failed_to_extract_plugin_package", - return := not_found - }}, - emqx_plugins:ensure_installed("nonexisting")), + ?assertMatch( + {error, #{ + reason := "bad_plugin_package", + return := eof + }}, + emqx_plugins:ensure_installed("fake-vsn") + ), + ?assertMatch( + {error, #{ + reason := "failed_to_extract_plugin_package", + return := not_found + }}, + emqx_plugins:ensure_installed("nonexisting") + ), ?assertEqual([], emqx_plugins:list()), ok = emqx_plugins:delete_package("fake-vsn"), %% idempotent @@ -292,8 +339,10 @@ t_bad_tar_gz(Config) -> %% create a corrupted .tar.gz %% failed install attempts should not leave behind extracted dir -t_bad_tar_gz2({init, Config}) -> Config; -t_bad_tar_gz2({'end', _Config}) -> ok; +t_bad_tar_gz2({init, Config}) -> + Config; +t_bad_tar_gz2({'end', _Config}) -> + ok; t_bad_tar_gz2(Config) -> WorkDir = proplists:get_value(data_dir, Config), NameVsn = "foo-0.2", @@ -310,45 +359,57 @@ t_bad_tar_gz2(Config) -> ?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))), ok = emqx_plugins:delete_package(NameVsn). -t_bad_info_json({init, Config}) -> Config; -t_bad_info_json({'end', _}) -> ok; +t_bad_info_json({init, Config}) -> + Config; +t_bad_info_json({'end', _}) -> + ok; t_bad_info_json(Config) -> NameVsn = "test-2", ok = write_info_file(Config, NameVsn, "bad-syntax"), - ?assertMatch({error, #{error := "bad_info_file", - return := {parse_error, _} - }}, - emqx_plugins:describe(NameVsn)), + ?assertMatch( + {error, #{ + error := "bad_info_file", + return := {parse_error, _} + }}, + emqx_plugins:describe(NameVsn) + ), ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"), - ?assertMatch({error, #{error := "bad_info_file_content", - mandatory_fields := _ - }}, - emqx_plugins:describe(NameVsn)), + ?assertMatch( + {error, #{ + error := "bad_info_file_content", + mandatory_fields := _ + }}, + emqx_plugins:describe(NameVsn) + ), ?assertEqual([], emqx_plugins:list()), emqx_plugins:purge(NameVsn), ok. t_elixir_plugin({init, Config}) -> Opts0 = - #{ target_path => "_build/prod/plugrelex/elixir_plugin_template" - , release_name => "elixir_plugin_template" - , git_url => "https://github.com/emqx/emqx-elixir-plugin.git" - , vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN - , workdir => "demo_src_elixir" - , shdir => emqx_plugins:install_dir() - }, + #{ + target_path => "_build/prod/plugrelex/elixir_plugin_template", + release_name => "elixir_plugin_template", + git_url => "https://github.com/emqx/emqx-elixir-plugin.git", + vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN, + workdir => "demo_src_elixir", + shdir => emqx_plugins:install_dir() + }, Opts = #{package := Package} = build_demo_plugin_package(Opts0), NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX), - [ {name_vsn, NameVsn} - , {plugin_opts, Opts} - | Config + [ + {name_vsn, NameVsn}, + {plugin_opts, Opts} + | Config ]; -t_elixir_plugin({'end', _Config}) -> ok; +t_elixir_plugin({'end', _Config}) -> + ok; t_elixir_plugin(Config) -> NameVsn = proplists:get_value(name_vsn, Config), - #{ release_name := ReleaseName - , vsn := PluginVsn - } = proplists:get_value(plugin_opts, Config), + #{ + release_name := ReleaseName, + vsn := PluginVsn + } = proplists:get_value(plugin_opts, Config), ok = emqx_plugins:ensure_installed(NameVsn), %% idempotent ok = emqx_plugins:ensure_installed(NameVsn), @@ -368,8 +429,10 @@ t_elixir_plugin(Config) -> 3 = 'Elixir.Kernel':'+'(1, 2), %% running app can not be un-installed - ?assertMatch({error, _}, - emqx_plugins:ensure_uninstalled(NameVsn)), + ?assertMatch( + {error, _}, + emqx_plugins:ensure_uninstalled(NameVsn) + ), %% stop ok = emqx_plugins:ensure_stopped(NameVsn), @@ -382,9 +445,15 @@ t_elixir_plugin(Config) -> %% still listed after stopped ReleaseNameBin = list_to_binary(ReleaseName), PluginVsnBin = list_to_binary(PluginVsn), - ?assertMatch([#{<<"name">> := ReleaseNameBin, - <<"rel_vsn">> := PluginVsnBin - }], emqx_plugins:list()), + ?assertMatch( + [ + #{ + <<"name">> := ReleaseNameBin, + <<"rel_vsn">> := PluginVsnBin + } + ], + emqx_plugins:list() + ), ok = emqx_plugins:ensure_uninstalled(NameVsn), ?assertEqual([], emqx_plugins:list()), ok. diff --git a/apps/emqx_plugins/test/emqx_plugins_tests.erl b/apps/emqx_plugins/test/emqx_plugins_tests.erl index f118cbb28..cda6dbf0f 100644 --- a/apps/emqx_plugins/test/emqx_plugins_tests.erl +++ b/apps/emqx_plugins/test/emqx_plugins_tests.erl @@ -23,23 +23,26 @@ ensure_configured_test_todo() -> meck_emqx(), - try test_ensure_configured() - after emqx_plugins:put_configured([]) + try + test_ensure_configured() + after + emqx_plugins:put_configured([]) end, meck:unload(emqx). - test_ensure_configured() -> ok = emqx_plugins:put_configured([]), - P1 =#{name_vsn => "p-1", enable => true}, - P2 =#{name_vsn => "p-2", enable => true}, - P3 =#{name_vsn => "p-3", enable => false}, + P1 = #{name_vsn => "p-1", enable => true}, + P2 = #{name_vsn => "p-2", enable => true}, + P3 = #{name_vsn => "p-3", enable => false}, emqx_plugins:ensure_configured(P1, front), emqx_plugins:ensure_configured(P2, {before, <<"p-1">>}), emqx_plugins:ensure_configured(P3, {before, <<"p-1">>}), ?assertEqual([P2, P3, P1], emqx_plugins:configured()), - ?assertThrow(#{error := "position_anchor_plugin_not_configured"}, - emqx_plugins:ensure_configured(P3, {before, <<"unknown-x">>})). + ?assertThrow( + #{error := "position_anchor_plugin_not_configured"}, + emqx_plugins:ensure_configured(P3, {before, <<"unknown-x">>}) + ). read_plugin_test() -> meck_emqx(), @@ -47,16 +50,20 @@ read_plugin_test() -> fun(_Dir) -> NameVsn = "bar-5", InfoFile = emqx_plugins:info_file(NameVsn), - FakeInfo = "name=bar, rel_vsn=\"5\", rel_apps=[justname_no_vsn]," - "description=\"desc bar\"", + FakeInfo = + "name=bar, rel_vsn=\"5\", rel_apps=[justname_no_vsn]," + "description=\"desc bar\"", try ok = write_file(InfoFile, FakeInfo), - ?assertMatch({error, #{error := "bad_rel_apps"}}, - emqx_plugins:read_plugin(NameVsn, #{})) + ?assertMatch( + {error, #{error := "bad_rel_apps"}}, + emqx_plugins:read_plugin(NameVsn, #{}) + ) after emqx_plugins:purge(NameVsn) end - end), + end + ), meck:unload(emqx). with_rand_install_dir(F) -> @@ -91,7 +98,8 @@ delete_package_test() -> Dir = File, ok = filelib:ensure_dir(filename:join([Dir, "foo"])), ?assertMatch({error, _}, emqx_plugins:delete_package("a-1")) - end), + end + ), meck:unload(emqx). %% purge plugin's install dir should mostly work and return ok @@ -110,15 +118,19 @@ purge_test() -> %% write a file for the dir path ok = file:write_file(Dir, "a"), ?assertEqual(ok, emqx_plugins:purge("a-1")) - end), + end + ), meck:unload(emqx). meck_emqx() -> meck:new(emqx, [unstick, passthrough]), - meck:expect(emqx, update_config, + meck:expect( + emqx, + update_config, fun(Path, Values, _Opts) -> emqx_config:put(Path, Values) - end), + end + ), %meck:expect(emqx, get_config, % fun(KeyPath, Default) -> % Map = emqx:get_raw_config(KeyPath, Default), diff --git a/apps/emqx_prometheus/rebar.config b/apps/emqx_prometheus/rebar.config index 974192a41..910ee9f82 100644 --- a/apps/emqx_prometheus/rebar.config +++ b/apps/emqx_prometheus/rebar.config @@ -1,23 +1,32 @@ %% -*- mode: erlang -*- -{deps, - [ {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.27.4"}}} - ]}. +{deps, [ + {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.27.4"}}} +]}. {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}. + +{project_plugins, [erlfmt]}. diff --git a/apps/emqx_prometheus/src/emqx_prometheus.app.src b/apps/emqx_prometheus/src/emqx_prometheus.app.src index 0edac7b69..de3089524 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.app.src +++ b/apps/emqx_prometheus/src/emqx_prometheus.app.src @@ -1,15 +1,17 @@ %% -*- mode: erlang -*- -{application, emqx_prometheus, - [{description, "Prometheus for EMQX"}, - {vsn, "5.0.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_prometheus_sup]}, - {applications, [kernel,stdlib,prometheus,emqx]}, - {mod, {emqx_prometheus_app,[]}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQX Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-prometheus"} - ]} - ]}. +{application, emqx_prometheus, [ + {description, "Prometheus for EMQX"}, + % strict semver, bump manually! + {vsn, "5.0.0"}, + {modules, []}, + {registered, [emqx_prometheus_sup]}, + {applications, [kernel, stdlib, prometheus, emqx]}, + {mod, {emqx_prometheus_app, []}}, + {env, []}, + {licenses, ["Apache-2.0"]}, + {maintainers, ["EMQX Team "]}, + {links, [ + {"Homepage", "https://emqx.io/"}, + {"Github", "https://github.com/emqx/emqx-prometheus"} + ]} +]}. diff --git a/apps/emqx_prometheus/src/emqx_prometheus.erl b/apps/emqx_prometheus/src/emqx_prometheus.erl index 131e3fc12..4bbfbe524 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus.erl @@ -28,38 +28,44 @@ -include_lib("prometheus/include/prometheus_model.hrl"). -include_lib("emqx/include/logger.hrl"). --import(prometheus_model_helpers, - [ create_mf/5 - , gauge_metric/1 - , counter_metric/1 - ]). +-import( + prometheus_model_helpers, + [ + create_mf/5, + gauge_metric/1, + counter_metric/1 + ] +). --export([ update/1 - , start/0 - , stop/0 - , restart/0 - % for rpc - , do_start/0 - , do_stop/0 - ]). +-export([ + update/1, + start/0, + stop/0, + restart/0, + % for rpc + do_start/0, + do_stop/0 +]). %% APIs -export([start_link/1]). %% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , code_change/3 - , terminate/2 - ]). +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + code_change/3, + terminate/2 +]). %% prometheus_collector callback --export([ deregister_cleanup/1 - , collect_mf/2 - , collect_metrics/2 - ]). +-export([ + deregister_cleanup/1, + collect_mf/2, + collect_metrics/2 +]). -export([collect/1]). @@ -72,8 +78,13 @@ %%-------------------------------------------------------------------- %% update new config update(Config) -> - case emqx_conf:update([prometheus], Config, - #{rawconf_with_defaults => true, override_to => cluster}) of + case + emqx_conf:update( + [prometheus], + Config, + #{rawconf_with_defaults => true, override_to => cluster} + ) + of {ok, #{raw_config := NewConfigRows}} -> case maps:get(<<"enable">>, Config, true) of true -> @@ -131,13 +142,12 @@ handle_call(_Msg, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({timeout, R, ?TIMER_MSG}, State = #state{timer=R, push_gateway=Uri}) -> +handle_info({timeout, R, ?TIMER_MSG}, State = #state{timer = R, push_gateway = Uri}) -> [Name, Ip] = string:tokens(atom_to_list(node()), "@"), - Url = lists:concat([Uri, "/metrics/job/", Name, "/instance/",Name, "~", Ip]), + Url = lists:concat([Uri, "/metrics/job/", Name, "/instance/", Name, "~", Ip]), Data = prometheus_text_format:format(), httpc:request(post, {Url, [], "text/plain", Data}, [{autoredirect, true}], []), {noreply, ensure_timer(State)}; - handle_info(_Msg, State) -> {noreply, State}. @@ -176,14 +186,15 @@ collect(<<"json">>) -> Metrics = emqx_metrics:all(), Stats = emqx_stats:getstats(), VMData = emqx_vm_data(), - #{stats => maps:from_list([collect_stats(Name, Stats) || Name <- emqx_stats()]), - metrics => maps:from_list([collect_stats(Name, VMData) || Name <- emqx_vm()]), - packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]), - messages => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]), - delivery => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]), - client => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_client()]), - session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()])}; - + #{ + stats => maps:from_list([collect_stats(Name, Stats) || Name <- emqx_stats()]), + metrics => maps:from_list([collect_stats(Name, VMData) || Name <- emqx_vm()]), + packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]), + messages => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]), + delivery => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]), + client => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_client()]), + session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()]) + }; collect(<<"prometheus">>) -> prometheus_text_format:format(). @@ -219,13 +230,11 @@ emqx_collect(emqx_connections_count, Stats) -> gauge_metric(?C('connections.count', Stats)); emqx_collect(emqx_connections_max, Stats) -> gauge_metric(?C('connections.max', Stats)); - %% sessions emqx_collect(emqx_sessions_count, Stats) -> gauge_metric(?C('sessions.count', Stats)); emqx_collect(emqx_sessions_max, Stats) -> gauge_metric(?C('sessions.max', Stats)); - %% pub/sub stats emqx_collect(emqx_topics_count, Stats) -> gauge_metric(?C('topics.count', Stats)); @@ -247,13 +256,11 @@ emqx_collect(emqx_subscriptions_shared_count, Stats) -> gauge_metric(?C('subscriptions.shared.count', Stats)); emqx_collect(emqx_subscriptions_shared_max, Stats) -> gauge_metric(?C('subscriptions.shared.max', Stats)); - %% retained emqx_collect(emqx_retained_count, Stats) -> gauge_metric(?C('retained.count', Stats)); emqx_collect(emqx_retained_max, Stats) -> gauge_metric(?C('retained.max', Stats)); - %%-------------------------------------------------------------------- %% Metrics - packets & bytes @@ -262,13 +269,11 @@ emqx_collect(emqx_bytes_received, Metrics) -> counter_metric(?C('bytes.received', Metrics)); emqx_collect(emqx_bytes_sent, Metrics) -> counter_metric(?C('bytes.sent', Metrics)); - %% received.sent emqx_collect(emqx_packets_received, Metrics) -> counter_metric(?C('packets.received', Metrics)); emqx_collect(emqx_packets_sent, Metrics) -> counter_metric(?C('packets.sent', Metrics)); - %% connect emqx_collect(emqx_packets_connect, Metrics) -> counter_metric(?C('packets.connect.received', Metrics)); @@ -278,7 +283,6 @@ emqx_collect(emqx_packets_connack_error, Metrics) -> counter_metric(?C('packets.connack.error', Metrics)); emqx_collect(emqx_packets_connack_auth_error, Metrics) -> counter_metric(?C('packets.connack.auth_error', Metrics)); - %% sub.unsub emqx_collect(emqx_packets_subscribe_received, Metrics) -> counter_metric(?C('packets.subscribe.received', Metrics)); @@ -294,7 +298,6 @@ emqx_collect(emqx_packets_unsubscribe_error, Metrics) -> counter_metric(?C('packets.unsubscribe.error', Metrics)); emqx_collect(emqx_packets_unsuback_sent, Metrics) -> counter_metric(?C('packets.unsuback.sent', Metrics)); - %% publish.puback emqx_collect(emqx_packets_publish_received, Metrics) -> counter_metric(?C('packets.publish.received', Metrics)); @@ -308,7 +311,6 @@ emqx_collect(emqx_packets_publish_auth_error, Metrics) -> counter_metric(?C('packets.publish.auth_error', Metrics)); emqx_collect(emqx_packets_publish_dropped, Metrics) -> counter_metric(?C('packets.publish.dropped', Metrics)); - %% puback emqx_collect(emqx_packets_puback_received, Metrics) -> counter_metric(?C('packets.puback.received', Metrics)); @@ -318,7 +320,6 @@ emqx_collect(emqx_packets_puback_inuse, Metrics) -> counter_metric(?C('packets.puback.inuse', Metrics)); emqx_collect(emqx_packets_puback_missed, Metrics) -> counter_metric(?C('packets.puback.missed', Metrics)); - %% pubrec emqx_collect(emqx_packets_pubrec_received, Metrics) -> counter_metric(?C('packets.pubrec.received', Metrics)); @@ -328,7 +329,6 @@ emqx_collect(emqx_packets_pubrec_inuse, Metrics) -> counter_metric(?C('packets.pubrec.inuse', Metrics)); emqx_collect(emqx_packets_pubrec_missed, Metrics) -> counter_metric(?C('packets.pubrec.missed', Metrics)); - %% pubrel emqx_collect(emqx_packets_pubrel_received, Metrics) -> counter_metric(?C('packets.pubrel.received', Metrics)); @@ -336,7 +336,6 @@ emqx_collect(emqx_packets_pubrel_sent, Metrics) -> counter_metric(?C('packets.pubrel.sent', Metrics)); emqx_collect(emqx_packets_pubrel_missed, Metrics) -> counter_metric(?C('packets.pubrel.missed', Metrics)); - %% pubcomp emqx_collect(emqx_packets_pubcomp_received, Metrics) -> counter_metric(?C('packets.pubcomp.received', Metrics)); @@ -346,77 +345,59 @@ emqx_collect(emqx_packets_pubcomp_inuse, Metrics) -> counter_metric(?C('packets.pubcomp.inuse', Metrics)); emqx_collect(emqx_packets_pubcomp_missed, Metrics) -> counter_metric(?C('packets.pubcomp.missed', Metrics)); - %% pingreq emqx_collect(emqx_packets_pingreq_received, Metrics) -> counter_metric(?C('packets.pingreq.received', Metrics)); emqx_collect(emqx_packets_pingresp_sent, Metrics) -> counter_metric(?C('packets.pingresp.sent', Metrics)); - %% disconnect emqx_collect(emqx_packets_disconnect_received, Metrics) -> counter_metric(?C('packets.disconnect.received', Metrics)); emqx_collect(emqx_packets_disconnect_sent, Metrics) -> counter_metric(?C('packets.disconnect.sent', Metrics)); - %% auth emqx_collect(emqx_packets_auth_received, Metrics) -> counter_metric(?C('packets.auth.received', Metrics)); emqx_collect(emqx_packets_auth_sent, Metrics) -> counter_metric(?C('packets.auth.sent', Metrics)); - %%-------------------------------------------------------------------- %% Metrics - messages %% messages emqx_collect(emqx_messages_received, Metrics) -> counter_metric(?C('messages.received', Metrics)); - emqx_collect(emqx_messages_sent, Metrics) -> counter_metric(?C('messages.sent', Metrics)); - emqx_collect(emqx_messages_qos0_received, Metrics) -> counter_metric(?C('messages.qos0.received', Metrics)); emqx_collect(emqx_messages_qos0_sent, Metrics) -> counter_metric(?C('messages.qos0.sent', Metrics)); - emqx_collect(emqx_messages_qos1_received, Metrics) -> counter_metric(?C('messages.qos1.received', Metrics)); emqx_collect(emqx_messages_qos1_sent, Metrics) -> counter_metric(?C('messages.qos1.sent', Metrics)); - emqx_collect(emqx_messages_qos2_received, Metrics) -> counter_metric(?C('messages.qos2.received', Metrics)); emqx_collect(emqx_messages_qos2_sent, Metrics) -> counter_metric(?C('messages.qos2.sent', Metrics)); - emqx_collect(emqx_messages_publish, Metrics) -> counter_metric(?C('messages.publish', Metrics)); - emqx_collect(emqx_messages_dropped, Metrics) -> counter_metric(?C('messages.dropped', Metrics)); - emqx_collect(emqx_messages_dropped_expired, Metrics) -> counter_metric(?C('messages.dropped.await_pubrel_timeout', Metrics)); - emqx_collect(emqx_messages_dropped_no_subscribers, Metrics) -> counter_metric(?C('messages.dropped.no_subscribers', Metrics)); - emqx_collect(emqx_messages_forward, Metrics) -> counter_metric(?C('messages.forward', Metrics)); - emqx_collect(emqx_messages_retained, Metrics) -> counter_metric(?C('messages.retained', Metrics)); - emqx_collect(emqx_messages_delayed, Stats) -> counter_metric(?C('messages.delayed', Stats)); - emqx_collect(emqx_messages_delivered, Stats) -> counter_metric(?C('messages.delivered', Stats)); - emqx_collect(emqx_messages_acked, Stats) -> counter_metric(?C('messages.acked', Stats)); - %%-------------------------------------------------------------------- %% Metrics - delivery @@ -432,7 +413,6 @@ emqx_collect(emqx_delivery_dropped_queue_full, Stats) -> counter_metric(?C('delivery.dropped.queue_full', Stats)); emqx_collect(emqx_delivery_dropped_expired, Stats) -> counter_metric(?C('delivery.dropped.expired', Stats)); - %%-------------------------------------------------------------------- %% Metrics - client @@ -450,7 +430,6 @@ emqx_collect(emqx_client_unsubscribe, Stats) -> counter_metric(?C('client.unsubscribe', Stats)); emqx_collect(emqx_client_disconnected, Stats) -> counter_metric(?C('client.disconnected', Stats)); - %%-------------------------------------------------------------------- %% Metrics - session @@ -464,31 +443,23 @@ emqx_collect(emqx_session_discarded, Stats) -> counter_metric(?C('session.discarded', Stats)); emqx_collect(emqx_session_terminated, Stats) -> counter_metric(?C('session.terminated', Stats)); - %%-------------------------------------------------------------------- %% VM emqx_collect(emqx_vm_cpu_use, VMData) -> gauge_metric(?C(cpu_use, VMData)); - emqx_collect(emqx_vm_cpu_idle, VMData) -> gauge_metric(?C(cpu_idle, VMData)); - emqx_collect(emqx_vm_run_queue, VMData) -> gauge_metric(?C(run_queue, VMData)); - emqx_collect(emqx_vm_process_messages_in_queues, VMData) -> gauge_metric(?C(process_total_messages, VMData)); - emqx_collect(emqx_vm_total_memory, VMData) -> gauge_metric(?C(total_memory, VMData)); - emqx_collect(emqx_vm_used_memory, VMData) -> gauge_metric(?C(used_memory, VMData)); - emqx_collect(emqx_cluster_nodes_running, ClusterData) -> gauge_metric(?C(nodes_running, ClusterData)); - emqx_collect(emqx_cluster_nodes_stopped, ClusterData) -> gauge_metric(?C(nodes_stopped, ClusterData)). @@ -497,142 +468,157 @@ emqx_collect(emqx_cluster_nodes_stopped, ClusterData) -> %%-------------------------------------------------------------------- emqx_stats() -> - [ emqx_connections_count - , emqx_connections_max - , emqx_sessions_count - , emqx_sessions_max - , emqx_topics_count - , emqx_topics_max - , emqx_suboptions_count - , emqx_suboptions_max - , emqx_subscribers_count - , emqx_subscribers_max - , emqx_subscriptions_count - , emqx_subscriptions_max - , emqx_subscriptions_shared_count - , emqx_subscriptions_shared_max - , emqx_retained_count - , emqx_retained_max + [ + emqx_connections_count, + emqx_connections_max, + emqx_sessions_count, + emqx_sessions_max, + emqx_topics_count, + emqx_topics_max, + emqx_suboptions_count, + emqx_suboptions_max, + emqx_subscribers_count, + emqx_subscribers_max, + emqx_subscriptions_count, + emqx_subscriptions_max, + emqx_subscriptions_shared_count, + emqx_subscriptions_shared_max, + emqx_retained_count, + emqx_retained_max ]. emqx_metrics_packets() -> - [ emqx_bytes_received - , emqx_bytes_sent - , emqx_packets_received - , emqx_packets_sent - , emqx_packets_connect - , emqx_packets_connack_sent - , emqx_packets_connack_error - , emqx_packets_connack_auth_error - , emqx_packets_publish_received - , emqx_packets_publish_sent - , emqx_packets_publish_inuse - , emqx_packets_publish_error - , emqx_packets_publish_auth_error - , emqx_packets_publish_dropped - , emqx_packets_puback_received - , emqx_packets_puback_sent - , emqx_packets_puback_inuse - , emqx_packets_puback_missed - , emqx_packets_pubrec_received - , emqx_packets_pubrec_sent - , emqx_packets_pubrec_inuse - , emqx_packets_pubrec_missed - , emqx_packets_pubrel_received - , emqx_packets_pubrel_sent - , emqx_packets_pubrel_missed - , emqx_packets_pubcomp_received - , emqx_packets_pubcomp_sent - , emqx_packets_pubcomp_inuse - , emqx_packets_pubcomp_missed - , emqx_packets_subscribe_received - , emqx_packets_subscribe_error - , emqx_packets_subscribe_auth_error - , emqx_packets_suback_sent - , emqx_packets_unsubscribe_received - , emqx_packets_unsubscribe_error - , emqx_packets_unsuback_sent - , emqx_packets_pingreq_received - , emqx_packets_pingresp_sent - , emqx_packets_disconnect_received - , emqx_packets_disconnect_sent - , emqx_packets_auth_received - , emqx_packets_auth_sent + [ + emqx_bytes_received, + emqx_bytes_sent, + emqx_packets_received, + emqx_packets_sent, + emqx_packets_connect, + emqx_packets_connack_sent, + emqx_packets_connack_error, + emqx_packets_connack_auth_error, + emqx_packets_publish_received, + emqx_packets_publish_sent, + emqx_packets_publish_inuse, + emqx_packets_publish_error, + emqx_packets_publish_auth_error, + emqx_packets_publish_dropped, + emqx_packets_puback_received, + emqx_packets_puback_sent, + emqx_packets_puback_inuse, + emqx_packets_puback_missed, + emqx_packets_pubrec_received, + emqx_packets_pubrec_sent, + emqx_packets_pubrec_inuse, + emqx_packets_pubrec_missed, + emqx_packets_pubrel_received, + emqx_packets_pubrel_sent, + emqx_packets_pubrel_missed, + emqx_packets_pubcomp_received, + emqx_packets_pubcomp_sent, + emqx_packets_pubcomp_inuse, + emqx_packets_pubcomp_missed, + emqx_packets_subscribe_received, + emqx_packets_subscribe_error, + emqx_packets_subscribe_auth_error, + emqx_packets_suback_sent, + emqx_packets_unsubscribe_received, + emqx_packets_unsubscribe_error, + emqx_packets_unsuback_sent, + emqx_packets_pingreq_received, + emqx_packets_pingresp_sent, + emqx_packets_disconnect_received, + emqx_packets_disconnect_sent, + emqx_packets_auth_received, + emqx_packets_auth_sent ]. emqx_metrics_messages() -> - [ emqx_messages_received - , emqx_messages_sent - , emqx_messages_qos0_received - , emqx_messages_qos0_sent - , emqx_messages_qos1_received - , emqx_messages_qos1_sent - , emqx_messages_qos2_received - , emqx_messages_qos2_sent - , emqx_messages_publish - , emqx_messages_dropped - , emqx_messages_dropped_expired - , emqx_messages_dropped_no_subscribers - , emqx_messages_forward - , emqx_messages_retained - , emqx_messages_delayed - , emqx_messages_delivered - , emqx_messages_acked + [ + emqx_messages_received, + emqx_messages_sent, + emqx_messages_qos0_received, + emqx_messages_qos0_sent, + emqx_messages_qos1_received, + emqx_messages_qos1_sent, + emqx_messages_qos2_received, + emqx_messages_qos2_sent, + emqx_messages_publish, + emqx_messages_dropped, + emqx_messages_dropped_expired, + emqx_messages_dropped_no_subscribers, + emqx_messages_forward, + emqx_messages_retained, + emqx_messages_delayed, + emqx_messages_delivered, + emqx_messages_acked ]. emqx_metrics_delivery() -> - [ emqx_delivery_dropped - , emqx_delivery_dropped_no_local - , emqx_delivery_dropped_too_large - , emqx_delivery_dropped_qos0_msg - , emqx_delivery_dropped_queue_full - , emqx_delivery_dropped_expired + [ + emqx_delivery_dropped, + emqx_delivery_dropped_no_local, + emqx_delivery_dropped_too_large, + emqx_delivery_dropped_qos0_msg, + emqx_delivery_dropped_queue_full, + emqx_delivery_dropped_expired ]. emqx_metrics_client() -> - [ emqx_client_connected - , emqx_client_authenticate - , emqx_client_auth_anonymous - , emqx_client_authorize - , emqx_client_subscribe - , emqx_client_unsubscribe - , emqx_client_disconnected + [ + emqx_client_connected, + emqx_client_authenticate, + emqx_client_auth_anonymous, + emqx_client_authorize, + emqx_client_subscribe, + emqx_client_unsubscribe, + emqx_client_disconnected ]. emqx_metrics_session() -> - [ emqx_session_created - , emqx_session_resumed - , emqx_session_takenover - , emqx_session_discarded - , emqx_session_terminated + [ + emqx_session_created, + emqx_session_resumed, + emqx_session_takenover, + emqx_session_discarded, + emqx_session_terminated ]. emqx_vm() -> - [ emqx_vm_cpu_use - , emqx_vm_cpu_idle - , emqx_vm_run_queue - , emqx_vm_process_messages_in_queues - , emqx_vm_total_memory - , emqx_vm_used_memory + [ + emqx_vm_cpu_use, + emqx_vm_cpu_idle, + emqx_vm_run_queue, + emqx_vm_process_messages_in_queues, + emqx_vm_total_memory, + emqx_vm_used_memory ]. emqx_vm_data() -> - Idle = case cpu_sup:util([detailed]) of - {_, 0, 0, _} -> 0; %% Not support for Windows - {_Num, _Use, IdleList, _} -> ?C(idle, IdleList) - end, + Idle = + case cpu_sup:util([detailed]) of + %% Not support for Windows + {_, 0, 0, _} -> 0; + {_Num, _Use, IdleList, _} -> ?C(idle, IdleList) + end, RunQueue = erlang:statistics(run_queue), - [{run_queue, RunQueue}, - {process_total_messages, 0}, %% XXX: Plan removed at v5.0 - {cpu_idle, Idle}, - {cpu_use, 100 - Idle}] ++ emqx_vm:mem_info(). + [ + {run_queue, RunQueue}, + %% XXX: Plan removed at v5.0 + {process_total_messages, 0}, + {cpu_idle, Idle}, + {cpu_use, 100 - Idle} + ] ++ emqx_vm:mem_info(). emqx_cluster() -> - [ emqx_cluster_nodes_running - , emqx_cluster_nodes_stopped + [ + emqx_cluster_nodes_running, + emqx_cluster_nodes_stopped ]. emqx_cluster_data() -> #{running_nodes := Running, stopped_nodes := Stopped} = mria_mnesia:cluster_info(), - [{nodes_running, length(Running)}, - {nodes_stopped, length(Stopped)}]. + [ + {nodes_running, length(Running)}, + {nodes_stopped, length(Stopped)} + ]. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_api.erl b/apps/emqx_prometheus/src/emqx_prometheus_api.erl index 72611d5cd..01764e1b5 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_api.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_api.erl @@ -22,14 +22,16 @@ -import(hoconsc, [ref/2]). --export([ api_spec/0 - , paths/0 - , schema/1 - ]). +-export([ + api_spec/0, + paths/0, + schema/1 +]). --export([ prometheus/2 - , stats/2 - ]). +-export([ + prometheus/2, + stats/2 +]). -define(SCHEMA_MODULE, emqx_prometheus_schema). @@ -37,32 +39,38 @@ api_spec() -> emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}). paths() -> - [ "/prometheus" - , "/prometheus/stats" + [ + "/prometheus", + "/prometheus/stats" ]. schema("/prometheus") -> - #{ 'operationId' => prometheus - , get => - #{ description => <<"Get Prometheus config info">> - , responses => - #{200 => prometheus_config_schema()} + #{ + 'operationId' => prometheus, + get => + #{ + description => <<"Get Prometheus config info">>, + responses => + #{200 => prometheus_config_schema()} + }, + put => + #{ + description => <<"Update Prometheus config">>, + 'requestBody' => prometheus_config_schema(), + responses => + #{200 => prometheus_config_schema()} } - , put => - #{ description => <<"Update Prometheus config">> - , 'requestBody' => prometheus_config_schema() - , responses => - #{200 => prometheus_config_schema()} - } - }; + }; schema("/prometheus/stats") -> - #{ 'operationId' => stats - , get => - #{ description => <<"Get Prometheus Data">> - , responses => - #{200 => prometheus_data_schema()} + #{ + 'operationId' => stats, + get => + #{ + description => <<"Get Prometheus Data">>, + responses => + #{200 => prometheus_data_schema()} } - }. + }. %%-------------------------------------------------------------------- %% API Handler funcs @@ -70,7 +78,6 @@ schema("/prometheus/stats") -> prometheus(get, _Params) -> {200, emqx:get_raw_config([<<"prometheus">>], #{})}; - prometheus(put, #{body := Body}) -> case emqx_prometheus:update(Body) of {ok, NewConfig} -> @@ -100,21 +107,25 @@ stats(get, #{headers := Headers}) -> prometheus_config_schema() -> emqx_dashboard_swagger:schema_with_example( - ref(?SCHEMA_MODULE, "prometheus"), - prometheus_config_example()). + ref(?SCHEMA_MODULE, "prometheus"), + prometheus_config_example() + ). prometheus_config_example() -> - #{ enable => true - , interval => "15s" - , push_gateway_server => <<"http://127.0.0.1:9091">> - }. + #{ + enable => true, + interval => "15s", + push_gateway_server => <<"http://127.0.0.1:9091">> + }. prometheus_data_schema() -> - #{ description => <<"Get Prometheus Data">> - , content => - #{ 'application/json' => - #{schema => #{type => object}} - , 'text/plain' => - #{schema => #{type => string}} + #{ + description => <<"Get Prometheus Data">>, + content => + #{ + 'application/json' => + #{schema => #{type => object}}, + 'text/plain' => + #{schema => #{type => string}} } - }. + }. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_app.erl b/apps/emqx_prometheus/src/emqx_prometheus_app.erl index 5b34ba1df..b9dd9c466 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_app.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_app.erl @@ -21,9 +21,10 @@ -include("emqx_prometheus.hrl"). %% Application callbacks --export([ start/2 - , stop/1 - ]). +-export([ + start/2, + stop/1 +]). start(_StartType, _StartArgs) -> {ok, Sup} = emqx_prometheus_sup:start_link(), diff --git a/apps/emqx_prometheus/src/emqx_prometheus_mria.erl b/apps/emqx_prometheus/src/emqx_prometheus_mria.erl index a79611439..c81e0885e 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_mria.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_mria.erl @@ -15,9 +15,10 @@ %%-------------------------------------------------------------------- -module(emqx_prometheus_mria). --export([deregister_cleanup/1, - collect_mf/2 - ]). +-export([ + deregister_cleanup/1, + collect_mf/2 +]). -include_lib("prometheus/include/prometheus.hrl"). @@ -43,39 +44,45 @@ deregister_cleanup(_) -> ok. _Registry :: prometheus_registry:registry(), Callback :: prometheus_collector:callback(). collect_mf(_Registry, Callback) -> - case mria_rlog:backend() of - rlog -> - Metrics = metrics(), - _ = [add_metric_family(Metric, Callback) || Metric <- Metrics], - ok; - mnesia -> - ok - end. + case mria_rlog:backend() of + rlog -> + Metrics = metrics(), + _ = [add_metric_family(Metric, Callback) || Metric <- Metrics], + ok; + mnesia -> + ok + end. add_metric_family({Name, Metrics}, Callback) -> - Callback(prometheus_model_helpers:create_mf( ?METRIC_NAME(Name) - , <<"">> - , gauge - , catch_all(Metrics) - )). + Callback( + prometheus_model_helpers:create_mf( + ?METRIC_NAME(Name), + <<"">>, + gauge, + catch_all(Metrics) + ) + ). %%==================================================================== %% Internal functions %%==================================================================== metrics() -> - Metrics = case mria_rlog:role() of - replicant -> - [lag, bootstrap_time, bootstrap_num_keys, message_queue_len, replayq_len]; - core -> - [last_intercepted_trans, weight, replicants, server_mql] - end, + Metrics = + case mria_rlog:role() of + replicant -> + [lag, bootstrap_time, bootstrap_num_keys, message_queue_len, replayq_len]; + core -> + [last_intercepted_trans, weight, replicants, server_mql] + end, [{MetricId, fun() -> get_shard_metric(MetricId) end} || MetricId <- Metrics]. get_shard_metric(Metric) -> %% TODO: only report shards that are up - [{[{shard, Shard}], get_shard_metric(Metric, Shard)} || - Shard <- mria_schema:shards(), Shard =/= undefined]. + [ + {[{shard, Shard}], get_shard_metric(Metric, Shard)} + || Shard <- mria_schema:shards(), Shard =/= undefined + ]. get_shard_metric(replicants, Shard) -> length(mria_status:agents(Shard)); @@ -88,6 +95,8 @@ get_shard_metric(Metric, Shard) -> end. catch_all(DataFun) -> - try DataFun() - catch _:_ -> undefined + try + DataFun() + catch + _:_ -> undefined end. diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 98f9a519b..300450260 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -20,11 +20,12 @@ -behaviour(hocon_schema). --export([ namespace/0 - , roots/0 - , fields/1 - , desc/1 - ]). +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). namespace() -> "prometheus". @@ -32,25 +33,36 @@ roots() -> ["prometheus"]. fields("prometheus") -> [ - {push_gateway_server, sc(string(), - #{ default => "http://127.0.0.1:9091" - , required => true - , desc => ?DESC(push_gateway_server) - })}, - {interval, sc(emqx_schema:duration_ms(), - #{ default => "15s" - , required => true - , desc => ?DESC(interval) - })}, - {enable, sc(boolean(), - #{ default => false - , required => true - , desc => ?DESC(enable) - })} + {push_gateway_server, + sc( + string(), + #{ + default => "http://127.0.0.1:9091", + required => true, + desc => ?DESC(push_gateway_server) + } + )}, + {interval, + sc( + emqx_schema:duration_ms(), + #{ + default => "15s", + required => true, + desc => ?DESC(interval) + } + )}, + {enable, + sc( + boolean(), + #{ + default => false, + required => true, + desc => ?DESC(enable) + } + )} ]. desc("prometheus") -> ?DESC(prometheus); -desc(_) -> - undefined. +desc(_) -> undefined. sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_sup.erl b/apps/emqx_prometheus/src/emqx_prometheus_sup.erl index 3766100d5..65023da14 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_sup.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_sup.erl @@ -18,21 +18,24 @@ -behaviour(supervisor). --export([ start_link/0 - , start_child/1 - , start_child/2 - , stop_child/1 - ]). +-export([ + start_link/0, + start_child/1, + start_child/2, + stop_child/1 +]). -export([init/1]). %% Helper macro for declaring children of supervisor --define(CHILD(Mod, Opts), #{id => Mod, - start => {Mod, start_link, [Opts]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [Mod]}). +-define(CHILD(Mod, Opts), #{ + id => Mod, + start => {Mod, start_link, [Opts]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [Mod] +}). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -45,7 +48,7 @@ start_child(ChildSpec) when is_map(ChildSpec) -> start_child(Mod, Opts) when is_atom(Mod) andalso is_map(Opts) -> assert_started(supervisor:start_child(?MODULE, ?CHILD(Mod, Opts))). --spec(stop_child(any()) -> ok | {error, term()}). +-spec stop_child(any()) -> ok | {error, term()}. stop_child(ChildId) -> case supervisor:terminate_child(?MODULE, ChildId) of ok -> supervisor:delete_child(?MODULE, ChildId); diff --git a/apps/emqx_prometheus/src/proto/emqx_prometheus_proto_v1.erl b/apps/emqx_prometheus/src/proto/emqx_prometheus_proto_v1.erl index 992c6e22b..c0529cabd 100644 --- a/apps/emqx_prometheus/src/proto/emqx_prometheus_proto_v1.erl +++ b/apps/emqx_prometheus/src/proto/emqx_prometheus_proto_v1.erl @@ -18,11 +18,12 @@ -behaviour(emqx_bpapi). --export([ introduced_in/0 +-export([ + introduced_in/0, - , start/1 - , stop/1 - ]). + start/1, + stop/1 +]). -include_lib("emqx/include/bpapi.hrl"). diff --git a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl index f1c4b3800..03e8d6d78 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl @@ -22,13 +22,14 @@ -compile(export_all). -define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). --define(CONF_DEFAULT, <<" -prometheus { - push_gateway_server = \"http://127.0.0.1:9091\" - interval = \"1s\" - enable = true -} -">>). +-define(CONF_DEFAULT, + <<"\n" + "prometheus {\n" + " push_gateway_server = \"http://127.0.0.1:9091\"\n" + " interval = \"1s\"\n" + " enable = true\n" + "}\n">> +). %%-------------------------------------------------------------------- %% Setups diff --git a/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl index 69836f033..e72d7865a 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_api_SUITE.erl @@ -67,9 +67,14 @@ t_prometheus_api(_) -> {ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, "", Auth), Conf = emqx_json:decode(Response, [return_maps]), - ?assertMatch(#{<<"push_gateway_server">> := _, - <<"interval">> := _, - <<"enable">> := _}, Conf), + ?assertMatch( + #{ + <<"push_gateway_server">> := _, + <<"interval">> := _, + <<"enable">> := _ + }, + Conf + ), NewConf = Conf#{<<"interval">> := <<"2s">>}, {ok, Response2} = emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, NewConf), diff --git a/apps/emqx_resource/include/emqx_resource.hrl b/apps/emqx_resource/include/emqx_resource.hrl index fdddcdc87..f10f4b440 100644 --- a/apps/emqx_resource/include/emqx_resource.hrl +++ b/apps/emqx_resource/include/emqx_resource.hrl @@ -30,12 +30,13 @@ }. -type resource_group() :: binary(). -type create_opts() :: #{ - health_check_interval => integer(), - health_check_timeout => integer(), - waiting_connect_complete => integer() - }. --type after_query() :: {[OnSuccess :: after_query_fun()], [OnFailed :: after_query_fun()]} | - undefined. + health_check_interval => integer(), + health_check_timeout => integer(), + waiting_connect_complete => integer() +}. +-type after_query() :: + {[OnSuccess :: after_query_fun()], [OnFailed :: after_query_fun()]} + | undefined. %% the `after_query_fun()` is mainly for callbacks that increment counters or do some fallback %% actions upon query failure diff --git a/apps/emqx_resource/include/emqx_resource_utils.hrl b/apps/emqx_resource/include/emqx_resource_utils.hrl index 8d13036ce..8d94746eb 100644 --- a/apps/emqx_resource/include/emqx_resource_utils.hrl +++ b/apps/emqx_resource/include/emqx_resource_utils.hrl @@ -15,13 +15,17 @@ %%-------------------------------------------------------------------- -define(SAFE_CALL(_EXP_), - ?SAFE_CALL(_EXP_, ok)). + ?SAFE_CALL(_EXP_, ok) +). -define(SAFE_CALL(_EXP_, _EXP_ON_FAIL_), - fun() -> - try (_EXP_) - catch _EXCLASS_:_EXCPTION_:_ST_ -> + fun() -> + try + (_EXP_) + catch + _EXCLASS_:_EXCPTION_:_ST_ -> _EXP_ON_FAIL_, {error, {_EXCLASS_, _EXCPTION_, _ST_}} - end - end()). + end + end() +). diff --git a/apps/emqx_resource/rebar.config b/apps/emqx_resource/rebar.config index d5d608a71..e6857829f 100644 --- a/apps/emqx_resource/rebar.config +++ b/apps/emqx_resource/rebar.config @@ -1,9 +1,10 @@ %% -*- mode: erlang -*- -{erl_opts, [ debug_info - , nowarn_unused_import - %, {d, 'RESOURCE_DEBUG'} - ]}. +{erl_opts, [ + debug_info, + nowarn_unused_import + %, {d, 'RESOURCE_DEBUG'} +]}. {erl_first_files, ["src/emqx_resource_transform.erl"]}. @@ -11,9 +12,11 @@ %% try to override the dialyzer 'race_conditions' defined in the top-level dir, %% but it doesn't work -{dialyzer, [{warnings, [unmatched_returns, error_handling]} - ]}. +{dialyzer, [{warnings, [unmatched_returns, error_handling]}]}. -{deps, [ {jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}} - , {emqx, {path, "../emqx"}} - ]}. +{deps, [ + {jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}}, + {emqx, {path, "../emqx"}} +]}. + +{project_plugins, [erlfmt]}. diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 9591c5718..56391713f 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -1,19 +1,19 @@ %% -*- mode: erlang -*- -{application, emqx_resource, - [{description, "An OTP application"}, - {vsn, "0.1.0"}, - {registered, []}, - {mod, {emqx_resource_app, []}}, - {applications, - [kernel, - stdlib, - gproc, - jsx, - emqx - ]}, - {env,[]}, - {modules, []}, +{application, emqx_resource, [ + {description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {emqx_resource_app, []}}, + {applications, [ + kernel, + stdlib, + gproc, + jsx, + emqx + ]}, + {env, []}, + {modules, []}, - {licenses, ["Apache 2.0"]}, - {links, []} - ]}. + {licenses, ["Apache 2.0"]}, + {links, []} +]}. diff --git a/apps/emqx_resource/src/emqx_resource.erl b/apps/emqx_resource/src/emqx_resource.erl index f6bdf55d4..48615a6e3 100644 --- a/apps/emqx_resource/src/emqx_resource.erl +++ b/apps/emqx_resource/src/emqx_resource.erl @@ -25,66 +25,93 @@ %% APIs for behaviour implementations --export([ query_success/1 - , query_failed/1 - ]). +-export([ + query_success/1, + query_failed/1 +]). %% APIs for instances --export([ check_config/2 - , check_and_create/4 - , check_and_create/5 - , check_and_create_local/4 - , check_and_create_local/5 - , check_and_recreate/4 - , check_and_recreate_local/4 - ]). +-export([ + check_config/2, + check_and_create/4, + check_and_create/5, + check_and_create_local/4, + check_and_create_local/5, + check_and_recreate/4, + check_and_recreate_local/4 +]). %% Sync resource instances and files %% provisional solution: rpc:multicall to all the nodes for creating/updating/removing %% todo: replicate operations --export([ create/4 %% store the config and start the instance - , create/5 - , create_local/4 - , create_local/5 - , create_dry_run/2 %% run start/2, health_check/2 and stop/1 sequentially - , create_dry_run_local/2 - , recreate/4 %% this will do create_dry_run, stop the old instance and start a new one - , recreate_local/4 - , remove/1 %% remove the config and stop the instance - , remove_local/1 - , reset_metrics/1 - , reset_metrics_local/1 - ]). + +%% store the config and start the instance +-export([ + create/4, + create/5, + create_local/4, + create_local/5, + %% run start/2, health_check/2 and stop/1 sequentially + create_dry_run/2, + create_dry_run_local/2, + %% this will do create_dry_run, stop the old instance and start a new one + recreate/4, + recreate_local/4, + %% remove the config and stop the instance + remove/1, + remove_local/1, + reset_metrics/1, + reset_metrics_local/1 +]). %% Calls to the callback module with current resource state %% They also save the state after the call finished (except query/2,3). --export([ restart/1 %% restart the instance. - , restart/2 - , health_check/1 %% verify if the resource is working normally - , set_resource_status_connecting/1 %% set resource status to disconnected - , stop/1 %% stop the instance - , query/2 %% query the instance - , query/3 %% query the instance with after_query() - ]). + +%% restart the instance. +-export([ + restart/1, + restart/2, + %% verify if the resource is working normally + health_check/1, + %% set resource status to disconnected + set_resource_status_connecting/1, + %% stop the instance + stop/1, + %% query the instance + query/2, + %% query the instance with after_query() + query/3 +]). %% Direct calls to the callback module --export([ call_start/3 %% start the instance - , call_health_check/3 %% verify if the resource is working normally - , call_stop/3 %% stop the instance - ]). --export([ list_instances/0 %% list all the instances, id only. - , list_instances_verbose/0 %% list all the instances - , get_instance/1 %% return the data of the instance - , list_instances_by_type/1 %% return all the instances of the same resource type - , generate_id/1 - , list_group_instances/1 - ]). +%% start the instance +-export([ + call_start/3, + %% verify if the resource is working normally + call_health_check/3, + %% stop the instance + call_stop/3 +]). --optional_callbacks([ on_query/4 - , on_health_check/2 - ]). +%% list all the instances, id only. +-export([ + list_instances/0, + %% list all the instances + list_instances_verbose/0, + %% return the data of the instance + get_instance/1, + %% return all the instances of the same resource type + list_instances_by_type/1, + generate_id/1, + list_group_instances/1 +]). + +-optional_callbacks([ + on_query/4, + on_health_check/2 +]). %% when calling emqx_resource:start/1 -callback on_start(instance_id(), resource_config()) -> @@ -98,7 +125,7 @@ %% when calling emqx_resource:health_check/2 -callback on_health_check(instance_id(), resource_state()) -> - {ok, resource_state()} | {error, Reason:: term(), resource_state()}. + {ok, resource_state()} | {error, Reason :: term(), resource_state()}. -spec list_types() -> [module()]. list_types() -> @@ -111,24 +138,26 @@ discover_resource_mods() -> -spec is_resource_mod(module()) -> boolean(). is_resource_mod(Module) -> Info = Module:module_info(attributes), - Behaviour = proplists:get_value(behavior, Info, []) ++ - proplists:get_value(behaviour, Info, []), + Behaviour = + proplists:get_value(behavior, Info, []) ++ + proplists:get_value(behaviour, Info, []), lists:member(?MODULE, Behaviour). -spec query_success(after_query()) -> ok. query_success(undefined) -> ok; -query_success({OnSucc, _}) -> - apply_query_after_calls(OnSucc). +query_success({OnSucc, _}) -> apply_query_after_calls(OnSucc). -spec query_failed(after_query()) -> ok. query_failed(undefined) -> ok; -query_failed({_, OnFailed}) -> - apply_query_after_calls(OnFailed). +query_failed({_, OnFailed}) -> apply_query_after_calls(OnFailed). apply_query_after_calls(Funcs) -> - lists:foreach(fun({Fun, Args}) -> + lists:foreach( + fun({Fun, Args}) -> safe_apply(Fun, Args) - end, Funcs). + end, + Funcs + ). %% ================================================================================= %% APIs for resource instances @@ -149,11 +178,13 @@ create(InstId, Group, ResourceType, Config, Opts) -> create_local(InstId, Group, ResourceType, Config) -> create_local(InstId, Group, ResourceType, Config, #{}). --spec create_local(instance_id(), - resource_group(), - resource_type(), - resource_config(), - create_opts()) -> +-spec create_local( + instance_id(), + resource_group(), + resource_type(), + resource_config(), + create_opts() +) -> {ok, resource_data() | 'already_created'} | {error, Reason :: term()}. create_local(InstId, Group, ResourceType, Config, Opts) -> call_instance(InstId, {create, InstId, Group, ResourceType, Config, Opts}). @@ -206,19 +237,25 @@ query(InstId, Request) -> query(InstId, Request, AfterQuery) -> case get_instance(InstId) of {ok, _Group, #{status := connecting}} -> - query_error(connecting, <<"cannot serve query when the resource " - "instance is still connecting">>); + query_error(connecting, << + "cannot serve query when the resource " + "instance is still connecting" + >>); {ok, _Group, #{status := disconnected}} -> - query_error(disconnected, <<"cannot serve query when the resource " - "instance is disconnected">>); + query_error(disconnected, << + "cannot serve query when the resource " + "instance is disconnected" + >>); {ok, _Group, #{mod := Mod, state := ResourceState, status := connected}} -> %% the resource state is readonly to Module:on_query/4 %% and the `after_query()` functions should be thread safe ok = emqx_plugin_libs_metrics:inc(resource_metrics, InstId, matched), - try Mod:on_query(InstId, Request, AfterQuery, ResourceState) - catch Err:Reason:ST -> - emqx_plugin_libs_metrics:inc(resource_metrics, InstId, exception), - erlang:raise(Err, Reason, ST) + try + Mod:on_query(InstId, Request, AfterQuery, ResourceState) + catch + Err:Reason:ST -> + emqx_plugin_libs_metrics:inc(resource_metrics, InstId, exception), + erlang:raise(Err, Reason, ST) end; {error, not_found} -> query_error(not_found, <<"the resource id not exists">>) @@ -258,9 +295,10 @@ list_instances_verbose() -> -spec list_instances_by_type(module()) -> [instance_id()]. list_instances_by_type(ResourceType) -> - filter_instances(fun(_, RT) when RT =:= ResourceType -> true; - (_, _) -> false - end). + filter_instances(fun + (_, RT) when RT =:= ResourceType -> true; + (_, _) -> false + end). -spec generate_id(term()) -> instance_id(). generate_id(Name) when is_binary(Name) -> @@ -276,7 +314,9 @@ call_start(InstId, Mod, Config) -> ?SAFE_CALL(Mod:on_start(InstId, Config)). -spec call_health_check(instance_id(), module(), resource_state()) -> - {ok, resource_state()} | {error, Reason:: term()} | {error, Reason:: term(), resource_state()}. + {ok, resource_state()} + | {error, Reason :: term()} + | {error, Reason :: term(), resource_state()}. call_health_check(InstId, Mod, ResourceState) -> ?SAFE_CALL(Mod:on_health_check(InstId, ResourceState)). @@ -289,58 +329,82 @@ call_stop(InstId, Mod, ResourceState) -> check_config(ResourceType, Conf) -> emqx_hocon:check(ResourceType, Conf). --spec check_and_create(instance_id(), - resource_group(), - resource_type(), - raw_resource_config()) -> +-spec check_and_create( + instance_id(), + resource_group(), + resource_type(), + raw_resource_config() +) -> {ok, resource_data() | 'already_created'} | {error, term()}. check_and_create(InstId, Group, ResourceType, RawConfig) -> check_and_create(InstId, Group, ResourceType, RawConfig, #{}). --spec check_and_create(instance_id(), - resource_group(), - resource_type(), - raw_resource_config(), - create_opts()) -> +-spec check_and_create( + instance_id(), + resource_group(), + resource_type(), + raw_resource_config(), + create_opts() +) -> {ok, resource_data() | 'already_created'} | {error, term()}. check_and_create(InstId, Group, ResourceType, RawConfig, Opts) -> - check_and_do(ResourceType, RawConfig, - fun(InstConf) -> create(InstId, Group, ResourceType, InstConf, Opts) end). + check_and_do( + ResourceType, + RawConfig, + fun(InstConf) -> create(InstId, Group, ResourceType, InstConf, Opts) end + ). --spec check_and_create_local(instance_id(), - resource_group(), - resource_type(), - raw_resource_config()) -> +-spec check_and_create_local( + instance_id(), + resource_group(), + resource_type(), + raw_resource_config() +) -> {ok, resource_data()} | {error, term()}. check_and_create_local(InstId, Group, ResourceType, RawConfig) -> check_and_create_local(InstId, Group, ResourceType, RawConfig, #{}). --spec check_and_create_local(instance_id(), - resource_group(), - resource_type(), - raw_resource_config(), - create_opts()) -> {ok, resource_data()} | {error, term()}. +-spec check_and_create_local( + instance_id(), + resource_group(), + resource_type(), + raw_resource_config(), + create_opts() +) -> {ok, resource_data()} | {error, term()}. check_and_create_local(InstId, Group, ResourceType, RawConfig, Opts) -> - check_and_do(ResourceType, RawConfig, - fun(InstConf) -> create_local(InstId, Group, ResourceType, InstConf, Opts) end). + check_and_do( + ResourceType, + RawConfig, + fun(InstConf) -> create_local(InstId, Group, ResourceType, InstConf, Opts) end + ). --spec check_and_recreate(instance_id(), - resource_type(), - raw_resource_config(), - create_opts()) -> +-spec check_and_recreate( + instance_id(), + resource_type(), + raw_resource_config(), + create_opts() +) -> {ok, resource_data()} | {error, term()}. check_and_recreate(InstId, ResourceType, RawConfig, Opts) -> - check_and_do(ResourceType, RawConfig, - fun(InstConf) -> recreate(InstId, ResourceType, InstConf, Opts) end). + check_and_do( + ResourceType, + RawConfig, + fun(InstConf) -> recreate(InstId, ResourceType, InstConf, Opts) end + ). --spec check_and_recreate_local(instance_id(), - resource_type(), - raw_resource_config(), - create_opts()) -> +-spec check_and_recreate_local( + instance_id(), + resource_type(), + raw_resource_config(), + create_opts() +) -> {ok, resource_data()} | {error, term()}. check_and_recreate_local(InstId, ResourceType, RawConfig, Opts) -> - check_and_do(ResourceType, RawConfig, - fun(InstConf) -> recreate_local(InstId, ResourceType, InstConf, Opts) end). + check_and_do( + ResourceType, + RawConfig, + fun(InstConf) -> recreate_local(InstId, ResourceType, InstConf, Opts) end + ). check_and_do(ResourceType, RawConfig, Do) when is_function(Do) -> case check_config(ResourceType, RawConfig) of @@ -355,8 +419,7 @@ filter_instances(Filter) -> inc_metrics_funcs(InstId) -> OnFailed = [{fun emqx_plugin_libs_metrics:inc/3, [resource_metrics, InstId, failed]}], - OnSucc = [ {fun emqx_plugin_libs_metrics:inc/3, [resource_metrics, InstId, success]} - ], + OnSucc = [{fun emqx_plugin_libs_metrics:inc/3, [resource_metrics, InstId, success]}], {OnSucc, OnFailed}. call_instance(InstId, Query) -> diff --git a/apps/emqx_resource/src/emqx_resource_health_check.erl b/apps/emqx_resource/src/emqx_resource_health_check.erl index 0faaebabb..ab74296a5 100644 --- a/apps/emqx_resource/src/emqx_resource_health_check.erl +++ b/apps/emqx_resource/src/emqx_resource_health_check.erl @@ -15,23 +15,29 @@ %%-------------------------------------------------------------------- -module(emqx_resource_health_check). --export([ start_link/3 - , create_checker/3 - , delete_checker/1 - ]). +-export([ + start_link/3, + create_checker/3, + delete_checker/1 +]). --export([ start_health_check/3 - , health_check_timeout_checker/4 - ]). +-export([ + start_health_check/3, + health_check_timeout_checker/4 +]). -define(SUP, emqx_resource_health_check_sup). -define(ID(NAME), {resource_health_check, NAME}). child_spec(Name, Sleep, Timeout) -> - #{id => ?ID(Name), - start => {?MODULE, start_link, [Name, Sleep, Timeout]}, - restart => transient, - shutdown => 5000, type => worker, modules => [?MODULE]}. + #{ + id => ?ID(Name), + start => {?MODULE, start_link, [Name, Sleep, Timeout]}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [?MODULE] + }. start_link(Name, Sleep, Timeout) -> Pid = proc_lib:spawn_link(?MODULE, start_health_check, [Name, Sleep, Timeout]), @@ -42,19 +48,22 @@ create_checker(Name, Sleep, Timeout) -> create_checker(Name, Sleep, Retry, Timeout) -> case supervisor:start_child(?SUP, child_spec(Name, Sleep, Timeout)) of - {ok, _} -> ok; - {error, already_present} -> ok; + {ok, _} -> + ok; + {error, already_present} -> + ok; {error, {already_started, _}} when Retry == false -> ok = delete_checker(Name), create_checker(Name, Sleep, true, Timeout); - Error -> Error + Error -> + Error end. delete_checker(Name) -> case supervisor:terminate_child(?SUP, ?ID(Name)) of ok -> supervisor:delete_child(?SUP, ?ID(Name)); Error -> Error - end. + end. start_health_check(Name, Sleep, Timeout) -> Pid = self(), @@ -63,13 +72,16 @@ start_health_check(Name, Sleep, Timeout) -> health_check(Name) -> receive - {Pid, begin_health_check} -> + {Pid, begin_health_check} -> case emqx_resource:health_check(Name) of ok -> emqx_alarm:deactivate(Name); {error, _} -> - emqx_alarm:activate(Name, #{name => Name}, - <>) + emqx_alarm:activate( + Name, + #{name => Name}, + <> + ) end, Pid ! health_check_finish end, @@ -81,8 +93,11 @@ health_check_timeout_checker(Pid, Name, SleepTime, Timeout) -> receive health_check_finish -> timer:sleep(SleepTime) after Timeout -> - emqx_alarm:activate(Name, #{name => Name}, - <>), + emqx_alarm:activate( + Name, + #{name => Name}, + <> + ), emqx_resource:set_resource_status_connecting(Name), receive health_check_finish -> timer:sleep(SleepTime) diff --git a/apps/emqx_resource/src/emqx_resource_instance.erl b/apps/emqx_resource/src/emqx_resource_instance.erl index 60b2babe5..97014e413 100644 --- a/apps/emqx_resource/src/emqx_resource_instance.erl +++ b/apps/emqx_resource/src/emqx_resource_instance.erl @@ -23,25 +23,28 @@ -export([start_link/2]). %% load resource instances from *.conf files --export([ lookup/1 - , get_metrics/1 - , reset_metrics/1 - , list_all/0 - , list_group/1 - ]). +-export([ + lookup/1, + get_metrics/1, + reset_metrics/1, + list_all/0, + list_group/1 +]). --export([ hash_call/2 - , hash_call/3 - ]). +-export([ + hash_call/2, + hash_call/3 +]). %% 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 +]). -record(state, {worker_pool, worker_id}). @@ -52,8 +55,12 @@ %%------------------------------------------------------------------------------ start_link(Pool, Id) -> - gen_server:start_link({local, proc_name(?MODULE, Id)}, - ?MODULE, {Pool, Id}, []). + gen_server:start_link( + {local, proc_name(?MODULE, Id)}, + ?MODULE, + {Pool, Id}, + [] + ). %% call the worker by the hash of resource-instance-id, to make sure we always handle %% operations on the same instance in the same worker. @@ -67,8 +74,7 @@ hash_call(InstId, Request, Timeout) -> lookup(InstId) -> case ets:lookup(emqx_resource_instance, InstId) of [] -> {error, not_found}; - [{_, Group, Data}] -> - {ok, Group, Data#{id => InstId, metrics => get_metrics(InstId)}} + [{_, Group, Data}] -> {ok, Group, Data#{id => InstId, metrics => get_metrics(InstId)}} end. make_test_id() -> @@ -103,39 +109,32 @@ list_group(Group) -> %%------------------------------------------------------------------------------ -spec init({atom(), integer()}) -> - {ok, State :: state()} | {ok, State :: state(), timeout() | hibernate | {continue, term()}} | - {stop, Reason :: term()} | ignore. + {ok, State :: state()} + | {ok, State :: state(), timeout() | hibernate | {continue, term()}} + | {stop, Reason :: term()} + | ignore. init({Pool, Id}) -> true = gproc_pool:connect_worker(Pool, {Pool, Id}), {ok, #state{worker_pool = Pool, worker_id = Id}}. handle_call({create, InstId, Group, ResourceType, Config, Opts}, _From, State) -> {reply, do_create(InstId, Group, ResourceType, Config, Opts), State}; - handle_call({create_dry_run, ResourceType, Config}, _From, State) -> {reply, do_create_dry_run(ResourceType, Config), State}; - handle_call({recreate, InstId, ResourceType, Config, Opts}, _From, State) -> {reply, do_recreate(InstId, ResourceType, Config, Opts), State}; - handle_call({reset_metrics, InstId}, _From, State) -> {reply, do_reset_metrics(InstId), State}; - handle_call({remove, InstId}, _From, State) -> {reply, do_remove(InstId), State}; - handle_call({restart, InstId, Opts}, _From, State) -> {reply, do_restart(InstId, Opts), State}; - handle_call({stop, InstId}, _From, State) -> {reply, do_stop(InstId), State}; - handle_call({health_check, InstId}, _From, State) -> {reply, do_health_check(InstId), State}; - handle_call({set_resource_status_connecting, InstId}, _From, State) -> {reply, do_set_resource_status_connecting(InstId), State}; - handle_call(Req, _From, State) -> logger:error("Received unexpected call: ~p", [Req]), {reply, ignored, State}. @@ -155,14 +154,17 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ %% suppress the race condition check, as these functions are protected in gproc workers --dialyzer({nowarn_function, [ do_recreate/4 - , do_create/5 - , do_restart/2 - , do_start/5 - , do_stop/1 - , do_health_check/1 - , start_and_check/6 - ]}). +-dialyzer( + {nowarn_function, [ + do_recreate/4, + do_create/5, + do_restart/2, + do_start/5, + do_stop/1, + do_health_check/1, + start_and_check/6 + ]} +). do_recreate(InstId, ResourceType, NewConfig, Opts) -> case lookup(InstId) of @@ -185,10 +187,11 @@ do_wait_for_resource_ready(_InstId, 0) -> timeout; do_wait_for_resource_ready(InstId, Retry) -> case force_lookup(InstId) of - #{status := connected} -> ok; + #{status := connected} -> + ok; _ -> timer:sleep(100), - do_wait_for_resource_ready(InstId, Retry-1) + do_wait_for_resource_ready(InstId, Retry - 1) end. do_create(InstId, Group, ResourceType, Config, Opts) -> @@ -197,8 +200,12 @@ do_create(InstId, Group, ResourceType, Config, Opts) -> {ok, already_created}; {error, not_found} -> ok = do_start(InstId, Group, ResourceType, Config, Opts), - ok = emqx_plugin_libs_metrics:create_metrics(resource_metrics, InstId, - [matched, success, failed, exception], [matched]), + ok = emqx_plugin_libs_metrics:create_metrics( + resource_metrics, + InstId, + [matched, success, failed, exception], + [matched] + ), {ok, force_lookup(InstId)} end. @@ -212,7 +219,8 @@ do_create_dry_run(ResourceType, Config) -> {error, _} = Error -> Error; _ -> ok end; - {error, Reason, _} -> {error, Reason} + {error, Reason, _} -> + {error, Reason} end; {error, Reason} -> {error, Reason} @@ -246,13 +254,18 @@ do_restart(InstId, Opts) -> end. do_start(InstId, Group, ResourceType, Config, Opts) when is_binary(InstId) -> - InitData = #{id => InstId, mod => ResourceType, config => Config, - status => connecting, state => undefined}, + InitData = #{ + id => InstId, + mod => ResourceType, + config => Config, + status => connecting, + state => undefined + }, %% The `emqx_resource:call_start/3` need the instance exist beforehand ets:insert(emqx_resource_instance, {InstId, Group, InitData}), spawn(fun() -> - start_and_check(InstId, Group, ResourceType, Config, Opts, InitData) - end), + start_and_check(InstId, Group, ResourceType, Config, Opts, InitData) + end), _ = wait_for_resource_ready(InstId, maps:get(wait_for_resource_ready, Opts, 5000)), ok. @@ -268,9 +281,11 @@ start_and_check(InstId, Group, ResourceType, Config, Opts, Data) -> end. create_default_checker(InstId, Opts) -> - emqx_resource_health_check:create_checker(InstId, + emqx_resource_health_check:create_checker( + InstId, maps:get(health_check_interval, Opts, 15000), - maps:get(health_check_timeout, Opts, 10000)). + maps:get(health_check_timeout, Opts, 10000) + ). do_stop(InstId) when is_binary(InstId) -> do_with_group_and_instance_data(InstId, fun do_stop/2, []). @@ -291,18 +306,24 @@ do_health_check(_Group, #{state := undefined}) -> do_health_check(Group, #{id := InstId, mod := Mod, state := ResourceState0} = Data) -> case emqx_resource:call_health_check(InstId, Mod, ResourceState0) of {ok, ResourceState1} -> - ets:insert(emqx_resource_instance, - {InstId, Group, Data#{status => connected, state => ResourceState1}}), + ets:insert( + emqx_resource_instance, + {InstId, Group, Data#{status => connected, state => ResourceState1}} + ), ok; {error, Reason} -> logger:error("health check for ~p failed: ~p", [InstId, Reason]), - ets:insert(emqx_resource_instance, - {InstId, Group, Data#{status => connecting}}), + ets:insert( + emqx_resource_instance, + {InstId, Group, Data#{status => connecting}} + ), {error, Reason}; {error, Reason, ResourceState1} -> logger:error("health check for ~p failed: ~p", [InstId, Reason]), - ets:insert(emqx_resource_instance, - {InstId, Group, Data#{status => connecting, state => ResourceState1}}), + ets:insert( + emqx_resource_instance, + {InstId, Group, Data#{status => connecting, state => ResourceState1}} + ), {error, Reason} end. @@ -311,7 +332,8 @@ do_set_resource_status_connecting(InstId) -> {ok, Group, #{id := InstId} = Data} -> logger:error("health check for ~p failed: timeout", [InstId]), ets:insert(emqx_resource_instance, {InstId, Group, Data#{status => connecting}}); - Error -> {error, Error} + Error -> + {error, Error} end. %%------------------------------------------------------------------------------ diff --git a/apps/emqx_resource/src/emqx_resource_sup.erl b/apps/emqx_resource/src/emqx_resource_sup.erl index a439c76bd..770ca1fed 100644 --- a/apps/emqx_resource/src/emqx_resource_sup.erl +++ b/apps/emqx_resource/src/emqx_resource_sup.erl @@ -22,7 +22,8 @@ -export([init/1]). -define(RESOURCE_INST_MOD, emqx_resource_instance). --define(POOL_SIZE, 64). %% set a very large pool size in case all the workers busy +%% set a very large pool size in case all the workers busy +-define(POOL_SIZE, 64). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). @@ -40,27 +41,39 @@ init([]) -> ResourceInsts = [ begin ensure_pool_worker(Pool, {Pool, Idx}, Idx), - #{id => {Mod, Idx}, - start => {Mod, start_link, [Pool, Idx]}, - restart => transient, - shutdown => 5000, type => worker, modules => [Mod]} - end || Idx <- lists:seq(1, ?POOL_SIZE)], - HealthCheck = - #{id => emqx_resource_health_check_sup, - start => {emqx_resource_health_check_sup, start_link, []}, - restart => transient, - shutdown => infinity, type => supervisor, modules => [emqx_resource_health_check_sup]}, + #{ + id => {Mod, Idx}, + start => {Mod, start_link, [Pool, Idx]}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [Mod] + } + end + || Idx <- lists:seq(1, ?POOL_SIZE) + ], + HealthCheck = + #{ + id => emqx_resource_health_check_sup, + start => {emqx_resource_health_check_sup, start_link, []}, + restart => transient, + shutdown => infinity, + type => supervisor, + modules => [emqx_resource_health_check_sup] + }, {ok, {SupFlags, [HealthCheck, Metrics | ResourceInsts]}}. %% internal functions ensure_pool(Pool, Type, Opts) -> - try gproc_pool:new(Pool, Type, Opts) + try + gproc_pool:new(Pool, Type, Opts) catch error:exists -> ok end. ensure_pool_worker(Pool, Name, Slot) -> - try gproc_pool:add_worker(Pool, Name, Slot) + try + gproc_pool:add_worker(Pool, Name, Slot) catch error:exists -> ok end. diff --git a/apps/emqx_resource/src/emqx_resource_validator.erl b/apps/emqx_resource/src/emqx_resource_validator.erl index 4d745d1e3..7623ae7fa 100644 --- a/apps/emqx_resource/src/emqx_resource_validator.erl +++ b/apps/emqx_resource/src/emqx_resource_validator.erl @@ -16,10 +16,11 @@ -module(emqx_resource_validator). --export([ min/2 - , max/2 - , not_empty/1 - ]). +-export([ + min/2, + max/2, + not_empty/1 +]). max(Type, Max) -> limit(Type, '=<', Max). @@ -28,16 +29,19 @@ min(Type, Min) -> limit(Type, '>=', Min). not_empty(ErrMsg) -> - fun(<<>>) -> {error, ErrMsg}; - (_) -> ok + fun + (<<>>) -> {error, ErrMsg}; + (_) -> ok end. limit(Type, Op, Expected) -> L = len(Type), fun(Value) -> Got = L(Value), - return(erlang:Op(Got, Expected), - err_limit({Type, {Op, Expected}, {got, Got}})) + return( + erlang:Op(Got, Expected), + err_limit({Type, {Op, Expected}, {got, Got}}) + ) end. len(array) -> fun erlang:length/1; @@ -48,5 +52,4 @@ err_limit({Type, {Op, Expected}, {got, Got}}) -> io_lib:format("Expect the ~ts value ~ts ~p but got: ~p", [Type, Op, Expected, Got]). return(true, _) -> ok; -return(false, Error) -> - {error, Error}. +return(false, Error) -> {error, Error}. diff --git a/apps/emqx_resource/src/proto/emqx_resource_proto_v1.erl b/apps/emqx_resource/src/proto/emqx_resource_proto_v1.erl index f39533c82..0aa4fd40b 100644 --- a/apps/emqx_resource/src/proto/emqx_resource_proto_v1.erl +++ b/apps/emqx_resource/src/proto/emqx_resource_proto_v1.erl @@ -18,52 +18,58 @@ -behaviour(emqx_bpapi). --export([ introduced_in/0 +-export([ + introduced_in/0, - , create/5 - , create_dry_run/2 - , recreate/4 - , remove/1 - , reset_metrics/1 - ]). + create/5, + create_dry_run/2, + recreate/4, + remove/1, + reset_metrics/1 +]). -include_lib("emqx/include/bpapi.hrl"). introduced_in() -> "5.0.0". --spec create( emqx_resource:instance_id() - , emqx_resource:resource_group() - , emqx_resource:resource_type() - , emqx_resource:resource_config() - , emqx_resource:create_opts() - ) -> - emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). +-spec create( + emqx_resource:instance_id(), + emqx_resource:resource_group(), + emqx_resource:resource_type(), + emqx_resource:resource_config(), + emqx_resource:create_opts() +) -> + emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). create(InstId, Group, ResourceType, Config, Opts) -> - emqx_cluster_rpc:multicall(emqx_resource, create_local, [InstId, Group, ResourceType, Config, Opts]). + emqx_cluster_rpc:multicall(emqx_resource, create_local, [ + InstId, Group, ResourceType, Config, Opts + ]). --spec create_dry_run( emqx_resource:resource_type() - , emqx_resource:resource_config() - ) -> - emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). +-spec create_dry_run( + emqx_resource:resource_type(), + emqx_resource:resource_config() +) -> + emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). create_dry_run(ResourceType, Config) -> emqx_cluster_rpc:multicall(emqx_resource, create_dry_run_local, [ResourceType, Config]). --spec recreate( emqx_resource:instance_id() - , emqx_resource:resource_type() - , emqx_resource:resource_config() - , emqx_resource:create_opts() - ) -> - emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). +-spec recreate( + emqx_resource:instance_id(), + emqx_resource:resource_type(), + emqx_resource:resource_config(), + emqx_resource:create_opts() +) -> + emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()). recreate(InstId, ResourceType, Config, Opts) -> emqx_cluster_rpc:multicall(emqx_resource, recreate_local, [InstId, ResourceType, Config, Opts]). -spec remove(emqx_resource:instance_id()) -> - emqx_cluster_rpc:multicall_return(ok). + emqx_cluster_rpc:multicall_return(ok). remove(InstId) -> emqx_cluster_rpc:multicall(emqx_resource, remove_local, [InstId]). -spec reset_metrics(emqx_resource:instance_id()) -> - emqx_cluster_rpc:multicall_return(ok). + emqx_cluster_rpc:multicall_return(ok). reset_metrics(InstId) -> emqx_cluster_rpc:multicall(emqx_resource, reset_metrics_local, [InstId]). diff --git a/apps/emqx_resource/test/emqx_resource_SUITE.erl b/apps/emqx_resource/test/emqx_resource_SUITE.erl index 8a952e036..973cf0ab7 100644 --- a/apps/emqx_resource/test/emqx_resource_SUITE.erl +++ b/apps/emqx_resource/test/emqx_resource_SUITE.erl @@ -60,22 +60,25 @@ t_check_config(_) -> t_create_remove(_) -> {error, _} = emqx_resource:check_and_create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{unknown => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{unknown => test_resource} + ), {ok, _} = emqx_resource:create( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource} + ), emqx_resource:recreate( - ?ID, - ?TEST_RESOURCE, - #{name => test_resource}, - #{}), + ?ID, + ?TEST_RESOURCE, + #{name => test_resource}, + #{} + ), #{pid := Pid} = emqx_resource:query(?ID, get_state), ?assert(is_process_alive(Pid)), @@ -87,22 +90,25 @@ t_create_remove(_) -> t_create_remove_local(_) -> {error, _} = emqx_resource:check_and_create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{unknown => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{unknown => test_resource} + ), {ok, _} = emqx_resource:create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource} + ), emqx_resource:recreate_local( - ?ID, - ?TEST_RESOURCE, - #{name => test_resource}, - #{}), + ?ID, + ?TEST_RESOURCE, + #{name => test_resource}, + #{} + ), #{pid := Pid} = emqx_resource:query(?ID, get_state), ?assert(is_process_alive(Pid)), @@ -110,10 +116,11 @@ t_create_remove_local(_) -> emqx_resource:set_resource_status_connecting(?ID), emqx_resource:recreate_local( - ?ID, - ?TEST_RESOURCE, - #{name => test_resource}, - #{}), + ?ID, + ?TEST_RESOURCE, + #{name => test_resource}, + #{} + ), ok = emqx_resource:remove_local(?ID), {error, _} = emqx_resource:remove_local(?ID), @@ -122,10 +129,11 @@ t_create_remove_local(_) -> t_query(_) -> {ok, _} = emqx_resource:create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource} + ), Pid = self(), Success = fun() -> Pid ! success end, @@ -142,28 +150,32 @@ t_query(_) -> ?assert(false) end, - ?assertMatch({error, {emqx_resource, #{reason := not_found}}}, - emqx_resource:query(<<"unknown">>, get_state)), + ?assertMatch( + {error, {emqx_resource, #{reason := not_found}}}, + emqx_resource:query(<<"unknown">>, get_state) + ), ok = emqx_resource:remove_local(?ID). t_healthy_timeout(_) -> {ok, _} = emqx_resource:create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => <<"test_resource">>}, - #{health_check_timeout => 200}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => <<"test_resource">>}, + #{health_check_timeout => 200} + ), timer:sleep(500), ok = emqx_resource:remove_local(?ID). t_healthy(_) -> {ok, _} = emqx_resource:create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => <<"test_resource">>}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => <<"test_resource">>} + ), timer:sleep(400), emqx_resource_health_check:create_checker(?ID, 15000, 10000), @@ -175,38 +187,44 @@ t_healthy(_) -> ?assertMatch( [#{status := connected}], - emqx_resource:list_instances_verbose()), + emqx_resource:list_instances_verbose() + ), erlang:exit(Pid, shutdown), ?assertEqual( {error, dead}, - emqx_resource:health_check(?ID)), + emqx_resource:health_check(?ID) + ), ?assertMatch( [#{status := connecting}], - emqx_resource:list_instances_verbose()), + emqx_resource:list_instances_verbose() + ), ok = emqx_resource:remove_local(?ID). t_stop_start(_) -> {error, _} = emqx_resource:check_and_create( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{unknown => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{unknown => test_resource} + ), {ok, _} = emqx_resource:check_and_create( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{<<"name">> => <<"test_resource">>}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{<<"name">> => <<"test_resource">>} + ), {ok, _} = emqx_resource:check_and_recreate( - ?ID, - ?TEST_RESOURCE, - #{<<"name">> => <<"test_resource">>}, - #{}), + ?ID, + ?TEST_RESOURCE, + #{<<"name">> => <<"test_resource">>}, + #{} + ), #{pid := Pid0} = emqx_resource:query(?ID, get_state), @@ -216,8 +234,10 @@ t_stop_start(_) -> ?assertNot(is_process_alive(Pid0)), - ?assertMatch({error, {emqx_resource, #{reason := disconnected}}}, - emqx_resource:query(?ID, get_state)), + ?assertMatch( + {error, {emqx_resource, #{reason := disconnected}}}, + emqx_resource:query(?ID, get_state) + ), ok = emqx_resource:restart(?ID), @@ -229,22 +249,25 @@ t_stop_start(_) -> t_stop_start_local(_) -> {error, _} = emqx_resource:check_and_create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{unknown => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{unknown => test_resource} + ), {ok, _} = emqx_resource:check_and_create_local( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{<<"name">> => <<"test_resource">>}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{<<"name">> => <<"test_resource">>} + ), {ok, _} = emqx_resource:check_and_recreate_local( - ?ID, - ?TEST_RESOURCE, - #{<<"name">> => <<"test_resource">>}, - #{}), + ?ID, + ?TEST_RESOURCE, + #{<<"name">> => <<"test_resource">>}, + #{} + ), #{pid := Pid0} = emqx_resource:query(?ID, get_state), @@ -254,8 +277,10 @@ t_stop_start_local(_) -> ?assertNot(is_process_alive(Pid0)), - ?assertMatch({error, {emqx_resource, #{reason := disconnected}}}, - emqx_resource:query(?ID, get_state)), + ?assertMatch( + {error, {emqx_resource, #{reason := disconnected}}}, + emqx_resource:query(?ID, get_state) + ), ok = emqx_resource:restart(?ID), @@ -265,60 +290,73 @@ t_stop_start_local(_) -> t_list_filter(_) -> {ok, _} = emqx_resource:create_local( - emqx_resource:generate_id(<<"a">>), - <<"group1">>, - ?TEST_RESOURCE, - #{name => a}), + emqx_resource:generate_id(<<"a">>), + <<"group1">>, + ?TEST_RESOURCE, + #{name => a} + ), {ok, _} = emqx_resource:create_local( - emqx_resource:generate_id(<<"a">>), - <<"group2">>, - ?TEST_RESOURCE, - #{name => grouped_a}), + emqx_resource:generate_id(<<"a">>), + <<"group2">>, + ?TEST_RESOURCE, + #{name => grouped_a} + ), [Id1] = emqx_resource:list_group_instances(<<"group1">>), ?assertMatch( {ok, <<"group1">>, #{config := #{name := a}}}, - emqx_resource:get_instance(Id1)), + emqx_resource:get_instance(Id1) + ), [Id2] = emqx_resource:list_group_instances(<<"group2">>), ?assertMatch( {ok, <<"group2">>, #{config := #{name := grouped_a}}}, - emqx_resource:get_instance(Id2)). + emqx_resource:get_instance(Id2) + ). t_create_dry_run_local(_) -> ?assertEqual( - ok, - emqx_resource:create_dry_run_local( - ?TEST_RESOURCE, - #{name => test_resource, register => true})), + ok, + emqx_resource:create_dry_run_local( + ?TEST_RESOURCE, + #{name => test_resource, register => true} + ) + ), ?assertEqual(undefined, whereis(test_resource)). t_create_dry_run_local_failed(_) -> - {Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE, - #{cteate_error => true}), + {Res, _} = emqx_resource:create_dry_run_local( + ?TEST_RESOURCE, + #{cteate_error => true} + ), ?assertEqual(error, Res), - {Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE, - #{name => test_resource, health_check_error => true}), + {Res, _} = emqx_resource:create_dry_run_local( + ?TEST_RESOURCE, + #{name => test_resource, health_check_error => true} + ), ?assertEqual(error, Res), - {Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE, - #{name => test_resource, stop_error => true}), + {Res, _} = emqx_resource:create_dry_run_local( + ?TEST_RESOURCE, + #{name => test_resource, stop_error => true} + ), ?assertEqual(error, Res). t_test_func(_) -> ?assertEqual(ok, erlang:apply(emqx_resource_validator:not_empty("not_empty"), [<<"someval">>])), ?assertEqual(ok, erlang:apply(emqx_resource_validator:min(int, 3), [4])), - ?assertEqual(ok, erlang:apply(emqx_resource_validator:max(array, 10), [[a,b,c,d]])), + ?assertEqual(ok, erlang:apply(emqx_resource_validator:max(array, 10), [[a, b, c, d]])), ?assertEqual(ok, erlang:apply(emqx_resource_validator:max(string, 10), ["less10"])). t_reset_metrics(_) -> {ok, _} = emqx_resource:create( - ?ID, - ?DEFAULT_RESOURCE_GROUP, - ?TEST_RESOURCE, - #{name => test_resource}), + ?ID, + ?DEFAULT_RESOURCE_GROUP, + ?TEST_RESOURCE, + #{name => test_resource} + ), #{pid := Pid} = emqx_resource:query(?ID, get_state), emqx_resource:reset_metrics(?ID), diff --git a/apps/emqx_resource/test/emqx_test_resource.erl b/apps/emqx_resource/test/emqx_test_resource.erl index c0c4b4ff5..783393e74 100644 --- a/apps/emqx_resource/test/emqx_test_resource.erl +++ b/apps/emqx_resource/test/emqx_test_resource.erl @@ -21,17 +21,21 @@ -behaviour(emqx_resource). %% callbacks of behaviour emqx_resource --export([ on_start/2 - , on_stop/2 - , on_query/4 - , on_health_check/2 - ]). +-export([ + on_start/2, + on_stop/2, + on_query/4, + on_health_check/2 +]). %% callbacks for emqx_resource config schema -export([roots/0]). -roots() -> [{name, fun name/1}, - {register, fun register/1}]. +roots() -> + [ + {name, fun name/1}, + {register, fun register/1} + ]. name(type) -> atom(); name(required) -> true; @@ -46,21 +50,27 @@ on_start(_InstId, #{create_error := true}) -> error("some error"); on_start(InstId, #{name := Name, stop_error := true} = Opts) -> Register = maps:get(register, Opts, false), - {ok, #{name => Name, - id => InstId, - stop_error => true, - pid => spawn_dummy_process(Name, Register)}}; + {ok, #{ + name => Name, + id => InstId, + stop_error => true, + pid => spawn_dummy_process(Name, Register) + }}; on_start(InstId, #{name := Name, health_check_error := true} = Opts) -> Register = maps:get(register, Opts, false), - {ok, #{name => Name, - id => InstId, - health_check_error => true, - pid => spawn_dummy_process(Name, Register)}}; + {ok, #{ + name => Name, + id => InstId, + health_check_error => true, + pid => spawn_dummy_process(Name, Register) + }}; on_start(InstId, #{name := Name} = Opts) -> Register = maps:get(register, Opts, false), - {ok, #{name => Name, - id => InstId, - pid => spawn_dummy_process(Name, Register)}}. + {ok, #{ + name => Name, + id => InstId, + pid => spawn_dummy_process(Name, Register) + }}. on_stop(_InstId, #{stop_error := true}) -> {error, stop_error}; @@ -86,13 +96,15 @@ on_health_check(_InstId, State = #{pid := Pid}) -> spawn_dummy_process(Name, Register) -> spawn( - fun() -> - true = case Register of - true -> register(Name, self()); - _ -> true - end, - Ref = make_ref(), - receive - Ref -> ok - end - end). + fun() -> + true = + case Register of + true -> register(Name, self()); + _ -> true + end, + Ref = make_ref(), + receive + Ref -> ok + end + end + ). diff --git a/apps/emqx_rule_engine/include/rule_engine.hrl b/apps/emqx_rule_engine/include/rule_engine.hrl index a8b8ae1bb..20c339377 100644 --- a/apps/emqx_rule_engine/include/rule_engine.hrl +++ b/apps/emqx_rule_engine/include/rule_engine.hrl @@ -23,7 +23,7 @@ -type rule_id() :: binary(). -type rule_name() :: binary(). --type mf() :: {Module::atom(), Fun::atom()}. +-type mf() :: {Module :: atom(), Fun :: atom()}. -type hook() :: atom() | 'any'. -type topic() :: binary(). @@ -36,60 +36,73 @@ -type bridge_channel_id() :: binary(). -type output_fun_args() :: map(). --type output() :: #{ - mod := builtin_output_module() | module(), - func := builtin_output_func() | atom(), - args => output_fun_args() -} | bridge_channel_id(). +-type output() :: + #{ + mod := builtin_output_module() | module(), + func := builtin_output_func() | atom(), + args => output_fun_args() + } + | bridge_channel_id(). -type rule() :: - #{ id := rule_id() - , name := binary() - , sql := binary() - , outputs := [output()] - , enable := boolean() - , description => binary() - , created_at := integer() %% epoch in millisecond precision - , updated_at := integer() %% epoch in millisecond precision - , from := list(topic()) - , is_foreach := boolean() - , fields := list() - , doeach := term() - , incase := term() - , conditions := tuple() - }. + #{ + id := rule_id(), + name := binary(), + sql := binary(), + outputs := [output()], + enable := boolean(), + description => binary(), + %% epoch in millisecond precision + created_at := integer(), + %% epoch in millisecond precision + updated_at := integer(), + from := list(topic()), + is_foreach := boolean(), + fields := list(), + doeach := term(), + incase := term(), + conditions := tuple() + }. %% Arithmetic operators --define(is_arith(Op), (Op =:= '+' orelse - Op =:= '-' orelse - Op =:= '*' orelse - Op =:= '/' orelse - Op =:= 'div')). +-define(is_arith(Op), + (Op =:= '+' orelse + Op =:= '-' orelse + Op =:= '*' orelse + Op =:= '/' orelse + Op =:= 'div') +). %% Compare operators --define(is_comp(Op), (Op =:= '=' orelse - Op =:= '=~' orelse - Op =:= '>' orelse - Op =:= '<' orelse - Op =:= '<=' orelse - Op =:= '>=' orelse - Op =:= '<>' orelse - Op =:= '!=')). +-define(is_comp(Op), + (Op =:= '=' orelse + Op =:= '=~' orelse + Op =:= '>' orelse + Op =:= '<' orelse + Op =:= '<=' orelse + Op =:= '>=' orelse + Op =:= '<>' orelse + Op =:= '!=') +). %% Logical operators -define(is_logical(Op), (Op =:= 'and' orelse Op =:= 'or')). -define(RAISE(_EXP_, _ERROR_), - ?RAISE(_EXP_, _ = do_nothing, _ERROR_)). + ?RAISE(_EXP_, _ = do_nothing, _ERROR_) +). -define(RAISE(_EXP_, _EXP_ON_FAIL_, _ERROR_), - fun() -> - try (_EXP_) - catch _EXCLASS_:_EXCPTION_:_ST_ -> + fun() -> + try + (_EXP_) + catch + _EXCLASS_:_EXCPTION_:_ST_ -> _EXP_ON_FAIL_, throw(_ERROR_) - end - end()). + end + end() +). %% Tables -define(RULE_TAB, emqx_rule_engine). diff --git a/apps/emqx_rule_engine/rebar.config b/apps/emqx_rule_engine/rebar.config index 72be78a1d..2bc40e977 100644 --- a/apps/emqx_rule_engine/rebar.config +++ b/apps/emqx_rule_engine/rebar.config @@ -1,28 +1,35 @@ %% -*- mode: erlang -*- -{deps, [ {emqx, {path, "../emqx"}} - ]}. +{deps, [{emqx, {path, "../emqx"}}]}. -{erl_opts, [warn_unused_vars, - warn_shadow_vars, - warn_unused_import, - warn_obsolete_guard, - no_debug_info, - compressed, %% for edge - {parse_transform} - ]}. +{erl_opts, [ + warn_unused_vars, + warn_shadow_vars, + warn_unused_import, + warn_obsolete_guard, + no_debug_info, + %% for edge + compressed, + {parse_transform} +]}. {overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}. {edoc_opts, [{preprocess, true}]}. -{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}. {plugins, [rebar3_proper]}. + +{project_plugins, [erlfmt]}. 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 9997043ff..2623dd0a7 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_api_schema.erl @@ -6,8 +6,7 @@ -include_lib("hocon/include/hoconsc.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ check_params/2 - ]). +-export([check_params/2]). -export([roots/0, fields/1]). @@ -20,10 +19,11 @@ check_params(Params, Tag) -> try hocon_tconf:check_plain(?MODULE, #{BTag => Params}, Opts, [Tag]) of #{Tag := Checked} -> {ok, Checked} catch - throw : Reason -> - ?SLOG(error, #{msg => "check_rule_params_failed", - reason => Reason - }), + throw:Reason -> + ?SLOG(error, #{ + msg => "check_rule_params_failed", + reason => Reason + }), {error, Reason} end. @@ -31,226 +31,285 @@ check_params(Params, Tag) -> %% Hocon Schema Definitions roots() -> - [ {"rule_creation", sc(ref("rule_creation"), #{desc => ?DESC("root_rule_creation")})} - , {"rule_info", sc(ref("rule_info"), #{desc => ?DESC("root_rule_info")})} - , {"rule_events", sc(ref("rule_events"), #{desc => ?DESC("root_rule_events")})} - , {"rule_test", sc(ref("rule_test"), #{desc => ?DESC("root_rule_test")})} + [ + {"rule_creation", sc(ref("rule_creation"), #{desc => ?DESC("root_rule_creation")})}, + {"rule_info", sc(ref("rule_info"), #{desc => ?DESC("root_rule_info")})}, + {"rule_events", sc(ref("rule_events"), #{desc => ?DESC("root_rule_events")})}, + {"rule_test", sc(ref("rule_test"), #{desc => ?DESC("root_rule_test")})} ]. fields("rule_creation") -> emqx_rule_engine_schema:fields("rules"); - fields("rule_info") -> - [ rule_id() - , {"metrics", sc(ref("metrics"), #{desc => ?DESC("ri_metrics")})} - , {"node_metrics", sc(hoconsc:array(ref("node_metrics")), - #{ desc => ?DESC("ri_node_metrics") - })} - , {"from", sc(hoconsc:array(binary()), - #{desc => ?DESC("ri_from"), example => "t/#"})} - , {"created_at", sc(binary(), - #{ desc => ?DESC("ri_created_at") - , example => "2021-12-01T15:00:43.153+08:00" - })} + [ + rule_id(), + {"metrics", sc(ref("metrics"), #{desc => ?DESC("ri_metrics")})}, + {"node_metrics", + sc( + hoconsc:array(ref("node_metrics")), + #{desc => ?DESC("ri_node_metrics")} + )}, + {"from", + sc( + hoconsc:array(binary()), + #{desc => ?DESC("ri_from"), example => "t/#"} + )}, + {"created_at", + sc( + binary(), + #{ + desc => ?DESC("ri_created_at"), + example => "2021-12-01T15:00:43.153+08:00" + } + )} ] ++ fields("rule_creation"); - %% 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 => ?DESC("rs_event"), required => true})} - , {"title", sc(binary(), #{desc => ?DESC("rs_title"), example => "some title"})} - , {"description", sc(binary(), #{desc => ?DESC("rs_description"), example => "some desc"})} - , {"columns", sc(map(), #{desc => ?DESC("rs_columns")})} - , {"test_columns", sc(map(), #{desc => ?DESC("rs_test_columns")})} - , {"sql_example", sc(binary(), #{desc => ?DESC("rs_sql_example")})} + ETopics = [ + binary_to_atom(emqx_rule_events:event_topic(E)) + || E <- emqx_rule_events:event_names() + ], + [ + {"event", sc(hoconsc:enum(ETopics), #{desc => ?DESC("rs_event"), required => true})}, + {"title", sc(binary(), #{desc => ?DESC("rs_title"), example => "some title"})}, + {"description", sc(binary(), #{desc => ?DESC("rs_description"), example => "some desc"})}, + {"columns", sc(map(), #{desc => ?DESC("rs_columns")})}, + {"test_columns", sc(map(), #{desc => ?DESC("rs_test_columns")})}, + {"sql_example", sc(binary(), #{desc => ?DESC("rs_sql_example")})} ]; - fields("rule_test") -> - [ {"context", sc(hoconsc:union([ ref("ctx_pub") - , ref("ctx_sub") - , ref("ctx_unsub") - , ref("ctx_delivered") - , ref("ctx_acked") - , ref("ctx_dropped") - , ref("ctx_connected") - , ref("ctx_disconnected") - , ref("ctx_connack") - , ref("ctx_check_authz_complete") - , ref("ctx_bridge_mqtt") - ]), - #{desc => ?DESC("test_context"), - default => #{}})} - , {"sql", sc(binary(), #{desc => ?DESC("test_sql"), required => true})} + [ + {"context", + sc( + hoconsc:union([ + ref("ctx_pub"), + ref("ctx_sub"), + ref("ctx_unsub"), + ref("ctx_delivered"), + ref("ctx_acked"), + ref("ctx_dropped"), + ref("ctx_connected"), + ref("ctx_disconnected"), + ref("ctx_connack"), + ref("ctx_check_authz_complete"), + ref("ctx_bridge_mqtt") + ]), + #{ + desc => ?DESC("test_context"), + default => #{} + } + )}, + {"sql", sc(binary(), #{desc => ?DESC("test_sql"), required => true})} ]; - fields("metrics") -> - [ {"sql.matched", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_sql_matched") - })} - , {"sql.matched.rate", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate") })} - , {"sql.matched.rate.max", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate_max") })} - , {"sql.matched.rate.last5m", sc(float(), - #{desc => ?DESC("metrics_sql_matched_rate_last5m") })} - , {"sql.passed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_passed") })} - , {"sql.failed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_failed") })} - , {"sql.failed.exception", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_sql_failed_exception") - })} - , {"sql.failed.unknown", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_sql_failed_unknown") - })} - , {"outputs.total", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_outputs_total") - })} - , {"outputs.success", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_outputs_success") - })} - , {"outputs.failed", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_outputs_failed") - })} - , {"outputs.failed.out_of_service", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_outputs_failed_out_of_service") - })} - , {"outputs.failed.unknown", sc(non_neg_integer(), #{ - desc => ?DESC("metrics_outputs_failed_unknown") - })} + [ + {"sql.matched", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_sql_matched") + })}, + {"sql.matched.rate", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate")})}, + {"sql.matched.rate.max", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate_max")})}, + {"sql.matched.rate.last5m", + sc( + float(), + #{desc => ?DESC("metrics_sql_matched_rate_last5m")} + )}, + {"sql.passed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_passed")})}, + {"sql.failed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_failed")})}, + {"sql.failed.exception", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_sql_failed_exception") + })}, + {"sql.failed.unknown", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_sql_failed_unknown") + })}, + {"outputs.total", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_outputs_total") + })}, + {"outputs.success", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_outputs_success") + })}, + {"outputs.failed", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_outputs_failed") + })}, + {"outputs.failed.out_of_service", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_outputs_failed_out_of_service") + })}, + {"outputs.failed.unknown", + sc(non_neg_integer(), #{ + desc => ?DESC("metrics_outputs_failed_unknown") + })} ]; - fields("node_metrics") -> - [ {"node", sc(binary(), #{desc => ?DESC("node_node"), example => "emqx@127.0.0.1"})} - ] ++ fields("metrics"); - + [{"node", sc(binary(), #{desc => ?DESC("node_node"), example => "emqx@127.0.0.1"})}] ++ + fields("metrics"); fields("ctx_pub") -> - [ {"event_type", sc(message_publish, #{desc => ?DESC("event_event_type"), required => true})} - , {"id", sc(binary(), #{desc => ?DESC("event_id")})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"payload", sc(binary(), #{desc => ?DESC("event_payload")})} - , {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"publish_received_at", sc(integer(), #{ - desc => ?DESC("event_publish_received_at")})} + [ + {"event_type", sc(message_publish, #{desc => ?DESC("event_event_type"), required => true})}, + {"id", sc(binary(), #{desc => ?DESC("event_id")})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}, + {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"publish_received_at", + sc(integer(), #{ + desc => ?DESC("event_publish_received_at") + })} ] ++ [qos()]; - fields("ctx_sub") -> - [ {"event_type", sc(session_subscribed, #{desc => ?DESC("event_event_type"), required => true})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"payload", sc(binary(), #{desc => ?DESC("event_payload")})} - , {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"publish_received_at", sc(integer(), #{ - desc => ?DESC("event_publish_received_at")})} + [ + {"event_type", + sc(session_subscribed, #{desc => ?DESC("event_event_type"), required => true})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}, + {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"publish_received_at", + sc(integer(), #{ + desc => ?DESC("event_publish_received_at") + })} ] ++ [qos()]; - fields("ctx_unsub") -> - [{"event_type", sc(session_unsubscribed, #{desc => ?DESC("event_event_type"), required => true})}] ++ - proplists:delete("event_type", fields("ctx_sub")); - + [ + {"event_type", + sc(session_unsubscribed, #{desc => ?DESC("event_event_type"), required => true})} + ] ++ + proplists:delete("event_type", fields("ctx_sub")); fields("ctx_delivered") -> - [ {"event_type", sc(message_delivered, #{desc => ?DESC("event_event_type"), required => true})} - , {"id", sc(binary(), #{desc => ?DESC("event_id")})} - , {"from_clientid", sc(binary(), #{desc => ?DESC("event_from_clientid")})} - , {"from_username", sc(binary(), #{desc => ?DESC("event_from_username")})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"payload", sc(binary(), #{desc => ?DESC("event_payload")})} - , {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"publish_received_at", sc(integer(), #{ - desc => ?DESC("event_publish_received_at")})} + [ + {"event_type", + sc(message_delivered, #{desc => ?DESC("event_event_type"), required => true})}, + {"id", sc(binary(), #{desc => ?DESC("event_id")})}, + {"from_clientid", sc(binary(), #{desc => ?DESC("event_from_clientid")})}, + {"from_username", sc(binary(), #{desc => ?DESC("event_from_username")})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}, + {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"publish_received_at", + sc(integer(), #{ + desc => ?DESC("event_publish_received_at") + })} ] ++ [qos()]; - fields("ctx_acked") -> [{"event_type", sc(message_acked, #{desc => ?DESC("event_event_type"), required => true})}] ++ - proplists:delete("event_type", fields("ctx_delivered")); - + proplists:delete("event_type", fields("ctx_delivered")); fields("ctx_dropped") -> - [ {"event_type", sc(message_dropped, #{desc => ?DESC("event_event_type"), required => true})} - , {"id", sc(binary(), #{desc => ?DESC("event_id")})} - , {"reason", sc(binary(), #{desc => ?DESC("event_ctx_dropped")})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"payload", sc(binary(), #{desc => ?DESC("event_payload")})} - , {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"publish_received_at", sc(integer(), #{ - desc => ?DESC("event_publish_received_at")})} + [ + {"event_type", sc(message_dropped, #{desc => ?DESC("event_event_type"), required => true})}, + {"id", sc(binary(), #{desc => ?DESC("event_id")})}, + {"reason", sc(binary(), #{desc => ?DESC("event_ctx_dropped")})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}, + {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"publish_received_at", + sc(integer(), #{ + desc => ?DESC("event_publish_received_at") + })} ] ++ [qos()]; - fields("ctx_connected") -> - [ {"event_type", sc(client_connected, #{desc => ?DESC("event_event_type"), required => true})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"mountpoint", sc(binary(), #{desc => ?DESC("event_mountpoint")})} - , {"peername", sc(binary(), #{desc => ?DESC("event_peername")})} - , {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})} - , {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})} - , {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})} - , {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})} - , {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})} - , {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})} - , {"is_bridge", sc(boolean(), #{desc => ?DESC("event_is_bridge"), default => false})} - , {"connected_at", sc(integer(), #{ - desc => ?DESC("event_connected_at")})} + [ + {"event_type", + sc(client_connected, #{desc => ?DESC("event_event_type"), required => true})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"mountpoint", sc(binary(), #{desc => ?DESC("event_mountpoint")})}, + {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}, + {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}, + {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})}, + {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})}, + {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})}, + {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})}, + {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})}, + {"is_bridge", sc(boolean(), #{desc => ?DESC("event_is_bridge"), default => false})}, + {"connected_at", + sc(integer(), #{ + desc => ?DESC("event_connected_at") + })} ]; - fields("ctx_disconnected") -> - [ {"event_type", sc(client_disconnected, #{desc => ?DESC("event_event_type"), required => true})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"reason", sc(binary(), #{desc => ?DESC("event_ctx_disconnected_reason")})} - , {"peername", sc(binary(), #{desc => ?DESC("event_peername")})} - , {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})} - , {"disconnected_at", sc(integer(), #{ - desc => ?DESC("event_ctx_disconnected_da")})} + [ + {"event_type", + sc(client_disconnected, #{desc => ?DESC("event_event_type"), required => true})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"reason", sc(binary(), #{desc => ?DESC("event_ctx_disconnected_reason")})}, + {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}, + {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}, + {"disconnected_at", + sc(integer(), #{ + desc => ?DESC("event_ctx_disconnected_da") + })} ]; - fields("ctx_connack") -> - [ {"event_type", sc(client_connack, #{desc => ?DESC("event_event_type"), required => true})} - , {"reason_code", sc(binary(), #{desc => ?DESC("event_ctx_connack_reason_code")})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"peername", sc(binary(), #{desc => ?DESC("event_peername")})} - , {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})} - , {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})} - , {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})} - , {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})} - , {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})} - , {"connected_at", sc(integer(), #{ - desc => ?DESC("event_connected_at")})} + [ + {"event_type", sc(client_connack, #{desc => ?DESC("event_event_type"), required => true})}, + {"reason_code", sc(binary(), #{desc => ?DESC("event_ctx_connack_reason_code")})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}, + {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}, + {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})}, + {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})}, + {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})}, + {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})}, + {"connected_at", + sc(integer(), #{ + desc => ?DESC("event_connected_at") + })} ]; fields("ctx_check_authz_complete") -> - [ {"event_type", sc(client_check_authz_complete, #{desc => ?DESC("event_event_type"), required => true})} - , {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})} - , {"username", sc(binary(), #{desc => ?DESC("event_username")})} - , {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"action", sc(binary(), #{desc => ?DESC("event_action")})} - , {"authz_source", sc(binary(), #{desc => ?DESC("event_authz_source")})} - , {"result", sc(binary(), #{desc => ?DESC("event_result")})} + [ + {"event_type", + sc(client_check_authz_complete, #{desc => ?DESC("event_event_type"), required => true})}, + {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}, + {"username", sc(binary(), #{desc => ?DESC("event_username")})}, + {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"action", sc(binary(), #{desc => ?DESC("event_action")})}, + {"authz_source", sc(binary(), #{desc => ?DESC("event_authz_source")})}, + {"result", sc(binary(), #{desc => ?DESC("event_result")})} ]; fields("ctx_bridge_mqtt") -> - [ {"event_type", sc('$bridges/mqtt:*', #{desc => ?DESC("event_event_type"), required => true})} - , {"id", sc(binary(), #{desc => ?DESC("event_id")})} - , {"payload", sc(binary(), #{desc => ?DESC("event_payload")})} - , {"topic", sc(binary(), #{desc => ?DESC("event_topic")})} - , {"server", sc(binary(), #{desc => ?DESC("event_server")})} - , {"dup", sc(binary(), #{desc => ?DESC("event_dup")})} - , {"retain", sc(binary(), #{desc => ?DESC("event_retain")})} - , {"message_received_at", sc(integer(), #{ - desc => ?DESC("event_publish_received_at")})} + [ + {"event_type", + sc('$bridges/mqtt:*', #{desc => ?DESC("event_event_type"), required => true})}, + {"id", sc(binary(), #{desc => ?DESC("event_id")})}, + {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}, + {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}, + {"server", sc(binary(), #{desc => ?DESC("event_server")})}, + {"dup", sc(binary(), #{desc => ?DESC("event_dup")})}, + {"retain", sc(binary(), #{desc => ?DESC("event_retain")})}, + {"message_received_at", + sc(integer(), #{ + desc => ?DESC("event_publish_received_at") + })} ] ++ [qos()]. qos() -> {"qos", sc(emqx_schema:qos(), #{desc => ?DESC("event_qos")})}. rule_id() -> - {"id", sc(binary(), - #{ desc => ?DESC("rule_id"), required => true - , example => "293fb66f" - })}. + {"id", + sc( + binary(), + #{ + desc => ?DESC("rule_id"), + required => true, + example => "293fb66f" + } + )}. sc(Type, Meta) -> hoconsc:mk(Type, Meta). ref(Field) -> hoconsc:ref(?MODULE, Field). diff --git a/apps/emqx_rule_engine/src/emqx_rule_date.erl b/apps/emqx_rule_engine/src/emqx_rule_date.erl index a06571b64..83056754b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_date.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_date.erl @@ -18,19 +18,27 @@ -export([date/3, date/4, parse_date/4]). --export([ is_int_char/1 - , is_symbol_char/1 - , is_m_char/1 - ]). +-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 + %%year() + year = "1970" :: string(), + %%month() + month = "1" :: string(), + %%day() + day = "1" :: string(), + %%hour() + hour = "0" :: string(), + %%minute() %% epoch in millisecond precision + minute = "0" :: string(), + %%second() %% epoch in millisecond precision + second = "0" :: string(), + %%integer() %% zone maybe some value + zone = "+00:00" :: string() }). %% -type time_unit() :: 'microsecond' @@ -42,43 +50,59 @@ date(TimeUnit, Offset, FormatString) -> date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)). date(TimeUnit, Offset, FormatString, TimeEpoch) -> - [Head|Other] = string:split(FormatString, "%", all), + [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), + 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), + [Head | Other] = string:split(FormatString, "%", all), R = create_tag([{st, Head}], Other), - IsZ = fun(V) -> case V of - {tag, $Z} -> true; - _ -> false - end end, + 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, + 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), + 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 +mlist(R) -> + %% %H Shows hour in 24-hour format [15] + [ + {$H, R#result.hour}, + %% %M Displays minutes [00-59] + {$M, R#result.minute}, + %% %S Displays seconds [00-59] + {$S, R#result.second}, + %% %y Displays year YYYY [2021] + {$y, R#result.year}, + %% %m Displays the number of the month [01-12] + {$m, R#result.month}, + %% %d Displays the number of the month [01-12] + {$d, R#result.day}, + %% %Z Displays Time zone + {$Z, R#result.zone} ]. rmap(Result) -> @@ -88,69 +112,95 @@ support_char() -> "HMSymdZ". create_tag(Head, []) -> Head; -create_tag(Head, [Val1|RVal]) -> +create_tag(Head, [Val1 | RVal]) -> case Val1 of - [] -> create_tag(Head ++ [{st, [$%]}], RVal); - [H| Other] -> + [] -> + 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) + false -> create_tag(Head ++ [{st, [$% | Val1]}], RVal) end end. -eval_tag(_,{st, Str}) -> +eval_tag(_, {st, Str}) -> Str; -eval_tag(Map,{tag, Char}) -> +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, + 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 - }. - + 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 . + C >= $0 andalso C =< $9. is_symbol_char(C) -> - C =:= $- orelse 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, +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]}) + false -> error({unexpected, [C | Other]}) end. -parse_string([], Input) -> {[], Input}; -parse_string([C|Other], Input) -> +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}. + {[C1 | Res], Input2}. -parse_times(0, _, Input) -> {[], Input}; +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}. + {[C1 | Res], Input2}. parse_int_times(Times, Input) -> parse_times(Times, is_int_char, Input). @@ -162,33 +212,42 @@ parse_fraction(Input) -> parse_second(Input) -> {M, Input1} = parse_int_times(2, Input), {M1, Input2} = parse_fraction(Input1), - {M++M1, Input2}. + {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}. + {[S | M ++ [C | V]], Input4}. -mlist1()-> +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 - ]). + %% %H Shows hour in 24-hour format [15] + [ + {$H, fun(Input) -> parse_int_times(2, Input) end}, + %% %M Displays minutes [00-59] + {$M, fun(Input) -> parse_int_times(2, Input) end}, + %% %S Displays seconds [00-59] + {$S, fun(Input) -> parse_second(Input) end}, + %% %y Displays year YYYY [2021] + {$y, fun(Input) -> parse_int_times(4, Input) end}, + %% %m Displays the number of the month [01-12] + {$m, fun(Input) -> parse_int_times(2, Input) end}, + %% %d Displays the number of the month [01-12] + {$d, fun(Input) -> parse_int_times(2, Input) end}, + %% %Z Displays Time zone + {$Z, fun(Input) -> parse_zone(Input) end} + ] + ). -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}. +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), @@ -199,12 +258,13 @@ parse_tag(Res, {tag, St}, InputString) -> NRes = update_result(St, Res, A), {NRes, B}. -parse_tags(Res, [], _) -> Res; -parse_tags(Res, [Tag|Others], InputString) -> +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), + [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.app.src b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src index bec85690f..ef6665e76 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.app.src +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.app.src @@ -1,15 +1,17 @@ %% -*- mode: erlang -*- -{application, emqx_rule_engine, - [{description, "EMQX Rule Engine"}, - {vsn, "5.0.0"}, % strict semver, bump manually! - {modules, []}, - {registered, [emqx_rule_engine_sup, emqx_rule_engine]}, - {applications, [kernel,stdlib,rulesql,getopt]}, - {mod, {emqx_rule_engine_app, []}}, - {env, []}, - {licenses, ["Apache-2.0"]}, - {maintainers, ["EMQX Team "]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-rule-engine"} - ]} - ]}. +{application, emqx_rule_engine, [ + {description, "EMQX Rule Engine"}, + % strict semver, bump manually! + {vsn, "5.0.0"}, + {modules, []}, + {registered, [emqx_rule_engine_sup, emqx_rule_engine]}, + {applications, [kernel, stdlib, rulesql, getopt]}, + {mod, {emqx_rule_engine_app, []}}, + {env, []}, + {licenses, ["Apache-2.0"]}, + {maintainers, ["EMQX Team "]}, + {links, [ + {"Homepage", "https://emqx.io/"}, + {"Github", "https://github.com/emqx/emqx-rule-engine"} + ]} +]}. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine.erl b/apps/emqx_rule_engine/src/emqx_rule_engine.erl index 597daa378..b08964646 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine.erl @@ -25,51 +25,56 @@ -export([start_link/0]). --export([ post_config_update/5 - , config_key_path/0 - ]). +-export([ + post_config_update/5, + config_key_path/0 +]). %% Rule Management --export([ load_rules/0 - ]). +-export([load_rules/0]). --export([ create_rule/1 - , insert_rule/1 - , update_rule/1 - , delete_rule/1 - , get_rule/1 - ]). +-export([ + create_rule/1, + insert_rule/1, + update_rule/1, + delete_rule/1, + get_rule/1 +]). --export([ get_rules/0 - , get_rules_for_topic/1 - , get_rules_with_same_event/1 - , get_rules_ordered_by_ts/0 - ]). +-export([ + get_rules/0, + get_rules_for_topic/1, + get_rules_with_same_event/1, + get_rules_ordered_by_ts/0 +]). %% exported for cluster_call --export([ do_delete_rule/1 - , do_insert_rule/1 - ]). +-export([ + do_delete_rule/1, + do_insert_rule/1 +]). --export([ load_hooks_for_rule/1 - , unload_hooks_for_rule/1 - , maybe_add_metrics_for_rule/1 - , clear_metrics_for_rule/1 - , reset_metrics_for_rule/1 - ]). +-export([ + load_hooks_for_rule/1, + unload_hooks_for_rule/1, + maybe_add_metrics_for_rule/1, + clear_metrics_for_rule/1, + reset_metrics_for_rule/1 +]). %% exported for `emqx_telemetry' -export([get_basic_usage_info/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 +]). -define(RULE_ENGINE, ?MODULE). @@ -77,24 +82,25 @@ %% NOTE: This order cannot be changed! This is to make the metric working during relup. %% Append elements to this list to add new metrics. --define(METRICS, [ 'sql.matched' - , 'sql.passed' - , 'sql.failed' - , 'sql.failed.exception' - , 'sql.failed.no_result' - , 'outputs.total' - , 'outputs.success' - , 'outputs.failed' - , 'outputs.failed.out_of_service' - , 'outputs.failed.unknown' - ]). +-define(METRICS, [ + 'sql.matched', + 'sql.passed', + 'sql.failed', + 'sql.failed.exception', + 'sql.failed.no_result', + 'outputs.total', + 'outputs.success', + 'outputs.failed', + 'outputs.failed.out_of_service', + 'outputs.failed.unknown' +]). -define(RATE_METRICS, ['sql.matched']). config_key_path() -> [rule_engine, rules]. --spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}). +-spec start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}. start_link() -> gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []). @@ -102,17 +108,26 @@ start_link() -> %% The config handler for emqx_rule_engine %%------------------------------------------------------------------------------ post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) -> - #{added := Added, removed := Removed, changed := Updated} - = emqx_map_lib:diff_maps(NewRules, OldRules), - maps_foreach(fun({Id, {_Old, New}}) -> + #{added := Added, removed := Removed, changed := Updated} = + emqx_map_lib:diff_maps(NewRules, OldRules), + maps_foreach( + fun({Id, {_Old, New}}) -> {ok, _} = update_rule(New#{id => bin(Id)}) - end, Updated), - maps_foreach(fun({Id, _Rule}) -> + end, + Updated + ), + maps_foreach( + fun({Id, _Rule}) -> ok = delete_rule(bin(Id)) - end, Removed), - maps_foreach(fun({Id, Rule}) -> + end, + Removed + ), + maps_foreach( + fun({Id, Rule}) -> {ok, _} = create_rule(Rule#{id => bin(Id)}) - end, Added), + end, + Added + ), {ok, get_rules()}. %%------------------------------------------------------------------------------ @@ -121,9 +136,12 @@ post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) -> -spec load_rules() -> ok. load_rules() -> - maps_foreach(fun({Id, Rule}) -> + maps_foreach( + fun({Id, Rule}) -> {ok, _} = create_rule(Rule#{id => bin(Id)}) - end, emqx:get_config([rule_engine, rules], #{})). + end, + emqx:get_config([rule_engine, rules], #{}) + ). -spec create_rule(map()) -> {ok, rule()} | {error, term()}. create_rule(Params = #{id := RuleId}) when is_binary(RuleId) -> @@ -141,11 +159,11 @@ update_rule(Params = #{id := RuleId}) when is_binary(RuleId) -> parse_and_insert(Params, CreatedAt) end. --spec(delete_rule(RuleId :: rule_id()) -> ok). +-spec delete_rule(RuleId :: rule_id()) -> ok. delete_rule(RuleId) when is_binary(RuleId) -> gen_server:call(?RULE_ENGINE, {delete_rule, RuleId}, ?T_CALL). --spec(insert_rule(Rule :: rule()) -> ok). +-spec insert_rule(Rule :: rule()) -> ok. insert_rule(Rule) -> gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL). @@ -153,30 +171,39 @@ insert_rule(Rule) -> %% Rule Management %%------------------------------------------------------------------------------ --spec(get_rules() -> [rule()]). +-spec get_rules() -> [rule()]. get_rules() -> get_all_records(?RULE_TAB). get_rules_ordered_by_ts() -> - lists:sort(fun(#{created_at := CreatedA}, #{created_at := CreatedB}) -> + lists:sort( + fun(#{created_at := CreatedA}, #{created_at := CreatedB}) -> CreatedA =< CreatedB - end, get_rules()). + end, + get_rules() + ). --spec(get_rules_for_topic(Topic :: binary()) -> [rule()]). +-spec get_rules_for_topic(Topic :: binary()) -> [rule()]. get_rules_for_topic(Topic) -> - [Rule || Rule = #{from := From} <- get_rules(), - emqx_plugin_libs_rule:can_topic_match_oneof(Topic, From)]. + [ + Rule + || Rule = #{from := From} <- get_rules(), + emqx_plugin_libs_rule:can_topic_match_oneof(Topic, From) + ]. --spec(get_rules_with_same_event(Topic :: binary()) -> [rule()]). +-spec get_rules_with_same_event(Topic :: binary()) -> [rule()]. get_rules_with_same_event(Topic) -> EventName = emqx_rule_events:event_name(Topic), - [Rule || Rule = #{from := From} <- get_rules(), - lists:any(fun(T) -> is_of_event_name(EventName, T) end, From)]. + [ + Rule + || Rule = #{from := From} <- get_rules(), + lists:any(fun(T) -> is_of_event_name(EventName, T) end, From) + ]. is_of_event_name(EventName, Topic) -> EventName =:= emqx_rule_events:event_name(Topic). --spec(get_rule(Id :: rule_id()) -> {ok, rule()} | not_found). +-spec get_rule(Id :: rule_id()) -> {ok, rule()} | not_found. get_rule(Id) -> case ets:lookup(?RULE_TAB, Id) of [{Id, Rule}] -> {ok, Rule#{id => Id}}; @@ -188,7 +215,8 @@ load_hooks_for_rule(#{from := Topics}) -> maybe_add_metrics_for_rule(Id) -> case emqx_plugin_libs_metrics:has_metrics(rule_metrics, Id) of - true -> ok; + true -> + ok; false -> ok = emqx_plugin_libs_metrics:create_metrics(rule_metrics, Id, ?METRICS, ?RATE_METRICS) end. @@ -196,86 +224,101 @@ maybe_add_metrics_for_rule(Id) -> clear_metrics_for_rule(Id) -> ok = emqx_plugin_libs_metrics:clear_metrics(rule_metrics, Id). --spec(reset_metrics_for_rule(rule_id()) -> ok). +-spec reset_metrics_for_rule(rule_id()) -> ok. reset_metrics_for_rule(Id) -> emqx_plugin_libs_metrics:reset_metrics(rule_metrics, Id). unload_hooks_for_rule(#{id := Id, from := Topics}) -> - lists:foreach(fun(Topic) -> - case get_rules_with_same_event(Topic) of - [#{id := Id0}] when Id0 == Id -> %% we are now deleting the last rule - emqx_rule_events:unload(Topic); - _ -> ok - end - end, Topics). + lists:foreach( + fun(Topic) -> + case get_rules_with_same_event(Topic) of + %% we are now deleting the last rule + [#{id := Id0}] when Id0 == Id -> + emqx_rule_events:unload(Topic); + _ -> + ok + end + end, + Topics + ). %%------------------------------------------------------------------------------ %% Telemetry helper functions %%------------------------------------------------------------------------------ --spec get_basic_usage_info() -> #{ num_rules => non_neg_integer() - , referenced_bridges => - #{ BridgeType => non_neg_integer() - } - } - when BridgeType :: atom(). +-spec get_basic_usage_info() -> + #{ + num_rules => non_neg_integer(), + referenced_bridges => + #{BridgeType => non_neg_integer()} + } +when + BridgeType :: atom(). get_basic_usage_info() -> try Rules = get_rules(), EnabledRules = lists:filter( - fun(#{enable := Enabled}) -> Enabled end, - Rules), + fun(#{enable := Enabled}) -> Enabled end, + Rules + ), NumRules = length(EnabledRules), ReferencedBridges = lists:foldl( - fun(#{outputs := Outputs}, Acc) -> - BridgeIDs = lists:filter(fun is_binary/1, Outputs), - tally_referenced_bridges(BridgeIDs, Acc) - end, - #{}, - EnabledRules), - #{ num_rules => NumRules - , referenced_bridges => ReferencedBridges - } + fun(#{outputs := Outputs}, Acc) -> + BridgeIDs = lists:filter(fun is_binary/1, Outputs), + tally_referenced_bridges(BridgeIDs, Acc) + end, + #{}, + EnabledRules + ), + #{ + num_rules => NumRules, + referenced_bridges => ReferencedBridges + } catch _:_ -> - #{ num_rules => 0 - , referenced_bridges => #{} - } + #{ + num_rules => 0, + referenced_bridges => #{} + } end. - tally_referenced_bridges(BridgeIDs, Acc0) -> lists:foldl( - fun(BridgeID, Acc) -> - {BridgeType, _BridgeName} = emqx_bridge:parse_bridge_id(BridgeID), - maps:update_with( - BridgeType, - fun(X) -> X + 1 end, - 1, - Acc) - end, - Acc0, - BridgeIDs). + fun(BridgeID, Acc) -> + {BridgeType, _BridgeName} = emqx_bridge:parse_bridge_id(BridgeID), + maps:update_with( + BridgeType, + fun(X) -> X + 1 end, + 1, + Acc + ) + end, + Acc0, + BridgeIDs + ). %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ init([]) -> - _TableId = ets:new(?KV_TAB, [named_table, set, public, {write_concurrency, true}, - {read_concurrency, true}]), + _TableId = ets:new(?KV_TAB, [ + named_table, + set, + public, + {write_concurrency, true}, + {read_concurrency, true} + ]), {ok, #{}}. handle_call({insert_rule, Rule}, _From, State) -> do_insert_rule(Rule), {reply, ok, State}; - handle_call({delete_rule, Rule}, _From, State) -> do_delete_rule(Rule), {reply, ok, State}; - handle_call(Req, _From, State) -> ?SLOG(error, #{msg => "unexpected_call", request => Req}), {reply, ignored, State}. @@ -321,7 +364,8 @@ parse_and_insert(Params = #{id := RuleId, sql := Sql, outputs := Outputs}, Creat }, ok = insert_rule(Rule), {ok, Rule}; - {error, Reason} -> {error, Reason} + {error, Reason} -> + {error, Reason} end. do_insert_rule(#{id := Id} = Rule) -> @@ -337,7 +381,8 @@ do_delete_rule(RuleId) -> ok = clear_metrics_for_rule(RuleId), true = ets:delete(?RULE_TAB, RuleId), ok; - not_found -> ok + not_found -> + ok end. parse_outputs(Outputs) -> 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 7d2e34f5b..32b098311 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_api.erl @@ -32,20 +32,33 @@ -export(['/rule_events'/2, '/rule_test'/2, '/rules'/2, '/rules/:id'/2, '/rules/:id/reset_metrics'/2]). -define(ERR_NO_RULE(ID), list_to_binary(io_lib:format("Rule ~ts Not Found", [(ID)]))). --define(ERR_BADARGS(REASON), - begin - R0 = err_msg(REASON), - <<"Bad Arguments: ", R0/binary>> - end). +-define(ERR_BADARGS(REASON), begin + R0 = err_msg(REASON), + <<"Bad Arguments: ", R0/binary>> +end). -define(CHECK_PARAMS(PARAMS, TAG, EXPR), case emqx_rule_api_schema:check_params(PARAMS, TAG) of {ok, CheckedParams} -> EXPR; {error, REASON} -> {400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(REASON)}} - end). --define(METRICS(MATCH, PASS, FAIL, FAIL_EX, FAIL_NORES, O_TOTAL, O_FAIL, O_FAIL_OOS, - O_FAIL_UNKNOWN, O_SUCC, RATE, RATE_MAX, RATE_5), + end +). +-define(METRICS( + MATCH, + PASS, + FAIL, + FAIL_EX, + FAIL_NORES, + O_TOTAL, + O_FAIL, + O_FAIL_OOS, + O_FAIL_UNKNOWN, + O_SUCC, + RATE, + RATE_MAX, + RATE_5 +), #{ 'sql.matched' => MATCH, 'sql.passed' => PASS, @@ -60,9 +73,23 @@ 'sql.matched.rate' => RATE, 'sql.matched.rate.max' => RATE_MAX, 'sql.matched.rate.last5m' => RATE_5 - }). --define(metrics(MATCH, PASS, FAIL, FAIL_EX, FAIL_NORES, O_TOTAL, O_FAIL, O_FAIL_OOS, - O_FAIL_UNKNOWN, O_SUCC, RATE, RATE_MAX, RATE_5), + } +). +-define(metrics( + MATCH, + PASS, + FAIL, + FAIL_EX, + FAIL_NORES, + O_TOTAL, + O_FAIL, + O_FAIL_OOS, + O_FAIL_UNKNOWN, + O_SUCC, + RATE, + RATE_MAX, + RATE_5 +), #{ 'sql.matched' := MATCH, 'sql.passed' := PASS, @@ -77,7 +104,8 @@ 'sql.matched.rate' := RATE, 'sql.matched.rate.max' := RATE_MAX, 'sql.matched.rate.last5m' := RATE_5 - }). + } +). namespace() -> "rule". @@ -107,7 +135,8 @@ schema("/rules") -> summary => <<"List Rules">>, responses => #{ 200 => mk(array(rule_info_schema()), #{desc => ?DESC("desc9")}) - }}, + } + }, post => #{ tags => [<<"rules">>], description => ?DESC("api2"), @@ -116,9 +145,9 @@ schema("/rules") -> responses => #{ 400 => error_schema('BAD_REQUEST', "Invalid Parameters"), 201 => rule_info_schema() - }} + } + } }; - schema("/rule_events") -> #{ 'operationId' => '/rule_events', @@ -131,7 +160,6 @@ schema("/rule_events") -> } } }; - schema("/rules/:id") -> #{ 'operationId' => '/rules/:id', @@ -166,7 +194,6 @@ schema("/rules/:id") -> } } }; - schema("/rules/:id/reset_metrics") -> #{ 'operationId' => '/rules/:id/reset_metrics', @@ -181,7 +208,6 @@ schema("/rules/:id/reset_metrics") -> } } }; - schema("/rule_test") -> #{ 'operationId' => '/rule_test', @@ -206,7 +232,7 @@ param_path_id() -> %%------------------------------------------------------------------------------ %% To get around the hocon bug, we replace crlf with spaces -replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> +replace_sql_clrf(#{<<"sql">> := SQL} = Params) -> NewSQL = re:replace(SQL, "[\r\n]", " ", [{return, binary}, global]), Params#{<<"sql">> => NewSQL}. @@ -216,7 +242,6 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> '/rules'(get, _Params) -> Records = emqx_rule_engine:get_rules_ordered_by_ts(), {200, format_rule_resp(Records)}; - '/rules'(post, #{body := Params0}) -> case maps:get(<<"id">>, Params0, list_to_binary(emqx_misc:gen_id(8))) of <<>> -> @@ -233,20 +258,29 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> [Rule] = get_one_rule(AllRules, Id), {201, format_rule_resp(Rule)}; {error, Reason} -> - ?SLOG(error, #{msg => "create_rule_failed", - id => Id, reason => Reason}), + ?SLOG(error, #{ + msg => "create_rule_failed", + id => Id, + reason => Reason + }), {400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}} end end end. '/rule_test'(post, #{body := Params}) -> - ?CHECK_PARAMS(Params, rule_test, case emqx_rule_sqltester:test(CheckedParams) of - {ok, Result} -> {200, Result}; - {error, {parse_error, Reason}} -> - {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}; - {error, nomatch} -> {412, #{code => 'NOT_MATCH', message => <<"SQL Not Match">>}} - end). + ?CHECK_PARAMS( + Params, + rule_test, + case emqx_rule_sqltester:test(CheckedParams) of + {ok, Result} -> + {200, Result}; + {error, {parse_error, Reason}} -> + {400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}}; + {error, nomatch} -> + {412, #{code => 'NOT_MATCH', message => <<"SQL Not Match">>}} + end + ). '/rules/:id'(get, #{bindings := #{id := Id}}) -> case emqx_rule_engine:get_rule(Id) of @@ -255,7 +289,6 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> not_found -> {404, #{code => 'NOT_FOUND', message => <<"Rule Id Not Found">>}} end; - '/rules/:id'(put, #{bindings := #{id := Id}, body := Params0}) -> Params = filter_out_request_body(Params0), ConfPath = emqx_rule_engine:config_key_path() ++ [Id], @@ -264,25 +297,35 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> [Rule] = get_one_rule(AllRules, Id), {200, format_rule_resp(Rule)}; {error, Reason} -> - ?SLOG(error, #{msg => "update_rule_failed", - id => Id, reason => Reason}), + ?SLOG(error, #{ + msg => "update_rule_failed", + id => Id, + reason => Reason + }), {400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}} end; - '/rules/:id'(delete, #{bindings := #{id := Id}}) -> ConfPath = emqx_rule_engine:config_key_path() ++ [Id], case emqx_conf:remove(ConfPath, #{override_to => cluster}) of - {ok, _} -> {204}; + {ok, _} -> + {204}; {error, Reason} -> - ?SLOG(error, #{msg => "delete_rule_failed", - id => Id, reason => Reason}), + ?SLOG(error, #{ + msg => "delete_rule_failed", + id => Id, + reason => Reason + }), {500, #{code => 'INTERNAL_ERROR', message => ?ERR_BADARGS(Reason)}} end. '/rules/:id/reset_metrics'(put, #{bindings := #{id := RuleId}}) -> case emqx_rule_engine_proto_v1:reset_metrics(RuleId) of - {ok, _TxnId, _Result} -> {200, <<"Reset Success">>}; - Failed -> {400, #{code => 'BAD_REQUEST', - message => err_msg(Failed)}} + {ok, _TxnId, _Result} -> + {200, <<"Reset Success">>}; + Failed -> + {400, #{ + code => 'BAD_REQUEST', + message => err_msg(Failed) + }} end. %%------------------------------------------------------------------------------ @@ -292,29 +335,31 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) -> err_msg(Msg) -> list_to_binary(io_lib:format("~0p", [Msg])). - format_rule_resp(Rules) when is_list(Rules) -> [format_rule_resp(R) || R <- Rules]; - -format_rule_resp(#{ id := Id, name := Name, - created_at := CreatedAt, - from := Topics, - outputs := Output, - sql := SQL, - enable := Enable, - description := Descr}) -> +format_rule_resp(#{ + id := Id, + name := Name, + created_at := CreatedAt, + from := Topics, + outputs := Output, + sql := SQL, + enable := Enable, + description := Descr +}) -> NodeMetrics = get_rule_metrics(Id), - #{id => Id, - name => Name, - from => Topics, - outputs => format_output(Output), - sql => SQL, - metrics => aggregate_metrics(NodeMetrics), - node_metrics => NodeMetrics, - enable => Enable, - created_at => format_datetime(CreatedAt, millisecond), - description => Descr - }. + #{ + id => Id, + name => Name, + from => Topics, + outputs => format_output(Output), + sql => SQL, + metrics => aggregate_metrics(NodeMetrics), + node_metrics => NodeMetrics, + enable => Enable, + created_at => format_datetime(CreatedAt, millisecond), + description => Descr + }. format_datetime(Timestamp, Unit) -> list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, Unit}])). @@ -323,62 +368,133 @@ format_output(Outputs) -> [do_format_output(Out) || Out <- Outputs]. do_format_output(#{mod := Mod, func := Func, args := Args}) -> - #{function => printable_function_name(Mod, Func), - args => maps:remove(preprocessed_tmpl, Args)}; + #{ + function => printable_function_name(Mod, Func), + args => maps:remove(preprocessed_tmpl, Args) + }; do_format_output(BridgeChannelId) when is_binary(BridgeChannelId) -> BridgeChannelId. printable_function_name(emqx_rule_outputs, Func) -> Func; printable_function_name(Mod, Func) -> - list_to_binary(lists:concat([Mod,":",Func])). + list_to_binary(lists:concat([Mod, ":", Func])). get_rule_metrics(Id) -> - Format = fun (Node, #{ + Format = fun( + Node, + #{ counters := - #{'sql.matched' := Matched, 'sql.passed' := Passed, 'sql.failed' := Failed, - 'sql.failed.exception' := FailedEx, - 'sql.failed.no_result' := FailedNoRes, - 'outputs.total' := OTotal, - 'outputs.failed' := OFailed, - 'outputs.failed.out_of_service' := OFailedOOS, - 'outputs.failed.unknown' := OFailedUnknown, - 'outputs.success' := OFailedSucc - }, + #{ + 'sql.matched' := Matched, + 'sql.passed' := Passed, + 'sql.failed' := Failed, + 'sql.failed.exception' := FailedEx, + 'sql.failed.no_result' := FailedNoRes, + 'outputs.total' := OTotal, + 'outputs.failed' := OFailed, + 'outputs.failed.out_of_service' := OFailedOOS, + 'outputs.failed.unknown' := OFailedUnknown, + 'outputs.success' := OFailedSucc + }, rate := - #{'sql.matched' := - #{current := Current, max := Max, last5m := Last5M} - }}) -> - #{ metrics => ?METRICS(Matched, Passed, Failed, FailedEx, FailedNoRes, - OTotal, OFailed, OFailedOOS, OFailedUnknown, OFailedSucc, Current, Max, Last5M) - , node => Node - } + #{ + 'sql.matched' := + #{current := Current, max := Max, last5m := Last5M} + } + } + ) -> + #{ + metrics => ?METRICS( + Matched, + Passed, + Failed, + FailedEx, + FailedNoRes, + OTotal, + OFailed, + OFailedOOS, + OFailedUnknown, + OFailedSucc, + Current, + Max, + Last5M + ), + node => Node + } end, - [Format(Node, emqx_plugin_libs_proto_v1:get_metrics(Node, rule_metrics, Id)) - || Node <- mria_mnesia:running_nodes()]. + [ + Format(Node, emqx_plugin_libs_proto_v1:get_metrics(Node, rule_metrics, Id)) + || Node <- mria_mnesia:running_nodes() + ]. aggregate_metrics(AllMetrics) -> InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - lists:foldl(fun - (#{metrics := ?metrics(Match1, Passed1, Failed1, FailedEx1, FailedNoRes1, - OTotal1, OFailed1, OFailedOOS1, OFailedUnknown1, OFailedSucc1, - Rate1, RateMax1, Rate5m1)}, - ?metrics(Match0, Passed0, Failed0, FailedEx0, FailedNoRes0, - OTotal0, OFailed0, OFailedOOS0, OFailedUnknown0, OFailedSucc0, - Rate0, RateMax0, Rate5m0)) -> - ?METRICS(Match1 + Match0, Passed1 + Passed0, Failed1 + Failed0, - FailedEx1 + FailedEx0, FailedNoRes1 + FailedNoRes0, - OTotal1 + OTotal0, OFailed1 + OFailed0, - OFailedOOS1 + OFailedOOS0, - OFailedUnknown1 + OFailedUnknown0, - OFailedSucc1 + OFailedSucc0, - Rate1 + Rate0, RateMax1 + RateMax0, Rate5m1 + Rate5m0) - end, InitMetrics, AllMetrics). + lists:foldl( + fun( + #{ + metrics := ?metrics( + Match1, + Passed1, + Failed1, + FailedEx1, + FailedNoRes1, + OTotal1, + OFailed1, + OFailedOOS1, + OFailedUnknown1, + OFailedSucc1, + Rate1, + RateMax1, + Rate5m1 + ) + }, + ?metrics( + Match0, + Passed0, + Failed0, + FailedEx0, + FailedNoRes0, + OTotal0, + OFailed0, + OFailedOOS0, + OFailedUnknown0, + OFailedSucc0, + Rate0, + RateMax0, + Rate5m0 + ) + ) -> + ?METRICS( + Match1 + Match0, + Passed1 + Passed0, + Failed1 + Failed0, + FailedEx1 + FailedEx0, + FailedNoRes1 + FailedNoRes0, + OTotal1 + OTotal0, + OFailed1 + OFailed0, + OFailedOOS1 + OFailedOOS0, + OFailedUnknown1 + OFailedUnknown0, + OFailedSucc1 + OFailedSucc0, + Rate1 + Rate0, + RateMax1 + RateMax0, + Rate5m1 + Rate5m0 + ) + end, + InitMetrics, + AllMetrics + ). get_one_rule(AllRules, Id) -> [R || R = #{id := Id0} <- AllRules, Id0 == Id]. filter_out_request_body(Conf) -> - ExtraConfs = [<<"id">>, <<"status">>, <<"node_status">>, <<"node_metrics">>, - <<"metrics">>, <<"node">>], + ExtraConfs = [ + <<"id">>, + <<"status">>, + <<"node_status">>, + <<"node_metrics">>, + <<"metrics">>, + <<"node">> + ], maps:without(ExtraConfs, Conf). diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index 005d78c7f..fd4cb8e6e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -21,96 +21,140 @@ -behaviour(hocon_schema). --export([ namespace/0 - , roots/0 - , fields/1 - , desc/1 - ]). +-export([ + namespace/0, + roots/0, + fields/1, + desc/1 +]). --export([ validate_sql/1 - ]). +-export([validate_sql/1]). namespace() -> rule_engine. roots() -> ["rule_engine"]. fields("rule_engine") -> - [ {ignore_sys_message, sc(boolean(), #{default => true, desc => ?DESC("rule_engine_ignore_sys_message") - })} - , {rules, sc(hoconsc:map("id", ref("rules")), #{desc => ?DESC("rule_engine_rules"), default => #{}})} + [ + {ignore_sys_message, + sc(boolean(), #{default => true, desc => ?DESC("rule_engine_ignore_sys_message")})}, + {rules, + sc(hoconsc:map("id", ref("rules")), #{ + desc => ?DESC("rule_engine_rules"), default => #{} + })} ]; - fields("rules") -> - [ rule_name() - , {"sql", sc(binary(), - #{ desc => ?DESC("rules_sql") - , example => "SELECT * FROM \"test/topic\" WHERE payload.x = 1" - , required => true - , validator => fun ?MODULE:validate_sql/1 - })} - , {"outputs", sc(hoconsc:array(hoconsc:union(outputs())), - #{ desc => ?DESC("rules_outputs") - , default => [] - , example => [ - <<"http:my_http_bridge">>, - #{function => republish, args => #{ - topic => <<"t/1">>, payload => <<"${payload}">>}}, - #{function => console} - ] - })} - , {"enable", sc(boolean(), #{desc => ?DESC("rules_enable"), default => true})} - , {"description", sc(binary(), - #{ desc => ?DESC("rules_description") - , example => "Some description" - , default => <<>> - })} + [ + rule_name(), + {"sql", + sc( + binary(), + #{ + desc => ?DESC("rules_sql"), + example => "SELECT * FROM \"test/topic\" WHERE payload.x = 1", + required => true, + validator => fun ?MODULE:validate_sql/1 + } + )}, + {"outputs", + sc( + hoconsc:array(hoconsc:union(outputs())), + #{ + desc => ?DESC("rules_outputs"), + default => [], + example => [ + <<"http:my_http_bridge">>, + #{ + function => republish, + args => #{ + topic => <<"t/1">>, payload => <<"${payload}">> + } + }, + #{function => console} + ] + } + )}, + {"enable", sc(boolean(), #{desc => ?DESC("rules_enable"), default => true})}, + {"description", + sc( + binary(), + #{ + desc => ?DESC("rules_description"), + example => "Some description", + default => <<>> + } + )} ]; - fields("builtin_output_republish") -> - [ {function, sc(republish, #{desc => ?DESC("republish_function")})} - , {args, sc(ref("republish_args"), #{default => #{}})} + [ + {function, sc(republish, #{desc => ?DESC("republish_function")})}, + {args, sc(ref("republish_args"), #{default => #{}})} ]; - fields("builtin_output_console") -> - [ {function, sc(console, #{desc => ?DESC("console_function")})} - %% we may support some args for the console output in the future - %, {args, sc(map(), #{desc => "The arguments of the built-in 'console' output", - % default => #{}})} + [ + {function, sc(console, #{desc => ?DESC("console_function")})} + %% we may support some args for the console output in the future + %, {args, sc(map(), #{desc => "The arguments of the built-in 'console' output", + % default => #{}})} ]; - fields("user_provided_function") -> - [ {function, sc(binary(), - #{ desc => ?DESC("user_provided_function_function") - , required => true - , example => "module:function" - })} - , {args, sc(map(), - #{ desc => ?DESC("user_provided_function_args") - , default => #{} - })} + [ + {function, + sc( + binary(), + #{ + desc => ?DESC("user_provided_function_function"), + required => true, + example => "module:function" + } + )}, + {args, + sc( + map(), + #{ + desc => ?DESC("user_provided_function_args"), + default => #{} + } + )} ]; - fields("republish_args") -> - [ {topic, sc(binary(), - #{ desc => ?DESC("republish_args_topic") - , required => true - , example => <<"a/1">> - })} - , {qos, sc(qos(), - #{ desc => ?DESC("republish_args_qos") - , default => <<"${qos}">> - , example => <<"${qos}">> - })} - , {retain, sc(hoconsc:union([binary(), boolean()]), - #{ desc => ?DESC("republish_args_retain") - , default => <<"${retain}">> - , example => <<"${retain}">> - })} - , {payload, sc(binary(), - #{ desc => ?DESC("republish_args_payload") - , default => <<"${payload}">> - , example => <<"${payload}">> - })} + [ + {topic, + sc( + binary(), + #{ + desc => ?DESC("republish_args_topic"), + required => true, + example => <<"a/1">> + } + )}, + {qos, + sc( + qos(), + #{ + desc => ?DESC("republish_args_qos"), + default => <<"${qos}">>, + example => <<"${qos}">> + } + )}, + {retain, + sc( + hoconsc:union([binary(), boolean()]), + #{ + desc => ?DESC("republish_args_retain"), + default => <<"${retain}">>, + example => <<"${retain}">> + } + )}, + {payload, + sc( + binary(), + #{ + desc => ?DESC("republish_args_payload"), + default => <<"${payload}">>, + example => <<"${payload}">> + } + )} ]. desc("rule_engine") -> @@ -129,18 +173,23 @@ desc(_) -> undefined. rule_name() -> - {"name", sc(binary(), - #{ desc => ?DESC("rules_name") - , default => "" - , required => true - , example => "foo" - })}. + {"name", + sc( + binary(), + #{ + desc => ?DESC("rules_name"), + default => "", + required => true, + example => "foo" + } + )}. outputs() -> - [ binary() - , ref("builtin_output_republish") - , ref("builtin_output_console") - , ref("user_provided_function") + [ + binary(), + ref("builtin_output_republish"), + ref("builtin_output_console"), + ref("user_provided_function") ]. qos() -> diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl index 033e8f04f..621766945 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_sup.erl @@ -28,11 +28,13 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Registry = #{id => emqx_rule_engine, - start => {emqx_rule_engine, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_rule_engine]}, + Registry = #{ + id => emqx_rule_engine, + start => {emqx_rule_engine, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_rule_engine] + }, Metrics = emqx_plugin_libs_metrics:child_spec(rule_metrics), {ok, {{one_for_one, 10, 10}, [Registry, Metrics]}}. diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 91c90f3e2..3070c054e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -20,72 +20,82 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). +-export([ + reload/0, + load/1, + unload/0, + unload/1, + event_names/0, + event_name/1, + event_topic/1, + eventmsg_publish/1 +]). --export([ reload/0 - , load/1 - , unload/0 - , unload/1 - , event_names/0 - , event_name/1 - , event_topic/1 - , eventmsg_publish/1 - ]). +-export([ + on_client_connected/3, + on_client_disconnected/4, + on_client_connack/4, + on_client_check_authz_complete/6, + on_session_subscribed/4, + on_session_unsubscribed/4, + on_message_publish/2, + on_message_dropped/4, + on_message_delivered/3, + on_message_acked/3, + on_delivery_dropped/4, + on_bridge_message_received/2 +]). --export([ on_client_connected/3 - , on_client_disconnected/4 - , on_client_connack/4 - , on_client_check_authz_complete/6 - , on_session_subscribed/4 - , on_session_unsubscribed/4 - , on_message_publish/2 - , on_message_dropped/4 - , on_message_delivered/3 - , on_message_acked/3 - , on_delivery_dropped/4 - , on_bridge_message_received/2 - ]). - --export([ event_info/0 - , columns/1 - , columns_with_exam/1 - ]). +-export([ + event_info/0, + columns/1, + columns_with_exam/1 +]). -ifdef(TEST). --export([ reason/1 - , hook_fun/1 - , printable_maps/1 - ]). +-export([ + reason/1, + hook_fun/1, + printable_maps/1 +]). -endif. -elvis([{elvis_style, dont_repeat_yourself, disable}]). event_names() -> - [ 'client.connected' - , 'client.disconnected' - , 'client.connack' - , 'client.check_authz_complete' - , 'session.subscribed' - , 'session.unsubscribed' - , 'message.publish' - , 'message.delivered' - , 'message.acked' - , 'message.dropped' - , 'delivery.dropped' + [ + 'client.connected', + 'client.disconnected', + 'client.connack', + 'client.check_authz_complete', + 'session.subscribed', + 'session.unsubscribed', + 'message.publish', + 'message.delivered', + 'message.acked', + 'message.dropped', + 'delivery.dropped' ]. reload() -> - lists:foreach(fun(Rule) -> + lists:foreach( + fun(Rule) -> ok = emqx_rule_engine:load_hooks_for_rule(Rule) - end, emqx_rule_engine:get_rules()). + end, + emqx_rule_engine:get_rules() + ). load(Topic) -> HookPoint = event_name(Topic), emqx_hooks:put(HookPoint, {?MODULE, hook_fun(HookPoint), [#{event_topic => Topic}]}). unload() -> - lists:foreach(fun(HookPoint) -> + lists:foreach( + fun(HookPoint) -> emqx_hooks:del(HookPoint, {?MODULE, hook_fun(HookPoint)}) - end, event_names()). + end, + event_names() + ). unload(Topic) -> HookPoint = event_name(Topic), @@ -96,7 +106,8 @@ unload(Topic) -> %%-------------------------------------------------------------------- on_message_publish(Message = #message{topic = Topic}, _Env) -> case ignore_sys_message(Message) of - true -> ok; + true -> + ok; false -> case emqx_rule_engine:get_rules_for_topic(Topic) of [] -> ok; @@ -109,70 +120,108 @@ on_bridge_message_received(Message, Env = #{event_topic := BridgeTopic}) -> apply_event(BridgeTopic, fun() -> with_basic_columns(BridgeTopic, Message) end, Env). on_client_connected(ClientInfo, ConnInfo, Env) -> - apply_event('client.connected', - fun() -> eventmsg_connected(ClientInfo, ConnInfo) end, Env). + apply_event( + 'client.connected', + fun() -> eventmsg_connected(ClientInfo, ConnInfo) end, + Env + ). on_client_connack(ConnInfo, Reason, _, Env) -> - apply_event('client.connack', - fun() -> eventmsg_connack(ConnInfo, Reason) end, Env). + apply_event( + 'client.connack', + fun() -> eventmsg_connack(ConnInfo, Reason) end, + Env + ). on_client_check_authz_complete(ClientInfo, PubSub, Topic, Result, AuthzSource, Env) -> - apply_event('client.check_authz_complete', - fun() -> eventmsg_check_authz_complete(ClientInfo, - PubSub, - Topic, - Result, - AuthzSource) end, Env). + apply_event( + 'client.check_authz_complete', + fun() -> + eventmsg_check_authz_complete( + ClientInfo, + PubSub, + Topic, + Result, + AuthzSource + ) + end, + Env + ). on_client_disconnected(ClientInfo, Reason, ConnInfo, Env) -> - apply_event('client.disconnected', - fun() -> eventmsg_disconnected(ClientInfo, ConnInfo, Reason) end, Env). + apply_event( + 'client.disconnected', + fun() -> eventmsg_disconnected(ClientInfo, ConnInfo, Reason) end, + Env + ). on_session_subscribed(ClientInfo, Topic, SubOpts, Env) -> - apply_event('session.subscribed', + apply_event( + 'session.subscribed', fun() -> eventmsg_sub_or_unsub('session.subscribed', ClientInfo, Topic, SubOpts) - end, Env). + end, + Env + ). on_session_unsubscribed(ClientInfo, Topic, SubOpts, Env) -> - apply_event('session.unsubscribed', + apply_event( + 'session.unsubscribed', fun() -> eventmsg_sub_or_unsub('session.unsubscribed', ClientInfo, Topic, SubOpts) - end, Env). + end, + Env + ). on_message_dropped(Message, _, Reason, Env) -> case ignore_sys_message(Message) of - true -> ok; + true -> + ok; false -> - apply_event('message.dropped', - fun() -> eventmsg_dropped(Message, Reason) end, Env) + apply_event( + 'message.dropped', + fun() -> eventmsg_dropped(Message, Reason) end, + Env + ) end, {ok, Message}. on_message_delivered(ClientInfo, Message, Env) -> case ignore_sys_message(Message) of - true -> ok; + true -> + ok; false -> - apply_event('message.delivered', - fun() -> eventmsg_delivered(ClientInfo, Message) end, Env) + apply_event( + 'message.delivered', + fun() -> eventmsg_delivered(ClientInfo, Message) end, + Env + ) end, {ok, Message}. on_message_acked(ClientInfo, Message, Env) -> case ignore_sys_message(Message) of - true -> ok; + true -> + ok; false -> - apply_event('message.acked', - fun() -> eventmsg_acked(ClientInfo, Message) end, Env) + apply_event( + 'message.acked', + fun() -> eventmsg_acked(ClientInfo, Message) end, + Env + ) end, {ok, Message}. on_delivery_dropped(ClientInfo, Message, Reason, Env) -> case ignore_sys_message(Message) of - true -> ok; + true -> + ok; false -> - apply_event('delivery.dropped', - fun() -> eventmsg_delivery_dropped(ClientInfo, Message, Reason) end, Env) + apply_event( + 'delivery.dropped', + fun() -> eventmsg_delivery_dropped(ClientInfo, Message, Reason) end, + Env + ) end, {ok, Message}. @@ -180,235 +229,335 @@ on_delivery_dropped(ClientInfo, Message, Reason, Env) -> %% Event Messages %%-------------------------------------------------------------------- -eventmsg_publish(Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, - topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}) -> - with_basic_columns('message.publish', - #{id => emqx_guid:to_hexstr(Id), - clientid => ClientId, - username => emqx_message:get_header(username, Message, undefined), - payload => Payload, - peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), - topic => Topic, - qos => QoS, - flags => Flags, - pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), - %% the column 'headers' will be removed in the next major release - headers => printable_maps(Headers), - publish_received_at => Timestamp - }). +eventmsg_publish( + Message = #message{ + id = Id, + from = ClientId, + qos = QoS, + flags = Flags, + topic = Topic, + headers = Headers, + payload = Payload, + timestamp = Timestamp + } +) -> + with_basic_columns( + 'message.publish', + #{ + id => emqx_guid:to_hexstr(Id), + clientid => ClientId, + username => emqx_message:get_header(username, Message, undefined), + payload => Payload, + peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), + topic => Topic, + qos => QoS, + flags => Flags, + pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), + %% the column 'headers' will be removed in the next major release + headers => printable_maps(Headers), + publish_received_at => Timestamp + } + ). -eventmsg_connected(_ClientInfo = #{ - clientid := ClientId, - username := Username, - is_bridge := IsBridge, - mountpoint := Mountpoint - }, - ConnInfo = #{ - peername := PeerName, - sockname := SockName, - clean_start := CleanStart, - proto_name := ProtoName, - proto_ver := ProtoVer, - connected_at := ConnectedAt - }) -> +eventmsg_connected( + _ClientInfo = #{ + clientid := ClientId, + username := Username, + is_bridge := IsBridge, + mountpoint := Mountpoint + }, + ConnInfo = #{ + peername := PeerName, + sockname := SockName, + clean_start := CleanStart, + proto_name := ProtoName, + proto_ver := ProtoVer, + connected_at := ConnectedAt + } +) -> Keepalive = maps:get(keepalive, ConnInfo, 0), ConnProps = maps:get(conn_props, ConnInfo, #{}), RcvMax = maps:get(receive_maximum, ConnInfo, 0), ExpiryInterval = maps:get(expiry_interval, ConnInfo, 0), - with_basic_columns('client.connected', - #{clientid => ClientId, - username => Username, - mountpoint => Mountpoint, - peername => ntoa(PeerName), - sockname => ntoa(SockName), - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - clean_start => CleanStart, - receive_maximum => RcvMax, - expiry_interval => ExpiryInterval div 1000, - is_bridge => IsBridge, - conn_props => printable_maps(ConnProps), - connected_at => ConnectedAt - }). + with_basic_columns( + 'client.connected', + #{ + clientid => ClientId, + username => Username, + mountpoint => Mountpoint, + peername => ntoa(PeerName), + sockname => ntoa(SockName), + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + clean_start => CleanStart, + receive_maximum => RcvMax, + expiry_interval => ExpiryInterval div 1000, + is_bridge => IsBridge, + conn_props => printable_maps(ConnProps), + connected_at => ConnectedAt + } + ). -eventmsg_disconnected(_ClientInfo = #{ - clientid := ClientId, - username := Username - }, - ConnInfo = #{ - peername := PeerName, - sockname := SockName, - disconnected_at := DisconnectedAt - }, Reason) -> - with_basic_columns('client.disconnected', - #{reason => reason(Reason), - clientid => ClientId, - username => Username, - peername => ntoa(PeerName), - sockname => ntoa(SockName), - disconn_props => printable_maps(maps:get(disconn_props, ConnInfo, #{})), - disconnected_at => DisconnectedAt - }). +eventmsg_disconnected( + _ClientInfo = #{ + clientid := ClientId, + username := Username + }, + ConnInfo = #{ + peername := PeerName, + sockname := SockName, + disconnected_at := DisconnectedAt + }, + Reason +) -> + with_basic_columns( + 'client.disconnected', + #{ + reason => reason(Reason), + clientid => ClientId, + username => Username, + peername => ntoa(PeerName), + sockname => ntoa(SockName), + disconn_props => printable_maps(maps:get(disconn_props, ConnInfo, #{})), + disconnected_at => DisconnectedAt + } + ). -eventmsg_connack(ConnInfo = #{ - clientid := ClientId, - clean_start := CleanStart, - username := Username, - peername := PeerName, - sockname := SockName, - proto_name := ProtoName, - proto_ver := ProtoVer - }, Reason) -> +eventmsg_connack( + ConnInfo = #{ + clientid := ClientId, + clean_start := CleanStart, + username := Username, + peername := PeerName, + sockname := SockName, + proto_name := ProtoName, + proto_ver := ProtoVer + }, + Reason +) -> Keepalive = maps:get(keepalive, ConnInfo, 0), ConnProps = maps:get(conn_props, ConnInfo, #{}), ExpiryInterval = maps:get(expiry_interval, ConnInfo, 0), - with_basic_columns('client.connack', - #{reason_code => reason(Reason), - clientid => ClientId, - clean_start => CleanStart, - username => Username, - peername => ntoa(PeerName), - sockname => ntoa(SockName), - proto_name => ProtoName, - proto_ver => ProtoVer, - keepalive => Keepalive, - expiry_interval => ExpiryInterval, - conn_props => printable_maps(ConnProps) - }). + with_basic_columns( + 'client.connack', + #{ + reason_code => reason(Reason), + clientid => ClientId, + clean_start => CleanStart, + username => Username, + peername => ntoa(PeerName), + sockname => ntoa(SockName), + proto_name => ProtoName, + proto_ver => ProtoVer, + keepalive => Keepalive, + expiry_interval => ExpiryInterval, + conn_props => printable_maps(ConnProps) + } + ). -eventmsg_check_authz_complete(_ClientInfo = #{ - clientid := ClientId, - username := Username, - peerhost := PeerHost - }, PubSub, Topic, Result, AuthzSource) -> - with_basic_columns('client.check_authz_complete', - #{clientid => ClientId, - username => Username, - peerhost => ntoa(PeerHost), - topic => Topic, - action => PubSub, - authz_source => AuthzSource, - result => Result - }). +eventmsg_check_authz_complete( + _ClientInfo = #{ + clientid := ClientId, + username := Username, + peerhost := PeerHost + }, + PubSub, + Topic, + Result, + AuthzSource +) -> + with_basic_columns( + 'client.check_authz_complete', + #{ + clientid => ClientId, + username => Username, + peerhost => ntoa(PeerHost), + topic => Topic, + action => PubSub, + authz_source => AuthzSource, + result => Result + } + ). -eventmsg_sub_or_unsub(Event, _ClientInfo = #{ - clientid := ClientId, - username := Username, - peerhost := PeerHost - }, Topic, SubOpts = #{qos := QoS}) -> +eventmsg_sub_or_unsub( + Event, + _ClientInfo = #{ + clientid := ClientId, + username := Username, + peerhost := PeerHost + }, + Topic, + SubOpts = #{qos := QoS} +) -> PropKey = sub_unsub_prop_key(Event), - with_basic_columns(Event, - #{clientid => ClientId, - username => Username, - peerhost => ntoa(PeerHost), - PropKey => printable_maps(maps:get(PropKey, SubOpts, #{})), - topic => Topic, - qos => QoS - }). + with_basic_columns( + Event, + #{ + clientid => ClientId, + username => Username, + peerhost => ntoa(PeerHost), + PropKey => printable_maps(maps:get(PropKey, SubOpts, #{})), + topic => Topic, + qos => QoS + } + ). -eventmsg_dropped(Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, - topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}, Reason) -> - with_basic_columns('message.dropped', - #{id => emqx_guid:to_hexstr(Id), - reason => Reason, - clientid => ClientId, - username => emqx_message:get_header(username, Message, undefined), - payload => Payload, - peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), - topic => Topic, - qos => QoS, - flags => Flags, - %% the column 'headers' will be removed in the next major release - headers => printable_maps(Headers), - pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), - publish_received_at => Timestamp - }). +eventmsg_dropped( + Message = #message{ + id = Id, + from = ClientId, + qos = QoS, + flags = Flags, + topic = Topic, + headers = Headers, + payload = Payload, + timestamp = Timestamp + }, + Reason +) -> + with_basic_columns( + 'message.dropped', + #{ + id => emqx_guid:to_hexstr(Id), + reason => Reason, + clientid => ClientId, + username => emqx_message:get_header(username, Message, undefined), + payload => Payload, + peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), + topic => Topic, + qos => QoS, + flags => Flags, + %% the column 'headers' will be removed in the next major release + headers => printable_maps(Headers), + pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), + publish_received_at => Timestamp + } + ). -eventmsg_delivered(_ClientInfo = #{ - peerhost := PeerHost, - clientid := ReceiverCId, - username := ReceiverUsername - }, Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, - topic = Topic, headers = Headers, payload = Payload, - timestamp = Timestamp}) -> - with_basic_columns('message.delivered', - #{id => emqx_guid:to_hexstr(Id), - from_clientid => ClientId, - from_username => emqx_message:get_header(username, Message, undefined), - clientid => ReceiverCId, - username => ReceiverUsername, - payload => Payload, - peerhost => ntoa(PeerHost), - topic => Topic, - qos => QoS, - flags => Flags, - %% the column 'headers' will be removed in the next major release - headers => printable_maps(Headers), - pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), - publish_received_at => Timestamp - }). +eventmsg_delivered( + _ClientInfo = #{ + peerhost := PeerHost, + clientid := ReceiverCId, + username := ReceiverUsername + }, + Message = #message{ + id = Id, + from = ClientId, + qos = QoS, + flags = Flags, + topic = Topic, + headers = Headers, + payload = Payload, + timestamp = Timestamp + } +) -> + with_basic_columns( + 'message.delivered', + #{ + id => emqx_guid:to_hexstr(Id), + from_clientid => ClientId, + from_username => emqx_message:get_header(username, Message, undefined), + clientid => ReceiverCId, + username => ReceiverUsername, + payload => Payload, + peerhost => ntoa(PeerHost), + topic => Topic, + qos => QoS, + flags => Flags, + %% the column 'headers' will be removed in the next major release + headers => printable_maps(Headers), + pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), + publish_received_at => Timestamp + } + ). -eventmsg_acked(_ClientInfo = #{ - peerhost := PeerHost, - clientid := ReceiverCId, - username := ReceiverUsername - }, - Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, - topic = Topic, headers = Headers, payload = Payload, - timestamp = Timestamp}) -> - with_basic_columns('message.acked', - #{id => emqx_guid:to_hexstr(Id), - from_clientid => ClientId, - from_username => emqx_message:get_header(username, Message, undefined), - clientid => ReceiverCId, - username => ReceiverUsername, - payload => Payload, - peerhost => ntoa(PeerHost), - topic => Topic, - qos => QoS, - flags => Flags, - %% the column 'headers' will be removed in the next major release - headers => printable_maps(Headers), - pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), - puback_props => printable_maps(emqx_message:get_header(puback_props, Message, #{})), - publish_received_at => Timestamp - }). +eventmsg_acked( + _ClientInfo = #{ + peerhost := PeerHost, + clientid := ReceiverCId, + username := ReceiverUsername + }, + Message = #message{ + id = Id, + from = ClientId, + qos = QoS, + flags = Flags, + topic = Topic, + headers = Headers, + payload = Payload, + timestamp = Timestamp + } +) -> + with_basic_columns( + 'message.acked', + #{ + id => emqx_guid:to_hexstr(Id), + from_clientid => ClientId, + from_username => emqx_message:get_header(username, Message, undefined), + clientid => ReceiverCId, + username => ReceiverUsername, + payload => Payload, + peerhost => ntoa(PeerHost), + topic => Topic, + qos => QoS, + flags => Flags, + %% the column 'headers' will be removed in the next major release + headers => printable_maps(Headers), + pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), + puback_props => printable_maps(emqx_message:get_header(puback_props, Message, #{})), + publish_received_at => Timestamp + } + ). -eventmsg_delivery_dropped(_ClientInfo = #{ - peerhost := PeerHost, - clientid := ReceiverCId, - username := ReceiverUsername - }, - Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, topic = Topic, - headers = Headers, payload = Payload, timestamp = Timestamp}, - Reason) -> - with_basic_columns('delivery.dropped', - #{id => emqx_guid:to_hexstr(Id), - reason => Reason, - from_clientid => ClientId, - from_username => emqx_message:get_header(username, Message, undefined), - clientid => ReceiverCId, - username => ReceiverUsername, - payload => Payload, - peerhost => ntoa(PeerHost), - topic => Topic, - qos => QoS, - flags => Flags, - %% the column 'headers' will be removed in the next major release - headers => printable_maps(Headers), - pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), - publish_received_at => Timestamp - }). +eventmsg_delivery_dropped( + _ClientInfo = #{ + peerhost := PeerHost, + clientid := ReceiverCId, + username := ReceiverUsername + }, + Message = #message{ + id = Id, + from = ClientId, + qos = QoS, + flags = Flags, + topic = Topic, + headers = Headers, + payload = Payload, + timestamp = Timestamp + }, + Reason +) -> + with_basic_columns( + 'delivery.dropped', + #{ + id => emqx_guid:to_hexstr(Id), + reason => Reason, + from_clientid => ClientId, + from_username => emqx_message:get_header(username, Message, undefined), + clientid => ReceiverCId, + username => ReceiverUsername, + payload => Payload, + peerhost => ntoa(PeerHost), + topic => Topic, + qos => QoS, + flags => Flags, + %% the column 'headers' will be removed in the next major release + headers => printable_maps(Headers), + pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), + publish_received_at => Timestamp + } + ). sub_unsub_prop_key('session.subscribed') -> sub_props; sub_unsub_prop_key('session.unsubscribed') -> unsub_props. with_basic_columns(EventName, Data) when is_map(Data) -> - Data#{event => EventName, - timestamp => erlang:system_time(millisecond), - node => node() - }. + Data#{ + event => EventName, + timestamp => erlang:system_time(millisecond), + node => node() + }. %%-------------------------------------------------------------------- %% rules applying @@ -416,7 +565,8 @@ with_basic_columns(EventName, Data) when is_map(Data) -> apply_event(EventName, GenEventMsg, _Env) -> EventTopic = event_topic(EventName), case emqx_rule_engine:get_rules_for_topic(EventTopic) of - [] -> ok; + [] -> + ok; Rules -> %% delay the generating of eventmsg after we have found some rules to apply emqx_rule_runtime:apply_rules(Rules, GenEventMsg()) @@ -429,18 +579,19 @@ columns(Event) -> [Key || {Key, _ExampleVal} <- columns_with_exam(Event)]. event_info() -> - [ event_info_message_publish() - , event_info_message_deliver() - , event_info_message_acked() - , event_info_message_dropped() - , event_info_client_connected() - , event_info_client_disconnected() - , event_info_client_connack() - , event_info_client_check_authz_complete() - , event_info_session_subscribed() - , event_info_session_unsubscribed() - , event_info_delivery_dropped() - , event_info_bridge_mqtt() + [ + event_info_message_publish(), + event_info_message_deliver(), + event_info_message_acked(), + event_info_message_dropped(), + event_info_client_connected(), + event_info_client_disconnected(), + event_info_client_connack(), + event_info_client_check_authz_complete(), + event_info_session_subscribed(), + event_info_session_unsubscribed(), + event_info_delivery_dropped(), + event_info_bridge_mqtt() ]. event_info_message_publish() -> @@ -469,7 +620,7 @@ event_info_message_dropped() -> 'message.dropped', {<<"message routing-drop">>, <<"消息转发丢弃"/utf8>>}, {<<"messages are discarded during routing, usually because there are no subscribers">>, - <<"消息在转发的过程中被丢弃,一般是由于没有订阅者"/utf8>>}, + <<"消息在转发的过程中被丢弃,一般是由于没有订阅者"/utf8>>}, <<"SELECT * FROM \"$events/message_dropped\" WHERE topic =~ 't/#'">> ). event_info_delivery_dropped() -> @@ -477,7 +628,7 @@ event_info_delivery_dropped() -> 'delivery.dropped', {<<"message delivery-drop">>, <<"消息投递丢弃"/utf8>>}, {<<"messages are discarded during delivery, i.e. because the message queue is full">>, - <<"消息在投递的过程中被丢弃,比如由于消息队列已满"/utf8>>}, + <<"消息在投递的过程中被丢弃,比如由于消息队列已满"/utf8>>}, <<"SELECT * FROM \"$events/delivery_dropped\" WHERE topic =~ 't/#'">> ). event_info_client_connected() -> @@ -496,18 +647,18 @@ event_info_client_disconnected() -> ). event_info_client_connack() -> event_info_common( - 'client.connack', - {<<"client connack">>, <<"连接确认"/utf8>>}, - {<<"client connack">>, <<"连接确认"/utf8>>}, - <<"SELECT * FROM \"$events/client_connack\"">> - ). + 'client.connack', + {<<"client connack">>, <<"连接确认"/utf8>>}, + {<<"client connack">>, <<"连接确认"/utf8>>}, + <<"SELECT * FROM \"$events/client_connack\"">> + ). event_info_client_check_authz_complete() -> event_info_common( - 'client.check_authz_complete', - {<<"client check authz complete">>, <<"鉴权结果"/utf8>>}, - {<<"client check authz complete">>, <<"鉴权结果"/utf8>>}, - <<"SELECT * FROM \"$events/client_check_authz_complete\"">> - ). + 'client.check_authz_complete', + {<<"client check authz complete">>, <<"鉴权结果"/utf8>>}, + {<<"client check authz complete">>, <<"鉴权结果"/utf8>>}, + <<"SELECT * FROM \"$events/client_check_authz_complete\"">> + ). event_info_session_subscribed() -> event_info_common( 'session.subscribed', @@ -522,7 +673,7 @@ event_info_session_unsubscribed() -> {<<"session unsubscribed">>, <<"会话取消订阅完成"/utf8>>}, <<"SELECT * FROM \"$events/session_unsubscribed\" WHERE topic =~ 't/#'">> ). -event_info_bridge_mqtt()-> +event_info_bridge_mqtt() -> event_info_common( <<"$bridges/mqtt:*">>, {<<"MQTT bridge message">>, <<"MQTT 桥接消息"/utf8>>}, @@ -531,239 +682,255 @@ event_info_bridge_mqtt()-> ). event_info_common(Event, {TitleEN, TitleZH}, {DescrEN, DescrZH}, SqlExam) -> - #{event => event_topic(Event), - title => #{en => TitleEN, zh => TitleZH}, - description => #{en => DescrEN, zh => DescrZH}, - test_columns => test_columns(Event), - columns => columns(Event), - sql_example => SqlExam + #{ + event => event_topic(Event), + title => #{en => TitleEN, zh => TitleZH}, + description => #{en => DescrEN, zh => DescrZH}, + test_columns => test_columns(Event), + columns => columns(Event), + sql_example => SqlExam }. test_columns('message.dropped') -> - [ {<<"reason">>, [<<"no_subscribers">>, <<"the reason of dropping">>]} - ] ++ test_columns('message.publish'); + [{<<"reason">>, [<<"no_subscribers">>, <<"the reason of dropping">>]}] ++ + test_columns('message.publish'); test_columns('message.publish') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid of the sender">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username of the sender">>]} - , {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]} - , {<<"qos">>, [1, <<"the QoS of the MQTT message">>]} - , {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid of the sender">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username of the sender">>]}, + {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]}, + {<<"qos">>, [1, <<"the QoS of the MQTT message">>]}, + {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} ]; test_columns('delivery.dropped') -> - [ {<<"reason">>, [<<"queue_full">>, <<"the reason of dropping">>]} - ] ++ test_columns('message.delivered'); + [{<<"reason">>, [<<"queue_full">>, <<"the reason of dropping">>]}] ++ + test_columns('message.delivered'); test_columns('message.acked') -> test_columns('message.delivered'); test_columns('message.delivered') -> - [ {<<"from_clientid">>, [<<"c_emqx_1">>, <<"the clientid of the sender">>]} - , {<<"from_username">>, [<<"u_emqx_1">>, <<"the username of the sender">>]} - , {<<"clientid">>, [<<"c_emqx_2">>, <<"the clientid of the receiver">>]} - , {<<"username">>, [<<"u_emqx_2">>, <<"the username of the receiver">>]} - , {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]} - , {<<"qos">>, [1, <<"the QoS of the MQTT message">>]} - , {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} + [ + {<<"from_clientid">>, [<<"c_emqx_1">>, <<"the clientid of the sender">>]}, + {<<"from_username">>, [<<"u_emqx_1">>, <<"the username of the sender">>]}, + {<<"clientid">>, [<<"c_emqx_2">>, <<"the clientid of the receiver">>]}, + {<<"username">>, [<<"u_emqx_2">>, <<"the username of the receiver">>]}, + {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]}, + {<<"qos">>, [1, <<"the QoS of the MQTT message">>]}, + {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} ]; test_columns('client.connected') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]} - , {<<"peername">>, [<<"127.0.0.1:52918">>, <<"the IP address and port of the client">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"peername">>, [<<"127.0.0.1:52918">>, <<"the IP address and port of the client">>]} ]; test_columns('client.disconnected') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]} - , {<<"reason">>, [<<"normal">>, <<"the reason for shutdown">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"reason">>, [<<"normal">>, <<"the reason for shutdown">>]} ]; test_columns('client.connack') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]} - , {<<"reason_code">>, [<<"sucess">>, <<"the reason code">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"reason_code">>, [<<"sucess">>, <<"the reason code">>]} ]; test_columns('client.check_authz_complete') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]} - , {<<"topic">>, [<<"t/1">>, <<"the topic of the MQTT message">>]} - , {<<"action">>, [<<"publish">>, <<"the action of publish or subscribe">>]} - , {<<"result">>, [<<"allow">>,<<"the authz check complete result">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"topic">>, [<<"t/1">>, <<"the topic of the MQTT message">>]}, + {<<"action">>, [<<"publish">>, <<"the action of publish or subscribe">>]}, + {<<"result">>, [<<"allow">>, <<"the authz check complete result">>]} ]; test_columns('session.unsubscribed') -> test_columns('session.subscribed'); test_columns('session.subscribed') -> - [ {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]} - , {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]} - , {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]} - , {<<"qos">>, [1, <<"the QoS of the MQTT message">>]} + [ + {<<"clientid">>, [<<"c_emqx">>, <<"the clientid if the client">>]}, + {<<"username">>, [<<"u_emqx">>, <<"the username if the client">>]}, + {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]}, + {<<"qos">>, [1, <<"the QoS of the MQTT message">>]} ]; test_columns(<<"$bridges/mqtt", _/binary>>) -> - [ {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]} - , {<<"qos">>, [1, <<"the QoS of the MQTT message">>]} - , {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} + [ + {<<"topic">>, [<<"t/a">>, <<"the topic of the MQTT message">>]}, + {<<"qos">>, [1, <<"the QoS of the MQTT message">>]}, + {<<"payload">>, [<<"{\"msg\": \"hello\"}">>, <<"the payload of the MQTT message">>]} ]. columns_with_exam('message.publish') -> - [ {<<"event">>, 'message.publish'} - , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"flags">>, #{}} - , {<<"headers">>, undefined} - , {<<"publish_received_at">>, erlang:system_time(millisecond)} - , columns_example_props(pub_props) - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'message.publish'}, + {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"payload">>, <<"{\"msg\": \"hello\"}">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"flags">>, #{}}, + {<<"headers">>, undefined}, + {<<"publish_received_at">>, erlang:system_time(millisecond)}, + columns_example_props(pub_props), + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('message.delivered') -> columns_message_ack_delivered('message.delivered'); columns_with_exam('message.acked') -> - [ columns_example_props(puback_props) - ] ++ - columns_message_ack_delivered('message.acked'); + [columns_example_props(puback_props)] ++ + columns_message_ack_delivered('message.acked'); columns_with_exam('message.dropped') -> - [ {<<"event">>, 'message.dropped'} - , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} - , {<<"reason">>, no_subscribers} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"flags">>, #{}} - , {<<"publish_received_at">>, erlang:system_time(millisecond)} - , columns_example_props(pub_props) - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'message.dropped'}, + {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())}, + {<<"reason">>, no_subscribers}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"payload">>, <<"{\"msg\": \"hello\"}">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"flags">>, #{}}, + {<<"publish_received_at">>, erlang:system_time(millisecond)}, + columns_example_props(pub_props), + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('delivery.dropped') -> - [ {<<"event">>, 'delivery.dropped'} - , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} - , {<<"reason">>, queue_full} - , {<<"from_clientid">>, <<"c_emqx_1">>} - , {<<"from_username">>, <<"u_emqx_1">>} - , {<<"clientid">>, <<"c_emqx_2">>} - , {<<"username">>, <<"u_emqx_2">>} - , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"flags">>, #{}} - , columns_example_props(pub_props) - , {<<"publish_received_at">>, erlang:system_time(millisecond)} - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'delivery.dropped'}, + {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())}, + {<<"reason">>, queue_full}, + {<<"from_clientid">>, <<"c_emqx_1">>}, + {<<"from_username">>, <<"u_emqx_1">>}, + {<<"clientid">>, <<"c_emqx_2">>}, + {<<"username">>, <<"u_emqx_2">>}, + {<<"payload">>, <<"{\"msg\": \"hello\"}">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"flags">>, #{}}, + columns_example_props(pub_props), + {<<"publish_received_at">>, erlang:system_time(millisecond)}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('client.connected') -> - [ {<<"event">>, 'client.connected'} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"mountpoint">>, undefined} - , {<<"peername">>, <<"192.168.0.10:56431">>} - , {<<"sockname">>, <<"0.0.0.0:1883">>} - , {<<"proto_name">>, <<"MQTT">>} - , {<<"proto_ver">>, 5} - , {<<"keepalive">>, 60} - , {<<"clean_start">>, true} - , {<<"expiry_interval">>, 3600} - , {<<"is_bridge">>, false} - , columns_example_props(conn_props) - , {<<"connected_at">>, erlang:system_time(millisecond)} - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'client.connected'}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"mountpoint">>, undefined}, + {<<"peername">>, <<"192.168.0.10:56431">>}, + {<<"sockname">>, <<"0.0.0.0:1883">>}, + {<<"proto_name">>, <<"MQTT">>}, + {<<"proto_ver">>, 5}, + {<<"keepalive">>, 60}, + {<<"clean_start">>, true}, + {<<"expiry_interval">>, 3600}, + {<<"is_bridge">>, false}, + columns_example_props(conn_props), + {<<"connected_at">>, erlang:system_time(millisecond)}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('client.disconnected') -> - [ {<<"event">>, 'client.disconnected'} - , {<<"reason">>, normal} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"peername">>, <<"192.168.0.10:56431">>} - , {<<"sockname">>, <<"0.0.0.0:1883">>} - , columns_example_props(disconn_props) - , {<<"disconnected_at">>, erlang:system_time(millisecond)} - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'client.disconnected'}, + {<<"reason">>, normal}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"peername">>, <<"192.168.0.10:56431">>}, + {<<"sockname">>, <<"0.0.0.0:1883">>}, + columns_example_props(disconn_props), + {<<"disconnected_at">>, erlang:system_time(millisecond)}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('client.connack') -> - [ {<<"event">>, 'client.connected'} - , {<<"reason_code">>, success} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"peername">>, <<"192.168.0.10:56431">>} - , {<<"sockname">>, <<"0.0.0.0:1883">>} - , {<<"proto_name">>, <<"MQTT">>} - , {<<"proto_ver">>, 5} - , {<<"keepalive">>, 60} - , {<<"clean_start">>, true} - , {<<"expiry_interval">>, 3600} - , {<<"connected_at">>, erlang:system_time(millisecond)} - , columns_example_props(conn_props) - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'client.connected'}, + {<<"reason_code">>, success}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"peername">>, <<"192.168.0.10:56431">>}, + {<<"sockname">>, <<"0.0.0.0:1883">>}, + {<<"proto_name">>, <<"MQTT">>}, + {<<"proto_ver">>, 5}, + {<<"keepalive">>, 60}, + {<<"clean_start">>, true}, + {<<"expiry_interval">>, 3600}, + {<<"connected_at">>, erlang:system_time(millisecond)}, + columns_example_props(conn_props), + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('client.check_authz_complete') -> - [ {<<"event">>, 'client.check_authz_complete'} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"action">>, <<"publish">>} - , {<<"authz_source">>, <<"cache">>} - , {<<"result">>, <<"allow">>} - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, 'client.check_authz_complete'}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"action">>, <<"publish">>}, + {<<"authz_source">>, <<"cache">>}, + {<<"result">>, <<"allow">>}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]; columns_with_exam('session.subscribed') -> - [ columns_example_props(sub_props) - ] ++ columns_message_sub_unsub('session.subscribed'); + [columns_example_props(sub_props)] ++ columns_message_sub_unsub('session.subscribed'); columns_with_exam('session.unsubscribed') -> - [ columns_example_props(unsub_props) - ] ++ columns_message_sub_unsub('session.unsubscribed'); + [columns_example_props(unsub_props)] ++ columns_message_sub_unsub('session.unsubscribed'); columns_with_exam(<<"$bridges/mqtt", _/binary>> = EventName) -> - [ {<<"event">>, EventName} - , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} - , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} - , {<<"server">>, <<"192.168.0.10:1883">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"dup">>, false} - , {<<"retain">>, false} - , columns_example_props(pub_props) - %% the time that we receiced the message from remote broker - , {<<"message_received_at">>, erlang:system_time(millisecond)} - %% the time that the rule is triggered - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, EventName}, + {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())}, + {<<"payload">>, <<"{\"msg\": \"hello\"}">>}, + {<<"server">>, <<"192.168.0.10:1883">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"dup">>, false}, + {<<"retain">>, false}, + columns_example_props(pub_props), + %% the time that we receiced the message from remote broker + {<<"message_received_at">>, erlang:system_time(millisecond)}, + %% the time that the rule is triggered + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]. columns_message_sub_unsub(EventName) -> - [ {<<"event">>, EventName} - , {<<"clientid">>, <<"c_emqx">>} - , {<<"username">>, <<"u_emqx">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, EventName}, + {<<"clientid">>, <<"c_emqx">>}, + {<<"username">>, <<"u_emqx">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]. columns_message_ack_delivered(EventName) -> - [ {<<"event">>, EventName} - , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} - , {<<"from_clientid">>, <<"c_emqx_1">>} - , {<<"from_username">>, <<"u_emqx_1">>} - , {<<"clientid">>, <<"c_emqx_2">>} - , {<<"username">>, <<"u_emqx_2">>} - , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} - , {<<"peerhost">>, <<"192.168.0.10">>} - , {<<"topic">>, <<"t/a">>} - , {<<"qos">>, 1} - , {<<"flags">>, #{}} - , {<<"publish_received_at">>, erlang:system_time(millisecond)} - , columns_example_props(pub_props) - , {<<"timestamp">>, erlang:system_time(millisecond)} - , {<<"node">>, node()} + [ + {<<"event">>, EventName}, + {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())}, + {<<"from_clientid">>, <<"c_emqx_1">>}, + {<<"from_username">>, <<"u_emqx_1">>}, + {<<"clientid">>, <<"c_emqx_2">>}, + {<<"username">>, <<"u_emqx_2">>}, + {<<"payload">>, <<"{\"msg\": \"hello\"}">>}, + {<<"peerhost">>, <<"192.168.0.10">>}, + {<<"topic">>, <<"t/a">>}, + {<<"qos">>, 1}, + {<<"flags">>, #{}}, + {<<"publish_received_at">>, erlang:system_time(millisecond)}, + columns_example_props(pub_props), + {<<"timestamp">>, erlang:system_time(millisecond)}, + {<<"node">>, node()} ]. columns_example_props(PropType) -> @@ -777,21 +944,23 @@ columns_example_props(PropType) -> {PropType, maps:merge(Props, UserProps)}. columns_example_props_specific(pub_props) -> - #{ 'Payload-Format-Indicator' => 0 - , 'Message-Expiry-Interval' => 30 - }; + #{ + 'Payload-Format-Indicator' => 0, + 'Message-Expiry-Interval' => 30 + }; columns_example_props_specific(puback_props) -> - #{ 'Reason-String' => <<"OK">> - }; + #{'Reason-String' => <<"OK">>}; columns_example_props_specific(conn_props) -> - #{ 'Session-Expiry-Interval' => 7200 - , 'Receive-Maximum' => 32 - }; + #{ + 'Session-Expiry-Interval' => 7200, + 'Receive-Maximum' => 32 + }; columns_example_props_specific(disconn_props) -> - #{ 'Session-Expiry-Interval' => 7200 - , 'Reason-String' => <<"Redirect to another server">> - , 'Server Reference' => <<"192.168.22.129">> - }; + #{ + 'Session-Expiry-Interval' => 7200, + 'Reason-String' => <<"Redirect to another server">>, + 'Server Reference' => <<"192.168.22.129">> + }; columns_example_props_specific(sub_props) -> #{}; columns_example_props_specific(unsub_props) -> @@ -817,19 +986,15 @@ reason({Error, _}) when is_atom(Error) -> Error; reason(_) -> internal_error. ntoa(undefined) -> undefined; -ntoa({IpAddr, Port}) -> - iolist_to_binary([inet:ntoa(IpAddr), ":", integer_to_list(Port)]); -ntoa(IpAddr) -> - iolist_to_binary(inet:ntoa(IpAddr)). +ntoa({IpAddr, Port}) -> iolist_to_binary([inet:ntoa(IpAddr), ":", integer_to_list(Port)]); +ntoa(IpAddr) -> iolist_to_binary(inet:ntoa(IpAddr)). event_name(<<"$events/client_connected", _/binary>>) -> 'client.connected'; event_name(<<"$events/client_disconnected", _/binary>>) -> 'client.disconnected'; event_name(<<"$events/client_connack", _/binary>>) -> 'client.connack'; -event_name(<<"$events/client_check_authz_complete", _/binary>>) -> - 'client.check_authz_complete'; +event_name(<<"$events/client_check_authz_complete", _/binary>>) -> 'client.check_authz_complete'; event_name(<<"$events/session_subscribed", _/binary>>) -> 'session.subscribed'; -event_name(<<"$events/session_unsubscribed", _/binary>>) -> - 'session.unsubscribed'; +event_name(<<"$events/session_unsubscribed", _/binary>>) -> 'session.unsubscribed'; event_name(<<"$events/message_delivered", _/binary>>) -> 'message.delivered'; event_name(<<"$events/message_acked", _/binary>>) -> 'message.acked'; event_name(<<"$events/message_dropped", _/binary>>) -> 'message.dropped'; @@ -840,8 +1005,7 @@ event_name(_) -> 'message.publish'. event_topic('client.connected') -> <<"$events/client_connected">>; event_topic('client.disconnected') -> <<"$events/client_disconnected">>; event_topic('client.connack') -> <<"$events/client_connack">>; -event_topic('client.check_authz_complete') -> - <<"$events/client_check_authz_complete">>; +event_topic('client.check_authz_complete') -> <<"$events/client_check_authz_complete">>; event_topic('session.subscribed') -> <<"$events/session_subscribed">>; event_topic('session.unsubscribed') -> <<"$events/session_unsubscribed">>; event_topic('message.delivered') -> <<"$events/message_delivered">>; @@ -851,10 +1015,12 @@ event_topic('delivery.dropped') -> <<"$events/delivery_dropped">>; event_topic('message.publish') -> <<"$events/message_publish">>; event_topic(<<"$bridges/", _/binary>> = Topic) -> Topic. -printable_maps(undefined) -> #{}; +printable_maps(undefined) -> + #{}; printable_maps(Headers) -> maps:fold( - fun (K, V0, AccIn) when K =:= peerhost; K =:= peername; K =:= sockname -> + fun + (K, V0, AccIn) when K =:= peerhost; K =:= peername; K =:= sockname -> AccIn#{K => ntoa(V0)}; ('User-Property', V0, AccIn) when is_list(V0) -> AccIn#{ @@ -863,15 +1029,22 @@ printable_maps(Headers) -> %% However, this does not allow duplicate property keys. To allow %% duplicate keys, we have to use the 'User-Property-Pairs' field instead. 'User-Property' => maps:from_list(V0), - 'User-Property-Pairs' => [#{ - key => Key, - value => Value - } || {Key, Value} <- V0] + 'User-Property-Pairs' => [ + #{ + key => Key, + value => Value + } + || {Key, Value} <- V0 + ] }; - (K, V0, AccIn) -> AccIn#{K => V0} - end, #{}, Headers). + (K, V0, AccIn) -> + AccIn#{K => V0} + end, + #{}, + Headers + ). ignore_sys_message(#message{flags = Flags}) -> ConfigRootKey = emqx_rule_engine_schema:namespace(), maps:get(sys, Flags, false) andalso - emqx:get_config([ConfigRootKey, ignore_sys_message]). + emqx:get_config([ConfigRootKey, ignore_sys_message]). diff --git a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl index e7f5da52f..1c887327c 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_funcs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_funcs.erl @@ -21,257 +21,303 @@ -include_lib("emqx/include/logger.hrl"). %% IoT Funcs --export([ msgid/0 - , qos/0 - , flags/0 - , flag/1 - , topic/0 - , topic/1 - , clientid/0 - , clientip/0 - , peerhost/0 - , username/0 - , payload/0 - , payload/1 - , contains_topic/2 - , contains_topic/3 - , contains_topic_match/2 - , contains_topic_match/3 - , null/0 - ]). +-export([ + msgid/0, + qos/0, + flags/0, + flag/1, + topic/0, + topic/1, + clientid/0, + clientip/0, + peerhost/0, + username/0, + payload/0, + payload/1, + contains_topic/2, + contains_topic/3, + contains_topic_match/2, + contains_topic_match/3, + null/0 +]). %% Arithmetic Funcs --export([ '+'/2 - , '-'/2 - , '*'/2 - , '/'/2 - , 'div'/2 - , mod/2 - , eq/2 - ]). +-export([ + '+'/2, + '-'/2, + '*'/2, + '/'/2, + 'div'/2, + mod/2, + eq/2 +]). %% Math Funcs --export([ abs/1 - , acos/1 - , acosh/1 - , asin/1 - , asinh/1 - , atan/1 - , atanh/1 - , ceil/1 - , cos/1 - , cosh/1 - , exp/1 - , floor/1 - , fmod/2 - , log/1 - , log10/1 - , log2/1 - , power/2 - , round/1 - , sin/1 - , sinh/1 - , sqrt/1 - , tan/1 - , tanh/1 - ]). +-export([ + abs/1, + acos/1, + acosh/1, + asin/1, + asinh/1, + atan/1, + atanh/1, + ceil/1, + cos/1, + cosh/1, + exp/1, + floor/1, + fmod/2, + log/1, + log10/1, + log2/1, + power/2, + round/1, + sin/1, + sinh/1, + sqrt/1, + tan/1, + tanh/1 +]). %% Bits Funcs --export([ bitnot/1 - , bitand/2 - , bitor/2 - , bitxor/2 - , bitsl/2 - , bitsr/2 - , bitsize/1 - , subbits/2 - , subbits/3 - , subbits/6 - ]). +-export([ + bitnot/1, + bitand/2, + bitor/2, + bitxor/2, + bitsl/2, + bitsr/2, + bitsize/1, + subbits/2, + subbits/3, + subbits/6 +]). %% Data Type Conversion --export([ str/1 - , str_utf8/1 - , bool/1 - , int/1 - , float/1 - , float/2 - , map/1 - , bin2hexstr/1 - , hexstr2bin/1 - ]). +-export([ + str/1, + str_utf8/1, + bool/1, + int/1, + float/1, + float/2, + map/1, + bin2hexstr/1, + hexstr2bin/1 +]). %% Data Type Validation Funcs --export([ is_null/1 - , is_not_null/1 - , is_str/1 - , is_bool/1 - , is_int/1 - , is_float/1 - , is_num/1 - , is_map/1 - , is_array/1 - ]). +-export([ + is_null/1, + is_not_null/1, + is_str/1, + is_bool/1, + is_int/1, + is_float/1, + is_num/1, + is_map/1, + is_array/1 +]). %% String Funcs --export([ lower/1 - , ltrim/1 - , reverse/1 - , rtrim/1 - , strlen/1 - , substr/2 - , substr/3 - , trim/1 - , upper/1 - , split/2 - , split/3 - , concat/2 - , tokens/2 - , tokens/3 - , sprintf_s/2 - , pad/2 - , pad/3 - , pad/4 - , replace/3 - , replace/4 - , regex_match/2 - , regex_replace/3 - , ascii/1 - , find/2 - , find/3 - ]). +-export([ + lower/1, + ltrim/1, + reverse/1, + rtrim/1, + strlen/1, + substr/2, + substr/3, + trim/1, + upper/1, + split/2, + split/3, + concat/2, + tokens/2, + tokens/3, + sprintf_s/2, + pad/2, + pad/3, + pad/4, + replace/3, + replace/4, + regex_match/2, + regex_replace/3, + ascii/1, + find/2, + find/3 +]). %% Map Funcs --export([ map_new/0 - ]). +-export([map_new/0]). --export([ map_get/2 - , map_get/3 - , map_put/3 - ]). +-export([ + map_get/2, + map_get/3, + map_put/3 +]). %% For backward compatibility --export([ mget/2 - , mget/3 - , mput/3 - ]). +-export([ + mget/2, + mget/3, + mput/3 +]). %% Array Funcs --export([ nth/2 - , length/1 - , sublist/2 - , sublist/3 - , first/1 - , last/1 - , contains/2 - ]). +-export([ + nth/2, + length/1, + sublist/2, + sublist/3, + first/1, + last/1, + contains/2 +]). %% Hash Funcs --export([ md5/1 - , sha/1 - , sha256/1 - ]). +-export([ + md5/1, + sha/1, + sha256/1 +]). %% Data encode and decode --export([ base64_encode/1 - , base64_decode/1 - , json_decode/1 - , json_encode/1 - , term_decode/1 - , term_encode/1 - ]). +-export([ + base64_encode/1, + base64_decode/1, + json_decode/1, + json_encode/1, + term_decode/1, + term_encode/1 +]). %% Date functions --export([ now_rfc3339/0 - , now_rfc3339/1 - , unix_ts_to_rfc3339/1 - , unix_ts_to_rfc3339/2 - , rfc3339_to_unix_ts/1 - , rfc3339_to_unix_ts/2 - , now_timestamp/0 - , now_timestamp/1 - , format_date/3 - , format_date/4 - , date_to_unix_ts/4 - ]). +-export([ + now_rfc3339/0, + now_rfc3339/1, + unix_ts_to_rfc3339/1, + unix_ts_to_rfc3339/2, + rfc3339_to_unix_ts/1, + 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 - -export([ proc_dict_get/1 - , proc_dict_put/2 - , proc_dict_del/1 - , kv_store_get/1 - , kv_store_get/2 - , kv_store_put/2 - , kv_store_del/1 - ]). +-export([ + proc_dict_get/1, + proc_dict_put/2, + proc_dict_del/1, + kv_store_get/1, + kv_store_get/2, + kv_store_put/2, + kv_store_del/1 +]). -export(['$handle_undefined_function'/2]). --compile({no_auto_import, - [ abs/1 - , ceil/1 - , floor/1 - , round/1 - , map_get/2 - ]}). +-compile( + {no_auto_import, [ + abs/1, + ceil/1, + floor/1, + round/1, + map_get/2 + ]} +). -define(is_var(X), is_binary(X)). %% @doc "msgid()" Func msgid() -> - fun(#{id := MsgId}) -> MsgId; (_) -> undefined end. + fun + (#{id := MsgId}) -> MsgId; + (_) -> undefined + end. %% @doc "qos()" Func qos() -> - fun(#{qos := QoS}) -> QoS; (_) -> undefined end. + fun + (#{qos := QoS}) -> QoS; + (_) -> undefined + end. %% @doc "topic()" Func topic() -> - fun(#{topic := Topic}) -> Topic; (_) -> undefined end. + fun + (#{topic := Topic}) -> Topic; + (_) -> undefined + end. %% @doc "topic(N)" Func topic(I) when is_integer(I) -> - fun(#{topic := Topic}) -> + fun + (#{topic := Topic}) -> lists:nth(I, emqx_topic:tokens(Topic)); - (_) -> undefined + (_) -> + undefined end. %% @doc "flags()" Func flags() -> - fun(#{flags := Flags}) -> Flags; (_) -> #{} end. + fun + (#{flags := Flags}) -> Flags; + (_) -> #{} + end. %% @doc "flags(Name)" Func flag(Name) -> - fun(#{flags := Flags}) -> emqx_rule_maps:nested_get({var,Name}, Flags); (_) -> undefined end. + fun + (#{flags := Flags}) -> emqx_rule_maps:nested_get({var, Name}, Flags); + (_) -> undefined + end. %% @doc "clientid()" Func clientid() -> - fun(#{from := ClientId}) -> ClientId; (_) -> undefined end. + fun + (#{from := ClientId}) -> ClientId; + (_) -> undefined + end. %% @doc "username()" Func username() -> - fun(#{username := Username}) -> Username; (_) -> undefined end. + fun + (#{username := Username}) -> Username; + (_) -> undefined + end. %% @doc "clientip()" Func clientip() -> peerhost(). peerhost() -> - fun(#{peerhost := Addr}) -> Addr; (_) -> undefined end. + fun + (#{peerhost := Addr}) -> Addr; + (_) -> undefined + end. payload() -> - fun(#{payload := Payload}) -> Payload; (_) -> undefined end. + fun + (#{payload := Payload}) -> Payload; + (_) -> undefined + end. payload(Path) -> - fun(#{payload := Payload}) when erlang:is_map(Payload) -> + fun + (#{payload := Payload}) when erlang:is_map(Payload) -> emqx_rule_maps:nested_get(map_path(Path), Payload); - (_) -> undefined + (_) -> + undefined end. %% @doc Check if a topic_filter contains a specific topic %% TopicFilters = [{<<"t/a">>, #{qos => 0}]. --spec(contains_topic(emqx_types:topic_filters(), emqx_types:topic()) - -> true | false). +-spec contains_topic(emqx_types:topic_filters(), emqx_types:topic()) -> + true | false. contains_topic(TopicFilters, Topic) -> case find_topic_filter(Topic, TopicFilters, fun eq/2) of not_found -> false; @@ -283,8 +329,8 @@ contains_topic(TopicFilters, Topic, QoS) -> _ -> false end. --spec(contains_topic_match(emqx_types:topic_filters(), emqx_types:topic()) - -> true | false). +-spec contains_topic_match(emqx_types:topic_filters(), emqx_types:topic()) -> + true | false. contains_topic_match(TopicFilters, Topic) -> case find_topic_filter(Topic, TopicFilters, fun emqx_topic:match/2) of not_found -> false; @@ -298,10 +344,13 @@ contains_topic_match(TopicFilters, Topic, QoS) -> find_topic_filter(Filter, TopicFilters, Func) -> try - [case Func(Topic, Filter) of - true -> throw(Result); - false -> ok - end || Result = #{topic := Topic} <- TopicFilters], + [ + case Func(Topic, Filter) of + true -> throw(Result); + false -> ok + end + || Result = #{topic := Topic} <- TopicFilters + ], not_found catch throw:Result -> Result @@ -317,7 +366,6 @@ null() -> %% plus 2 numbers '+'(X, Y) when is_number(X), is_number(Y) -> X + Y; - %% string concatenation %% this requires one of the arguments is string, the other argument will be converted %% to string automatically (implicit conversion) @@ -355,7 +403,7 @@ acos(N) when is_number(N) -> acosh(N) when is_number(N) -> math:acosh(N). -asin(N) when is_number(N)-> +asin(N) when is_number(N) -> math:asin(N). asinh(N) when is_number(N) -> @@ -364,19 +412,19 @@ asinh(N) when is_number(N) -> atan(N) when is_number(N) -> math:atan(N). -atanh(N) when is_number(N)-> +atanh(N) when is_number(N) -> math:atanh(N). ceil(N) when is_number(N) -> erlang:ceil(N). -cos(N) when is_number(N)-> +cos(N) when is_number(N) -> math:cos(N). cosh(N) when is_number(N) -> math:cosh(N). -exp(N) when is_number(N)-> +exp(N) when is_number(N) -> math:exp(N). floor(N) when is_number(N) -> @@ -391,7 +439,7 @@ log(N) when is_number(N) -> log10(N) when is_number(N) -> math:log10(N). -log2(N) when is_number(N)-> +log2(N) when is_number(N) -> math:log2(N). power(X, Y) when is_number(X), is_number(Y) -> @@ -446,7 +494,9 @@ subbits(Bits, Len) when is_integer(Len), is_bitstring(Bits) -> subbits(Bits, Start, Len) when is_integer(Start), is_integer(Len), is_bitstring(Bits) -> get_subbits(Bits, Start, Len, <<"integer">>, <<"unsigned">>, <<"big">>). -subbits(Bits, Start, Len, Type, Signedness, Endianness) when is_integer(Start), is_integer(Len), is_bitstring(Bits) -> +subbits(Bits, Start, Len, Type, Signedness, Endianness) when + is_integer(Start), is_integer(Len), is_bitstring(Bits) +-> get_subbits(Bits, Start, Len, Type, Signedness, Endianness). get_subbits(Bits, Start, Len, Type, Signedness, Endianness) -> @@ -455,7 +505,8 @@ get_subbits(Bits, Start, Len, Type, Signedness, Endianness) -> <<_:Begin, Rem/bits>> when Rem =/= <<>> -> Sz = bit_size(Rem), do_get_subbits(Rem, Sz, Len, Type, Signedness, Endianness); - _ -> undefined + _ -> + undefined end. -define(match_bits(Bits0, Pattern, ElesePattern), @@ -464,46 +515,80 @@ get_subbits(Bits, Start, Len, Type, Signedness, Endianness) -> SubBits; ElesePattern -> SubBits - end). + end +). do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); - + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"big">>) -> - ?match_bits(Bits, <>, - <>); - + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>); - + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>); + ?match_bits( + Bits, + <>, + <> + ); do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"little">>) -> - ?match_bits(Bits, <>, - <>). + ?match_bits( + Bits, + <>, + <> + ). %%------------------------------------------------------------------------------ %% Data Type Conversion Funcs @@ -590,9 +675,11 @@ strlen(S) when is_binary(S) -> substr(S, Start) when is_binary(S), is_integer(Start) -> string:slice(S, Start). -substr(S, Start, Length) when is_binary(S), - is_integer(Start), - is_integer(Length) -> +substr(S, Start, Length) when + is_binary(S), + is_integer(Start), + is_integer(Length) +-> string:slice(S, Start, Length). trim(S) when is_binary(S) -> @@ -601,26 +688,28 @@ trim(S) when is_binary(S) -> upper(S) when is_binary(S) -> string:uppercase(S). -split(S, P) when is_binary(S),is_binary(P) -> +split(S, P) when is_binary(S), is_binary(P) -> [R || R <- string:split(S, P, all), R =/= <<>> andalso R =/= ""]. split(S, P, <<"notrim">>) -> string:split(S, P, all); - split(S, P, <<"leading_notrim">>) -> string:split(S, P, leading); -split(S, P, <<"leading">>) when is_binary(S),is_binary(P) -> +split(S, P, <<"leading">>) when is_binary(S), is_binary(P) -> [R || R <- string:split(S, P, leading), R =/= <<>> andalso R =/= ""]; split(S, P, <<"trailing_notrim">>) -> string:split(S, P, trailing); -split(S, P, <<"trailing">>) when is_binary(S),is_binary(P) -> +split(S, P, <<"trailing">>) when is_binary(S), is_binary(P) -> [R || R <- string:split(S, P, trailing), R =/= <<>> andalso R =/= ""]. tokens(S, Separators) -> [list_to_binary(R) || R <- string:lexemes(binary_to_list(S), binary_to_list(Separators))]. tokens(S, Separators, <<"nocrlf">>) -> - [list_to_binary(R) || R <- string:lexemes(binary_to_list(S), binary_to_list(Separators) ++ [$\r,$\n,[$\r,$\n]])]. + [ + list_to_binary(R) + || R <- string:lexemes(binary_to_list(S), binary_to_list(Separators) ++ [$\r, $\n, [$\r, $\n]]) + ]. %% implicit convert args to strings, and then do concatenation concat(S1, S2) -> @@ -634,21 +723,17 @@ pad(S, Len) when is_binary(S), is_integer(Len) -> pad(S, Len, <<"trailing">>) when is_binary(S), is_integer(Len) -> iolist_to_binary(string:pad(S, Len, trailing)); - pad(S, Len, <<"both">>) when is_binary(S), is_integer(Len) -> iolist_to_binary(string:pad(S, Len, both)); - pad(S, Len, <<"leading">>) when is_binary(S), is_integer(Len) -> iolist_to_binary(string:pad(S, Len, leading)). pad(S, Len, <<"trailing">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) -> Chars = unicode:characters_to_list(Char, utf8), iolist_to_binary(string:pad(S, Len, trailing, Chars)); - pad(S, Len, <<"both">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) -> Chars = unicode:characters_to_list(Char, utf8), iolist_to_binary(string:pad(S, Len, both, Chars)); - pad(S, Len, <<"leading">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) -> Chars = unicode:characters_to_list(Char, utf8), iolist_to_binary(string:pad(S, Len, leading, Chars)). @@ -658,24 +743,24 @@ replace(SrcStr, P, RepStr) when is_binary(SrcStr), is_binary(P), is_binary(RepSt replace(SrcStr, P, RepStr, <<"all">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) -> iolist_to_binary(string:replace(SrcStr, P, RepStr, all)); - -replace(SrcStr, P, RepStr, <<"trailing">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) -> +replace(SrcStr, P, RepStr, <<"trailing">>) when + is_binary(SrcStr), is_binary(P), is_binary(RepStr) +-> iolist_to_binary(string:replace(SrcStr, P, RepStr, trailing)); - replace(SrcStr, P, RepStr, <<"leading">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) -> iolist_to_binary(string:replace(SrcStr, P, RepStr, leading)). regex_match(Str, RE) -> - case re:run(Str, RE, [global,{capture,none}]) of + case re:run(Str, RE, [global, {capture, none}]) of match -> true; nomatch -> false end. regex_replace(SrcStr, RE, RepStr) -> - re:replace(SrcStr, RE, RepStr, [global, {return,binary}]). + re:replace(SrcStr, RE, RepStr, [global, {return, binary}]). ascii(Char) when is_binary(Char) -> - [FirstC| _] = binary_to_list(Char), + [FirstC | _] = binary_to_list(Char), FirstC. find(S, P) when is_binary(S), is_binary(P) -> @@ -683,7 +768,6 @@ find(S, P) when is_binary(S), is_binary(P) -> find(S, P, <<"trailing">>) when is_binary(S), is_binary(P) -> find_s(S, P, trailing); - find(S, P, <<"leading">>) when is_binary(S), is_binary(P) -> find_s(S, P, leading). @@ -735,7 +819,8 @@ mget(Key, Map) -> mget(Key, Map, Default) -> case maps:find(Key, Map) of - {ok, Val} -> Val; + {ok, Val} -> + Val; error when is_atom(Key) -> %% the map may have an equivalent binary-form key BinKey = emqx_plugin_libs_rule:bin(Key), @@ -744,14 +829,16 @@ mget(Key, Map, Default) -> error -> Default end; error when is_binary(Key) -> - try %% the map may have an equivalent atom-form key + %% the map may have an equivalent atom-form key + try AtomKey = list_to_existing_atom(binary_to_list(Key)), case maps:find(AtomKey, Map) of {ok, Val} -> Val; error -> Default end - catch error:badarg -> - Default + catch + error:badarg -> + Default end; error -> Default @@ -759,7 +846,8 @@ mget(Key, Map, Default) -> mput(Key, Val, Map) -> case maps:find(Key, Map) of - {ok, _} -> maps:put(Key, Val, Map); + {ok, _} -> + maps:put(Key, Val, Map); error when is_atom(Key) -> %% the map may have an equivalent binary-form key BinKey = emqx_plugin_libs_rule:bin(Key), @@ -768,14 +856,16 @@ mput(Key, Val, Map) -> error -> maps:put(Key, Val, Map) end; error when is_binary(Key) -> - try %% the map may have an equivalent atom-form key + %% the map may have an equivalent atom-form key + try AtomKey = list_to_existing_atom(binary_to_list(Key)), case maps:find(AtomKey, Map) of {ok, _} -> maps:put(AtomKey, Val, Map); error -> maps:put(Key, Val, Map) end - catch error:badarg -> - maps:put(Key, Val, Map) + catch + error:badarg -> + maps:put(Key, Val, Map) end; error -> maps:put(Key, Val, Map) @@ -863,14 +953,18 @@ unix_ts_to_rfc3339(Epoch) -> unix_ts_to_rfc3339(Epoch, Unit) when is_integer(Epoch) -> emqx_plugin_libs_rule:bin( calendar:system_time_to_rfc3339( - Epoch, [{unit, time_unit(Unit)}])). + Epoch, [{unit, time_unit(Unit)}] + ) + ). rfc3339_to_unix_ts(DateTime) -> rfc3339_to_unix_ts(DateTime, <<"second">>). rfc3339_to_unix_ts(DateTime, Unit) when is_binary(DateTime) -> - calendar:rfc3339_to_system_time(binary_to_list(DateTime), - [{unit, time_unit(Unit)}]). + calendar:rfc3339_to_system_time( + binary_to_list(DateTime), + [{unit, time_unit(Unit)}] + ). now_timestamp() -> erlang:system_time(second). @@ -885,22 +979,30 @@ 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))). + 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)). + 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)). + 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 @@ -922,9 +1024,8 @@ date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) -> % '$handle_undefined_function'(Fun, Args) -> % error({sql_function_not_supported, function_literal(Fun, Args)}). -'$handle_undefined_function'(sprintf, [Format|Args]) -> +'$handle_undefined_function'(sprintf, [Format | Args]) -> erlang:apply(fun sprintf_s/2, [Format, Args]); - '$handle_undefined_function'(Fun, Args) -> error({sql_function_not_supported, function_literal(Fun, Args)}). @@ -935,8 +1036,12 @@ function_literal(Fun, []) when is_atom(Fun) -> atom_to_list(Fun) ++ "()"; function_literal(Fun, [FArg | Args]) when is_atom(Fun), is_list(Args) -> WithFirstArg = io_lib:format("~ts(~0p", [atom_to_list(Fun), FArg]), - lists:foldl(fun(Arg, Literal) -> - io_lib:format("~ts, ~0p", [Literal, Arg]) - end, WithFirstArg, Args) ++ ")"; + lists:foldl( + fun(Arg, Literal) -> + io_lib:format("~ts, ~0p", [Literal, Arg]) + end, + WithFirstArg, + Args + ) ++ ")"; function_literal(Fun, Args) -> {invalid_func, {Fun, Args}}. diff --git a/apps/emqx_rule_engine/src/emqx_rule_maps.erl b/apps/emqx_rule_engine/src/emqx_rule_maps.erl index 31ae2eee2..5d887b68b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_maps.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_maps.erl @@ -16,14 +16,15 @@ -module(emqx_rule_maps). --export([ nested_get/2 - , nested_get/3 - , nested_put/3 - , range_gen/2 - , range_get/3 - , atom_key_map/1 - , unsafe_atom_key_map/1 - ]). +-export([ + nested_get/2, + nested_get/3, + nested_put/3, + range_gen/2, + range_get/3, + atom_key_map/1, + unsafe_atom_key_map/1 +]). nested_get(Key, Data) -> nested_get(Key, Data, undefined). @@ -41,8 +42,10 @@ do_nested_get([Key | More], Data, OrgData, Default) -> do_nested_get([], Val, _OrgData, _Default) -> Val. -nested_put(Key, Val, Data) when not is_map(Data), - not is_list(Data) -> +nested_put(Key, Val, Data) when + not is_map(Data), + not is_list(Data) +-> nested_put(Key, Val, #{}); nested_put({var, Key}, Val, Map) -> general_map_put({key, Key}, Val, Map, Map); @@ -56,19 +59,27 @@ do_nested_put([], Val, _Map, _OrgData) -> Val. general_map_get(Key, Map, OrgData, Default) -> - general_find(Key, Map, OrgData, + general_find( + Key, + Map, + OrgData, fun ({equivalent, {_EquiKey, Val}}) -> Val; ({found, {_Key, Val}}) -> Val; (not_found) -> Default - end). + end + ). general_map_put(Key, Val, Map, OrgData) -> - general_find(Key, Map, OrgData, + general_find( + Key, + Map, + OrgData, fun ({equivalent, {EquiKey, _Val}}) -> do_put(EquiKey, Val, Map, OrgData); (_) -> do_put(Key, Val, Map, OrgData) - end). + end + ). general_find(KeyOrIndex, Data, OrgData, Handler) when is_binary(Data) -> try emqx_json:decode(Data, [return_maps]) of @@ -78,7 +89,8 @@ general_find(KeyOrIndex, Data, OrgData, Handler) when is_binary(Data) -> end; general_find({key, Key}, Map, _OrgData, Handler) when is_map(Map) -> case maps:find(Key, Map) of - {ok, Val} -> Handler({found, {{key, Key}, Val}}); + {ok, Val} -> + Handler({found, {{key, Key}, Val}}); error when is_atom(Key) -> %% the map may have an equivalent binary-form key BinKey = emqx_plugin_libs_rule:bin(Key), @@ -87,14 +99,16 @@ general_find({key, Key}, Map, _OrgData, Handler) when is_map(Map) -> error -> Handler(not_found) end; error when is_binary(Key) -> - try %% the map may have an equivalent atom-form key + %% the map may have an equivalent atom-form key + try AtomKey = list_to_existing_atom(binary_to_list(Key)), case maps:find(AtomKey, Map) of {ok, Val} -> Handler({equivalent, {{key, AtomKey}, Val}}); error -> Handler(not_found) end - catch error:badarg -> - Handler(not_found) + catch + error:badarg -> + Handler(not_found) end; error -> Handler(not_found) @@ -122,18 +136,21 @@ do_put({index, Index0}, Val, List, OrgData) -> setnth(_, Data, Val) when not is_list(Data) -> setnth(head, [], Val); setnth(head, List, Val) when is_list(List) -> [Val | List]; -setnth(head, _List, Val) -> [Val]; +setnth(head, _List, Val) -> + [Val]; setnth(tail, List, Val) when is_list(List) -> List ++ [Val]; -setnth(tail, _List, Val) -> [Val]; +setnth(tail, _List, Val) -> + [Val]; setnth(I, List, _Val) when not is_integer(I) -> List; -setnth(0, List, _Val) -> List; +setnth(0, List, _Val) -> + List; setnth(I, List, Val) when is_integer(I), I > 0 -> do_setnth(I, List, Val); setnth(I, List, Val) when is_integer(I), I < 0 -> lists:reverse(do_setnth(-I, lists:reverse(List), Val)). do_setnth(1, [_ | Rest], Val) -> [Val | Rest]; -do_setnth(I, [E | Rest], Val) -> [E | setnth(I-1, Rest, Val)]; +do_setnth(I, [E | Rest], Val) -> [E | setnth(I - 1, Rest, Val)]; do_setnth(_, [], _Val) -> []. getnth(0, _) -> @@ -144,8 +161,10 @@ getnth(I, L) when I < 0 -> do_getnth(-I, lists:reverse(L)). do_getnth(I, L) -> - try {ok, lists:nth(I, L)} - catch error:_ -> {error, not_found} + try + {ok, lists:nth(I, L)} + catch + error:_ -> {error, not_found} end. handle_getnth(Index, List, IndexPattern, Handler) -> @@ -170,7 +189,8 @@ do_range_get(Begin, End, List) -> EndIndex = index(End, TotalLen), lists:sublist(List, BeginIndex, (EndIndex - BeginIndex + 1)). -index(0, _) -> error({invalid_index, 0}); +index(0, _) -> + error({invalid_index, 0}); index(Index, _) when Index > 0 -> Index; index(Index, Len) when Index < 0 -> Len + Index + 1. @@ -180,26 +200,36 @@ index(Index, Len) when Index < 0 -> %%%------------------------------------------------------------------- atom_key_map(BinKeyMap) when is_map(BinKeyMap) -> maps:fold( - fun(K, V, Acc) when is_binary(K) -> - Acc#{binary_to_existing_atom(K, utf8) => atom_key_map(V)}; - (K, V, Acc) when is_list(K) -> - Acc#{list_to_existing_atom(K) => atom_key_map(V)}; - (K, V, Acc) when is_atom(K) -> - Acc#{K => atom_key_map(V)} - end, #{}, BinKeyMap); + fun + (K, V, Acc) when is_binary(K) -> + Acc#{binary_to_existing_atom(K, utf8) => atom_key_map(V)}; + (K, V, Acc) when is_list(K) -> + Acc#{list_to_existing_atom(K) => atom_key_map(V)}; + (K, V, Acc) when is_atom(K) -> + Acc#{K => atom_key_map(V)} + end, + #{}, + BinKeyMap + ); atom_key_map(ListV) when is_list(ListV) -> [atom_key_map(V) || V <- ListV]; -atom_key_map(Val) -> Val. +atom_key_map(Val) -> + Val. unsafe_atom_key_map(BinKeyMap) when is_map(BinKeyMap) -> maps:fold( - fun(K, V, Acc) when is_binary(K) -> - Acc#{binary_to_atom(K, utf8) => unsafe_atom_key_map(V)}; - (K, V, Acc) when is_list(K) -> - Acc#{list_to_atom(K) => unsafe_atom_key_map(V)}; - (K, V, Acc) when is_atom(K) -> - Acc#{K => unsafe_atom_key_map(V)} - end, #{}, BinKeyMap); + fun + (K, V, Acc) when is_binary(K) -> + Acc#{binary_to_atom(K, utf8) => unsafe_atom_key_map(V)}; + (K, V, Acc) when is_list(K) -> + Acc#{list_to_atom(K) => unsafe_atom_key_map(V)}; + (K, V, Acc) when is_atom(K) -> + Acc#{K => unsafe_atom_key_map(V)} + end, + #{}, + BinKeyMap + ); unsafe_atom_key_map(ListV) when is_list(ListV) -> [unsafe_atom_key_map(V) || V <- ListV]; -unsafe_atom_key_map(Val) -> Val. +unsafe_atom_key_map(Val) -> + Val. diff --git a/apps/emqx_rule_engine/src/emqx_rule_outputs.erl b/apps/emqx_rule_engine/src/emqx_rule_outputs.erl index 6a858a73b..a0d06c978 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_outputs.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_outputs.erl @@ -22,20 +22,18 @@ -include_lib("emqx/include/emqx.hrl"). %% APIs --export([ parse_output/1 - ]). +-export([parse_output/1]). %% callbacks of emqx_rule_output --export([ pre_process_output_args/2 - ]). +-export([pre_process_output_args/2]). %% output functions --export([ console/3 - , republish/3 - ]). +-export([ + console/3, + republish/3 +]). --optional_callbacks([ pre_process_output_args/2 - ]). +-optional_callbacks([pre_process_output_args/2]). -callback pre_process_output_args(FuncName :: atom(), output_fun_args()) -> output_fun_args(). @@ -44,20 +42,32 @@ %%-------------------------------------------------------------------- parse_output(#{function := OutputFunc} = Output) -> {Mod, Func} = parse_output_func(OutputFunc), - #{mod => Mod, func => Func, - args => pre_process_args(Mod, Func, maps:get(args, Output, #{}))}. + #{ + mod => Mod, + func => Func, + args => pre_process_args(Mod, Func, maps:get(args, Output, #{})) + }. %%-------------------------------------------------------------------- %% callbacks of emqx_rule_output %%-------------------------------------------------------------------- -pre_process_output_args(republish, #{topic := Topic, qos := QoS, retain := Retain, - payload := Payload} = Args) -> - Args#{preprocessed_tmpl => #{ +pre_process_output_args( + republish, + #{ + topic := Topic, + qos := QoS, + retain := Retain, + payload := Payload + } = Args +) -> + Args#{ + preprocessed_tmpl => #{ topic => emqx_plugin_libs_rule:preproc_tmpl(Topic), qos => preproc_vars(QoS), retain => preproc_vars(Retain), payload => emqx_plugin_libs_rule:preproc_tmpl(Payload) - }}; + } + }; pre_process_output_args(_, Args) -> Args. @@ -66,35 +76,55 @@ pre_process_output_args(_, Args) -> %%-------------------------------------------------------------------- -spec console(map(), map(), map()) -> any(). console(Selected, #{metadata := #{rule_id := RuleId}} = Envs, _Args) -> - ?ULOG("[rule output] ~ts~n" - "\tOutput Data: ~p~n" - "\tEnvs: ~p~n", [RuleId, Selected, Envs]). + ?ULOG( + "[rule output] ~ts~n" + "\tOutput Data: ~p~n" + "\tEnvs: ~p~n", + [RuleId, Selected, Envs] + ). -republish(_Selected, #{topic := Topic, headers := #{republish_by := RuleId}, - metadata := #{rule_id := RuleId}}, _Args) -> +republish( + _Selected, + #{ + topic := Topic, + headers := #{republish_by := RuleId}, + metadata := #{rule_id := RuleId} + }, + _Args +) -> ?SLOG(error, #{msg => "recursive_republish_detected", topic => Topic}); - %% republish a PUBLISH message -republish(Selected, #{flags := Flags, metadata := #{rule_id := RuleId}}, - #{preprocessed_tmpl := #{ +republish( + Selected, + #{flags := Flags, metadata := #{rule_id := RuleId}}, + #{ + preprocessed_tmpl := #{ qos := QoSTks, retain := RetainTks, topic := TopicTks, - payload := PayloadTks}}) -> + payload := PayloadTks + } + } +) -> Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected), Payload = format_msg(PayloadTks, Selected), QoS = replace_simple_var(QoSTks, Selected, 0), Retain = replace_simple_var(RetainTks, Selected, false), ?TRACE("RULE", "republish_message", #{topic => Topic, payload => Payload}), safe_publish(RuleId, Topic, QoS, Flags#{retain => Retain}, Payload); - %% in case this is a "$events/" event -republish(Selected, #{metadata := #{rule_id := RuleId}}, - #{preprocessed_tmpl := #{ - qos := QoSTks, - retain := RetainTks, - topic := TopicTks, - payload := PayloadTks}}) -> +republish( + Selected, + #{metadata := #{rule_id := RuleId}}, + #{ + preprocessed_tmpl := #{ + qos := QoSTks, + retain := RetainTks, + topic := TopicTks, + payload := PayloadTks + } + } +) -> Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected), Payload = format_msg(PayloadTks, Selected), QoS = replace_simple_var(QoSTks, Selected, 0), @@ -114,8 +144,10 @@ get_output_mod_func(OutputFunc) when is_atom(OutputFunc) -> {emqx_rule_outputs, OutputFunc}; get_output_mod_func(OutputFunc) when is_binary(OutputFunc) -> ToAtom = fun(Bin) -> - try binary_to_existing_atom(Bin) of Atom -> Atom - catch error:badarg -> error({unknown_output_function, OutputFunc}) + try binary_to_existing_atom(Bin) of + Atom -> Atom + catch + error:badarg -> error({unknown_output_function, OutputFunc}) end end, case string:split(OutputFunc, ":", all) of @@ -158,7 +190,8 @@ preproc_vars(Data) -> replace_simple_var(Tokens, Data, Default) when is_list(Tokens) -> [Var] = emqx_plugin_libs_rule:proc_tmpl(Tokens, Data, #{return => rawlist}), case Var of - undefined -> Default; %% cannot find the variable from Data + %% cannot find the variable from Data + undefined -> Default; _ -> Var end; replace_simple_var(Val, _Data, _Default) -> diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl index 8211426ab..81cabd40b 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl @@ -20,32 +20,37 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ apply_rule/2 - , apply_rules/2 - , clear_rule_payload/0 - ]). +-export([ + apply_rule/2, + apply_rules/2, + clear_rule_payload/0 +]). --import(emqx_rule_maps, - [ nested_get/2 - , range_gen/2 - , range_get/3 - ]). +-import( + emqx_rule_maps, + [ + nested_get/2, + range_gen/2, + range_get/3 + ] +). --compile({no_auto_import,[alias/1]}). +-compile({no_auto_import, [alias/1]}). -type input() :: map(). -type alias() :: atom(). -type collection() :: {alias(), [term()]}. -define(ephemeral_alias(TYPE, NAME), - iolist_to_binary(io_lib:format("_v_~ts_~p_~p", [TYPE, NAME, erlang:system_time()]))). + iolist_to_binary(io_lib:format("_v_~ts_~p_~p", [TYPE, NAME, erlang:system_time()])) +). -define(ActionMaxRetry, 3). %%------------------------------------------------------------------------------ %% Apply rules %%------------------------------------------------------------------------------ --spec(apply_rules(list(rule()), input()) -> ok). +-spec apply_rules(list(rule()), input()) -> ok. apply_rules([], _Input) -> ok; apply_rules([#{enable := false} | More], Input) -> @@ -61,54 +66,77 @@ apply_rule_discard_result(Rule, Input) -> apply_rule(Rule = #{id := RuleID}, Input) -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.matched'), clear_rule_payload(), - try do_apply_rule(Rule, add_metadata(Input, #{rule_id => RuleID})) + try + do_apply_rule(Rule, add_metadata(Input, #{rule_id => RuleID})) catch %% ignore the errors if select or match failed _:Reason = {select_and_transform_error, Error} -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'), - ?SLOG(warning, #{msg => "SELECT_clause_exception", - rule_id => RuleID, reason => Error}), + ?SLOG(warning, #{ + msg => "SELECT_clause_exception", + rule_id => RuleID, + reason => Error + }), {error, Reason}; _:Reason = {match_conditions_error, Error} -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'), - ?SLOG(warning, #{msg => "WHERE_clause_exception", - rule_id => RuleID, reason => Error}), + ?SLOG(warning, #{ + msg => "WHERE_clause_exception", + rule_id => RuleID, + reason => Error + }), {error, Reason}; _:Reason = {select_and_collect_error, Error} -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'), - ?SLOG(warning, #{msg => "FOREACH_clause_exception", - rule_id => RuleID, reason => Error}), + ?SLOG(warning, #{ + msg => "FOREACH_clause_exception", + rule_id => RuleID, + reason => Error + }), {error, Reason}; _:Reason = {match_incase_error, Error} -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'), - ?SLOG(warning, #{msg => "INCASE_clause_exception", - rule_id => RuleID, reason => Error}), + ?SLOG(warning, #{ + msg => "INCASE_clause_exception", + rule_id => RuleID, + reason => Error + }), {error, Reason}; Class:Error:StkTrace -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'), - ?SLOG(error, #{msg => "apply_rule_failed", - rule_id => RuleID, - exception => Class, - reason => Error, - stacktrace => StkTrace - }), + ?SLOG(error, #{ + msg => "apply_rule_failed", + rule_id => RuleID, + exception => Class, + reason => Error, + stacktrace => StkTrace + }), {error, {Error, StkTrace}} end. -do_apply_rule(#{ - id := RuleId, - is_foreach := true, - fields := Fields, - doeach := DoEach, - incase := InCase, - conditions := Conditions, - outputs := Outputs - }, Input) -> - {Selected, Collection} = ?RAISE(select_and_collect(Fields, Input), - {select_and_collect_error, {_EXCLASS_,_EXCPTION_,_ST_}}), +do_apply_rule( + #{ + id := RuleId, + is_foreach := true, + fields := Fields, + doeach := DoEach, + incase := InCase, + conditions := Conditions, + outputs := Outputs + }, + Input +) -> + {Selected, Collection} = ?RAISE( + select_and_collect(Fields, Input), + {select_and_collect_error, {_EXCLASS_, _EXCPTION_, _ST_}} + ), ColumnsAndSelected = maps:merge(Input, Selected), - case ?RAISE(match_conditions(Conditions, ColumnsAndSelected), - {match_conditions_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of + case + ?RAISE( + match_conditions(Conditions, ColumnsAndSelected), + {match_conditions_error, {_EXCLASS_, _EXCPTION_, _ST_}} + ) + of true -> Collection2 = filter_collection(Input, InCase, DoEach, Collection), case Collection2 of @@ -122,17 +150,26 @@ do_apply_rule(#{ ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.failed.no_result'), {error, nomatch} end; - -do_apply_rule(#{id := RuleId, - is_foreach := false, - fields := Fields, - conditions := Conditions, - outputs := Outputs - }, Input) -> - Selected = ?RAISE(select_and_transform(Fields, Input), - {select_and_transform_error, {_EXCLASS_,_EXCPTION_,_ST_}}), - case ?RAISE(match_conditions(Conditions, maps:merge(Input, Selected)), - {match_conditions_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of +do_apply_rule( + #{ + id := RuleId, + is_foreach := false, + fields := Fields, + conditions := Conditions, + outputs := Outputs + }, + Input +) -> + Selected = ?RAISE( + select_and_transform(Fields, Input), + {select_and_transform_error, {_EXCLASS_, _EXCPTION_, _ST_}} + ), + case + ?RAISE( + match_conditions(Conditions, maps:merge(Input, Selected)), + {match_conditions_error, {_EXCLASS_, _EXCPTION_, _ST_}} + ) + of true -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.passed'), {ok, handle_output_list(RuleId, Outputs, Selected, Input)}; @@ -154,15 +191,19 @@ select_and_transform(['*' | More], Input, Output) -> select_and_transform(More, Input, maps:merge(Output, Input)); select_and_transform([{as, Field, Alias} | More], Input, Output) -> Val = eval(Field, Input), - select_and_transform(More, + select_and_transform( + More, nested_put(Alias, Val, Input), - nested_put(Alias, Val, Output)); + nested_put(Alias, Val, Output) + ); select_and_transform([Field | More], Input, Output) -> Val = eval(Field, Input), Key = alias(Field), - select_and_transform(More, + select_and_transform( + More, nested_put(Key, Val, Input), - nested_put(Key, Val, Output)). + nested_put(Key, Val, Output) + ). %% FOREACH Clause -spec select_and_collect(list(), input()) -> {input(), collection()}. @@ -174,9 +215,11 @@ select_and_collect([{as, Field, {_, A} = Alias}], Input, {Output, _}) -> {nested_put(Alias, Val, Output), {A, ensure_list(Val)}}; select_and_collect([{as, Field, Alias} | More], Input, {Output, LastKV}) -> Val = eval(Field, Input), - select_and_collect(More, + select_and_collect( + More, nested_put(Alias, Val, Input), - {nested_put(Alias, Val, Output), LastKV}); + {nested_put(Alias, Val, Output), LastKV} + ); select_and_collect([Field], Input, {Output, _}) -> Val = eval(Field, Input), Key = alias(Field), @@ -184,24 +227,36 @@ select_and_collect([Field], Input, {Output, _}) -> select_and_collect([Field | More], Input, {Output, LastKV}) -> Val = eval(Field, Input), Key = alias(Field), - select_and_collect(More, + select_and_collect( + More, nested_put(Key, Val, Input), - {nested_put(Key, Val, Output), LastKV}). + {nested_put(Key, Val, Output), LastKV} + ). %% Filter each item got from FOREACH filter_collection(Input, InCase, DoEach, {CollKey, CollVal}) -> lists:filtermap( fun(Item) -> InputAndItem = maps:merge(Input, #{CollKey => Item}), - case ?RAISE(match_conditions(InCase, InputAndItem), - {match_incase_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of + case + ?RAISE( + match_conditions(InCase, InputAndItem), + {match_incase_error, {_EXCLASS_, _EXCPTION_, _ST_}} + ) + of true when DoEach == [] -> {true, InputAndItem}; true -> - {true, ?RAISE(select_and_transform(DoEach, InputAndItem), - {doeach_error, {_EXCLASS_,_EXCPTION_,_ST_}})}; - false -> false + {true, + ?RAISE( + select_and_transform(DoEach, InputAndItem), + {doeach_error, {_EXCLASS_, _EXCPTION_, _ST_}} + )}; + false -> + false end - end, CollVal). + end, + CollVal + ). %% Conditional Clauses such as WHERE, WHEN. match_conditions({'and', L, R}, Data) -> @@ -212,7 +267,8 @@ match_conditions({'not', Var}, Data) -> case eval(Var, Data) of Bool when is_boolean(Bool) -> not Bool; - _other -> false + _other -> + false end; match_conditions({in, Var, {list, Vals}}, Data) -> lists:member(eval(Var, Data), [eval(V, Data) || V <- Vals]); @@ -250,8 +306,10 @@ do_compare('!=', L, R) -> L /= R; do_compare('=~', T, F) -> emqx_topic:match(T, F). number(Bin) -> - try binary_to_integer(Bin) - catch error:badarg -> binary_to_float(Bin) + try + binary_to_integer(Bin) + catch + error:badarg -> binary_to_float(Bin) end. handle_output_list(RuleId, Outputs, Selected, Envs) -> @@ -266,13 +324,20 @@ handle_output(RuleId, OutId, Selected, Envs) -> catch throw:out_of_service -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed'), - ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed.out_of_service'), + ok = emqx_plugin_libs_metrics:inc( + rule_metrics, RuleId, 'outputs.failed.out_of_service' + ), ?SLOG(warning, #{msg => "out_of_service", output => OutId}); Err:Reason:ST -> ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed'), ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed.unknown'), - ?SLOG(error, #{msg => "output_failed", output => OutId, exception => Err, - reason => Reason, stacktrace => ST}) + ?SLOG(error, #{ + msg => "output_failed", + output => OutId, + exception => Err, + reason => Reason, + stacktrace => ST + }) end. do_handle_output(BridgeId, Selected, _Envs) when is_binary(BridgeId) -> @@ -280,7 +345,8 @@ do_handle_output(BridgeId, Selected, _Envs) when is_binary(BridgeId) -> case emqx_bridge:send_message(BridgeId, Selected) of {error, {Err, _}} when Err == bridge_not_found; Err == bridge_stopped -> throw(out_of_service); - Result -> Result + Result -> + Result end; do_handle_output(#{mod := Mod, func := Func, args := Args}, Selected, Envs) -> %% the function can also throw 'out_of_service' @@ -382,8 +448,10 @@ apply_func(Name, Args, Input) when is_atom(Name) -> do_apply_func(Name, Args, Input); apply_func(Name, Args, Input) when is_binary(Name) -> FunName = - try binary_to_existing_atom(Name, utf8) - catch error:badarg -> error({sql_function_not_supported, Name}) + try + binary_to_existing_atom(Name, utf8) + catch + error:badarg -> error({sql_function_not_supported, Name}) end, do_apply_func(FunName, Args, Input). @@ -391,7 +459,8 @@ do_apply_func(Name, Args, Input) -> case erlang:apply(emqx_rule_funcs, Name, Args) of Func when is_function(Func) -> erlang:apply(Func, [Input]); - Result -> Result + Result -> + Result end. add_metadata(Input, Metadata) when is_map(Input), is_map(Metadata) -> @@ -417,8 +486,10 @@ cache_payload(DecodedP) -> DecodedP. safe_decode_and_cache(MaybeJson) -> - try cache_payload(emqx_json:decode(MaybeJson, [return_maps])) - catch _:_ -> error({decode_json_failed, MaybeJson}) + try + cache_payload(emqx_json:decode(MaybeJson, [return_maps])) + catch + _:_ -> error({decode_json_failed, MaybeJson}) end. ensure_list(List) when is_list(List) -> List; diff --git a/apps/emqx_rule_engine/src/emqx_rule_sqlparser.erl b/apps/emqx_rule_engine/src/emqx_rule_sqlparser.erl index 973614a62..1435d2e60 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_sqlparser.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_sqlparser.erl @@ -20,84 +20,89 @@ -export([parse/1]). --export([ select_fields/1 - , select_is_foreach/1 - , select_doeach/1 - , select_incase/1 - , select_from/1 - , select_where/1 - ]). +-export([ + select_fields/1, + select_is_foreach/1, + select_doeach/1, + select_incase/1, + select_from/1, + select_where/1 +]). --import(proplists, [ get_value/2 - , get_value/3 - ]). +-import(proplists, [ + get_value/2, + get_value/3 +]). -record(select, {fields, from, where, is_foreach, doeach, incase}). --opaque(select() :: #select{}). +-opaque select() :: #select{}. --type const() :: {const, number()|binary()}. +-type const() :: {const, number() | binary()}. -type variable() :: binary() | list(binary()). -type alias() :: binary() | list(binary()). --type field() :: const() | variable() - | {as, field(), alias()} - | {'fun', atom(), list(field())}. +-type field() :: + const() + | variable() + | {as, field(), alias()} + | {'fun', atom(), list(field())}. -export_type([select/0]). %% Parse one select statement. --spec(parse(string() | binary()) -> {ok, select()} | {error, term()}). +-spec parse(string() | binary()) -> {ok, select()} | {error, term()}. parse(Sql) -> - try case rulesql:parsetree(Sql) of + try + case rulesql:parsetree(Sql) of {ok, {select, Clauses}} -> {ok, #select{ - is_foreach = false, - fields = get_value(fields, Clauses), - doeach = [], - incase = {}, - from = get_value(from, Clauses), - where = get_value(where, Clauses) - }}; + is_foreach = false, + fields = get_value(fields, Clauses), + doeach = [], + incase = {}, + from = get_value(from, Clauses), + where = get_value(where, Clauses) + }}; {ok, {foreach, Clauses}} -> {ok, #select{ - is_foreach = true, - fields = get_value(fields, Clauses), - doeach = get_value(do, Clauses, []), - incase = get_value(incase, Clauses, {}), - from = get_value(from, Clauses), - where = get_value(where, Clauses) - }}; - Error -> {error, Error} + is_foreach = true, + fields = get_value(fields, Clauses), + doeach = get_value(do, Clauses, []), + incase = get_value(incase, Clauses, {}), + from = get_value(from, Clauses), + where = get_value(where, Clauses) + }}; + Error -> + {error, Error} end catch _Error:Reason:StackTrace -> {error, {Reason, StackTrace}} end. --spec(select_fields(select()) -> list(field())). +-spec select_fields(select()) -> list(field()). select_fields(#select{fields = Fields}) -> Fields. --spec(select_is_foreach(select()) -> boolean()). +-spec select_is_foreach(select()) -> boolean(). select_is_foreach(#select{is_foreach = IsForeach}) -> IsForeach. --spec(select_doeach(select()) -> list(field())). +-spec select_doeach(select()) -> list(field()). select_doeach(#select{doeach = DoEach}) -> DoEach. --spec(select_incase(select()) -> list(field())). +-spec select_incase(select()) -> list(field()). select_incase(#select{incase = InCase}) -> InCase. --spec(select_from(select()) -> list(binary())). +-spec select_from(select()) -> list(binary()). select_from(#select{from = From}) -> From. --spec(select_where(select()) -> tuple()). +-spec select_where(select()) -> tuple(). select_where(#select{where = Where}) -> Where. - diff --git a/apps/emqx_rule_engine/src/emqx_rule_sqltester.erl b/apps/emqx_rule_engine/src/emqx_rule_sqltester.erl index a791863f6..cc545a96e 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_sqltester.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_sqltester.erl @@ -17,10 +17,11 @@ -include("rule_engine.hrl"). -include_lib("emqx/include/logger.hrl"). --export([ test/1 - , echo_action/2 - , get_selected_data/3 - ]). +-export([ + test/1, + echo_action/2, + get_selected_data/3 +]). -spec test(#{sql := binary(), context := map()}) -> {ok, map() | list()} | {error, term()}. test(#{sql := Sql, context := Context}) -> @@ -60,9 +61,7 @@ test_rule(Sql, Select, Context, EventTopics) -> created_at => erlang:system_time(millisecond) }, FullContext = fill_default_values(hd(EventTopics), emqx_rule_maps:atom_key_map(Context)), - try - emqx_rule_runtime:apply_rule(Rule, FullContext) - of + try emqx_rule_runtime:apply_rule(Rule, FullContext) of {ok, Data} -> {ok, flatten(Data)}; {error, Reason} -> {error, Reason} after @@ -76,8 +75,10 @@ is_publish_topic(<<"$events/", _/binary>>) -> false; is_publish_topic(<<"$bridges/", _/binary>>) -> false; is_publish_topic(_Topic) -> true. -flatten([]) -> []; -flatten([D1]) -> D1; +flatten([]) -> + []; +flatten([D1]) -> + D1; flatten([D1 | L]) when is_list(D1) -> D1 ++ flatten(L). @@ -92,4 +93,6 @@ envs_examp(EventTopic) -> EventName = emqx_rule_events:event_name(EventTopic), emqx_rule_maps:atom_key_map( maps:from_list( - emqx_rule_events:columns_with_exam(EventName))). + emqx_rule_events:columns_with_exam(EventName) + ) + ). diff --git a/apps/emqx_rule_engine/src/proto/emqx_rule_engine_proto_v1.erl b/apps/emqx_rule_engine/src/proto/emqx_rule_engine_proto_v1.erl index cc3a79a74..501a1d05c 100644 --- a/apps/emqx_rule_engine/src/proto/emqx_rule_engine_proto_v1.erl +++ b/apps/emqx_rule_engine/src/proto/emqx_rule_engine_proto_v1.erl @@ -18,10 +18,11 @@ -behaviour(emqx_bpapi). --export([ introduced_in/0 +-export([ + introduced_in/0, - , reset_metrics/1 - ]). + reset_metrics/1 +]). -include_lib("emqx/include/bpapi.hrl"). -include_lib("emqx_rule_engine/include/rule_engine.hrl"). @@ -30,6 +31,6 @@ introduced_in() -> "5.0.0". -spec reset_metrics(rule_id()) -> - emqx_cluster_rpc:multicall_return(ok). + emqx_cluster_rpc:multicall_return(ok). reset_metrics(RuleId) -> emqx_cluster_rpc:multicall(emqx_rule_engine, reset_metrics_for_rule, [RuleId]). diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl index 0abd47a59..af19a24f1 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_SUITE.erl @@ -29,75 +29,71 @@ -define(TMP_RULEID, atom_to_binary(?FUNCTION_NAME)). all() -> - [ {group, engine} - , {group, funcs} - , {group, registry} - , {group, runtime} - , {group, events} - , {group, bugs} + [ + {group, engine}, + {group, funcs}, + {group, registry}, + {group, runtime}, + {group, events}, + {group, bugs} ]. suite() -> [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. groups() -> - [{engine, [sequence], - [t_create_rule - ]}, - {funcs, [], - [t_kv_store - ]}, - {registry, [sequence], - [t_add_get_remove_rule, - t_add_get_remove_rules, - t_create_existing_rule, - t_get_rules_for_topic, - t_get_rules_for_topic_2, - t_get_rules_with_same_event - ]}, - {runtime, [], - [t_match_atom_and_binary, - t_sqlselect_0, - t_sqlselect_00, - t_sqlselect_01, - t_sqlselect_02, - t_sqlselect_1, - t_sqlselect_2, - t_sqlselect_3, - t_sqlparse_event_1, - t_sqlparse_event_2, - t_sqlparse_event_3, - t_sqlparse_foreach_1, - t_sqlparse_foreach_2, - t_sqlparse_foreach_3, - t_sqlparse_foreach_4, - t_sqlparse_foreach_5, - t_sqlparse_foreach_6, - t_sqlparse_foreach_7, - t_sqlparse_foreach_8, - t_sqlparse_case_when_1, - t_sqlparse_case_when_2, - t_sqlparse_case_when_3, - t_sqlparse_array_index_1, - t_sqlparse_array_index_2, - t_sqlparse_array_index_3, - t_sqlparse_array_index_4, - t_sqlparse_array_index_5, - t_sqlparse_select_matadata_1, - t_sqlparse_array_range_1, - t_sqlparse_array_range_2, - t_sqlparse_true_false, - t_sqlparse_undefined_variable, - t_sqlparse_new_map, - t_sqlparse_invalid_json - ]}, - {events, [], - [t_events - ]}, - {bugs, [], - [t_sqlparse_payload_as, - t_sqlparse_nested_get - ]} + [ + {engine, [sequence], [t_create_rule]}, + {funcs, [], [t_kv_store]}, + {registry, [sequence], [ + t_add_get_remove_rule, + t_add_get_remove_rules, + t_create_existing_rule, + t_get_rules_for_topic, + t_get_rules_for_topic_2, + t_get_rules_with_same_event + ]}, + {runtime, [], [ + t_match_atom_and_binary, + t_sqlselect_0, + t_sqlselect_00, + t_sqlselect_01, + t_sqlselect_02, + t_sqlselect_1, + t_sqlselect_2, + t_sqlselect_3, + t_sqlparse_event_1, + t_sqlparse_event_2, + t_sqlparse_event_3, + t_sqlparse_foreach_1, + t_sqlparse_foreach_2, + t_sqlparse_foreach_3, + t_sqlparse_foreach_4, + t_sqlparse_foreach_5, + t_sqlparse_foreach_6, + t_sqlparse_foreach_7, + t_sqlparse_foreach_8, + t_sqlparse_case_when_1, + t_sqlparse_case_when_2, + t_sqlparse_case_when_3, + t_sqlparse_array_index_1, + t_sqlparse_array_index_2, + t_sqlparse_array_index_3, + t_sqlparse_array_index_4, + t_sqlparse_array_index_5, + t_sqlparse_select_matadata_1, + t_sqlparse_array_range_1, + t_sqlparse_array_range_2, + t_sqlparse_true_false, + t_sqlparse_undefined_variable, + t_sqlparse_new_map, + t_sqlparse_invalid_json + ]}, + {events, [], [t_events]}, + {bugs, [], [ + t_sqlparse_payload_as, + t_sqlparse_nested_get + ]} ]. %%------------------------------------------------------------------------------ @@ -107,8 +103,9 @@ groups() -> init_per_suite(Config) -> application:load(emqx_conf), ok = emqx_common_test_helpers:start_apps( - [emqx_conf, emqx_rule_engine, emqx_authz], - fun set_special_configs/1), + [emqx_conf, emqx_rule_engine, emqx_authz], + fun set_special_configs/1 + ), Config. end_per_suite(_Config) -> @@ -117,10 +114,13 @@ end_per_suite(_Config) -> set_special_configs(emqx_authz) -> {ok, _} = emqx:update_config( - [authorization], - #{<<"no_match">> => atom_to_binary(allow), - <<"cache">> => #{<<"enable">> => atom_to_binary(true)}, - <<"sources">> => []}), + [authorization], + #{ + <<"no_match">> => atom_to_binary(allow), + <<"cache">> => #{<<"enable">> => atom_to_binary(true)}, + <<"sources">> => [] + } + ), ok; set_special_configs(_) -> ok. @@ -149,25 +149,31 @@ end_per_group(_Groupname, _Config) -> init_per_testcase(t_events, Config) -> init_events_counters(), - SQL = "SELECT * FROM \"$events/client_connected\", " - "\"$events/client_disconnected\", " - "\"$events/client_connack\", " - "\"$events/client_check_authz_complete\", " - "\"$events/session_subscribed\", " - "\"$events/session_unsubscribed\", " - "\"$events/message_acked\", " - "\"$events/message_delivered\", " - "\"$events/message_dropped\", " - "\"$events/delivery_dropped\", " - "\"t1\"", + SQL = + "SELECT * FROM \"$events/client_connected\", " + "\"$events/client_disconnected\", " + "\"$events/client_connack\", " + "\"$events/client_check_authz_complete\", " + "\"$events/session_subscribed\", " + "\"$events/session_unsubscribed\", " + "\"$events/message_acked\", " + "\"$events/message_delivered\", " + "\"$events/message_dropped\", " + "\"$events/delivery_dropped\", " + "\"t1\"", {ok, Rule} = emqx_rule_engine:create_rule( - #{id => <<"rule:t_events">>, + #{ + id => <<"rule:t_events">>, sql => SQL, outputs => [ - #{function => <<"emqx_rule_engine_SUITE:output_record_triggered_events">>, - args => #{}} + #{ + function => <<"emqx_rule_engine_SUITE:output_record_triggered_events">>, + args => #{} + } ], - description => <<"to console and record triggered events">>}), + description => <<"to console and record triggered events">> + } + ), ?assertMatch(#{id := <<"rule:t_events">>}, Rule), [{hook_points_rules, Rule} | Config]; init_per_testcase(_TestCase, Config) -> @@ -184,13 +190,18 @@ end_per_testcase(_TestCase, _Config) -> %%------------------------------------------------------------------------------ t_create_rule(_Config) -> {ok, #{id := Id}} = emqx_rule_engine:create_rule( - #{sql => <<"select * from \"t/a\"">>, - id => <<"t_create_rule">>, - outputs => [#{function => console}], - description => <<"debug rule">>}), + #{ + sql => <<"select * from \"t/a\"">>, + id => <<"t_create_rule">>, + outputs => [#{function => console}], + description => <<"debug rule">> + } + ), ct:pal("======== emqx_rule_engine:get_rules :~p", [emqx_rule_engine:get_rules()]), - ?assertMatch({ok, #{id := Id, from := [<<"t/a">>]}}, - emqx_rule_engine:get_rule(Id)), + ?assertMatch( + {ok, #{id := Id, from := [<<"t/a">>]}}, + emqx_rule_engine:get_rule(Id) + ), delete_rule(Id), ok. @@ -228,8 +239,11 @@ t_add_get_remove_rule(_Config) -> t_add_get_remove_rules(_Config) -> delete_rules_by_ids(emqx_rule_engine:get_rules()), ok = insert_rules( - [make_simple_rule(<<"rule-debug-1">>), - make_simple_rule(<<"rule-debug-2">>)]), + [ + make_simple_rule(<<"rule-debug-1">>), + make_simple_rule(<<"rule-debug-2">>) + ] + ), ?assertEqual(2, length(emqx_rule_engine:get_rules())), ok = delete_rules_by_ids([<<"rule-debug-1">>, <<"rule-debug-2">>]), ?assertEqual([], emqx_rule_engine:get_rules()), @@ -238,10 +252,12 @@ t_add_get_remove_rules(_Config) -> t_create_existing_rule(_Config) -> %% create a rule using given rule id {ok, _} = emqx_rule_engine:create_rule( - #{id => <<"an_existing_rule">>, - sql => <<"select * from \"t/#\"">>, - outputs => [#{function => console}] - }), + #{ + id => <<"an_existing_rule">>, + sql => <<"select * from \"t/#\"">>, + outputs => [#{function => console}] + } + ), {ok, #{sql := SQL}} = emqx_rule_engine:get_rule(<<"an_existing_rule">>), ?assertEqual(<<"select * from \"t/#\"">>, SQL), @@ -252,40 +268,64 @@ t_create_existing_rule(_Config) -> t_get_rules_for_topic(_Config) -> Len0 = length(emqx_rule_engine:get_rules_for_topic(<<"simple/topic">>)), ok = insert_rules( - [make_simple_rule(<<"rule-debug-1">>), - make_simple_rule(<<"rule-debug-2">>)]), - ?assertEqual(Len0+2, length(emqx_rule_engine:get_rules_for_topic(<<"simple/topic">>))), + [ + make_simple_rule(<<"rule-debug-1">>), + make_simple_rule(<<"rule-debug-2">>) + ] + ), + ?assertEqual(Len0 + 2, length(emqx_rule_engine:get_rules_for_topic(<<"simple/topic">>))), ok = delete_rules_by_ids([<<"rule-debug-1">>, <<"rule-debug-2">>]), ok. t_get_rules_ordered_by_ts(_Config) -> Now = fun() -> erlang:system_time(nanosecond) end, ok = insert_rules( - [make_simple_rule_with_ts(<<"rule-debug-0">>, Now()), - make_simple_rule_with_ts(<<"rule-debug-1">>, Now()), - make_simple_rule_with_ts(<<"rule-debug-2">>, Now()) - ]), - ?assertMatch([ - #{id := <<"rule-debug-0">>}, - #{id := <<"rule-debug-1">>}, - #{id := <<"rule-debug-2">>} - ], emqx_rule_engine:get_rules_ordered_by_ts()). + [ + make_simple_rule_with_ts(<<"rule-debug-0">>, Now()), + make_simple_rule_with_ts(<<"rule-debug-1">>, Now()), + make_simple_rule_with_ts(<<"rule-debug-2">>, Now()) + ] + ), + ?assertMatch( + [ + #{id := <<"rule-debug-0">>}, + #{id := <<"rule-debug-1">>}, + #{id := <<"rule-debug-2">>} + ], + emqx_rule_engine:get_rules_ordered_by_ts() + ). t_get_rules_for_topic_2(_Config) -> Len0 = length(emqx_rule_engine:get_rules_for_topic(<<"simple/1">>)), ok = insert_rules( - [make_simple_rule(<<"rule-debug-1">>, <<"select * from \"simple/#\"">>, [<<"simple/#">>]), - make_simple_rule(<<"rule-debug-2">>, <<"select * from \"simple/+\"">>, [<<"simple/+">>]), - make_simple_rule(<<"rule-debug-3">>, <<"select * from \"simple/+/1\"">>, [<<"simple/+/1">>]), - make_simple_rule(<<"rule-debug-4">>, <<"select * from \"simple/1\"">>, [<<"simple/1">>]), - make_simple_rule(<<"rule-debug-5">>, <<"select * from \"simple/2,simple/+,simple/3\"">>, - [<<"simple/2">>,<<"simple/+">>, <<"simple/3">>]), - make_simple_rule(<<"rule-debug-6">>, <<"select * from \"simple/2,simple/3,simple/4\"">>, - [<<"simple/2">>,<<"simple/3">>, <<"simple/4">>]) - ]), - ?assertEqual(Len0+4, length(emqx_rule_engine:get_rules_for_topic(<<"simple/1">>))), - ok = delete_rules_by_ids([<<"rule-debug-1">>, <<"rule-debug-2">>,<<"rule-debug-3">>, - <<"rule-debug-4">>,<<"rule-debug-5">>, <<"rule-debug-6">>]), + [ + make_simple_rule(<<"rule-debug-1">>, <<"select * from \"simple/#\"">>, [<<"simple/#">>]), + make_simple_rule(<<"rule-debug-2">>, <<"select * from \"simple/+\"">>, [<<"simple/+">>]), + make_simple_rule(<<"rule-debug-3">>, <<"select * from \"simple/+/1\"">>, [ + <<"simple/+/1">> + ]), + make_simple_rule(<<"rule-debug-4">>, <<"select * from \"simple/1\"">>, [<<"simple/1">>]), + make_simple_rule( + <<"rule-debug-5">>, + <<"select * from \"simple/2,simple/+,simple/3\"">>, + [<<"simple/2">>, <<"simple/+">>, <<"simple/3">>] + ), + make_simple_rule( + <<"rule-debug-6">>, + <<"select * from \"simple/2,simple/3,simple/4\"">>, + [<<"simple/2">>, <<"simple/3">>, <<"simple/4">>] + ) + ] + ), + ?assertEqual(Len0 + 4, length(emqx_rule_engine:get_rules_for_topic(<<"simple/1">>))), + ok = delete_rules_by_ids([ + <<"rule-debug-1">>, + <<"rule-debug-2">>, + <<"rule-debug-3">>, + <<"rule-debug-4">>, + <<"rule-debug-5">>, + <<"rule-debug-6">> + ]), ok. t_get_rules_with_same_event(_Config) -> @@ -294,41 +334,95 @@ t_get_rules_with_same_event(_Config) -> ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/client_connected">>)), ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/client_disconnected">>)), ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/session_subscribed">>)), - ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/session_unsubscribed">>)), + ?assertEqual( + [], emqx_rule_engine:get_rules_with_same_event(<<"$events/session_unsubscribed">>) + ), ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/message_delivered">>)), ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/message_acked">>)), ?assertEqual([], emqx_rule_engine:get_rules_with_same_event(<<"$events/message_dropped">>)), ok = insert_rules( - [make_simple_rule(<<"r1">>, <<"select * from \"simple/#\"">>, [<<"simple/#">>]), - make_simple_rule(<<"r2">>, <<"select * from \"abc/+\"">>, [<<"abc/+">>]), - make_simple_rule(<<"r3">>, <<"select * from \"$events/client_connected\"">>, - [<<"$events/client_connected">>]), - make_simple_rule(<<"r4">>, <<"select * from \"$events/client_disconnected\"">>, - [<<"$events/client_disconnected">>]), - make_simple_rule(<<"r5">>, <<"select * from \"$events/session_subscribed\"">>, - [<<"$events/session_subscribed">>]), - make_simple_rule(<<"r6">>, <<"select * from \"$events/session_unsubscribed\"">>, - [<<"$events/session_unsubscribed">>]), - make_simple_rule(<<"r7">>, <<"select * from \"$events/message_delivered\"">>, - [<<"$events/message_delivered">>]), - make_simple_rule(<<"r8">>, <<"select * from \"$events/message_acked\"">>, - [<<"$events/message_acked">>]), - make_simple_rule(<<"r9">>, <<"select * from \"$events/message_dropped\"">>, - [<<"$events/message_dropped">>]), - make_simple_rule(<<"r10">>, <<"select * from \"t/1, " - "$events/session_subscribed, $events/client_connected\"">>, - [<<"t/1">>, <<"$events/session_subscribed">>, <<"$events/client_connected">>]) - ]), + [ + make_simple_rule(<<"r1">>, <<"select * from \"simple/#\"">>, [<<"simple/#">>]), + make_simple_rule(<<"r2">>, <<"select * from \"abc/+\"">>, [<<"abc/+">>]), + make_simple_rule( + <<"r3">>, + <<"select * from \"$events/client_connected\"">>, + [<<"$events/client_connected">>] + ), + make_simple_rule( + <<"r4">>, + <<"select * from \"$events/client_disconnected\"">>, + [<<"$events/client_disconnected">>] + ), + make_simple_rule( + <<"r5">>, + <<"select * from \"$events/session_subscribed\"">>, + [<<"$events/session_subscribed">>] + ), + make_simple_rule( + <<"r6">>, + <<"select * from \"$events/session_unsubscribed\"">>, + [<<"$events/session_unsubscribed">>] + ), + make_simple_rule( + <<"r7">>, + <<"select * from \"$events/message_delivered\"">>, + [<<"$events/message_delivered">>] + ), + make_simple_rule( + <<"r8">>, + <<"select * from \"$events/message_acked\"">>, + [<<"$events/message_acked">>] + ), + make_simple_rule( + <<"r9">>, + <<"select * from \"$events/message_dropped\"">>, + [<<"$events/message_dropped">>] + ), + make_simple_rule( + <<"r10">>, + << + "select * from \"t/1, " + "$events/session_subscribed, $events/client_connected\"" + >>, + [<<"t/1">>, <<"$events/session_subscribed">>, <<"$events/client_connected">>] + ) + ] + ), ?assertEqual(PubN + 3, length(emqx_rule_engine:get_rules_with_same_event(PubT))), - ?assertEqual(2, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/client_connected">>))), - ?assertEqual(1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/client_disconnected">>))), - ?assertEqual(2, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/session_subscribed">>))), - ?assertEqual(1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/session_unsubscribed">>))), - ?assertEqual(1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_delivered">>))), - ?assertEqual(1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_acked">>))), - ?assertEqual(1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_dropped">>))), - ok = delete_rules_by_ids([<<"r1">>, <<"r2">>,<<"r3">>, <<"r4">>, - <<"r5">>, <<"r6">>, <<"r7">>, <<"r8">>, <<"r9">>, <<"r10">>]), + ?assertEqual( + 2, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/client_connected">>)) + ), + ?assertEqual( + 1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/client_disconnected">>)) + ), + ?assertEqual( + 2, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/session_subscribed">>)) + ), + ?assertEqual( + 1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/session_unsubscribed">>)) + ), + ?assertEqual( + 1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_delivered">>)) + ), + ?assertEqual( + 1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_acked">>)) + ), + ?assertEqual( + 1, length(emqx_rule_engine:get_rules_with_same_event(<<"$events/message_dropped">>)) + ), + ok = delete_rules_by_ids([ + <<"r1">>, + <<"r2">>, + <<"r3">>, + <<"r4">>, + <<"r5">>, + <<"r6">>, + <<"r7">>, + <<"r8">>, + <<"r9">>, + <<"r10">> + ]), ok. %%------------------------------------------------------------------------------ @@ -337,17 +431,21 @@ t_get_rules_with_same_event(_Config) -> t_events(_Config) -> {ok, Client} = emqtt:start_link( - [ {username, <<"u_event">>} - , {clientid, <<"c_event">>} - , {proto_ver, v5} - , {properties, #{'Session-Expiry-Interval' => 60}} - ]), + [ + {username, <<"u_event">>}, + {clientid, <<"c_event">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 60}} + ] + ), {ok, Client2} = emqtt:start_link( - [ {username, <<"u_event2">>} - , {clientid, <<"c_event2">>} - , {proto_ver, v5} - , {properties, #{'Session-Expiry-Interval' => 60}} - ]), + [ + {username, <<"u_event2">>}, + {clientid, <<"c_event2">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 60}} + ] + ), ct:pal("====== verify $events/client_connected, $events/client_connack"), client_connected(Client, Client2), ct:pal("====== verify $events/message_dropped"), @@ -372,15 +470,20 @@ t_events(_Config) -> client_connack_failed() -> {ok, Client} = emqtt:start_link( - [ {username, <<"u_event3">>} - , {clientid, <<"c_event3">>} - , {proto_ver, v5} - , {properties, #{'Session-Expiry-Interval' => 60}} - ]), + [ + {username, <<"u_event3">>}, + {clientid, <<"c_event3">>}, + {proto_ver, v5}, + {properties, #{'Session-Expiry-Interval' => 60}} + ] + ), try meck:new(emqx_access_control, [non_strict, passthrough]), - meck:expect(emqx_access_control, authenticate, - fun(_) -> {error, bad_username_or_password} end), + meck:expect( + emqx_access_control, + authenticate, + fun(_) -> {error, bad_username_or_password} end + ), process_flag(trap_exit, true), ?assertMatch({error, _}, emqtt:connect(Client)), timer:sleep(300), @@ -391,8 +494,13 @@ client_connack_failed() -> ok. message_publish(Client) -> - emqtt:publish(Client, <<"t1">>, #{'Message-Expiry-Interval' => 60}, - <<"{\"id\": 1, \"name\": \"ha\"}">>, [{qos, 1}]), + emqtt:publish( + Client, + <<"t1">>, + #{'Message-Expiry-Interval' => 60}, + <<"{\"id\": 1, \"name\": \"ha\"}">>, + [{qos, 1}] + ), verify_event('message.publish'), ok. client_connected(Client, Client2) -> @@ -407,12 +515,16 @@ client_disconnected(Client, Client2) -> verify_event('client.disconnected'), ok. session_subscribed(Client2) -> - {ok, _, _} = emqtt:subscribe(Client2, #{'User-Property' => {<<"topic_name">>, <<"t1">>}}, <<"t1">>, 1), + {ok, _, _} = emqtt:subscribe( + Client2, #{'User-Property' => {<<"topic_name">>, <<"t1">>}}, <<"t1">>, 1 + ), verify_event('session.subscribed'), verify_event('client.check_authz_complete'), ok. session_unsubscribed(Client2) -> - {ok, _, _} = emqtt:unsubscribe(Client2, #{'User-Property' => {<<"topic_name">>, <<"t1">>}}, <<"t1">>), + {ok, _, _} = emqtt:unsubscribe( + Client2, #{'User-Property' => {<<"topic_name">>, <<"t1">>}}, <<"t1">> + ), verify_event('session.unsubscribed'), ok. @@ -437,23 +549,29 @@ message_acked(_Client) -> ok. t_match_atom_and_binary(_Config) -> - SQL = "SELECT connected_at as ts, * " + SQL = + "SELECT connected_at as ts, * " "FROM \"$events/client_connected\" " "WHERE username = 'emqx2' ", Repub = republish_output(<<"t2">>, <<"user:${ts}">>), {ok, TopicRule} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{username, <<"emqx1">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), ct:sleep(100), {ok, Client2} = emqtt:start_link([{username, <<"emqx2">>}]), {ok, _} = emqtt:connect(Client2), - receive {publish, #{topic := T, payload := Payload}} -> - ?assertEqual(<<"t2">>, T), - <<"user:", ConnAt/binary>> = Payload, - _ = binary_to_integer(ConnAt) + receive + {publish, #{topic := T, payload := Payload}} -> + ?assertEqual(<<"t2">>, T), + <<"user:", ConnAt/binary>> = Payload, + _ = binary_to_integer(ConnAt) after 1000 -> ct:fail(wait_for_t2) end, @@ -463,116 +581,192 @@ t_match_atom_and_binary(_Config) -> t_sqlselect_0(_Config) -> %% Verify SELECT with and without 'AS' - Sql = "select * " - "from \"t/#\" " - "where payload.cmd.info = 'tt'", - ?assertMatch({ok,#{payload := <<"{\"cmd\": {\"info\":\"tt\"}}">>}}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => + Sql = + "select * " + "from \"t/#\" " + "where payload.cmd.info = 'tt'", + ?assertMatch( + {ok, #{payload := <<"{\"cmd\": {\"info\":\"tt\"}}">>}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"cmd\": {\"info\":\"tt\"}}">>, - topic => <<"t/a">>}})), - Sql2 = "select payload.cmd as cmd " - "from \"t/#\" " - "where cmd.info = 'tt'", - ?assertMatch({ok,#{<<"cmd">> := #{<<"info">> := <<"tt">>}}}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "select payload.cmd as cmd " + "from \"t/#\" " + "where cmd.info = 'tt'", + ?assertMatch( + {ok, #{<<"cmd">> := #{<<"info">> := <<"tt">>}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => <<"{\"cmd\": {\"info\":\"tt\"}}">>, - topic => <<"t/a">>}})), - Sql3 = "select payload.cmd as cmd, cmd.info as info " - "from \"t/#\" " - "where cmd.info = 'tt' and info = 'tt'", - ?assertMatch({ok,#{<<"cmd">> := #{<<"info">> := <<"tt">>}, - <<"info">> := <<"tt">>}}, - emqx_rule_sqltester:test( - #{sql => Sql3, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + Sql3 = + "select payload.cmd as cmd, cmd.info as info " + "from \"t/#\" " + "where cmd.info = 'tt' and info = 'tt'", + ?assertMatch( + {ok, #{ + <<"cmd">> := #{<<"info">> := <<"tt">>}, + <<"info">> := <<"tt">> + }}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => + #{ + payload => <<"{\"cmd\": {\"info\":\"tt\"}}">>, - topic => <<"t/a">>}})), + topic => <<"t/a">> + } + } + ) + ), %% cascaded as - Sql4 = "select payload.cmd as cmd, cmd.info as meta.info " - "from \"t/#\" " - "where cmd.info = 'tt' and meta.info = 'tt'", - ?assertMatch({ok,#{<<"cmd">> := #{<<"info">> := <<"tt">>}, - <<"meta">> := #{<<"info">> := <<"tt">>}}}, - emqx_rule_sqltester:test( - #{sql => Sql4, - context => - #{payload => + Sql4 = + "select payload.cmd as cmd, cmd.info as meta.info " + "from \"t/#\" " + "where cmd.info = 'tt' and meta.info = 'tt'", + ?assertMatch( + {ok, #{ + <<"cmd">> := #{<<"info">> := <<"tt">>}, + <<"meta">> := #{<<"info">> := <<"tt">>} + }}, + emqx_rule_sqltester:test( + #{ + sql => Sql4, + context => + #{ + payload => <<"{\"cmd\": {\"info\":\"tt\"}}">>, - topic => <<"t/a">>}})). + topic => <<"t/a">> + } + } + ) + ). t_sqlselect_00(_Config) -> %% Verify plus/subtract and unary_add_or_subtract - Sql = "select 1-1 as a " - "from \"t/#\" ", - ?assertMatch({ok,#{<<"a">> := 0}}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => <<"">>, - topic => <<"t/a">>}})), - Sql1 = "select -1 + 1 as a " - "from \"t/#\" ", - ?assertMatch({ok,#{<<"a">> := 0}}, - emqx_rule_sqltester:test( - #{sql => Sql1, - context => - #{payload => <<"">>, - topic => <<"t/a">>}})), - Sql2 = "select 1 + 1 as a " - "from \"t/#\" ", - ?assertMatch({ok,#{<<"a">> := 2}}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => <<"">>, - topic => <<"t/a">>}})), - Sql3 = "select +1 as a " - "from \"t/#\" ", - ?assertMatch({ok,#{<<"a">> := 1}}, - emqx_rule_sqltester:test( - #{sql => Sql3, - context => - #{payload => <<"">>, - topic => <<"t/a">>}})). + Sql = + "select 1-1 as a " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := 0}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ), + Sql1 = + "select -1 + 1 as a " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := 0}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => + #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "select 1 + 1 as a " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := 2}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ), + Sql3 = + "select +1 as a " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := 1}}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => + #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlselect_01(_Config) -> - SQL = "SELECT json_decode(payload) as p, payload " + SQL = + "SELECT json_decode(payload) as p, payload " "FROM \"t3/#\", \"t1\" " "WHERE p.x = 1", Repub = republish_output(<<"t2">>), {ok, TopicRule1} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{username, <<"emqx">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), emqtt:publish(Client, <<"t1">>, <<"{\"x\":1}">>, 0), ct:sleep(100), - receive {publish, #{topic := T, payload := Payload}} -> - ?assertEqual(<<"t2">>, T), - ?assertEqual(<<"{\"x\":1}">>, Payload) + receive + {publish, #{topic := T, payload := Payload}} -> + ?assertEqual(<<"t2">>, T), + ?assertEqual(<<"{\"x\":1}">>, Payload) after 1000 -> ct:fail(wait_for_t2) end, emqtt:publish(Client, <<"t1">>, <<"{\"x\":2}">>, 0), - receive {publish, #{topic := <<"t2">>, payload := _}} -> - ct:fail(unexpected_t2) + receive + {publish, #{topic := <<"t2">>, payload := _}} -> + ct:fail(unexpected_t2) after 1000 -> ok end, emqtt:publish(Client, <<"t3/a">>, <<"{\"x\":1}">>, 0), - receive {publish, #{topic := T3, payload := Payload3}} -> - ?assertEqual(<<"t2">>, T3), - ?assertEqual(<<"{\"x\":1}">>, Payload3) + receive + {publish, #{topic := T3, payload := Payload3}} -> + ?assertEqual(<<"t2">>, T3), + ?assertEqual(<<"{\"x\":1}">>, Payload3) after 1000 -> ct:fail(wait_for_t2) end, @@ -581,36 +775,44 @@ t_sqlselect_01(_Config) -> delete_rule(TopicRule1). t_sqlselect_02(_Config) -> - SQL = "SELECT * " + SQL = + "SELECT * " "FROM \"t3/#\", \"t1\" " "WHERE payload.x = 1", Repub = republish_output(<<"t2">>), {ok, TopicRule1} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{username, <<"emqx">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), emqtt:publish(Client, <<"t1">>, <<"{\"x\":1}">>, 0), ct:sleep(100), - receive {publish, #{topic := T, payload := Payload}} -> - ?assertEqual(<<"t2">>, T), - ?assertEqual(<<"{\"x\":1}">>, Payload) + receive + {publish, #{topic := T, payload := Payload}} -> + ?assertEqual(<<"t2">>, T), + ?assertEqual(<<"{\"x\":1}">>, Payload) after 1000 -> ct:fail(wait_for_t2) end, emqtt:publish(Client, <<"t1">>, <<"{\"x\":2}">>, 0), - receive {publish, #{topic := <<"t2">>, payload := Payload0}} -> - ct:fail({unexpected_t2, Payload0}) + receive + {publish, #{topic := <<"t2">>, payload := Payload0}} -> + ct:fail({unexpected_t2, Payload0}) after 1000 -> ok end, emqtt:publish(Client, <<"t3/a">>, <<"{\"x\":1}">>, 0), - receive {publish, #{topic := T3, payload := Payload3}} -> - ?assertEqual(<<"t2">>, T3), - ?assertEqual(<<"{\"x\":1}">>, Payload3) + receive + {publish, #{topic := T3, payload := Payload3}} -> + ?assertEqual(<<"t2">>, T3), + ?assertEqual(<<"{\"x\":1}">>, Payload3) after 1000 -> ct:fail(wait_for_t2) end, @@ -619,28 +821,35 @@ t_sqlselect_02(_Config) -> delete_rule(TopicRule1). t_sqlselect_1(_Config) -> - SQL = "SELECT json_decode(payload) as p, payload " + SQL = + "SELECT json_decode(payload) as p, payload " "FROM \"t1\" " "WHERE p.x = 1 and p.y = 2", Repub = republish_output(<<"t2">>), {ok, TopicRule} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{username, <<"emqx">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), ct:sleep(200), emqtt:publish(Client, <<"t1">>, <<"{\"x\":1,\"y\":2}">>, 0), - receive {publish, #{topic := T, payload := Payload}} -> - ?assertEqual(<<"t2">>, T), - ?assertEqual(<<"{\"x\":1,\"y\":2}">>, Payload) + receive + {publish, #{topic := T, payload := Payload}} -> + ?assertEqual(<<"t2">>, T), + ?assertEqual(<<"{\"x\":1,\"y\":2}">>, Payload) after 1000 -> ct:fail(wait_for_t2) end, emqtt:publish(Client, <<"t1">>, <<"{\"x\":1,\"y\":1}">>, 0), - receive {publish, #{topic := <<"t2">>, payload := _}} -> - ct:fail(unexpected_t2) + receive + {publish, #{topic := <<"t2">>, payload := _}} -> + ct:fail(unexpected_t2) after 1000 -> ok end, @@ -653,20 +862,25 @@ t_sqlselect_2(_Config) -> SQL = "SELECT * FROM \"t2\" ", Repub = republish_output(<<"t2">>), {ok, TopicRule} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{username, <<"emqx">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), emqtt:publish(Client, <<"t2">>, <<"{\"x\":1,\"y\":144}">>, 0), Fun = fun() -> - receive {publish, #{topic := <<"t2">>, payload := _}} -> + receive + {publish, #{topic := <<"t2">>, payload := _}} -> received_t2 - after 500 -> - received_nothing - end - end, + after 500 -> + received_nothing + end + end, received_t2 = Fun(), received_t2 = Fun(), received_nothing = Fun(), @@ -676,29 +890,36 @@ t_sqlselect_2(_Config) -> t_sqlselect_3(_Config) -> %% republish the client.connected msg - SQL = "SELECT * " + SQL = + "SELECT * " "FROM \"$events/client_connected\" " "WHERE username = 'emqx1'", Repub = republish_output(<<"t2">>, <<"clientid=${clientid}">>), {ok, TopicRule} = emqx_rule_engine:create_rule( - #{sql => SQL, id => ?TMP_RULEID, - outputs => [Repub]}), + #{ + sql => SQL, + id => ?TMP_RULEID, + outputs => [Repub] + } + ), {ok, Client} = emqtt:start_link([{clientid, <<"emqx0">>}, {username, <<"emqx0">>}]), {ok, _} = emqtt:connect(Client), {ok, _, _} = emqtt:subscribe(Client, <<"t2">>, 0), ct:sleep(200), {ok, Client1} = emqtt:start_link([{clientid, <<"c_emqx1">>}, {username, <<"emqx1">>}]), {ok, _} = emqtt:connect(Client1), - receive {publish, #{topic := T, payload := Payload}} -> - ?assertEqual(<<"t2">>, T), - ?assertEqual(<<"clientid=c_emqx1">>, Payload) + receive + {publish, #{topic := T, payload := Payload}} -> + ?assertEqual(<<"t2">>, T), + ?assertEqual(<<"clientid=c_emqx1">>, Payload) after 1000 -> ct:fail(wait_for_t2) end, emqtt:publish(Client, <<"t1">>, <<"{\"x\":1,\"y\":1}">>, 0), - receive {publish, #{topic := <<"t2">>, payload := _}} -> - ct:fail(unexpected_t2) + receive + {publish, #{topic := <<"t2">>, payload := _}} -> + ct:fail(unexpected_t2) after 1000 -> ok end, @@ -707,189 +928,337 @@ t_sqlselect_3(_Config) -> delete_rule(TopicRule). t_sqlparse_event_1(_Config) -> - Sql = "select topic as tp " - "from \"$events/session_subscribed\" ", - ?assertMatch({ok,#{<<"tp">> := <<"t/tt">>}}, + Sql = + "select topic as tp " + "from \"$events/session_subscribed\" ", + ?assertMatch( + {ok, #{<<"tp">> := <<"t/tt">>}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{topic => <<"t/tt">>}})). + #{ + sql => Sql, + context => #{topic => <<"t/tt">>} + } + ) + ). t_sqlparse_event_2(_Config) -> - Sql = "select clientid " - "from \"$events/client_connected\" ", - ?assertMatch({ok,#{<<"clientid">> := <<"abc">>}}, + Sql = + "select clientid " + "from \"$events/client_connected\" ", + ?assertMatch( + {ok, #{<<"clientid">> := <<"abc">>}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{clientid => <<"abc">>}})). + #{ + sql => Sql, + context => #{clientid => <<"abc">>} + } + ) + ). t_sqlparse_event_3(_Config) -> - Sql = "select clientid, topic as tp " - "from \"t/tt\", \"$events/client_connected\" ", - ?assertMatch({ok,#{<<"clientid">> := <<"abc">>, <<"tp">> := <<"t/tt">>}}, + Sql = + "select clientid, topic as tp " + "from \"t/tt\", \"$events/client_connected\" ", + ?assertMatch( + {ok, #{<<"clientid">> := <<"abc">>, <<"tp">> := <<"t/tt">>}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{clientid => <<"abc">>, topic => <<"t/tt">>}})). + #{ + sql => Sql, + context => #{clientid => <<"abc">>, topic => <<"t/tt">>} + } + ) + ). t_sqlparse_foreach_1(_Config) -> %% Verify foreach with and without 'AS' - Sql = "foreach payload.sensors as s " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"s">> := 1}, #{<<"s">> := 2}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"sensors\": [1, 2]}">>, - topic => <<"t/a">>}})), - Sql2 = "foreach payload.sensors " - "from \"t/#\" ", - ?assertMatch({ok,[#{item := 1}, #{item := 2}]}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => #{payload => <<"{\"sensors\": [1, 2]}">>, - topic => <<"t/a">>}})), - Sql3 = "foreach payload.sensors " - "from \"t/#\" ", - ?assertMatch({ok,[#{item := #{<<"cmd">> := <<"1">>}, clientid := <<"c_a">>}, - #{item := #{<<"cmd">> := <<"2">>, <<"name">> := <<"ct">>}, clientid := <<"c_a">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql3, - context => #{ - payload => <<"{\"sensors\": [{\"cmd\":\"1\"}, {\"cmd\":\"2\",\"name\":\"ct\"}]}">>, - clientid => <<"c_a">>, - topic => <<"t/a">>}})), - Sql4 = "foreach payload.sensors " - "from \"t/#\" ", - {ok,[#{metadata := #{rule_id := TRuleId}}, - #{metadata := #{rule_id := TRuleId}}]} = - emqx_rule_sqltester:test( - #{sql => Sql4, - context => #{ - payload => <<"{\"sensors\": [1, 2]}">>, - topic => <<"t/a">>}}), + Sql = + "foreach payload.sensors as s " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{<<"s">> := 1}, #{<<"s">> := 2}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"sensors\": [1, 2]}">>, + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "foreach payload.sensors " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{item := 1}, #{item := 2}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => #{ + payload => <<"{\"sensors\": [1, 2]}">>, + topic => <<"t/a">> + } + } + ) + ), + Sql3 = + "foreach payload.sensors " + "from \"t/#\" ", + ?assertMatch( + {ok, [ + #{item := #{<<"cmd">> := <<"1">>}, clientid := <<"c_a">>}, + #{item := #{<<"cmd">> := <<"2">>, <<"name">> := <<"ct">>}, clientid := <<"c_a">>} + ]}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => #{ + payload => + <<"{\"sensors\": [{\"cmd\":\"1\"}, {\"cmd\":\"2\",\"name\":\"ct\"}]}">>, + clientid => <<"c_a">>, + topic => <<"t/a">> + } + } + ) + ), + Sql4 = + "foreach payload.sensors " + "from \"t/#\" ", + {ok, [ + #{metadata := #{rule_id := TRuleId}}, + #{metadata := #{rule_id := TRuleId}} + ]} = + emqx_rule_sqltester:test( + #{ + sql => Sql4, + context => #{ + payload => <<"{\"sensors\": [1, 2]}">>, + topic => <<"t/a">> + } + } + ), ?assert(is_binary(TRuleId)). t_sqlparse_foreach_2(_Config) -> %% Verify foreach-do with and without 'AS' - Sql = "foreach payload.sensors as s " - "do s.cmd as msg_type " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"msg_type">> := <<"1">>},#{<<"msg_type">> := <<"2">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => + Sql = + "foreach payload.sensors as s " + "do s.cmd as msg_type " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{<<"msg_type">> := <<"1">>}, #{<<"msg_type">> := <<"2">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":\"1\"}, {\"cmd\":\"2\"}]}">>, - topic => <<"t/a">>}})), - Sql2 = "foreach payload.sensors " - "do item.cmd as msg_type " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"msg_type">> := <<"1">>},#{<<"msg_type">> := <<"2">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "foreach payload.sensors " + "do item.cmd as msg_type " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{<<"msg_type">> := <<"1">>}, #{<<"msg_type">> := <<"2">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":\"1\"}, {\"cmd\":\"2\"}]}">>, - topic => <<"t/a">>}})), - Sql3 = "foreach payload.sensors " - "do item as item " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"item">> := 1},#{<<"item">> := 2}]}, - emqx_rule_sqltester:test( - #{sql => Sql3, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + Sql3 = + "foreach payload.sensors " + "do item as item " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{<<"item">> := 1}, #{<<"item">> := 2}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => + #{ + payload => <<"{\"sensors\": [1, 2]}">>, - topic => <<"t/a">>}})). + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_foreach_3(_Config) -> %% Verify foreach-incase with and without 'AS' - Sql = "foreach payload.sensors as s " - "incase s.cmd != 1 " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"s">> := #{<<"cmd">> := 2}}, - #{<<"s">> := #{<<"cmd">> := 3}} - ]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => + Sql = + "foreach payload.sensors as s " + "incase s.cmd != 1 " + "from \"t/#\" ", + ?assertMatch( + {ok, [ + #{<<"s">> := #{<<"cmd">> := 2}}, + #{<<"s">> := #{<<"cmd">> := 3}} + ]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":1}, {\"cmd\":2}, {\"cmd\":3}]}">>, - topic => <<"t/a">>}})), - Sql2 = "foreach payload.sensors " - "incase item.cmd != 1 " - "from \"t/#\" ", - ?assertMatch({ok,[#{item := #{<<"cmd">> := 2}}, - #{item := #{<<"cmd">> := 3}} - ]}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "foreach payload.sensors " + "incase item.cmd != 1 " + "from \"t/#\" ", + ?assertMatch( + {ok, [ + #{item := #{<<"cmd">> := 2}}, + #{item := #{<<"cmd">> := 3}} + ]}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":1}, {\"cmd\":2}, {\"cmd\":3}]}">>, - topic => <<"t/a">>}})). + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_foreach_4(_Config) -> %% Verify foreach-do-incase - Sql = "foreach payload.sensors as s " - "do s.cmd as msg_type, s.name as name " - "incase is_not_null(s.cmd) " - "from \"t/#\" ", - ?assertMatch({ok,[#{<<"msg_type">> := <<"1">>},#{<<"msg_type">> := <<"2">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => + Sql = + "foreach payload.sensors as s " + "do s.cmd as msg_type, s.name as name " + "incase is_not_null(s.cmd) " + "from \"t/#\" ", + ?assertMatch( + {ok, [#{<<"msg_type">> := <<"1">>}, #{<<"msg_type">> := <<"2">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":\"1\"}, {\"cmd\":\"2\"}]}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok,[#{<<"msg_type">> := <<"1">>, <<"name">> := <<"n1">>}, #{<<"msg_type">> := <<"2">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, [#{<<"msg_type">> := <<"1">>, <<"name">> := <<"n1">>}, #{<<"msg_type">> := <<"2">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": [{\"cmd\":\"1\", \"name\":\"n1\"}, {\"cmd\":\"2\"}, {\"name\":\"n3\"}]}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok,[]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => <<"{\"sensors\": [1, 2]}">>, - topic => <<"t/a">>}})). + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, []}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": [1, 2]}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_foreach_5(_Config) -> %% Verify foreach on a empty-list or non-list variable - Sql = "foreach payload.sensors as s " - "do s.cmd as msg_type, s.name as name " - "from \"t/#\" ", - ?assertMatch({ok,[]}, emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => <<"{\"sensors\": 1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok,[]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => <<"{\"sensors\": []}">>, - topic => <<"t/a">>}})), - Sql2 = "foreach payload.sensors " - "from \"t/#\" ", - ?assertMatch({ok,[]}, emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => <<"{\"sensors\": 1}">>, - topic => <<"t/a">>}})). + Sql = + "foreach payload.sensors as s " + "do s.cmd as msg_type, s.name as name " + "from \"t/#\" ", + ?assertMatch( + {ok, []}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": 1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, []}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => <<"{\"sensors\": []}">>, + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "foreach payload.sensors " + "from \"t/#\" ", + ?assertMatch( + {ok, []}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => <<"{\"sensors\": 1}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_foreach_6(_Config) -> %% Verify foreach on a empty-list or non-list variable - Sql = "foreach json_decode(payload) " - "do item.id as zid, timestamp as t " - "from \"t/#\" ", + Sql = + "foreach json_decode(payload) " + "do item.id as zid, timestamp as t " + "from \"t/#\" ", {ok, Res} = emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => <<"[{\"id\": 5},{\"id\": 15}]">>, - topic => <<"t/a">>}}), - [#{<<"t">> := Ts1, <<"zid">> := Zid1}, - #{<<"t">> := Ts2, <<"zid">> := Zid2}] = Res, + #{ + sql => Sql, + context => + #{ + payload => <<"[{\"id\": 5},{\"id\": 15}]">>, + topic => <<"t/a">> + } + } + ), + [ + #{<<"t">> := Ts1, <<"zid">> := Zid1}, + #{<<"t">> := Ts2, <<"zid">> := Zid2} + ] = Res, ?assertEqual(true, is_integer(Ts1)), ?assertEqual(true, is_integer(Ts2)), ?assert(Zid1 == 5 orelse Zid1 == 15), @@ -897,549 +1266,1004 @@ t_sqlparse_foreach_6(_Config) -> t_sqlparse_foreach_7(_Config) -> %% Verify foreach-do-incase and cascaded AS - Sql = "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " - "do info.cmd as msg_type, info.name as name " - "incase is_not_null(info.cmd) " - "from \"t/#\" " - "where s.page = '2' ", - Payload = <<"{\"sensors\": {\"page\": 2, \"collection\": " - "{\"info\":[{\"name\":\"cmd1\", \"cmd\":\"1\"}, {\"cmd\":\"2\"}]} } }">>, - ?assertMatch({ok,[#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}, #{<<"msg_type">> := <<"2">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => Payload, - topic => <<"t/a">>}})), - Sql2 = "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " - "do info.cmd as msg_type, info.name as name " - "incase is_not_null(info.cmd) " - "from \"t/#\" " - "where s.page = '3' ", - ?assertMatch({error, nomatch}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => Payload, - topic => <<"t/a">>}})). + Sql = + "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " + "do info.cmd as msg_type, info.name as name " + "incase is_not_null(info.cmd) " + "from \"t/#\" " + "where s.page = '2' ", + Payload = << + "{\"sensors\": {\"page\": 2, \"collection\": " + "{\"info\":[{\"name\":\"cmd1\", \"cmd\":\"1\"}, {\"cmd\":\"2\"}]} } }" + >>, + ?assertMatch( + {ok, [#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}, #{<<"msg_type">> := <<"2">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => Payload, + topic => <<"t/a">> + } + } + ) + ), + Sql2 = + "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " + "do info.cmd as msg_type, info.name as name " + "incase is_not_null(info.cmd) " + "from \"t/#\" " + "where s.page = '3' ", + ?assertMatch( + {error, nomatch}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => Payload, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_foreach_8(_Config) -> %% Verify foreach-do-incase and cascaded AS - Sql = "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " - "do info.cmd as msg_type, info.name as name " - "incase is_map(info) " - "from \"t/#\" " - "where s.page = '2' ", - Payload = <<"{\"sensors\": {\"page\": 2, \"collection\": " - "{\"info\":[\"haha\", {\"name\":\"cmd1\", \"cmd\":\"1\"}]} } }">>, - ?assertMatch({ok,[#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}]}, - emqx_rule_sqltester:test( - #{sql => Sql, - context => - #{payload => Payload, - topic => <<"t/a">>}})), + Sql = + "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, c.info as info " + "do info.cmd as msg_type, info.name as name " + "incase is_map(info) " + "from \"t/#\" " + "where s.page = '2' ", + Payload = << + "{\"sensors\": {\"page\": 2, \"collection\": " + "{\"info\":[\"haha\", {\"name\":\"cmd1\", \"cmd\":\"1\"}]} } }" + >>, + ?assertMatch( + {ok, [#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}]}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => + #{ + payload => Payload, + topic => <<"t/a">> + } + } + ) + ), - Sql3 = "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, sublist(2,1,c.info) as info " - "do info.cmd as msg_type, info.name as name " - "from \"t/#\" " - "where s.page = '2' ", - [?assertMatch({ok,[#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}]}, - emqx_rule_sqltester:test( - #{sql => SqlN, - context => - #{payload => Payload, - topic => <<"t/a">>}})) - || SqlN <- [Sql3]]. + Sql3 = + "foreach json_decode(payload) as p, p.sensors as s, s.collection as c, sublist(2,1,c.info) as info " + "do info.cmd as msg_type, info.name as name " + "from \"t/#\" " + "where s.page = '2' ", + [ + ?assertMatch( + {ok, [#{<<"name">> := <<"cmd1">>, <<"msg_type">> := <<"1">>}]}, + emqx_rule_sqltester:test( + #{ + sql => SqlN, + context => + #{ + payload => Payload, + topic => <<"t/a">> + } + } + ) + ) + || SqlN <- [Sql3] + ]. t_sqlparse_case_when_1(_Config) -> %% case-when-else clause - Sql = "select " - " case when payload.x < 0 then 0 " - " when payload.x > 7 then 7 " - " else payload.x " - " end as y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"y">> := 1}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 0}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 0}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 0}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": -1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 7}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 7}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 7}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 8}">>, - topic => <<"t/a">>}})), + Sql = + "select " + " case when payload.x < 0 then 0 " + " when payload.x > 7 then 7 " + " else payload.x " + " end as y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"y">> := 1}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 0}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 0}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 0}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": -1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 7}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 7}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 7}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 8}">>, + topic => <<"t/a">> + } + } + ) + ), ok. t_sqlparse_case_when_2(_Config) -> % switch clause - Sql = "select " - " case payload.x when 1 then 2 " - " when 2 then 3 " - " else 4 " - " end as y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"y">> := 2}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 3}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 2}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 4}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 4}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 4}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 7}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 4}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 8}">>, - topic => <<"t/a">>}})). + Sql = + "select " + " case payload.x when 1 then 2 " + " when 2 then 3 " + " else 4 " + " end as y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"y">> := 2}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 3}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 2}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 4}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 4}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 4}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 7}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 4}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 8}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_case_when_3(_Config) -> %% case-when clause - Sql = "select " - " case when payload.x < 0 then 0 " - " when payload.x > 7 then 7 " - " end as y " - "from \"t/#\" ", - ?assertMatch({ok, #{}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 5}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 0}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 0}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": -1}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 7}">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{<<"y">> := 7}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 8}">>, - topic => <<"t/a">>}})), + Sql = + "select " + " case when payload.x < 0 then 0 " + " when payload.x > 7 then 7 " + " end as y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 5}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 0}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 0}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": -1}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 7}">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{<<"y">> := 7}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 8}">>, + topic => <<"t/a">> + } + } + ) + ), ok. t_sqlparse_array_index_1(_Config) -> %% index get - Sql = "select " - " json_decode(payload) as p, " - " p[1] as a " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"a">> := #{<<"x">> := 1}}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"[{\"x\": 1}]">>, - topic => <<"t/a">>}})), - ?assertMatch({ok, #{}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{payload => <<"{\"x\": 1}">>, - topic => <<"t/a">>}})), + Sql = + "select " + " json_decode(payload) as p, " + " p[1] as a " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := #{<<"x">> := 1}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"[{\"x\": 1}]">>, + topic => <<"t/a">> + } + } + ) + ), + ?assertMatch( + {ok, #{}}, + emqx_rule_sqltester:test( + #{ + sql => Sql, + context => #{ + payload => <<"{\"x\": 1}">>, + topic => <<"t/a">> + } + } + ) + ), %% index get without 'as' - Sql2 = "select " - " payload.x[2] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"x">> := [3]}}}, emqx_rule_sqltester:test( - #{sql => Sql2, - context => #{payload => #{<<"x">> => [1,3,4]}, - topic => <<"t/a">>}})), + Sql2 = + "select " + " payload.x[2] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"x">> := [3]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => #{ + payload => #{<<"x">> => [1, 3, 4]}, + topic => <<"t/a">> + } + } + ) + ), %% index get without 'as' again - Sql3 = "select " - " payload.x[2].y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"x">> := [#{<<"y">> := 3}]}}}, emqx_rule_sqltester:test( - #{sql => Sql3, - context => #{payload => #{<<"x">> => [1,#{y => 3},4]}, - topic => <<"t/a">>}})), + Sql3 = + "select " + " payload.x[2].y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"x">> := [#{<<"y">> := 3}]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => #{ + payload => #{<<"x">> => [1, #{y => 3}, 4]}, + topic => <<"t/a">> + } + } + ) + ), %% index get with 'as' - Sql4 = "select " - " payload.x[2].y as b " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"b">> := 3}}, emqx_rule_sqltester:test( - #{sql => Sql4, - context => #{payload => #{<<"x">> => [1,#{y => 3},4]}, - topic => <<"t/a">>}})). + Sql4 = + "select " + " payload.x[2].y as b " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"b">> := 3}}, + emqx_rule_sqltester:test( + #{ + sql => Sql4, + context => #{ + payload => #{<<"x">> => [1, #{y => 3}, 4]}, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_index_2(_Config) -> %% array get with negative index - Sql1 = "select " - " payload.x[-2].y as b " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"b">> := 3}}, emqx_rule_sqltester:test( - #{sql => Sql1, - context => #{payload => #{<<"x">> => [1,#{y => 3},4]}, - topic => <<"t/a">>}})), + Sql1 = + "select " + " payload.x[-2].y as b " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"b">> := 3}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => #{ + payload => #{<<"x">> => [1, #{y => 3}, 4]}, + topic => <<"t/a">> + } + } + ) + ), %% array append to head or tail of a list: - Sql2 = "select " - " payload.x as b, " - " 1 as c[-0], " - " 2 as c[-0], " - " b as c[0] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"b">> := 0, <<"c">> := [0,1,2]}}, emqx_rule_sqltester:test( - #{sql => Sql2, - context => #{payload => #{<<"x">> => 0}, - topic => <<"t/a">>}})), + Sql2 = + "select " + " payload.x as b, " + " 1 as c[-0], " + " 2 as c[-0], " + " b as c[0] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"b">> := 0, <<"c">> := [0, 1, 2]}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => #{ + payload => #{<<"x">> => 0}, + topic => <<"t/a">> + } + } + ) + ), %% construct an empty list: - Sql3 = "select " - " [] as c, " - " 1 as c[-0], " - " 2 as c[-0], " - " 0 as c[0] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"c">> := [0,1,2]}}, emqx_rule_sqltester:test( - #{sql => Sql3, - context => #{payload => <<"">>, - topic => <<"t/a">>}})), + Sql3 = + "select " + " [] as c, " + " 1 as c[-0], " + " 2 as c[-0], " + " 0 as c[0] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"c">> := [0, 1, 2]}}, + emqx_rule_sqltester:test( + #{ + sql => Sql3, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ), %% construct a list: - Sql4 = "select " - " [payload.a, \"topic\", 'c'] as c, " - " 1 as c[-0], " - " 2 as c[-0], " - " 0 as c[0] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"c">> := [0,11,<<"t/a">>,<<"c">>,1,2]}}, emqx_rule_sqltester:test( - #{sql => Sql4, - context => #{payload => <<"{\"a\":11}">>, - topic => <<"t/a">> - }})). + Sql4 = + "select " + " [payload.a, \"topic\", 'c'] as c, " + " 1 as c[-0], " + " 2 as c[-0], " + " 0 as c[0] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"c">> := [0, 11, <<"t/a">>, <<"c">>, 1, 2]}}, + emqx_rule_sqltester:test( + #{ + sql => Sql4, + context => #{ + payload => <<"{\"a\":11}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_index_3(_Config) -> %% array with json string payload: - Sql0 = "select " - "payload," - "payload.x[2].y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"x">> := [1, #{<<"y">> := [1,2]}, 3]}}}, - emqx_rule_sqltester:test( - #{sql => Sql0, - context => #{payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, - topic => <<"t/a">>}})), + Sql0 = + "select " + "payload," + "payload.x[2].y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"x">> := [1, #{<<"y">> := [1, 2]}, 3]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql0, + context => #{ + payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, + topic => <<"t/a">> + } + } + ) + ), %% same as above but don't select payload: - Sql1 = "select " - "payload.x[2].y as b " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"b">> := [1,2]}}, emqx_rule_sqltester:test( - #{sql => Sql1, - context => #{payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, - topic => <<"t/a">>}})), + Sql1 = + "select " + "payload.x[2].y as b " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"b">> := [1, 2]}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => #{ + payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, + topic => <<"t/a">> + } + } + ) + ), %% same as above but add 'as' clause: - Sql2 = "select " - "payload.x[2].y as b.c " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"b">> := #{<<"c">> := [1,2]}}}, emqx_rule_sqltester:test( - #{sql => Sql2, - context => #{payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, - topic => <<"t/a">>}})). + Sql2 = + "select " + "payload.x[2].y as b.c " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"b">> := #{<<"c">> := [1, 2]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => #{ + payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_index_4(_Config) -> %% array with json string payload: - Sql0 = "select " - "0 as payload.x[2].y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"x">> := [#{<<"y">> := 0}]}}}, - emqx_rule_sqltester:test( - #{sql => Sql0, - context => #{payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, - topic => <<"t/a">>}})), + Sql0 = + "select " + "0 as payload.x[2].y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"x">> := [#{<<"y">> := 0}]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql0, + context => #{ + payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, + topic => <<"t/a">> + } + } + ) + ), %% array with json string payload, and also select payload.x: - Sql1 = "select " - "payload.x, " - "0 as payload.x[2].y " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"x">> := [1, #{<<"y">> := 0}, 3]}}}, - emqx_rule_sqltester:test( - #{sql => Sql1, - context => #{payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, - topic => <<"t/a">>}})). + Sql1 = + "select " + "payload.x, " + "0 as payload.x[2].y " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"x">> := [1, #{<<"y">> := 0}, 3]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => #{ + payload => <<"{\"x\": [1,{\"y\": [1,2]},3]}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_index_5(_Config) -> - Sql00 = "select " - " [1,2,3,4] " - "from \"t/#\" ", + Sql00 = + "select " + " [1,2,3,4] " + "from \"t/#\" ", {ok, Res00} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => <<"">>, - topic => <<"t/a">>}}), - ?assert(lists:any(fun({_K, V}) -> - V =:= [1,2,3,4] - end, maps:to_list(Res00))). + #{ + sql => Sql00, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ), + ?assert( + lists:any( + fun({_K, V}) -> + V =:= [1, 2, 3, 4] + end, + maps:to_list(Res00) + ) + ). t_sqlparse_select_matadata_1(_Config) -> %% array with json string payload: - Sql0 = "select " - "payload " - "from \"t/#\" ", - ?assertNotMatch({ok, #{<<"payload">> := <<"abc">>, metadata := _}}, - emqx_rule_sqltester:test( - #{sql => Sql0, - context => #{payload => <<"abc">>, - topic => <<"t/a">>}})), - Sql1 = "select " - "payload, metadata " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := <<"abc">>, <<"metadata">> := _}}, - emqx_rule_sqltester:test( - #{sql => Sql1, - context => #{payload => <<"abc">>, - topic => <<"t/a">>}})). + Sql0 = + "select " + "payload " + "from \"t/#\" ", + ?assertNotMatch( + {ok, #{<<"payload">> := <<"abc">>, metadata := _}}, + emqx_rule_sqltester:test( + #{ + sql => Sql0, + context => #{ + payload => <<"abc">>, + topic => <<"t/a">> + } + } + ) + ), + Sql1 = + "select " + "payload, metadata " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := <<"abc">>, <<"metadata">> := _}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => #{ + payload => <<"abc">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_range_1(_Config) -> %% get a range of list - Sql0 = "select " - " payload.a[1..4] as c " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"c">> := [0,1,2,3]}}, emqx_rule_sqltester:test( - #{sql => Sql0, - context => #{payload => <<"{\"a\":[0,1,2,3,4,5]}">>, - topic => <<"t/a">>}})), - %% get a range from non-list data - Sql02 = "select " - " payload.a[1..4] as c " - "from \"t/#\" ", - ?assertMatch({error, {select_and_transform_error, {error,{range_get,non_list_data},_}}}, + Sql0 = + "select " + " payload.a[1..4] as c " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"c">> := [0, 1, 2, 3]}}, emqx_rule_sqltester:test( - #{sql => Sql02, + #{ + sql => Sql0, + context => #{ + payload => <<"{\"a\":[0,1,2,3,4,5]}">>, + topic => <<"t/a">> + } + } + ) + ), + %% get a range from non-list data + Sql02 = + "select " + " payload.a[1..4] as c " + "from \"t/#\" ", + ?assertMatch( + {error, {select_and_transform_error, {error, {range_get, non_list_data}, _}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql02, context => - #{payload => <<"{\"x\":[0,1,2,3,4,5]}">>, - topic => <<"t/a">>}})), + #{ + payload => <<"{\"x\":[0,1,2,3,4,5]}">>, + topic => <<"t/a">> + } + } + ) + ), %% construct a range: - Sql1 = "select " - " [1..4] as c, " - " 5 as c[-0], " - " 6 as c[-0], " - " 0 as c[0] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"c">> := [0,1,2,3,4,5,6]}}, emqx_rule_sqltester:test( - #{sql => Sql1, - context => #{payload => <<"">>, - topic => <<"t/a">>}})). + Sql1 = + "select " + " [1..4] as c, " + " 5 as c[-0], " + " 6 as c[-0], " + " 0 as c[0] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"c">> := [0, 1, 2, 3, 4, 5, 6]}}, + emqx_rule_sqltester:test( + #{ + sql => Sql1, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_array_range_2(_Config) -> %% construct a range without 'as' - Sql00 = "select " - " [1..4] " - "from \"t/#\" ", + Sql00 = + "select " + " [1..4] " + "from \"t/#\" ", {ok, Res00} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => <<"">>, - topic => <<"t/a">>}}), - ?assert(lists:any(fun({_K, V}) -> - V =:= [1,2,3,4] - end, maps:to_list(Res00))), + #{ + sql => Sql00, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ), + ?assert( + lists:any( + fun({_K, V}) -> + V =:= [1, 2, 3, 4] + end, + maps:to_list(Res00) + ) + ), %% construct a range without 'as' - Sql01 = "select " - " a[2..4] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"a">> := [2,3,4]}}, + Sql01 = + "select " + " a[2..4] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"a">> := [2, 3, 4]}}, emqx_rule_sqltester:test( - #{sql => Sql01, - context => #{<<"a">> => [1,2,3,4,5], - topic => <<"t/a">>}})), + #{ + sql => Sql01, + context => #{ + <<"a">> => [1, 2, 3, 4, 5], + topic => <<"t/a">> + } + } + ) + ), %% get a range of list without 'as' - Sql02 = "select " - " payload.a[1..4] " - "from \"t/#\" ", - ?assertMatch({ok, #{<<"payload">> := #{<<"a">> := [0,1,2,3]}}}, emqx_rule_sqltester:test( - #{sql => Sql02, - context => #{payload => <<"{\"a\":[0,1,2,3,4,5]}">>, - topic => <<"t/a">>}})). + Sql02 = + "select " + " payload.a[1..4] " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"payload">> := #{<<"a">> := [0, 1, 2, 3]}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql02, + context => #{ + payload => <<"{\"a\":[0,1,2,3,4,5]}">>, + topic => <<"t/a">> + } + } + ) + ). t_sqlparse_true_false(_Config) -> %% construct a range without 'as' - Sql00 = "select " - " true as a, false as b, " - " false as x.y, true as c[-0] " - "from \"t/#\" ", + Sql00 = + "select " + " true as a, false as b, " + " false as x.y, true as c[-0] " + "from \"t/#\" ", {ok, Res00} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => <<"">>, - topic => <<"t/a">>}}), - ?assertMatch(#{<<"a">> := true, <<"b">> := false, - <<"x">> := #{<<"y">> := false}, - <<"c">> := [true] - }, Res00). + #{ + sql => Sql00, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ), + ?assertMatch( + #{ + <<"a">> := true, + <<"b">> := false, + <<"x">> := #{<<"y">> := false}, + <<"c">> := [true] + }, + Res00 + ). t_sqlparse_undefined_variable(_Config) -> %% undefined == undefined - Sql00 = "select " - "a, b " - "from \"t/#\" " - "where a = b" - , + Sql00 = + "select " + "a, b " + "from \"t/#\" " + "where a = b", + {ok, Res00} = emqx_rule_sqltester:test( - #{sql => Sql00, context => #{payload => <<"">>, topic => <<"t/a">>}}), + #{sql => Sql00, context => #{payload => <<"">>, topic => <<"t/a">>}} + ), ?assertEqual(#{<<"a">> => undefined, <<"b">> => undefined}, Res00), ?assertEqual(2, map_size(Res00)), %% undefined compare to non-undefined variables should return false - Sql01 = "select " - "a, b " - "from \"t/#\" " - "where a > b" - , + Sql01 = + "select " + "a, b " + "from \"t/#\" " + "where a > b", + {error, nomatch} = emqx_rule_sqltester:test( - #{sql => Sql01, - context => #{payload => <<"{\"b\":1}">>, topic => <<"t/a">>}}), - Sql02 = "select " - "a < b as c " - "from \"t/#\" " - , + #{ + sql => Sql01, + context => #{payload => <<"{\"b\":1}">>, topic => <<"t/a">>} + } + ), + Sql02 = + "select " + "a < b as c " + "from \"t/#\" ", + {ok, Res02} = emqx_rule_sqltester:test( - #{sql => Sql02, - context => #{payload => <<"{\"b\":1}">>, topic => <<"t/a">>}}), + #{ + sql => Sql02, + context => #{payload => <<"{\"b\":1}">>, topic => <<"t/a">>} + } + ), ?assertMatch(#{<<"c">> := false}, Res02). t_sqlparse_new_map(_Config) -> %% construct a range without 'as' - Sql00 = "select " - " map_new() as a, map_new() as b, " - " map_new() as x.y, map_new() as c[-0] " - "from \"t/#\" ", + Sql00 = + "select " + " map_new() as a, map_new() as b, " + " map_new() as x.y, map_new() as c[-0] " + "from \"t/#\" ", {ok, Res00} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => <<"">>, - topic => <<"t/a">>}}), - ?assertMatch(#{<<"a">> := #{}, <<"b">> := #{}, - <<"x">> := #{<<"y">> := #{}}, - <<"c">> := [#{}] - }, Res00). + #{ + sql => Sql00, + context => #{ + payload => <<"">>, + topic => <<"t/a">> + } + } + ), + ?assertMatch( + #{ + <<"a">> := #{}, + <<"b">> := #{}, + <<"x">> := #{<<"y">> := #{}}, + <<"c">> := [#{}] + }, + Res00 + ). t_sqlparse_payload_as(_Config) -> %% https://github.com/emqx/emqx/issues/3866 - Sql00 = "SELECT " - " payload, map_get('engineWorkTime', payload.params, -1) as payload.params.engineWorkTime, " - " map_get('hydOilTem', payload.params, -1) as payload.params.hydOilTem " - "FROM \"t/#\" ", - Payload1 = <<"{ \"msgId\": 1002, \"params\": { \"convertTemp\": 20, \"engineSpeed\": 42, \"hydOilTem\": 30 } }">>, + Sql00 = + "SELECT " + " payload, map_get('engineWorkTime', payload.params, -1) as payload.params.engineWorkTime, " + " map_get('hydOilTem', payload.params, -1) as payload.params.hydOilTem " + "FROM \"t/#\" ", + Payload1 = + <<"{ \"msgId\": 1002, \"params\": { \"convertTemp\": 20, \"engineSpeed\": 42, \"hydOilTem\": 30 } }">>, {ok, Res01} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => Payload1, - topic => <<"t/a">>}}), - ?assertMatch(#{ - <<"payload">> := #{ - <<"params">> := #{ - <<"convertTemp">> := 20, - <<"engineSpeed">> := 42, - <<"engineWorkTime">> := -1, - <<"hydOilTem">> := 30 + #{ + sql => Sql00, + context => #{ + payload => Payload1, + topic => <<"t/a">> } } - }, Res01), + ), + ?assertMatch( + #{ + <<"payload">> := #{ + <<"params">> := #{ + <<"convertTemp">> := 20, + <<"engineSpeed">> := 42, + <<"engineWorkTime">> := -1, + <<"hydOilTem">> := 30 + } + } + }, + Res01 + ), Payload2 = <<"{ \"msgId\": 1002, \"params\": { \"convertTemp\": 20, \"engineSpeed\": 42 } }">>, {ok, Res02} = emqx_rule_sqltester:test( - #{sql => Sql00, - context => #{payload => Payload2, - topic => <<"t/a">>}}), - ?assertMatch(#{ - <<"payload">> := #{ - <<"params">> := #{ - <<"convertTemp">> := 20, - <<"engineSpeed">> := 42, - <<"engineWorkTime">> := -1, - <<"hydOilTem">> := -1 + #{ + sql => Sql00, + context => #{ + payload => Payload2, + topic => <<"t/a">> } } - }, Res02). + ), + ?assertMatch( + #{ + <<"payload">> := #{ + <<"params">> := #{ + <<"convertTemp">> := 20, + <<"engineSpeed">> := 42, + <<"engineWorkTime">> := -1, + <<"hydOilTem">> := -1 + } + } + }, + Res02 + ). t_sqlparse_nested_get(_Config) -> - Sql = "select payload as p, p.a.b as c " - "from \"t/#\" ", - ?assertMatch({ok,#{<<"c">> := 0}}, + Sql = + "select payload as p, p.a.b as c " + "from \"t/#\" ", + ?assertMatch( + {ok, #{<<"c">> := 0}}, emqx_rule_sqltester:test( - #{sql => Sql, - context => #{ - topic => <<"t/1">>, - payload => <<"{\"a\": {\"b\": 0}}">> - }})). + #{ + sql => Sql, + context => #{ + topic => <<"t/1">>, + payload => <<"{\"a\": {\"b\": 0}}">> + } + } + ) + ). t_sqlparse_invalid_json(_Config) -> - Sql02 = "select " + Sql02 = + "select " " payload.a[1..4] as c " "from \"t/#\" ", - ?assertMatch({error, {select_and_transform_error, {error,{decode_json_failed,_},_}}}, - emqx_rule_sqltester:test( - #{sql => Sql02, - context => - #{payload => <<"{\"x\":[0,1,2,3,}">>, - topic => <<"t/a">>}})), + ?assertMatch( + {error, {select_and_transform_error, {error, {decode_json_failed, _}, _}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql02, + context => + #{ + payload => <<"{\"x\":[0,1,2,3,}">>, + topic => <<"t/a">> + } + } + ) + ), - - Sql2 = "foreach payload.sensors " + Sql2 = + "foreach payload.sensors " "do item.cmd as msg_type " "from \"t/#\" ", - ?assertMatch({error, {select_and_collect_error, {error,{decode_json_failed,_},_}}}, - emqx_rule_sqltester:test( - #{sql => Sql2, - context => - #{payload => - <<"{\"sensors\": [{\"cmd\":\"1\"} {\"cmd\":}]}">>, - topic => <<"t/a">>}})). + ?assertMatch( + {error, {select_and_collect_error, {error, {decode_json_failed, _}, _}}}, + emqx_rule_sqltester:test( + #{ + sql => Sql2, + context => + #{ + payload => + <<"{\"sensors\": [{\"cmd\":\"1\"} {\"cmd\":}]}">>, + topic => <<"t/a">> + } + } + ) + ). %%------------------------------------------------------------------------------ %% Test cases for telemetry functions %%------------------------------------------------------------------------------ t_get_basic_usage_info_0(_Config) -> ?assertEqual( - #{ num_rules => 0 - , referenced_bridges => #{} - }, - emqx_rule_engine:get_basic_usage_info()), + #{ + num_rules => 0, + referenced_bridges => #{} + }, + emqx_rule_engine:get_basic_usage_info() + ), ok. t_get_basic_usage_info_1(_Config) -> {ok, _} = emqx_rule_engine:create_rule( - #{id => <<"rule:t_get_basic_usage_info:1">>, - sql => <<"select 1 from topic">>, - outputs => - [ #{function => <<"erlang:hibernate">>, args => #{}} - , #{function => console} - , <<"http:my_http_bridge">> - , <<"http:my_http_bridge">> - ]}), + #{ + id => <<"rule:t_get_basic_usage_info:1">>, + sql => <<"select 1 from topic">>, + outputs => + [ + #{function => <<"erlang:hibernate">>, args => #{}}, + #{function => console}, + <<"http:my_http_bridge">>, + <<"http:my_http_bridge">> + ] + } + ), {ok, _} = emqx_rule_engine:create_rule( - #{id => <<"rule:t_get_basic_usage_info:2">>, - sql => <<"select 1 from topic">>, - outputs => - [ <<"mqtt:my_mqtt_bridge">> - , <<"http:my_http_bridge">> - ]}), + #{ + id => <<"rule:t_get_basic_usage_info:2">>, + sql => <<"select 1 from topic">>, + outputs => + [ + <<"mqtt:my_mqtt_bridge">>, + <<"http:my_http_bridge">> + ] + } + ), ?assertEqual( - #{ num_rules => 2 - , referenced_bridges => - #{ mqtt => 1 - , http => 3 - } - }, - emqx_rule_engine:get_basic_usage_info()), + #{ + num_rules => 2, + referenced_bridges => + #{ + mqtt => 1, + http => 3 + } + }, + emqx_rule_engine:get_basic_usage_info() + ), ok. %%------------------------------------------------------------------------------ @@ -1449,8 +2273,10 @@ t_get_basic_usage_info_1(_Config) -> republish_output(Topic) -> republish_output(Topic, <<"${payload}">>). republish_output(Topic, Payload) -> - #{function => republish, - args => #{payload => Payload, topic => Topic, qos => 0, retain => false}}. + #{ + function => republish, + args => #{payload => Payload, topic => Topic, qos => 0, retain => false} + }. make_simple_rule_with_ts(RuleId, Ts) when is_binary(RuleId) -> SQL = <<"select * from \"simple/topic\"">>, @@ -1467,15 +2293,15 @@ make_simple_rule(RuleId, SQL, Topics) when is_binary(RuleId) -> make_simple_rule(RuleId, SQL, Topics, Ts) when is_binary(RuleId) -> #{ - id => RuleId, - sql => SQL, - from => Topics, - fields => [<<"*">>], - is_foreach => false, - conditions => {}, - outputs => [#{mod => emqx_rule_outputs, func => console, args => #{}}], - description => <<"simple rule">>, - created_at => Ts + id => RuleId, + sql => SQL, + from => Topics, + fields => [<<"*">>], + is_foreach => false, + conditions => {}, + outputs => [#{mod => emqx_rule_outputs, func => console, args => #{}}], + description => <<"simple rule">>, + created_at => Ts }. output_record_triggered_events(Data = #{event := EventName}, _Envs, _Args) -> @@ -1488,34 +2314,39 @@ verify_event(EventName) -> [] -> ct:fail({no_such_event, EventName, ets:tab2list(events_record_tab)}); Records -> - [begin - %% verify fields can be formatted to JSON string - _ = emqx_json:encode(Fields), - %% verify metadata fields - verify_metadata_fields(EventName, Fields), - %% verify available fields for each event name - verify_event_fields(EventName, Fields) - end || {_Name, Fields} <- Records] + [ + begin + %% verify fields can be formatted to JSON string + _ = emqx_json:encode(Fields), + %% verify metadata fields + verify_metadata_fields(EventName, Fields), + %% verify available fields for each event name + verify_event_fields(EventName, Fields) + end + || {_Name, Fields} <- Records + ] end. verify_metadata_fields(_EventName, #{metadata := Metadata}) -> ?assertMatch( #{rule_id := <<"rule:t_events">>}, - Metadata). + Metadata + ). verify_event_fields('message.publish', Fields) -> - #{id := ID, - clientid := ClientId, - username := Username, - payload := Payload, - peerhost := PeerHost, - topic := Topic, - qos := QoS, - flags := Flags, - headers := Headers, - pub_props := Properties, - timestamp := Timestamp, - publish_received_at := EventAt + #{ + id := ID, + clientid := ClientId, + username := Username, + payload := Payload, + peerhost := PeerHost, + topic := Topic, + qos := QoS, + flags := Flags, + headers := Headers, + pub_props := Properties, + timestamp := Timestamp, + publish_received_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1530,25 +2361,25 @@ verify_event_fields('message.publish', Fields) -> ?assert(is_map(Flags)), ?assert(is_map(Headers)), ?assertMatch(#{'Message-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('client.connected', Fields) -> - #{clientid := ClientId, - username := Username, - mountpoint := MountPoint, - peername := PeerName, - sockname := SockName, - proto_name := ProtoName, - proto_ver := ProtoVer, - keepalive := Keepalive, - clean_start := CleanStart, - expiry_interval := ExpiryInterval, - is_bridge := IsBridge, - conn_props := Properties, - timestamp := Timestamp, - connected_at := EventAt + #{ + clientid := ClientId, + username := Username, + mountpoint := MountPoint, + peername := PeerName, + sockname := SockName, + proto_name := ProtoName, + proto_ver := ProtoVer, + keepalive := Keepalive, + clean_start := CleanStart, + expiry_interval := ExpiryInterval, + is_bridge := IsBridge, + conn_props := Properties, + timestamp := Timestamp, + connected_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1565,19 +2396,19 @@ verify_event_fields('client.connected', Fields) -> ?assertEqual(60, ExpiryInterval), ?assertEqual(false, IsBridge), ?assertMatch(#{'Session-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('client.disconnected', Fields) -> - #{reason := Reason, - clientid := ClientId, - username := Username, - peername := PeerName, - sockname := SockName, - disconn_props := Properties, - timestamp := Timestamp, - disconnected_at := EventAt + #{ + reason := Reason, + clientid := ClientId, + username := Username, + peername := PeerName, + sockname := SockName, + disconn_props := Properties, + timestamp := Timestamp, + disconnected_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1588,18 +2419,20 @@ verify_event_fields('client.disconnected', Fields) -> verify_peername(PeerName), verify_peername(SockName), ?assertMatch(#{'User-Property' := #{<<"reason">> := <<"normal">>}}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - -verify_event_fields(SubUnsub, Fields) when SubUnsub == 'session.subscribed' - ; SubUnsub == 'session.unsubscribed' -> - #{clientid := ClientId, - username := Username, - peerhost := PeerHost, - topic := Topic, - qos := QoS, - timestamp := Timestamp +verify_event_fields(SubUnsub, Fields) when + SubUnsub == 'session.subscribed'; + SubUnsub == 'session.unsubscribed' +-> + #{ + clientid := ClientId, + username := Username, + peerhost := PeerHost, + topic := Topic, + qos := QoS, + timestamp := Timestamp } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1614,28 +2447,31 @@ verify_event_fields(SubUnsub, Fields) when SubUnsub == 'session.subscribed' 'session.subscribed' -> sub_props; 'session.unsubscribed' -> unsub_props end, - ?assertMatch(#{'User-Property' := #{<<"topic_name">> := <<"t1">>}}, - maps:get(PropKey, Fields)), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000); - + ?assertMatch( + #{'User-Property' := #{<<"topic_name">> := <<"t1">>}}, + maps:get(PropKey, Fields) + ), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000); verify_event_fields('delivery.dropped', Fields) -> - #{event := 'delivery.dropped', - id := ID, - metadata := #{rule_id := RuleId}, - reason := Reason, - clientid := ClientId, - username := Username, - from_clientid := FromClientId, - from_username := FromUsername, - node := Node, - payload := Payload, - peerhost := PeerHost, - pub_props := Properties, - publish_received_at := EventAt, - qos := QoS, - flags := Flags, - timestamp := Timestamp, - topic := Topic} = Fields, + #{ + event := 'delivery.dropped', + id := ID, + metadata := #{rule_id := RuleId}, + reason := Reason, + clientid := ClientId, + username := Username, + from_clientid := FromClientId, + from_username := FromUsername, + node := Node, + payload := Payload, + peerhost := PeerHost, + pub_props := Properties, + publish_received_at := EventAt, + qos := QoS, + flags := Flags, + timestamp := Timestamp, + topic := Topic + } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, RcvdAtElapse = Now - EventAt, @@ -1653,23 +2489,23 @@ verify_event_fields('delivery.dropped', Fields) -> ?assertEqual(1, QoS), ?assert(is_map(Flags)), ?assertMatch(#{'Message-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('message.dropped', Fields) -> - #{id := ID, - reason := Reason, - clientid := ClientId, - username := Username, - payload := Payload, - peerhost := PeerHost, - topic := Topic, - qos := QoS, - flags := Flags, - pub_props := Properties, - timestamp := Timestamp, - publish_received_at := EventAt + #{ + id := ID, + reason := Reason, + clientid := ClientId, + username := Username, + payload := Payload, + peerhost := PeerHost, + topic := Topic, + qos := QoS, + flags := Flags, + pub_props := Properties, + timestamp := Timestamp, + publish_received_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1684,24 +2520,24 @@ verify_event_fields('message.dropped', Fields) -> ?assertEqual(1, QoS), ?assert(is_map(Flags)), ?assertMatch(#{'Message-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('message.delivered', Fields) -> - #{id := ID, - clientid := ClientId, - username := Username, - from_clientid := FromClientId, - from_username := FromUsername, - payload := Payload, - peerhost := PeerHost, - topic := Topic, - qos := QoS, - flags := Flags, - pub_props := Properties, - timestamp := Timestamp, - publish_received_at := EventAt + #{ + id := ID, + clientid := ClientId, + username := Username, + from_clientid := FromClientId, + from_username := FromUsername, + payload := Payload, + peerhost := PeerHost, + topic := Topic, + qos := QoS, + flags := Flags, + pub_props := Properties, + timestamp := Timestamp, + publish_received_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1717,25 +2553,25 @@ verify_event_fields('message.delivered', Fields) -> ?assertEqual(1, QoS), ?assert(is_map(Flags)), ?assertMatch(#{'Message-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('message.acked', Fields) -> - #{id := ID, - clientid := ClientId, - username := Username, - from_clientid := FromClientId, - from_username := FromUsername, - payload := Payload, - peerhost := PeerHost, - topic := Topic, - qos := QoS, - flags := Flags, - pub_props := PubProps, - puback_props := PubAckProps, - timestamp := Timestamp, - publish_received_at := EventAt + #{ + id := ID, + clientid := ClientId, + username := Username, + from_clientid := FromClientId, + from_username := FromUsername, + payload := Payload, + peerhost := PeerHost, + topic := Topic, + qos := QoS, + flags := Flags, + pub_props := PubProps, + puback_props := PubAckProps, + timestamp := Timestamp, + publish_received_at := EventAt } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1752,23 +2588,23 @@ verify_event_fields('message.acked', Fields) -> ?assert(is_map(Flags)), ?assertMatch(#{'Message-Expiry-Interval' := 60}, PubProps), ?assert(is_map(PubAckProps)), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000), - ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60*1000), + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000), + ?assert(0 =< RcvdAtElapse andalso RcvdAtElapse =< 60 * 1000), ?assert(EventAt =< Timestamp); - verify_event_fields('client.connack', Fields) -> - #{clientid := ClientId, - clean_start := CleanStart, - username := Username, - peername := PeerName, - sockname := SockName, - proto_name := ProtoName, - proto_ver := ProtoVer, - keepalive := Keepalive, - expiry_interval := ExpiryInterval, - conn_props := Properties, - reason_code := Reason, - timestamp := Timestamp + #{ + clientid := ClientId, + clean_start := CleanStart, + username := Username, + peername := PeerName, + sockname := SockName, + proto_name := ProtoName, + proto_ver := ProtoVer, + keepalive := Keepalive, + expiry_interval := ExpiryInterval, + conn_props := Properties, + reason_code := Reason, + timestamp := Timestamp } = Fields, Now = erlang:system_time(millisecond), TimestampElapse = Now - Timestamp, @@ -1783,22 +2619,32 @@ verify_event_fields('client.connack', Fields) -> ?assert(is_boolean(CleanStart)), ?assertEqual(60000, ExpiryInterval), ?assertMatch(#{'Session-Expiry-Interval' := 60}, Properties), - ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60*1000); - + ?assert(0 =< TimestampElapse andalso TimestampElapse =< 60 * 1000); verify_event_fields('client.check_authz_complete', Fields) -> - #{clientid := ClientId, - action := Action, - result := Result, - topic := Topic, - authz_source := AuthzSource, - username := Username - } = Fields, + #{ + clientid := ClientId, + action := Action, + result := Result, + topic := Topic, + authz_source := AuthzSource, + username := Username + } = Fields, ?assertEqual(<<"t1">>, Topic), ?assert(lists:member(Action, [subscribe, publish])), ?assert(lists:member(Result, [allow, deny])), - ?assert(lists:member(AuthzSource, [cache, default, file, - http, mongodb, mysql, redis, - postgresql, built_in_database])), + ?assert( + lists:member(AuthzSource, [ + cache, + default, + file, + http, + mongodb, + mysql, + redis, + postgresql, + built_in_database + ]) + ), ?assert(lists:member(ClientId, [<<"c_event">>, <<"c_event2">>])), ?assert(lists:member(Username, [<<"u_event">>, <<"u_event2">>])). @@ -1807,7 +2653,8 @@ verify_peername(PeerName) -> [IPAddrS, PortS] -> verify_ipaddr(IPAddrS), _ = binary_to_integer(PortS); - _ -> ct:fail({invalid_peername, PeerName}) + _ -> + ct:fail({invalid_peername, PeerName}) end. verify_ipaddr(IPAddrS) -> @@ -1821,24 +2668,31 @@ init_events_counters() -> %%------------------------------------------------------------------------------ deps_path(App, RelativePath) -> Path0 = code:lib_dir(App), - Path = case file:read_link(Path0) of - {ok, Resolved} -> Resolved; - {error, _} -> Path0 - end, + Path = + case file:read_link(Path0) of + {ok, Resolved} -> Resolved; + {error, _} -> Path0 + end, filename:join([Path, RelativePath]). local_path(RelativePath) -> deps_path(emqx_rule_engine, RelativePath). insert_rules(Rules) -> - lists:foreach(fun(Rule) -> - ok = emqx_rule_engine:insert_rule(Rule) - end, Rules). + lists:foreach( + fun(Rule) -> + ok = emqx_rule_engine:insert_rule(Rule) + end, + Rules + ). delete_rules_by_ids(Ids) -> - lists:foreach(fun(Id) -> - ok = emqx_rule_engine:delete_rule(Id) - end, Ids). + lists:foreach( + fun(Id) -> + ok = emqx_rule_engine:delete_rule(Id) + end, + Ids + ). delete_rule(#{id := Id}) -> ok = emqx_rule_engine:delete_rule(Id); diff --git a/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl index 498e34b4b..79a83ebaa 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_engine_api_SUITE.erl @@ -38,15 +38,19 @@ t_crud_rule_api(_Config) -> }, {201, Rule} = emqx_rule_engine_api:'/rules'(post, #{body => Params0}), %% if we post again with the same params, it return with 400 "rule id already exists" - ?assertMatch({400, #{code := _, message := _Message}}, - emqx_rule_engine_api:'/rules'(post, #{body => Params0})), + ?assertMatch( + {400, #{code := _, message := _Message}}, + emqx_rule_engine_api:'/rules'(post, #{body => Params0}) + ), ?assertEqual(RuleID, maps:get(id, Rule)), {200, Rules} = emqx_rule_engine_api:'/rules'(get, #{}), ct:pal("RList : ~p", [Rules]), ?assert(length(Rules) > 0), - {200, Rule0} = emqx_rule_engine_api:'/rules/:id/reset_metrics'(put, #{bindings => #{id => RuleID}}), + {200, Rule0} = emqx_rule_engine_api:'/rules/:id/reset_metrics'(put, #{ + bindings => #{id => RuleID} + }), ?assertEqual(<<"Reset Success">>, Rule0), {200, Rule1} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}), @@ -54,19 +58,26 @@ t_crud_rule_api(_Config) -> ?assertEqual(Rule, Rule1), {200, Rule2} = emqx_rule_engine_api:'/rules/:id'(put, #{ - bindings => #{id => RuleID}, - body => Params0#{<<"sql">> => <<"select * from \"t/b\"">>} - }), + bindings => #{id => RuleID}, + body => Params0#{<<"sql">> => <<"select * from \"t/b\"">>} + }), {200, Rule3} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}), %ct:pal("RShow : ~p", [Rule3]), ?assertEqual(Rule3, Rule2), ?assertEqual(<<"select * from \"t/b\"">>, maps:get(sql, Rule3)), - ?assertMatch({204}, emqx_rule_engine_api:'/rules/:id'(delete, - #{bindings => #{id => RuleID}})), + ?assertMatch( + {204}, + emqx_rule_engine_api:'/rules/:id'( + delete, + #{bindings => #{id => RuleID}} + ) + ), %ct:pal("Show After Deleted: ~p", [NotFound]), - ?assertMatch({404, #{code := _, message := _Message}}, - emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}})), + ?assertMatch( + {404, #{code := _, message := _Message}}, + emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}) + ), ok. diff --git a/apps/emqx_rule_engine/test/emqx_rule_events_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_events_SUITE.erl index 0226066b3..ad8d28159 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_events_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_events_SUITE.erl @@ -9,23 +9,30 @@ all() -> emqx_common_test_helpers:all(?MODULE). t_mod_hook_fun(_) -> Funcs = emqx_rule_events:module_info(exports), - [?assert(lists:keymember(emqx_rule_events:hook_fun(Event), 1, Funcs)) || - Event <- ['client.connected', - 'client.disconnected', - 'session.subscribed', - 'session.unsubscribed', - 'message.acked', - 'message.dropped', - 'message.delivered' - ]]. + [ + ?assert(lists:keymember(emqx_rule_events:hook_fun(Event), 1, Funcs)) + || Event <- [ + 'client.connected', + 'client.disconnected', + 'session.subscribed', + 'session.unsubscribed', + 'message.acked', + 'message.dropped', + 'message.delivered' + ] + ]. t_printable_maps(_) -> - Headers = #{peerhost => {127,0,0,1}, - peername => {{127,0,0,1}, 9980}, - sockname => {{127,0,0,1}, 1883} - }, + Headers = #{ + peerhost => {127, 0, 0, 1}, + peername => {{127, 0, 0, 1}, 9980}, + sockname => {{127, 0, 0, 1}, 1883} + }, ?assertMatch( - #{peerhost := <<"127.0.0.1">>, - peername := <<"127.0.0.1:9980">>, - sockname := <<"127.0.0.1:1883">> - }, emqx_rule_events:printable_maps(Headers)). + #{ + peerhost := <<"127.0.0.1">>, + peername := <<"127.0.0.1:9980">>, + sockname := <<"127.0.0.1:1883">> + }, + emqx_rule_events:printable_maps(Headers) + ). 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 a795e2cda..132f874e4 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_funcs_SUITE.erl @@ -35,7 +35,9 @@ t_msgid(_) -> Msg = message(), ?assertEqual(undefined, apply_func(msgid, [], #{})), - ?assertEqual(emqx_guid:to_hexstr(emqx_message:id(Msg)), apply_func(msgid, [], eventmsg_publish(Msg))). + ?assertEqual( + emqx_guid:to_hexstr(emqx_message:id(Msg)), apply_func(msgid, [], eventmsg_publish(Msg)) + ). t_qos(_) -> ?assertEqual(undefined, apply_func(qos, [], #{})), @@ -61,12 +63,12 @@ t_clientid(_) -> ?assertEqual(<<"clientid">>, apply_func(clientid, [], Msg)). t_clientip(_) -> - Msg = emqx_message:set_header(peerhost, {127,0,0,1}, message()), + Msg = emqx_message:set_header(peerhost, {127, 0, 0, 1}, message()), ?assertEqual(undefined, apply_func(clientip, [], #{})), ?assertEqual(<<"127.0.0.1">>, apply_func(clientip, [], eventmsg_publish(Msg))). t_peerhost(_) -> - Msg = emqx_message:set_header(peerhost, {127,0,0,1}, message()), + Msg = emqx_message:set_header(peerhost, {127, 0, 0, 1}, message()), ?assertEqual(undefined, apply_func(peerhost, [], #{})), ?assertEqual(<<"127.0.0.1">>, apply_func(peerhost, [], eventmsg_publish(Msg))). @@ -87,7 +89,7 @@ t_str(_) -> ?assertEqual(<<"abc">>, emqx_rule_funcs:str("abc")), ?assertEqual(<<"abc">>, emqx_rule_funcs:str(abc)), ?assertEqual(<<"{\"a\":1}">>, emqx_rule_funcs:str(#{a => 1})), - ?assertEqual(<<"[{\"a\":1},{\"b\":1}]">>, emqx_rule_funcs:str([#{a => 1},#{b => 1}])), + ?assertEqual(<<"[{\"a\":1},{\"b\":1}]">>, emqx_rule_funcs:str([#{a => 1}, #{b => 1}])), ?assertEqual(<<"1">>, emqx_rule_funcs:str(1)), ?assertEqual(<<"2.0">>, emqx_rule_funcs:str(2.0)), ?assertEqual(<<"true">>, emqx_rule_funcs:str(true)), @@ -97,7 +99,9 @@ t_str(_) -> ?assertEqual(<<"abc 你好"/utf8>>, emqx_rule_funcs:str_utf8("abc 你好")), ?assertEqual(<<"abc 你好"/utf8>>, emqx_rule_funcs:str_utf8(<<"abc 你好"/utf8>>)), ?assertEqual(<<"abc">>, emqx_rule_funcs:str_utf8(abc)), - ?assertEqual(<<"{\"a\":\"abc 你好\"}"/utf8>>, emqx_rule_funcs:str_utf8(#{a => <<"abc 你好"/utf8>>})), + ?assertEqual( + <<"{\"a\":\"abc 你好\"}"/utf8>>, emqx_rule_funcs:str_utf8(#{a => <<"abc 你好"/utf8>>}) + ), ?assertEqual(<<"1">>, emqx_rule_funcs:str_utf8(1)), ?assertEqual(<<"2.0">>, emqx_rule_funcs:str_utf8(2.0)), ?assertEqual(<<"true">>, emqx_rule_funcs:str_utf8(true)), @@ -126,7 +130,9 @@ t_float(_) -> ?assertError(_, emqx_rule_funcs:float("a")). t_map(_) -> - ?assertEqual(#{ver => <<"1.0">>, name => "emqx"}, emqx_rule_funcs:map([{ver, <<"1.0">>}, {name, "emqx"}])), + ?assertEqual( + #{ver => <<"1.0">>, name => "emqx"}, emqx_rule_funcs:map([{ver, <<"1.0">>}, {name, "emqx"}]) + ), ?assertEqual(#{<<"a">> => 1}, emqx_rule_funcs:map(<<"{\"a\":1}">>)), ?assertError(_, emqx_rule_funcs:map(<<"a">>)), ?assertError(_, emqx_rule_funcs:map("a")), @@ -151,31 +157,42 @@ t_proc_dict_put_get_del(_) -> ?assertEqual(undefined, emqx_rule_funcs:proc_dict_get(<<"abc">>)). t_term_encode(_) -> - TestData = [<<"abc">>, #{a => 1}, #{<<"3">> => [1,2,4]}], - lists:foreach(fun(Data) -> - ?assertEqual(Data, + TestData = [<<"abc">>, #{a => 1}, #{<<"3">> => [1, 2, 4]}], + lists:foreach( + fun(Data) -> + ?assertEqual( + Data, emqx_rule_funcs:term_decode( - emqx_rule_funcs:term_encode(Data))) - end, TestData). + emqx_rule_funcs:term_encode(Data) + ) + ) + end, + TestData + ). t_hexstr2bin(_) -> - ?assertEqual(<<1,2>>, emqx_rule_funcs:hexstr2bin(<<"0102">>)), - ?assertEqual(<<17,33>>, emqx_rule_funcs:hexstr2bin(<<"1121">>)). + ?assertEqual(<<1, 2>>, emqx_rule_funcs:hexstr2bin(<<"0102">>)), + ?assertEqual(<<17, 33>>, emqx_rule_funcs:hexstr2bin(<<"1121">>)). t_bin2hexstr(_) -> - ?assertEqual(<<"0102">>, emqx_rule_funcs:bin2hexstr(<<1,2>>)), - ?assertEqual(<<"1121">>, emqx_rule_funcs:bin2hexstr(<<17,33>>)). + ?assertEqual(<<"0102">>, emqx_rule_funcs:bin2hexstr(<<1, 2>>)), + ?assertEqual(<<"1121">>, emqx_rule_funcs:bin2hexstr(<<17, 33>>)). t_hex_convert(_) -> ?PROPTEST(hex_convert). hex_convert() -> - ?FORALL(L, list(range(0, 255)), - begin - AbitraryBin = list_to_binary(L), - AbitraryBin == emqx_rule_funcs:hexstr2bin( - emqx_rule_funcs:bin2hexstr(AbitraryBin)) - end). + ?FORALL( + L, + list(range(0, 255)), + begin + AbitraryBin = list_to_binary(L), + AbitraryBin == + emqx_rule_funcs:hexstr2bin( + emqx_rule_funcs:bin2hexstr(AbitraryBin) + ) + end + ). t_is_null(_) -> ?assertEqual(true, emqx_rule_funcs:is_null(undefined)), @@ -184,50 +201,80 @@ t_is_null(_) -> ?assertEqual(false, emqx_rule_funcs:is_null(<<"a">>)). t_is_not_null(_) -> - [?assertEqual(emqx_rule_funcs:is_not_null(T), not emqx_rule_funcs:is_null(T)) - || T <- [undefined, a, <<"a">>, <<>>]]. + [ + ?assertEqual(emqx_rule_funcs:is_not_null(T), not emqx_rule_funcs:is_null(T)) + || T <- [undefined, a, <<"a">>, <<>>] + ]. t_is_str(_) -> - [?assertEqual(true, emqx_rule_funcs:is_str(T)) - || T <- [<<"a">>, <<>>, <<"abc">>]], - [?assertEqual(false, emqx_rule_funcs:is_str(T)) - || T <- ["a", a, 1]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_str(T)) + || T <- [<<"a">>, <<>>, <<"abc">>] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_str(T)) + || T <- ["a", a, 1] + ]. t_is_bool(_) -> - [?assertEqual(true, emqx_rule_funcs:is_bool(T)) - || T <- [true, false]], - [?assertEqual(false, emqx_rule_funcs:is_bool(T)) - || T <- ["a", <<>>, a, 2]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_bool(T)) + || T <- [true, false] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_bool(T)) + || T <- ["a", <<>>, a, 2] + ]. t_is_int(_) -> - [?assertEqual(true, emqx_rule_funcs:is_int(T)) - || T <- [1, 2, -1]], - [?assertEqual(false, emqx_rule_funcs:is_int(T)) - || T <- [1.1, "a", a]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_int(T)) + || T <- [1, 2, -1] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_int(T)) + || T <- [1.1, "a", a] + ]. t_is_float(_) -> - [?assertEqual(true, emqx_rule_funcs:is_float(T)) - || T <- [1.1, 2.0, -1.2]], - [?assertEqual(false, emqx_rule_funcs:is_float(T)) - || T <- [1, "a", a, <<>>]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_float(T)) + || T <- [1.1, 2.0, -1.2] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_float(T)) + || T <- [1, "a", a, <<>>] + ]. t_is_num(_) -> - [?assertEqual(true, emqx_rule_funcs:is_num(T)) - || T <- [1.1, 2.0, -1.2, 1]], - [?assertEqual(false, emqx_rule_funcs:is_num(T)) - || T <- ["a", a, <<>>]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_num(T)) + || T <- [1.1, 2.0, -1.2, 1] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_num(T)) + || T <- ["a", a, <<>>] + ]. t_is_map(_) -> - [?assertEqual(true, emqx_rule_funcs:is_map(T)) - || T <- [#{}, #{a =>1}]], - [?assertEqual(false, emqx_rule_funcs:is_map(T)) - || T <- ["a", a, <<>>]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_map(T)) + || T <- [#{}, #{a => 1}] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_map(T)) + || T <- ["a", a, <<>>] + ]. t_is_array(_) -> - [?assertEqual(true, emqx_rule_funcs:is_array(T)) - || T <- [[], [1,2]]], - [?assertEqual(false, emqx_rule_funcs:is_array(T)) - || T <- [<<>>, a]]. + [ + ?assertEqual(true, emqx_rule_funcs:is_array(T)) + || T <- [[], [1, 2]] + ], + [ + ?assertEqual(false, emqx_rule_funcs:is_array(T)) + || T <- [<<>>, a] + ]. %%------------------------------------------------------------------------------ %% Test cases for arith op @@ -237,22 +284,30 @@ t_arith_op(_) -> ?PROPTEST(prop_arith_op). prop_arith_op() -> - ?FORALL({X, Y}, {number(), number()}, - begin - (X + Y) == apply_func('+', [X, Y]) andalso + ?FORALL( + {X, Y}, + {number(), number()}, + begin + (X + Y) == apply_func('+', [X, Y]) andalso (X - Y) == apply_func('-', [X, Y]) andalso (X * Y) == apply_func('*', [X, Y]) andalso - (if Y =/= 0 -> + (if + Y =/= 0 -> (X / Y) == apply_func('/', [X, Y]); - true -> true - end) andalso - (case is_integer(X) - andalso is_pos_integer(Y) of - true -> - (X rem Y) == apply_func('mod', [X, Y]); - false -> true + true -> + true + end) andalso + (case + is_integer(X) andalso + is_pos_integer(Y) + of + true -> + (X rem Y) == apply_func('mod', [X, Y]); + false -> + true end) - end). + end + ). is_pos_integer(X) -> is_integer(X) andalso X > 0. @@ -266,29 +321,45 @@ t_math_fun(_) -> prop_math_fun() -> Excluded = [module_info, atanh, asin, acos], - MathFuns = [{F, A} || {F, A} <- math:module_info(exports), - not lists:member(F, Excluded), - erlang:function_exported(emqx_rule_funcs, F, A)], - ?FORALL({X, Y}, {pos_integer(), pos_integer()}, - begin - lists:foldl(fun({F, 1}, True) -> - True andalso comp_with_math(F, X); - ({F = fmod, 2}, True) -> - True andalso (if Y =/= 0 -> - comp_with_math(F, X, Y); - true -> true - end); - ({F, 2}, True) -> - True andalso comp_with_math(F, X, Y) - end, true, MathFuns) - end). + MathFuns = [ + {F, A} + || {F, A} <- math:module_info(exports), + not lists:member(F, Excluded), + erlang:function_exported(emqx_rule_funcs, F, A) + ], + ?FORALL( + {X, Y}, + {pos_integer(), pos_integer()}, + begin + lists:foldl( + fun + ({F, 1}, True) -> + True andalso comp_with_math(F, X); + ({F = fmod, 2}, True) -> + True andalso + (if + Y =/= 0 -> + comp_with_math(F, X, Y); + true -> + true + end); + ({F, 2}, True) -> + True andalso comp_with_math(F, X, Y) + end, + true, + MathFuns + ) + end + ). -comp_with_math(Fun, X) - when Fun =:= exp; - Fun =:= sinh; - Fun =:= cosh -> - if X < 710 -> math:Fun(X) == apply_func(Fun, [X]); - true -> true +comp_with_math(Fun, X) when + Fun =:= exp; + Fun =:= sinh; + Fun =:= cosh +-> + if + X < 710 -> math:Fun(X) == apply_func(Fun, [X]); + true -> true end; comp_with_math(F, X) -> math:F(X) == apply_func(F, [X]). @@ -304,15 +375,18 @@ t_bits_op(_) -> ?PROPTEST(prop_bits_op). prop_bits_op() -> - ?FORALL({X, Y}, {integer(), integer()}, - begin - (bnot X) == apply_func(bitnot, [X]) andalso + ?FORALL( + {X, Y}, + {integer(), integer()}, + begin + (bnot X) == apply_func(bitnot, [X]) andalso (X band Y) == apply_func(bitand, [X, Y]) andalso (X bor Y) == apply_func(bitor, [X, Y]) andalso (X bxor Y) == apply_func(bitxor, [X, Y]) andalso (X bsl Y) == apply_func(bitsl, [X, Y]) andalso (X bsr Y) == apply_func(bitsr, [X, Y]) - end). + end + ). %%------------------------------------------------------------------------------ %% Test cases for string @@ -346,77 +420,140 @@ t_trim(_) -> t_split_all(_) -> ?assertEqual([], apply_func(split, [<<>>, <<"/">>])), ?assertEqual([], apply_func(split, [<<"/">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(split, [<<"/a/b//c/">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(split, [<<"a,b,c">>, <<",">>])), - ?assertEqual([<<"a">>,<<" b ">>,<<"c">>], apply_func(split, [<<"a, b ,c">>, <<",">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c\r\n">>], apply_func(split, [<<"a,b,c\r\n">>, <<",">>])). + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(split, [<<"/a/b//c/">>, <<"/">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(split, [<<"a,b,c">>, <<",">>])), + ?assertEqual([<<"a">>, <<" b ">>, <<"c">>], apply_func(split, [<<"a, b ,c">>, <<",">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c\r\n">>], apply_func(split, [<<"a,b,c\r\n">>, <<",">>])). t_split_notrim_all(_) -> ?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"notrim">>])), - ?assertEqual([<<>>,<<>>], apply_func(split, [<<"/">>, <<"/">>, <<"notrim">>])), - ?assertEqual([<<>>, <<"a">>,<<"b">>,<<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"notrim">>])), - ?assertEqual([<<>>, <<"a">>,<<"b">>, <<>>, <<"c">>, <<>>], apply_func(split, [<<"/a/b//c/">>, <<"/">>, <<"notrim">>])), - ?assertEqual([<<>>, <<"a">>,<<"b">>,<<"c\n">>], apply_func(split, [<<",a,b,c\n">>, <<",">>, <<"notrim">>])), - ?assertEqual([<<"a">>,<<" b">>,<<"c\r\n">>], apply_func(split, [<<"a, b,c\r\n">>, <<",">>, <<"notrim">>])), - ?assertEqual([<<"哈哈"/utf8>>,<<" 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"notrim">>])). + ?assertEqual([<<>>, <<>>], apply_func(split, [<<"/">>, <<"/">>, <<"notrim">>])), + ?assertEqual( + [<<>>, <<"a">>, <<"b">>, <<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"notrim">>]) + ), + ?assertEqual( + [<<>>, <<"a">>, <<"b">>, <<>>, <<"c">>, <<>>], + apply_func(split, [<<"/a/b//c/">>, <<"/">>, <<"notrim">>]) + ), + ?assertEqual( + [<<>>, <<"a">>, <<"b">>, <<"c\n">>], + apply_func(split, [<<",a,b,c\n">>, <<",">>, <<"notrim">>]) + ), + ?assertEqual( + [<<"a">>, <<" b">>, <<"c\r\n">>], + apply_func(split, [<<"a, b,c\r\n">>, <<",">>, <<"notrim">>]) + ), + ?assertEqual( + [<<"哈哈"/utf8>>, <<" 你好"/utf8>>, <<" 是的\r\n"/utf8>>], + apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"notrim">>]) + ). t_split_leading(_) -> ?assertEqual([], apply_func(split, [<<>>, <<"/">>, <<"leading">>])), ?assertEqual([], apply_func(split, [<<"/">>, <<"/">>, <<"leading">>])), ?assertEqual([<<"a/b/c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"leading">>])), - ?assertEqual([<<"a">>,<<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading">>])), - ?assertEqual([<<"a">>,<<"b,c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"leading">>])), - ?assertEqual([<<"a b">>,<<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"leading">>])), - ?assertEqual([<<"哈哈"/utf8>>,<<" 你好, 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"leading">>])). + ?assertEqual( + [<<"a">>, <<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading">>]) + ), + ?assertEqual( + [<<"a">>, <<"b,c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"leading">>]) + ), + ?assertEqual( + [<<"a b">>, <<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"leading">>]) + ), + ?assertEqual( + [<<"哈哈"/utf8>>, <<" 你好, 是的\r\n"/utf8>>], + apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"leading">>]) + ). t_split_leading_notrim(_) -> ?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"leading_notrim">>])), - ?assertEqual([<<>>,<<>>], apply_func(split, [<<"/">>, <<"/">>, <<"leading_notrim">>])), - ?assertEqual([<<>>, <<"a/b/c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"leading_notrim">>])), - ?assertEqual([<<"a">>,<<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading_notrim">>])), - ?assertEqual([<<"a">>,<<"b,c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"leading_notrim">>])), - ?assertEqual([<<"a b">>,<<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"leading_notrim">>])), - ?assertEqual([<<"哈哈"/utf8>>,<<" 你好, 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"leading_notrim">>])). + ?assertEqual([<<>>, <<>>], apply_func(split, [<<"/">>, <<"/">>, <<"leading_notrim">>])), + ?assertEqual( + [<<>>, <<"a/b/c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"leading_notrim">>]) + ), + ?assertEqual( + [<<"a">>, <<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading_notrim">>]) + ), + ?assertEqual( + [<<"a">>, <<"b,c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"leading_notrim">>]) + ), + ?assertEqual( + [<<"a b">>, <<"c\r\n">>], + apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"leading_notrim">>]) + ), + ?assertEqual( + [<<"哈哈"/utf8>>, <<" 你好, 是的\r\n"/utf8>>], + apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"leading_notrim">>]) + ). t_split_trailing(_) -> ?assertEqual([], apply_func(split, [<<>>, <<"/">>, <<"trailing">>])), ?assertEqual([], apply_func(split, [<<"/">>, <<"/">>, <<"trailing">>])), ?assertEqual([<<"/a/b">>, <<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"trailing">>])), ?assertEqual([<<"a/b//c">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"trailing">>])), - ?assertEqual([<<"a,b">>,<<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"trailing">>])), - ?assertEqual([<<"a b">>,<<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"trailing">>])), - ?assertEqual([<<"哈哈, 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"trailing">>])). + ?assertEqual( + [<<"a,b">>, <<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"trailing">>]) + ), + ?assertEqual( + [<<"a b">>, <<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"trailing">>]) + ), + ?assertEqual( + [<<"哈哈, 你好"/utf8>>, <<" 是的\r\n"/utf8>>], + apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"trailing">>]) + ). t_split_trailing_notrim(_) -> ?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"trailing_notrim">>])), ?assertEqual([<<>>, <<>>], apply_func(split, [<<"/">>, <<"/">>, <<"trailing_notrim">>])), - ?assertEqual([<<"/a/b">>, <<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"trailing_notrim">>])), - ?assertEqual([<<"a/b//c">>, <<>>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"trailing_notrim">>])), - ?assertEqual([<<"a,b">>,<<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"trailing_notrim">>])), - ?assertEqual([<<"a b">>,<<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"trailing_notrim">>])), - ?assertEqual([<<"哈哈, 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"trailing_notrim">>])). + ?assertEqual( + [<<"/a/b">>, <<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"trailing_notrim">>]) + ), + ?assertEqual( + [<<"a/b//c">>, <<>>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"trailing_notrim">>]) + ), + ?assertEqual( + [<<"a,b">>, <<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"trailing_notrim">>]) + ), + ?assertEqual( + [<<"a b">>, <<"c\r\n">>], + apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"trailing_notrim">>]) + ), + ?assertEqual( + [<<"哈哈, 你好"/utf8>>, <<" 是的\r\n"/utf8>>], + apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"trailing_notrim">>]) + ). t_tokens(_) -> ?assertEqual([], apply_func(tokens, [<<>>, <<"/">>])), ?assertEqual([], apply_func(tokens, [<<"/">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"/a/b/c">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"/a/b//c/">>, <<"/">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<" /a/ b /c">>, <<" /">>])), - ?assertEqual([<<"a">>,<<"\nb">>,<<"c\n">>], apply_func(tokens, [<<"a ,\nb,c\n">>, <<", ">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c\r\n">>], apply_func(tokens, [<<"a ,b,c\r\n">>, <<", ">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"a,b, c\n">>, <<", ">>, <<"nocrlf">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"a,b,c\r\n">>, <<",">>, <<"nocrlf">>])), - ?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"a,b\r\n,c\n">>, <<",">>, <<"nocrlf">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"/a/b/c">>, <<"/">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"/a/b//c/">>, <<"/">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<" /a/ b /c">>, <<" /">>])), + ?assertEqual([<<"a">>, <<"\nb">>, <<"c\n">>], apply_func(tokens, [<<"a ,\nb,c\n">>, <<", ">>])), + ?assertEqual([<<"a">>, <<"b">>, <<"c\r\n">>], apply_func(tokens, [<<"a ,b,c\r\n">>, <<", ">>])), + ?assertEqual( + [<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"a,b, c\n">>, <<", ">>, <<"nocrlf">>]) + ), + ?assertEqual( + [<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"a,b,c\r\n">>, <<",">>, <<"nocrlf">>]) + ), + ?assertEqual( + [<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"a,b\r\n,c\n">>, <<",">>, <<"nocrlf">>]) + ), ?assertEqual([], apply_func(tokens, [<<"\r\n">>, <<",">>, <<"nocrlf">>])), ?assertEqual([], apply_func(tokens, [<<"\r\n">>, <<",">>, <<"nocrlf">>])), - ?assertEqual([<<"哈哈"/utf8>>,<<"你好"/utf8>>,<<"是的"/utf8>>], apply_func(tokens, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<", ">>, <<"nocrlf">>])). + ?assertEqual( + [<<"哈哈"/utf8>>, <<"你好"/utf8>>, <<"是的"/utf8>>], + apply_func(tokens, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<", ">>, <<"nocrlf">>]) + ). t_concat(_) -> ?assertEqual(<<"ab">>, apply_func(concat, [<<"a">>, <<"b">>])), ?assertEqual(<<"ab">>, apply_func('+', [<<"a">>, <<"b">>])), - ?assertEqual(<<"哈哈你好"/utf8>>, apply_func(concat, [<<"哈哈"/utf8>>,<<"你好"/utf8>>])), + ?assertEqual(<<"哈哈你好"/utf8>>, apply_func(concat, [<<"哈哈"/utf8>>, <<"你好"/utf8>>])), ?assertEqual(<<"abc">>, apply_func(concat, [apply_func(concat, [<<"a">>, <<"b">>]), <<"c">>])), ?assertEqual(<<"a">>, apply_func(concat, [<<"">>, <<"a">>])), ?assertEqual(<<"a">>, apply_func(concat, [<<"a">>, <<"">>])), @@ -424,8 +561,13 @@ t_concat(_) -> t_sprintf(_) -> ?assertEqual(<<"Hello Shawn!">>, apply_func(sprintf, [<<"Hello ~ts!">>, <<"Shawn">>])), - ?assertEqual(<<"Name: ABC, Count: 2">>, apply_func(sprintf, [<<"Name: ~ts, Count: ~p">>, <<"ABC">>, 2])), - ?assertEqual(<<"Name: ABC, Count: 2, Status: {ok,running}">>, apply_func(sprintf, [<<"Name: ~ts, Count: ~p, Status: ~p">>, <<"ABC">>, 2, {ok, running}])). + ?assertEqual( + <<"Name: ABC, Count: 2">>, apply_func(sprintf, [<<"Name: ~ts, Count: ~p">>, <<"ABC">>, 2]) + ), + ?assertEqual( + <<"Name: ABC, Count: 2, Status: {ok,running}">>, + apply_func(sprintf, [<<"Name: ~ts, Count: ~p, Status: ~p">>, <<"ABC">>, 2, {ok, running}]) + ). t_pad(_) -> ?assertEqual(<<"abc ">>, apply_func(pad, [<<"abc">>, 5])), @@ -449,8 +591,12 @@ t_replace(_) -> ?assertEqual(<<"ab-c--">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>])), ?assertEqual(<<"ab::c::::">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"::">>])), ?assertEqual(<<"ab-c--">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"all">>])), - ?assertEqual(<<"ab-c ">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"leading">>])), - ?assertEqual(<<"ab c -">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"trailing">>])). + ?assertEqual( + <<"ab-c ">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"leading">>]) + ), + ?assertEqual( + <<"ab c -">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"trailing">>]) + ). t_ascii(_) -> ?assertEqual(97, apply_func(ascii, [<<"a">>])), @@ -483,7 +629,7 @@ t_regex_replace(_) -> ?assertEqual(<<"aebed">>, apply_func(regex_replace, [<<"accbcd">>, <<"c+">>, <<"e">>])), ?assertEqual(<<"a[cc]b[c]d">>, apply_func(regex_replace, [<<"accbcd">>, <<"c+">>, <<"[&]">>])). -ascii_string() -> list(range(0,127)). +ascii_string() -> list(range(0, 127)). bin(S) -> iolist_to_binary(S). @@ -492,34 +638,34 @@ bin(S) -> iolist_to_binary(S). %%------------------------------------------------------------------------------ t_nth(_) -> - ?assertEqual(2, apply_func(nth, [2, [1,2,3,4]])), - ?assertEqual(4, apply_func(nth, [4, [1,2,3,4]])). + ?assertEqual(2, apply_func(nth, [2, [1, 2, 3, 4]])), + ?assertEqual(4, apply_func(nth, [4, [1, 2, 3, 4]])). t_length(_) -> - ?assertEqual(4, apply_func(length, [[1,2,3,4]])), + ?assertEqual(4, apply_func(length, [[1, 2, 3, 4]])), ?assertEqual(0, apply_func(length, [[]])). t_slice(_) -> - ?assertEqual([1,2,3,4], apply_func(sublist, [4, [1,2,3,4]])), - ?assertEqual([1,2], apply_func(sublist, [2, [1,2,3,4]])), - ?assertEqual([4], apply_func(sublist, [4, 1, [1,2,3,4]])), - ?assertEqual([4], apply_func(sublist, [4, 2, [1,2,3,4]])), - ?assertEqual([], apply_func(sublist, [5, 2, [1,2,3,4]])), - ?assertEqual([2,3], apply_func(sublist, [2, 2, [1,2,3,4]])), - ?assertEqual([1], apply_func(sublist, [1, 1, [1,2,3,4]])). + ?assertEqual([1, 2, 3, 4], apply_func(sublist, [4, [1, 2, 3, 4]])), + ?assertEqual([1, 2], apply_func(sublist, [2, [1, 2, 3, 4]])), + ?assertEqual([4], apply_func(sublist, [4, 1, [1, 2, 3, 4]])), + ?assertEqual([4], apply_func(sublist, [4, 2, [1, 2, 3, 4]])), + ?assertEqual([], apply_func(sublist, [5, 2, [1, 2, 3, 4]])), + ?assertEqual([2, 3], apply_func(sublist, [2, 2, [1, 2, 3, 4]])), + ?assertEqual([1], apply_func(sublist, [1, 1, [1, 2, 3, 4]])). t_first_last(_) -> - ?assertEqual(1, apply_func(first, [[1,2,3,4]])), - ?assertEqual(4, apply_func(last, [[1,2,3,4]])). + ?assertEqual(1, apply_func(first, [[1, 2, 3, 4]])), + ?assertEqual(4, apply_func(last, [[1, 2, 3, 4]])). t_contains(_) -> - ?assertEqual(true, apply_func(contains, [1, [1,2,3,4]])), - ?assertEqual(true, apply_func(contains, [3, [1,2,3,4]])), - ?assertEqual(true, apply_func(contains, [<<"a">>, [<<>>,<<"ab">>,3,<<"a">>]])), - ?assertEqual(true, apply_func(contains, [#{a=>b}, [#{a=>1}, #{a=>b}]])), - ?assertEqual(false, apply_func(contains, [#{a=>b}, [#{a=>1}]])), + ?assertEqual(true, apply_func(contains, [1, [1, 2, 3, 4]])), + ?assertEqual(true, apply_func(contains, [3, [1, 2, 3, 4]])), + ?assertEqual(true, apply_func(contains, [<<"a">>, [<<>>, <<"ab">>, 3, <<"a">>]])), + ?assertEqual(true, apply_func(contains, [#{a => b}, [#{a => 1}, #{a => b}]])), + ?assertEqual(false, apply_func(contains, [#{a => b}, [#{a => 1}]])), ?assertEqual(false, apply_func(contains, [3, [1, 2]])), - ?assertEqual(false, apply_func(contains, [<<"c">>, [<<>>,<<"ab">>,3,<<"a">>]])). + ?assertEqual(false, apply_func(contains, [<<"c">>, [<<>>, <<"ab">>, 3, <<"a">>]])). t_map_get(_) -> ?assertEqual(1, apply_func(map_get, [<<"a">>, #{a => 1}])), @@ -532,7 +678,9 @@ t_map_put(_) -> ?assertEqual(#{<<"a">> => 1}, apply_func(map_put, [<<"a">>, 1, #{}])), ?assertEqual(#{a => 2}, apply_func(map_put, [<<"a">>, 2, #{a => 1}])), ?assertEqual(#{<<"a">> => #{<<"b">> => 1}}, apply_func(map_put, [<<"a.b">>, 1, #{}])), - ?assertEqual(#{a => #{b => 1, <<"c">> => 1}}, apply_func(map_put, [<<"a.c">>, 1, #{a => #{b => 1}}])), + ?assertEqual( + #{a => #{b => 1, <<"c">> => 1}}, apply_func(map_put, [<<"a.c">>, 1, #{a => #{b => 1}}]) + ), ?assertEqual(#{a => 2}, apply_func(map_put, [<<"a">>, 2, #{a => 1}])). t_mget(_) -> @@ -579,20 +727,26 @@ t_subbits2_1(_) -> ?assertEqual(127, apply_func(subbits, [<<255:8>>, 2, 7])), ?assertEqual(127, apply_func(subbits, [<<255:8>>, 2, 8])). t_subbits2_integer(_) -> - ?assertEqual(456, apply_func(subbits, [<<456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>])), - ?assertEqual(-456, apply_func(subbits, [<<-456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>])). + ?assertEqual( + 456, + apply_func(subbits, [<<456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>]) + ), + ?assertEqual( + -456, + apply_func(subbits, [<<-456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>]) + ). t_subbits2_float(_) -> R = apply_func(subbits, [<<5.3:64/float>>, 1, 64, <<"float">>, <<"unsigned">>, <<"big">>]), RL = (5.3 - R), ct:pal(";;;;~p", [R]), - ?assert( (RL >= 0 andalso RL < 0.0001) orelse (RL =< 0 andalso RL > -0.0001)), + ?assert((RL >= 0 andalso RL < 0.0001) orelse (RL =< 0 andalso RL > -0.0001)), R2 = apply_func(subbits, [<<-5.3:64/float>>, 1, 64, <<"float">>, <<"signed">>, <<"big">>]), RL2 = (5.3 + R2), ct:pal(";;;;~p", [R2]), - ?assert( (RL2 >= 0 andalso RL2 < 0.0001) orelse (RL2 =< 0 andalso RL2 > -0.0001)). + ?assert((RL2 >= 0 andalso RL2 < 0.0001) orelse (RL2 =< 0 andalso RL2 > -0.0001)). %%------------------------------------------------------------------------------ %% Test cases for Hash funcs @@ -602,12 +756,15 @@ t_hash_funcs(_) -> ?PROPTEST(prop_hash_fun). prop_hash_fun() -> - ?FORALL(S, binary(), - begin - (32 == byte_size(apply_func(md5, [S]))) andalso + ?FORALL( + S, + binary(), + begin + (32 == byte_size(apply_func(md5, [S]))) andalso (40 == byte_size(apply_func(sha, [S]))) andalso (64 == byte_size(apply_func(sha256, [S]))) - end). + end + ). %%------------------------------------------------------------------------------ %% Test cases for base64 @@ -617,72 +774,131 @@ t_base64_encode(_) -> ?PROPTEST(prop_base64_encode). prop_base64_encode() -> - ?FORALL(S, list(range(0, 255)), - begin - Bin = iolist_to_binary(S), - Bin == base64:decode(apply_func(base64_encode, [Bin])) - end). + ?FORALL( + S, + list(range(0, 255)), + begin + Bin = iolist_to_binary(S), + Bin == base64:decode(apply_func(base64_encode, [Bin])) + end + ). %%-------------------------------------------------------------------- %% Date functions %%-------------------------------------------------------------------- t_now_rfc3339(_) -> - ?assert(is_integer( - calendar:rfc3339_to_system_time( - binary_to_list(apply_func(now_rfc3339, []))))). + ?assert( + is_integer( + calendar:rfc3339_to_system_time( + binary_to_list(apply_func(now_rfc3339, [])) + ) + ) + ). t_now_rfc3339_1(_) -> - [?assert(is_integer( - calendar:rfc3339_to_system_time( - binary_to_list(apply_func(now_rfc3339, [atom_to_binary(Unit, utf8)])), - [{unit, Unit}]))) - || Unit <- [second,millisecond,microsecond,nanosecond]]. + [ + ?assert( + is_integer( + calendar:rfc3339_to_system_time( + binary_to_list(apply_func(now_rfc3339, [atom_to_binary(Unit, utf8)])), + [{unit, Unit}] + ) + ) + ) + || Unit <- [second, millisecond, microsecond, nanosecond] + ]. t_now_timestamp(_) -> ?assert(is_integer(apply_func(now_timestamp, []))). t_now_timestamp_1(_) -> - [?assert(is_integer( - apply_func(now_timestamp, [atom_to_binary(Unit, utf8)]))) - || Unit <- [second,millisecond,microsecond,nanosecond]]. + [ + ?assert( + is_integer( + apply_func(now_timestamp, [atom_to_binary(Unit, utf8)]) + ) + ) + || Unit <- [second, millisecond, microsecond, nanosecond] + ]. t_unix_ts_to_rfc3339(_) -> - [begin - BUnit = atom_to_binary(Unit, utf8), - Epoch = apply_func(now_timestamp, [BUnit]), - DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]), - ?assertEqual(Epoch, - calendar:rfc3339_to_system_time(binary_to_list(DateTime), [{unit, Unit}])) - end || Unit <- [second,millisecond,microsecond,nanosecond]]. + [ + begin + BUnit = atom_to_binary(Unit, utf8), + Epoch = apply_func(now_timestamp, [BUnit]), + DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]), + ?assertEqual( + Epoch, + calendar:rfc3339_to_system_time(binary_to_list(DateTime), [{unit, Unit}]) + ) + end + || Unit <- [second, millisecond, microsecond, nanosecond] + ]. t_rfc3339_to_unix_ts(_) -> - [begin - BUnit = atom_to_binary(Unit, utf8), - Epoch = apply_func(now_timestamp, [BUnit]), - DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]), - ?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit)) - end || Unit <- [second,millisecond,microsecond,nanosecond]]. + [ + begin + BUnit = atom_to_binary(Unit, utf8), + Epoch = apply_func(now_timestamp, [BUnit]), + DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]), + ?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])])), + ?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])])), + ?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])])). + ?FORALL( + S, + erlang:system_time(second), + S == + apply_func( + date_to_unix_ts, + Args ++ + [ + apply_func( + format_date, + Args ++ [S] + ) + ] + ) + ). %%------------------------------------------------------------------------------ %% Utility functions @@ -699,8 +915,10 @@ apply_func(Name, Args, Msg) -> apply_func(Name, Args, emqx_message:to_map(Msg)). message() -> - emqx_message:set_flags(#{dup => false}, - emqx_message:make(<<"clientid">>, 1, <<"topic/#">>, <<"payload">>)). + emqx_message:set_flags( + #{dup => false}, + emqx_message:make(<<"clientid">>, 1, <<"topic/#">>, <<"payload">>) + ). % t_contains_topic(_) -> % error('TODO'). @@ -831,13 +1049,15 @@ message() -> % t_json_decode(_) -> % error('TODO'). - %%------------------------------------------------------------------------------ %% CT functions %%------------------------------------------------------------------------------ all() -> - IsTestCase = fun("t_" ++ _) -> true; (_) -> false end, + IsTestCase = fun + ("t_" ++ _) -> true; + (_) -> false + end, [F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))]. suite() -> diff --git a/apps/emqx_rule_engine/test/emqx_rule_maps_SUITE.erl b/apps/emqx_rule_engine/test/emqx_rule_maps_SUITE.erl index a25145acd..965173b08 100644 --- a/apps/emqx_rule_engine/test/emqx_rule_maps_SUITE.erl +++ b/apps/emqx_rule_engine/test/emqx_rule_maps_SUITE.erl @@ -22,20 +22,27 @@ -compile(export_all). -compile(nowarn_export_all). --import(emqx_rule_maps, - [ nested_get/2 - , nested_get/3 - , nested_put/3 - , atom_key_map/1 - ]). +-import( + emqx_rule_maps, + [ + nested_get/2, + nested_get/3, + nested_put/3, + atom_key_map/1 + ] +). --define(path(Path), {path, - [case K of - {ic, Key} -> {index, {const, Key}}; - {iv, Key} -> {index, {var, Key}}; - {i, Path1} -> {index, Path1}; - _ -> {key, K} - end || K <- Path]}). +-define(path(Path), + {path, [ + case K of + {ic, Key} -> {index, {const, Key}}; + {iv, Key} -> {index, {var, Key}}; + {i, Path1} -> {index, Path1}; + _ -> {key, K} + end + || K <- Path + ]} +). -define(PROPTEST(Prop), true = proper:quickcheck(Prop)). @@ -44,8 +51,8 @@ t_nested_put_map(_) -> ?assertEqual(#{a => a}, nested_put(?path([a]), a, #{})), ?assertEqual(#{a => undefined}, nested_put(?path([a]), undefined, #{})), ?assertEqual(#{a => 1}, nested_put(?path([a]), 1, not_map)), - ?assertEqual(#{a => #{b => b}}, nested_put(?path([a,b]), b, #{})), - ?assertEqual(#{a => #{b => #{c => c}}}, nested_put(?path([a,b,c]), c, #{})), + ?assertEqual(#{a => #{b => b}}, nested_put(?path([a, b]), b, #{})), + ?assertEqual(#{a => #{b => #{c => c}}}, nested_put(?path([a, b, c]), c, #{})), ?assertEqual(#{<<"k">> => v1}, nested_put(?path([k]), v1, #{<<"k">> => v0})), ?assertEqual(#{k => v1}, nested_put(?path([k]), v1, #{k => v0})), ?assertEqual(#{<<"k">> => v1, a => b}, nested_put(?path([k]), v1, #{<<"k">> => v0, a => b})), @@ -53,122 +60,194 @@ t_nested_put_map(_) -> ?assertEqual(#{k => v1}, nested_put(?path([k]), v1, #{k => v0})), ?assertEqual(#{k => v1, a => b}, nested_put(?path([k]), v1, #{k => v0, a => b})), ?assertEqual(#{<<"k">> => v1, a => b}, nested_put(?path([k]), v1, #{<<"k">> => v0, a => b})), - ?assertEqual(#{<<"k">> => #{<<"t">> => v1}}, nested_put(?path([k,t]), v1, #{<<"k">> => #{<<"t">> => v0}})), - ?assertEqual(#{<<"k">> => #{t => v1}}, nested_put(?path([k,t]), v1, #{<<"k">> => #{t => v0}})), - ?assertEqual(#{k => #{<<"t">> => #{a => v1}}}, nested_put(?path([k,t,a]), v1, #{k => #{<<"t">> => v0}})), - ?assertEqual(#{k => #{<<"t">> => #{<<"a">> => v1}}}, nested_put(?path([k,t,<<"a">>]), v1, #{k => #{<<"t">> => v0}})). + ?assertEqual( + #{<<"k">> => #{<<"t">> => v1}}, + nested_put(?path([k, t]), v1, #{<<"k">> => #{<<"t">> => v0}}) + ), + ?assertEqual(#{<<"k">> => #{t => v1}}, nested_put(?path([k, t]), v1, #{<<"k">> => #{t => v0}})), + ?assertEqual( + #{k => #{<<"t">> => #{a => v1}}}, nested_put(?path([k, t, a]), v1, #{k => #{<<"t">> => v0}}) + ), + ?assertEqual( + #{k => #{<<"t">> => #{<<"a">> => v1}}}, + nested_put(?path([k, t, <<"a">>]), v1, #{k => #{<<"t">> => v0}}) + ). t_nested_put_index(_) -> - ?assertEqual([1,a,3], nested_put(?path([{ic,2}]), a, [1,2,3])), - ?assertEqual([1,2,3], nested_put(?path([{ic,0}]), a, [1,2,3])), - ?assertEqual([1,2,3], nested_put(?path([{ic,4}]), a, [1,2,3])), - ?assertEqual([1,[a],3], nested_put(?path([{ic,2}, {ic,1}]), a, [1,[2],3])), - ?assertEqual([1,[[a]],3], nested_put(?path([{ic,2}, {ic,1}, {ic,1}]), a, [1,[[2]],3])), - ?assertEqual([1,[[2]],3], nested_put(?path([{ic,2}, {ic,1}, {ic,2}]), a, [1,[[2]],3])), - ?assertEqual([1,[a],1], nested_put(?path([{ic,2}, {i,?path([{ic,3}])}]), a, [1,[2],1])), + ?assertEqual([1, a, 3], nested_put(?path([{ic, 2}]), a, [1, 2, 3])), + ?assertEqual([1, 2, 3], nested_put(?path([{ic, 0}]), a, [1, 2, 3])), + ?assertEqual([1, 2, 3], nested_put(?path([{ic, 4}]), a, [1, 2, 3])), + ?assertEqual([1, [a], 3], nested_put(?path([{ic, 2}, {ic, 1}]), a, [1, [2], 3])), + ?assertEqual([1, [[a]], 3], nested_put(?path([{ic, 2}, {ic, 1}, {ic, 1}]), a, [1, [[2]], 3])), + ?assertEqual([1, [[2]], 3], nested_put(?path([{ic, 2}, {ic, 1}, {ic, 2}]), a, [1, [[2]], 3])), + ?assertEqual([1, [a], 1], nested_put(?path([{ic, 2}, {i, ?path([{ic, 3}])}]), a, [1, [2], 1])), %% nested_put to the first or tail of a list: - ?assertEqual([a], nested_put(?path([{ic,head}]), a, not_list)), - ?assertEqual([a], nested_put(?path([{ic,head}]), a, [])), - ?assertEqual([a,1,2,3], nested_put(?path([{ic,head}]), a, [1,2,3])), - ?assertEqual([a], nested_put(?path([{ic,tail}]), a, not_list)), - ?assertEqual([a], nested_put(?path([{ic,tail}]), a, [])), - ?assertEqual([1,2,3,a], nested_put(?path([{ic,tail}]), a, [1,2,3])). + ?assertEqual([a], nested_put(?path([{ic, head}]), a, not_list)), + ?assertEqual([a], nested_put(?path([{ic, head}]), a, [])), + ?assertEqual([a, 1, 2, 3], nested_put(?path([{ic, head}]), a, [1, 2, 3])), + ?assertEqual([a], nested_put(?path([{ic, tail}]), a, not_list)), + ?assertEqual([a], nested_put(?path([{ic, tail}]), a, [])), + ?assertEqual([1, 2, 3, a], nested_put(?path([{ic, tail}]), a, [1, 2, 3])). t_nested_put_negative_index(_) -> - ?assertEqual([1,2,a], nested_put(?path([{ic,-1}]), a, [1,2,3])), - ?assertEqual([1,a,3], nested_put(?path([{ic,-2}]), a, [1,2,3])), - ?assertEqual([a,2,3], nested_put(?path([{ic,-3}]), a, [1,2,3])), - ?assertEqual([1,2,3], nested_put(?path([{ic,-4}]), a, [1,2,3])). + ?assertEqual([1, 2, a], nested_put(?path([{ic, -1}]), a, [1, 2, 3])), + ?assertEqual([1, a, 3], nested_put(?path([{ic, -2}]), a, [1, 2, 3])), + ?assertEqual([a, 2, 3], nested_put(?path([{ic, -3}]), a, [1, 2, 3])), + ?assertEqual([1, 2, 3], nested_put(?path([{ic, -4}]), a, [1, 2, 3])). t_nested_put_mix_map_index(_) -> - ?assertEqual(#{a => [a]}, nested_put(?path([a, {ic,2}]), a, #{})), - ?assertEqual(#{a => [#{b => 0}]}, nested_put(?path([a, {ic,2}, b]), 0, #{})), - ?assertEqual(#{a => [1,a,3]}, nested_put(?path([a, {ic,2}]), a, #{a => [1,2,3]})), - ?assertEqual([1,#{a => c},3], nested_put(?path([{ic,2}, a]), c, [1,#{a => b},3])), - ?assertEqual([1,#{a => [c]},3], nested_put(?path([{ic,2}, a, {ic, 1}]), c, [1,#{a => [b]},3])), - ?assertEqual(#{a => [1,a,3], b => 2}, nested_put(?path([a, {iv,b}]), a, #{a => [1,2,3], b => 2})), - ?assertEqual(#{a => [1,2,3], b => 2}, nested_put(?path([a, {iv,c}]), a, #{a => [1,2,3], b => 2})), - ?assertEqual(#{a => [#{c => a},1,2,3]}, nested_put(?path([a, {ic,head}, c]), a, #{a => [1,2,3]})). + ?assertEqual(#{a => [a]}, nested_put(?path([a, {ic, 2}]), a, #{})), + ?assertEqual(#{a => [#{b => 0}]}, nested_put(?path([a, {ic, 2}, b]), 0, #{})), + ?assertEqual(#{a => [1, a, 3]}, nested_put(?path([a, {ic, 2}]), a, #{a => [1, 2, 3]})), + ?assertEqual([1, #{a => c}, 3], nested_put(?path([{ic, 2}, a]), c, [1, #{a => b}, 3])), + ?assertEqual( + [1, #{a => [c]}, 3], nested_put(?path([{ic, 2}, a, {ic, 1}]), c, [1, #{a => [b]}, 3]) + ), + ?assertEqual( + #{a => [1, a, 3], b => 2}, nested_put(?path([a, {iv, b}]), a, #{a => [1, 2, 3], b => 2}) + ), + ?assertEqual( + #{a => [1, 2, 3], b => 2}, nested_put(?path([a, {iv, c}]), a, #{a => [1, 2, 3], b => 2}) + ), + ?assertEqual( + #{a => [#{c => a}, 1, 2, 3]}, nested_put(?path([a, {ic, head}, c]), a, #{a => [1, 2, 3]}) + ). t_nested_get_map(_) -> ?assertEqual(undefined, nested_get(?path([a]), not_map)), ?assertEqual(#{a => 1}, nested_get(?path([]), #{a => 1})), ?assertEqual(#{b => c}, nested_get(?path([a]), #{a => #{b => c}})), - ?assertEqual(undefined, nested_get(?path([a,b,c]), not_map)), - ?assertEqual(undefined, nested_get(?path([a,b,c]), #{})), - ?assertEqual(undefined, nested_get(?path([a,b,c]), #{a => #{}})), - ?assertEqual(undefined, nested_get(?path([a,b,c]), #{a => #{b => #{}}})), - ?assertEqual(v1, nested_get(?path([p,x]), #{p => #{x => v1}})), - ?assertEqual(v1, nested_get(?path([<<"p">>,<<"x">>]), #{p => #{x => v1}})), - ?assertEqual(c, nested_get(?path([a,b,c]), #{a => #{b => #{c => c}}})). + ?assertEqual(undefined, nested_get(?path([a, b, c]), not_map)), + ?assertEqual(undefined, nested_get(?path([a, b, c]), #{})), + ?assertEqual(undefined, nested_get(?path([a, b, c]), #{a => #{}})), + ?assertEqual(undefined, nested_get(?path([a, b, c]), #{a => #{b => #{}}})), + ?assertEqual(v1, nested_get(?path([p, x]), #{p => #{x => v1}})), + ?assertEqual(v1, nested_get(?path([<<"p">>, <<"x">>]), #{p => #{x => v1}})), + ?assertEqual(c, nested_get(?path([a, b, c]), #{a => #{b => #{c => c}}})). t_nested_get_map_1(_) -> ?assertEqual(1, nested_get(?path([a]), <<"{\"a\": 1}">>)), ?assertEqual(<<"{\"b\": 1}">>, nested_get(?path([a]), #{a => <<"{\"b\": 1}">>})), - ?assertEqual(1, nested_get(?path([a,b]), #{a => <<"{\"b\": 1}">>})). + ?assertEqual(1, nested_get(?path([a, b]), #{a => <<"{\"b\": 1}">>})). t_nested_get_index(_) -> %% single index get - ?assertEqual(1, nested_get(?path([{ic,1}]), [1,2,3])), - ?assertEqual(2, nested_get(?path([{ic,2}]), [1,2,3])), - ?assertEqual(3, nested_get(?path([{ic,3}]), [1,2,3])), - ?assertEqual(undefined, nested_get(?path([{ic,0}]), [1,2,3])), - ?assertEqual("not_found", nested_get(?path([{ic,0}]), [1,2,3], "not_found")), - ?assertEqual(undefined, nested_get(?path([{ic,4}]), [1,2,3])), - ?assertEqual("not_found", nested_get(?path([{ic,4}]), [1,2,3], "not_found")), + ?assertEqual(1, nested_get(?path([{ic, 1}]), [1, 2, 3])), + ?assertEqual(2, nested_get(?path([{ic, 2}]), [1, 2, 3])), + ?assertEqual(3, nested_get(?path([{ic, 3}]), [1, 2, 3])), + ?assertEqual(undefined, nested_get(?path([{ic, 0}]), [1, 2, 3])), + ?assertEqual("not_found", nested_get(?path([{ic, 0}]), [1, 2, 3], "not_found")), + ?assertEqual(undefined, nested_get(?path([{ic, 4}]), [1, 2, 3])), + ?assertEqual("not_found", nested_get(?path([{ic, 4}]), [1, 2, 3], "not_found")), %% multiple index get - ?assertEqual(c, nested_get(?path([{ic,2}, {ic,3}]), [1,[a,b,c],3])), - ?assertEqual("I", nested_get(?path([{ic,2}, {ic,3}, {ic,1}]), [1,[a,b,["I","II","III"]],3])), - ?assertEqual(undefined, nested_get(?path([{ic,2}, {ic,1}, {ic,1}]), [1,[a,b,["I","II","III"]],3])), - ?assertEqual(default, nested_get(?path([{ic,2}, {ic,1}, {ic,1}]), [1,[a,b,["I","II","III"]],3], default)). + ?assertEqual(c, nested_get(?path([{ic, 2}, {ic, 3}]), [1, [a, b, c], 3])), + ?assertEqual( + "I", nested_get(?path([{ic, 2}, {ic, 3}, {ic, 1}]), [1, [a, b, ["I", "II", "III"]], 3]) + ), + ?assertEqual( + undefined, + nested_get(?path([{ic, 2}, {ic, 1}, {ic, 1}]), [1, [a, b, ["I", "II", "III"]], 3]) + ), + ?assertEqual( + default, + nested_get(?path([{ic, 2}, {ic, 1}, {ic, 1}]), [1, [a, b, ["I", "II", "III"]], 3], default) + ). t_nested_get_negative_index(_) -> - ?assertEqual(3, nested_get(?path([{ic,-1}]), [1,2,3])), - ?assertEqual(2, nested_get(?path([{ic,-2}]), [1,2,3])), - ?assertEqual(1, nested_get(?path([{ic,-3}]), [1,2,3])), - ?assertEqual(undefined, nested_get(?path([{ic,-4}]), [1,2,3])). + ?assertEqual(3, nested_get(?path([{ic, -1}]), [1, 2, 3])), + ?assertEqual(2, nested_get(?path([{ic, -2}]), [1, 2, 3])), + ?assertEqual(1, nested_get(?path([{ic, -3}]), [1, 2, 3])), + ?assertEqual(undefined, nested_get(?path([{ic, -4}]), [1, 2, 3])). t_nested_get_mix_map_index(_) -> %% index const - ?assertEqual(1, nested_get(?path([a, {ic,1}]), #{a => [1,2,3]})), - ?assertEqual(2, nested_get(?path([{ic,2}, a]), [1,#{a => 2},3])), - ?assertEqual(undefined, nested_get(?path([a, {ic,0}]), #{a => [1,2,3]})), - ?assertEqual("not_found", nested_get(?path([a, {ic,0}]), #{a => [1,2,3]}, "not_found")), - ?assertEqual("not_found", nested_get(?path([b, {ic,1}]), #{a => [1,2,3]}, "not_found")), - ?assertEqual(undefined, nested_get(?path([{ic,4}, a]), [1,2,3,4])), - ?assertEqual("not_found", nested_get(?path([{ic,4}, a]), [1,2,3,4], "not_found")), - ?assertEqual(c, nested_get(?path([a, {ic,2}, {ic,3}]), #{a => [1,[a,b,c],3]})), - ?assertEqual("I", nested_get(?path([{ic,2}, c, {ic,1}]), [1,#{a => a, b => b, c => ["I","II","III"]},3])), - ?assertEqual("I", nested_get(?path([{ic,2}, c, d]), [1,#{a => a, b => b, c => #{d => "I"}},3])), - ?assertEqual(undefined, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3])), - ?assertEqual(default, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3], default)), + ?assertEqual(1, nested_get(?path([a, {ic, 1}]), #{a => [1, 2, 3]})), + ?assertEqual(2, nested_get(?path([{ic, 2}, a]), [1, #{a => 2}, 3])), + ?assertEqual(undefined, nested_get(?path([a, {ic, 0}]), #{a => [1, 2, 3]})), + ?assertEqual("not_found", nested_get(?path([a, {ic, 0}]), #{a => [1, 2, 3]}, "not_found")), + ?assertEqual("not_found", nested_get(?path([b, {ic, 1}]), #{a => [1, 2, 3]}, "not_found")), + ?assertEqual(undefined, nested_get(?path([{ic, 4}, a]), [1, 2, 3, 4])), + ?assertEqual("not_found", nested_get(?path([{ic, 4}, a]), [1, 2, 3, 4], "not_found")), + ?assertEqual(c, nested_get(?path([a, {ic, 2}, {ic, 3}]), #{a => [1, [a, b, c], 3]})), + ?assertEqual( + "I", + nested_get(?path([{ic, 2}, c, {ic, 1}]), [1, #{a => a, b => b, c => ["I", "II", "III"]}, 3]) + ), + ?assertEqual( + "I", nested_get(?path([{ic, 2}, c, d]), [1, #{a => a, b => b, c => #{d => "I"}}, 3]) + ), + ?assertEqual( + undefined, nested_get(?path([{ic, 2}, c, e]), [1, #{a => a, b => b, c => #{d => "I"}}, 3]) + ), + ?assertEqual( + default, + nested_get(?path([{ic, 2}, c, e]), [1, #{a => a, b => b, c => #{d => "I"}}, 3], default) + ), %% index var - ?assertEqual(1, nested_get(?path([a, {iv,<<"b">>}]), #{a => [1,2,3], b => 1})), - ?assertEqual(1, nested_get(?path([a, {iv,b}]), #{a => [1,2,3], b => 1})), - ?assertEqual(undefined, nested_get(?path([a, {iv,c}]), #{a => [1,2,3], b => 1})), - ?assertEqual(undefined, nested_get(?path([a, {iv,b}]), #{a => [1,2,3], b => 4})), - ?assertEqual("I", nested_get(?path([{i,?path([{ic, 3}])}, c, d]), - [1,#{a => a, b => b, c => #{d => "I"}},2], default)), - ?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), - #{a => [1,2,3], b => [#{c => 3}]})), - ?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), - #{a => [1,2,3], b => [#{c => 3}]}, default)), - ?assertEqual(default, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), - #{a => [1,2,3], b => [#{c => 4}]}, default)), - ?assertEqual(default, nested_get(?path([a, {i,?path([b,{ic,2},c])}]), - #{a => [1,2,3], b => [#{c => 3}]}, default)). + ?assertEqual(1, nested_get(?path([a, {iv, <<"b">>}]), #{a => [1, 2, 3], b => 1})), + ?assertEqual(1, nested_get(?path([a, {iv, b}]), #{a => [1, 2, 3], b => 1})), + ?assertEqual(undefined, nested_get(?path([a, {iv, c}]), #{a => [1, 2, 3], b => 1})), + ?assertEqual(undefined, nested_get(?path([a, {iv, b}]), #{a => [1, 2, 3], b => 4})), + ?assertEqual( + "I", + nested_get( + ?path([{i, ?path([{ic, 3}])}, c, d]), + [1, #{a => a, b => b, c => #{d => "I"}}, 2], + default + ) + ), + ?assertEqual( + 3, + nested_get( + ?path([a, {i, ?path([b, {ic, 1}, c])}]), + #{a => [1, 2, 3], b => [#{c => 3}]} + ) + ), + ?assertEqual( + 3, + nested_get( + ?path([a, {i, ?path([b, {ic, 1}, c])}]), + #{a => [1, 2, 3], b => [#{c => 3}]}, + default + ) + ), + ?assertEqual( + default, + nested_get( + ?path([a, {i, ?path([b, {ic, 1}, c])}]), + #{a => [1, 2, 3], b => [#{c => 4}]}, + default + ) + ), + ?assertEqual( + default, + nested_get( + ?path([a, {i, ?path([b, {ic, 2}, c])}]), + #{a => [1, 2, 3], b => [#{c => 3}]}, + default + ) + ). t_atom_key_map(_) -> ?assertEqual(#{a => 1}, atom_key_map(#{<<"a">> => 1})), - ?assertEqual(#{a => 1, b => #{a => 2}}, - atom_key_map(#{<<"a">> => 1, <<"b">> => #{<<"a">> => 2}})), - ?assertEqual([#{a => 1}, #{b => #{a => 2}}], - atom_key_map([#{<<"a">> => 1}, #{<<"b">> => #{<<"a">> => 2}}])), - ?assertEqual(#{a => 1, b => [#{a => 2}, #{c => 2}]}, - atom_key_map(#{<<"a">> => 1, <<"b">> => [#{<<"a">> => 2}, #{<<"c">> => 2}]})). + ?assertEqual( + #{a => 1, b => #{a => 2}}, + atom_key_map(#{<<"a">> => 1, <<"b">> => #{<<"a">> => 2}}) + ), + ?assertEqual( + [#{a => 1}, #{b => #{a => 2}}], + atom_key_map([#{<<"a">> => 1}, #{<<"b">> => #{<<"a">> => 2}}]) + ), + ?assertEqual( + #{a => 1, b => [#{a => 2}, #{c => 2}]}, + atom_key_map(#{<<"a">> => 1, <<"b">> => [#{<<"a">> => 2}, #{<<"c">> => 2}]}) + ). all() -> - IsTestCase = fun("t_" ++ _) -> true; (_) -> false end, + IsTestCase = fun + ("t_" ++ _) -> true; + (_) -> false + end, [F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))]. suite() -> diff --git a/apps/emqx_rule_engine/test/prop_rule_maps.erl b/apps/emqx_rule_engine/test/prop_rule_maps.erl index 659a423e1..15eb4ab6d 100644 --- a/apps/emqx_rule_engine/test/prop_rule_maps.erl +++ b/apps/emqx_rule_engine/test/prop_rule_maps.erl @@ -3,8 +3,14 @@ -include_lib("proper/include/proper.hrl"). prop_get_put_single_key() -> - ?FORALL({Key, Val}, {term(), term()}, - begin - Val =:= emqx_rule_maps:nested_get({var, Key}, - emqx_rule_maps:nested_put({var, Key}, Val, #{})) - end). + ?FORALL( + {Key, Val}, + {term(), term()}, + begin + Val =:= + emqx_rule_maps:nested_get( + {var, Key}, + emqx_rule_maps:nested_put({var, Key}, Val, #{}) + ) + end + ). From 9b970bd71da3294d5b644254410036d973286a89 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 27 Apr 2022 15:51:47 +0200 Subject: [PATCH 31/43] style: add the last reformat commit to git-blam-ignore --- git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git-blame-ignore-revs b/git-blame-ignore-revs index b8bd2d03e..a4c32a60d 100644 --- a/git-blame-ignore-revs +++ b/git-blame-ignore-revs @@ -35,3 +35,5 @@ b4451823350ec46126c49ca915b4b169dd4cf49e a4feb3e6e95c18cb531416112e57520c5ba00d40 # reformat apps/emqx_dashboard 07444e3da53c408695630bc0f57340f557106942 +# reformat all remaning apps +02c3f87b316e8370287d5cd46de4f103ffe48433 From 4c0da19ee6a16631e256d74e7d3dd78e059c90d2 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 27 Apr 2022 16:11:19 +0200 Subject: [PATCH 32/43] ci: try to clean up workspace before checkout --- .github/workflows/build_slim_packages.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_slim_packages.yaml b/.github/workflows/build_slim_packages.yaml index a05424169..951ebbcfb 100644 --- a/.github/workflows/build_slim_packages.yaml +++ b/.github/workflows/build_slim_packages.yaml @@ -43,7 +43,9 @@ jobs: container: "ghcr.io/emqx/emqx-builder/5.0-10:${{ matrix.elixir }}-${{ matrix.otp }}-${{ matrix.os }}" steps: - - uses: AutoModality/action-clean@v1 + - name: cleanup + run: | + rm -rf "${GITHUB_WORKSPACE}/" - uses: actions/checkout@v1 - name: prepare run: | From 0c7bbf9e64db9305beb058d3b52c3151f4053b23 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 26 Apr 2022 09:17:15 +0800 Subject: [PATCH 33/43] revert: ssl option should not provide default cert file revert commit 3b9b12fe36a9b68b36cfbf7b6e9a4d484263563d in PR#7527 --- apps/emqx/src/emqx_schema.erl | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 471670beb..f8c4ad335 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -1824,13 +1824,7 @@ common_ssl_opts_schema(Defaults) -> %% @doc Make schema for SSL listener options. %% When it's for ranch listener, an extra field `handshake_timeout' is added. -spec server_ssl_opts_schema(map(), boolean()) -> hocon_schema:field_schema(). -server_ssl_opts_schema(Defaults1, IsRanchListener) -> - Defaults0 = #{ - cacertfile => emqx:cert_file("cacert.pem"), - certfile => emqx:cert_file("cert.pem"), - keyfile => emqx:cert_file("key.pem") - }, - Defaults = maps:merge(Defaults0, Defaults1), +server_ssl_opts_schema(Defaults, IsRanchListener) -> D = fun(Field) -> maps:get(to_atom(Field), Defaults, undefined) end, Df = fun(Field, Default) -> maps:get(to_atom(Field), Defaults, Default) end, common_ssl_opts_schema(Defaults) ++ @@ -1883,15 +1877,7 @@ server_ssl_opts_schema(Defaults1, IsRanchListener) -> %% @doc Make schema for SSL client. -spec client_ssl_opts_schema(map()) -> hocon_schema:field_schema(). -client_ssl_opts_schema(Defaults1) -> - %% assert - true = lists:all(fun(K) -> is_atom(K) end, maps:keys(Defaults1)), - Defaults0 = #{ - cacertfile => emqx:cert_file("cacert.pem"), - certfile => emqx:cert_file("client-cert.pem"), - keyfile => emqx:cert_file("client-key.pem") - }, - Defaults = maps:merge(Defaults0, Defaults1), +client_ssl_opts_schema(Defaults) -> common_ssl_opts_schema(Defaults) ++ [ {"server_name_indication", From e5d4e272b2e1418c66ff246268e333ab88ef9da2 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Tue, 26 Apr 2022 21:50:21 +0800 Subject: [PATCH 34/43] fix(ssl): sni option should be atom --- apps/emqx/src/emqx_tls_lib.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index 9f0c80be2..5f7c895cf 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -476,7 +476,7 @@ to_client_opts(Opts) -> CertFile = ensure_str(Get(certfile)), CAFile = ensure_str(Get(cacertfile)), Verify = GetD(verify, verify_none), - SNI = ensure_str(Get(server_name_indication)), + SNI = ensure_sni(Get(server_name_indication)), Versions = integral_versions(Get(versions)), Ciphers = integral_ciphers(Versions, Get(ciphers)), filter([ @@ -505,6 +505,11 @@ fuzzy_map_get(Key, Options, Default) -> Default end. +ensure_sni(disable) -> disable; +ensure_sni(undefined) -> undefined; +ensure_sni(L) when is_list(L) -> L; +ensure_sni(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). + ensure_str(undefined) -> undefined; ensure_str(L) when is_list(L) -> L; ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). From 4f9b42a2506b51a0c17d1f90ec1712030df56d88 Mon Sep 17 00:00:00 2001 From: JimMoen Date: Thu, 28 Apr 2022 09:45:18 +0800 Subject: [PATCH 35/43] style: make erlfmt happy --- apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl index 03e8d6d78..16590b114 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl @@ -22,14 +22,14 @@ -compile(export_all). -define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). --define(CONF_DEFAULT, - <<"\n" +-define(CONF_DEFAULT, << + "\n" "prometheus {\n" " push_gateway_server = \"http://127.0.0.1:9091\"\n" " interval = \"1s\"\n" " enable = true\n" - "}\n">> -). + "}\n" +>>). %%-------------------------------------------------------------------- %% Setups From 2c95fba4df1264a9968e799c8d8488eda99a1e02 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 28 Apr 2022 09:55:51 +0800 Subject: [PATCH 36/43] fix: api_listener min TLS ct fail --- .../test/emqx_mgmt_api_listeners_SUITE.erl | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) 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 169a272e3..29adfc302 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_listeners_SUITE.erl @@ -72,19 +72,19 @@ t_wss_crud_listeners_by_id(_) -> crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type). crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> - TcpPath = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), + OriginPath = emqx_mgmt_api_test_util:api_path(["listeners", ListenerId]), NewPath = emqx_mgmt_api_test_util:api_path(["listeners", NewListenerId]), - TcpListener = request(get, TcpPath, [], []), + OriginListener = request(get, OriginPath, [], []), %% create with full options ?assertEqual({error, not_found}, is_running(NewListenerId)), ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, NewPath, [], [])), - NewConf = TcpListener#{ + NewConf = OriginListener#{ <<"id">> => NewListenerId, <<"bind">> => <<"0.0.0.0:2883">> }, Create = request(post, NewPath, [], NewConf), - ?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(Create))), + ?assertEqual(lists:sort(maps:keys(OriginListener)), lists:sort(maps:keys(Create))), Get1 = request(get, NewPath, [], []), ?assertMatch(Create, Get1), ?assert(is_running(NewListenerId)), @@ -93,20 +93,42 @@ crud_listeners_by_id(ListenerId, NewListenerId, MinListenerId, BadId, Type) -> MinPath = emqx_mgmt_api_test_util:api_path(["listeners", MinListenerId]), ?assertEqual({error, not_found}, is_running(MinListenerId)), ?assertMatch({error, {"HTTP/1.1", 404, _}}, request(get, MinPath, [], [])), - MinConf = #{ - <<"id">> => MinListenerId, - <<"bind">> => <<"0.0.0.0:3883">>, - <<"type">> => Type - }, + MinConf = + case OriginListener of + #{ + <<"ssl">> := + #{ + <<"cacertfile">> := CaCertFile, + <<"certfile">> := CertFile, + <<"keyfile">> := KeyFile + } + } -> + #{ + <<"id">> => MinListenerId, + <<"bind">> => <<"0.0.0.0:3883">>, + <<"type">> => Type, + <<"ssl">> => #{ + <<"cacertfile">> => CaCertFile, + <<"certfile">> => CertFile, + <<"keyfile">> => KeyFile + } + }; + _ -> + #{ + <<"id">> => MinListenerId, + <<"bind">> => <<"0.0.0.0:3883">>, + <<"type">> => Type + } + end, MinCreate = request(post, MinPath, [], MinConf), - ?assertEqual(lists:sort(maps:keys(TcpListener)), lists:sort(maps:keys(MinCreate))), + ?assertEqual(lists:sort(maps:keys(OriginListener)), lists:sort(maps:keys(MinCreate))), MinGet = request(get, MinPath, [], []), ?assertMatch(MinCreate, MinGet), ?assert(is_running(MinListenerId)), %% bad create(same port) BadPath = emqx_mgmt_api_test_util:api_path(["listeners", BadId]), - BadConf = TcpListener#{ + BadConf = OriginListener#{ <<"id">> => BadId, <<"bind">> => <<"0.0.0.0:2883">> }, From 974380a3d4cb560c615e6632c59f50ad7aca4151 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Wed, 27 Apr 2022 09:21:16 +0200 Subject: [PATCH 37/43] feat(dashboard): add listener enable/disable config toggle --- .../i18n/emqx_dashboard_i18n.conf | 10 +++ apps/emqx_dashboard/src/emqx_dashboard.erl | 9 ++- .../src/emqx_dashboard_schema.erl | 61 +++++++++++++++---- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 07bd75f9b..9badd75e0 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -132,6 +132,16 @@ Note: `sample_interval` should be a divisor of 60.""" zh: "HTTPS" } } + listener_enable { + desc { + en: "Ignore or enable this listener" + zh: "忽略或启用该监听器配置" + } + label { + en: "Enable" + zh: "启用" + } + } bind { desc { en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)." diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index c13e20b00..af96bb15a 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -153,10 +153,13 @@ apps() -> ]. listeners(Listeners) -> - lists:map( + lists:filtermap( fun({Protocol, Conf}) -> - {Conf1, Bind} = ip_port(Conf), - {listener_name(Protocol, Conf1), Protocol, Bind, ranch_opts(Conf1)} + maps:get(enable, Conf) andalso + begin + {Conf1, Bind} = ip_port(Conf), + {true, {listener_name(Protocol, Conf1), Protocol, Bind, ranch_opts(Conf1)}} + end end, maps:to_list(Listeners) ). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 27a7aa571..0f4221c84 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -76,6 +76,44 @@ fields("listeners") -> )} ]; fields("http") -> + [ + {"enable", + sc( + boolean(), + #{ + default => true, + desc => ?DESC(listener_enable) + } + )} + | common_listener_fields() + ]; +fields("https") -> + [ + {"enable", + sc( + boolean(), + #{ + default => false, + desc => ?DESC(listener_enable) + } + )} + | common_listener_fields() ++ + exclude_fields( + ["enable", "fail_if_no_peer_cert"], + emqx_schema:server_ssl_opts_schema(#{}, true) + ) + ]. + +exclude_fields([], Fields) -> + Fields; +exclude_fields([FieldName | Rest], Fields) -> + %% assert field exists + case lists:keytake(FieldName, 1, Fields) of + {value, _, New} -> exclude_fields(Rest, New); + false -> error({FieldName, Fields}) + end. + +common_listener_fields() -> [ {"bind", fun bind/1}, {"num_acceptors", @@ -126,19 +164,18 @@ fields("http") -> desc => ?DESC(ipv6_v6only) } )} - ]; -fields("https") -> - fields("http") ++ - proplists:delete( - "fail_if_no_peer_cert", - emqx_schema:server_ssl_opts_schema(#{}, true) - ). + ]. -desc("dashboard") -> ?DESC(desc_dashboard); -desc("listeners") -> ?DESC(desc_listeners); -desc("http") -> ?DESC(desc_http); -desc("https") -> ?DESC(desc_https); -desc(_) -> undefined. +desc("dashboard") -> + ?DESC(desc_dashboard); +desc("listeners") -> + ?DESC(desc_listeners); +desc("http") -> + ?DESC(desc_http); +desc("https") -> + ?DESC(desc_https); +desc(_) -> + undefined. bind(type) -> hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]); bind(default) -> 18083; From 204f04be655c4589a2513600199a4fa0f25caf8d Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 19:51:00 +0800 Subject: [PATCH 38/43] fix: handshake_timeout is ranch option not socket options --- apps/emqx_dashboard/src/emqx_dashboard.erl | 27 ++++++---------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index af96bb15a..36daed7db 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -175,34 +175,21 @@ init_i18n() -> Lang = emqx_conf:get([dashboard, i18n_lang], en), init_i18n(File, Lang). -ranch_opts(RanchOptions) -> +ranch_opts(Options) -> Keys = [ - {ack_timeout, handshake_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)}. + RanchOpts = maps:with(Keys, Options), + SocketOpts = maps:fold(fun filter_false/3, [], maps:without([enable | Keys], Options)), + RanchOpts#{socket_opts => SocketOpts}. -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}; - V -> - {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]. +filter_false(_K, false, S) -> S; +filter_false(K, V, S) -> [{K, V} | S]. listener_name(Protocol, #{port := Port, ip := IP}) -> Name = From b06747d96116ef8eae658f1d3c56c42800997b9b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 20:19:19 +0800 Subject: [PATCH 39/43] chore: bump typeref to 0.9.1 to fix flatten error --- mix.exs | 2 +- rebar.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index b999480ab..b87579a10 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.9.0", override: true}, + {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", 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}, diff --git a/rebar.config b/rebar.config index 95e3f705c..54114c608 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.9.0"}}} + , {typerefl, {git, "https://github.com/ieQu1/typerefl", {tag, "0.9.1"}}} , {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"}}} From 5c08c4ff4e64deaf1fe7c9363ae4dc3759bb4faa Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 22:24:32 +0800 Subject: [PATCH 40/43] chore: more detailed about dashboard inet6 option --- .../i18n/emqx_dashboard_i18n.conf | 6 +++--- apps/emqx_dashboard/src/emqx_dashboard.erl | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf index 9badd75e0..d6587c203 100644 --- a/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf +++ b/apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf @@ -74,8 +74,8 @@ Note: `sample_interval` should be a divisor of 60.""" } inet6 { desc { - en: "Enable IPv6 support." - zh: "启用IPv6" + en: "Enable IPv6 support, default is false, which means IPv4 only." + zh: "启用IPv6, 如果机器不支持IPv6,请关闭此选项,否则会导致仪表盘无法使用。" } label { en: "IPv6" @@ -85,7 +85,7 @@ Note: `sample_interval` should be a divisor of 60.""" ipv6_v6only { desc { en: "Disable IPv4-to-IPv6 mapping for the listener." - zh: "禁用IPv4-to-IPv6映射" + zh: "当开启 inet6 功能的同时禁用 IPv4-to-IPv6 映射。该配置仅在 inet6 功能开启时有效。" } label { en: "IPv6 only" diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 36daed7db..b76858d4b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -185,8 +185,21 @@ ranch_opts(Options) -> socket ], RanchOpts = maps:with(Keys, Options), - SocketOpts = maps:fold(fun filter_false/3, [], maps:without([enable | Keys], Options)), - RanchOpts#{socket_opts => SocketOpts}. + SocketOpts = maps:fold( + fun filter_false/3, + [], + maps:without([enable, inet6, ipv6_v6only | Keys], Options) + ), + InetOpts = + case Options of + #{inet6 := true, ipv6_v6only := true} -> + [inet6, {ipv6_v6only, true}]; + #{inet6 := true, ipv6_v6only := false} -> + [inet6]; + _ -> + [inet] + end, + RanchOpts#{socket_opts => InetOpts ++ SocketOpts}. filter_false(_K, false, S) -> S; filter_false(K, V, S) -> [{K, V} | S]. From 2eab3f1cdbb5347f6e93ee6c440db63dcb14c693 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Wed, 27 Apr 2022 23:17:02 +0800 Subject: [PATCH 41/43] fix: dashboard https's password can't update --- .../src/emqx_dashboard_config.erl | 44 ++++++++++++----- .../src/emqx_dashboard_schema.erl | 49 ++++++++++--------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_config.erl b/apps/emqx_dashboard/src/emqx_dashboard_config.erl index aea8304da..1061661f5 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_config.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_config.erl @@ -63,22 +63,42 @@ remove_handler() -> ok. pre_config_update(_Path, UpdateConf0, RawConf) -> - UpdateConf = - case UpdateConf0 of - #{<<"default_password">> := <<"******">>} -> - maps:remove(<<"default_password">>, UpdateConf0); - _ -> - UpdateConf0 - end, + UpdateConf = remove_sensitive_data(UpdateConf0), NewConf = emqx_map_lib:deep_merge(RawConf, UpdateConf), {ok, NewConf}. +-define(SENSITIVE_PASSWORD, <<"******">>). + +remove_sensitive_data(Conf0) -> + Conf1 = + case Conf0 of + #{<<"default_password">> := ?SENSITIVE_PASSWORD} -> + maps:remove(<<"default_password">>, Conf0); + _ -> + Conf0 + end, + case Conf1 of + #{<<"listeners">> := #{<<"https">> := #{<<"password">> := ?SENSITIVE_PASSWORD}}} -> + emqx_map_lib:deep_remove([<<"listeners">>, <<"https">>, <<"password">>], Conf1); + _ -> + Conf1 + end. + post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) -> - #{listeners := NewListeners} = NewConf, - #{listeners := OldListeners} = OldConf, + #{listeners := #{http := NewHttp, https := NewHttps}} = NewConf, + #{listeners := #{http := OldHttp, https := OldHttps}} = OldConf, _ = - case NewListeners =:= OldListeners of - true -> ok; - false -> erlang:send_after(500, ?MODULE, {update_listeners, OldListeners, NewListeners}) + case diff_listeners(OldHttp, NewHttp, OldHttps, NewHttps) of + identical -> ok; + {Stop, Start} -> erlang:send_after(500, ?MODULE, {update_listeners, Stop, Start}) end, ok. + +diff_listeners(Http, Http, Https, Https) -> + identical; +diff_listeners(OldHttp, NewHttp, Https, Https) -> + {#{http => OldHttp}, #{http => NewHttp}}; +diff_listeners(Http, Http, OldHttps, NewHttps) -> + {#{https => OldHttps}, #{https => NewHttps}}; +diff_listeners(OldHttp, NewHttp, OldHttps, NewHttps) -> + {#{http => OldHttp, https => OldHttps}, #{http => NewHttp, https => NewHttps}}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 0f4221c84..4bfc97797 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -77,26 +77,14 @@ fields("listeners") -> ]; fields("http") -> [ - {"enable", - sc( - boolean(), - #{ - default => true, - desc => ?DESC(listener_enable) - } - )} + enable(true), + bind(18803) | common_listener_fields() ]; fields("https") -> [ - {"enable", - sc( - boolean(), - #{ - default => false, - desc => ?DESC(listener_enable) - } - )} + enable(false), + bind(18804) | common_listener_fields() ++ exclude_fields( ["enable", "fail_if_no_peer_cert"], @@ -115,7 +103,6 @@ exclude_fields([FieldName | Rest], Fields) -> common_listener_fields() -> [ - {"bind", fun bind/1}, {"num_acceptors", sc( integer(), @@ -166,6 +153,28 @@ common_listener_fields() -> )} ]. +enable(Bool) -> + {"enable", + sc( + boolean(), + #{ + default => Bool, + required => true, + desc => ?DESC(listener_enable) + } + )}. + +bind(Port) -> + {"bind", + sc( + hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]), + #{ + default => Port, + required => true, + desc => ?DESC(bind) + } + )}. + desc("dashboard") -> ?DESC(desc_dashboard); desc("listeners") -> @@ -177,12 +186,6 @@ desc("https") -> desc(_) -> undefined. -bind(type) -> hoconsc:union([non_neg_integer(), emqx_schema:ip_port()]); -bind(default) -> 18083; -bind(required) -> true; -bind(desc) -> ?DESC(bind); -bind(_) -> undefined. - default_username(type) -> binary(); default_username(default) -> "admin"; default_username(required) -> true; From 3e8bedda76ba3eb1031a91cf4b3fadcb35e8a82b Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 28 Apr 2022 09:41:00 +0800 Subject: [PATCH 42/43] fix: dashboard ct add enable options --- apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl | 1 + 1 file changed, 1 insertion(+) 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 3501494cb..84eb46cc8 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -37,6 +37,7 @@ set_default_config(DefaultUsername) -> Config = #{ listeners => #{ http => #{ + enable => true, port => 18083 } }, From c5b42ea0d993cba91c99b7d822bb2baaa512c648 Mon Sep 17 00:00:00 2001 From: Zhongwen Deng Date: Thu, 28 Apr 2022 14:06:57 +0800 Subject: [PATCH 43/43] chore: reformat emqx_prometheus_SUITE --- apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl index 03e8d6d78..16590b114 100644 --- a/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl +++ b/apps/emqx_prometheus/test/emqx_prometheus_SUITE.erl @@ -22,14 +22,14 @@ -compile(export_all). -define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). --define(CONF_DEFAULT, - <<"\n" +-define(CONF_DEFAULT, << + "\n" "prometheus {\n" " push_gateway_server = \"http://127.0.0.1:9091\"\n" " interval = \"1s\"\n" " enable = true\n" - "}\n">> -). + "}\n" +>>). %%-------------------------------------------------------------------- %% Setups