style: reformat all remaining apps
This commit is contained in:
parent
37be7a4977
commit
02c3f87b31
|
|
@ -1,8 +1,9 @@
|
||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info]}.
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
]}.
|
|
||||||
|
|
||||||
{shell, [
|
{shell, [
|
||||||
% {config, "config/sys.config"},
|
% {config, "config/sys.config"},
|
||||||
{apps, [emqx_bridge]}
|
{apps, [emqx_bridge]}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_bridge,
|
{application, emqx_bridge, [
|
||||||
[{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_bridge_app, []}},
|
{mod, {emqx_bridge_app, []}},
|
||||||
{applications,
|
{applications, [
|
||||||
[kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
emqx,
|
emqx,
|
||||||
emqx_connector
|
emqx_connector
|
||||||
]},
|
]},
|
||||||
{env,[]},
|
{env, []},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache 2.0"]},
|
{licenses, ["Apache 2.0"]},
|
||||||
{links, []}
|
{links, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -18,48 +18,48 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ post_config_update/5
|
-export([post_config_update/5]).
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ load_hook/0
|
-export([
|
||||||
, unload_hook/0
|
load_hook/0,
|
||||||
]).
|
unload_hook/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([on_message_publish/1]).
|
-export([on_message_publish/1]).
|
||||||
|
|
||||||
-export([ resource_type/1
|
-export([
|
||||||
, bridge_type/1
|
resource_type/1,
|
||||||
, resource_id/1
|
bridge_type/1,
|
||||||
, resource_id/2
|
resource_id/1,
|
||||||
, bridge_id/2
|
resource_id/2,
|
||||||
, parse_bridge_id/1
|
bridge_id/2,
|
||||||
]).
|
parse_bridge_id/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ load/0
|
-export([
|
||||||
, lookup/1
|
load/0,
|
||||||
, lookup/2
|
lookup/1,
|
||||||
, lookup/3
|
lookup/2,
|
||||||
, list/0
|
lookup/3,
|
||||||
, list_bridges_by_connector/1
|
list/0,
|
||||||
, create/2
|
list_bridges_by_connector/1,
|
||||||
, create/3
|
create/2,
|
||||||
, recreate/2
|
create/3,
|
||||||
, recreate/3
|
recreate/2,
|
||||||
, create_dry_run/2
|
recreate/3,
|
||||||
, remove/1
|
create_dry_run/2,
|
||||||
, remove/2
|
remove/1,
|
||||||
, update/2
|
remove/2,
|
||||||
, update/3
|
update/2,
|
||||||
, stop/2
|
update/3,
|
||||||
, restart/2
|
stop/2,
|
||||||
, reset_metrics/1
|
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'
|
%% exported for `emqx_telemetry'
|
||||||
-export([get_basic_usage_info/0]).
|
-export([get_basic_usage_info/0]).
|
||||||
|
|
@ -69,18 +69,25 @@ load_hook() ->
|
||||||
load_hook(Bridges).
|
load_hook(Bridges).
|
||||||
|
|
||||||
load_hook(Bridges) ->
|
load_hook(Bridges) ->
|
||||||
lists:foreach(fun({_Type, Bridge}) ->
|
lists:foreach(
|
||||||
lists:foreach(fun({_Name, BridgeConf}) ->
|
fun({_Type, Bridge}) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({_Name, BridgeConf}) ->
|
||||||
do_load_hook(BridgeConf)
|
do_load_hook(BridgeConf)
|
||||||
end, maps:to_list(Bridge))
|
end,
|
||||||
end, maps:to_list(Bridges)).
|
maps:to_list(Bridge)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
maps:to_list(Bridges)
|
||||||
|
).
|
||||||
|
|
||||||
do_load_hook(#{local_topic := _} = Conf) ->
|
do_load_hook(#{local_topic := _} = Conf) ->
|
||||||
case maps:get(direction, Conf, egress) of
|
case maps:get(direction, Conf, egress) of
|
||||||
egress -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []});
|
egress -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []});
|
||||||
ingress -> ok
|
ingress -> ok
|
||||||
end;
|
end;
|
||||||
do_load_hook(_Conf) -> ok.
|
do_load_hook(_Conf) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
unload_hook() ->
|
unload_hook() ->
|
||||||
ok = emqx_hooks:del('message.publish', {?MODULE, on_message_publish}).
|
ok = emqx_hooks:del('message.publish', {?MODULE, on_message_publish}).
|
||||||
|
|
@ -90,23 +97,36 @@ on_message_publish(Message = #message{topic = Topic, flags = Flags}) ->
|
||||||
false ->
|
false ->
|
||||||
Msg = emqx_rule_events:eventmsg_publish(Message),
|
Msg = emqx_rule_events:eventmsg_publish(Message),
|
||||||
send_to_matched_egress_bridges(Topic, Msg);
|
send_to_matched_egress_bridges(Topic, Msg);
|
||||||
true -> ok
|
true ->
|
||||||
|
ok
|
||||||
end,
|
end,
|
||||||
{ok, Message}.
|
{ok, Message}.
|
||||||
|
|
||||||
send_to_matched_egress_bridges(Topic, Msg) ->
|
send_to_matched_egress_bridges(Topic, Msg) ->
|
||||||
lists:foreach(fun (Id) ->
|
lists:foreach(
|
||||||
try send_message(Id, Msg) of
|
fun(Id) ->
|
||||||
{error, Reason} ->
|
try send_message(Id, Msg) of
|
||||||
?SLOG(error, #{msg => "send_message_to_bridge_failed",
|
{error, Reason} ->
|
||||||
bridge => Id, error => Reason});
|
?SLOG(error, #{
|
||||||
_ -> ok
|
msg => "send_message_to_bridge_failed",
|
||||||
catch Err:Reason:ST ->
|
bridge => Id,
|
||||||
?SLOG(error, #{msg => "send_message_to_bridge_exception",
|
error => Reason
|
||||||
bridge => Id, error => Err, reason => Reason,
|
});
|
||||||
stacktrace => ST})
|
_ ->
|
||||||
end
|
ok
|
||||||
end, get_matched_bridges(Topic)).
|
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) ->
|
send_message(BridgeId, Message) ->
|
||||||
{BridgeType, BridgeName} = parse_bridge_id(BridgeId),
|
{BridgeType, BridgeName} = parse_bridge_id(BridgeId),
|
||||||
|
|
@ -132,8 +152,8 @@ bridge_type(emqx_connector_mqtt) -> mqtt;
|
||||||
bridge_type(emqx_connector_http) -> http.
|
bridge_type(emqx_connector_http) -> http.
|
||||||
|
|
||||||
post_config_update(_, _Req, NewConf, OldConf, _AppEnv) ->
|
post_config_update(_, _Req, NewConf, OldConf, _AppEnv) ->
|
||||||
#{added := Added, removed := Removed, changed := Updated}
|
#{added := Added, removed := Removed, changed := Updated} =
|
||||||
= diff_confs(NewConf, OldConf),
|
diff_confs(NewConf, OldConf),
|
||||||
%% The config update will be failed if any task in `perform_bridge_changes` failed.
|
%% The config update will be failed if any task in `perform_bridge_changes` failed.
|
||||||
Result = perform_bridge_changes([
|
Result = perform_bridge_changes([
|
||||||
{fun remove/3, Removed},
|
{fun remove/3, Removed},
|
||||||
|
|
@ -150,15 +170,19 @@ perform_bridge_changes(Tasks) ->
|
||||||
perform_bridge_changes([], Result) ->
|
perform_bridge_changes([], Result) ->
|
||||||
Result;
|
Result;
|
||||||
perform_bridge_changes([{Action, MapConfs} | Tasks], Result0) ->
|
perform_bridge_changes([{Action, MapConfs} | Tasks], Result0) ->
|
||||||
Result = maps:fold(fun
|
Result = maps:fold(
|
||||||
({_Type, _Name}, _Conf, {error, Reason}) ->
|
fun
|
||||||
{error, Reason};
|
({_Type, _Name}, _Conf, {error, Reason}) ->
|
||||||
({Type, Name}, Conf, _) ->
|
{error, Reason};
|
||||||
case Action(Type, Name, Conf) of
|
({Type, Name}, Conf, _) ->
|
||||||
{error, Reason} -> {error, Reason};
|
case Action(Type, Name, Conf) of
|
||||||
Return -> Return
|
{error, Reason} -> {error, Reason};
|
||||||
end
|
Return -> Return
|
||||||
end, Result0, MapConfs),
|
end
|
||||||
|
end,
|
||||||
|
Result0,
|
||||||
|
MapConfs
|
||||||
|
),
|
||||||
perform_bridge_changes(Tasks, Result).
|
perform_bridge_changes(Tasks, Result).
|
||||||
|
|
||||||
load() ->
|
load() ->
|
||||||
|
|
@ -184,18 +208,29 @@ parse_bridge_id(BridgeId) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list() ->
|
list() ->
|
||||||
lists:foldl(fun({Type, NameAndConf}, Bridges) ->
|
lists:foldl(
|
||||||
lists:foldl(fun({Name, RawConf}, Acc) ->
|
fun({Type, NameAndConf}, Bridges) ->
|
||||||
|
lists:foldl(
|
||||||
|
fun({Name, RawConf}, Acc) ->
|
||||||
case lookup(Type, Name, RawConf) of
|
case lookup(Type, Name, RawConf) of
|
||||||
{error, not_found} -> Acc;
|
{error, not_found} -> Acc;
|
||||||
{ok, Res} -> [Res | Acc]
|
{ok, Res} -> [Res | Acc]
|
||||||
end
|
end
|
||||||
end, Bridges, maps:to_list(NameAndConf))
|
end,
|
||||||
end, [], maps:to_list(emqx:get_raw_config([bridges], #{}))).
|
Bridges,
|
||||||
|
maps:to_list(NameAndConf)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
maps:to_list(emqx:get_raw_config([bridges], #{}))
|
||||||
|
).
|
||||||
|
|
||||||
list_bridges_by_connector(ConnectorId) ->
|
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) ->
|
lookup(Id) ->
|
||||||
{Type, Name} = parse_bridge_id(Id),
|
{Type, Name} = parse_bridge_id(Id),
|
||||||
|
|
@ -206,10 +241,15 @@ lookup(Type, Name) ->
|
||||||
lookup(Type, Name, RawConf).
|
lookup(Type, Name, RawConf).
|
||||||
lookup(Type, Name, RawConf) ->
|
lookup(Type, Name, RawConf) ->
|
||||||
case emqx_resource:get_instance(resource_id(Type, Name)) of
|
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, _, Data} ->
|
||||||
{ok, #{type => Type, name => Name, resource_data => Data,
|
{ok, #{
|
||||||
raw_config => RawConf}}
|
type => Type,
|
||||||
|
name => Name,
|
||||||
|
resource_data => Data,
|
||||||
|
raw_config => RawConf
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
reset_metrics(ResourceId) ->
|
reset_metrics(ResourceId) ->
|
||||||
|
|
@ -227,13 +267,21 @@ create(BridgeId, Conf) ->
|
||||||
create(BridgeType, BridgeName, Conf).
|
create(BridgeType, BridgeName, Conf).
|
||||||
|
|
||||||
create(Type, Name, Conf) ->
|
create(Type, Name, Conf) ->
|
||||||
?SLOG(info, #{msg => "create bridge", type => Type, name => Name,
|
?SLOG(info, #{
|
||||||
config => Conf}),
|
msg => "create bridge",
|
||||||
case emqx_resource:create_local(resource_id(Type, Name),
|
type => Type,
|
||||||
<<"emqx_bridge">>,
|
name => Name,
|
||||||
emqx_bridge:resource_type(Type),
|
config => Conf
|
||||||
parse_confs(Type, Name, Conf),
|
}),
|
||||||
#{}) of
|
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, already_created} -> maybe_disable_bridge(Type, Name, Conf);
|
||||||
{ok, _} -> maybe_disable_bridge(Type, Name, Conf);
|
{ok, _} -> maybe_disable_bridge(Type, Name, Conf);
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
|
|
@ -254,15 +302,25 @@ update(Type, Name, {OldConf, Conf}) ->
|
||||||
%%
|
%%
|
||||||
case if_only_to_toggle_enable(OldConf, Conf) of
|
case if_only_to_toggle_enable(OldConf, Conf) of
|
||||||
false ->
|
false ->
|
||||||
?SLOG(info, #{msg => "update bridge", type => Type, name => Name,
|
?SLOG(info, #{
|
||||||
config => Conf}),
|
msg => "update bridge",
|
||||||
|
type => Type,
|
||||||
|
name => Name,
|
||||||
|
config => Conf
|
||||||
|
}),
|
||||||
case recreate(Type, Name, Conf) of
|
case recreate(Type, Name, Conf) of
|
||||||
{ok, _} -> maybe_disable_bridge(Type, Name, Conf);
|
{ok, _} ->
|
||||||
|
maybe_disable_bridge(Type, Name, Conf);
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
?SLOG(warning, #{ msg => "updating_a_non-exist_bridge_need_create_a_new_one"
|
?SLOG(warning, #{
|
||||||
, type => Type, name => Name, config => Conf}),
|
msg => "updating_a_non-exist_bridge_need_create_a_new_one",
|
||||||
|
type => Type,
|
||||||
|
name => Name,
|
||||||
|
config => Conf
|
||||||
|
}),
|
||||||
create(Type, Name, Conf);
|
create(Type, Name, Conf);
|
||||||
{error, Reason} -> {error, {update_bridge_failed, Reason}}
|
{error, Reason} ->
|
||||||
|
{error, {update_bridge_failed, Reason}}
|
||||||
end;
|
end;
|
||||||
true ->
|
true ->
|
||||||
%% we don't need to recreate the bridge if this config change is only to
|
%% 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, emqx:get_config([bridges, Type, Name])).
|
||||||
|
|
||||||
recreate(Type, Name, Conf) ->
|
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),
|
emqx_bridge:resource_type(Type),
|
||||||
parse_confs(Type, Name, Conf),
|
parse_confs(Type, Name, Conf),
|
||||||
#{}).
|
#{}
|
||||||
|
).
|
||||||
|
|
||||||
create_dry_run(Type, Conf) ->
|
create_dry_run(Type, Conf) ->
|
||||||
|
Conf0 = Conf#{
|
||||||
Conf0 = Conf#{<<"egress">> =>
|
<<"egress">> =>
|
||||||
#{ <<"remote_topic">> => <<"t">>
|
#{
|
||||||
, <<"remote_qos">> => 0
|
<<"remote_topic">> => <<"t">>,
|
||||||
, <<"retain">> => true
|
<<"remote_qos">> => 0,
|
||||||
, <<"payload">> => <<"val">>
|
<<"retain">> => true,
|
||||||
},
|
<<"payload">> => <<"val">>
|
||||||
<<"ingress">> =>
|
},
|
||||||
#{ <<"remote_topic">> => <<"t">>
|
<<"ingress">> =>
|
||||||
}},
|
#{<<"remote_topic">> => <<"t">>}
|
||||||
|
},
|
||||||
case emqx_resource:check_config(emqx_bridge:resource_type(Type), Conf0) of
|
case emqx_resource:check_config(emqx_bridge:resource_type(Type), Conf0) of
|
||||||
{ok, Conf1} ->
|
{ok, Conf1} ->
|
||||||
emqx_resource:create_dry_run_local(emqx_bridge:resource_type(Type), 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
|
case emqx_resource:remove_local(resource_id(Type, Name)) of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, not_found} -> ok;
|
{error, not_found} -> ok;
|
||||||
{error, Reason} ->
|
{error, Reason} -> {error, Reason}
|
||||||
{error, Reason}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
diff_confs(NewConfs, OldConfs) ->
|
diff_confs(NewConfs, OldConfs) ->
|
||||||
emqx_map_lib:diff_maps(flatten_confs(NewConfs),
|
emqx_map_lib:diff_maps(
|
||||||
flatten_confs(OldConfs)).
|
flatten_confs(NewConfs),
|
||||||
|
flatten_confs(OldConfs)
|
||||||
|
).
|
||||||
|
|
||||||
flatten_confs(Conf0) ->
|
flatten_confs(Conf0) ->
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
lists:flatmap(fun({Type, Conf}) ->
|
lists:flatmap(
|
||||||
|
fun({Type, Conf}) ->
|
||||||
do_flatten_confs(Type, Conf)
|
do_flatten_confs(Type, Conf)
|
||||||
end, maps:to_list(Conf0))).
|
end,
|
||||||
|
maps:to_list(Conf0)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
do_flatten_confs(Type, Conf0) ->
|
do_flatten_confs(Type, Conf0) ->
|
||||||
[{{Type, Name}, Conf} || {Name, Conf} <- maps:to_list(Conf0)].
|
[{{Type, Name}, Conf} || {Name, Conf} <- maps:to_list(Conf0)].
|
||||||
|
|
||||||
get_matched_bridges(Topic) ->
|
get_matched_bridges(Topic) ->
|
||||||
Bridges = emqx:get_config([bridges], #{}),
|
Bridges = emqx:get_config([bridges], #{}),
|
||||||
maps:fold(fun (BType, Conf, Acc0) ->
|
maps:fold(
|
||||||
maps:fold(fun
|
fun(BType, Conf, Acc0) ->
|
||||||
%% Confs for MQTT, Kafka bridges have the `direction` flag
|
maps:fold(
|
||||||
(_BName, #{direction := ingress}, Acc1) ->
|
fun
|
||||||
Acc1;
|
%% Confs for MQTT, Kafka bridges have the `direction` flag
|
||||||
(BName, #{direction := egress} = Egress, Acc1) ->
|
(_BName, #{direction := ingress}, Acc1) ->
|
||||||
%% HTTP, MySQL bridges only have egress direction
|
Acc1;
|
||||||
get_matched_bridge_id(Egress, Topic, BType, BName, Acc1)
|
(BName, #{direction := egress} = Egress, Acc1) ->
|
||||||
end, Acc0, Conf)
|
%% HTTP, MySQL bridges only have egress direction
|
||||||
end, [], Bridges).
|
get_matched_bridge_id(Egress, Topic, BType, BName, Acc1)
|
||||||
|
end,
|
||||||
|
Acc0,
|
||||||
|
Conf
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
Bridges
|
||||||
|
).
|
||||||
|
|
||||||
get_matched_bridge_id(#{enable := false}, _Topic, _BType, _BName, Acc) ->
|
get_matched_bridge_id(#{enable := false}, _Topic, _BType, _BName, Acc) ->
|
||||||
Acc;
|
Acc;
|
||||||
|
|
@ -351,38 +425,56 @@ get_matched_bridge_id(#{local_topic := Filter}, Topic, BType, BName, Acc) ->
|
||||||
false -> Acc
|
false -> Acc
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_confs(http, _Name,
|
parse_confs(
|
||||||
#{ url := Url
|
http,
|
||||||
, method := Method
|
_Name,
|
||||||
, body := Body
|
#{
|
||||||
, headers := Headers
|
url := Url,
|
||||||
, request_timeout := ReqTimeout
|
method := Method,
|
||||||
} = Conf) ->
|
body := Body,
|
||||||
|
headers := Headers,
|
||||||
|
request_timeout := ReqTimeout
|
||||||
|
} = Conf
|
||||||
|
) ->
|
||||||
{BaseUrl, Path} = parse_url(Url),
|
{BaseUrl, Path} = parse_url(Url),
|
||||||
{ok, BaseUrl2} = emqx_http_lib:uri_parse(BaseUrl),
|
{ok, BaseUrl2} = emqx_http_lib:uri_parse(BaseUrl),
|
||||||
Conf#{ base_url => BaseUrl2
|
Conf#{
|
||||||
, request =>
|
base_url => BaseUrl2,
|
||||||
#{ path => Path
|
request =>
|
||||||
, method => Method
|
#{
|
||||||
, body => Body
|
path => Path,
|
||||||
, headers => Headers
|
method => Method,
|
||||||
, request_timeout => ReqTimeout
|
body => Body,
|
||||||
}
|
headers => Headers,
|
||||||
};
|
request_timeout => ReqTimeout
|
||||||
parse_confs(Type, Name, #{connector := ConnId, direction := Direction} = Conf)
|
}
|
||||||
when is_binary(ConnId) ->
|
};
|
||||||
|
parse_confs(Type, Name, #{connector := ConnId, direction := Direction} = Conf) when
|
||||||
|
is_binary(ConnId)
|
||||||
|
->
|
||||||
case emqx_connector:parse_connector_id(ConnId) of
|
case emqx_connector:parse_connector_id(ConnId) of
|
||||||
{Type, ConnName} ->
|
{Type, ConnName} ->
|
||||||
ConnectorConfs = emqx:get_config([connectors, Type, ConnName]),
|
ConnectorConfs = emqx:get_config([connectors, Type, ConnName]),
|
||||||
make_resource_confs(Direction, ConnectorConfs,
|
make_resource_confs(
|
||||||
maps:without([connector, direction], Conf), Type, Name);
|
Direction,
|
||||||
|
ConnectorConfs,
|
||||||
|
maps:without([connector, direction], Conf),
|
||||||
|
Type,
|
||||||
|
Name
|
||||||
|
);
|
||||||
{_ConnType, _ConnName} ->
|
{_ConnType, _ConnName} ->
|
||||||
error({cannot_use_connector_with_different_type, ConnId})
|
error({cannot_use_connector_with_different_type, ConnId})
|
||||||
end;
|
end;
|
||||||
parse_confs(Type, Name, #{connector := ConnectorConfs, direction := Direction} = Conf)
|
parse_confs(Type, Name, #{connector := ConnectorConfs, direction := Direction} = Conf) when
|
||||||
when is_map(ConnectorConfs) ->
|
is_map(ConnectorConfs)
|
||||||
make_resource_confs(Direction, ConnectorConfs,
|
->
|
||||||
maps:without([connector, direction], Conf), Type, Name).
|
make_resource_confs(
|
||||||
|
Direction,
|
||||||
|
ConnectorConfs,
|
||||||
|
maps:without([connector, direction], Conf),
|
||||||
|
Type,
|
||||||
|
Name
|
||||||
|
).
|
||||||
|
|
||||||
make_resource_confs(ingress, ConnectorConfs, BridgeConf, Type, Name) ->
|
make_resource_confs(ingress, ConnectorConfs, BridgeConf, Type, Name) ->
|
||||||
BName = bridge_id(Type, Name),
|
BName = bridge_id(Type, Name),
|
||||||
|
|
@ -417,39 +509,48 @@ if_only_to_toggle_enable(OldConf, Conf) ->
|
||||||
#{added := Added, removed := Removed, changed := Updated} =
|
#{added := Added, removed := Removed, changed := Updated} =
|
||||||
emqx_map_lib:diff_maps(OldConf, Conf),
|
emqx_map_lib:diff_maps(OldConf, Conf),
|
||||||
case {Added, Removed, Updated} of
|
case {Added, Removed, Updated} of
|
||||||
{Added, Removed, #{enable := _}= Updated}
|
{Added, Removed, #{enable := _} = Updated} when
|
||||||
when map_size(Added) =:= 0,
|
map_size(Added) =:= 0,
|
||||||
map_size(Removed) =:= 0,
|
map_size(Removed) =:= 0,
|
||||||
map_size(Updated) =:= 1 -> true;
|
map_size(Updated) =:= 1
|
||||||
{_, _, _} -> false
|
->
|
||||||
|
true;
|
||||||
|
{_, _, _} ->
|
||||||
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_basic_usage_info() ->
|
-spec get_basic_usage_info() ->
|
||||||
#{ num_bridges => non_neg_integer()
|
#{
|
||||||
, count_by_type =>
|
num_bridges => non_neg_integer(),
|
||||||
#{ BridgeType => non_neg_integer()
|
count_by_type =>
|
||||||
}
|
#{BridgeType => non_neg_integer()}
|
||||||
} when BridgeType :: atom().
|
}
|
||||||
|
when
|
||||||
|
BridgeType :: atom().
|
||||||
get_basic_usage_info() ->
|
get_basic_usage_info() ->
|
||||||
InitialAcc = #{num_bridges => 0, count_by_type => #{}},
|
InitialAcc = #{num_bridges => 0, count_by_type => #{}},
|
||||||
try
|
try
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun(#{resource_data := #{config := #{enable := false}}}, Acc) ->
|
fun
|
||||||
Acc;
|
(#{resource_data := #{config := #{enable := false}}}, Acc) ->
|
||||||
(#{type := BridgeType}, Acc) ->
|
Acc;
|
||||||
NumBridges = maps:get(num_bridges, Acc),
|
(#{type := BridgeType}, Acc) ->
|
||||||
CountByType0 = maps:get(count_by_type, Acc),
|
NumBridges = maps:get(num_bridges, Acc),
|
||||||
CountByType = maps:update_with(
|
CountByType0 = maps:get(count_by_type, Acc),
|
||||||
binary_to_atom(BridgeType, utf8),
|
CountByType = maps:update_with(
|
||||||
fun(X) -> X + 1 end,
|
binary_to_atom(BridgeType, utf8),
|
||||||
1,
|
fun(X) -> X + 1 end,
|
||||||
CountByType0),
|
1,
|
||||||
Acc#{ num_bridges => NumBridges + 1
|
CountByType0
|
||||||
, count_by_type => CountByType
|
),
|
||||||
}
|
Acc#{
|
||||||
end,
|
num_bridges => NumBridges + 1,
|
||||||
InitialAcc,
|
count_by_type => CountByType
|
||||||
list())
|
}
|
||||||
|
end,
|
||||||
|
InitialAcc,
|
||||||
|
list()
|
||||||
|
)
|
||||||
catch
|
catch
|
||||||
%% for instance, when the bridge app is not ready yet.
|
%% for instance, when the bridge app is not ready yet.
|
||||||
_:_ ->
|
_:_ ->
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,23 @@
|
||||||
-import(hoconsc, [mk/2, array/1, enum/1]).
|
-import(hoconsc, [mk/2, array/1, enum/1]).
|
||||||
|
|
||||||
%% Swagger specs from hocon schema
|
%% Swagger specs from hocon schema
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, namespace/0
|
schema/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% API callbacks
|
%% API callbacks
|
||||||
-export([ '/bridges'/2
|
-export([
|
||||||
, '/bridges/:id'/2
|
'/bridges'/2,
|
||||||
, '/bridges/:id/operation/:operation'/2
|
'/bridges/:id'/2,
|
||||||
, '/nodes/:node/bridges/:id/operation/:operation'/2
|
'/bridges/:id/operation/:operation'/2,
|
||||||
, '/bridges/:id/reset_metrics'/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]).
|
-define(TYPES, [mqtt, http]).
|
||||||
|
|
||||||
|
|
@ -51,35 +52,45 @@
|
||||||
EXPR
|
EXPR
|
||||||
catch
|
catch
|
||||||
error:{invalid_bridge_id, Id0} ->
|
error:{invalid_bridge_id, Id0} ->
|
||||||
{400, error_msg('INVALID_ID', <<"invalid_bridge_id: ", Id0/binary,
|
{400,
|
||||||
". Bridge Ids must be of format {type}:{name}">>)}
|
error_msg(
|
||||||
end).
|
'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),
|
-define(METRICS(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{
|
||||||
#{ matched => MATCH,
|
matched => MATCH,
|
||||||
success => SUCC,
|
success => SUCC,
|
||||||
failed => FAILED,
|
failed => FAILED,
|
||||||
rate => RATE,
|
rate => RATE,
|
||||||
rate_last5m => RATE_5,
|
rate_last5m => RATE_5,
|
||||||
rate_max => RATE_MAX
|
rate_max => RATE_MAX
|
||||||
}).
|
}).
|
||||||
-define(metrics(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX),
|
-define(metrics(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{
|
||||||
#{ matched := MATCH,
|
matched := MATCH,
|
||||||
success := SUCC,
|
success := SUCC,
|
||||||
failed := FAILED,
|
failed := FAILED,
|
||||||
rate := RATE,
|
rate := RATE,
|
||||||
rate_last5m := RATE_5,
|
rate_last5m := RATE_5,
|
||||||
rate_max := RATE_MAX
|
rate_max := RATE_MAX
|
||||||
}).
|
}).
|
||||||
|
|
||||||
namespace() -> "bridge".
|
namespace() -> "bridge".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
||||||
|
|
||||||
paths() -> ["/bridges", "/bridges/:id", "/bridges/:id/operation/:operation",
|
paths() ->
|
||||||
"/nodes/:node/bridges/:id/operation/:operation",
|
[
|
||||||
"/bridges/:id/reset_metrics"].
|
"/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) when is_atom(Code) ->
|
||||||
error_schema([Code], Message);
|
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).
|
emqx_dashboard_swagger:error_codes(Codes, Message).
|
||||||
|
|
||||||
get_response_body_schema() ->
|
get_response_body_schema() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(emqx_bridge_schema:get_response(),
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
bridge_info_examples(get)).
|
emqx_bridge_schema:get_response(),
|
||||||
|
bridge_info_examples(get)
|
||||||
|
).
|
||||||
|
|
||||||
param_path_operation_cluster() ->
|
param_path_operation_cluster() ->
|
||||||
{operation, mk(enum([enable, disable, stop, restart]),
|
{operation,
|
||||||
#{ in => path
|
mk(
|
||||||
, required => true
|
enum([enable, disable, stop, restart]),
|
||||||
, example => <<"start">>
|
#{
|
||||||
, desc => ?DESC("desc_param_path_operation_cluster")
|
in => path,
|
||||||
})}.
|
required => true,
|
||||||
|
example => <<"start">>,
|
||||||
|
desc => ?DESC("desc_param_path_operation_cluster")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
param_path_operation_on_node() ->
|
param_path_operation_on_node() ->
|
||||||
{operation, mk(enum([stop, restart]),
|
{operation,
|
||||||
#{ in => path
|
mk(
|
||||||
, required => true
|
enum([stop, restart]),
|
||||||
, example => <<"start">>
|
#{
|
||||||
, desc => ?DESC("desc_param_path_operation_on_node")
|
in => path,
|
||||||
})}.
|
required => true,
|
||||||
|
example => <<"start">>,
|
||||||
|
desc => ?DESC("desc_param_path_operation_on_node")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
param_path_node() ->
|
param_path_node() ->
|
||||||
{node, mk(binary(),
|
{node,
|
||||||
#{ in => path
|
mk(
|
||||||
, required => true
|
binary(),
|
||||||
, example => <<"emqx@127.0.0.1">>
|
#{
|
||||||
, desc => ?DESC("desc_param_path_node")
|
in => path,
|
||||||
})}.
|
required => true,
|
||||||
|
example => <<"emqx@127.0.0.1">>,
|
||||||
|
desc => ?DESC("desc_param_path_node")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
param_path_id() ->
|
param_path_id() ->
|
||||||
{id, mk(binary(),
|
{id,
|
||||||
#{ in => path
|
mk(
|
||||||
, required => true
|
binary(),
|
||||||
, example => <<"http:my_http_bridge">>
|
#{
|
||||||
, desc => ?DESC("desc_param_path_id")
|
in => path,
|
||||||
})}.
|
required => true,
|
||||||
|
example => <<"http:my_http_bridge">>,
|
||||||
|
desc => ?DESC("desc_param_path_id")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
bridge_info_array_example(Method) ->
|
bridge_info_array_example(Method) ->
|
||||||
[Config || #{value := Config} <- maps:values(bridge_info_examples(Method))].
|
[Config || #{value := Config} <- maps:values(bridge_info_examples(Method))].
|
||||||
|
|
@ -136,7 +165,8 @@ bridge_info_examples(Method) ->
|
||||||
}).
|
}).
|
||||||
|
|
||||||
conn_bridge_examples(Method) ->
|
conn_bridge_examples(Method) ->
|
||||||
lists:foldl(fun(Type, Acc) ->
|
lists:foldl(
|
||||||
|
fun(Type, Acc) ->
|
||||||
SType = atom_to_list(Type),
|
SType = atom_to_list(Type),
|
||||||
KeyIngress = bin(SType ++ "_ingress"),
|
KeyIngress = bin(SType ++ "_ingress"),
|
||||||
KeyEgress = bin(SType ++ "_egress"),
|
KeyEgress = bin(SType ++ "_egress"),
|
||||||
|
|
@ -150,19 +180,25 @@ conn_bridge_examples(Method) ->
|
||||||
value => info_example(Type, egress, Method)
|
value => info_example(Type, egress, Method)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end, #{}, ?CONN_TYPES).
|
end,
|
||||||
|
#{},
|
||||||
|
?CONN_TYPES
|
||||||
|
).
|
||||||
|
|
||||||
info_example(Type, Direction, Method) ->
|
info_example(Type, Direction, Method) ->
|
||||||
maps:merge(info_example_basic(Type, Direction),
|
maps:merge(
|
||||||
method_example(Type, Direction, Method)).
|
info_example_basic(Type, Direction),
|
||||||
|
method_example(Type, Direction, Method)
|
||||||
|
).
|
||||||
|
|
||||||
method_example(Type, Direction, Method) when Method == get; Method == post ->
|
method_example(Type, Direction, Method) when Method == get; Method == post ->
|
||||||
SType = atom_to_list(Type),
|
SType = atom_to_list(Type),
|
||||||
SDir = atom_to_list(Direction),
|
SDir = atom_to_list(Direction),
|
||||||
SName = case Type of
|
SName =
|
||||||
http -> "my_" ++ SType ++ "_bridge";
|
case Type of
|
||||||
_ -> "my_" ++ SDir ++ "_" ++ SType ++ "_bridge"
|
http -> "my_" ++ SType ++ "_bridge";
|
||||||
end,
|
_ -> "my_" ++ SDir ++ "_" ++ SType ++ "_bridge"
|
||||||
|
end,
|
||||||
TypeNameExamp = #{
|
TypeNameExamp = #{
|
||||||
type => bin(SType),
|
type => bin(SType),
|
||||||
name => bin(SName)
|
name => bin(SName)
|
||||||
|
|
@ -175,8 +211,10 @@ maybe_with_metrics_example(TypeNameExamp, get) ->
|
||||||
TypeNameExamp#{
|
TypeNameExamp#{
|
||||||
metrics => ?METRICS(0, 0, 0, 0, 0, 0),
|
metrics => ?METRICS(0, 0, 0, 0, 0, 0),
|
||||||
node_metrics => [
|
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, _) ->
|
maybe_with_metrics_example(TypeNameExamp, _) ->
|
||||||
|
|
@ -231,8 +269,9 @@ schema("/bridges") ->
|
||||||
description => ?DESC("desc_api1"),
|
description => ?DESC("desc_api1"),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => emqx_dashboard_swagger:schema_with_example(
|
200 => emqx_dashboard_swagger:schema_with_example(
|
||||||
array(emqx_bridge_schema:get_response()),
|
array(emqx_bridge_schema:get_response()),
|
||||||
bridge_info_array_example(get))
|
bridge_info_array_example(get)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
post => #{
|
post => #{
|
||||||
|
|
@ -240,15 +279,15 @@ schema("/bridges") ->
|
||||||
summary => <<"Create Bridge">>,
|
summary => <<"Create Bridge">>,
|
||||||
description => ?DESC("desc_api2"),
|
description => ?DESC("desc_api2"),
|
||||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||||
emqx_bridge_schema:post_request(),
|
emqx_bridge_schema:post_request(),
|
||||||
bridge_info_examples(post)),
|
bridge_info_examples(post)
|
||||||
|
),
|
||||||
responses => #{
|
responses => #{
|
||||||
201 => get_response_body_schema(),
|
201 => get_response_body_schema(),
|
||||||
400 => error_schema('ALREADY_EXISTS', "Bridge already exists")
|
400 => error_schema('ALREADY_EXISTS', "Bridge already exists")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/bridges/:id") ->
|
schema("/bridges/:id") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/bridges/:id',
|
'operationId' => '/bridges/:id',
|
||||||
|
|
@ -268,8 +307,9 @@ schema("/bridges/:id") ->
|
||||||
description => ?DESC("desc_api4"),
|
description => ?DESC("desc_api4"),
|
||||||
parameters => [param_path_id()],
|
parameters => [param_path_id()],
|
||||||
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
'requestBody' => emqx_dashboard_swagger:schema_with_examples(
|
||||||
emqx_bridge_schema:put_request(),
|
emqx_bridge_schema:put_request(),
|
||||||
bridge_info_examples(put)),
|
bridge_info_examples(put)
|
||||||
|
),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => get_response_body_schema(),
|
200 => get_response_body_schema(),
|
||||||
404 => error_schema('NOT_FOUND', "Bridge not found"),
|
404 => error_schema('NOT_FOUND', "Bridge not found"),
|
||||||
|
|
@ -287,7 +327,6 @@ schema("/bridges/:id") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/bridges/:id/reset_metrics") ->
|
schema("/bridges/:id/reset_metrics") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/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") ->
|
schema("/nodes/:node/bridges/:id/operation/:operation") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/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">>,
|
200 => <<"Operation success">>,
|
||||||
400 => error_schema('INVALID_ID', "Bad bridge ID"),
|
400 => error_schema('INVALID_ID', "Bad bridge ID"),
|
||||||
403 => error_schema('FORBIDDEN_REQUEST', "forbidden operation")
|
403 => error_schema('FORBIDDEN_REQUEST', "forbidden operation")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
@ -353,15 +390,18 @@ schema("/nodes/:node/bridges/:id/operation/:operation") ->
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
'/bridges'(get, _Params) ->
|
'/bridges'(get, _Params) ->
|
||||||
{200, zip_bridges([[format_resp(Data) || Data <- emqx_bridge_proto_v1:list_bridges(Node)]
|
{200,
|
||||||
|| Node <- mria_mnesia:running_nodes()])}.
|
zip_bridges([
|
||||||
|
[format_resp(Data) || Data <- emqx_bridge_proto_v1:list_bridges(Node)]
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
])}.
|
||||||
|
|
||||||
'/bridges/:id'(get, #{bindings := #{id := Id}}) ->
|
'/bridges/:id'(get, #{bindings := #{id := Id}}) ->
|
||||||
?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200));
|
?TRY_PARSE_ID(Id, lookup_from_all_nodes(BridgeType, BridgeName, 200));
|
||||||
|
|
||||||
'/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) ->
|
'/bridges/:id'(put, #{bindings := #{id := Id}, body := Conf0}) ->
|
||||||
Conf = filter_out_request_body(Conf0),
|
Conf = filter_out_request_body(Conf0),
|
||||||
?TRY_PARSE_ID(Id,
|
?TRY_PARSE_ID(
|
||||||
|
Id,
|
||||||
case emqx_bridge:lookup(BridgeType, BridgeName) of
|
case emqx_bridge:lookup(BridgeType, BridgeName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
case ensure_bridge_created(BridgeType, BridgeName, Conf) of
|
case ensure_bridge_created(BridgeType, BridgeName, Conf) of
|
||||||
|
|
@ -371,24 +411,31 @@ schema("/nodes/:node/bridges/:id/operation/:operation") ->
|
||||||
{400, Error}
|
{400, Error}
|
||||||
end;
|
end;
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, error_msg('NOT_FOUND',<<"bridge not found">>)}
|
{404, error_msg('NOT_FOUND', <<"bridge not found">>)}
|
||||||
end);
|
end
|
||||||
|
);
|
||||||
'/bridges/:id'(delete, #{bindings := #{id := Id}}) ->
|
'/bridges/:id'(delete, #{bindings := #{id := Id}}) ->
|
||||||
?TRY_PARSE_ID(Id,
|
?TRY_PARSE_ID(
|
||||||
case emqx_conf:remove(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
Id,
|
||||||
#{override_to => cluster}) of
|
case
|
||||||
|
emqx_conf:remove(
|
||||||
|
emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
||||||
|
#{override_to => cluster}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, _} -> {204};
|
{ok, _} -> {204};
|
||||||
{error, Reason} ->
|
{error, Reason} -> {500, error_msg('INTERNAL_ERROR', Reason)}
|
||||||
{500, error_msg('INTERNAL_ERROR', Reason)}
|
end
|
||||||
end).
|
).
|
||||||
|
|
||||||
'/bridges/:id/reset_metrics'(put, #{bindings := #{id := Id}}) ->
|
'/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
|
case emqx_bridge:reset_metrics(emqx_bridge:resource_id(BridgeType, BridgeName)) of
|
||||||
ok -> {200, <<"Reset success">>};
|
ok -> {200, <<"Reset success">>};
|
||||||
Reason -> {400, error_msg('BAD_REQUEST', Reason)}
|
Reason -> {400, error_msg('BAD_REQUEST', Reason)}
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) ->
|
lookup_from_all_nodes(BridgeType, BridgeName, SuccCode) ->
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
|
|
@ -407,40 +454,58 @@ lookup_from_local_node(BridgeType, BridgeName) ->
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
'/bridges/:id/operation/:operation'(post, #{bindings :=
|
'/bridges/:id/operation/:operation'(post, #{
|
||||||
#{id := Id, operation := Op}}) ->
|
bindings :=
|
||||||
?TRY_PARSE_ID(Id, case operation_func(Op) of
|
#{id := Id, operation := Op}
|
||||||
invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)};
|
}) ->
|
||||||
OperFunc when OperFunc == enable; OperFunc == disable ->
|
?TRY_PARSE_ID(
|
||||||
case emqx_conf:update(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
Id,
|
||||||
{OperFunc, BridgeType, BridgeName}, #{override_to => cluster}) of
|
case operation_func(Op) of
|
||||||
{ok, _} -> {200};
|
invalid ->
|
||||||
{error, {pre_config_update, _, bridge_not_found}} ->
|
{400, error_msg('BAD_REQUEST', <<"invalid operation">>)};
|
||||||
{404, error_msg('NOT_FOUND', <<"bridge not found">>)};
|
OperFunc when OperFunc == enable; OperFunc == disable ->
|
||||||
{error, Reason} ->
|
case
|
||||||
{500, error_msg('INTERNAL_ERROR', Reason)}
|
emqx_conf:update(
|
||||||
end;
|
emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
||||||
OperFunc ->
|
{OperFunc, BridgeType, BridgeName},
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
#{override_to => cluster}
|
||||||
operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName)
|
)
|
||||||
end).
|
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 :=
|
'/nodes/:node/bridges/:id/operation/:operation'(post, #{
|
||||||
#{id := Id, operation := Op}}) ->
|
bindings :=
|
||||||
?TRY_PARSE_ID(Id, case operation_func(Op) of
|
#{id := Id, operation := Op}
|
||||||
invalid -> {400, error_msg('BAD_REQUEST', <<"invalid operation">>)};
|
}) ->
|
||||||
OperFunc when OperFunc == restart; OperFunc == stop ->
|
?TRY_PARSE_ID(
|
||||||
ConfMap = emqx:get_config([bridges, BridgeType, BridgeName]),
|
Id,
|
||||||
case maps:get(enable, ConfMap, false) of
|
case operation_func(Op) of
|
||||||
false -> {403, error_msg('FORBIDDEN_REQUEST', <<"forbidden operation">>)};
|
invalid ->
|
||||||
true ->
|
{400, error_msg('BAD_REQUEST', <<"invalid operation">>)};
|
||||||
case emqx_bridge:OperFunc(BridgeType, BridgeName) of
|
OperFunc when OperFunc == restart; OperFunc == stop ->
|
||||||
ok -> {200};
|
ConfMap = emqx:get_config([bridges, BridgeType, BridgeName]),
|
||||||
{error, Reason} ->
|
case maps:get(enable, ConfMap, false) of
|
||||||
{500, error_msg('INTERNAL_ERROR', Reason)}
|
false ->
|
||||||
end
|
{403, error_msg('FORBIDDEN_REQUEST', <<"forbidden operation">>)};
|
||||||
end
|
true ->
|
||||||
end).
|
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(<<"stop">>) -> stop;
|
||||||
operation_func(<<"restart">>) -> restart;
|
operation_func(<<"restart">>) -> restart;
|
||||||
|
|
@ -449,10 +514,11 @@ operation_func(<<"disable">>) -> disable;
|
||||||
operation_func(_) -> invalid.
|
operation_func(_) -> invalid.
|
||||||
|
|
||||||
operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) ->
|
operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) ->
|
||||||
RpcFunc = case OperFunc of
|
RpcFunc =
|
||||||
restart -> restart_bridges_to_all_nodes;
|
case OperFunc of
|
||||||
stop -> stop_bridges_to_all_nodes
|
restart -> restart_bridges_to_all_nodes;
|
||||||
end,
|
stop -> stop_bridges_to_all_nodes
|
||||||
|
end,
|
||||||
case is_ok(emqx_bridge_proto_v1:RpcFunc(Nodes, BridgeType, BridgeName)) of
|
case is_ok(emqx_bridge_proto_v1:RpcFunc(Nodes, BridgeType, BridgeName)) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{200};
|
{200};
|
||||||
|
|
@ -461,48 +527,70 @@ operation_to_all_nodes(Nodes, OperFunc, BridgeType, BridgeName) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
ensure_bridge_created(BridgeType, BridgeName, Conf) ->
|
ensure_bridge_created(BridgeType, BridgeName, Conf) ->
|
||||||
case emqx_conf:update(emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
case
|
||||||
Conf, #{override_to => cluster}) of
|
emqx_conf:update(
|
||||||
|
emqx_bridge:config_key_path() ++ [BridgeType, BridgeName],
|
||||||
|
Conf,
|
||||||
|
#{override_to => cluster}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, _} -> ok;
|
{ok, _} -> ok;
|
||||||
{error, Reason} ->
|
{error, Reason} -> {error, error_msg('BAD_REQUEST', Reason)}
|
||||||
{error, error_msg('BAD_REQUEST', Reason)}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
zip_bridges([BridgesFirstNode | _] = BridgesAllNodes) ->
|
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),
|
Bridges = pick_bridges_by_id(Type, Name, BridgesAllNodes),
|
||||||
[format_bridge_info(Bridges) | Acc]
|
[format_bridge_info(Bridges) | Acc]
|
||||||
end, [], BridgesFirstNode).
|
end,
|
||||||
|
[],
|
||||||
|
BridgesFirstNode
|
||||||
|
).
|
||||||
|
|
||||||
pick_bridges_by_id(Type, Name, BridgesAllNodes) ->
|
pick_bridges_by_id(Type, Name, BridgesAllNodes) ->
|
||||||
lists:foldl(fun(BridgesOneNode, Acc) ->
|
lists:foldl(
|
||||||
case [Bridge || Bridge = #{type := Type0, name := Name0} <- BridgesOneNode,
|
fun(BridgesOneNode, Acc) ->
|
||||||
Type0 == Type, Name0 == Name] of
|
case
|
||||||
[BridgeInfo] -> [BridgeInfo | Acc];
|
[
|
||||||
|
Bridge
|
||||||
|
|| Bridge = #{type := Type0, name := Name0} <- BridgesOneNode,
|
||||||
|
Type0 == Type,
|
||||||
|
Name0 == Name
|
||||||
|
]
|
||||||
|
of
|
||||||
|
[BridgeInfo] ->
|
||||||
|
[BridgeInfo | Acc];
|
||||||
[] ->
|
[] ->
|
||||||
?SLOG(warning, #{msg => "bridge_inconsistent_in_cluster",
|
?SLOG(warning, #{
|
||||||
bridge => emqx_bridge:bridge_id(Type, Name)}),
|
msg => "bridge_inconsistent_in_cluster",
|
||||||
|
bridge => emqx_bridge:bridge_id(Type, Name)
|
||||||
|
}),
|
||||||
Acc
|
Acc
|
||||||
end
|
end
|
||||||
end, [], BridgesAllNodes).
|
end,
|
||||||
|
[],
|
||||||
|
BridgesAllNodes
|
||||||
|
).
|
||||||
|
|
||||||
format_bridge_info([FirstBridge | _] = Bridges) ->
|
format_bridge_info([FirstBridge | _] = Bridges) ->
|
||||||
Res = maps:remove(node, FirstBridge),
|
Res = maps:remove(node, FirstBridge),
|
||||||
NodeStatus = collect_status(Bridges),
|
NodeStatus = collect_status(Bridges),
|
||||||
NodeMetrics = collect_metrics(Bridges),
|
NodeMetrics = collect_metrics(Bridges),
|
||||||
Res#{ status => aggregate_status(NodeStatus)
|
Res#{
|
||||||
, node_status => NodeStatus
|
status => aggregate_status(NodeStatus),
|
||||||
, metrics => aggregate_metrics(NodeMetrics)
|
node_status => NodeStatus,
|
||||||
, node_metrics => NodeMetrics
|
metrics => aggregate_metrics(NodeMetrics),
|
||||||
}.
|
node_metrics => NodeMetrics
|
||||||
|
}.
|
||||||
|
|
||||||
collect_status(Bridges) ->
|
collect_status(Bridges) ->
|
||||||
[maps:with([node, status], B) || B <- Bridges].
|
[maps:with([node, status], B) || B <- Bridges].
|
||||||
|
|
||||||
aggregate_status(AllStatus) ->
|
aggregate_status(AllStatus) ->
|
||||||
Head = fun ([A | _]) -> A end,
|
Head = fun([A | _]) -> A end,
|
||||||
HeadVal = maps:get(status, Head(AllStatus), connecting),
|
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
|
case AllRes of
|
||||||
true -> HeadVal;
|
true -> HeadVal;
|
||||||
false -> inconsistent
|
false -> inconsistent
|
||||||
|
|
@ -512,15 +600,31 @@ collect_metrics(Bridges) ->
|
||||||
[maps:with([node, metrics], B) || B <- Bridges].
|
[maps:with([node, metrics], B) || B <- Bridges].
|
||||||
|
|
||||||
aggregate_metrics(AllMetrics) ->
|
aggregate_metrics(AllMetrics) ->
|
||||||
InitMetrics = ?METRICS(0,0,0,0,0,0),
|
InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0),
|
||||||
lists:foldl(fun(#{metrics := ?metrics(Match1, Succ1, Failed1, Rate1, Rate5m1, RateMax1)},
|
lists:foldl(
|
||||||
?metrics(Match0, Succ0, Failed0, Rate0, Rate5m0, RateMax0)) ->
|
fun(
|
||||||
?METRICS(Match1 + Match0, Succ1 + Succ0, Failed1 + Failed0,
|
#{metrics := ?metrics(Match1, Succ1, Failed1, Rate1, Rate5m1, RateMax1)},
|
||||||
Rate1 + Rate0, Rate5m1 + Rate5m0, RateMax1 + RateMax0)
|
?metrics(Match0, Succ0, Failed0, Rate0, Rate5m0, RateMax0)
|
||||||
end, InitMetrics, AllMetrics).
|
) ->
|
||||||
|
?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,
|
format_resp(#{
|
||||||
resource_data := #{status := Status, metrics := Metrics}}) ->
|
type := Type,
|
||||||
|
name := BridgeName,
|
||||||
|
raw_config := RawConf,
|
||||||
|
resource_data := #{status := Status, metrics := Metrics}
|
||||||
|
}) ->
|
||||||
RawConfFull = fill_defaults(Type, RawConf),
|
RawConfFull = fill_defaults(Type, RawConf),
|
||||||
RawConfFull#{
|
RawConfFull#{
|
||||||
type => Type,
|
type => Type,
|
||||||
|
|
@ -531,10 +635,11 @@ format_resp(#{type := Type, name := BridgeName, raw_config := RawConf,
|
||||||
}.
|
}.
|
||||||
|
|
||||||
format_metrics(#{
|
format_metrics(#{
|
||||||
counters := #{failed := Failed, exception := Ex, matched := Match, success := Succ},
|
counters := #{failed := Failed, exception := Ex, matched := Match, success := Succ},
|
||||||
rate := #{
|
rate := #{
|
||||||
matched := #{current := Rate, last5m := Rate5m, max := RateMax}
|
matched := #{current := Rate, last5m := Rate5m, max := RateMax}
|
||||||
} }) ->
|
}
|
||||||
|
}) ->
|
||||||
?METRICS(Match, Succ, Failed + Ex, Rate, Rate5m, RateMax).
|
?METRICS(Match, Succ, Failed + Ex, Rate, Rate5m, RateMax).
|
||||||
|
|
||||||
fill_defaults(Type, RawConf) ->
|
fill_defaults(Type, RawConf) ->
|
||||||
|
|
@ -551,14 +656,31 @@ unpack_bridge_conf(Type, PackedConf) ->
|
||||||
RawConf.
|
RawConf.
|
||||||
|
|
||||||
is_ok(ResL) ->
|
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]};
|
[] -> {ok, [Res || {ok, Res} <- ResL]};
|
||||||
ErrL -> {error, ErrL}
|
ErrL -> {error, ErrL}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
filter_out_request_body(Conf) ->
|
filter_out_request_body(Conf) ->
|
||||||
ExtraConfs = [<<"id">>, <<"type">>, <<"name">>, <<"status">>, <<"node_status">>,
|
ExtraConfs = [
|
||||||
<<"node_metrics">>, <<"metrics">>, <<"node">>],
|
<<"id">>,
|
||||||
|
<<"type">>,
|
||||||
|
<<"name">>,
|
||||||
|
<<"status">>,
|
||||||
|
<<"node_status">>,
|
||||||
|
<<"node_metrics">>,
|
||||||
|
<<"metrics">>,
|
||||||
|
<<"node">>
|
||||||
|
],
|
||||||
maps:without(ExtraConfs, Conf).
|
maps:without(ExtraConfs, Conf).
|
||||||
|
|
||||||
error_msg(Code, Msg) when is_binary(Msg) ->
|
error_msg(Code, Msg) when is_binary(Msg) ->
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@
|
||||||
|
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
-export([ pre_config_update/3
|
-export([
|
||||||
, post_config_update/5
|
pre_config_update/3,
|
||||||
]).
|
post_config_update/5
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TOP_LELVE_HDLR_PATH, (emqx_bridge:config_key_path())).
|
-define(TOP_LELVE_HDLR_PATH, (emqx_bridge:config_key_path())).
|
||||||
-define(LEAF_NODE_HDLR_PATH, (emqx_bridge:config_key_path() ++ ['?', '?'])).
|
-define(LEAF_NODE_HDLR_PATH, (emqx_bridge:config_key_path() ++ ['?', '?'])).
|
||||||
|
|
|
||||||
|
|
@ -15,45 +15,66 @@ roots() -> [].
|
||||||
|
|
||||||
fields("config") ->
|
fields("config") ->
|
||||||
basic_config() ++
|
basic_config() ++
|
||||||
[ {url, mk(binary(),
|
[
|
||||||
#{ required => true
|
{url,
|
||||||
, desc => ?DESC("config_url")
|
mk(
|
||||||
})}
|
binary(),
|
||||||
, {local_topic, mk(binary(),
|
#{
|
||||||
#{ desc => ?DESC("config_local_topic")
|
required => true,
|
||||||
})}
|
desc => ?DESC("config_url")
|
||||||
, {method, mk(method(),
|
}
|
||||||
#{ default => post
|
)},
|
||||||
, desc => ?DESC("config_method")
|
{local_topic,
|
||||||
})}
|
mk(
|
||||||
, {headers, mk(map(),
|
binary(),
|
||||||
#{ default => #{
|
#{desc => ?DESC("config_local_topic")}
|
||||||
<<"accept">> => <<"application/json">>,
|
)},
|
||||||
<<"cache-control">> => <<"no-cache">>,
|
{method,
|
||||||
<<"connection">> => <<"keep-alive">>,
|
mk(
|
||||||
<<"content-type">> => <<"application/json">>,
|
method(),
|
||||||
<<"keep-alive">> => <<"timeout=5">>}
|
#{
|
||||||
, desc => ?DESC("config_headers")
|
default => post,
|
||||||
})
|
desc => ?DESC("config_method")
|
||||||
}
|
}
|
||||||
, {body, mk(binary(),
|
)},
|
||||||
#{ default => <<"${payload}">>
|
{headers,
|
||||||
, desc => ?DESC("config_body")
|
mk(
|
||||||
})}
|
map(),
|
||||||
, {request_timeout, mk(emqx_schema:duration_ms(),
|
#{
|
||||||
#{ default => <<"15s">>
|
default => #{
|
||||||
, desc => ?DESC("config_request_timeout")
|
<<"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") ->
|
fields("post") ->
|
||||||
[ type_field()
|
[
|
||||||
, name_field()
|
type_field(),
|
||||||
|
name_field()
|
||||||
] ++ fields("config");
|
] ++ fields("config");
|
||||||
|
|
||||||
fields("put") ->
|
fields("put") ->
|
||||||
fields("config");
|
fields("config");
|
||||||
|
|
||||||
fields("get") ->
|
fields("get") ->
|
||||||
emqx_bridge_schema:metrics_status_fields() ++ fields("post").
|
emqx_bridge_schema:metrics_status_fields() ++ fields("post").
|
||||||
|
|
||||||
|
|
@ -65,32 +86,47 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
basic_config() ->
|
basic_config() ->
|
||||||
[ {enable,
|
[
|
||||||
mk(boolean(),
|
{enable,
|
||||||
#{ desc => ?DESC("config_enable")
|
mk(
|
||||||
, default => true
|
boolean(),
|
||||||
})}
|
#{
|
||||||
, {direction,
|
desc => ?DESC("config_enable"),
|
||||||
mk(egress,
|
default => true
|
||||||
#{ desc => ?DESC("config_direction")
|
}
|
||||||
, default => egress
|
)},
|
||||||
})}
|
{direction,
|
||||||
]
|
mk(
|
||||||
++ proplists:delete(base_url, emqx_connector_http:fields(config)).
|
egress,
|
||||||
|
#{
|
||||||
|
desc => ?DESC("config_direction"),
|
||||||
|
default => egress
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
] ++
|
||||||
|
proplists:delete(base_url, emqx_connector_http:fields(config)).
|
||||||
|
|
||||||
%%======================================================================================
|
%%======================================================================================
|
||||||
|
|
||||||
type_field() ->
|
type_field() ->
|
||||||
{type, mk(http,
|
{type,
|
||||||
#{ required => true
|
mk(
|
||||||
, desc => ?DESC("desc_type")
|
http,
|
||||||
})}.
|
#{
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC("desc_type")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
name_field() ->
|
name_field() ->
|
||||||
{name, mk(binary(),
|
{name,
|
||||||
#{ required => true
|
mk(
|
||||||
, desc => ?DESC("desc_name")
|
binary(),
|
||||||
})}.
|
#{
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC("desc_name")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
method() ->
|
method() ->
|
||||||
enum([post, put, get, delete]).
|
enum([post, put, get, delete]).
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,20 @@
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
%% API functions
|
%% API functions
|
||||||
-export([ start_link/0
|
-export([
|
||||||
, ensure_all_started/1
|
start_link/0,
|
||||||
]).
|
ensure_all_started/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([init/1,
|
-export([
|
||||||
handle_call/3,
|
init/1,
|
||||||
handle_cast/2,
|
handle_call/3,
|
||||||
handle_info/2,
|
handle_cast/2,
|
||||||
terminate/2,
|
handle_info/2,
|
||||||
code_change/3]).
|
terminate/2,
|
||||||
|
code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
-record(state, {}).
|
-record(state, {}).
|
||||||
|
|
||||||
|
|
@ -52,7 +55,6 @@ handle_call(_Request, _From, State) ->
|
||||||
handle_cast({start_and_monitor, Configs}, State) ->
|
handle_cast({start_and_monitor, Configs}, State) ->
|
||||||
ok = load_bridges(Configs),
|
ok = load_bridges(Configs),
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
handle_cast(_Msg, State) ->
|
handle_cast(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
@ -67,13 +69,22 @@ code_change(_OldVsn, State, _Extra) ->
|
||||||
|
|
||||||
%%============================================================================
|
%%============================================================================
|
||||||
load_bridges(Configs) ->
|
load_bridges(Configs) ->
|
||||||
lists:foreach(fun({Type, NamedConf}) ->
|
lists:foreach(
|
||||||
lists:foreach(fun({Name, Conf}) ->
|
fun({Type, NamedConf}) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({Name, Conf}) ->
|
||||||
_Res = emqx_bridge:create(Type, Name, Conf),
|
_Res = emqx_bridge:create(Type, Name, Conf),
|
||||||
?tp(emqx_bridge_monitor_loaded_bridge,
|
?tp(
|
||||||
#{ type => Type
|
emqx_bridge_monitor_loaded_bridge,
|
||||||
, name => Name
|
#{
|
||||||
, res => _Res
|
type => Type,
|
||||||
})
|
name => Name,
|
||||||
end, maps:to_list(NamedConf))
|
res => _Res
|
||||||
end, maps:to_list(Configs)).
|
}
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
maps:to_list(NamedConf)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
maps:to_list(Configs)
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -12,31 +12,27 @@
|
||||||
roots() -> [].
|
roots() -> [].
|
||||||
|
|
||||||
fields("ingress") ->
|
fields("ingress") ->
|
||||||
[ emqx_bridge_schema:direction_field(ingress, emqx_connector_mqtt_schema:ingress_desc())
|
[emqx_bridge_schema:direction_field(ingress, emqx_connector_mqtt_schema:ingress_desc())] ++
|
||||||
]
|
emqx_bridge_schema:common_bridge_fields() ++
|
||||||
++ emqx_bridge_schema:common_bridge_fields()
|
proplists:delete(hookpoint, emqx_connector_mqtt_schema:fields("ingress"));
|
||||||
++ proplists:delete(hookpoint, emqx_connector_mqtt_schema:fields("ingress"));
|
|
||||||
|
|
||||||
fields("egress") ->
|
fields("egress") ->
|
||||||
[ emqx_bridge_schema:direction_field(egress, emqx_connector_mqtt_schema:egress_desc())
|
[emqx_bridge_schema:direction_field(egress, emqx_connector_mqtt_schema:egress_desc())] ++
|
||||||
]
|
emqx_bridge_schema:common_bridge_fields() ++
|
||||||
++ emqx_bridge_schema:common_bridge_fields()
|
emqx_connector_mqtt_schema:fields("egress");
|
||||||
++ emqx_connector_mqtt_schema:fields("egress");
|
|
||||||
|
|
||||||
fields("post_ingress") ->
|
fields("post_ingress") ->
|
||||||
[ type_field()
|
[
|
||||||
, name_field()
|
type_field(),
|
||||||
|
name_field()
|
||||||
] ++ proplists:delete(enable, fields("ingress"));
|
] ++ proplists:delete(enable, fields("ingress"));
|
||||||
fields("post_egress") ->
|
fields("post_egress") ->
|
||||||
[ type_field()
|
[
|
||||||
, name_field()
|
type_field(),
|
||||||
|
name_field()
|
||||||
] ++ proplists:delete(enable, fields("egress"));
|
] ++ proplists:delete(enable, fields("egress"));
|
||||||
|
|
||||||
fields("put_ingress") ->
|
fields("put_ingress") ->
|
||||||
proplists:delete(enable, fields("ingress"));
|
proplists:delete(enable, fields("ingress"));
|
||||||
fields("put_egress") ->
|
fields("put_egress") ->
|
||||||
proplists:delete(enable, fields("egress"));
|
proplists:delete(enable, fields("egress"));
|
||||||
|
|
||||||
fields("get_ingress") ->
|
fields("get_ingress") ->
|
||||||
emqx_bridge_schema:metrics_status_fields() ++ fields("post_ingress");
|
emqx_bridge_schema:metrics_status_fields() ++ fields("post_ingress");
|
||||||
fields("get_egress") ->
|
fields("get_egress") ->
|
||||||
|
|
@ -49,13 +45,21 @@ desc(_) ->
|
||||||
|
|
||||||
%%======================================================================================
|
%%======================================================================================
|
||||||
type_field() ->
|
type_field() ->
|
||||||
{type, mk(mqtt,
|
{type,
|
||||||
#{ required => true
|
mk(
|
||||||
, desc => ?DESC("desc_type")
|
mqtt,
|
||||||
})}.
|
#{
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC("desc_type")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
name_field() ->
|
name_field() ->
|
||||||
{name, mk(binary(),
|
{name,
|
||||||
#{ required => true
|
mk(
|
||||||
, desc => ?DESC("desc_name")
|
binary(),
|
||||||
})}.
|
#{
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC("desc_name")
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,17 @@
|
||||||
|
|
||||||
-export([roots/0, fields/1, desc/1, namespace/0]).
|
-export([roots/0, fields/1, desc/1, namespace/0]).
|
||||||
|
|
||||||
-export([ get_response/0
|
-export([
|
||||||
, put_request/0
|
get_response/0,
|
||||||
, post_request/0
|
put_request/0,
|
||||||
]).
|
post_request/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ common_bridge_fields/0
|
-export([
|
||||||
, metrics_status_fields/0
|
common_bridge_fields/0,
|
||||||
, direction_field/2
|
metrics_status_fields/0,
|
||||||
]).
|
direction_field/2
|
||||||
|
]).
|
||||||
|
|
||||||
%%======================================================================================
|
%%======================================================================================
|
||||||
%% Hocon Schema Definitions
|
%% Hocon Schema Definitions
|
||||||
|
|
@ -34,43 +36,68 @@ post_request() ->
|
||||||
http_schema("post").
|
http_schema("post").
|
||||||
|
|
||||||
http_schema(Method) ->
|
http_schema(Method) ->
|
||||||
Schemas = lists:flatmap(fun(Type) ->
|
Schemas = lists:flatmap(
|
||||||
[ref(schema_mod(Type), Method ++ "_ingress"),
|
fun(Type) ->
|
||||||
ref(schema_mod(Type), Method ++ "_egress")]
|
[
|
||||||
end, ?CONN_TYPES),
|
ref(schema_mod(Type), Method ++ "_ingress"),
|
||||||
hoconsc:union([ref(emqx_bridge_http_schema, Method)
|
ref(schema_mod(Type), Method ++ "_egress")
|
||||||
| Schemas]).
|
]
|
||||||
|
end,
|
||||||
|
?CONN_TYPES
|
||||||
|
),
|
||||||
|
hoconsc:union([
|
||||||
|
ref(emqx_bridge_http_schema, Method)
|
||||||
|
| Schemas
|
||||||
|
]).
|
||||||
|
|
||||||
common_bridge_fields() ->
|
common_bridge_fields() ->
|
||||||
[ {enable,
|
[
|
||||||
mk(boolean(),
|
{enable,
|
||||||
#{ desc => ?DESC("desc_enable")
|
mk(
|
||||||
, default => true
|
boolean(),
|
||||||
})}
|
#{
|
||||||
, {connector,
|
desc => ?DESC("desc_enable"),
|
||||||
mk(binary(),
|
default => true
|
||||||
#{ required => true
|
}
|
||||||
, example => <<"mqtt:my_mqtt_connector">>
|
)},
|
||||||
, desc => ?DESC("desc_connector")
|
{connector,
|
||||||
})}
|
mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
required => true,
|
||||||
|
example => <<"mqtt:my_mqtt_connector">>,
|
||||||
|
desc => ?DESC("desc_connector")
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
metrics_status_fields() ->
|
metrics_status_fields() ->
|
||||||
[ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})}
|
[
|
||||||
, {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")),
|
{"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})},
|
||||||
#{ desc => ?DESC("desc_node_metrics")})}
|
{"node_metrics",
|
||||||
, {"status", mk(status(), #{desc => ?DESC("desc_status")})}
|
mk(
|
||||||
, {"node_status", mk(hoconsc:array(ref(?MODULE, "node_status")),
|
hoconsc:array(ref(?MODULE, "node_metrics")),
|
||||||
#{ desc => ?DESC("desc_node_status")})}
|
#{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_field(Dir, Desc) ->
|
||||||
{direction, mk(Dir,
|
{direction,
|
||||||
#{ required => true
|
mk(
|
||||||
, default => egress
|
Dir,
|
||||||
, desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.</br>"
|
#{
|
||||||
++ Desc
|
required => true,
|
||||||
})}.
|
default => egress,
|
||||||
|
desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.</br>" ++
|
||||||
|
Desc
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
%%======================================================================================
|
%%======================================================================================
|
||||||
%% For config files
|
%% For config files
|
||||||
|
|
@ -80,31 +107,49 @@ namespace() -> "bridge".
|
||||||
roots() -> [bridges].
|
roots() -> [bridges].
|
||||||
|
|
||||||
fields(bridges) ->
|
fields(bridges) ->
|
||||||
[{http, mk(hoconsc:map(name, ref(emqx_bridge_http_schema, "config")),
|
[
|
||||||
#{desc => ?DESC("bridges_http")})}]
|
{http,
|
||||||
++ [{T, mk(hoconsc:map(name, hoconsc:union([ ref(schema_mod(T), "ingress")
|
mk(
|
||||||
, ref(schema_mod(T), "egress")
|
hoconsc:map(name, ref(emqx_bridge_http_schema, "config")),
|
||||||
])),
|
#{desc => ?DESC("bridges_http")}
|
||||||
#{desc => ?DESC("bridges_name")})} || T <- ?CONN_TYPES];
|
)}
|
||||||
|
] ++
|
||||||
|
[
|
||||||
|
{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") ->
|
fields("metrics") ->
|
||||||
[ {"matched", mk(integer(), #{desc => ?DESC("metric_matched")})}
|
[
|
||||||
, {"success", mk(integer(), #{desc => ?DESC("metric_success")})}
|
{"matched", mk(integer(), #{desc => ?DESC("metric_matched")})},
|
||||||
, {"failed", mk(integer(), #{desc => ?DESC("metric_failed")})}
|
{"success", mk(integer(), #{desc => ?DESC("metric_success")})},
|
||||||
, {"rate", mk(float(), #{desc => ?DESC("metric_rate")})}
|
{"failed", mk(integer(), #{desc => ?DESC("metric_failed")})},
|
||||||
, {"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})}
|
{"rate", mk(float(), #{desc => ?DESC("metric_rate")})},
|
||||||
, {"rate_last5m", mk(float(),
|
{"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})},
|
||||||
#{desc => ?DESC("metric_rate_last5m")})}
|
{"rate_last5m",
|
||||||
|
mk(
|
||||||
|
float(),
|
||||||
|
#{desc => ?DESC("metric_rate_last5m")}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("node_metrics") ->
|
fields("node_metrics") ->
|
||||||
[ node_name()
|
[
|
||||||
, {"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
node_name(),
|
||||||
|
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("node_status") ->
|
fields("node_status") ->
|
||||||
[ node_name()
|
[
|
||||||
, {"status", mk(status(), #{})}
|
node_name(),
|
||||||
|
{"status", mk(status(), #{})}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc(bridges) ->
|
desc(bridges) ->
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,19 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
SupFlags = #{strategy => one_for_one,
|
SupFlags = #{
|
||||||
intensity => 10,
|
strategy => one_for_one,
|
||||||
period => 10},
|
intensity => 10,
|
||||||
|
period => 10
|
||||||
|
},
|
||||||
ChildSpecs = [
|
ChildSpecs = [
|
||||||
#{id => emqx_bridge_monitor,
|
#{
|
||||||
start => {emqx_bridge_monitor, start_link, []},
|
id => emqx_bridge_monitor,
|
||||||
restart => permanent,
|
start => {emqx_bridge_monitor, start_link, []},
|
||||||
type => worker,
|
restart => permanent,
|
||||||
modules => [emqx_bridge_monitor]}
|
type => worker,
|
||||||
|
modules => [emqx_bridge_monitor]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
{ok, {SupFlags, ChildSpecs}}.
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
|
introduced_in/0,
|
||||||
|
|
||||||
, list_bridges/1
|
list_bridges/1,
|
||||||
, lookup_from_all_nodes/3
|
lookup_from_all_nodes/3,
|
||||||
, restart_bridges_to_all_nodes/3
|
restart_bridges_to_all_nodes/3,
|
||||||
, stop_bridges_to_all_nodes/3
|
stop_bridges_to_all_nodes/3
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
|
@ -40,19 +41,34 @@ list_bridges(Node) ->
|
||||||
-type key() :: atom() | binary() | [byte()].
|
-type key() :: atom() | binary() | [byte()].
|
||||||
|
|
||||||
-spec restart_bridges_to_all_nodes([node()], key(), key()) ->
|
-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) ->
|
restart_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) ->
|
||||||
erpc:multicall(Nodes, emqx_bridge, restart,
|
erpc:multicall(
|
||||||
[BridgeType, BridgeName], ?TIMEOUT).
|
Nodes,
|
||||||
|
emqx_bridge,
|
||||||
|
restart,
|
||||||
|
[BridgeType, BridgeName],
|
||||||
|
?TIMEOUT
|
||||||
|
).
|
||||||
|
|
||||||
-spec stop_bridges_to_all_nodes([node()], key(), key()) ->
|
-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) ->
|
stop_bridges_to_all_nodes(Nodes, BridgeType, BridgeName) ->
|
||||||
erpc:multicall(Nodes, emqx_bridge, stop,
|
erpc:multicall(
|
||||||
[BridgeType, BridgeName], ?TIMEOUT).
|
Nodes,
|
||||||
|
emqx_bridge,
|
||||||
|
stop,
|
||||||
|
[BridgeType, BridgeName],
|
||||||
|
?TIMEOUT
|
||||||
|
).
|
||||||
|
|
||||||
-spec lookup_from_all_nodes([node()], key(), key()) ->
|
-spec lookup_from_all_nodes([node()], key(), key()) ->
|
||||||
emqx_rpc:erpc_multicall().
|
emqx_rpc:erpc_multicall().
|
||||||
lookup_from_all_nodes(Nodes, BridgeType, BridgeName) ->
|
lookup_from_all_nodes(Nodes, BridgeType, BridgeName) ->
|
||||||
erpc:multicall(Nodes, emqx_bridge_api, lookup_from_local_node,
|
erpc:multicall(
|
||||||
[BridgeType, BridgeName], ?TIMEOUT).
|
Nodes,
|
||||||
|
emqx_bridge_api,
|
||||||
|
lookup_from_local_node,
|
||||||
|
[BridgeType, BridgeName],
|
||||||
|
?TIMEOUT
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
%% to avoid inter-suite dependencies
|
%% to avoid inter-suite dependencies
|
||||||
|
|
@ -32,8 +32,12 @@ init_per_suite(Config) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_common_test_helpers:stop_apps([emqx, emqx_bridge,
|
emqx_common_test_helpers:stop_apps([
|
||||||
emqx_resource, emqx_connector]).
|
emqx,
|
||||||
|
emqx_bridge,
|
||||||
|
emqx_resource,
|
||||||
|
emqx_connector
|
||||||
|
]).
|
||||||
|
|
||||||
init_per_testcase(t_get_basic_usage_info_1, Config) ->
|
init_per_testcase(t_get_basic_usage_info_1, Config) ->
|
||||||
setup_fake_telemetry_data(),
|
setup_fake_telemetry_data(),
|
||||||
|
|
@ -43,13 +47,15 @@ init_per_testcase(_TestCase, Config) ->
|
||||||
|
|
||||||
end_per_testcase(t_get_basic_usage_info_1, _Config) ->
|
end_per_testcase(t_get_basic_usage_info_1, _Config) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun({BridgeType, BridgeName}) ->
|
fun({BridgeType, BridgeName}) ->
|
||||||
ok = emqx_bridge:remove(BridgeType, BridgeName)
|
ok = emqx_bridge:remove(BridgeType, BridgeName)
|
||||||
end,
|
end,
|
||||||
[ {http, <<"basic_usage_info_http">>}
|
[
|
||||||
, {http, <<"basic_usage_info_http_disabled">>}
|
{http, <<"basic_usage_info_http">>},
|
||||||
, {mqtt, <<"basic_usage_info_mqtt">>}
|
{http, <<"basic_usage_info_http_disabled">>},
|
||||||
]),
|
{mqtt, <<"basic_usage_info_mqtt">>}
|
||||||
|
]
|
||||||
|
),
|
||||||
ok = emqx_config:delete_override_conf_files(),
|
ok = emqx_config:delete_override_conf_files(),
|
||||||
ok = emqx_config:put([bridges], #{}),
|
ok = emqx_config:put([bridges], #{}),
|
||||||
ok = emqx_config:put_raw([bridges], #{}),
|
ok = emqx_config:put_raw([bridges], #{}),
|
||||||
|
|
@ -59,53 +65,68 @@ end_per_testcase(_TestCase, _Config) ->
|
||||||
|
|
||||||
t_get_basic_usage_info_0(_Config) ->
|
t_get_basic_usage_info_0(_Config) ->
|
||||||
?assertEqual(
|
?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) ->
|
t_get_basic_usage_info_1(_Config) ->
|
||||||
BasicUsageInfo = emqx_bridge:get_basic_usage_info(),
|
BasicUsageInfo = emqx_bridge:get_basic_usage_info(),
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
#{ num_bridges => 2
|
#{
|
||||||
, count_by_type => #{ http => 1
|
num_bridges => 2,
|
||||||
, mqtt => 1
|
count_by_type => #{
|
||||||
}
|
http => 1,
|
||||||
|
mqtt => 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
BasicUsageInfo).
|
BasicUsageInfo
|
||||||
|
).
|
||||||
|
|
||||||
setup_fake_telemetry_data() ->
|
setup_fake_telemetry_data() ->
|
||||||
ConnectorConf =
|
ConnectorConf =
|
||||||
#{<<"connectors">> =>
|
#{
|
||||||
#{<<"mqtt">> => #{<<"my_mqtt_connector">> =>
|
<<"connectors">> =>
|
||||||
#{ server => "127.0.0.1:1883" }}}},
|
#{
|
||||||
MQTTConfig = #{ connector => <<"mqtt:my_mqtt_connector">>
|
<<"mqtt">> => #{
|
||||||
, enable => true
|
<<"my_mqtt_connector">> =>
|
||||||
, direction => ingress
|
#{server => "127.0.0.1:1883"}
|
||||||
, 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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_connector_schema, ConnectorConf),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, Conf),
|
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, Conf),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,15 @@
|
||||||
-define(CONF_DEFAULT, <<"bridges: {}">>).
|
-define(CONF_DEFAULT, <<"bridges: {}">>).
|
||||||
-define(BRIDGE_TYPE, <<"http">>).
|
-define(BRIDGE_TYPE, <<"http">>).
|
||||||
-define(BRIDGE_NAME, <<"test_bridge">>).
|
-define(BRIDGE_NAME, <<"test_bridge">>).
|
||||||
-define(URL(PORT, PATH), list_to_binary(
|
-define(URL(PORT, PATH),
|
||||||
io_lib:format("http://localhost:~s/~s",
|
list_to_binary(
|
||||||
[integer_to_list(PORT), PATH]))).
|
io_lib:format(
|
||||||
-define(HTTP_BRIDGE(URL, TYPE, NAME),
|
"http://localhost:~s/~s",
|
||||||
#{
|
[integer_to_list(PORT), PATH]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
-define(HTTP_BRIDGE(URL, TYPE, NAME), #{
|
||||||
<<"type">> => TYPE,
|
<<"type">> => TYPE,
|
||||||
<<"name">> => NAME,
|
<<"name">> => NAME,
|
||||||
<<"url">> => URL,
|
<<"url">> => URL,
|
||||||
|
|
@ -40,7 +44,6 @@
|
||||||
<<"headers">> => #{
|
<<"headers">> => #{
|
||||||
<<"content-type">> => <<"application/json">>
|
<<"content-type">> => <<"application/json">>
|
||||||
}
|
}
|
||||||
|
|
||||||
}).
|
}).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
|
|
@ -50,15 +53,17 @@ groups() ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
suite() ->
|
suite() ->
|
||||||
[{timetrap,{seconds,60}}].
|
[{timetrap, {seconds, 60}}].
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
%% some testcases (may from other app) already get emqx_connector started
|
%% some testcases (may from other app) already get emqx_connector started
|
||||||
_ = application:stop(emqx_resource),
|
_ = application:stop(emqx_resource),
|
||||||
_ = application:stop(emqx_connector),
|
_ = application:stop(emqx_connector),
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_bridge, emqx_dashboard],
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
fun set_special_configs/1),
|
[emqx_bridge, emqx_dashboard],
|
||||||
|
fun set_special_configs/1
|
||||||
|
),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT),
|
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
@ -79,9 +84,12 @@ end_per_testcase(_, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
clear_resources() ->
|
clear_resources() ->
|
||||||
lists:foreach(fun(#{type := Type, name := Name}) ->
|
lists:foreach(
|
||||||
|
fun(#{type := Type, name := Name}) ->
|
||||||
ok = emqx_bridge:remove(Type, Name)
|
ok = emqx_bridge:remove(Type, Name)
|
||||||
end, emqx_bridge:list()).
|
end,
|
||||||
|
emqx_bridge:list()
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% HTTP server for testing
|
%% HTTP server for testing
|
||||||
|
|
@ -95,12 +103,12 @@ start_http_server(HandleFun) ->
|
||||||
end),
|
end),
|
||||||
receive
|
receive
|
||||||
{port, Port} -> Port
|
{port, Port} -> Port
|
||||||
after
|
after 2000 -> error({timeout, start_http_server})
|
||||||
2000 -> error({timeout, start_http_server})
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
listen_on_random_port() ->
|
listen_on_random_port() ->
|
||||||
Min = 1024, Max = 65000,
|
Min = 1024,
|
||||||
|
Max = 65000,
|
||||||
Port = rand:uniform(Max - Min) + Min,
|
Port = rand:uniform(Max - Min) + Min,
|
||||||
case gen_tcp:listen(Port, [{active, false}, {reuseaddr, true}, binary]) of
|
case gen_tcp:listen(Port, [{active, false}, {reuseaddr, true}, binary]) of
|
||||||
{ok, Sock} -> {Port, Sock};
|
{ok, Sock} -> {Port, Sock};
|
||||||
|
|
@ -109,16 +117,18 @@ listen_on_random_port() ->
|
||||||
|
|
||||||
loop(Sock, HandleFun, Parent) ->
|
loop(Sock, HandleFun, Parent) ->
|
||||||
{ok, Conn} = gen_tcp:accept(Sock),
|
{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),
|
gen_tcp:controlling_process(Conn, Handler),
|
||||||
loop(Sock, HandleFun, Parent).
|
loop(Sock, HandleFun, Parent).
|
||||||
|
|
||||||
make_response(CodeStr, Str) ->
|
make_response(CodeStr, Str) ->
|
||||||
B = iolist_to_binary(Str),
|
B = iolist_to_binary(Str),
|
||||||
iolist_to_binary(
|
iolist_to_binary(
|
||||||
io_lib:fwrite(
|
io_lib:fwrite(
|
||||||
"HTTP/1.0 ~s\r\nContent-Type: text/html\r\nContent-Length: ~p\r\n\r\n~s",
|
"HTTP/1.0 ~s\r\nContent-Type: text/html\r\nContent-Length: ~p\r\n\r\n~s",
|
||||||
[CodeStr, size(B), B])).
|
[CodeStr, size(B), B]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
handle_fun_200_ok(Conn, Parent) ->
|
handle_fun_200_ok(Conn, Parent) ->
|
||||||
case gen_tcp:recv(Conn, 0) of
|
case gen_tcp:recv(Conn, 0) of
|
||||||
|
|
@ -151,18 +161,22 @@ t_http_crud_apis(_) ->
|
||||||
%% then we add a http bridge, using POST
|
%% then we add a http bridge, using POST
|
||||||
%% POST /bridges/ will create a bridge
|
%% POST /bridges/ will create a bridge
|
||||||
URL1 = ?URL(Port, "path1"),
|
URL1 = ?URL(Port, "path1"),
|
||||||
{ok, 201, Bridge} = request(post, uri(["bridges"]),
|
{ok, 201, Bridge} = request(
|
||||||
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
|
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
|
),
|
||||||
|
|
||||||
%ct:pal("---bridge: ~p", [Bridge]),
|
%ct:pal("---bridge: ~p", [Bridge]),
|
||||||
#{ <<"type">> := ?BRIDGE_TYPE
|
#{
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"status">> := _
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"node_status">> := [_|_]
|
<<"status">> := _,
|
||||||
, <<"metrics">> := _
|
<<"node_status">> := [_ | _],
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"metrics">> := _,
|
||||||
, <<"url">> := URL1
|
<<"node_metrics">> := [_ | _],
|
||||||
} = jsx:decode(Bridge),
|
<<"url">> := URL1
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
|
|
||||||
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
||||||
%% send an message to emqx and the message should be forwarded to the HTTP server
|
%% 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)),
|
emqx:publish(emqx_message:make(<<"emqx_http/1">>, Body)),
|
||||||
?assert(
|
?assert(
|
||||||
receive
|
receive
|
||||||
{http_server, received, #{method := <<"POST">>, path := <<"/path1">>,
|
{http_server, received, #{
|
||||||
body := Body}} ->
|
method := <<"POST">>,
|
||||||
|
path := <<"/path1">>,
|
||||||
|
body := Body
|
||||||
|
}} ->
|
||||||
true;
|
true;
|
||||||
Msg ->
|
Msg ->
|
||||||
ct:pal("error: http got unexpected request: ~p", [Msg]),
|
ct:pal("error: http got unexpected request: ~p", [Msg]),
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
%% update the request-path of the bridge
|
%% update the request-path of the bridge
|
||||||
URL2 = ?URL(Port, "path2"),
|
URL2 = ?URL(Port, "path2"),
|
||||||
{ok, 200, Bridge2} = request(put, uri(["bridges", BridgeID]),
|
{ok, 200, Bridge2} = request(
|
||||||
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
put,
|
||||||
?assertMatch(#{ <<"type">> := ?BRIDGE_TYPE
|
uri(["bridges", BridgeID]),
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
, <<"status">> := _
|
),
|
||||||
, <<"node_status">> := [_|_]
|
?assertMatch(
|
||||||
, <<"metrics">> := _
|
#{
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"url">> := URL2
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
}, jsx:decode(Bridge2)),
|
<<"status">> := _,
|
||||||
|
<<"node_status">> := [_ | _],
|
||||||
|
<<"metrics">> := _,
|
||||||
|
<<"node_metrics">> := [_ | _],
|
||||||
|
<<"url">> := URL2
|
||||||
|
},
|
||||||
|
jsx:decode(Bridge2)
|
||||||
|
),
|
||||||
|
|
||||||
%% list all bridges again, assert Bridge2 is in it
|
%% list all bridges again, assert Bridge2 is in it
|
||||||
{ok, 200, Bridge2Str} = request(get, uri(["bridges"]), []),
|
{ok, 200, Bridge2Str} = request(get, uri(["bridges"]), []),
|
||||||
?assertMatch([#{ <<"type">> := ?BRIDGE_TYPE
|
?assertMatch(
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
[
|
||||||
, <<"status">> := _
|
#{
|
||||||
, <<"node_status">> := [_|_]
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"metrics">> := _
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"status">> := _,
|
||||||
, <<"url">> := URL2
|
<<"node_status">> := [_ | _],
|
||||||
}], jsx:decode(Bridge2Str)),
|
<<"metrics">> := _,
|
||||||
|
<<"node_metrics">> := [_ | _],
|
||||||
|
<<"url">> := URL2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
jsx:decode(Bridge2Str)
|
||||||
|
),
|
||||||
|
|
||||||
%% get the bridge by id
|
%% get the bridge by id
|
||||||
{ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge3Str} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"type">> := ?BRIDGE_TYPE
|
?assertMatch(
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
#{
|
||||||
, <<"status">> := _
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"node_status">> := [_|_]
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"metrics">> := _
|
<<"status">> := _,
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"node_status">> := [_ | _],
|
||||||
, <<"url">> := URL2
|
<<"metrics">> := _,
|
||||||
}, jsx:decode(Bridge3Str)),
|
<<"node_metrics">> := [_ | _],
|
||||||
|
<<"url">> := URL2
|
||||||
|
},
|
||||||
|
jsx:decode(Bridge3Str)
|
||||||
|
),
|
||||||
|
|
||||||
%% send an message to emqx again, check the path has been changed
|
%% send an message to emqx again, check the path has been changed
|
||||||
emqx:publish(emqx_message:make(<<"emqx_http/1">>, Body)),
|
emqx:publish(emqx_message:make(<<"emqx_http/1">>, Body)),
|
||||||
|
|
@ -225,25 +260,35 @@ t_http_crud_apis(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
||||||
|
|
||||||
%% update a deleted bridge returns an error
|
%% update a deleted bridge returns an error
|
||||||
{ok, 404, ErrMsg2} = request(put, uri(["bridges", BridgeID]),
|
{ok, 404, ErrMsg2} = request(
|
||||||
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
put,
|
||||||
|
uri(["bridges", BridgeID]),
|
||||||
|
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
#{ <<"code">> := _
|
#{
|
||||||
, <<"message">> := <<"bridge not found">>
|
<<"code">> := _,
|
||||||
}, jsx:decode(ErrMsg2)),
|
<<"message">> := <<"bridge not found">>
|
||||||
|
},
|
||||||
|
jsx:decode(ErrMsg2)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_start_stop_bridges(_) ->
|
t_start_stop_bridges(_) ->
|
||||||
lists:foreach(fun(Type) ->
|
lists:foreach(
|
||||||
|
fun(Type) ->
|
||||||
do_start_stop_bridges(Type)
|
do_start_stop_bridges(Type)
|
||||||
end, [node, cluster]).
|
end,
|
||||||
|
[node, cluster]
|
||||||
|
).
|
||||||
|
|
||||||
do_start_stop_bridges(Type) ->
|
do_start_stop_bridges(Type) ->
|
||||||
%% assert we there's no bridges at first
|
%% 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),
|
Port = start_http_server(fun handle_fun_200_ok/2),
|
||||||
URL1 = ?URL(Port, "abc"),
|
URL1 = ?URL(Port, "abc"),
|
||||||
{ok, 201, Bridge} = request(post, uri(["bridges"]),
|
{ok, 201, Bridge} = request(
|
||||||
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
|
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
|
),
|
||||||
%ct:pal("the bridge ==== ~p", [Bridge]),
|
%ct:pal("the bridge ==== ~p", [Bridge]),
|
||||||
#{ <<"type">> := ?BRIDGE_TYPE
|
#{
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"status">> := <<"connected">>
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"node_status">> := [_|_]
|
<<"status">> := <<"connected">>,
|
||||||
, <<"metrics">> := _
|
<<"node_status">> := [_ | _],
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"metrics">> := _,
|
||||||
, <<"url">> := URL1
|
<<"node_metrics">> := [_ | _],
|
||||||
} = jsx:decode(Bridge),
|
<<"url">> := URL1
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
||||||
%% stop it
|
%% stop it
|
||||||
{ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"disconnected">>
|
?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)),
|
||||||
}, jsx:decode(Bridge2)),
|
|
||||||
%% start again
|
%% start again
|
||||||
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)),
|
||||||
}, jsx:decode(Bridge3)),
|
|
||||||
%% restart an already started bridge
|
%% restart an already started bridge
|
||||||
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)),
|
||||||
}, jsx:decode(Bridge3)),
|
|
||||||
%% stop it again
|
%% stop it again
|
||||||
{ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
|
||||||
%% restart a stopped bridge
|
%% restart a stopped bridge
|
||||||
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)),
|
||||||
}, jsx:decode(Bridge4)),
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
||||||
|
|
@ -295,33 +340,34 @@ t_enable_disable_bridges(_) ->
|
||||||
|
|
||||||
Port = start_http_server(fun handle_fun_200_ok/2),
|
Port = start_http_server(fun handle_fun_200_ok/2),
|
||||||
URL1 = ?URL(Port, "abc"),
|
URL1 = ?URL(Port, "abc"),
|
||||||
{ok, 201, Bridge} = request(post, uri(["bridges"]),
|
{ok, 201, Bridge} = request(
|
||||||
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
|
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
|
),
|
||||||
%ct:pal("the bridge ==== ~p", [Bridge]),
|
%ct:pal("the bridge ==== ~p", [Bridge]),
|
||||||
#{ <<"type">> := ?BRIDGE_TYPE
|
#{
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"status">> := <<"connected">>
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"node_status">> := [_|_]
|
<<"status">> := <<"connected">>,
|
||||||
, <<"metrics">> := _
|
<<"node_status">> := [_ | _],
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"metrics">> := _,
|
||||||
, <<"url">> := URL1
|
<<"node_metrics">> := [_ | _],
|
||||||
} = jsx:decode(Bridge),
|
<<"url">> := URL1
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
||||||
%% disable it
|
%% disable it
|
||||||
{ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"disconnected">>
|
?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)),
|
||||||
}, jsx:decode(Bridge2)),
|
|
||||||
%% enable again
|
%% enable again
|
||||||
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)),
|
||||||
}, jsx:decode(Bridge3)),
|
|
||||||
%% enable an already started bridge
|
%% enable an already started bridge
|
||||||
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge3)),
|
||||||
}, jsx:decode(Bridge3)),
|
|
||||||
%% disable it again
|
%% disable it again
|
||||||
{ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>),
|
||||||
|
|
||||||
|
|
@ -331,8 +377,7 @@ t_enable_disable_bridges(_) ->
|
||||||
%% enable a stopped bridge
|
%% enable a stopped bridge
|
||||||
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
|
||||||
{ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []),
|
{ok, 200, Bridge4} = request(get, uri(["bridges", BridgeID]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(Bridge4)),
|
||||||
}, jsx:decode(Bridge4)),
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
|
||||||
|
|
@ -343,17 +388,21 @@ t_reset_bridges(_) ->
|
||||||
|
|
||||||
Port = start_http_server(fun handle_fun_200_ok/2),
|
Port = start_http_server(fun handle_fun_200_ok/2),
|
||||||
URL1 = ?URL(Port, "abc"),
|
URL1 = ?URL(Port, "abc"),
|
||||||
{ok, 201, Bridge} = request(post, uri(["bridges"]),
|
{ok, 201, Bridge} = request(
|
||||||
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)),
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
|
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME)
|
||||||
|
),
|
||||||
%ct:pal("the bridge ==== ~p", [Bridge]),
|
%ct:pal("the bridge ==== ~p", [Bridge]),
|
||||||
#{ <<"type">> := ?BRIDGE_TYPE
|
#{
|
||||||
, <<"name">> := ?BRIDGE_NAME
|
<<"type">> := ?BRIDGE_TYPE,
|
||||||
, <<"status">> := <<"connected">>
|
<<"name">> := ?BRIDGE_NAME,
|
||||||
, <<"node_status">> := [_|_]
|
<<"status">> := <<"connected">>,
|
||||||
, <<"metrics">> := _
|
<<"node_status">> := [_ | _],
|
||||||
, <<"node_metrics">> := [_|_]
|
<<"metrics">> := _,
|
||||||
, <<"url">> := URL1
|
<<"node_metrics">> := [_ | _],
|
||||||
} = jsx:decode(Bridge),
|
<<"url">> := URL1
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
BridgeID = emqx_bridge:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME),
|
||||||
{ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []),
|
{ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,16 @@
|
||||||
-define(REDIS_DEFAULT_PORT, 6379).
|
-define(REDIS_DEFAULT_PORT, 6379).
|
||||||
-define(PGSQL_DEFAULT_PORT, 5432).
|
-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].`
|
-define(SERVERS_DESC,
|
||||||
For each Node should be: ").
|
"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), "
|
-define(SERVER_DESC(TYPE, DEFAULT_PORT),
|
||||||
The IPv4 or IPv6 address or the hostname to connect to.</br>
|
"\n"
|
||||||
A host entry has the following form: `Host[:Port]`.</br>
|
"The IPv4 or IPv6 address or the hostname to connect to.</br>\n"
|
||||||
The " ++ TYPE ++ " default port " ++ DEFAULT_PORT ++ " is used if `[:Port]` is not specified."
|
"A host entry has the following form: `Host[:Port]`.</br>\n"
|
||||||
|
"The " ++ TYPE ++ " default port " ++ DEFAULT_PORT ++ " is used if `[:Port]` is not specified."
|
||||||
).
|
).
|
||||||
|
|
||||||
-define(THROW_ERROR(Str), erlang:throw({error, Str})).
|
-define(THROW_ERROR(Str), erlang:throw({error, Str})).
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{erl_opts, [
|
{erl_opts, [
|
||||||
nowarn_unused_import,
|
nowarn_unused_import,
|
||||||
debug_info
|
debug_info
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{deps, [
|
{deps, [
|
||||||
{emqx, {path, "../emqx"}},
|
{emqx, {path, "../emqx"}},
|
||||||
{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}},
|
{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}},
|
||||||
{mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}},
|
{mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}},
|
||||||
{epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}},
|
{epgsql, {git, "https://github.com/emqx/epgsql", {tag, "4.7-emqx.2"}}},
|
||||||
%% NOTE: mind poolboy version when updating mongodb-erlang version
|
%% NOTE: mind poolboy version when updating mongodb-erlang version
|
||||||
{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}},
|
{mongodb, {git, "https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}},
|
||||||
%% NOTE: mind poolboy version when updating eredis_cluster version
|
%% NOTE: mind poolboy version when updating eredis_cluster version
|
||||||
{eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.1"}}},
|
{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
|
%% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git
|
||||||
%% (which has overflow_ttl feature added).
|
%% (which has overflow_ttl feature added).
|
||||||
%% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07).
|
%% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07).
|
||||||
%% By accident, We have always been using the upstream fork due to
|
%% By accident, We have always been using the upstream fork due to
|
||||||
%% eredis_cluster's dependency getting resolved earlier.
|
%% eredis_cluster's dependency getting resolved earlier.
|
||||||
%% Here we pin 1.5.2 to avoid surprises in the future.
|
%% Here we pin 1.5.2 to avoid surprises in the future.
|
||||||
{poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}},
|
{poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}},
|
||||||
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}}
|
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.5.0"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{shell, [
|
{shell, [
|
||||||
% {config, "config/sys.config"},
|
% {config, "config/sys.config"},
|
||||||
{apps, [emqx_connector]}
|
{apps, [emqx_connector]}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_connector,
|
{application, emqx_connector, [
|
||||||
[{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.1"},
|
{vsn, "0.1.1"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_connector_app, []}},
|
{mod, {emqx_connector_app, []}},
|
||||||
{applications,
|
{applications, [
|
||||||
[kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
ecpool,
|
ecpool,
|
||||||
emqx_resource,
|
emqx_resource,
|
||||||
eredis_cluster,
|
eredis_cluster,
|
||||||
eredis,
|
eredis,
|
||||||
epgsql,
|
epgsql,
|
||||||
eldap2,
|
eldap2,
|
||||||
mysql,
|
mysql,
|
||||||
mongodb,
|
mongodb,
|
||||||
ehttpc,
|
ehttpc,
|
||||||
emqx,
|
emqx,
|
||||||
emqtt
|
emqtt
|
||||||
]},
|
]},
|
||||||
{env,[]},
|
{env, []},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache 2.0"]},
|
{licenses, ["Apache 2.0"]},
|
||||||
{links, []}
|
{links, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,27 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_connector).
|
-module(emqx_connector).
|
||||||
|
|
||||||
-export([ config_key_path/0
|
-export([
|
||||||
, pre_config_update/3
|
config_key_path/0,
|
||||||
, post_config_update/5
|
pre_config_update/3,
|
||||||
]).
|
post_config_update/5
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ parse_connector_id/1
|
-export([
|
||||||
, connector_id/2
|
parse_connector_id/1,
|
||||||
]).
|
connector_id/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ list_raw/0
|
-export([
|
||||||
, lookup_raw/1
|
list_raw/0,
|
||||||
, lookup_raw/2
|
lookup_raw/1,
|
||||||
, create_dry_run/2
|
lookup_raw/2,
|
||||||
, update/2
|
create_dry_run/2,
|
||||||
, update/3
|
update/2,
|
||||||
, delete/1
|
update/3,
|
||||||
, delete/2
|
delete/1,
|
||||||
]).
|
delete/2
|
||||||
|
]).
|
||||||
|
|
||||||
config_key_path() ->
|
config_key_path() ->
|
||||||
[connectors].
|
[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)})
|
throw({dependency_bridges_exist, emqx_bridge:bridge_id(BType, BName)})
|
||||||
end),
|
end),
|
||||||
_ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf)
|
_ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf)
|
||||||
catch throw:Error -> {error, Error}
|
catch
|
||||||
|
throw:Error -> {error, Error}
|
||||||
end;
|
end;
|
||||||
post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) ->
|
post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) ->
|
||||||
ConnId = connector_id(Type, Name),
|
ConnId = connector_id(Type, Name),
|
||||||
foreach_linked_bridges(ConnId,
|
foreach_linked_bridges(
|
||||||
|
ConnId,
|
||||||
fun(#{type := BType, name := BName}) ->
|
fun(#{type := BType, name := BName}) ->
|
||||||
BridgeConf = emqx:get_config([bridges, BType, BName]),
|
BridgeConf = emqx:get_config([bridges, BType, BName]),
|
||||||
case emqx_bridge:update(BType, BName, {BridgeConf#{connector => OldConf},
|
case
|
||||||
BridgeConf#{connector => NewConf}}) of
|
emqx_bridge:update(
|
||||||
|
BType,
|
||||||
|
BName,
|
||||||
|
{BridgeConf#{connector => OldConf}, BridgeConf#{connector => NewConf}}
|
||||||
|
)
|
||||||
|
of
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
{error, Reason} -> error({update_bridge_error, Reason})
|
{error, Reason} -> error({update_bridge_error, Reason})
|
||||||
end
|
end
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
connector_id(Type0, Name0) ->
|
connector_id(Type0, Name0) ->
|
||||||
Type = bin(Type0),
|
Type = bin(Type0),
|
||||||
|
|
@ -80,13 +91,22 @@ parse_connector_id(ConnectorId) ->
|
||||||
|
|
||||||
list_raw() ->
|
list_raw() ->
|
||||||
case get_raw_connector_conf() of
|
case get_raw_connector_conf() of
|
||||||
not_found -> [];
|
not_found ->
|
||||||
|
[];
|
||||||
Config ->
|
Config ->
|
||||||
lists:foldl(fun({Type, NameAndConf}, Connectors) ->
|
lists:foldl(
|
||||||
lists:foldl(fun({Name, RawConf}, Acc) ->
|
fun({Type, NameAndConf}, Connectors) ->
|
||||||
[RawConf#{<<"type">> => Type, <<"name">> => Name} | Acc]
|
lists:foldl(
|
||||||
end, Connectors, maps:to_list(NameAndConf))
|
fun({Name, RawConf}, Acc) ->
|
||||||
end, [], maps:to_list(Config))
|
[RawConf#{<<"type">> => Type, <<"name">> => Name} | Acc]
|
||||||
|
end,
|
||||||
|
Connectors,
|
||||||
|
maps:to_list(NameAndConf)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
maps:to_list(Config)
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup_raw(Id) when is_binary(Id) ->
|
lookup_raw(Id) when is_binary(Id) ->
|
||||||
|
|
@ -96,7 +116,8 @@ lookup_raw(Id) when is_binary(Id) ->
|
||||||
lookup_raw(Type, Name) ->
|
lookup_raw(Type, Name) ->
|
||||||
Path = [bin(P) || P <- [Type, Name]],
|
Path = [bin(P) || P <- [Type, Name]],
|
||||||
case get_raw_connector_conf() of
|
case get_raw_connector_conf() of
|
||||||
not_found -> {error, not_found};
|
not_found ->
|
||||||
|
{error, not_found};
|
||||||
Conf ->
|
Conf ->
|
||||||
case emqx_map_lib:deep_get(Path, Conf, not_found) of
|
case emqx_map_lib:deep_get(Path, Conf, not_found) of
|
||||||
not_found -> {error, not_found};
|
not_found -> {error, not_found};
|
||||||
|
|
@ -123,7 +144,8 @@ delete(Type, Name) ->
|
||||||
|
|
||||||
get_raw_connector_conf() ->
|
get_raw_connector_conf() ->
|
||||||
case emqx:get_raw_config(config_key_path(), not_found) of
|
case emqx:get_raw_config(config_key_path(), not_found) of
|
||||||
not_found -> not_found;
|
not_found ->
|
||||||
|
not_found;
|
||||||
RawConf ->
|
RawConf ->
|
||||||
#{<<"connectors">> := Conf} =
|
#{<<"connectors">> := Conf} =
|
||||||
emqx_config:fill_defaults(#{<<"connectors">> => RawConf}),
|
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).
|
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
|
||||||
|
|
||||||
foreach_linked_bridges(ConnId, Do) ->
|
foreach_linked_bridges(ConnId, Do) ->
|
||||||
lists:foreach(fun
|
lists:foreach(
|
||||||
(#{raw_config := #{<<"connector">> := ConnId0}} = Bridge) when ConnId0 == ConnId ->
|
fun
|
||||||
Do(Bridge);
|
(#{raw_config := #{<<"connector">> := ConnId0}} = Bridge) when ConnId0 == ConnId ->
|
||||||
(_) -> ok
|
Do(Bridge);
|
||||||
end, emqx_bridge:list()).
|
(_) ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
emqx_bridge:list()
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,14 @@
|
||||||
EXPR
|
EXPR
|
||||||
catch
|
catch
|
||||||
error:{invalid_connector_id, Id0} ->
|
error:{invalid_connector_id, Id0} ->
|
||||||
{400, #{code => 'INVALID_ID', message => <<"invalid_connector_id: ", Id0/binary,
|
{400, #{
|
||||||
". Connector Ids must be of format {type}:{name}">>}}
|
code => 'INVALID_ID',
|
||||||
end).
|
message =>
|
||||||
|
<<"invalid_connector_id: ", Id0/binary,
|
||||||
|
". Connector Ids must be of format {type}:{name}">>
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
namespace() -> "connector".
|
namespace() -> "connector".
|
||||||
|
|
||||||
|
|
@ -58,21 +63,25 @@ error_schema(Codes, Message) when is_binary(Message) ->
|
||||||
|
|
||||||
put_request_body_schema() ->
|
put_request_body_schema() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
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() ->
|
post_request_body_schema() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
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() ->
|
get_response_body_schema() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
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) ->
|
connector_info_array_example(Method) ->
|
||||||
[Config || #{value := Config} <- maps:values(connector_info_examples(Method))].
|
[Config || #{value := Config} <- maps:values(connector_info_examples(Method))].
|
||||||
|
|
||||||
connector_info_examples(Method) ->
|
connector_info_examples(Method) ->
|
||||||
lists:foldl(fun(Type, Acc) ->
|
lists:foldl(
|
||||||
|
fun(Type, Acc) ->
|
||||||
SType = atom_to_list(Type),
|
SType = atom_to_list(Type),
|
||||||
maps:merge(Acc, #{
|
maps:merge(Acc, #{
|
||||||
Type => #{
|
Type => #{
|
||||||
|
|
@ -80,11 +89,16 @@ connector_info_examples(Method) ->
|
||||||
value => info_example(Type, Method)
|
value => info_example(Type, Method)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end, #{}, ?CONN_TYPES).
|
end,
|
||||||
|
#{},
|
||||||
|
?CONN_TYPES
|
||||||
|
).
|
||||||
|
|
||||||
info_example(Type, Method) ->
|
info_example(Type, Method) ->
|
||||||
maps:merge(info_example_basic(Type),
|
maps:merge(
|
||||||
method_example(Type, Method)).
|
info_example_basic(Type),
|
||||||
|
method_example(Type, Method)
|
||||||
|
).
|
||||||
|
|
||||||
method_example(Type, Method) when Method == get; Method == post ->
|
method_example(Type, Method) when Method == get; Method == post ->
|
||||||
SType = atom_to_list(Type),
|
SType = atom_to_list(Type),
|
||||||
|
|
@ -115,11 +129,17 @@ info_example_basic(mqtt) ->
|
||||||
}.
|
}.
|
||||||
|
|
||||||
param_path_id() ->
|
param_path_id() ->
|
||||||
[{id, mk(binary(),
|
[
|
||||||
#{ in => path
|
{id,
|
||||||
, example => <<"mqtt:my_mqtt_connector">>
|
mk(
|
||||||
, desc => ?DESC("id")
|
binary(),
|
||||||
})}].
|
#{
|
||||||
|
in => path,
|
||||||
|
example => <<"mqtt:my_mqtt_connector">>,
|
||||||
|
desc => ?DESC("id")
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
].
|
||||||
|
|
||||||
schema("/connectors_test") ->
|
schema("/connectors_test") ->
|
||||||
#{
|
#{
|
||||||
|
|
@ -135,7 +155,6 @@ schema("/connectors_test") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/connectors") ->
|
schema("/connectors") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/connectors',
|
'operationId' => '/connectors',
|
||||||
|
|
@ -145,8 +164,9 @@ schema("/connectors") ->
|
||||||
summary => <<"List connectors">>,
|
summary => <<"List connectors">>,
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => emqx_dashboard_swagger:schema_with_example(
|
200 => emqx_dashboard_swagger:schema_with_example(
|
||||||
array(emqx_connector_schema:get_response()),
|
array(emqx_connector_schema:get_response()),
|
||||||
connector_info_array_example(get))
|
connector_info_array_example(get)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
post => #{
|
post => #{
|
||||||
|
|
@ -160,7 +180,6 @@ schema("/connectors") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/connectors/:id") ->
|
schema("/connectors/:id") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/connectors/:id',
|
'operationId' => '/connectors/:id',
|
||||||
|
|
@ -185,7 +204,8 @@ schema("/connectors/:id") ->
|
||||||
200 => get_response_body_schema(),
|
200 => get_response_body_schema(),
|
||||||
404 => error_schema(['NOT_FOUND'], "Connector not found"),
|
404 => error_schema(['NOT_FOUND'], "Connector not found"),
|
||||||
400 => error_schema(['INVALID_ID'], "Bad connector ID")
|
400 => error_schema(['INVALID_ID'], "Bad connector ID")
|
||||||
}},
|
}
|
||||||
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
tags => [<<"connectors">>],
|
tags => [<<"connectors">>],
|
||||||
desc => ?DESC("conn_id_delete"),
|
desc => ?DESC("conn_id_delete"),
|
||||||
|
|
@ -196,7 +216,8 @@ schema("/connectors/:id") ->
|
||||||
403 => error_schema(['DEPENDENCY_EXISTS'], "Cannot remove dependent connector"),
|
403 => error_schema(['DEPENDENCY_EXISTS'], "Cannot remove dependent connector"),
|
||||||
404 => error_schema(['NOT_FOUND'], "Delete failed, not found"),
|
404 => error_schema(['NOT_FOUND'], "Delete failed, not found"),
|
||||||
400 => error_schema(['INVALID_ID'], "Bad connector ID")
|
400 => error_schema(['INVALID_ID'], "Bad connector ID")
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
'/connectors_test'(post, #{body := #{<<"type">> := ConnType} = Params}) ->
|
'/connectors_test'(post, #{body := #{<<"type">> := ConnType} = Params}) ->
|
||||||
|
|
@ -209,67 +230,83 @@ schema("/connectors/:id") ->
|
||||||
|
|
||||||
'/connectors'(get, _Request) ->
|
'/connectors'(get, _Request) ->
|
||||||
{200, [format_resp(Conn) || Conn <- emqx_connector:list_raw()]};
|
{200, [format_resp(Conn) || Conn <- emqx_connector:list_raw()]};
|
||||||
|
|
||||||
'/connectors'(post, #{body := #{<<"type">> := ConnType, <<"name">> := ConnName} = Params}) ->
|
'/connectors'(post, #{body := #{<<"type">> := ConnType, <<"name">> := ConnName} = Params}) ->
|
||||||
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{400, error_msg('ALREADY_EXISTS', <<"connector already exists">>)};
|
{400, error_msg('ALREADY_EXISTS', <<"connector already exists">>)};
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
case emqx_connector:update(ConnType, ConnName,
|
case
|
||||||
filter_out_request_body(Params)) of
|
emqx_connector:update(
|
||||||
|
ConnType,
|
||||||
|
ConnName,
|
||||||
|
filter_out_request_body(Params)
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, #{raw_config := RawConf}} ->
|
{ok, #{raw_config := RawConf}} ->
|
||||||
{201, format_resp(RawConf#{<<"type">> => ConnType,
|
{201,
|
||||||
<<"name">> => ConnName})};
|
format_resp(RawConf#{
|
||||||
|
<<"type">> => ConnType,
|
||||||
|
<<"name">> => ConnName
|
||||||
|
})};
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
{400, error_msg('BAD_REQUEST', Error)}
|
{400, error_msg('BAD_REQUEST', Error)}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
|
||||||
'/connectors'(post, _) ->
|
'/connectors'(post, _) ->
|
||||||
{400, error_msg('BAD_REQUEST', <<"missing some required fields: [name, type]">>)}.
|
{400, error_msg('BAD_REQUEST', <<"missing some required fields: [name, type]">>)}.
|
||||||
|
|
||||||
'/connectors/:id'(get, #{bindings := #{id := Id}}) ->
|
'/connectors/:id'(get, #{bindings := #{id := Id}}) ->
|
||||||
?TRY_PARSE_ID(Id,
|
?TRY_PARSE_ID(
|
||||||
|
Id,
|
||||||
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
||||||
{ok, Conf} ->
|
{ok, Conf} ->
|
||||||
{200, format_resp(Conf)};
|
{200, format_resp(Conf)};
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
||||||
end);
|
end
|
||||||
|
);
|
||||||
'/connectors/:id'(put, #{bindings := #{id := Id}, body := Params0}) ->
|
'/connectors/:id'(put, #{bindings := #{id := Id}, body := Params0}) ->
|
||||||
Params = filter_out_request_body(Params0),
|
Params = filter_out_request_body(Params0),
|
||||||
?TRY_PARSE_ID(Id,
|
?TRY_PARSE_ID(
|
||||||
|
Id,
|
||||||
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
case emqx_connector:update(ConnType, ConnName, Params) of
|
case emqx_connector:update(ConnType, ConnName, Params) of
|
||||||
{ok, #{raw_config := RawConf}} ->
|
{ok, #{raw_config := RawConf}} ->
|
||||||
{200, format_resp(RawConf#{<<"type">> => ConnType,
|
{200,
|
||||||
<<"name">> => ConnName})};
|
format_resp(RawConf#{
|
||||||
|
<<"type">> => ConnType,
|
||||||
|
<<"name">> => ConnName
|
||||||
|
})};
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
{500, error_msg('INTERNAL_ERROR', Error)}
|
{500, error_msg('INTERNAL_ERROR', Error)}
|
||||||
end;
|
end;
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
||||||
end);
|
end
|
||||||
|
);
|
||||||
'/connectors/:id'(delete, #{bindings := #{id := Id}}) ->
|
'/connectors/:id'(delete, #{bindings := #{id := Id}}) ->
|
||||||
?TRY_PARSE_ID(Id,
|
?TRY_PARSE_ID(
|
||||||
|
Id,
|
||||||
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
case emqx_connector:lookup_raw(ConnType, ConnName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
case emqx_connector:delete(ConnType, ConnName) of
|
case emqx_connector:delete(ConnType, ConnName) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{204};
|
{204};
|
||||||
{error, {post_config_update, _, {dependency_bridges_exist, BridgeID}}} ->
|
{error, {post_config_update, _, {dependency_bridges_exist, BridgeID}}} ->
|
||||||
{403, error_msg('DEPENDENCY_EXISTS',
|
{403,
|
||||||
<<"Cannot remove the connector as it's in use by a bridge: ",
|
error_msg(
|
||||||
BridgeID/binary>>)};
|
'DEPENDENCY_EXISTS',
|
||||||
|
<<"Cannot remove the connector as it's in use by a bridge: ",
|
||||||
|
BridgeID/binary>>
|
||||||
|
)};
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
{500, error_msg('INTERNAL_ERROR', Error)}
|
{500, error_msg('INTERNAL_ERROR', Error)}
|
||||||
end;
|
end;
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
error_msg(Code, Msg) when is_binary(Msg) ->
|
error_msg(Code, Msg) when is_binary(Msg) ->
|
||||||
#{code => Code, message => Msg};
|
#{code => Code, message => Msg};
|
||||||
|
|
@ -277,8 +314,11 @@ error_msg(Code, Msg) ->
|
||||||
#{code => Code, message => bin(io_lib:format("~p", [Msg]))}.
|
#{code => Code, message => bin(io_lib:format("~p", [Msg]))}.
|
||||||
|
|
||||||
format_resp(#{<<"type">> := ConnType, <<"name">> := ConnName} = RawConf) ->
|
format_resp(#{<<"type">> := ConnType, <<"name">> := ConnName} = RawConf) ->
|
||||||
NumOfBridges = length(emqx_bridge:list_bridges_by_connector(
|
NumOfBridges = length(
|
||||||
emqx_connector:connector_id(ConnType, ConnName))),
|
emqx_bridge:list_bridges_by_connector(
|
||||||
|
emqx_connector:connector_id(ConnType, ConnName)
|
||||||
|
)
|
||||||
|
),
|
||||||
RawConf#{
|
RawConf#{
|
||||||
<<"type">> => ConnType,
|
<<"type">> => ConnType,
|
||||||
<<"name">> => ConnName,
|
<<"name">> => ConnName,
|
||||||
|
|
|
||||||
|
|
@ -25,32 +25,34 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
-type url() :: emqx_http_lib:uri_map().
|
-type url() :: emqx_http_lib:uri_map().
|
||||||
-reflect_type([url/0]).
|
-reflect_type([url/0]).
|
||||||
-typerefl_from_string({url/0, emqx_http_lib, uri_parse}).
|
-typerefl_from_string({url/0, emqx_http_lib, uri_parse}).
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
, desc/1
|
fields/1,
|
||||||
, validations/0
|
desc/1,
|
||||||
, namespace/0
|
validations/0,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ check_ssl_opts/2
|
-export([check_ssl_opts/2]).
|
||||||
]).
|
|
||||||
|
|
||||||
-type connect_timeout() :: emqx_schema:duration() | infinity.
|
-type connect_timeout() :: emqx_schema:duration() | infinity.
|
||||||
-type pool_type() :: random | hash.
|
-type pool_type() :: random | hash.
|
||||||
|
|
||||||
-reflect_type([ connect_timeout/0
|
-reflect_type([
|
||||||
, pool_type/0
|
connect_timeout/0,
|
||||||
]).
|
pool_type/0
|
||||||
|
]).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
%% Hocon schema
|
%% Hocon schema
|
||||||
|
|
@ -61,63 +63,96 @@ roots() ->
|
||||||
fields(config).
|
fields(config).
|
||||||
|
|
||||||
fields(config) ->
|
fields(config) ->
|
||||||
[ {base_url,
|
[
|
||||||
sc(url(),
|
{base_url,
|
||||||
#{ required => true
|
sc(
|
||||||
, validator => fun(#{query := _Query}) ->
|
url(),
|
||||||
|
#{
|
||||||
|
required => true,
|
||||||
|
validator => fun
|
||||||
|
(#{query := _Query}) ->
|
||||||
{error, "There must be no query in the base_url"};
|
{error, "There must be no query in the base_url"};
|
||||||
(_) -> ok
|
(_) ->
|
||||||
end
|
ok
|
||||||
, desc => ?DESC("base_url")
|
end,
|
||||||
})}
|
desc => ?DESC("base_url")
|
||||||
, {connect_timeout,
|
}
|
||||||
sc(emqx_schema:duration_ms(),
|
)},
|
||||||
#{ default => "15s"
|
{connect_timeout,
|
||||||
, desc => ?DESC("connect_timeout")
|
sc(
|
||||||
})}
|
emqx_schema:duration_ms(),
|
||||||
, {max_retries,
|
#{
|
||||||
sc(non_neg_integer(),
|
default => "15s",
|
||||||
#{ default => 5
|
desc => ?DESC("connect_timeout")
|
||||||
, desc => ?DESC("max_retries")
|
}
|
||||||
})}
|
)},
|
||||||
, {retry_interval,
|
{max_retries,
|
||||||
sc(emqx_schema:duration(),
|
sc(
|
||||||
#{ default => "1s"
|
non_neg_integer(),
|
||||||
, desc => ?DESC("retry_interval")
|
#{
|
||||||
})}
|
default => 5,
|
||||||
, {pool_type,
|
desc => ?DESC("max_retries")
|
||||||
sc(pool_type(),
|
}
|
||||||
#{ default => random
|
)},
|
||||||
, desc => ?DESC("pool_type")
|
{retry_interval,
|
||||||
})}
|
sc(
|
||||||
, {pool_size,
|
emqx_schema:duration(),
|
||||||
sc(pos_integer(),
|
#{
|
||||||
#{ default => 8
|
default => "1s",
|
||||||
, desc => ?DESC("pool_size")
|
desc => ?DESC("retry_interval")
|
||||||
})}
|
}
|
||||||
, {enable_pipelining,
|
)},
|
||||||
sc(boolean(),
|
{pool_type,
|
||||||
#{ default => true
|
sc(
|
||||||
, desc => ?DESC("enable_pipelining")
|
pool_type(),
|
||||||
})}
|
#{
|
||||||
, {request, hoconsc:mk(
|
default => random,
|
||||||
ref("request"),
|
desc => ?DESC("pool_type")
|
||||||
#{ default => undefined
|
}
|
||||||
, required => false
|
)},
|
||||||
, desc => ?DESC("request")
|
{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();
|
] ++ emqx_connector_schema_lib:ssl_fields();
|
||||||
|
|
||||||
fields("request") ->
|
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")})}
|
{method,
|
||||||
, {body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})}
|
hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{
|
||||||
, {headers, hoconsc:mk(map(), #{required => false, desc => ?DESC("headers")})}
|
required => false, desc => ?DESC("method")
|
||||||
, {request_timeout,
|
})},
|
||||||
sc(emqx_schema:duration_ms(),
|
{path, hoconsc:mk(binary(), #{required => false, desc => ?DESC("path")})},
|
||||||
#{ required => false
|
{body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})},
|
||||||
, desc => ?DESC("request_timeout")
|
{headers, hoconsc:mk(map(), #{required => false, desc => ?DESC("headers")})},
|
||||||
})}
|
{request_timeout,
|
||||||
|
sc(
|
||||||
|
emqx_schema:duration_ms(),
|
||||||
|
#{
|
||||||
|
required => false,
|
||||||
|
desc => ?DESC("request_timeout")
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc(config) ->
|
desc(config) ->
|
||||||
|
|
@ -128,24 +163,34 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
validations() ->
|
validations() ->
|
||||||
[ {check_ssl_opts, fun check_ssl_opts/1} ].
|
[{check_ssl_opts, fun check_ssl_opts/1}].
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
ref(Field) -> hoconsc:ref(?MODULE, Field).
|
ref(Field) -> hoconsc:ref(?MODULE, Field).
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
|
|
||||||
on_start(InstId, #{base_url := #{scheme := Scheme,
|
on_start(
|
||||||
host := Host,
|
InstId,
|
||||||
port := Port,
|
#{
|
||||||
path := BasePath},
|
base_url := #{
|
||||||
connect_timeout := ConnectTimeout,
|
scheme := Scheme,
|
||||||
max_retries := MaxRetries,
|
host := Host,
|
||||||
retry_interval := RetryInterval,
|
port := Port,
|
||||||
pool_type := PoolType,
|
path := BasePath
|
||||||
pool_size := PoolSize} = Config) ->
|
},
|
||||||
?SLOG(info, #{msg => "starting_http_connector",
|
connect_timeout := ConnectTimeout,
|
||||||
connector => InstId, config => Config}),
|
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} =
|
{Transport, TransportOpts} =
|
||||||
case Scheme of
|
case Scheme of
|
||||||
http ->
|
http ->
|
||||||
|
|
@ -155,16 +200,18 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
|
||||||
{tls, SSLOpts}
|
{tls, SSLOpts}
|
||||||
end,
|
end,
|
||||||
NTransportOpts = emqx_misc:ipv6_probe(TransportOpts),
|
NTransportOpts = emqx_misc:ipv6_probe(TransportOpts),
|
||||||
PoolOpts = [ {host, Host}
|
PoolOpts = [
|
||||||
, {port, Port}
|
{host, Host},
|
||||||
, {connect_timeout, ConnectTimeout}
|
{port, Port},
|
||||||
, {retry, MaxRetries}
|
{connect_timeout, ConnectTimeout},
|
||||||
, {retry_timeout, RetryInterval}
|
{retry, MaxRetries},
|
||||||
, {keepalive, 30000}
|
{retry_timeout, RetryInterval},
|
||||||
, {pool_type, PoolType}
|
{keepalive, 30000},
|
||||||
, {pool_size, PoolSize}
|
{pool_type, PoolType},
|
||||||
, {transport, Transport}
|
{pool_size, PoolSize},
|
||||||
, {transport_opts, NTransportOpts}],
|
{transport, Transport},
|
||||||
|
{transport_opts, NTransportOpts}
|
||||||
|
],
|
||||||
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
State = #{
|
State = #{
|
||||||
pool_name => PoolName,
|
pool_name => PoolName,
|
||||||
|
|
@ -177,54 +224,84 @@ on_start(InstId, #{base_url := #{scheme := Scheme,
|
||||||
case ehttpc_sup:start_pool(PoolName, PoolOpts) of
|
case ehttpc_sup:start_pool(PoolName, PoolOpts) of
|
||||||
{ok, _} -> {ok, State};
|
{ok, _} -> {ok, State};
|
||||||
{error, {already_started, _}} -> {ok, State};
|
{error, {already_started, _}} -> {ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} -> {error, Reason}
|
||||||
{error, Reason}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{pool_name := PoolName}) ->
|
on_stop(InstId, #{pool_name := PoolName}) ->
|
||||||
?SLOG(info, #{msg => "stopping_http_connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping_http_connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
ehttpc_sup:stop_pool(PoolName).
|
ehttpc_sup:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {send_message, Msg}, AfterQuery, State) ->
|
on_query(InstId, {send_message, Msg}, AfterQuery, State) ->
|
||||||
case maps:get(request, State, undefined) of
|
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 ->
|
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)
|
on_query(InstId, {Method, {Path, Headers, Body}, Timeout}, AfterQuery, State)
|
||||||
end;
|
end;
|
||||||
on_query(InstId, {Method, Request}, AfterQuery, State) ->
|
on_query(InstId, {Method, Request}, AfterQuery, State) ->
|
||||||
on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State);
|
on_query(InstId, {undefined, Method, Request, 5000}, AfterQuery, State);
|
||||||
on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) ->
|
on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) ->
|
||||||
on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State);
|
on_query(InstId, {undefined, Method, Request, Timeout}, AfterQuery, State);
|
||||||
on_query(InstId, {KeyOrNum, Method, Request, Timeout}, AfterQuery,
|
on_query(
|
||||||
#{pool_name := PoolName, base_path := BasePath} = State) ->
|
InstId,
|
||||||
?TRACE("QUERY", "http_connector_received",
|
{KeyOrNum, Method, Request, Timeout},
|
||||||
#{request => Request, connector => InstId, state => State}),
|
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),
|
NRequest = formalize_request(Method, BasePath, Request),
|
||||||
case Result = ehttpc:request(case KeyOrNum of
|
case
|
||||||
undefined -> PoolName;
|
Result = ehttpc:request(
|
||||||
_ -> {PoolName, KeyOrNum}
|
case KeyOrNum of
|
||||||
end, Method, NRequest, Timeout) of
|
undefined -> PoolName;
|
||||||
|
_ -> {PoolName, KeyOrNum}
|
||||||
|
end,
|
||||||
|
Method,
|
||||||
|
NRequest,
|
||||||
|
Timeout
|
||||||
|
)
|
||||||
|
of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "http_connector_do_reqeust_failed",
|
?SLOG(error, #{
|
||||||
request => NRequest, reason => Reason,
|
msg => "http_connector_do_reqeust_failed",
|
||||||
connector => InstId}),
|
request => NRequest,
|
||||||
|
reason => Reason,
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
{ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
{ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
||||||
emqx_resource:query_success(AfterQuery);
|
emqx_resource:query_success(AfterQuery);
|
||||||
{ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
{ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 ->
|
||||||
emqx_resource:query_success(AfterQuery);
|
emqx_resource:query_success(AfterQuery);
|
||||||
{ok, StatusCode, _} ->
|
{ok, StatusCode, _} ->
|
||||||
?SLOG(error, #{msg => "http connector do request, received error response",
|
?SLOG(error, #{
|
||||||
request => NRequest, connector => InstId,
|
msg => "http connector do request, received error response",
|
||||||
status_code => StatusCode}),
|
request => NRequest,
|
||||||
|
connector => InstId,
|
||||||
|
status_code => StatusCode
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
{ok, StatusCode, _, _} ->
|
{ok, StatusCode, _, _} ->
|
||||||
?SLOG(error, #{msg => "http connector do request, received error response",
|
?SLOG(error, #{
|
||||||
request => NRequest, connector => InstId,
|
msg => "http connector do request, received error response",
|
||||||
status_code => StatusCode}),
|
request => NRequest,
|
||||||
|
connector => InstId,
|
||||||
|
status_code => StatusCode
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterQuery)
|
emqx_resource:query_failed(AfterQuery)
|
||||||
end,
|
end,
|
||||||
Result.
|
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) ->
|
on_health_check(_InstId, #{host := Host, port := Port, connect_timeout := Timeout} = State) ->
|
||||||
case do_health_check(Host, Port, Timeout) of
|
case do_health_check(Host, Port, Timeout) of
|
||||||
ok -> {ok, State};
|
ok -> {ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} -> {error, {http_health_check_failed, Reason}, State}
|
||||||
{error, {http_health_check_failed, Reason}, State}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_health_check(Host, Port, Timeout) ->
|
do_health_check(Host, Port, Timeout) ->
|
||||||
case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), Timeout) of
|
case gen_tcp:connect(Host, Port, emqx_misc:ipv6_probe([]), Timeout) of
|
||||||
{ok, Sock} -> gen_tcp:close(Sock), ok;
|
{ok, Sock} ->
|
||||||
{error, Reason} -> {error, Reason}
|
gen_tcp:close(Sock),
|
||||||
|
ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
@ -250,47 +329,64 @@ preprocess_request(undefined) ->
|
||||||
undefined;
|
undefined;
|
||||||
preprocess_request(Req) when map_size(Req) == 0 ->
|
preprocess_request(Req) when map_size(Req) == 0 ->
|
||||||
undefined;
|
undefined;
|
||||||
preprocess_request(#{
|
preprocess_request(
|
||||||
method := Method,
|
#{
|
||||||
path := Path,
|
method := Method,
|
||||||
body := Body,
|
path := Path,
|
||||||
headers := Headers
|
body := Body,
|
||||||
} = Req) ->
|
headers := Headers
|
||||||
#{ method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method))
|
} = Req
|
||||||
, path => emqx_plugin_libs_rule:preproc_tmpl(Path)
|
) ->
|
||||||
, body => emqx_plugin_libs_rule:preproc_tmpl(Body)
|
#{
|
||||||
, headers => preproc_headers(Headers)
|
method => emqx_plugin_libs_rule:preproc_tmpl(bin(Method)),
|
||||||
, request_timeout => maps:get(request_timeout, Req, 30000)
|
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) ->
|
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(K)),
|
||||||
emqx_plugin_libs_rule:preproc_tmpl(bin(V))
|
emqx_plugin_libs_rule:preproc_tmpl(bin(V))
|
||||||
} | Acc]
|
}
|
||||||
end, [], Headers);
|
end,
|
||||||
preproc_headers(Headers) when is_list(Headers) ->
|
Headers
|
||||||
lists:map(fun({K, V}) ->
|
).
|
||||||
{
|
|
||||||
emqx_plugin_libs_rule:preproc_tmpl(bin(K)),
|
|
||||||
emqx_plugin_libs_rule:preproc_tmpl(bin(V))
|
|
||||||
}
|
|
||||||
end, Headers).
|
|
||||||
|
|
||||||
process_request(#{
|
process_request(
|
||||||
method := MethodTks,
|
#{
|
||||||
path := PathTks,
|
method := MethodTks,
|
||||||
body := BodyTks,
|
path := PathTks,
|
||||||
headers := HeadersTks,
|
body := BodyTks,
|
||||||
request_timeout := ReqTimeout
|
headers := HeadersTks,
|
||||||
} = Conf, Msg) ->
|
request_timeout := ReqTimeout
|
||||||
Conf#{ method => make_method(emqx_plugin_libs_rule:proc_tmpl(MethodTks, Msg))
|
} = Conf,
|
||||||
, path => emqx_plugin_libs_rule:proc_tmpl(PathTks, Msg)
|
Msg
|
||||||
, body => process_request_body(BodyTks, Msg)
|
) ->
|
||||||
, headers => proc_headers(HeadersTks, Msg)
|
Conf#{
|
||||||
, request_timeout => ReqTimeout
|
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) ->
|
process_request_body([], Msg) ->
|
||||||
emqx_json:encode(Msg);
|
emqx_json:encode(Msg);
|
||||||
|
|
@ -298,12 +394,15 @@ process_request_body(BodyTks, Msg) ->
|
||||||
emqx_plugin_libs_rule:proc_tmpl(BodyTks, Msg).
|
emqx_plugin_libs_rule:proc_tmpl(BodyTks, Msg).
|
||||||
|
|
||||||
proc_headers(HeaderTks, 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(K, Msg),
|
||||||
emqx_plugin_libs_rule:proc_tmpl(V, 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 == <<"POST">>; M == <<"post">> -> post;
|
||||||
make_method(M) when M == <<"PUT">>; M == <<"put">> -> put;
|
make_method(M) when M == <<"PUT">>; M == <<"put">> -> put;
|
||||||
|
|
@ -315,19 +414,19 @@ check_ssl_opts(Conf) ->
|
||||||
|
|
||||||
check_ssl_opts(URLFrom, Conf) ->
|
check_ssl_opts(URLFrom, Conf) ->
|
||||||
#{scheme := Scheme} = hocon_maps:get(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
|
case {Scheme, maps:get(enable, SSL, false)} of
|
||||||
{http, false} -> true;
|
{http, false} -> true;
|
||||||
{https, true} -> true;
|
{https, true} -> true;
|
||||||
{_, _} -> false
|
{_, _} -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
formalize_request(Method, BasePath, {Path, Headers, _Body})
|
formalize_request(Method, BasePath, {Path, Headers, _Body}) when
|
||||||
when Method =:= get; Method =:= delete ->
|
Method =:= get; Method =:= delete
|
||||||
|
->
|
||||||
formalize_request(Method, BasePath, {Path, Headers});
|
formalize_request(Method, BasePath, {Path, Headers});
|
||||||
formalize_request(_Method, BasePath, {Path, Headers, Body}) ->
|
formalize_request(_Method, BasePath, {Path, Headers, Body}) ->
|
||||||
{filename:join(BasePath, Path), Headers, Body};
|
{filename:join(BasePath, Path), Headers, Body};
|
||||||
|
|
||||||
formalize_request(_Method, BasePath, {Path, Headers}) ->
|
formalize_request(_Method, BasePath, {Path, Headers}) ->
|
||||||
{filename:join(BasePath, Path), Headers}.
|
{filename:join(BasePath, Path), Headers}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([do_health_check/1]).
|
-export([do_health_check/1]).
|
||||||
|
|
||||||
|
|
@ -43,54 +44,84 @@ roots() ->
|
||||||
fields(_) -> [].
|
fields(_) -> [].
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, #{servers := Servers0,
|
on_start(
|
||||||
port := Port,
|
InstId,
|
||||||
bind_dn := BindDn,
|
#{
|
||||||
bind_password := BindPassword,
|
servers := Servers0,
|
||||||
timeout := Timeout,
|
port := Port,
|
||||||
pool_size := PoolSize,
|
bind_dn := BindDn,
|
||||||
auto_reconnect := AutoReconn,
|
bind_password := BindPassword,
|
||||||
ssl := SSL} = Config) ->
|
timeout := Timeout,
|
||||||
?SLOG(info, #{msg => "starting_ldap_connector",
|
pool_size := PoolSize,
|
||||||
connector => InstId, config => Config}),
|
auto_reconnect := AutoReconn,
|
||||||
Servers = [begin proplists:get_value(host, S) end || S <- Servers0],
|
ssl := SSL
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
} = Config
|
||||||
true ->
|
) ->
|
||||||
[{ssl, true},
|
?SLOG(info, #{
|
||||||
{sslopts, emqx_tls_lib:to_client_opts(SSL)}
|
msg => "starting_ldap_connector",
|
||||||
];
|
connector => InstId,
|
||||||
false -> [{ssl, false}]
|
config => Config
|
||||||
end,
|
}),
|
||||||
Opts = [{servers, Servers},
|
Servers = [
|
||||||
{port, Port},
|
begin
|
||||||
{bind_dn, BindDn},
|
proplists:get_value(host, S)
|
||||||
{bind_password, BindPassword},
|
end
|
||||||
{timeout, Timeout},
|
|| S <- Servers0
|
||||||
{pool_size, PoolSize},
|
],
|
||||||
{auto_reconnect, reconn_interval(AutoReconn)},
|
SslOpts =
|
||||||
{servers, Servers}],
|
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),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts) of
|
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ SslOpts) of
|
||||||
ok -> {ok, #{poolname => PoolName}};
|
ok -> {ok, #{poolname => PoolName}};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
?SLOG(info, #{msg => "stopping_ldap_connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping_ldap_connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
Request = {Base, Filter, Attributes},
|
Request = {Base, Filter, Attributes},
|
||||||
?TRACE("QUERY", "ldap_connector_received",
|
?TRACE(
|
||||||
#{request => Request, connector => InstId, state => State}),
|
"QUERY",
|
||||||
case Result = ecpool:pick_and_do(
|
"ldap_connector_received",
|
||||||
PoolName,
|
#{request => Request, connector => InstId, state => State}
|
||||||
{?MODULE, search, [Base, Filter, Attributes]},
|
),
|
||||||
no_handover) of
|
case
|
||||||
|
Result = ecpool:pick_and_do(
|
||||||
|
PoolName,
|
||||||
|
{?MODULE, search, [Base, Filter, Attributes]},
|
||||||
|
no_handover
|
||||||
|
)
|
||||||
|
of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "ldap_connector_do_request_failed",
|
?SLOG(error, #{
|
||||||
request => Request, connector => InstId, reason => Reason}),
|
msg => "ldap_connector_do_request_failed",
|
||||||
|
request => Request,
|
||||||
|
connector => InstId,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterQuery);
|
emqx_resource:query_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
@ -107,38 +138,45 @@ reconn_interval(true) -> 15;
|
||||||
reconn_interval(false) -> false.
|
reconn_interval(false) -> false.
|
||||||
|
|
||||||
search(Conn, Base, Filter, Attributes) ->
|
search(Conn, Base, Filter, Attributes) ->
|
||||||
eldap2:search(Conn, [{base, Base},
|
eldap2:search(Conn, [
|
||||||
{filter, Filter},
|
{base, Base},
|
||||||
{attributes, Attributes},
|
{filter, Filter},
|
||||||
{deref, eldap2:'derefFindingBaseObj'()}]).
|
{attributes, Attributes},
|
||||||
|
{deref, eldap2:'derefFindingBaseObj'()}
|
||||||
|
]).
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
connect(Opts) ->
|
connect(Opts) ->
|
||||||
Servers = proplists:get_value(servers, Opts, ["localhost"]),
|
Servers = proplists:get_value(servers, Opts, ["localhost"]),
|
||||||
Port = proplists:get_value(port, Opts, 389),
|
Port = proplists:get_value(port, Opts, 389),
|
||||||
Timeout = proplists:get_value(timeout, Opts, 30),
|
Timeout = proplists:get_value(timeout, Opts, 30),
|
||||||
BindDn = proplists:get_value(bind_dn, Opts),
|
BindDn = proplists:get_value(bind_dn, Opts),
|
||||||
BindPassword = proplists:get_value(bind_password, Opts),
|
BindPassword = proplists:get_value(bind_password, Opts),
|
||||||
SslOpts = case proplists:get_value(ssl, Opts, false) of
|
SslOpts =
|
||||||
true ->
|
case proplists:get_value(ssl, Opts, false) of
|
||||||
[{sslopts, proplists:get_value(sslopts, Opts, [])}, {ssl, true}];
|
true ->
|
||||||
false ->
|
[{sslopts, proplists:get_value(sslopts, Opts, [])}, {ssl, true}];
|
||||||
[{ssl, false}]
|
false ->
|
||||||
end,
|
[{ssl, false}]
|
||||||
LdapOpts = [{port, Port},
|
end,
|
||||||
{timeout, Timeout}] ++ SslOpts,
|
LdapOpts =
|
||||||
|
[
|
||||||
|
{port, Port},
|
||||||
|
{timeout, Timeout}
|
||||||
|
] ++ SslOpts,
|
||||||
{ok, LDAP} = eldap2:open(Servers, LdapOpts),
|
{ok, LDAP} = eldap2:open(Servers, LdapOpts),
|
||||||
ok = eldap2:simple_bind(LDAP, BindDn, BindPassword),
|
ok = eldap2:simple_bind(LDAP, BindDn, BindPassword),
|
||||||
{ok, LDAP}.
|
{ok, LDAP}.
|
||||||
|
|
||||||
ldap_fields() ->
|
ldap_fields() ->
|
||||||
[ {servers, fun servers/1}
|
[
|
||||||
, {port, fun port/1}
|
{servers, fun servers/1},
|
||||||
, {pool_size, fun emqx_connector_schema_lib:pool_size/1}
|
{port, fun port/1},
|
||||||
, {bind_dn, fun bind_dn/1}
|
{pool_size, fun emqx_connector_schema_lib:pool_size/1},
|
||||||
, {bind_password, fun emqx_connector_schema_lib:password/1}
|
{bind_dn, fun bind_dn/1},
|
||||||
, {timeout, fun duration/1}
|
{bind_password, fun emqx_connector_schema_lib:password/1},
|
||||||
, {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
|
{timeout, fun duration/1},
|
||||||
|
{auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
servers(type) -> list();
|
servers(type) -> list();
|
||||||
|
|
@ -159,14 +197,18 @@ duration(type) -> emqx_schema:duration_ms();
|
||||||
duration(_) -> undefined.
|
duration(_) -> undefined.
|
||||||
|
|
||||||
to_servers_raw(Servers) ->
|
to_servers_raw(Servers) ->
|
||||||
{ok, lists:map( fun(Server) ->
|
{ok,
|
||||||
case string:tokens(Server, ": ") of
|
lists:map(
|
||||||
[Ip] ->
|
fun(Server) ->
|
||||||
[{host, Ip}];
|
case string:tokens(Server, ": ") of
|
||||||
[Ip, Port] ->
|
[Ip] ->
|
||||||
[{host, Ip}, {port, list_to_integer(Port)}]
|
[{host, Ip}];
|
||||||
end
|
[Ip, Port] ->
|
||||||
end, string:tokens(str(Servers), ", "))}.
|
[{host, Ip}, {port, list_to_integer(Port)}]
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
string:tokens(str(Servers), ", ")
|
||||||
|
)}.
|
||||||
|
|
||||||
str(A) when is_atom(A) ->
|
str(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,12 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% ecpool callback
|
%% ecpool callback
|
||||||
-export([connect/1]).
|
-export([connect/1]).
|
||||||
|
|
@ -40,57 +41,73 @@
|
||||||
-define(HEALTH_CHECK_TIMEOUT, 10000).
|
-define(HEALTH_CHECK_TIMEOUT, 10000).
|
||||||
|
|
||||||
%% mongo servers don't need parse
|
%% mongo servers don't need parse
|
||||||
-define( MONGO_HOST_OPTIONS
|
-define(MONGO_HOST_OPTIONS, #{
|
||||||
, #{ host_type => hostname
|
host_type => hostname,
|
||||||
, default_port => ?MONGO_DEFAULT_PORT}).
|
default_port => ?MONGO_DEFAULT_PORT
|
||||||
|
}).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {config, #{type => hoconsc:union(
|
[
|
||||||
[ hoconsc:ref(?MODULE, single)
|
{config, #{
|
||||||
, hoconsc:ref(?MODULE, rs)
|
type => hoconsc:union(
|
||||||
, hoconsc:ref(?MODULE, sharded)
|
[
|
||||||
])}}
|
hoconsc:ref(?MODULE, single),
|
||||||
|
hoconsc:ref(?MODULE, rs),
|
||||||
|
hoconsc:ref(?MODULE, sharded)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(single) ->
|
fields(single) ->
|
||||||
[ {mongo_type, #{type => single,
|
[
|
||||||
default => single,
|
{mongo_type, #{
|
||||||
required => true,
|
type => single,
|
||||||
desc => ?DESC("single_mongo_type")}}
|
default => single,
|
||||||
, {server, fun server/1}
|
required => true,
|
||||||
, {w_mode, fun w_mode/1}
|
desc => ?DESC("single_mongo_type")
|
||||||
|
}},
|
||||||
|
{server, fun server/1},
|
||||||
|
{w_mode, fun w_mode/1}
|
||||||
] ++ mongo_fields();
|
] ++ mongo_fields();
|
||||||
fields(rs) ->
|
fields(rs) ->
|
||||||
[ {mongo_type, #{type => rs,
|
[
|
||||||
default => rs,
|
{mongo_type, #{
|
||||||
required => true,
|
type => rs,
|
||||||
desc => ?DESC("rs_mongo_type")}}
|
default => rs,
|
||||||
, {servers, fun servers/1}
|
required => true,
|
||||||
, {w_mode, fun w_mode/1}
|
desc => ?DESC("rs_mongo_type")
|
||||||
, {r_mode, fun r_mode/1}
|
}},
|
||||||
, {replica_set_name, fun replica_set_name/1}
|
{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();
|
] ++ mongo_fields();
|
||||||
fields(sharded) ->
|
fields(sharded) ->
|
||||||
[ {mongo_type, #{type => sharded,
|
[
|
||||||
default => sharded,
|
{mongo_type, #{
|
||||||
required => true,
|
type => sharded,
|
||||||
desc => ?DESC("sharded_mongo_type")}}
|
default => sharded,
|
||||||
, {servers, fun servers/1}
|
required => true,
|
||||||
, {w_mode, fun w_mode/1}
|
desc => ?DESC("sharded_mongo_type")
|
||||||
|
}},
|
||||||
|
{servers, fun servers/1},
|
||||||
|
{w_mode, fun w_mode/1}
|
||||||
] ++ mongo_fields();
|
] ++ mongo_fields();
|
||||||
fields(topology) ->
|
fields(topology) ->
|
||||||
[ {pool_size, fun emqx_connector_schema_lib:pool_size/1}
|
[
|
||||||
, {max_overflow, fun max_overflow/1}
|
{pool_size, fun emqx_connector_schema_lib:pool_size/1},
|
||||||
, {overflow_ttl, fun duration/1}
|
{max_overflow, fun max_overflow/1},
|
||||||
, {overflow_check_period, fun duration/1}
|
{overflow_ttl, fun duration/1},
|
||||||
, {local_threshold_ms, fun duration/1}
|
{overflow_check_period, fun duration/1},
|
||||||
, {connect_timeout_ms, fun duration/1}
|
{local_threshold_ms, fun duration/1},
|
||||||
, {socket_timeout_ms, fun duration/1}
|
{connect_timeout_ms, fun duration/1},
|
||||||
, {server_selection_timeout_ms, fun duration/1}
|
{socket_timeout_ms, fun duration/1},
|
||||||
, {wait_queue_timeout_ms, fun duration/1}
|
{server_selection_timeout_ms, fun duration/1},
|
||||||
, {heartbeat_frequency_ms, fun duration/1}
|
{wait_queue_timeout_ms, fun duration/1},
|
||||||
, {min_heartbeat_frequency_ms, fun duration/1}
|
{heartbeat_frequency_ms, fun duration/1},
|
||||||
|
{min_heartbeat_frequency_ms, fun duration/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc(single) ->
|
desc(single) ->
|
||||||
|
|
@ -105,69 +122,96 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
mongo_fields() ->
|
mongo_fields() ->
|
||||||
[ {srv_record, fun srv_record/1}
|
[
|
||||||
, {pool_size, fun emqx_connector_schema_lib:pool_size/1}
|
{srv_record, fun srv_record/1},
|
||||||
, {username, fun emqx_connector_schema_lib:username/1}
|
{pool_size, fun emqx_connector_schema_lib:pool_size/1},
|
||||||
, {password, fun emqx_connector_schema_lib:password/1}
|
{username, fun emqx_connector_schema_lib:username/1},
|
||||||
, {auth_source, #{ type => binary()
|
{password, fun emqx_connector_schema_lib:password/1},
|
||||||
, required => false
|
{auth_source, #{
|
||||||
, desc => ?DESC("auth_source")
|
type => binary(),
|
||||||
}}
|
required => false,
|
||||||
, {database, fun emqx_connector_schema_lib:database/1}
|
desc => ?DESC("auth_source")
|
||||||
, {topology, #{type => hoconsc:ref(?MODULE, topology), required => false}}
|
}},
|
||||||
|
{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,
|
on_start(
|
||||||
pool_size := PoolSize,
|
InstId,
|
||||||
ssl := SSL}) ->
|
Config = #{
|
||||||
Msg = case Type of
|
mongo_type := Type,
|
||||||
single -> "starting_mongodb_single_connector";
|
pool_size := PoolSize,
|
||||||
rs -> "starting_mongodb_replica_set_connector";
|
ssl := SSL
|
||||||
sharded -> "starting_mongodb_sharded_connector"
|
}
|
||||||
end,
|
) ->
|
||||||
|
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}),
|
?SLOG(info, #{msg => Msg, connector => InstId, config => Config}),
|
||||||
NConfig = #{hosts := Hosts} = may_parse_srv_and_txt_records(Config),
|
NConfig = #{hosts := Hosts} = may_parse_srv_and_txt_records(Config),
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
SslOpts =
|
||||||
true ->
|
case maps:get(enable, SSL) of
|
||||||
[{ssl, true},
|
true ->
|
||||||
{ssl_opts, emqx_tls_lib:to_client_opts(SSL)}
|
[
|
||||||
];
|
{ssl, true},
|
||||||
false -> [{ssl, false}]
|
{ssl_opts, emqx_tls_lib:to_client_opts(SSL)}
|
||||||
end,
|
];
|
||||||
|
false ->
|
||||||
|
[{ssl, false}]
|
||||||
|
end,
|
||||||
Topology = maps:get(topology, NConfig, #{}),
|
Topology = maps:get(topology, NConfig, #{}),
|
||||||
Opts = [{mongo_type, init_type(NConfig)},
|
Opts = [
|
||||||
{hosts, Hosts},
|
{mongo_type, init_type(NConfig)},
|
||||||
{pool_size, PoolSize},
|
{hosts, Hosts},
|
||||||
{options, init_topology_options(maps:to_list(Topology), [])},
|
{pool_size, PoolSize},
|
||||||
{worker_options, init_worker_options(maps:to_list(NConfig), SslOpts)}],
|
{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),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts) of
|
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}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
?SLOG(info, #{msg => "stopping_mongodb_connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping_mongodb_connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId,
|
on_query(
|
||||||
{Action, Collection, Selector, Projector},
|
InstId,
|
||||||
AfterQuery,
|
{Action, Collection, Selector, Projector},
|
||||||
#{poolname := PoolName} = State) ->
|
AfterQuery,
|
||||||
|
#{poolname := PoolName} = State
|
||||||
|
) ->
|
||||||
Request = {Action, Collection, Selector, Projector},
|
Request = {Action, Collection, Selector, Projector},
|
||||||
?TRACE("QUERY", "mongodb_connector_received",
|
?TRACE(
|
||||||
#{request => Request, connector => InstId, state => State}),
|
"QUERY",
|
||||||
case ecpool:pick_and_do(PoolName,
|
"mongodb_connector_received",
|
||||||
{?MODULE, mongo_query, [Action, Collection, Selector, Projector]},
|
#{request => Request, connector => InstId, state => State}
|
||||||
no_handover) of
|
),
|
||||||
|
case
|
||||||
|
ecpool:pick_and_do(
|
||||||
|
PoolName,
|
||||||
|
{?MODULE, mongo_query, [Action, Collection, Selector, Projector]},
|
||||||
|
no_handover
|
||||||
|
)
|
||||||
|
of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "mongodb_connector_do_query_failed",
|
?SLOG(error, #{
|
||||||
request => Request, reason => Reason,
|
msg => "mongodb_connector_do_query_failed",
|
||||||
connector => InstId}),
|
request => Request,
|
||||||
|
reason => Reason,
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterQuery),
|
emqx_resource:query_failed(AfterQuery),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
{ok, Cursor} when is_pid(Cursor) ->
|
{ok, Cursor} when is_pid(Cursor) ->
|
||||||
|
|
@ -182,12 +226,16 @@ on_query(InstId,
|
||||||
on_health_check(InstId, #{poolname := PoolName} = State) ->
|
on_health_check(InstId, #{poolname := PoolName} = State) ->
|
||||||
case health_check(PoolName) of
|
case health_check(PoolName) of
|
||||||
true ->
|
true ->
|
||||||
?tp(debug, emqx_connector_mongo_health_check, #{instance_id => InstId,
|
?tp(debug, emqx_connector_mongo_health_check, #{
|
||||||
status => ok}),
|
instance_id => InstId,
|
||||||
|
status => ok
|
||||||
|
}),
|
||||||
{ok, State};
|
{ok, State};
|
||||||
false ->
|
false ->
|
||||||
?tp(warning, emqx_connector_mongo_health_check, #{instance_id => InstId,
|
?tp(warning, emqx_connector_mongo_health_check, #{
|
||||||
status => failed}),
|
instance_id => InstId,
|
||||||
|
status => failed
|
||||||
|
}),
|
||||||
{error, health_check_failed, State}
|
{error, health_check_failed, State}
|
||||||
end.
|
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
|
%% we don't care if this returns something or not, we just to test the connection
|
||||||
try do_test_query(Conn) of
|
try do_test_query(Conn) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(warning, #{msg => "mongo_connection_health_check_error",
|
?SLOG(warning, #{
|
||||||
worker => Worker,
|
msg => "mongo_connection_health_check_error",
|
||||||
reason => Reason}),
|
worker => Worker,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
false;
|
false;
|
||||||
_ ->
|
_ ->
|
||||||
true
|
true
|
||||||
catch
|
catch
|
||||||
Class:Error ->
|
Class:Error ->
|
||||||
?SLOG(warning, #{msg => "mongo_connection_health_check_exception",
|
?SLOG(warning, #{
|
||||||
worker => Worker,
|
msg => "mongo_connection_health_check_exception",
|
||||||
class => Class,
|
worker => Worker,
|
||||||
error => Error}),
|
class => Class,
|
||||||
|
error => Error
|
||||||
|
}),
|
||||||
false
|
false
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
?SLOG(warning, #{msg => "mongo_connection_health_check_error",
|
?SLOG(warning, #{
|
||||||
worker => Worker,
|
msg => "mongo_connection_health_check_error",
|
||||||
reason => worker_not_found}),
|
worker => Worker,
|
||||||
|
reason => worker_not_found
|
||||||
|
}),
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_test_query(Conn) ->
|
do_test_query(Conn) ->
|
||||||
mongoc:transaction_query(
|
mongoc:transaction_query(
|
||||||
Conn,
|
Conn,
|
||||||
fun(Conf = #{pool := Worker}) ->
|
fun(Conf = #{pool := Worker}) ->
|
||||||
Query = mongoc:find_one_query(Conf, <<"foo">>, #{}, #{}, 0),
|
Query = mongoc:find_one_query(Conf, <<"foo">>, #{}, #{}, 0),
|
||||||
mc_worker_api:find_one(Worker, Query)
|
mc_worker_api:find_one(Worker, Query)
|
||||||
end,
|
end,
|
||||||
#{},
|
#{},
|
||||||
?HEALTH_CHECK_TIMEOUT).
|
?HEALTH_CHECK_TIMEOUT
|
||||||
|
).
|
||||||
|
|
||||||
connect(Opts) ->
|
connect(Opts) ->
|
||||||
Type = proplists:get_value(mongo_type, Opts, single),
|
Type = proplists:get_value(mongo_type, Opts, single),
|
||||||
|
|
@ -244,10 +299,8 @@ connect(Opts) ->
|
||||||
|
|
||||||
mongo_query(Conn, find, Collection, Selector, Projector) ->
|
mongo_query(Conn, find, Collection, Selector, Projector) ->
|
||||||
mongo_api:find(Conn, Collection, Selector, Projector);
|
mongo_api:find(Conn, Collection, Selector, Projector);
|
||||||
|
|
||||||
mongo_query(Conn, find_one, Collection, Selector, Projector) ->
|
mongo_query(Conn, find_one, Collection, Selector, Projector) ->
|
||||||
mongo_api:find_one(Conn, Collection, Selector, Projector);
|
mongo_api:find_one(Conn, Collection, Selector, Projector);
|
||||||
|
|
||||||
%% Todo xxx
|
%% Todo xxx
|
||||||
mongo_query(_Conn, _Action, _Collection, _Selector, _Projector) ->
|
mongo_query(_Conn, _Action, _Collection, _Selector, _Projector) ->
|
||||||
ok.
|
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, [{r_mode, V} | Acc]);
|
||||||
init_worker_options([_ | R], Acc) ->
|
init_worker_options([_ | R], Acc) ->
|
||||||
init_worker_options(R, Acc);
|
init_worker_options(R, Acc);
|
||||||
init_worker_options([], Acc) -> Acc.
|
init_worker_options([], Acc) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
%% Schema funcs
|
%% 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_(Config).
|
may_parse_srv_and_txt_records_(Config).
|
||||||
|
|
||||||
may_parse_srv_and_txt_records_(#{mongo_type := Type,
|
may_parse_srv_and_txt_records_(
|
||||||
srv_record := false,
|
#{
|
||||||
servers := Servers} = Config) ->
|
mongo_type := Type,
|
||||||
|
srv_record := false,
|
||||||
|
servers := Servers
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
case Type =:= rs andalso maps:is_key(replica_set_name, Config) =:= false of
|
case Type =:= rs andalso maps:is_key(replica_set_name, Config) =:= false of
|
||||||
true ->
|
true ->
|
||||||
error({missing_parameter, replica_set_name});
|
error({missing_parameter, replica_set_name});
|
||||||
false ->
|
false ->
|
||||||
Config#{hosts => servers_to_bin(Servers)}
|
Config#{hosts => servers_to_bin(Servers)}
|
||||||
end;
|
end;
|
||||||
may_parse_srv_and_txt_records_(#{mongo_type := Type,
|
may_parse_srv_and_txt_records_(
|
||||||
srv_record := true,
|
#{
|
||||||
servers := Servers} = Config) ->
|
mongo_type := Type,
|
||||||
|
srv_record := true,
|
||||||
|
servers := Servers
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
Hosts = parse_srv_records(Type, Servers),
|
Hosts = parse_srv_records(Type, Servers),
|
||||||
ExtraOpts = parse_txt_records(Type, Servers),
|
ExtraOpts = parse_txt_records(Type, Servers),
|
||||||
maps:merge(Config#{hosts => Hosts}, ExtraOpts).
|
maps:merge(Config#{hosts => Hosts}, ExtraOpts).
|
||||||
|
|
||||||
parse_srv_records(Type, Servers) ->
|
parse_srv_records(Type, Servers) ->
|
||||||
Fun = fun(AccIn, {IpOrHost, _Port}) ->
|
Fun = fun(AccIn, {IpOrHost, _Port}) ->
|
||||||
case inet_res:lookup("_mongodb._tcp."
|
case
|
||||||
++ ip_or_host_to_string(IpOrHost), in, srv) of
|
inet_res:lookup(
|
||||||
[] ->
|
"_mongodb._tcp." ++
|
||||||
error(service_not_found);
|
ip_or_host_to_string(IpOrHost),
|
||||||
Services ->
|
in,
|
||||||
[ [server_to_bin({Host, Port}) || {_, _, Port, Host} <- Services]
|
srv
|
||||||
| AccIn]
|
)
|
||||||
end
|
of
|
||||||
end,
|
[] ->
|
||||||
|
error(service_not_found);
|
||||||
|
Services ->
|
||||||
|
[
|
||||||
|
[server_to_bin({Host, Port}) || {_, _, Port, Host} <- Services]
|
||||||
|
| AccIn
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end,
|
||||||
Res = lists:foldl(Fun, [], Servers),
|
Res = lists:foldl(Fun, [], Servers),
|
||||||
case Type of
|
case Type of
|
||||||
single -> lists:nth(1, Res);
|
single -> lists:nth(1, Res);
|
||||||
_ -> Res
|
_ -> Res
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_txt_records(Type, Servers) ->
|
parse_txt_records(Type, Servers) ->
|
||||||
Fields = case Type of
|
Fields =
|
||||||
rs -> ["authSource", "replicaSet"];
|
case Type of
|
||||||
_ -> ["authSource"]
|
rs -> ["authSource", "replicaSet"];
|
||||||
end,
|
_ -> ["authSource"]
|
||||||
|
end,
|
||||||
Fun = fun(AccIn, {IpOrHost, _Port}) ->
|
Fun = fun(AccIn, {IpOrHost, _Port}) ->
|
||||||
case inet_res:lookup(IpOrHost, in, txt) of
|
case inet_res:lookup(IpOrHost, in, txt) of
|
||||||
[] ->
|
[] ->
|
||||||
#{};
|
#{};
|
||||||
[[QueryString]] ->
|
[[QueryString]] ->
|
||||||
case uri_string:dissect_query(QueryString) of
|
case uri_string:dissect_query(QueryString) of
|
||||||
{error, _, _} ->
|
{error, _, _} ->
|
||||||
error({invalid_txt_record, invalid_query_string});
|
error({invalid_txt_record, invalid_query_string});
|
||||||
Options ->
|
Options ->
|
||||||
maps:merge(AccIn, take_and_convert(Fields, Options))
|
maps:merge(AccIn, take_and_convert(Fields, Options))
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
error({invalid_txt_record, multiple_records})
|
error({invalid_txt_record, multiple_records})
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
lists:foldl(Fun, #{}, Servers).
|
lists:foldl(Fun, #{}, Servers).
|
||||||
|
|
||||||
take_and_convert(Fields, Options) ->
|
take_and_convert(Fields, Options) ->
|
||||||
|
|
@ -430,8 +501,8 @@ take_and_convert([Field | More], Options, Acc) ->
|
||||||
take_and_convert(More, Options, Acc)
|
take_and_convert(More, Options, Acc)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec ip_or_host_to_string(binary() | string() | tuple())
|
-spec ip_or_host_to_string(binary() | string() | tuple()) ->
|
||||||
-> string().
|
string().
|
||||||
ip_or_host_to_string(Ip) when is_tuple(Ip) ->
|
ip_or_host_to_string(Ip) when is_tuple(Ip) ->
|
||||||
inet:ntoa(Ip);
|
inet:ntoa(Ip);
|
||||||
ip_or_host_to_string(Host) ->
|
ip_or_host_to_string(Host) ->
|
||||||
|
|
@ -448,18 +519,20 @@ server_to_bin({IpOrHost, Port}) ->
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
%% typereflt funcs
|
%% typereflt funcs
|
||||||
|
|
||||||
-spec to_server_raw(string())
|
-spec to_server_raw(string()) ->
|
||||||
-> {string(), pos_integer()}.
|
{string(), pos_integer()}.
|
||||||
to_server_raw(Server) ->
|
to_server_raw(Server) ->
|
||||||
emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS).
|
emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS).
|
||||||
|
|
||||||
-spec to_servers_raw(string())
|
-spec to_servers_raw(string()) ->
|
||||||
-> [{string(), pos_integer()}].
|
[{string(), pos_integer()}].
|
||||||
to_servers_raw(Servers) ->
|
to_servers_raw(Servers) ->
|
||||||
lists:map( fun(Server) ->
|
lists:map(
|
||||||
emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS)
|
fun(Server) ->
|
||||||
end
|
emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS)
|
||||||
, string:tokens(str(Servers), ", ")).
|
end,
|
||||||
|
string:tokens(str(Servers), ", ")
|
||||||
|
).
|
||||||
|
|
||||||
str(A) when is_atom(A) ->
|
str(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
|
|
|
||||||
|
|
@ -23,28 +23,32 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% API and callbacks for supervisor
|
%% API and callbacks for supervisor
|
||||||
-export([ start_link/0
|
-export([
|
||||||
, init/1
|
start_link/0,
|
||||||
, create_bridge/1
|
init/1,
|
||||||
, drop_bridge/1
|
create_bridge/1,
|
||||||
, bridges/0
|
drop_bridge/1,
|
||||||
]).
|
bridges/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([on_message_received/3]).
|
-export([on_message_received/3]).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-import(hoconsc, [mk/2]).
|
-import(hoconsc, [mk/2]).
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1]).
|
roots/0,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
%% Hocon schema
|
%% Hocon schema
|
||||||
|
|
@ -53,25 +57,34 @@ roots() ->
|
||||||
|
|
||||||
fields("config") ->
|
fields("config") ->
|
||||||
emqx_connector_mqtt_schema:fields("config");
|
emqx_connector_mqtt_schema:fields("config");
|
||||||
|
|
||||||
fields("get") ->
|
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("post");
|
||||||
|
|
||||||
fields("put") ->
|
fields("put") ->
|
||||||
emqx_connector_mqtt_schema:fields("connector");
|
emqx_connector_mqtt_schema:fields("connector");
|
||||||
|
|
||||||
fields("post") ->
|
fields("post") ->
|
||||||
[ {type, mk(mqtt,
|
[
|
||||||
#{ required => true
|
{type,
|
||||||
, desc => ?DESC("type")
|
mk(
|
||||||
})}
|
mqtt,
|
||||||
, {name, mk(binary(),
|
#{
|
||||||
#{ required => true
|
required => true,
|
||||||
, desc => ?DESC("name")
|
desc => ?DESC("type")
|
||||||
})}
|
}
|
||||||
|
)},
|
||||||
|
{name,
|
||||||
|
mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC("name")
|
||||||
|
}
|
||||||
|
)}
|
||||||
] ++ fields("put").
|
] ++ fields("put").
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
|
|
@ -80,23 +93,29 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
SupFlag = #{strategy => one_for_one,
|
SupFlag = #{
|
||||||
intensity => 100,
|
strategy => one_for_one,
|
||||||
period => 10},
|
intensity => 100,
|
||||||
|
period => 10
|
||||||
|
},
|
||||||
{ok, {SupFlag, []}}.
|
{ok, {SupFlag, []}}.
|
||||||
|
|
||||||
bridge_spec(Config) ->
|
bridge_spec(Config) ->
|
||||||
#{id => maps:get(name, Config),
|
#{
|
||||||
start => {emqx_connector_mqtt_worker, start_link, [Config]},
|
id => maps:get(name, Config),
|
||||||
restart => permanent,
|
start => {emqx_connector_mqtt_worker, start_link, [Config]},
|
||||||
shutdown => 5000,
|
restart => permanent,
|
||||||
type => worker,
|
shutdown => 5000,
|
||||||
modules => [emqx_connector_mqtt_worker]}.
|
type => worker,
|
||||||
|
modules => [emqx_connector_mqtt_worker]
|
||||||
|
}.
|
||||||
|
|
||||||
-spec(bridges() -> [{node(), map()}]).
|
-spec bridges() -> [{node(), map()}].
|
||||||
bridges() ->
|
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) ->
|
create_bridge(Config) ->
|
||||||
supervisor:start_child(?MODULE, bridge_spec(Config)).
|
supervisor:start_child(?MODULE, bridge_spec(Config)).
|
||||||
|
|
@ -121,8 +140,11 @@ on_message_received(Msg, HookPoint, InstId) ->
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, Conf) ->
|
on_start(InstId, Conf) ->
|
||||||
InstanceId = binary_to_atom(InstId, utf8),
|
InstanceId = binary_to_atom(InstId, utf8),
|
||||||
?SLOG(info, #{msg => "starting_mqtt_connector",
|
?SLOG(info, #{
|
||||||
connector => InstanceId, config => Conf}),
|
msg => "starting_mqtt_connector",
|
||||||
|
connector => InstanceId,
|
||||||
|
config => Conf
|
||||||
|
}),
|
||||||
BasicConf = basic_config(Conf),
|
BasicConf = basic_config(Conf),
|
||||||
BridgeConf = BasicConf#{
|
BridgeConf = BasicConf#{
|
||||||
name => InstanceId,
|
name => InstanceId,
|
||||||
|
|
@ -142,19 +164,25 @@ on_start(InstId, Conf) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(_InstId, #{name := InstanceId}) ->
|
on_stop(_InstId, #{name := InstanceId}) ->
|
||||||
?SLOG(info, #{msg => "stopping_mqtt_connector",
|
?SLOG(info, #{
|
||||||
connector => InstanceId}),
|
msg => "stopping_mqtt_connector",
|
||||||
|
connector => InstanceId
|
||||||
|
}),
|
||||||
case ?MODULE:drop_bridge(InstanceId) of
|
case ?MODULE:drop_bridge(InstanceId) of
|
||||||
ok -> ok;
|
ok ->
|
||||||
{error, not_found} -> ok;
|
ok;
|
||||||
|
{error, not_found} ->
|
||||||
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "stop_mqtt_connector",
|
?SLOG(error, #{
|
||||||
connector => InstanceId, reason => Reason})
|
msg => "stop_mqtt_connector",
|
||||||
|
connector => InstanceId,
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_query(_InstId, {message_received, _Msg}, AfterQuery, _State) ->
|
on_query(_InstId, {message_received, _Msg}, AfterQuery, _State) ->
|
||||||
emqx_resource:query_success(AfterQuery);
|
emqx_resource:query_success(AfterQuery);
|
||||||
|
|
||||||
on_query(_InstId, {send_message, Msg}, AfterQuery, #{name := InstanceId}) ->
|
on_query(_InstId, {send_message, Msg}, AfterQuery, #{name := InstanceId}) ->
|
||||||
?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}),
|
?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}),
|
||||||
emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg),
|
emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg),
|
||||||
|
|
@ -178,7 +206,8 @@ make_sub_confs(undefined, _) ->
|
||||||
undefined;
|
undefined;
|
||||||
make_sub_confs(SubRemoteConf, InstId) ->
|
make_sub_confs(SubRemoteConf, InstId) ->
|
||||||
case maps:take(hookpoint, SubRemoteConf) of
|
case maps:take(hookpoint, SubRemoteConf) of
|
||||||
error -> SubRemoteConf;
|
error ->
|
||||||
|
SubRemoteConf;
|
||||||
{HookPoint, SubConf} ->
|
{HookPoint, SubConf} ->
|
||||||
MFA = {?MODULE, on_message_received, [HookPoint, InstId]},
|
MFA = {?MODULE, on_message_received, [HookPoint, InstId]},
|
||||||
SubConf#{on_message_received => MFA}
|
SubConf#{on_message_received => MFA}
|
||||||
|
|
@ -192,22 +221,24 @@ make_forward_confs(FrowardConf) ->
|
||||||
FrowardConf.
|
FrowardConf.
|
||||||
|
|
||||||
basic_config(#{
|
basic_config(#{
|
||||||
server := Server,
|
server := Server,
|
||||||
reconnect_interval := ReconnIntv,
|
reconnect_interval := ReconnIntv,
|
||||||
proto_ver := ProtoVer,
|
proto_ver := ProtoVer,
|
||||||
username := User,
|
username := User,
|
||||||
password := Password,
|
password := Password,
|
||||||
clean_start := CleanStart,
|
clean_start := CleanStart,
|
||||||
keepalive := KeepAlive,
|
keepalive := KeepAlive,
|
||||||
retry_interval := RetryIntv,
|
retry_interval := RetryIntv,
|
||||||
max_inflight := MaxInflight,
|
max_inflight := MaxInflight,
|
||||||
replayq := ReplayQ,
|
replayq := ReplayQ,
|
||||||
ssl := #{enable := EnableSsl} = Ssl}) ->
|
ssl := #{enable := EnableSsl} = Ssl
|
||||||
|
}) ->
|
||||||
#{
|
#{
|
||||||
replayq => ReplayQ,
|
replayq => ReplayQ,
|
||||||
%% connection opts
|
%% connection opts
|
||||||
server => Server,
|
server => Server,
|
||||||
connect_timeout => 30, %% 30s
|
%% 30s
|
||||||
|
connect_timeout => 30,
|
||||||
reconnect_interval => ReconnIntv,
|
reconnect_interval => ReconnIntv,
|
||||||
proto_ver => ProtoVer,
|
proto_ver => ProtoVer,
|
||||||
bridge_mode => true,
|
bridge_mode => true,
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,12 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% ecpool connect & reconnect
|
%% ecpool connect & reconnect
|
||||||
-export([connect/1, prepare_sql_to_conn/2]).
|
-export([connect/1, prepare_sql_to_conn/2]).
|
||||||
|
|
@ -38,9 +39,10 @@
|
||||||
|
|
||||||
-export([do_health_check/1]).
|
-export([do_health_check/1]).
|
||||||
|
|
||||||
-define( MYSQL_HOST_OPTIONS
|
-define(MYSQL_HOST_OPTIONS, #{
|
||||||
, #{ host_type => inet_addr
|
host_type => inet_addr,
|
||||||
, default_port => ?MYSQL_DEFAULT_PORT}).
|
default_port => ?MYSQL_DEFAULT_PORT
|
||||||
|
}).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
%% Hocon schema
|
%% Hocon schema
|
||||||
|
|
@ -48,11 +50,10 @@ roots() ->
|
||||||
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
|
||||||
|
|
||||||
fields(config) ->
|
fields(config) ->
|
||||||
[ {server, fun server/1}
|
[{server, fun server/1}] ++
|
||||||
] ++
|
emqx_connector_schema_lib:relational_db_fields() ++
|
||||||
emqx_connector_schema_lib:relational_db_fields() ++
|
emqx_connector_schema_lib:ssl_fields() ++
|
||||||
emqx_connector_schema_lib:ssl_fields() ++
|
emqx_connector_schema_lib:prepare_statement_fields().
|
||||||
emqx_connector_schema_lib:prepare_statement_fields().
|
|
||||||
|
|
||||||
server(type) -> emqx_schema:ip_port();
|
server(type) -> emqx_schema:ip_port();
|
||||||
server(required) -> true;
|
server(required) -> true;
|
||||||
|
|
@ -62,47 +63,64 @@ server(desc) -> ?DESC("server");
|
||||||
server(_) -> undefined.
|
server(_) -> undefined.
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, #{server := {Host, Port},
|
on_start(
|
||||||
database := DB,
|
InstId,
|
||||||
username := User,
|
#{
|
||||||
password := Password,
|
server := {Host, Port},
|
||||||
auto_reconnect := AutoReconn,
|
database := DB,
|
||||||
pool_size := PoolSize,
|
username := User,
|
||||||
ssl := SSL } = Config) ->
|
password := Password,
|
||||||
?SLOG(info, #{msg => "starting_mysql_connector",
|
auto_reconnect := AutoReconn,
|
||||||
connector => InstId, config => Config}),
|
pool_size := PoolSize,
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
ssl := SSL
|
||||||
true ->
|
} = Config
|
||||||
[{ssl, emqx_tls_lib:to_client_opts(SSL)}];
|
) ->
|
||||||
false ->
|
?SLOG(info, #{
|
||||||
[]
|
msg => "starting_mysql_connector",
|
||||||
end,
|
connector => InstId,
|
||||||
Options = [{host, Host},
|
config => Config
|
||||||
{port, Port},
|
}),
|
||||||
{user, User},
|
SslOpts =
|
||||||
{password, Password},
|
case maps:get(enable, SSL) of
|
||||||
{database, DB},
|
true ->
|
||||||
{auto_reconnect, reconn_interval(AutoReconn)},
|
[{ssl, emqx_tls_lib:to_client_opts(SSL)}];
|
||||||
{pool_size, PoolSize}],
|
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),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
Prepares = maps:get(prepare_statement, Config, #{}),
|
Prepares = maps:get(prepare_statement, Config, #{}),
|
||||||
State = init_prepare(#{poolname => PoolName, prepare_statement => Prepares}),
|
State = init_prepare(#{poolname => PoolName, prepare_statement => Prepares}),
|
||||||
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of
|
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of
|
||||||
ok -> {ok, State};
|
ok -> {ok, State};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
?SLOG(info, #{msg => "stopping_mysql_connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping_mysql_connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {Type, SQLOrKey}, AfterQuery, State) ->
|
on_query(InstId, {Type, SQLOrKey}, AfterQuery, State) ->
|
||||||
on_query(InstId, {Type, SQLOrKey, [], default_timeout}, 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}, AfterQuery, State) ->
|
||||||
on_query(InstId, {Type, SQLOrKey, Params, default_timeout}, AfterQuery, State);
|
on_query(InstId, {Type, SQLOrKey, Params, default_timeout}, AfterQuery, State);
|
||||||
on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery,
|
on_query(
|
||||||
#{poolname := PoolName, prepare_statement := Prepares} = State) ->
|
InstId,
|
||||||
|
{Type, SQLOrKey, Params, Timeout},
|
||||||
|
AfterQuery,
|
||||||
|
#{poolname := PoolName, prepare_statement := Prepares} = State
|
||||||
|
) ->
|
||||||
LogMeta = #{connector => InstId, sql => SQLOrKey, state => State},
|
LogMeta = #{connector => InstId, sql => SQLOrKey, state => State},
|
||||||
?TRACE("QUERY", "mysql_connector_received", LogMeta),
|
?TRACE("QUERY", "mysql_connector_received", LogMeta),
|
||||||
Worker = ecpool:get_client(PoolName),
|
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]),
|
Result = erlang:apply(mysql, MySqlFunction, [Conn, SQLOrKey, Params, Timeout]),
|
||||||
case Result of
|
case Result of
|
||||||
{error, disconnected} ->
|
{error, disconnected} ->
|
||||||
?SLOG(error,
|
?SLOG(
|
||||||
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected}),
|
error,
|
||||||
|
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected}
|
||||||
|
),
|
||||||
%% kill the poll worker to trigger reconnection
|
%% kill the poll worker to trigger reconnection
|
||||||
_ = exit(Conn, restart),
|
_ = exit(Conn, restart),
|
||||||
emqx_resource:query_failed(AfterQuery),
|
emqx_resource:query_failed(AfterQuery),
|
||||||
Result;
|
Result;
|
||||||
{error, not_prepared} ->
|
{error, not_prepared} ->
|
||||||
?SLOG(warning,
|
?SLOG(
|
||||||
LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared}),
|
warning,
|
||||||
|
LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared}
|
||||||
|
),
|
||||||
case prepare_sql(Prepares, PoolName) of
|
case prepare_sql(Prepares, PoolName) of
|
||||||
ok ->
|
ok ->
|
||||||
%% not return result, next loop will try again
|
%% not return result, next loop will try again
|
||||||
on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, State);
|
on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, State);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error,
|
?SLOG(
|
||||||
LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason}),
|
error,
|
||||||
|
LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason}
|
||||||
|
),
|
||||||
emqx_resource:query_failed(AfterQuery),
|
emqx_resource:query_failed(AfterQuery),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error,
|
?SLOG(
|
||||||
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}),
|
error,
|
||||||
|
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}
|
||||||
|
),
|
||||||
emqx_resource:query_failed(AfterQuery),
|
emqx_resource:query_failed(AfterQuery),
|
||||||
Result;
|
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
|
case emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, State) of
|
||||||
{ok, State} ->
|
{ok, State} ->
|
||||||
case do_health_check_prepares(State) of
|
case do_health_check_prepares(State) of
|
||||||
ok->
|
ok ->
|
||||||
{ok, State};
|
{ok, State};
|
||||||
{ok, NState} ->
|
{ok, NState} ->
|
||||||
{ok, NState};
|
{ok, NState};
|
||||||
|
|
@ -161,7 +187,7 @@ on_health_check(_InstId, #{poolname := PoolName} = State) ->
|
||||||
do_health_check(Conn) ->
|
do_health_check(Conn) ->
|
||||||
ok == element(1, mysql:query(Conn, <<"SELECT count(1) AS T">>)).
|
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;
|
ok;
|
||||||
do_health_check_prepares(State = #{poolname := PoolName, prepare_statement := {error, Prepares}}) ->
|
do_health_check_prepares(State = #{poolname := PoolName, prepare_statement := {error, Prepares}}) ->
|
||||||
%% retry to prepare
|
%% retry to prepare
|
||||||
|
|
@ -180,8 +206,8 @@ reconn_interval(false) -> false.
|
||||||
connect(Options) ->
|
connect(Options) ->
|
||||||
mysql:start_link(Options).
|
mysql:start_link(Options).
|
||||||
|
|
||||||
-spec to_server(string())
|
-spec to_server(string()) ->
|
||||||
-> {inet:ip_address() | inet:hostname(), pos_integer()}.
|
{inet:ip_address() | inet:hostname(), pos_integer()}.
|
||||||
to_server(Str) ->
|
to_server(Str) ->
|
||||||
emqx_connector_schema_lib:parse_server(Str, ?MYSQL_HOST_OPTIONS).
|
emqx_connector_schema_lib:parse_server(Str, ?MYSQL_HOST_OPTIONS).
|
||||||
|
|
||||||
|
|
@ -215,20 +241,27 @@ prepare_sql(Prepares, PoolName) ->
|
||||||
|
|
||||||
do_prepare_sql(Prepares, PoolName) ->
|
do_prepare_sql(Prepares, PoolName) ->
|
||||||
Conns =
|
Conns =
|
||||||
[begin
|
[
|
||||||
{ok, Conn} = ecpool_worker:client(Worker),
|
begin
|
||||||
Conn
|
{ok, Conn} = ecpool_worker:client(Worker),
|
||||||
end || {_Name, Worker} <- ecpool:workers(PoolName)],
|
Conn
|
||||||
|
end
|
||||||
|
|| {_Name, Worker} <- ecpool:workers(PoolName)
|
||||||
|
],
|
||||||
prepare_sql_to_conn_list(Conns, Prepares).
|
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) ->
|
prepare_sql_to_conn_list([Conn | ConnList], PrepareList) ->
|
||||||
case prepare_sql_to_conn(Conn, PrepareList) of
|
case prepare_sql_to_conn(Conn, PrepareList) of
|
||||||
ok ->
|
ok ->
|
||||||
prepare_sql_to_conn_list(ConnList, PrepareList);
|
prepare_sql_to_conn_list(ConnList, PrepareList);
|
||||||
{error, R} ->
|
{error, R} ->
|
||||||
%% rollback
|
%% 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),
|
lists:foreach(Fun, PrepareList),
|
||||||
{error, R}
|
{error, R}
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -26,24 +26,26 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([connect/1]).
|
-export([connect/1]).
|
||||||
|
|
||||||
-export([ query/3
|
-export([
|
||||||
, prepared_query/3
|
query/3,
|
||||||
]).
|
prepared_query/3
|
||||||
|
]).
|
||||||
|
|
||||||
-export([do_health_check/1]).
|
-export([do_health_check/1]).
|
||||||
|
|
||||||
-define( PGSQL_HOST_OPTIONS
|
-define(PGSQL_HOST_OPTIONS, #{
|
||||||
, #{ host_type => inet_addr
|
host_type => inet_addr,
|
||||||
, default_port => ?PGSQL_DEFAULT_PORT}).
|
default_port => ?PGSQL_DEFAULT_PORT
|
||||||
|
}).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
|
|
||||||
|
|
@ -52,9 +54,9 @@ roots() ->
|
||||||
|
|
||||||
fields(config) ->
|
fields(config) ->
|
||||||
[{server, fun server/1}] ++
|
[{server, fun server/1}] ++
|
||||||
emqx_connector_schema_lib:relational_db_fields() ++
|
emqx_connector_schema_lib:relational_db_fields() ++
|
||||||
emqx_connector_schema_lib:ssl_fields() ++
|
emqx_connector_schema_lib:ssl_fields() ++
|
||||||
emqx_connector_schema_lib:prepare_statement_fields().
|
emqx_connector_schema_lib:prepare_statement_fields().
|
||||||
|
|
||||||
server(type) -> emqx_schema:ip_port();
|
server(type) -> emqx_schema:ip_port();
|
||||||
server(required) -> true;
|
server(required) -> true;
|
||||||
|
|
@ -64,52 +66,73 @@ server(desc) -> ?DESC("server");
|
||||||
server(_) -> undefined.
|
server(_) -> undefined.
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, #{server := {Host, Port},
|
on_start(
|
||||||
database := DB,
|
InstId,
|
||||||
username := User,
|
#{
|
||||||
password := Password,
|
server := {Host, Port},
|
||||||
auto_reconnect := AutoReconn,
|
database := DB,
|
||||||
pool_size := PoolSize,
|
username := User,
|
||||||
ssl := SSL} = Config) ->
|
password := Password,
|
||||||
?SLOG(info, #{msg => "starting_postgresql_connector",
|
auto_reconnect := AutoReconn,
|
||||||
connector => InstId, config => Config}),
|
pool_size := PoolSize,
|
||||||
SslOpts = case maps:get(enable, SSL) of
|
ssl := SSL
|
||||||
true ->
|
} = Config
|
||||||
[{ssl, true},
|
) ->
|
||||||
{ssl_opts, emqx_tls_lib:to_client_opts(SSL)}];
|
?SLOG(info, #{
|
||||||
false ->
|
msg => "starting_postgresql_connector",
|
||||||
[{ssl, false}]
|
connector => InstId,
|
||||||
end,
|
config => Config
|
||||||
Options = [{host, Host},
|
}),
|
||||||
{port, Port},
|
SslOpts =
|
||||||
{username, User},
|
case maps:get(enable, SSL) of
|
||||||
{password, Password},
|
true ->
|
||||||
{database, DB},
|
[
|
||||||
{auto_reconnect, reconn_interval(AutoReconn)},
|
{ssl, true},
|
||||||
{pool_size, PoolSize},
|
{ssl_opts, emqx_tls_lib:to_client_opts(SSL)}
|
||||||
{prepare_statement, maps:to_list(maps:get(prepare_statement, Config, #{}))}],
|
];
|
||||||
|
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),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of
|
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of
|
||||||
ok -> {ok, #{poolname => PoolName}};
|
ok -> {ok, #{poolname => PoolName}};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName}) ->
|
on_stop(InstId, #{poolname := PoolName}) ->
|
||||||
?SLOG(info, #{msg => "stopping postgresql connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping postgresql connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
emqx_plugin_libs_pool:stop_pool(PoolName).
|
emqx_plugin_libs_pool:stop_pool(PoolName).
|
||||||
|
|
||||||
on_query(InstId, {Type, NameOrSQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
on_query(InstId, {Type, NameOrSQL}, AfterQuery, #{poolname := _PoolName} = State) ->
|
||||||
on_query(InstId, {Type, NameOrSQL, []}, AfterQuery, State);
|
on_query(InstId, {Type, NameOrSQL, []}, AfterQuery, State);
|
||||||
|
|
||||||
on_query(InstId, {Type, NameOrSQL, Params}, AfterQuery, #{poolname := PoolName} = State) ->
|
on_query(InstId, {Type, NameOrSQL, Params}, AfterQuery, #{poolname := PoolName} = State) ->
|
||||||
?SLOG(debug, #{msg => "postgresql connector received sql query",
|
?SLOG(debug, #{
|
||||||
connector => InstId, sql => NameOrSQL, state => State}),
|
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
|
case Result = ecpool:pick_and_do(PoolName, {?MODULE, Type, [NameOrSQL, Params]}, no_handover) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{
|
?SLOG(error, #{
|
||||||
msg => "postgresql connector do sql query failed",
|
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_failed(AfterQuery);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterQuery)
|
emqx_resource:query_success(AfterQuery)
|
||||||
|
|
@ -127,7 +150,7 @@ reconn_interval(true) -> 15;
|
||||||
reconn_interval(false) -> false.
|
reconn_interval(false) -> false.
|
||||||
|
|
||||||
connect(Opts) ->
|
connect(Opts) ->
|
||||||
Host = proplists:get_value(host, Opts),
|
Host = proplists:get_value(host, Opts),
|
||||||
Username = proplists:get_value(username, Opts),
|
Username = proplists:get_value(username, Opts),
|
||||||
Password = proplists:get_value(password, Opts),
|
Password = proplists:get_value(password, Opts),
|
||||||
PrepareStatement = proplists:get_value(prepare_statement, Opts),
|
PrepareStatement = proplists:get_value(prepare_statement, Opts),
|
||||||
|
|
@ -177,7 +200,7 @@ conn_opts([_Opt | Opts], Acc) ->
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
%% typereflt funcs
|
%% typereflt funcs
|
||||||
|
|
||||||
-spec to_server(string())
|
-spec to_server(string()) ->
|
||||||
-> {inet:ip_address() | inet:hostname(), pos_integer()}.
|
{inet:ip_address() | inet:hostname(), pos_integer()}.
|
||||||
to_server(Str) ->
|
to_server(Str) ->
|
||||||
emqx_connector_schema_lib:parse_server(Str, ?PGSQL_HOST_OPTIONS).
|
emqx_connector_schema_lib:parse_server(Str, ?PGSQL_HOST_OPTIONS).
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([do_health_check/1]).
|
-export([do_health_check/1]).
|
||||||
|
|
||||||
|
|
@ -38,50 +39,59 @@
|
||||||
-export([cmd/3]).
|
-export([cmd/3]).
|
||||||
|
|
||||||
%% redis host don't need parse
|
%% redis host don't need parse
|
||||||
-define( REDIS_HOST_OPTIONS
|
-define(REDIS_HOST_OPTIONS, #{
|
||||||
, #{ host_type => hostname
|
host_type => hostname,
|
||||||
, default_port => ?REDIS_DEFAULT_PORT}).
|
default_port => ?REDIS_DEFAULT_PORT
|
||||||
|
}).
|
||||||
|
|
||||||
%%=====================================================================
|
%%=====================================================================
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {config, #{type => hoconsc:union(
|
[
|
||||||
[ hoconsc:ref(?MODULE, cluster)
|
{config, #{
|
||||||
, hoconsc:ref(?MODULE, single)
|
type => hoconsc:union(
|
||||||
, hoconsc:ref(?MODULE, sentinel)
|
[
|
||||||
])}
|
hoconsc:ref(?MODULE, cluster),
|
||||||
}
|
hoconsc:ref(?MODULE, single),
|
||||||
|
hoconsc:ref(?MODULE, sentinel)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}}
|
||||||
].
|
].
|
||||||
|
|
||||||
fields(single) ->
|
fields(single) ->
|
||||||
[ {server, fun server/1}
|
[
|
||||||
, {redis_type, #{type => hoconsc:enum([single]),
|
{server, fun server/1},
|
||||||
required => true,
|
{redis_type, #{
|
||||||
desc => ?DESC("single")
|
type => hoconsc:enum([single]),
|
||||||
}}
|
required => true,
|
||||||
|
desc => ?DESC("single")
|
||||||
|
}}
|
||||||
] ++
|
] ++
|
||||||
redis_fields() ++
|
redis_fields() ++
|
||||||
emqx_connector_schema_lib:ssl_fields();
|
emqx_connector_schema_lib:ssl_fields();
|
||||||
fields(cluster) ->
|
fields(cluster) ->
|
||||||
[ {servers, fun servers/1}
|
[
|
||||||
, {redis_type, #{type => hoconsc:enum([cluster]),
|
{servers, fun servers/1},
|
||||||
required => true,
|
{redis_type, #{
|
||||||
desc => ?DESC("cluster")
|
type => hoconsc:enum([cluster]),
|
||||||
}}
|
required => true,
|
||||||
|
desc => ?DESC("cluster")
|
||||||
|
}}
|
||||||
] ++
|
] ++
|
||||||
redis_fields() ++
|
redis_fields() ++
|
||||||
emqx_connector_schema_lib:ssl_fields();
|
emqx_connector_schema_lib:ssl_fields();
|
||||||
fields(sentinel) ->
|
fields(sentinel) ->
|
||||||
[ {servers, fun servers/1}
|
[
|
||||||
, {redis_type, #{type => hoconsc:enum([sentinel]),
|
{servers, fun servers/1},
|
||||||
required => true,
|
{redis_type, #{
|
||||||
desc => ?DESC("sentinel")
|
type => hoconsc:enum([sentinel]),
|
||||||
}}
|
required => true,
|
||||||
, {sentinel, #{type => string(), desc => ?DESC("sentinel_desc")
|
desc => ?DESC("sentinel")
|
||||||
}}
|
}},
|
||||||
|
{sentinel, #{type => string(), desc => ?DESC("sentinel_desc")}}
|
||||||
] ++
|
] ++
|
||||||
redis_fields() ++
|
redis_fields() ++
|
||||||
emqx_connector_schema_lib:ssl_fields().
|
emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
server(type) -> emqx_schema:ip_port();
|
server(type) -> emqx_schema:ip_port();
|
||||||
server(required) -> true;
|
server(required) -> true;
|
||||||
|
|
@ -98,62 +108,89 @@ servers(desc) -> ?DESC("servers");
|
||||||
servers(_) -> undefined.
|
servers(_) -> undefined.
|
||||||
|
|
||||||
%% ===================================================================
|
%% ===================================================================
|
||||||
on_start(InstId, #{redis_type := Type,
|
on_start(
|
||||||
database := Database,
|
InstId,
|
||||||
pool_size := PoolSize,
|
#{
|
||||||
auto_reconnect := AutoReconn,
|
redis_type := Type,
|
||||||
ssl := SSL } = Config) ->
|
database := Database,
|
||||||
?SLOG(info, #{msg => "starting_redis_connector",
|
pool_size := PoolSize,
|
||||||
connector => InstId, config => Config}),
|
auto_reconnect := AutoReconn,
|
||||||
Servers = case Type of
|
ssl := SSL
|
||||||
single -> [{servers, [maps:get(server, Config)]}];
|
} = Config
|
||||||
_ ->[{servers, maps:get(servers, Config)}]
|
) ->
|
||||||
end,
|
?SLOG(info, #{
|
||||||
Opts = [{pool_size, PoolSize},
|
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},
|
{database, Database},
|
||||||
{password, maps:get(password, Config, "")},
|
{password, maps:get(password, Config, "")},
|
||||||
{auto_reconnect, reconn_interval(AutoReconn)}
|
{auto_reconnect, reconn_interval(AutoReconn)}
|
||||||
] ++ Servers,
|
] ++ Servers,
|
||||||
Options = case maps:get(enable, SSL) of
|
Options =
|
||||||
true ->
|
case maps:get(enable, SSL) of
|
||||||
[{ssl, true},
|
true ->
|
||||||
{ssl_options, emqx_tls_lib:to_client_opts(SSL)}];
|
[
|
||||||
false -> [{ssl, false}]
|
{ssl, true},
|
||||||
end ++ [{sentinel, maps:get(sentinel, Config, undefined)}],
|
{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),
|
PoolName = emqx_plugin_libs_pool:pool_name(InstId),
|
||||||
case Type of
|
case Type of
|
||||||
cluster ->
|
cluster ->
|
||||||
case eredis_cluster:start_pool(PoolName, Opts ++ [{options, Options}]) of
|
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}
|
{error, Reason} -> {error, Reason}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ [{options, Options}]) of
|
case
|
||||||
ok -> {ok, #{poolname => PoolName, type => Type}};
|
emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts ++ [{options, Options}])
|
||||||
|
of
|
||||||
|
ok -> {ok, #{poolname => PoolName, type => Type}};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_stop(InstId, #{poolname := PoolName, type := Type}) ->
|
on_stop(InstId, #{poolname := PoolName, type := Type}) ->
|
||||||
?SLOG(info, #{msg => "stopping_redis_connector",
|
?SLOG(info, #{
|
||||||
connector => InstId}),
|
msg => "stopping_redis_connector",
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
case Type of
|
case Type of
|
||||||
cluster -> eredis_cluster:stop_pool(PoolName);
|
cluster -> eredis_cluster:stop_pool(PoolName);
|
||||||
_ -> emqx_plugin_libs_pool:stop_pool(PoolName)
|
_ -> emqx_plugin_libs_pool:stop_pool(PoolName)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) ->
|
on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) ->
|
||||||
?TRACE("QUERY", "redis_connector_received",
|
?TRACE(
|
||||||
#{connector => InstId, sql => Command, state => State}),
|
"QUERY",
|
||||||
Result = case Type of
|
"redis_connector_received",
|
||||||
cluster -> eredis_cluster:q(PoolName, Command);
|
#{connector => InstId, sql => Command, state => State}
|
||||||
_ -> ecpool:pick_and_do(PoolName, {?MODULE, cmd, [Type, Command]}, no_handover)
|
),
|
||||||
end,
|
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
|
case Result of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "redis_connector_do_cmd_query_failed",
|
?SLOG(error, #{
|
||||||
connector => InstId, sql => Command, reason => Reason}),
|
msg => "redis_connector_do_cmd_query_failed",
|
||||||
|
connector => InstId,
|
||||||
|
sql => Command,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
emqx_resource:query_failed(AfterCommand);
|
emqx_resource:query_failed(AfterCommand);
|
||||||
_ ->
|
_ ->
|
||||||
emqx_resource:query_success(AfterCommand)
|
emqx_resource:query_success(AfterCommand)
|
||||||
|
|
@ -161,14 +198,19 @@ on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := T
|
||||||
Result.
|
Result.
|
||||||
|
|
||||||
extract_eredis_cluster_workers(PoolName) ->
|
extract_eredis_cluster_workers(PoolName) ->
|
||||||
lists:flatten([gen_server:call(PoolPid, get_all_workers) ||
|
lists:flatten([
|
||||||
PoolPid <- eredis_cluster_monitor:get_all_pools(PoolName)]).
|
gen_server:call(PoolPid, get_all_workers)
|
||||||
|
|| PoolPid <- eredis_cluster_monitor:get_all_pools(PoolName)
|
||||||
|
]).
|
||||||
|
|
||||||
eredis_cluster_workers_exist_and_are_connected(Workers) ->
|
eredis_cluster_workers_exist_and_are_connected(Workers) ->
|
||||||
length(Workers) > 0 andalso lists:all(
|
length(Workers) > 0 andalso
|
||||||
fun({_, Pid, _, _}) ->
|
lists:all(
|
||||||
eredis_cluster_pool_worker:is_connected(Pid) =:= true
|
fun({_, Pid, _, _}) ->
|
||||||
end, Workers).
|
eredis_cluster_pool_worker:is_connected(Pid) =:= true
|
||||||
|
end,
|
||||||
|
Workers
|
||||||
|
).
|
||||||
|
|
||||||
on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) ->
|
on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) ->
|
||||||
case eredis_cluster:pool_exists(PoolName) of
|
case eredis_cluster:pool_exists(PoolName) of
|
||||||
|
|
@ -178,12 +220,9 @@ on_health_check(_InstId, #{type := cluster, poolname := PoolName} = State) ->
|
||||||
true -> {ok, State};
|
true -> {ok, State};
|
||||||
false -> {error, health_check_failed, State}
|
false -> {error, health_check_failed, State}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
false ->
|
false ->
|
||||||
{error, health_check_failed, State}
|
{error, health_check_failed, State}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
on_health_check(_InstId, #{poolname := PoolName} = State) ->
|
on_health_check(_InstId, #{poolname := PoolName} = State) ->
|
||||||
emqx_plugin_libs_pool:health_check(PoolName, fun ?MODULE:do_health_check/1, 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).
|
eredis:start_link(Opts).
|
||||||
|
|
||||||
redis_fields() ->
|
redis_fields() ->
|
||||||
[ {pool_size, fun emqx_connector_schema_lib:pool_size/1}
|
[
|
||||||
, {password, fun emqx_connector_schema_lib:password/1}
|
{pool_size, fun emqx_connector_schema_lib:pool_size/1},
|
||||||
, {database, #{type => integer(),
|
{password, fun emqx_connector_schema_lib:password/1},
|
||||||
default => 0,
|
{database, #{
|
||||||
required => true,
|
type => integer(),
|
||||||
desc => ?DESC("database")
|
default => 0,
|
||||||
}}
|
required => true,
|
||||||
, {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
|
desc => ?DESC("database")
|
||||||
|
}},
|
||||||
|
{auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
-spec to_server_raw(string())
|
-spec to_server_raw(string()) ->
|
||||||
-> {string(), pos_integer()}.
|
{string(), pos_integer()}.
|
||||||
to_server_raw(Server) ->
|
to_server_raw(Server) ->
|
||||||
emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS).
|
emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS).
|
||||||
|
|
||||||
-spec to_servers_raw(string())
|
-spec to_servers_raw(string()) ->
|
||||||
-> [{string(), pos_integer()}].
|
[{string(), pos_integer()}].
|
||||||
to_servers_raw(Servers) ->
|
to_servers_raw(Servers) ->
|
||||||
lists:map( fun(Server) ->
|
lists:map(
|
||||||
emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS)
|
fun(Server) ->
|
||||||
end
|
emqx_connector_schema_lib:parse_server(Server, ?REDIS_HOST_OPTIONS)
|
||||||
, string:tokens(str(Servers), ", ")).
|
end,
|
||||||
|
string:tokens(str(Servers), ", ")
|
||||||
|
).
|
||||||
|
|
||||||
str(A) when is_atom(A) ->
|
str(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@
|
||||||
|
|
||||||
-export([namespace/0, roots/0, fields/1, desc/1]).
|
-export([namespace/0, roots/0, fields/1, desc/1]).
|
||||||
|
|
||||||
-export([ get_response/0
|
-export([
|
||||||
, put_request/0
|
get_response/0,
|
||||||
, post_request/0
|
put_request/0,
|
||||||
]).
|
post_request/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% the config for http bridges do not need connectors
|
%% the config for http bridges do not need connectors
|
||||||
-define(CONN_TYPES, [mqtt]).
|
-define(CONN_TYPES, [mqtt]).
|
||||||
|
|
@ -55,18 +56,25 @@ namespace() -> connector.
|
||||||
|
|
||||||
roots() -> ["connectors"].
|
roots() -> ["connectors"].
|
||||||
|
|
||||||
fields(connectors) -> fields("connectors");
|
fields(connectors) ->
|
||||||
|
fields("connectors");
|
||||||
fields("connectors") ->
|
fields("connectors") ->
|
||||||
[ {mqtt,
|
[
|
||||||
mk(hoconsc:map(name,
|
{mqtt,
|
||||||
hoconsc:union([ ref(emqx_connector_mqtt_schema, "connector")
|
mk(
|
||||||
])),
|
hoconsc:map(
|
||||||
#{ desc => ?DESC("mqtt")
|
name,
|
||||||
})}
|
hoconsc:union([ref(emqx_connector_mqtt_schema, "connector")])
|
||||||
|
),
|
||||||
|
#{desc => ?DESC("mqtt")}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc(Record) when Record =:= connectors;
|
desc(Record) when
|
||||||
Record =:= "connectors" -> ?DESC("desc_connector");
|
Record =:= connectors;
|
||||||
|
Record =:= "connectors"
|
||||||
|
->
|
||||||
|
?DESC("desc_connector");
|
||||||
desc(_) ->
|
desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,32 +19,36 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
-export([ relational_db_fields/0
|
-export([
|
||||||
, ssl_fields/0
|
relational_db_fields/0,
|
||||||
, prepare_statement_fields/0
|
ssl_fields/0,
|
||||||
]).
|
prepare_statement_fields/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ ip_port_to_string/1
|
-export([
|
||||||
, parse_server/2
|
ip_port_to_string/1,
|
||||||
]).
|
parse_server/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ pool_size/1
|
-export([
|
||||||
, database/1
|
pool_size/1,
|
||||||
, username/1
|
database/1,
|
||||||
, password/1
|
username/1,
|
||||||
, auto_reconnect/1
|
password/1,
|
||||||
]).
|
auto_reconnect/1
|
||||||
|
]).
|
||||||
|
|
||||||
-type database() :: binary().
|
-type database() :: binary().
|
||||||
-type pool_size() :: pos_integer().
|
-type pool_size() :: pos_integer().
|
||||||
-type username() :: binary().
|
-type username() :: binary().
|
||||||
-type password() :: binary().
|
-type password() :: binary().
|
||||||
|
|
||||||
-reflect_type([ database/0
|
-reflect_type([
|
||||||
, pool_size/0
|
database/0,
|
||||||
, username/0
|
pool_size/0,
|
||||||
, password/0
|
username/0,
|
||||||
]).
|
password/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1]).
|
||||||
|
|
||||||
|
|
@ -53,24 +57,25 @@ roots() -> [].
|
||||||
fields(_) -> [].
|
fields(_) -> [].
|
||||||
|
|
||||||
ssl_fields() ->
|
ssl_fields() ->
|
||||||
[ {ssl, #{type => hoconsc:ref(emqx_schema, "ssl_client_opts"),
|
[
|
||||||
default => #{<<"enable">> => false},
|
{ssl, #{
|
||||||
desc => ?DESC("ssl")
|
type => hoconsc:ref(emqx_schema, "ssl_client_opts"),
|
||||||
}
|
default => #{<<"enable">> => false},
|
||||||
}
|
desc => ?DESC("ssl")
|
||||||
|
}}
|
||||||
].
|
].
|
||||||
|
|
||||||
relational_db_fields() ->
|
relational_db_fields() ->
|
||||||
[ {database, fun database/1}
|
[
|
||||||
, {pool_size, fun pool_size/1}
|
{database, fun database/1},
|
||||||
, {username, fun username/1}
|
{pool_size, fun pool_size/1},
|
||||||
, {password, fun password/1}
|
{username, fun username/1},
|
||||||
, {auto_reconnect, fun auto_reconnect/1}
|
{password, fun password/1},
|
||||||
|
{auto_reconnect, fun auto_reconnect/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
prepare_statement_fields() ->
|
prepare_statement_fields() ->
|
||||||
[ {prepare_statement, fun prepare_statement/1}
|
[{prepare_statement, fun prepare_statement/1}].
|
||||||
].
|
|
||||||
|
|
||||||
prepare_statement(type) -> map();
|
prepare_statement(type) -> map();
|
||||||
prepare_statement(desc) -> ?DESC("prepare_statement");
|
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
|
try string:tokens(str(Str), ": ") of
|
||||||
[Ip, Port] ->
|
[Ip, Port] ->
|
||||||
case parse_ip(Ip) of
|
case parse_ip(Ip) of
|
||||||
{ok, R} -> {R, list_to_integer(Port)}
|
{ok, R} -> {R, list_to_integer(Port)}
|
||||||
end;
|
end;
|
||||||
[Ip] ->
|
[Ip] ->
|
||||||
case parse_ip(Ip) of
|
case parse_ip(Ip) of
|
||||||
{ok, R} -> {R, DefaultPort}
|
{ok, R} -> {R, DefaultPort}
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
?THROW_ERROR("Bad server schema.")
|
?THROW_ERROR("Bad server schema.")
|
||||||
catch
|
catch
|
||||||
error : Reason ->
|
error:Reason ->
|
||||||
?THROW_ERROR(Reason)
|
?THROW_ERROR(Reason)
|
||||||
end;
|
end;
|
||||||
parse_server(Str, #{host_type := hostname, default_port := DefaultPort}) ->
|
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.")
|
?THROW_ERROR("Bad server schema.")
|
||||||
catch
|
catch
|
||||||
error : Reason ->
|
error:Reason ->
|
||||||
?THROW_ERROR(Reason)
|
?THROW_ERROR(Reason)
|
||||||
end;
|
end;
|
||||||
parse_server(_, _) ->
|
parse_server(_, _) ->
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%%
|
%%
|
||||||
|
|
@ -17,9 +16,10 @@
|
||||||
|
|
||||||
-module(emqx_connector_ssl).
|
-module(emqx_connector_ssl).
|
||||||
|
|
||||||
-export([ convert_certs/2
|
-export([
|
||||||
, clear_certs/2
|
convert_certs/2,
|
||||||
]).
|
clear_certs/2
|
||||||
|
]).
|
||||||
|
|
||||||
convert_certs(RltvDir, NewConfig) ->
|
convert_certs(RltvDir, NewConfig) ->
|
||||||
NewSSL = drop_invalid_certs(maps:get(<<"ssl">>, NewConfig, undefined)),
|
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(undefined) -> undefined;
|
||||||
drop_invalid_certs(SSL) -> emqx_tls_lib:drop_invalid_certs(SSL).
|
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) ->
|
map_get_oneof([Key | Keys], Map, Default) ->
|
||||||
case maps:find(Key, Map) of
|
case maps:find(Key, Map) of
|
||||||
error ->
|
error ->
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,24 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
SupFlags = #{strategy => one_for_all,
|
SupFlags = #{
|
||||||
intensity => 5,
|
strategy => one_for_all,
|
||||||
period => 20},
|
intensity => 5,
|
||||||
|
period => 20
|
||||||
|
},
|
||||||
ChildSpecs = [
|
ChildSpecs = [
|
||||||
child_spec(emqx_connector_mqtt)
|
child_spec(emqx_connector_mqtt)
|
||||||
],
|
],
|
||||||
{ok, {SupFlags, ChildSpecs}}.
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
child_spec(Mod) ->
|
child_spec(Mod) ->
|
||||||
#{id => Mod,
|
#{
|
||||||
start => {Mod, start_link, []},
|
id => Mod,
|
||||||
restart => permanent,
|
start => {Mod, start_link, []},
|
||||||
shutdown => 3000,
|
restart => permanent,
|
||||||
type => supervisor,
|
shutdown => 3000,
|
||||||
modules => [Mod]}.
|
type => supervisor,
|
||||||
|
modules => [Mod]
|
||||||
|
}.
|
||||||
|
|
||||||
%% internal functions
|
%% internal functions
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,24 @@
|
||||||
|
|
||||||
-module(emqx_connector_mqtt_mod).
|
-module(emqx_connector_mqtt_mod).
|
||||||
|
|
||||||
-export([ start/1
|
-export([
|
||||||
, send/2
|
start/1,
|
||||||
, stop/1
|
send/2,
|
||||||
, ping/1
|
stop/1,
|
||||||
]).
|
ping/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ ensure_subscribed/3
|
-export([
|
||||||
, ensure_unsubscribed/2
|
ensure_subscribed/3,
|
||||||
]).
|
ensure_unsubscribed/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% callbacks for emqtt
|
%% callbacks for emqtt
|
||||||
-export([ handle_puback/2
|
-export([
|
||||||
, handle_publish/3
|
handle_puback/2,
|
||||||
, handle_disconnected/2
|
handle_publish/3,
|
||||||
]).
|
handle_disconnected/2
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
@ -69,7 +72,7 @@ start(Config) ->
|
||||||
ok = sub_remote_topics(Pid, Subscriptions),
|
ok = sub_remote_topics(Pid, Subscriptions),
|
||||||
{ok, #{client_pid => Pid, subscriptions => Subscriptions}}
|
{ok, #{client_pid => Pid, subscriptions => Subscriptions}}
|
||||||
catch
|
catch
|
||||||
throw : Reason ->
|
throw:Reason ->
|
||||||
ok = stop(#{client_pid => Pid}),
|
ok = stop(#{client_pid => Pid}),
|
||||||
{error, error_reason(Reason, ServerStr)}
|
{error, error_reason(Reason, ServerStr)}
|
||||||
end;
|
end;
|
||||||
|
|
@ -90,13 +93,14 @@ stop(#{client_pid := Pid}) ->
|
||||||
|
|
||||||
ping(undefined) ->
|
ping(undefined) ->
|
||||||
pang;
|
pang;
|
||||||
|
|
||||||
ping(#{client_pid := Pid}) ->
|
ping(#{client_pid := Pid}) ->
|
||||||
emqtt:ping(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
|
case emqtt:subscribe(Pid, Topic, QoS) of
|
||||||
{ok, _, _} -> Conn#{subscriptions => [{Topic, QoS}|Subs]};
|
{ok, _, _} -> Conn#{subscriptions => [{Topic, QoS} | Subs]};
|
||||||
Error -> {error, Error}
|
Error -> {error, Error}
|
||||||
end;
|
end;
|
||||||
ensure_subscribed(_Conn, _Topic, _QoS) ->
|
ensure_subscribed(_Conn, _Topic, _QoS) ->
|
||||||
|
|
@ -120,15 +124,14 @@ safe_stop(Pid, StopF, Timeout) ->
|
||||||
try
|
try
|
||||||
StopF()
|
StopF()
|
||||||
catch
|
catch
|
||||||
_ : _ ->
|
_:_ ->
|
||||||
ok
|
ok
|
||||||
end,
|
end,
|
||||||
receive
|
receive
|
||||||
{'DOWN', MRef, _, _, _} ->
|
{'DOWN', MRef, _, _, _} ->
|
||||||
ok
|
ok
|
||||||
after
|
after Timeout ->
|
||||||
Timeout ->
|
exit(Pid, kill)
|
||||||
exit(Pid, kill)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
send(Conn, Msgs) ->
|
send(Conn, Msgs) ->
|
||||||
|
|
@ -157,26 +160,38 @@ send(#{client_pid := ClientPid} = Conn, [Msg | Rest], PktIds) ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle_puback(#{packet_id := PktId, reason_code := RC}, Parent)
|
handle_puback(#{packet_id := PktId, reason_code := RC}, Parent) when
|
||||||
when RC =:= ?RC_SUCCESS;
|
RC =:= ?RC_SUCCESS;
|
||||||
RC =:= ?RC_NO_MATCHING_SUBSCRIBERS ->
|
RC =:= ?RC_NO_MATCHING_SUBSCRIBERS
|
||||||
Parent ! {batch_ack, PktId}, ok;
|
->
|
||||||
|
Parent ! {batch_ack, PktId},
|
||||||
|
ok;
|
||||||
handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
|
handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
|
||||||
?SLOG(warning, #{msg => "publish_to_remote_node_falied",
|
?SLOG(warning, #{
|
||||||
packet_id => PktId, reason_code => RC}).
|
msg => "publish_to_remote_node_falied",
|
||||||
|
packet_id => PktId,
|
||||||
|
reason_code => RC
|
||||||
|
}).
|
||||||
|
|
||||||
handle_publish(Msg, undefined, _Opts) ->
|
handle_publish(Msg, undefined, _Opts) ->
|
||||||
?SLOG(error, #{msg => "cannot_publish_to_local_broker_as"
|
?SLOG(error, #{
|
||||||
"_'ingress'_is_not_configured",
|
msg =>
|
||||||
message => Msg});
|
"cannot_publish_to_local_broker_as"
|
||||||
|
"_'ingress'_is_not_configured",
|
||||||
|
message => Msg
|
||||||
|
});
|
||||||
handle_publish(#{properties := Props} = Msg0, Vars, Opts) ->
|
handle_publish(#{properties := Props} = Msg0, Vars, Opts) ->
|
||||||
Msg = format_msg_received(Msg0, Opts),
|
Msg = format_msg_received(Msg0, Opts),
|
||||||
?SLOG(debug, #{msg => "publish_to_local_broker",
|
?SLOG(debug, #{
|
||||||
message => Msg, vars => Vars}),
|
msg => "publish_to_local_broker",
|
||||||
|
message => Msg,
|
||||||
|
vars => Vars
|
||||||
|
}),
|
||||||
case Vars of
|
case Vars of
|
||||||
#{on_message_received := {Mod, Func, Args}} ->
|
#{on_message_received := {Mod, Func, Args}} ->
|
||||||
_ = erlang:apply(Mod, Func, [Msg | Args]);
|
_ = erlang:apply(Mod, Func, [Msg | Args]);
|
||||||
_ -> ok
|
_ ->
|
||||||
|
ok
|
||||||
end,
|
end,
|
||||||
maybe_publish_to_local_broker(Msg, Vars, Props).
|
maybe_publish_to_local_broker(Msg, Vars, Props).
|
||||||
|
|
||||||
|
|
@ -184,12 +199,14 @@ handle_disconnected(Reason, Parent) ->
|
||||||
Parent ! {disconnected, self(), Reason}.
|
Parent ! {disconnected, self(), Reason}.
|
||||||
|
|
||||||
make_hdlr(Parent, Vars, Opts) ->
|
make_hdlr(Parent, Vars, Opts) ->
|
||||||
#{puback => {fun ?MODULE:handle_puback/2, [Parent]},
|
#{
|
||||||
publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]},
|
puback => {fun ?MODULE:handle_puback/2, [Parent]},
|
||||||
disconnected => {fun ?MODULE:handle_disconnected/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}) ->
|
sub_remote_topics(ClientPid, #{remote_topic := FromTopic, remote_qos := QoS}) ->
|
||||||
case emqtt:subscribe(ClientPid, FromTopic, QoS) of
|
case emqtt:subscribe(ClientPid, FromTopic, QoS) of
|
||||||
{ok, _, _} -> ok;
|
{ok, _, _} -> ok;
|
||||||
|
|
@ -199,52 +216,82 @@ sub_remote_topics(ClientPid, #{remote_topic := FromTopic, remote_qos := QoS}) ->
|
||||||
process_config(Config) ->
|
process_config(Config) ->
|
||||||
maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config).
|
maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config).
|
||||||
|
|
||||||
maybe_publish_to_local_broker(#{topic := Topic} = Msg, #{remote_topic := SubTopic} = Vars,
|
maybe_publish_to_local_broker(
|
||||||
Props) ->
|
#{topic := Topic} = Msg,
|
||||||
|
#{remote_topic := SubTopic} = Vars,
|
||||||
|
Props
|
||||||
|
) ->
|
||||||
case maps:get(local_topic, Vars, undefined) of
|
case maps:get(local_topic, Vars, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
ok; %% local topic is not set, discard it
|
%% local topic is not set, discard it
|
||||||
|
ok;
|
||||||
_ ->
|
_ ->
|
||||||
case emqx_topic:match(Topic, SubTopic) of
|
case emqx_topic:match(Topic, SubTopic) of
|
||||||
true ->
|
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;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
?SLOG(warning, #{msg => "discard_message_as_topic_not_matched",
|
?SLOG(warning, #{
|
||||||
message => Msg, subscribed => SubTopic, got_topic => Topic})
|
msg => "discard_message_as_topic_not_matched",
|
||||||
|
message => Msg,
|
||||||
|
subscribed => SubTopic,
|
||||||
|
got_topic => Topic
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format_msg_received(#{dup := Dup, payload := Payload, properties := Props,
|
format_msg_received(
|
||||||
qos := QoS, retain := Retain, topic := Topic}, #{server := Server}) ->
|
#{
|
||||||
#{ id => emqx_guid:to_hexstr(emqx_guid:gen())
|
dup := Dup,
|
||||||
, server => Server
|
payload := Payload,
|
||||||
, payload => Payload
|
properties := Props,
|
||||||
, topic => Topic
|
qos := QoS,
|
||||||
, qos => QoS
|
retain := Retain,
|
||||||
, dup => Dup
|
topic := Topic
|
||||||
, retain => Retain
|
},
|
||||||
, pub_props => printable_maps(Props)
|
#{server := Server}
|
||||||
, message_received_at => erlang:system_time(millisecond)
|
) ->
|
||||||
}.
|
#{
|
||||||
|
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) ->
|
printable_maps(Headers) ->
|
||||||
maps:fold(
|
maps:fold(
|
||||||
fun ('User-Property', V0, AccIn) when is_list(V0) ->
|
fun
|
||||||
|
('User-Property', V0, AccIn) when is_list(V0) ->
|
||||||
AccIn#{
|
AccIn#{
|
||||||
'User-Property' => maps:from_list(V0),
|
'User-Property' => maps:from_list(V0),
|
||||||
'User-Property-Pairs' => [#{
|
'User-Property-Pairs' => [
|
||||||
key => Key,
|
#{
|
||||||
value => Value
|
key => Key,
|
||||||
} || {Key, Value} <- V0]
|
value => Value
|
||||||
|
}
|
||||||
|
|| {Key, Value} <- V0
|
||||||
|
]
|
||||||
};
|
};
|
||||||
(K, V0, AccIn) -> AccIn#{K => V0}
|
(K, V0, AccIn) ->
|
||||||
end, #{}, Headers).
|
AccIn#{K => V0}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Headers
|
||||||
|
).
|
||||||
|
|
||||||
ip_port_to_server_str(Host, Port) ->
|
ip_port_to_server_str(Host, Port) ->
|
||||||
HostStr = case inet:ntoa(Host) of
|
HostStr =
|
||||||
{error, einval} -> Host;
|
case inet:ntoa(Host) of
|
||||||
IPStr -> IPStr
|
{error, einval} -> Host;
|
||||||
end,
|
IPStr -> IPStr
|
||||||
|
end,
|
||||||
list_to_binary(io_lib:format("~s:~w", [HostStr, Port])).
|
list_to_binary(io_lib:format("~s:~w", [HostStr, Port])).
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
-module(emqx_connector_mqtt_msg).
|
-module(emqx_connector_mqtt_msg).
|
||||||
|
|
||||||
-export([ to_binary/1
|
-export([
|
||||||
, from_binary/1
|
to_binary/1,
|
||||||
, make_pub_vars/2
|
from_binary/1,
|
||||||
, to_remote_msg/2
|
make_pub_vars/2,
|
||||||
, to_broker_msg/3
|
to_remote_msg/2,
|
||||||
, estimate_size/1
|
to_broker_msg/3,
|
||||||
]).
|
estimate_size/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ replace_vars_in_str/2
|
-export([
|
||||||
, replace_simple_var/2
|
replace_vars_in_str/2,
|
||||||
]).
|
replace_simple_var/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export_type([msg/0]).
|
-export_type([msg/0]).
|
||||||
|
|
||||||
|
|
@ -34,7 +36,6 @@
|
||||||
|
|
||||||
-include_lib("emqtt/include/emqtt.hrl").
|
-include_lib("emqtt/include/emqtt.hrl").
|
||||||
|
|
||||||
|
|
||||||
-type msg() :: emqx_types:message().
|
-type msg() :: emqx_types:message().
|
||||||
-type exp_msg() :: emqx_types:message() | #mqtt_msg{}.
|
-type exp_msg() :: emqx_types:message() | #mqtt_msg{}.
|
||||||
|
|
||||||
|
|
@ -46,7 +47,8 @@
|
||||||
payload := binary()
|
payload := binary()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
make_pub_vars(_, undefined) -> undefined;
|
make_pub_vars(_, undefined) ->
|
||||||
|
undefined;
|
||||||
make_pub_vars(Mountpoint, Conf) when is_map(Conf) ->
|
make_pub_vars(Mountpoint, Conf) when is_map(Conf) ->
|
||||||
Conf#{mountpoint => Mountpoint}.
|
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
|
%% Shame that we have to know the callback module here
|
||||||
%% would be great if we can get rid of #mqtt_msg{} record
|
%% would be great if we can get rid of #mqtt_msg{} record
|
||||||
%% and use #message{} in all places.
|
%% and use #message{} in all places.
|
||||||
-spec to_remote_msg(msg() | map(), variables())
|
-spec to_remote_msg(msg() | map(), variables()) ->
|
||||||
-> exp_msg().
|
exp_msg().
|
||||||
to_remote_msg(#message{flags = Flags0} = Msg, Vars) ->
|
to_remote_msg(#message{flags = Flags0} = Msg, Vars) ->
|
||||||
Retain0 = maps:get(retain, Flags0, false),
|
Retain0 = maps:get(retain, Flags0, false),
|
||||||
MapMsg = maps:put(retain, Retain0, emqx_rule_events:eventmsg_publish(Msg)),
|
MapMsg = maps:put(retain, Retain0, emqx_rule_events:eventmsg_publish(Msg)),
|
||||||
to_remote_msg(MapMsg, Vars);
|
to_remote_msg(MapMsg, Vars);
|
||||||
to_remote_msg(MapMsg, #{remote_topic := TopicToken, payload := PayloadToken,
|
to_remote_msg(MapMsg, #{
|
||||||
remote_qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) ->
|
remote_topic := TopicToken,
|
||||||
|
payload := PayloadToken,
|
||||||
|
remote_qos := QoSToken,
|
||||||
|
retain := RetainToken,
|
||||||
|
mountpoint := Mountpoint
|
||||||
|
}) when is_map(MapMsg) ->
|
||||||
Topic = replace_vars_in_str(TopicToken, MapMsg),
|
Topic = replace_vars_in_str(TopicToken, MapMsg),
|
||||||
Payload = process_payload(PayloadToken, MapMsg),
|
Payload = process_payload(PayloadToken, MapMsg),
|
||||||
QoS = replace_simple_var(QoSToken, MapMsg),
|
QoS = replace_simple_var(QoSToken, MapMsg),
|
||||||
Retain = replace_simple_var(RetainToken, MapMsg),
|
Retain = replace_simple_var(RetainToken, MapMsg),
|
||||||
#mqtt_msg{qos = QoS,
|
#mqtt_msg{
|
||||||
retain = Retain,
|
qos = QoS,
|
||||||
topic = topic(Mountpoint, Topic),
|
retain = Retain,
|
||||||
props = #{},
|
topic = topic(Mountpoint, Topic),
|
||||||
payload = Payload};
|
props = #{},
|
||||||
|
payload = Payload
|
||||||
|
};
|
||||||
to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) ->
|
to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) ->
|
||||||
Msg#message{topic = topic(Mountpoint, Topic)}.
|
Msg#message{topic = topic(Mountpoint, Topic)}.
|
||||||
|
|
||||||
%% published from remote node over a MQTT connection
|
%% published from remote node over a MQTT connection
|
||||||
to_broker_msg(#{dup := Dup} = MapMsg,
|
to_broker_msg(
|
||||||
#{local_topic := TopicToken, payload := PayloadToken,
|
#{dup := Dup} = MapMsg,
|
||||||
local_qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}, Props) ->
|
#{
|
||||||
|
local_topic := TopicToken,
|
||||||
|
payload := PayloadToken,
|
||||||
|
local_qos := QoSToken,
|
||||||
|
retain := RetainToken,
|
||||||
|
mountpoint := Mountpoint
|
||||||
|
},
|
||||||
|
Props
|
||||||
|
) ->
|
||||||
Topic = replace_vars_in_str(TopicToken, MapMsg),
|
Topic = replace_vars_in_str(TopicToken, MapMsg),
|
||||||
Payload = process_payload(PayloadToken, MapMsg),
|
Payload = process_payload(PayloadToken, MapMsg),
|
||||||
QoS = replace_simple_var(QoSToken, MapMsg),
|
QoS = replace_simple_var(QoSToken, MapMsg),
|
||||||
Retain = replace_simple_var(RetainToken, MapMsg),
|
Retain = replace_simple_var(RetainToken, MapMsg),
|
||||||
set_headers(Props,
|
set_headers(
|
||||||
emqx_message:set_flags(#{dup => Dup, retain => Retain},
|
Props,
|
||||||
emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload))).
|
emqx_message:set_flags(
|
||||||
|
#{dup => Dup, retain => Retain},
|
||||||
|
emqx_message:make(bridge, QoS, topic(Mountpoint, Topic), Payload)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
process_payload([], Msg) ->
|
process_payload([], Msg) ->
|
||||||
emqx_json:encode(Msg);
|
emqx_json:encode(Msg);
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,17 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, desc/1
|
fields/1,
|
||||||
]).
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ ingress_desc/0
|
-export([
|
||||||
, egress_desc/0
|
ingress_desc/0,
|
||||||
]).
|
egress_desc/0
|
||||||
|
]).
|
||||||
|
|
||||||
-import(emqx_schema, [mk_duration/2]).
|
-import(emqx_schema, [mk_duration/2]).
|
||||||
|
|
||||||
|
|
@ -40,146 +42,210 @@ roots() ->
|
||||||
|
|
||||||
fields("config") ->
|
fields("config") ->
|
||||||
fields("connector") ++
|
fields("connector") ++
|
||||||
topic_mappings();
|
topic_mappings();
|
||||||
|
|
||||||
fields("connector") ->
|
fields("connector") ->
|
||||||
[ {mode,
|
[
|
||||||
sc(hoconsc:enum([cluster_shareload]),
|
{mode,
|
||||||
#{ default => cluster_shareload
|
sc(
|
||||||
, desc => ?DESC("mode")
|
hoconsc:enum([cluster_shareload]),
|
||||||
})}
|
#{
|
||||||
, {server,
|
default => cluster_shareload,
|
||||||
sc(emqx_schema:ip_port(),
|
desc => ?DESC("mode")
|
||||||
#{ required => true
|
}
|
||||||
, desc => ?DESC("server")
|
)},
|
||||||
})}
|
{server,
|
||||||
, {reconnect_interval, mk_duration(
|
sc(
|
||||||
"Reconnect interval. Delay for the MQTT bridge to retry establishing the connection "
|
emqx_schema:ip_port(),
|
||||||
"in case of transportation failure.",
|
#{
|
||||||
#{default => "15s"})}
|
required => true,
|
||||||
, {proto_ver,
|
desc => ?DESC("server")
|
||||||
sc(hoconsc:enum([v3, v4, v5]),
|
}
|
||||||
#{ default => v4
|
)},
|
||||||
, desc => ?DESC("proto_ver")
|
{reconnect_interval,
|
||||||
})}
|
mk_duration(
|
||||||
, {username,
|
"Reconnect interval. Delay for the MQTT bridge to retry establishing the connection "
|
||||||
sc(binary(),
|
"in case of transportation failure.",
|
||||||
#{ default => "emqx"
|
#{default => "15s"}
|
||||||
, desc => ?DESC("username")
|
)},
|
||||||
})}
|
{proto_ver,
|
||||||
, {password,
|
sc(
|
||||||
sc(binary(),
|
hoconsc:enum([v3, v4, v5]),
|
||||||
#{ default => "emqx"
|
#{
|
||||||
, desc => ?DESC("password")
|
default => v4,
|
||||||
})}
|
desc => ?DESC("proto_ver")
|
||||||
, {clean_start,
|
}
|
||||||
sc(boolean(),
|
)},
|
||||||
#{ default => true
|
{username,
|
||||||
, desc => ?DESC("clean_start")
|
sc(
|
||||||
})}
|
binary(),
|
||||||
, {keepalive, mk_duration("MQTT Keepalive.", #{default => "300s"})}
|
#{
|
||||||
, {retry_interval, mk_duration(
|
default => "emqx",
|
||||||
"Message retry interval. Delay for the MQTT bridge to retry sending the QoS1/QoS2 "
|
desc => ?DESC("username")
|
||||||
"messages in case of ACK not received.",
|
}
|
||||||
#{default => "15s"})}
|
)},
|
||||||
, {max_inflight,
|
{password,
|
||||||
sc(non_neg_integer(),
|
sc(
|
||||||
#{ default => 32
|
binary(),
|
||||||
, desc => ?DESC("max_inflight")
|
#{
|
||||||
})}
|
default => "emqx",
|
||||||
, {replayq,
|
desc => ?DESC("password")
|
||||||
sc(ref("replayq"), #{})}
|
}
|
||||||
|
)},
|
||||||
|
{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();
|
] ++ emqx_connector_schema_lib:ssl_fields();
|
||||||
|
|
||||||
fields("ingress") ->
|
fields("ingress") ->
|
||||||
%% the message maybe subscribed by rules, in this case 'local_topic' is not necessary
|
%% the message maybe subscribed by rules, in this case 'local_topic' is not necessary
|
||||||
[ {remote_topic,
|
[
|
||||||
sc(binary(),
|
{remote_topic,
|
||||||
#{ required => true
|
sc(
|
||||||
, validator => fun emqx_schema:non_empty_string/1
|
binary(),
|
||||||
, desc => ?DESC("ingress_remote_topic")
|
#{
|
||||||
})}
|
required => true,
|
||||||
, {remote_qos,
|
validator => fun emqx_schema:non_empty_string/1,
|
||||||
sc(qos(),
|
desc => ?DESC("ingress_remote_topic")
|
||||||
#{ default => 1
|
}
|
||||||
, desc => ?DESC("ingress_remote_qos")
|
)},
|
||||||
})}
|
{remote_qos,
|
||||||
, {local_topic,
|
sc(
|
||||||
sc(binary(),
|
qos(),
|
||||||
#{ validator => fun emqx_schema:non_empty_string/1
|
#{
|
||||||
, desc => ?DESC("ingress_local_topic")
|
default => 1,
|
||||||
})}
|
desc => ?DESC("ingress_remote_qos")
|
||||||
, {local_qos,
|
}
|
||||||
sc(qos(),
|
)},
|
||||||
#{ default => <<"${qos}">>
|
{local_topic,
|
||||||
, desc => ?DESC("ingress_local_qos")
|
sc(
|
||||||
})}
|
binary(),
|
||||||
, {hookpoint,
|
#{
|
||||||
sc(binary(),
|
validator => fun emqx_schema:non_empty_string/1,
|
||||||
#{ desc => ?DESC("ingress_hookpoint")
|
desc => ?DESC("ingress_local_topic")
|
||||||
})}
|
}
|
||||||
|
)},
|
||||||
|
{local_qos,
|
||||||
|
sc(
|
||||||
|
qos(),
|
||||||
|
#{
|
||||||
|
default => <<"${qos}">>,
|
||||||
|
desc => ?DESC("ingress_local_qos")
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{hookpoint,
|
||||||
|
sc(
|
||||||
|
binary(),
|
||||||
|
#{desc => ?DESC("ingress_hookpoint")}
|
||||||
|
)},
|
||||||
|
|
||||||
, {retain,
|
{retain,
|
||||||
sc(hoconsc:union([boolean(), binary()]),
|
sc(
|
||||||
#{ default => <<"${retain}">>
|
hoconsc:union([boolean(), binary()]),
|
||||||
, desc => ?DESC("retain")
|
#{
|
||||||
})}
|
default => <<"${retain}">>,
|
||||||
|
desc => ?DESC("retain")
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
|
||||||
, {payload,
|
{payload,
|
||||||
sc(binary(),
|
sc(
|
||||||
#{ default => <<"${payload}">>
|
binary(),
|
||||||
, desc => ?DESC("payload")
|
#{
|
||||||
})}
|
default => <<"${payload}">>,
|
||||||
|
desc => ?DESC("payload")
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
fields("egress") ->
|
fields("egress") ->
|
||||||
%% the message maybe sent from rules, in this case 'local_topic' is not necessary
|
%% the message maybe sent from rules, in this case 'local_topic' is not necessary
|
||||||
[ {local_topic,
|
[
|
||||||
sc(binary(),
|
{local_topic,
|
||||||
#{ desc => ?DESC("egress_local_topic")
|
sc(
|
||||||
, validator => fun emqx_schema:non_empty_string/1
|
binary(),
|
||||||
})}
|
#{
|
||||||
, {remote_topic,
|
desc => ?DESC("egress_local_topic"),
|
||||||
sc(binary(),
|
validator => fun emqx_schema:non_empty_string/1
|
||||||
#{ required => true
|
}
|
||||||
, validator => fun emqx_schema:non_empty_string/1
|
)},
|
||||||
, desc => ?DESC("egress_remote_topic")
|
{remote_topic,
|
||||||
})}
|
sc(
|
||||||
, {remote_qos,
|
binary(),
|
||||||
sc(qos(),
|
#{
|
||||||
#{ required => true
|
required => true,
|
||||||
, desc => ?DESC("egress_remote_qos")
|
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,
|
{retain,
|
||||||
sc(hoconsc:union([boolean(), binary()]),
|
sc(
|
||||||
#{ required => true
|
hoconsc:union([boolean(), binary()]),
|
||||||
, desc => ?DESC("retain")
|
#{
|
||||||
})}
|
required => true,
|
||||||
|
desc => ?DESC("retain")
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
|
||||||
, {payload,
|
{payload,
|
||||||
sc(binary(),
|
sc(
|
||||||
#{ required => true
|
binary(),
|
||||||
, desc => ?DESC("payload")
|
#{
|
||||||
})}
|
required => true,
|
||||||
|
desc => ?DESC("payload")
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("replayq") ->
|
fields("replayq") ->
|
||||||
[ {dir,
|
[
|
||||||
sc(hoconsc:union([boolean(), string()]),
|
{dir,
|
||||||
#{ desc => ?DESC("dir")
|
sc(
|
||||||
})}
|
hoconsc:union([boolean(), string()]),
|
||||||
, {seg_bytes,
|
#{desc => ?DESC("dir")}
|
||||||
sc(emqx_schema:bytesize(),
|
)},
|
||||||
#{ default => "100MB"
|
{seg_bytes,
|
||||||
, desc => ?DESC("seg_bytes")
|
sc(
|
||||||
})}
|
emqx_schema:bytesize(),
|
||||||
, {offload,
|
#{
|
||||||
sc(boolean(),
|
default => "100MB",
|
||||||
#{ default => false
|
desc => ?DESC("seg_bytes")
|
||||||
, desc => ?DESC("offload")
|
}
|
||||||
})}
|
)},
|
||||||
|
{offload,
|
||||||
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
desc => ?DESC("offload")
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc("connector") ->
|
desc("connector") ->
|
||||||
|
|
@ -194,34 +260,37 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
topic_mappings() ->
|
topic_mappings() ->
|
||||||
[ {ingress,
|
[
|
||||||
sc(ref("ingress"),
|
{ingress,
|
||||||
#{ default => #{}
|
sc(
|
||||||
})}
|
ref("ingress"),
|
||||||
, {egress,
|
#{default => #{}}
|
||||||
sc(ref("egress"),
|
)},
|
||||||
#{ default => #{}
|
{egress,
|
||||||
})}
|
sc(
|
||||||
|
ref("egress"),
|
||||||
|
#{default => #{}}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
ingress_desc() -> "
|
ingress_desc() ->
|
||||||
The ingress config defines how this bridge receive messages from the remote MQTT broker, and then
|
"\n"
|
||||||
send them to the local broker.</br>
|
"The ingress config defines how this bridge receive messages from the remote MQTT broker, and then\n"
|
||||||
Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain',
|
"send them to the local broker.</br>\n"
|
||||||
'payload'.</br>
|
"Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain',\n"
|
||||||
NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is
|
"'payload'.</br>\n"
|
||||||
configured, then messages got from the remote broker will be sent to both the 'local_topic' and
|
"NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is\n"
|
||||||
the rule.
|
"configured, then messages got from the remote broker will be sent to both the 'local_topic' and\n"
|
||||||
".
|
"the rule.\n".
|
||||||
|
|
||||||
egress_desc() -> "
|
egress_desc() ->
|
||||||
The egress config defines how this bridge forwards messages from the local broker to the remote
|
"\n"
|
||||||
broker.</br>
|
"The egress config defines how this bridge forwards messages from the local broker to the remote\n"
|
||||||
Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.</br>
|
"broker.</br>\n"
|
||||||
NOTE: if this bridge is used as the output of a rule (emqx rule engine), and also local_topic
|
"Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.</br>\n"
|
||||||
is configured, then both the data got from the rule and the MQTT messages that matches
|
"NOTE: if this bridge is used as the output of a rule (emqx rule engine), and also local_topic\n"
|
||||||
local_topic will be forwarded.
|
"is configured, then both the data got from the rule and the MQTT messages that matches\n"
|
||||||
".
|
"local_topic will be forwarded.\n".
|
||||||
|
|
||||||
qos() ->
|
qos() ->
|
||||||
hoconsc:union([emqx_schema:qos(), binary()]).
|
hoconsc:union([emqx_schema:qos(), binary()]).
|
||||||
|
|
|
||||||
|
|
@ -66,43 +66,46 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([ start_link/1
|
-export([
|
||||||
, register_metrics/0
|
start_link/1,
|
||||||
, stop/1
|
register_metrics/0,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% gen_statem callbacks
|
%% gen_statem callbacks
|
||||||
-export([ terminate/3
|
-export([
|
||||||
, code_change/4
|
terminate/3,
|
||||||
, init/1
|
code_change/4,
|
||||||
, callback_mode/0
|
init/1,
|
||||||
]).
|
callback_mode/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% state functions
|
%% state functions
|
||||||
-export([ idle/3
|
-export([
|
||||||
, connected/3
|
idle/3,
|
||||||
]).
|
connected/3
|
||||||
|
]).
|
||||||
|
|
||||||
%% management APIs
|
%% management APIs
|
||||||
-export([ ensure_started/1
|
-export([
|
||||||
, ensure_stopped/1
|
ensure_started/1,
|
||||||
, status/1
|
ensure_stopped/1,
|
||||||
, ping/1
|
status/1,
|
||||||
, send_to_remote/2
|
ping/1,
|
||||||
]).
|
send_to_remote/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ get_forwards/1
|
-export([get_forwards/1]).
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ get_subscriptions/1
|
-export([get_subscriptions/1]).
|
||||||
]).
|
|
||||||
|
|
||||||
%% Internal
|
%% Internal
|
||||||
-export([msg_marshaller/1]).
|
-export([msg_marshaller/1]).
|
||||||
|
|
||||||
-export_type([ config/0
|
-export_type([
|
||||||
, ack_ref/0
|
config/0,
|
||||||
]).
|
ack_ref/0
|
||||||
|
]).
|
||||||
|
|
||||||
-type id() :: atom() | string() | pid().
|
-type id() :: atom() | string() | pid().
|
||||||
-type qos() :: emqx_types:qos().
|
-type qos() :: emqx_types:qos().
|
||||||
|
|
@ -113,7 +116,6 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
|
|
||||||
%% same as default in-flight limit for emqtt
|
%% same as default in-flight limit for emqtt
|
||||||
-define(DEFAULT_INFLIGHT_SIZE, 32).
|
-define(DEFAULT_INFLIGHT_SIZE, 32).
|
||||||
-define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)).
|
-define(DEFAULT_RECONNECT_DELAY_MS, timer:seconds(5)).
|
||||||
|
|
@ -188,8 +190,10 @@ callback_mode() -> [state_functions].
|
||||||
|
|
||||||
%% @doc Config should be a map().
|
%% @doc Config should be a map().
|
||||||
init(#{name := Name} = ConnectOpts) ->
|
init(#{name := Name} = ConnectOpts) ->
|
||||||
?SLOG(debug, #{msg => "starting_bridge_worker",
|
?SLOG(debug, #{
|
||||||
name => Name}),
|
msg => "starting_bridge_worker",
|
||||||
|
name => Name
|
||||||
|
}),
|
||||||
erlang:process_flag(trap_exit, true),
|
erlang:process_flag(trap_exit, true),
|
||||||
Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})),
|
Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})),
|
||||||
State = init_state(ConnectOpts),
|
State = init_state(ConnectOpts),
|
||||||
|
|
@ -205,31 +209,44 @@ init_state(Opts) ->
|
||||||
Mountpoint = maps:get(forward_mountpoint, Opts, undefined),
|
Mountpoint = maps:get(forward_mountpoint, Opts, undefined),
|
||||||
MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_INFLIGHT_SIZE),
|
MaxInflightSize = maps:get(max_inflight, Opts, ?DEFAULT_INFLIGHT_SIZE),
|
||||||
Name = maps:get(name, Opts, undefined),
|
Name = maps:get(name, Opts, undefined),
|
||||||
#{start_type => StartType,
|
#{
|
||||||
reconnect_interval => ReconnDelayMs,
|
start_type => StartType,
|
||||||
mountpoint => format_mountpoint(Mountpoint),
|
reconnect_interval => ReconnDelayMs,
|
||||||
inflight => [],
|
mountpoint => format_mountpoint(Mountpoint),
|
||||||
max_inflight => MaxInflightSize,
|
inflight => [],
|
||||||
connection => undefined,
|
max_inflight => MaxInflightSize,
|
||||||
name => Name}.
|
connection => undefined,
|
||||||
|
name => Name
|
||||||
|
}.
|
||||||
|
|
||||||
open_replayq(Name, QCfg) ->
|
open_replayq(Name, QCfg) ->
|
||||||
Dir = maps:get(dir, QCfg, undefined),
|
Dir = maps:get(dir, QCfg, undefined),
|
||||||
SegBytes = maps:get(seg_bytes, QCfg, ?DEFAULT_SEG_BYTES),
|
SegBytes = maps:get(seg_bytes, QCfg, ?DEFAULT_SEG_BYTES),
|
||||||
MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE),
|
MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE),
|
||||||
QueueConfig = case Dir =:= undefined orelse Dir =:= "" of
|
QueueConfig =
|
||||||
true -> #{mem_only => true};
|
case Dir =:= undefined orelse Dir =:= "" of
|
||||||
false -> #{dir => filename:join([Dir, node(), Name]),
|
true ->
|
||||||
seg_bytes => SegBytes, max_total_size => MaxTotalSize}
|
#{mem_only => true};
|
||||||
end,
|
false ->
|
||||||
replayq:open(QueueConfig#{sizer => fun emqx_connector_mqtt_msg:estimate_size/1,
|
#{
|
||||||
marshaller => fun ?MODULE:msg_marshaller/1}).
|
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) ->
|
pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) ->
|
||||||
ConnectOpts#{subscriptions => pre_process_in_out(in, InConf),
|
ConnectOpts#{
|
||||||
forwards => pre_process_in_out(out, OutConf)}.
|
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) ->
|
pre_process_in_out(in, Conf) when is_map(Conf) ->
|
||||||
Conf1 = pre_process_conf(local_topic, Conf),
|
Conf1 = pre_process_conf(local_topic, Conf),
|
||||||
Conf2 = pre_process_conf(local_qos, Conf1),
|
Conf2 = pre_process_conf(local_qos, Conf1),
|
||||||
|
|
@ -245,7 +262,8 @@ pre_process_in_out_common(Conf) ->
|
||||||
|
|
||||||
pre_process_conf(Key, Conf) ->
|
pre_process_conf(Key, Conf) ->
|
||||||
case maps:find(Key, Conf) of
|
case maps:find(Key, Conf) of
|
||||||
error -> Conf;
|
error ->
|
||||||
|
Conf;
|
||||||
{ok, Val} when is_binary(Val) ->
|
{ok, Val} when is_binary(Val) ->
|
||||||
Conf#{Key => emqx_plugin_libs_rule:preproc_tmpl(Val)};
|
Conf#{Key => emqx_plugin_libs_rule:preproc_tmpl(Val)};
|
||||||
{ok, Val} ->
|
{ok, Val} ->
|
||||||
|
|
@ -276,7 +294,6 @@ idle(info, idle, #{start_type := auto} = State) ->
|
||||||
connecting(State);
|
connecting(State);
|
||||||
idle(state_timeout, reconnect, State) ->
|
idle(state_timeout, reconnect, State) ->
|
||||||
connecting(State);
|
connecting(State);
|
||||||
|
|
||||||
idle(Type, Content, State) ->
|
idle(Type, Content, State) ->
|
||||||
common(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) ->
|
connected(internal, maybe_send, State) ->
|
||||||
{_, NewState} = pop_and_send(State),
|
{_, NewState} = pop_and_send(State),
|
||||||
{keep_state, NewState};
|
{keep_state, NewState};
|
||||||
|
connected(
|
||||||
connected(info, {disconnected, Conn, Reason},
|
info,
|
||||||
#{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State) ->
|
{disconnected, Conn, Reason},
|
||||||
|
#{connection := Connection, name := Name, reconnect_interval := ReconnectDelayMs} = State
|
||||||
|
) ->
|
||||||
?tp(info, disconnected, #{name => Name, reason => Reason}),
|
?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 ->
|
true ->
|
||||||
{next_state, idle, State#{connection => undefined}, {state_timeout, ReconnectDelayMs, reconnect}};
|
{next_state, idle, State#{connection => undefined},
|
||||||
|
{state_timeout, ReconnectDelayMs, reconnect}};
|
||||||
false ->
|
false ->
|
||||||
keep_state_and_data
|
keep_state_and_data
|
||||||
end;
|
end;
|
||||||
|
|
@ -317,7 +337,7 @@ connected(Type, Content, State) ->
|
||||||
%% Common handlers
|
%% Common handlers
|
||||||
common(StateName, {call, From}, status, _State) ->
|
common(StateName, {call, From}, status, _State) ->
|
||||||
{keep_state_and_data, [{reply, From, StateName}]};
|
{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),
|
Reply = emqx_connector_mqtt_mod:ping(Conn),
|
||||||
{keep_state_and_data, [{reply, From, Reply}]};
|
{keep_state_and_data, [{reply, From, Reply}]};
|
||||||
common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) ->
|
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]),
|
NewQ = replayq:append(Q, [Msg]),
|
||||||
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
|
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
|
||||||
common(StateName, Type, Content, #{name := Name} = State) ->
|
common(StateName, Type, Content, #{name := Name} = State) ->
|
||||||
?SLOG(notice, #{msg => "bridge_discarded_event",
|
?SLOG(notice, #{
|
||||||
name => Name, type => Type, state_name => StateName,
|
msg => "bridge_discarded_event",
|
||||||
content => Content}),
|
name => Name,
|
||||||
|
type => Type,
|
||||||
|
state_name => StateName,
|
||||||
|
content => Content
|
||||||
|
}),
|
||||||
{keep_state, State}.
|
{keep_state, State}.
|
||||||
|
|
||||||
do_connect(#{connect_opts := ConnectOpts,
|
do_connect(
|
||||||
inflight := Inflight,
|
#{
|
||||||
name := Name} = State) ->
|
connect_opts := ConnectOpts,
|
||||||
|
inflight := Inflight,
|
||||||
|
name := Name
|
||||||
|
} = State
|
||||||
|
) ->
|
||||||
case emqx_connector_mqtt_mod:start(ConnectOpts) of
|
case emqx_connector_mqtt_mod:start(ConnectOpts) of
|
||||||
{ok, Conn} ->
|
{ok, Conn} ->
|
||||||
?tp(info, connected, #{name => Name, inflight => length(Inflight)}),
|
?tp(info, connected, #{name => Name, inflight => length(Inflight)}),
|
||||||
{ok, State#{connection => Conn}};
|
{ok, State#{connection => Conn}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
ConnectOpts1 = obfuscate(ConnectOpts),
|
ConnectOpts1 = obfuscate(ConnectOpts),
|
||||||
?SLOG(error, #{msg => "failed_to_connect",
|
?SLOG(error, #{
|
||||||
config => ConnectOpts1, reason => Reason}),
|
msg => "failed_to_connect",
|
||||||
|
config => ConnectOpts1,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, Reason, State}
|
{error, Reason, State}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Retry all inflight (previously sent but not acked) batches.
|
%% 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) ->
|
retry_inflight(State, [#{q_ack_ref := QAckRef, msg := Msg} | Rest] = OldInf) ->
|
||||||
case do_send(State, QAckRef, Msg) of
|
case do_send(State, QAckRef, Msg) of
|
||||||
{ok, State1} ->
|
{ok, State1} ->
|
||||||
|
|
@ -386,28 +418,49 @@ pop_and_send_loop(#{replayq := Q} = State, N) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) ->
|
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) ->
|
||||||
?SLOG(error, #{msg => "cannot_forward_messages_to_remote_broker"
|
?SLOG(error, #{
|
||||||
"_as_'egress'_is_not_configured",
|
msg =>
|
||||||
messages => Msg});
|
"cannot_forward_messages_to_remote_broker"
|
||||||
do_send(#{inflight := Inflight,
|
"_as_'egress'_is_not_configured",
|
||||||
connection := Connection,
|
messages => Msg
|
||||||
mountpoint := Mountpoint,
|
});
|
||||||
connect_opts := #{forwards := Forwards}} = State, QAckRef, 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),
|
Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards),
|
||||||
ExportMsg = fun(Message) ->
|
ExportMsg = fun(Message) ->
|
||||||
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
|
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
|
||||||
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
|
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
|
||||||
end,
|
end,
|
||||||
?SLOG(debug, #{msg => "publish_to_remote_broker",
|
?SLOG(debug, #{
|
||||||
message => Msg, vars => Vars}),
|
msg => "publish_to_remote_broker",
|
||||||
|
message => Msg,
|
||||||
|
vars => Vars
|
||||||
|
}),
|
||||||
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(Msg)]) of
|
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(Msg)]) of
|
||||||
{ok, Refs} ->
|
{ok, Refs} ->
|
||||||
{ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef,
|
{ok, State#{
|
||||||
send_ack_ref => map_set(Refs),
|
inflight := Inflight ++
|
||||||
msg => Msg}]}};
|
[
|
||||||
|
#{
|
||||||
|
q_ack_ref => QAckRef,
|
||||||
|
send_ack_ref => map_set(Refs),
|
||||||
|
msg => Msg
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(info, #{msg => "mqtt_bridge_produce_failed",
|
?SLOG(info, #{
|
||||||
reason => Reason}),
|
msg => "mqtt_bridge_produce_failed",
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, State}
|
{error, State}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -427,8 +480,10 @@ handle_batch_ack(#{inflight := Inflight0, replayq := Q} = State, Ref) ->
|
||||||
State#{inflight := Inflight}.
|
State#{inflight := Inflight}.
|
||||||
|
|
||||||
do_ack([], Ref) ->
|
do_ack([], Ref) ->
|
||||||
?SLOG(debug, #{msg => "stale_batch_ack_reference",
|
?SLOG(debug, #{
|
||||||
ref => Ref}),
|
msg => "stale_batch_ack_reference",
|
||||||
|
ref => Ref
|
||||||
|
}),
|
||||||
[];
|
[];
|
||||||
do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
|
do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
|
||||||
case maps:is_key(Ref, Refs) of
|
case maps:is_key(Ref, Refs) of
|
||||||
|
|
@ -443,8 +498,16 @@ do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
|
||||||
drop_acked_batches(_Q, []) ->
|
drop_acked_batches(_Q, []) ->
|
||||||
?tp(debug, inflight_drained, #{}),
|
?tp(debug, inflight_drained, #{}),
|
||||||
[];
|
[];
|
||||||
drop_acked_batches(Q, [#{send_ack_ref := Refs,
|
drop_acked_batches(
|
||||||
q_ack_ref := QAckRef} | Rest] = All) ->
|
Q,
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
send_ack_ref := Refs,
|
||||||
|
q_ack_ref := QAckRef
|
||||||
|
}
|
||||||
|
| Rest
|
||||||
|
] = All
|
||||||
|
) ->
|
||||||
case maps:size(Refs) of
|
case maps:size(Refs) of
|
||||||
0 ->
|
0 ->
|
||||||
%% all messages are acked by bridge target
|
%% all messages are acked by bridge target
|
||||||
|
|
@ -475,18 +538,25 @@ format_mountpoint(Prefix) ->
|
||||||
name(Id) -> list_to_atom(str(Id)).
|
name(Id) -> list_to_atom(str(Id)).
|
||||||
|
|
||||||
register_metrics() ->
|
register_metrics() ->
|
||||||
lists:foreach(fun emqx_metrics:ensure/1,
|
lists:foreach(
|
||||||
['bridge.mqtt.message_sent_to_remote',
|
fun emqx_metrics:ensure/1,
|
||||||
'bridge.mqtt.message_received_from_remote'
|
[
|
||||||
]).
|
'bridge.mqtt.message_sent_to_remote',
|
||||||
|
'bridge.mqtt.message_received_from_remote'
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
obfuscate(Map) ->
|
obfuscate(Map) ->
|
||||||
maps:fold(fun(K, V, Acc) ->
|
maps:fold(
|
||||||
case is_sensitive(K) of
|
fun(K, V, Acc) ->
|
||||||
true -> [{K, '***'} | Acc];
|
case is_sensitive(K) of
|
||||||
false -> [{K, V} | Acc]
|
true -> [{K, '***'} | Acc];
|
||||||
end
|
false -> [{K, V} | Acc]
|
||||||
end, [], Map).
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
Map
|
||||||
|
).
|
||||||
|
|
||||||
is_sensitive(password) -> true;
|
is_sensitive(password) -> true;
|
||||||
is_sensitive(_) -> false.
|
is_sensitive(_) -> false.
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,23 @@
|
||||||
-include("emqx_dashboard/include/emqx_dashboard.hrl").
|
-include("emqx_dashboard/include/emqx_dashboard.hrl").
|
||||||
|
|
||||||
%% output functions
|
%% output functions
|
||||||
-export([ inspect/3
|
-export([inspect/3]).
|
||||||
]).
|
|
||||||
|
|
||||||
-define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>).
|
-define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>).
|
||||||
-define(CONNECTR_TYPE, <<"mqtt">>).
|
-define(CONNECTR_TYPE, <<"mqtt">>).
|
||||||
-define(CONNECTR_NAME, <<"test_connector">>).
|
-define(CONNECTR_NAME, <<"test_connector">>).
|
||||||
-define(BRIDGE_NAME_INGRESS, <<"ingress_test_bridge">>).
|
-define(BRIDGE_NAME_INGRESS, <<"ingress_test_bridge">>).
|
||||||
-define(BRIDGE_NAME_EGRESS, <<"egress_test_bridge">>).
|
-define(BRIDGE_NAME_EGRESS, <<"egress_test_bridge">>).
|
||||||
-define(MQTT_CONNECTOR(Username),
|
-define(MQTT_CONNECTOR(Username), #{
|
||||||
#{
|
|
||||||
<<"server">> => <<"127.0.0.1:1883">>,
|
<<"server">> => <<"127.0.0.1:1883">>,
|
||||||
<<"username">> => Username,
|
<<"username">> => Username,
|
||||||
<<"password">> => <<"">>,
|
<<"password">> => <<"">>,
|
||||||
<<"proto_ver">> => <<"v4">>,
|
<<"proto_ver">> => <<"v4">>,
|
||||||
<<"ssl">> => #{<<"enable">> => false}
|
<<"ssl">> => #{<<"enable">> => false}
|
||||||
}).
|
}).
|
||||||
-define(MQTT_CONNECTOR2(Server),
|
-define(MQTT_CONNECTOR2(Server), ?MQTT_CONNECTOR(<<"user1">>)#{<<"server">> => Server}).
|
||||||
?MQTT_CONNECTOR(<<"user1">>)#{<<"server">> => Server}).
|
|
||||||
|
|
||||||
-define(MQTT_BRIDGE_INGRESS(ID),
|
-define(MQTT_BRIDGE_INGRESS(ID), #{
|
||||||
#{
|
|
||||||
<<"connector">> => ID,
|
<<"connector">> => ID,
|
||||||
<<"direction">> => <<"ingress">>,
|
<<"direction">> => <<"ingress">>,
|
||||||
<<"remote_topic">> => <<"remote_topic/#">>,
|
<<"remote_topic">> => <<"remote_topic/#">>,
|
||||||
|
|
@ -57,8 +53,7 @@
|
||||||
<<"retain">> => <<"${retain}">>
|
<<"retain">> => <<"${retain}">>
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(MQTT_BRIDGE_EGRESS(ID),
|
-define(MQTT_BRIDGE_EGRESS(ID), #{
|
||||||
#{
|
|
||||||
<<"connector">> => ID,
|
<<"connector">> => ID,
|
||||||
<<"direction">> => <<"egress">>,
|
<<"direction">> => <<"egress">>,
|
||||||
<<"local_topic">> => <<"local_topic/#">>,
|
<<"local_topic">> => <<"local_topic/#">>,
|
||||||
|
|
@ -68,10 +63,14 @@
|
||||||
<<"retain">> => <<"${retain}">>
|
<<"retain">> => <<"${retain}">>
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(metrics(MATCH, SUCC, FAILED, SPEED, SPEED5M, SPEEDMAX),
|
-define(metrics(MATCH, SUCC, FAILED, SPEED, SPEED5M, SPEEDMAX), #{
|
||||||
#{<<"matched">> := MATCH, <<"success">> := SUCC,
|
<<"matched">> := MATCH,
|
||||||
<<"failed">> := FAILED, <<"rate">> := SPEED,
|
<<"success">> := SUCC,
|
||||||
<<"rate_last5m">> := SPEED5M, <<"rate_max">> := SPEEDMAX}).
|
<<"failed">> := FAILED,
|
||||||
|
<<"rate">> := SPEED,
|
||||||
|
<<"rate_last5m">> := SPEED5M,
|
||||||
|
<<"rate_max">> := SPEEDMAX
|
||||||
|
}).
|
||||||
|
|
||||||
inspect(Selected, _Envs, _Args) ->
|
inspect(Selected, _Envs, _Args) ->
|
||||||
persistent_term:put(?MODULE, #{inspect => Selected}).
|
persistent_term:put(?MODULE, #{inspect => Selected}).
|
||||||
|
|
@ -83,24 +82,37 @@ groups() ->
|
||||||
[].
|
[].
|
||||||
|
|
||||||
suite() ->
|
suite() ->
|
||||||
[{timetrap,{seconds,30}}].
|
[{timetrap, {seconds, 30}}].
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
%% some testcases (may from other app) already get emqx_connector started
|
%% some testcases (may from other app) already get emqx_connector started
|
||||||
_ = application:stop(emqx_resource),
|
_ = application:stop(emqx_resource),
|
||||||
_ = application:stop(emqx_connector),
|
_ = application:stop(emqx_connector),
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_rule_engine, emqx_connector,
|
ok = emqx_common_test_helpers:start_apps(
|
||||||
emqx_bridge, emqx_dashboard], fun set_special_configs/1),
|
[
|
||||||
|
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_connector_schema, <<"connectors: {}">>),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_rule_engine_schema,
|
ok = emqx_common_test_helpers:load_config(
|
||||||
<<"rule_engine {rules {}}">>),
|
emqx_rule_engine_schema,
|
||||||
|
<<"rule_engine {rules {}}">>
|
||||||
|
),
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?BRIDGE_CONF_DEFAULT),
|
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?BRIDGE_CONF_DEFAULT),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
end_per_suite(_Config) ->
|
||||||
emqx_common_test_helpers:stop_apps([emqx_rule_engine, emqx_connector, emqx_bridge,
|
emqx_common_test_helpers:stop_apps([
|
||||||
emqx_dashboard]),
|
emqx_rule_engine,
|
||||||
|
emqx_connector,
|
||||||
|
emqx_bridge,
|
||||||
|
emqx_dashboard
|
||||||
|
]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
|
@ -116,15 +128,24 @@ end_per_testcase(_, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
clear_resources() ->
|
clear_resources() ->
|
||||||
lists:foreach(fun(#{id := Id}) ->
|
lists:foreach(
|
||||||
|
fun(#{id := Id}) ->
|
||||||
ok = emqx_rule_engine:delete_rule(Id)
|
ok = emqx_rule_engine:delete_rule(Id)
|
||||||
end, emqx_rule_engine:get_rules()),
|
end,
|
||||||
lists:foreach(fun(#{type := Type, name := Name}) ->
|
emqx_rule_engine:get_rules()
|
||||||
|
),
|
||||||
|
lists:foreach(
|
||||||
|
fun(#{type := Type, name := Name}) ->
|
||||||
ok = emqx_bridge:remove(Type, Name)
|
ok = emqx_bridge:remove(Type, Name)
|
||||||
end, emqx_bridge:list()),
|
end,
|
||||||
lists:foreach(fun(#{<<"type">> := Type, <<"name">> := Name}) ->
|
emqx_bridge:list()
|
||||||
|
),
|
||||||
|
lists:foreach(
|
||||||
|
fun(#{<<"type">> := Type, <<"name">> := Name}) ->
|
||||||
ok = emqx_connector:delete(Type, Name)
|
ok = emqx_connector:delete(Type, Name)
|
||||||
end, emqx_connector:list_raw()).
|
end,
|
||||||
|
emqx_connector:list_raw()
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Testcases
|
%% Testcases
|
||||||
|
|
@ -137,103 +158,144 @@ t_mqtt_crud_apis(_) ->
|
||||||
%% then we add a mqtt connector, using POST
|
%% then we add a mqtt connector, using POST
|
||||||
%% POST /connectors/ will create a connector
|
%% POST /connectors/ will create a connector
|
||||||
User1 = <<"user1">>,
|
User1 = <<"user1">>,
|
||||||
{ok, 400, <<"{\"code\":\"BAD_REQUEST\",\"message\""
|
{ok, 400, <<
|
||||||
":\"missing some required fields: [name, type]\"}">>}
|
"{\"code\":\"BAD_REQUEST\",\"message\""
|
||||||
= request(post, uri(["connectors"]),
|
":\"missing some required fields: [name, type]\"}"
|
||||||
?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE
|
>>} =
|
||||||
}),
|
request(
|
||||||
{ok, 201, Connector} = request(post, uri(["connectors"]),
|
post,
|
||||||
?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE
|
uri(["connectors"]),
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
?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
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"server">> := <<"127.0.0.1:1883">>
|
<<"name">> := ?CONNECTR_NAME,
|
||||||
, <<"username">> := User1
|
<<"server">> := <<"127.0.0.1:1883">>,
|
||||||
, <<"password">> := <<"">>
|
<<"username">> := User1,
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"password">> := <<"">>,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"proto_ver">> := <<"v4">>,
|
||||||
} = jsx:decode(Connector),
|
<<"ssl">> := #{<<"enable">> := false}
|
||||||
|
} = jsx:decode(Connector),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% update the request-path of the connector
|
%% update the request-path of the connector
|
||||||
User2 = <<"user2">>,
|
User2 = <<"user2">>,
|
||||||
{ok, 200, Connector2} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 200, Connector2} = request(
|
||||||
?MQTT_CONNECTOR(User2)),
|
put,
|
||||||
?assertMatch(#{ <<"type">> := ?CONNECTR_TYPE
|
uri(["connectors", ConnctorID]),
|
||||||
, <<"name">> := ?CONNECTR_NAME
|
?MQTT_CONNECTOR(User2)
|
||||||
, <<"server">> := <<"127.0.0.1:1883">>
|
),
|
||||||
, <<"username">> := User2
|
?assertMatch(
|
||||||
, <<"password">> := <<"">>
|
#{
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"name">> := ?CONNECTR_NAME,
|
||||||
}, jsx:decode(Connector2)),
|
<<"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
|
%% list all connectors again, assert Connector2 is in it
|
||||||
{ok, 200, Connector2Str} = request(get, uri(["connectors"]), []),
|
{ok, 200, Connector2Str} = request(get, uri(["connectors"]), []),
|
||||||
?assertMatch([#{ <<"type">> := ?CONNECTR_TYPE
|
?assertMatch(
|
||||||
, <<"name">> := ?CONNECTR_NAME
|
[
|
||||||
, <<"server">> := <<"127.0.0.1:1883">>
|
#{
|
||||||
, <<"username">> := User2
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"password">> := <<"">>
|
<<"name">> := ?CONNECTR_NAME,
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"server">> := <<"127.0.0.1:1883">>,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"username">> := User2,
|
||||||
}], jsx:decode(Connector2Str)),
|
<<"password">> := <<"">>,
|
||||||
|
<<"proto_ver">> := <<"v4">>,
|
||||||
|
<<"ssl">> := #{<<"enable">> := false}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
jsx:decode(Connector2Str)
|
||||||
|
),
|
||||||
|
|
||||||
%% get the connector by id
|
%% get the connector by id
|
||||||
{ok, 200, Connector3Str} = request(get, uri(["connectors", ConnctorID]), []),
|
{ok, 200, Connector3Str} = request(get, uri(["connectors", ConnctorID]), []),
|
||||||
?assertMatch(#{ <<"type">> := ?CONNECTR_TYPE
|
?assertMatch(
|
||||||
, <<"name">> := ?CONNECTR_NAME
|
#{
|
||||||
, <<"server">> := <<"127.0.0.1:1883">>
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"username">> := User2
|
<<"name">> := ?CONNECTR_NAME,
|
||||||
, <<"password">> := <<"">>
|
<<"server">> := <<"127.0.0.1:1883">>,
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"username">> := User2,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"password">> := <<"">>,
|
||||||
}, jsx:decode(Connector3Str)),
|
<<"proto_ver">> := <<"v4">>,
|
||||||
|
<<"ssl">> := #{<<"enable">> := false}
|
||||||
|
},
|
||||||
|
jsx:decode(Connector3Str)
|
||||||
|
),
|
||||||
|
|
||||||
%% delete the connector
|
%% delete the connector
|
||||||
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
|
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
|
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
|
||||||
|
|
||||||
%% update a deleted connector returns an error
|
%% update a deleted connector returns an error
|
||||||
{ok, 404, ErrMsg2} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 404, ErrMsg2} = request(
|
||||||
?MQTT_CONNECTOR(User2)),
|
put,
|
||||||
|
uri(["connectors", ConnctorID]),
|
||||||
|
?MQTT_CONNECTOR(User2)
|
||||||
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
#{ <<"code">> := _
|
#{
|
||||||
, <<"message">> := <<"connector not found">>
|
<<"code">> := _,
|
||||||
}, jsx:decode(ErrMsg2)),
|
<<"message">> := <<"connector not found">>
|
||||||
|
},
|
||||||
|
jsx:decode(ErrMsg2)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_mqtt_conn_bridge_ingress(_) ->
|
t_mqtt_conn_bridge_ingress(_) ->
|
||||||
%% then we add a mqtt connector, using POST
|
%% then we add a mqtt connector, using POST
|
||||||
User1 = <<"user1">>,
|
User1 = <<"user1">>,
|
||||||
{ok, 201, Connector} = request(post, uri(["connectors"]),
|
{ok, 201, Connector} = request(
|
||||||
?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE
|
post,
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
uri(["connectors"]),
|
||||||
}),
|
?MQTT_CONNECTOR(User1)#{
|
||||||
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
#{ <<"type">> := ?CONNECTR_TYPE
|
#{
|
||||||
, <<"name">> := ?CONNECTR_NAME
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"server">> := <<"127.0.0.1:1883">>
|
<<"name">> := ?CONNECTR_NAME,
|
||||||
, <<"num_of_bridges">> := 0
|
<<"server">> := <<"127.0.0.1:1883">>,
|
||||||
, <<"username">> := User1
|
<<"num_of_bridges">> := 0,
|
||||||
, <<"password">> := <<"">>
|
<<"username">> := User1,
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"password">> := <<"">>,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"proto_ver">> := <<"v4">>,
|
||||||
} = jsx:decode(Connector),
|
<<"ssl">> := #{<<"enable">> := false}
|
||||||
|
} = jsx:decode(Connector),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% ... and a MQTT bridge, using POST
|
%% ... and a MQTT bridge, using POST
|
||||||
%% we bind this bridge to the connector created just now
|
%% we bind this bridge to the connector created just now
|
||||||
timer:sleep(50),
|
timer:sleep(50),
|
||||||
{ok, 201, Bridge} = request(post, uri(["bridges"]),
|
{ok, 201, Bridge} = request(
|
||||||
|
post,
|
||||||
|
uri(["bridges"]),
|
||||||
?MQTT_BRIDGE_INGRESS(ConnctorID)#{
|
?MQTT_BRIDGE_INGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_INGRESS
|
<<"name">> => ?BRIDGE_NAME_INGRESS
|
||||||
}),
|
}
|
||||||
#{ <<"type">> := ?CONNECTR_TYPE
|
),
|
||||||
, <<"name">> := ?BRIDGE_NAME_INGRESS
|
#{
|
||||||
, <<"connector">> := ConnctorID
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
} = jsx:decode(Bridge),
|
<<"name">> := ?BRIDGE_NAME_INGRESS,
|
||||||
|
<<"connector">> := ConnctorID
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
|
BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
|
||||||
wait_for_resource_ready(BridgeIDIngress, 5),
|
wait_for_resource_ready(BridgeIDIngress, 5),
|
||||||
|
|
||||||
|
|
@ -257,12 +319,12 @@ t_mqtt_conn_bridge_ingress(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
%% get the connector by id, verify the num_of_bridges now is 1
|
%% get the connector by id, verify the num_of_bridges now is 1
|
||||||
{ok, 200, Connector1Str} = request(get, uri(["connectors", ConnctorID]), []),
|
{ok, 200, Connector1Str} = request(get, uri(["connectors", ConnctorID]), []),
|
||||||
?assertMatch(#{ <<"num_of_bridges">> := 1
|
?assertMatch(#{<<"num_of_bridges">> := 1}, jsx:decode(Connector1Str)),
|
||||||
}, jsx:decode(Connector1Str)),
|
|
||||||
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
|
||||||
|
|
@ -276,30 +338,39 @@ t_mqtt_conn_bridge_ingress(_) ->
|
||||||
t_mqtt_conn_bridge_egress(_) ->
|
t_mqtt_conn_bridge_egress(_) ->
|
||||||
%% then we add a mqtt connector, using POST
|
%% then we add a mqtt connector, using POST
|
||||||
User1 = <<"user1">>,
|
User1 = <<"user1">>,
|
||||||
{ok, 201, Connector} = request(post, uri(["connectors"]),
|
{ok, 201, Connector} = request(
|
||||||
?MQTT_CONNECTOR(User1)#{ <<"type">> => ?CONNECTR_TYPE
|
post,
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
uri(["connectors"]),
|
||||||
}),
|
?MQTT_CONNECTOR(User1)#{
|
||||||
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
%ct:pal("---connector: ~p", [Connector]),
|
%ct:pal("---connector: ~p", [Connector]),
|
||||||
#{ <<"server">> := <<"127.0.0.1:1883">>
|
#{
|
||||||
, <<"username">> := User1
|
<<"server">> := <<"127.0.0.1:1883">>,
|
||||||
, <<"password">> := <<"">>
|
<<"username">> := User1,
|
||||||
, <<"proto_ver">> := <<"v4">>
|
<<"password">> := <<"">>,
|
||||||
, <<"ssl">> := #{<<"enable">> := false}
|
<<"proto_ver">> := <<"v4">>,
|
||||||
} = jsx:decode(Connector),
|
<<"ssl">> := #{<<"enable">> := false}
|
||||||
|
} = jsx:decode(Connector),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% ... and a MQTT bridge, using POST
|
%% ... and a MQTT bridge, using POST
|
||||||
%% we bind this bridge to the connector created just now
|
%% 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)#{
|
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"name">> => ?BRIDGE_NAME_EGRESS
|
||||||
}),
|
}
|
||||||
#{ <<"type">> := ?CONNECTR_TYPE
|
),
|
||||||
, <<"name">> := ?BRIDGE_NAME_EGRESS
|
#{
|
||||||
, <<"connector">> := ConnctorID
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
} = jsx:decode(Bridge),
|
<<"name">> := ?BRIDGE_NAME_EGRESS,
|
||||||
|
<<"connector">> := ConnctorID
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
||||||
wait_for_resource_ready(BridgeIDEgress, 5),
|
wait_for_resource_ready(BridgeIDEgress, 5),
|
||||||
|
|
||||||
|
|
@ -324,14 +395,19 @@ t_mqtt_conn_bridge_egress(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
%% verify the metrics of the bridge
|
%% verify the metrics of the bridge
|
||||||
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
||||||
?assertMatch(#{ <<"metrics">> := ?metrics(1, 1, 0, _, _, _)
|
?assertMatch(
|
||||||
, <<"node_metrics">> :=
|
#{
|
||||||
[#{<<"node">> := _, <<"metrics">> := ?metrics(1, 1, 0, _, _, _)}]
|
<<"metrics">> := ?metrics(1, 1, 0, _, _, _),
|
||||||
}, jsx:decode(BridgeStr)),
|
<<"node_metrics">> :=
|
||||||
|
[#{<<"node">> := _, <<"metrics">> := ?metrics(1, 1, 0, _, _, _)}]
|
||||||
|
},
|
||||||
|
jsx:decode(BridgeStr)
|
||||||
|
),
|
||||||
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
{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
|
%% - cannot delete a connector that is used by at least one bridge
|
||||||
t_mqtt_conn_update(_) ->
|
t_mqtt_conn_update(_) ->
|
||||||
%% then we add a mqtt connector, using POST
|
%% then we add a mqtt connector, using POST
|
||||||
{ok, 201, Connector} = request(post, uri(["connectors"]),
|
{ok, 201, Connector} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)
|
post,
|
||||||
#{ <<"type">> => ?CONNECTR_TYPE
|
uri(["connectors"]),
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
|
||||||
}),
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
%ct:pal("---connector: ~p", [Connector]),
|
%ct:pal("---connector: ~p", [Connector]),
|
||||||
#{ <<"server">> := <<"127.0.0.1:1883">>
|
#{<<"server">> := <<"127.0.0.1:1883">>} = jsx:decode(Connector),
|
||||||
} = jsx:decode(Connector),
|
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% ... and a MQTT bridge, using POST
|
%% ... and a MQTT bridge, using POST
|
||||||
%% we bind this bridge to the connector created just now
|
%% 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)#{
|
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"name">> => ?BRIDGE_NAME_EGRESS
|
||||||
}),
|
}
|
||||||
#{ <<"type">> := ?CONNECTR_TYPE
|
),
|
||||||
, <<"name">> := ?BRIDGE_NAME_EGRESS
|
#{
|
||||||
, <<"connector">> := ConnctorID
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
} = jsx:decode(Bridge),
|
<<"name">> := ?BRIDGE_NAME_EGRESS,
|
||||||
|
<<"connector">> := ConnctorID
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
||||||
wait_for_resource_ready(BridgeIDEgress, 5),
|
wait_for_resource_ready(BridgeIDEgress, 5),
|
||||||
|
|
||||||
%% Then we try to update 'server' of the connector, to an unavailable IP address
|
%% 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,
|
%% 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.
|
%% and the target resource we're going to update is unavailable.
|
||||||
{ok, 200, _} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 200, _} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)),
|
put,
|
||||||
|
uri(["connectors", ConnctorID]),
|
||||||
|
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)
|
||||||
|
),
|
||||||
%% we fix the 'server' parameter to a normal one, it should work
|
%% we fix the 'server' parameter to a normal one, it should work
|
||||||
{ok, 200, _} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 200, _} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1 : 1883">>)),
|
put,
|
||||||
|
uri(["connectors", ConnctorID]),
|
||||||
|
?MQTT_CONNECTOR2(<<"127.0.0.1 : 1883">>)
|
||||||
|
),
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
||||||
|
|
@ -390,40 +478,51 @@ t_mqtt_conn_update(_) ->
|
||||||
t_mqtt_conn_update2(_) ->
|
t_mqtt_conn_update2(_) ->
|
||||||
%% then we add a mqtt connector, using POST
|
%% then we add a mqtt connector, using POST
|
||||||
%% but this connector is point to a unreachable server "2603"
|
%% but this connector is point to a unreachable server "2603"
|
||||||
{ok, 201, Connector} = request(post, uri(["connectors"]),
|
{ok, 201, Connector} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)
|
post,
|
||||||
#{ <<"type">> => ?CONNECTR_TYPE
|
uri(["connectors"]),
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)#{
|
||||||
}),
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
#{ <<"server">> := <<"127.0.0.1:2603">>
|
#{<<"server">> := <<"127.0.0.1:2603">>} = jsx:decode(Connector),
|
||||||
} = jsx:decode(Connector),
|
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% ... and a MQTT bridge, using POST
|
%% ... and a MQTT bridge, using POST
|
||||||
%% we bind this bridge to the connector created just now
|
%% 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)#{
|
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"name">> => ?BRIDGE_NAME_EGRESS
|
||||||
}),
|
}
|
||||||
#{ <<"type">> := ?CONNECTR_TYPE
|
),
|
||||||
, <<"name">> := ?BRIDGE_NAME_EGRESS
|
#{
|
||||||
, <<"status">> := <<"disconnected">>
|
<<"type">> := ?CONNECTR_TYPE,
|
||||||
, <<"connector">> := ConnctorID
|
<<"name">> := ?BRIDGE_NAME_EGRESS,
|
||||||
} = jsx:decode(Bridge),
|
<<"status">> := <<"disconnected">>,
|
||||||
|
<<"connector">> := ConnctorID
|
||||||
|
} = jsx:decode(Bridge),
|
||||||
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
||||||
%% We try to fix the 'server' parameter, to another unavailable server..
|
%% 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
|
%% The update should success: we don't check the connectivity of the new config
|
||||||
%% if the resource is now disconnected.
|
%% if the resource is now disconnected.
|
||||||
{ok, 200, _} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 200, _} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:2604">>)),
|
put,
|
||||||
|
uri(["connectors", ConnctorID]),
|
||||||
|
?MQTT_CONNECTOR2(<<"127.0.0.1:2604">>)
|
||||||
|
),
|
||||||
%% we fix the 'server' parameter to a normal one, it should work
|
%% we fix the 'server' parameter to a normal one, it should work
|
||||||
{ok, 200, _} = request(put, uri(["connectors", ConnctorID]),
|
{ok, 200, _} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)),
|
put,
|
||||||
|
uri(["connectors", ConnctorID]),
|
||||||
|
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)
|
||||||
|
),
|
||||||
wait_for_resource_ready(BridgeIDEgress, 5),
|
wait_for_resource_ready(BridgeIDEgress, 5),
|
||||||
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
||||||
?assertMatch(#{ <<"status">> := <<"connected">>
|
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(BridgeStr)),
|
||||||
}, jsx:decode(BridgeStr)),
|
|
||||||
%% delete the bridge
|
%% delete the bridge
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
||||||
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
|
||||||
|
|
@ -434,21 +533,26 @@ t_mqtt_conn_update2(_) ->
|
||||||
|
|
||||||
t_mqtt_conn_update3(_) ->
|
t_mqtt_conn_update3(_) ->
|
||||||
%% we add a mqtt connector, using POST
|
%% we add a mqtt connector, using POST
|
||||||
{ok, 201, _} = request(post, uri(["connectors"]),
|
{ok, 201, _} = request(
|
||||||
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)
|
post,
|
||||||
#{ <<"type">> => ?CONNECTR_TYPE
|
uri(["connectors"]),
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
|
||||||
}),
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
|
||||||
%% ... and a MQTT bridge, using POST
|
%% ... and a MQTT bridge, using POST
|
||||||
%% we bind this bridge to the connector created just now
|
%% 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)#{
|
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"name">> => ?BRIDGE_NAME_EGRESS
|
||||||
}),
|
}
|
||||||
#{ <<"connector">> := ConnctorID
|
),
|
||||||
} = jsx:decode(Bridge),
|
#{<<"connector">> := ConnctorID} = jsx:decode(Bridge),
|
||||||
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
||||||
wait_for_resource_ready(BridgeIDEgress, 5),
|
wait_for_resource_ready(BridgeIDEgress, 5),
|
||||||
|
|
||||||
|
|
@ -462,37 +566,54 @@ t_mqtt_conn_update3(_) ->
|
||||||
t_mqtt_conn_testing(_) ->
|
t_mqtt_conn_testing(_) ->
|
||||||
%% APIs for testing the connectivity
|
%% APIs for testing the connectivity
|
||||||
%% then we add a mqtt connector, using POST
|
%% 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">>)#{
|
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"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">>)#{
|
?MQTT_CONNECTOR2(<<"127.0.0.1:2883">>)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"name">> => ?BRIDGE_NAME_EGRESS
|
||||||
}).
|
}
|
||||||
|
).
|
||||||
|
|
||||||
t_ingress_mqtt_bridge_with_rules(_) ->
|
t_ingress_mqtt_bridge_with_rules(_) ->
|
||||||
{ok, 201, _} = request(post, uri(["connectors"]),
|
{ok, 201, _} = request(
|
||||||
?MQTT_CONNECTOR(<<"user1">>)#{ <<"type">> => ?CONNECTR_TYPE
|
post,
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
uri(["connectors"]),
|
||||||
}),
|
?MQTT_CONNECTOR(<<"user1">>)#{
|
||||||
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?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)#{
|
?MQTT_BRIDGE_INGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_INGRESS
|
<<"name">> => ?BRIDGE_NAME_INGRESS
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
|
BridgeIDIngress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
|
||||||
|
|
||||||
{ok, 201, Rule} = request(post, uri(["rules"]),
|
{ok, 201, Rule} = request(
|
||||||
#{<<"name">> => <<"A rule get messages from a source mqtt bridge">>,
|
post,
|
||||||
<<"enable">> => true,
|
uri(["rules"]),
|
||||||
<<"outputs">> => [#{<<"function">> => "emqx_connector_api_SUITE:inspect"}],
|
#{
|
||||||
<<"sql">> => <<"SELECT * from \"$bridges/", BridgeIDIngress/binary, "\"">>
|
<<"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),
|
#{<<"id">> := RuleId} = jsx:decode(Rule),
|
||||||
|
|
||||||
%% we now test if the bridge works as expected
|
%% we now test if the bridge works as expected
|
||||||
|
|
@ -517,63 +638,81 @@ t_ingress_mqtt_bridge_with_rules(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
%% and also the rule should be matched, with matched + 1:
|
%% and also the rule should be matched, with matched + 1:
|
||||||
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
|
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
|
||||||
#{ <<"id">> := RuleId
|
#{
|
||||||
, <<"metrics">> := #{
|
<<"id">> := RuleId,
|
||||||
<<"sql.matched">> := 1,
|
<<"metrics">> := #{
|
||||||
<<"sql.passed">> := 1,
|
<<"sql.matched">> := 1,
|
||||||
<<"sql.failed">> := 0,
|
<<"sql.passed">> := 1,
|
||||||
<<"sql.failed.exception">> := 0,
|
<<"sql.failed">> := 0,
|
||||||
<<"sql.failed.no_result">> := 0,
|
<<"sql.failed.exception">> := 0,
|
||||||
<<"sql.matched.rate">> := _,
|
<<"sql.failed.no_result">> := 0,
|
||||||
<<"sql.matched.rate.max">> := _,
|
<<"sql.matched.rate">> := _,
|
||||||
<<"sql.matched.rate.last5m">> := _,
|
<<"sql.matched.rate.max">> := _,
|
||||||
<<"outputs.total">> := 1,
|
<<"sql.matched.rate.last5m">> := _,
|
||||||
<<"outputs.success">> := 1,
|
<<"outputs.total">> := 1,
|
||||||
<<"outputs.failed">> := 0,
|
<<"outputs.success">> := 1,
|
||||||
<<"outputs.failed.out_of_service">> := 0,
|
<<"outputs.failed">> := 0,
|
||||||
<<"outputs.failed.unknown">> := 0
|
<<"outputs.failed.out_of_service">> := 0,
|
||||||
}
|
<<"outputs.failed.unknown">> := 0
|
||||||
} = jsx:decode(Rule1),
|
}
|
||||||
|
} = jsx:decode(Rule1),
|
||||||
%% we also check if the outputs of the rule is triggered
|
%% we also check if the outputs of the rule is triggered
|
||||||
?assertMatch(#{inspect := #{
|
?assertMatch(
|
||||||
event := <<"$bridges/mqtt", _/binary>>,
|
#{
|
||||||
id := MsgId,
|
inspect := #{
|
||||||
payload := Payload,
|
event := <<"$bridges/mqtt", _/binary>>,
|
||||||
topic := RemoteTopic,
|
id := MsgId,
|
||||||
qos := 0,
|
payload := Payload,
|
||||||
dup := false,
|
topic := RemoteTopic,
|
||||||
retain := false,
|
qos := 0,
|
||||||
pub_props := #{},
|
dup := false,
|
||||||
timestamp := _
|
retain := false,
|
||||||
}} when is_binary(MsgId), persistent_term:get(?MODULE)),
|
pub_props := #{},
|
||||||
|
timestamp := _
|
||||||
|
}
|
||||||
|
} when is_binary(MsgId),
|
||||||
|
persistent_term:get(?MODULE)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
|
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
|
||||||
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []).
|
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []).
|
||||||
|
|
||||||
t_egress_mqtt_bridge_with_rules(_) ->
|
t_egress_mqtt_bridge_with_rules(_) ->
|
||||||
{ok, 201, _} = request(post, uri(["connectors"]),
|
{ok, 201, _} = request(
|
||||||
?MQTT_CONNECTOR(<<"user1">>)#{ <<"type">> => ?CONNECTR_TYPE
|
post,
|
||||||
, <<"name">> => ?CONNECTR_NAME
|
uri(["connectors"]),
|
||||||
}),
|
?MQTT_CONNECTOR(<<"user1">>)#{
|
||||||
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
|
<<"name">> => ?CONNECTR_NAME
|
||||||
|
}
|
||||||
|
),
|
||||||
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?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)#{
|
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
|
||||||
<<"type">> => ?CONNECTR_TYPE,
|
<<"type">> => ?CONNECTR_TYPE,
|
||||||
<<"name">> => ?BRIDGE_NAME_EGRESS
|
<<"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),
|
BridgeIDEgress = emqx_bridge:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
|
||||||
|
|
||||||
{ok, 201, Rule} = request(post, uri(["rules"]),
|
{ok, 201, Rule} = request(
|
||||||
#{<<"name">> => <<"A rule send messages to a sink mqtt bridge">>,
|
post,
|
||||||
<<"enable">> => true,
|
uri(["rules"]),
|
||||||
<<"outputs">> => [BridgeIDEgress],
|
#{
|
||||||
<<"sql">> => <<"SELECT * from \"t/1\"">>
|
<<"name">> => <<"A rule send messages to a sink mqtt bridge">>,
|
||||||
}),
|
<<"enable">> => true,
|
||||||
|
<<"outputs">> => [BridgeIDEgress],
|
||||||
|
<<"sql">> => <<"SELECT * from \"t/1\"">>
|
||||||
|
}
|
||||||
|
),
|
||||||
#{<<"id">> := RuleId} = jsx:decode(Rule),
|
#{<<"id">> := RuleId} = jsx:decode(Rule),
|
||||||
|
|
||||||
%% we now test if the bridge works as expected
|
%% we now test if the bridge works as expected
|
||||||
|
|
@ -597,7 +736,8 @@ t_egress_mqtt_bridge_with_rules(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
emqx:unsubscribe(RemoteTopic),
|
emqx:unsubscribe(RemoteTopic),
|
||||||
|
|
||||||
%% PUBLISH a message to the rule.
|
%% PUBLISH a message to the rule.
|
||||||
|
|
@ -609,23 +749,24 @@ t_egress_mqtt_bridge_with_rules(_) ->
|
||||||
wait_for_resource_ready(BridgeIDEgress, 5),
|
wait_for_resource_ready(BridgeIDEgress, 5),
|
||||||
emqx:publish(emqx_message:make(RuleTopic, Payload2)),
|
emqx:publish(emqx_message:make(RuleTopic, Payload2)),
|
||||||
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
|
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
|
||||||
#{ <<"id">> := RuleId
|
#{
|
||||||
, <<"metrics">> := #{
|
<<"id">> := RuleId,
|
||||||
<<"sql.matched">> := 1,
|
<<"metrics">> := #{
|
||||||
<<"sql.passed">> := 1,
|
<<"sql.matched">> := 1,
|
||||||
<<"sql.failed">> := 0,
|
<<"sql.passed">> := 1,
|
||||||
<<"sql.failed.exception">> := 0,
|
<<"sql.failed">> := 0,
|
||||||
<<"sql.failed.no_result">> := 0,
|
<<"sql.failed.exception">> := 0,
|
||||||
<<"sql.matched.rate">> := _,
|
<<"sql.failed.no_result">> := 0,
|
||||||
<<"sql.matched.rate.max">> := _,
|
<<"sql.matched.rate">> := _,
|
||||||
<<"sql.matched.rate.last5m">> := _,
|
<<"sql.matched.rate.max">> := _,
|
||||||
<<"outputs.total">> := 1,
|
<<"sql.matched.rate.last5m">> := _,
|
||||||
<<"outputs.success">> := 1,
|
<<"outputs.total">> := 1,
|
||||||
<<"outputs.failed">> := 0,
|
<<"outputs.success">> := 1,
|
||||||
<<"outputs.failed.out_of_service">> := 0,
|
<<"outputs.failed">> := 0,
|
||||||
<<"outputs.failed.unknown">> := 0
|
<<"outputs.failed.out_of_service">> := 0,
|
||||||
}
|
<<"outputs.failed.unknown">> := 0
|
||||||
} = jsx:decode(Rule1),
|
}
|
||||||
|
} = jsx:decode(Rule1),
|
||||||
%% we should receive a message on the "remote" broker, with specified topic
|
%% we should receive a message on the "remote" broker, with specified topic
|
||||||
?assert(
|
?assert(
|
||||||
receive
|
receive
|
||||||
|
|
@ -637,14 +778,19 @@ t_egress_mqtt_bridge_with_rules(_) ->
|
||||||
false
|
false
|
||||||
after 100 ->
|
after 100 ->
|
||||||
false
|
false
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
|
|
||||||
%% verify the metrics of the bridge
|
%% verify the metrics of the bridge
|
||||||
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
|
||||||
?assertMatch(#{ <<"metrics">> := ?metrics(2, 2, 0, _, _, _)
|
?assertMatch(
|
||||||
, <<"node_metrics">> :=
|
#{
|
||||||
[#{<<"node">> := _, <<"metrics">> := ?metrics(2, 2, 0, _, _, _)}]
|
<<"metrics">> := ?metrics(2, 2, 0, _, _, _),
|
||||||
}, jsx:decode(BridgeStr)),
|
<<"node_metrics">> :=
|
||||||
|
[#{<<"node">> := _, <<"metrics">> := ?metrics(2, 2, 0, _, _, _)}]
|
||||||
|
},
|
||||||
|
jsx:decode(BridgeStr)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
|
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
|
||||||
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
|
||||||
|
|
@ -658,8 +804,9 @@ wait_for_resource_ready(InstId, 0) ->
|
||||||
ct:fail(wait_resource_timeout);
|
ct:fail(wait_resource_timeout);
|
||||||
wait_for_resource_ready(InstId, Retry) ->
|
wait_for_resource_ready(InstId, Retry) ->
|
||||||
case emqx_bridge:lookup(InstId) of
|
case emqx_bridge:lookup(InstId) of
|
||||||
{ok, #{resource_data := #{status := connected}}} -> ok;
|
{ok, #{resource_data := #{status := connected}}} ->
|
||||||
|
ok;
|
||||||
_ ->
|
_ ->
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
wait_for_resource_ready(InstId, Retry-1)
|
wait_for_resource_ready(InstId, Retry - 1)
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -65,20 +65,24 @@ t_lifecycle(_Config) ->
|
||||||
perform_lifecycle_check(PoolName, InitialConfig) ->
|
perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
{ok, #{config := CheckedConfig}} =
|
{ok, #{config := CheckedConfig}} =
|
||||||
emqx_resource:check_config(?MONGO_RESOURCE_MOD, InitialConfig),
|
emqx_resource:check_config(?MONGO_RESOURCE_MOD, InitialConfig),
|
||||||
{ok, #{state := #{poolname := ReturnedPoolName} = State,
|
{ok, #{
|
||||||
status := InitialStatus}}
|
state := #{poolname := ReturnedPoolName} = State,
|
||||||
= emqx_resource:create_local(
|
status := InitialStatus
|
||||||
PoolName,
|
}} =
|
||||||
?CONNECTOR_RESOURCE_GROUP,
|
emqx_resource:create_local(
|
||||||
?MONGO_RESOURCE_MOD,
|
PoolName,
|
||||||
CheckedConfig,
|
?CONNECTOR_RESOURCE_GROUP,
|
||||||
#{}
|
?MONGO_RESOURCE_MOD,
|
||||||
),
|
CheckedConfig,
|
||||||
|
#{}
|
||||||
|
),
|
||||||
?assertEqual(InitialStatus, connected),
|
?assertEqual(InitialStatus, connected),
|
||||||
% Instance should match the state and status of the just started resource
|
% Instance should match the state and status of the just started resource
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := InitialStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := InitialStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
% % Perform query as further check that the resource is working as expected
|
% % Perform query as further check that the resource is working as expected
|
||||||
?assertMatch([], emqx_resource:query(PoolName, test_query_find())),
|
?assertMatch([], emqx_resource:query(PoolName, test_query_find())),
|
||||||
|
|
@ -86,11 +90,13 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
||||||
% Resource will be listed still, but state will be changed and healthcheck will fail
|
% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||||
% as the worker no longer exists.
|
% as the worker no longer exists.
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := StoppedStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := StoppedStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(StoppedStatus, disconnected),
|
?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.
|
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
||||||
% Can call stop/1 again on an already stopped instance
|
% 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)),
|
?assertEqual(ok, emqx_resource:restart(PoolName)),
|
||||||
% async restart, need to wait resource
|
% async restart, need to wait resource
|
||||||
timer:sleep(500),
|
timer:sleep(500),
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}}
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
|
||||||
= emqx_resource:get_instance(PoolName),
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
?assertMatch([], emqx_resource:query(PoolName, test_query_find())),
|
?assertMatch([], emqx_resource:query(PoolName, test_query_find())),
|
||||||
?assertMatch(undefined, emqx_resource:query(PoolName, test_query_find_one())),
|
?assertMatch(undefined, emqx_resource:query(PoolName, test_query_find_one())),
|
||||||
|
|
@ -115,12 +121,19 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
% %%------------------------------------------------------------------------------
|
% %%------------------------------------------------------------------------------
|
||||||
|
|
||||||
mongo_config() ->
|
mongo_config() ->
|
||||||
RawConfig = list_to_binary(io_lib:format("""
|
RawConfig = list_to_binary(
|
||||||
mongo_type = single
|
io_lib:format(
|
||||||
database = mqtt
|
""
|
||||||
pool_size = 8
|
"\n"
|
||||||
server = \"~s:~b\"
|
" mongo_type = single\n"
|
||||||
""", [?MONGO_HOST, ?MONGO_DEFAULT_PORT])),
|
" database = mqtt\n"
|
||||||
|
" pool_size = 8\n"
|
||||||
|
" server = \"~s:~b\"\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
[?MONGO_HOST, ?MONGO_DEFAULT_PORT]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, Config} = hocon:binary(RawConfig),
|
{ok, Config} = hocon:binary(RawConfig),
|
||||||
#{<<"config">> => Config}.
|
#{<<"config">> => Config}.
|
||||||
|
|
|
||||||
|
|
@ -22,23 +22,36 @@
|
||||||
send_and_ack_test() ->
|
send_and_ack_test() ->
|
||||||
%% delegate from gen_rpc to rpc for unit test
|
%% delegate from gen_rpc to rpc for unit test
|
||||||
meck:new(emqtt, [passthrough, no_history]),
|
meck:new(emqtt, [passthrough, no_history]),
|
||||||
meck:expect(emqtt, start_link, 1,
|
meck:expect(
|
||||||
fun(_) ->
|
emqtt,
|
||||||
{ok, spawn_link(fun() -> ok end)}
|
start_link,
|
||||||
end),
|
1,
|
||||||
|
fun(_) ->
|
||||||
|
{ok, spawn_link(fun() -> ok end)}
|
||||||
|
end
|
||||||
|
),
|
||||||
meck:expect(emqtt, connect, 1, {ok, dummy}),
|
meck:expect(emqtt, connect, 1, {ok, dummy}),
|
||||||
meck:expect(emqtt, stop, 1,
|
meck:expect(
|
||||||
fun(Pid) -> Pid ! stop end),
|
emqtt,
|
||||||
meck:expect(emqtt, publish, 2,
|
stop,
|
||||||
fun(Client, Msg) ->
|
1,
|
||||||
Client ! {publish, Msg},
|
fun(Pid) -> Pid ! stop end
|
||||||
{ok, Msg} %% as packet id
|
),
|
||||||
end),
|
meck:expect(
|
||||||
|
emqtt,
|
||||||
|
publish,
|
||||||
|
2,
|
||||||
|
fun(Client, Msg) ->
|
||||||
|
Client ! {publish, Msg},
|
||||||
|
%% as packet id
|
||||||
|
{ok, Msg}
|
||||||
|
end
|
||||||
|
),
|
||||||
try
|
try
|
||||||
Max = 1,
|
Max = 1,
|
||||||
Batch = lists:seq(1, Max),
|
Batch = lists:seq(1, Max),
|
||||||
{ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127,0,0,1}, 1883}}),
|
{ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127, 0, 0, 1}, 1883}}),
|
||||||
% %% return last packet id as batch reference
|
% %% return last packet id as batch reference
|
||||||
{ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch),
|
{ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch),
|
||||||
|
|
||||||
ok = emqx_connector_mqtt_mod:stop(Conn)
|
ok = emqx_connector_mqtt_mod:stop(Conn)
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@
|
||||||
-define(BRIDGE_NAME, test).
|
-define(BRIDGE_NAME, test).
|
||||||
-define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test).
|
-define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test).
|
||||||
-define(WAIT(PATTERN, TIMEOUT),
|
-define(WAIT(PATTERN, TIMEOUT),
|
||||||
receive
|
receive
|
||||||
PATTERN ->
|
PATTERN ->
|
||||||
ok
|
ok
|
||||||
after
|
after TIMEOUT ->
|
||||||
TIMEOUT ->
|
error(timeout)
|
||||||
error(timeout)
|
end
|
||||||
end).
|
).
|
||||||
|
|
||||||
-export([start/1, send/2, stop/1]).
|
-export([start/1, send/2, stop/1]).
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ manual_start_stop_test() ->
|
||||||
Ref = make_ref(),
|
Ref = make_ref(),
|
||||||
TestPid = self(),
|
TestPid = self(),
|
||||||
BridgeName = manual_start_stop,
|
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},
|
Config = Config0#{start_type := manual},
|
||||||
{ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}),
|
{ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}),
|
||||||
%% call ensure_started again should yield the same result
|
%% call ensure_started again should yield the same result
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,11 @@ t_lifecycle(_Config) ->
|
||||||
|
|
||||||
perform_lifecycle_check(PoolName, InitialConfig) ->
|
perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
{ok, #{config := CheckedConfig}} =
|
{ok, #{config := CheckedConfig}} =
|
||||||
emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig),
|
emqx_resource:check_config(?MYSQL_RESOURCE_MOD, InitialConfig),
|
||||||
{ok, #{state := #{poolname := ReturnedPoolName} = State,
|
{ok, #{
|
||||||
status := InitialStatus}} = emqx_resource:create_local(
|
state := #{poolname := ReturnedPoolName} = State,
|
||||||
|
status := InitialStatus
|
||||||
|
}} = emqx_resource:create_local(
|
||||||
PoolName,
|
PoolName,
|
||||||
?CONNECTOR_RESOURCE_GROUP,
|
?CONNECTOR_RESOURCE_GROUP,
|
||||||
?MYSQL_RESOURCE_MOD,
|
?MYSQL_RESOURCE_MOD,
|
||||||
|
|
@ -75,23 +77,32 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
),
|
),
|
||||||
?assertEqual(InitialStatus, connected),
|
?assertEqual(InitialStatus, connected),
|
||||||
% Instance should match the state and status of the just started resource
|
% Instance should match the state and status of the just started resource
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := InitialStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := InitialStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
% % Perform query as further check that the resource is working as expected
|
% % 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_no_params())),
|
||||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())),
|
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())),
|
||||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName,
|
?assertMatch(
|
||||||
test_query_with_params_and_timeout())),
|
{ok, _, [[1]]},
|
||||||
|
emqx_resource:query(
|
||||||
|
PoolName,
|
||||||
|
test_query_with_params_and_timeout()
|
||||||
|
)
|
||||||
|
),
|
||||||
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
||||||
% Resource will be listed still, but state will be changed and healthcheck will fail
|
% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||||
% as the worker no longer exists.
|
% as the worker no longer exists.
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := StoppedStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := StoppedStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(StoppedStatus, disconnected),
|
?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.
|
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
||||||
% Can call stop/1 again on an already stopped instance
|
% 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)),
|
?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_no_params())),
|
||||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())),
|
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName, test_query_with_params())),
|
||||||
?assertMatch({ok, _, [[1]]}, emqx_resource:query(PoolName,
|
?assertMatch(
|
||||||
test_query_with_params_and_timeout())),
|
{ok, _, [[1]]},
|
||||||
|
emqx_resource:query(
|
||||||
|
PoolName,
|
||||||
|
test_query_with_params_and_timeout()
|
||||||
|
)
|
||||||
|
),
|
||||||
% Stop and remove the resource in one go.
|
% Stop and remove the resource in one go.
|
||||||
?assertEqual(ok, emqx_resource:remove_local(PoolName)),
|
?assertEqual(ok, emqx_resource:remove_local(PoolName)),
|
||||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
||||||
|
|
@ -118,14 +134,21 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
% %%------------------------------------------------------------------------------
|
% %%------------------------------------------------------------------------------
|
||||||
|
|
||||||
mysql_config() ->
|
mysql_config() ->
|
||||||
RawConfig = list_to_binary(io_lib:format("""
|
RawConfig = list_to_binary(
|
||||||
auto_reconnect = true
|
io_lib:format(
|
||||||
database = mqtt
|
""
|
||||||
username= root
|
"\n"
|
||||||
password = public
|
" auto_reconnect = true\n"
|
||||||
pool_size = 8
|
" database = mqtt\n"
|
||||||
server = \"~s:~b\"
|
" username= root\n"
|
||||||
""", [?MYSQL_HOST, ?MYSQL_DEFAULT_PORT])),
|
" password = public\n"
|
||||||
|
" pool_size = 8\n"
|
||||||
|
" server = \"~s:~b\"\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
[?MYSQL_HOST, ?MYSQL_DEFAULT_PORT]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, Config} = hocon:binary(RawConfig),
|
{ok, Config} = hocon:binary(RawConfig),
|
||||||
#{<<"config">> => Config}.
|
#{<<"config">> => Config}.
|
||||||
|
|
|
||||||
|
|
@ -65,20 +65,24 @@ t_lifecycle(_Config) ->
|
||||||
perform_lifecycle_check(PoolName, InitialConfig) ->
|
perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
{ok, #{config := CheckedConfig}} =
|
{ok, #{config := CheckedConfig}} =
|
||||||
emqx_resource:check_config(?PGSQL_RESOURCE_MOD, InitialConfig),
|
emqx_resource:check_config(?PGSQL_RESOURCE_MOD, InitialConfig),
|
||||||
{ok, #{state := #{poolname := ReturnedPoolName} = State,
|
{ok, #{
|
||||||
status := InitialStatus}}
|
state := #{poolname := ReturnedPoolName} = State,
|
||||||
= emqx_resource:create_local(
|
status := InitialStatus
|
||||||
PoolName,
|
}} =
|
||||||
?CONNECTOR_RESOURCE_GROUP,
|
emqx_resource:create_local(
|
||||||
?PGSQL_RESOURCE_MOD,
|
PoolName,
|
||||||
CheckedConfig,
|
?CONNECTOR_RESOURCE_GROUP,
|
||||||
#{}
|
?PGSQL_RESOURCE_MOD,
|
||||||
),
|
CheckedConfig,
|
||||||
|
#{}
|
||||||
|
),
|
||||||
?assertEqual(InitialStatus, connected),
|
?assertEqual(InitialStatus, connected),
|
||||||
% Instance should match the state and status of the just started resource
|
% Instance should match the state and status of the just started resource
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := InitialStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := InitialStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
% % Perform query as further check that the resource is working as expected
|
% % 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_no_params())),
|
||||||
|
|
@ -86,11 +90,13 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
||||||
% Resource will be listed still, but state will be changed and healthcheck will fail
|
% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||||
% as the worker no longer exists.
|
% as the worker no longer exists.
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := StoppedStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := StoppedStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(StoppedStatus, disconnected),
|
?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.
|
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
||||||
% Can call stop/1 again on an already stopped instance
|
% 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)),
|
?assertEqual(ok, emqx_resource:restart(PoolName)),
|
||||||
% async restart, need to wait resource
|
% async restart, need to wait resource
|
||||||
timer:sleep(500),
|
timer:sleep(500),
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}}
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
|
||||||
= emqx_resource:get_instance(PoolName),
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(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_no_params())),
|
||||||
?assertMatch({ok, _, [{1}]}, emqx_resource:query(PoolName, test_query_with_params())),
|
?assertMatch({ok, _, [{1}]}, emqx_resource:query(PoolName, test_query_with_params())),
|
||||||
|
|
@ -115,14 +121,21 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
|
||||||
% %%------------------------------------------------------------------------------
|
% %%------------------------------------------------------------------------------
|
||||||
|
|
||||||
pgsql_config() ->
|
pgsql_config() ->
|
||||||
RawConfig = list_to_binary(io_lib:format("""
|
RawConfig = list_to_binary(
|
||||||
auto_reconnect = true
|
io_lib:format(
|
||||||
database = mqtt
|
""
|
||||||
username= root
|
"\n"
|
||||||
password = public
|
" auto_reconnect = true\n"
|
||||||
pool_size = 8
|
" database = mqtt\n"
|
||||||
server = \"~s:~b\"
|
" username= root\n"
|
||||||
""", [?PGSQL_HOST, ?PGSQL_DEFAULT_PORT])),
|
" password = public\n"
|
||||||
|
" pool_size = 8\n"
|
||||||
|
" server = \"~s:~b\"\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
[?PGSQL_HOST, ?PGSQL_DEFAULT_PORT]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, Config} = hocon:binary(RawConfig),
|
{ok, Config} = hocon:binary(RawConfig),
|
||||||
#{<<"config">> => Config}.
|
#{<<"config">> => Config}.
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,10 @@ t_sentinel_lifecycle(_Config) ->
|
||||||
perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) ->
|
perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) ->
|
||||||
{ok, #{config := CheckedConfig}} =
|
{ok, #{config := CheckedConfig}} =
|
||||||
emqx_resource:check_config(?REDIS_RESOURCE_MOD, InitialConfig),
|
emqx_resource:check_config(?REDIS_RESOURCE_MOD, InitialConfig),
|
||||||
{ok, #{state := #{poolname := ReturnedPoolName} = State,
|
{ok, #{
|
||||||
status := InitialStatus}} = emqx_resource:create_local(
|
state := #{poolname := ReturnedPoolName} = State,
|
||||||
|
status := InitialStatus
|
||||||
|
}} = emqx_resource:create_local(
|
||||||
PoolName,
|
PoolName,
|
||||||
?CONNECTOR_RESOURCE_GROUP,
|
?CONNECTOR_RESOURCE_GROUP,
|
||||||
?REDIS_RESOURCE_MOD,
|
?REDIS_RESOURCE_MOD,
|
||||||
|
|
@ -90,20 +92,24 @@ perform_lifecycle_check(PoolName, InitialConfig, RedisCommand) ->
|
||||||
),
|
),
|
||||||
?assertEqual(InitialStatus, connected),
|
?assertEqual(InitialStatus, connected),
|
||||||
% Instance should match the state and status of the just started resource
|
% Instance should match the state and status of the just started resource
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := InitialStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := InitialStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
% Perform query as further check that the resource is working as expected
|
% Perform query as further check that the resource is working as expected
|
||||||
?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})),
|
?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})),
|
||||||
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
?assertEqual(ok, emqx_resource:stop(PoolName)),
|
||||||
% Resource will be listed still, but state will be changed and healthcheck will fail
|
% Resource will be listed still, but state will be changed and healthcheck will fail
|
||||||
% as the worker no longer exists.
|
% as the worker no longer exists.
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{state := State,
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{
|
||||||
status := StoppedStatus}}
|
state := State,
|
||||||
= emqx_resource:get_instance(PoolName),
|
status := StoppedStatus
|
||||||
|
}} =
|
||||||
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(StoppedStatus, disconnected),
|
?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.
|
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
|
||||||
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
|
||||||
% Can call stop/1 again on an already stopped instance
|
% 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)),
|
?assertEqual(ok, emqx_resource:restart(PoolName)),
|
||||||
% async restart, need to wait resource
|
% async restart, need to wait resource
|
||||||
timer:sleep(500),
|
timer:sleep(500),
|
||||||
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}}
|
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
|
||||||
= emqx_resource:get_instance(PoolName),
|
emqx_resource:get_instance(PoolName),
|
||||||
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
?assertEqual(ok, emqx_resource:health_check(PoolName)),
|
||||||
?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})),
|
?assertEqual({ok, <<"PONG">>}, emqx_resource:query(PoolName, {cmd, RedisCommand})),
|
||||||
% Stop and remove the resource in one go.
|
% Stop and remove the resource in one go.
|
||||||
|
|
@ -136,14 +142,21 @@ redis_config_sentinel() ->
|
||||||
redis_config_base("sentinel", "servers").
|
redis_config_base("sentinel", "servers").
|
||||||
|
|
||||||
redis_config_base(Type, ServerKey) ->
|
redis_config_base(Type, ServerKey) ->
|
||||||
RawConfig = list_to_binary(io_lib:format("""
|
RawConfig = list_to_binary(
|
||||||
auto_reconnect = true
|
io_lib:format(
|
||||||
database = 1
|
""
|
||||||
pool_size = 8
|
"\n"
|
||||||
redis_type = ~s
|
" auto_reconnect = true\n"
|
||||||
password = public
|
" database = 1\n"
|
||||||
~s = \"~s:~b\"
|
" pool_size = 8\n"
|
||||||
""", [Type, ServerKey, ?REDIS_HOST, ?REDIS_PORT])),
|
" redis_type = ~s\n"
|
||||||
|
" password = public\n"
|
||||||
|
" ~s = \"~s:~b\"\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
[Type, ServerKey, ?REDIS_HOST, ?REDIS_PORT]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, Config} = hocon:binary(RawConfig),
|
{ok, Config} = hocon:binary(RawConfig),
|
||||||
#{<<"config">> => Config}.
|
#{<<"config">> => Config}.
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,11 @@
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
-export([ check_fields/1
|
-export([
|
||||||
, start_apps/1
|
check_fields/1,
|
||||||
, stop_apps/1
|
start_apps/1,
|
||||||
]).
|
stop_apps/1
|
||||||
|
]).
|
||||||
|
|
||||||
check_fields({FieldName, FieldValue}) ->
|
check_fields({FieldName, FieldValue}) ->
|
||||||
?assert(is_atom(FieldName)),
|
?assert(is_atom(FieldName)),
|
||||||
|
|
@ -30,10 +31,10 @@ check_fields({FieldName, FieldValue}) ->
|
||||||
is_map(FieldValue) ->
|
is_map(FieldValue) ->
|
||||||
ct:pal("~p~n", [{FieldName, FieldValue}]),
|
ct:pal("~p~n", [{FieldName, FieldValue}]),
|
||||||
?assert(
|
?assert(
|
||||||
(maps:is_key(type, FieldValue)
|
(maps:is_key(type, FieldValue) andalso
|
||||||
andalso maps:is_key(default, FieldValue))
|
maps:is_key(default, FieldValue)) orelse
|
||||||
orelse ((maps:is_key(required, FieldValue)
|
(maps:is_key(required, FieldValue) andalso
|
||||||
andalso maps:get(required, FieldValue) =:= false))
|
maps:get(required, FieldValue) =:= false)
|
||||||
);
|
);
|
||||||
true ->
|
true ->
|
||||||
?assert(is_function(FieldValue))
|
?assert(is_function(FieldValue))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
]}.
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_plugins,
|
{application, emqx_plugins, [
|
||||||
[{description, "EMQX Plugin Management"},
|
{description, "EMQX Plugin Management"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{mod, {emqx_plugins_app,[]}},
|
{mod, {emqx_plugins_app, []}},
|
||||||
{applications, [kernel,stdlib,emqx]},
|
{applications, [kernel, stdlib, emqx]},
|
||||||
{env, []}
|
{env, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -19,35 +19,37 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ ensure_installed/1
|
-export([
|
||||||
, ensure_uninstalled/1
|
ensure_installed/1,
|
||||||
, ensure_enabled/1
|
ensure_uninstalled/1,
|
||||||
, ensure_enabled/2
|
ensure_enabled/1,
|
||||||
, ensure_disabled/1
|
ensure_enabled/2,
|
||||||
, purge/1
|
ensure_disabled/1,
|
||||||
, delete_package/1
|
purge/1,
|
||||||
]).
|
delete_package/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ ensure_started/0
|
-export([
|
||||||
, ensure_started/1
|
ensure_started/0,
|
||||||
, ensure_stopped/0
|
ensure_started/1,
|
||||||
, ensure_stopped/1
|
ensure_stopped/0,
|
||||||
, restart/1
|
ensure_stopped/1,
|
||||||
, list/0
|
restart/1,
|
||||||
, describe/1
|
list/0,
|
||||||
, parse_name_vsn/1
|
describe/1,
|
||||||
]).
|
parse_name_vsn/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ get_config/2
|
-export([
|
||||||
, put_config/2
|
get_config/2,
|
||||||
]).
|
put_config/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% internal
|
%% internal
|
||||||
-export([ do_ensure_started/1
|
-export([do_ensure_started/1]).
|
||||||
]).
|
|
||||||
-export([
|
-export([
|
||||||
install_dir/0
|
install_dir/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
@ -58,8 +60,10 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include("emqx_plugins.hrl").
|
-include("emqx_plugins.hrl").
|
||||||
|
|
||||||
-type name_vsn() :: binary() | string(). %% "my_plugin-0.1.0"
|
%% "my_plugin-0.1.0"
|
||||||
-type plugin() :: map(). %% the parse result of the JSON info file
|
-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()}.
|
-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
|
case erl_tar:extract(TarGz, [{cwd, install_dir()}, compressed]) of
|
||||||
ok ->
|
ok ->
|
||||||
case read_plugin(NameVsn, #{}) of
|
case read_plugin(NameVsn, #{}) of
|
||||||
{ok, _} -> ok;
|
{ok, _} ->
|
||||||
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}),
|
?SLOG(warning, Reason#{msg => "failed_to_read_after_install"}),
|
||||||
_ = ensure_uninstalled(NameVsn),
|
_ = ensure_uninstalled(NameVsn),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, {_, enoent}} ->
|
{error, {_, enoent}} ->
|
||||||
{error, #{ reason => "failed_to_extract_plugin_package"
|
{error, #{
|
||||||
, path => TarGz
|
reason => "failed_to_extract_plugin_package",
|
||||||
, return => not_found
|
path => TarGz,
|
||||||
}};
|
return => not_found
|
||||||
|
}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, #{ reason => "bad_plugin_package"
|
{error, #{
|
||||||
, path => TarGz
|
reason => "bad_plugin_package",
|
||||||
, return => Reason
|
path => TarGz,
|
||||||
}}
|
return => Reason
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Ensure files and directories for the given plugin are delete.
|
%% @doc Ensure files and directories for the given plugin are delete.
|
||||||
|
|
@ -110,13 +117,15 @@ do_ensure_installed(NameVsn) ->
|
||||||
ensure_uninstalled(NameVsn) ->
|
ensure_uninstalled(NameVsn) ->
|
||||||
case read_plugin(NameVsn, #{}) of
|
case read_plugin(NameVsn, #{}) of
|
||||||
{ok, #{running_status := RunningSt}} when RunningSt =/= stopped ->
|
{ok, #{running_status := RunningSt}} when RunningSt =/= stopped ->
|
||||||
{error, #{reason => "bad_plugin_running_status",
|
{error, #{
|
||||||
hint => "stop_the_plugin_first"
|
reason => "bad_plugin_running_status",
|
||||||
}};
|
hint => "stop_the_plugin_first"
|
||||||
|
}};
|
||||||
{ok, #{config_status := enabled}} ->
|
{ok, #{config_status := enabled}} ->
|
||||||
{error, #{reason => "bad_plugin_config_status",
|
{error, #{
|
||||||
hint => "disable_the_plugin_first"
|
reason => "bad_plugin_config_status",
|
||||||
}};
|
hint => "disable_the_plugin_first"
|
||||||
|
}};
|
||||||
_ ->
|
_ ->
|
||||||
purge(NameVsn)
|
purge(NameVsn)
|
||||||
end.
|
end.
|
||||||
|
|
@ -141,9 +150,10 @@ ensure_state(NameVsn, Position, State) when is_binary(NameVsn) ->
|
||||||
ensure_state(NameVsn, Position, State) ->
|
ensure_state(NameVsn, Position, State) ->
|
||||||
case read_plugin(NameVsn, #{}) of
|
case read_plugin(NameVsn, #{}) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
Item = #{ name_vsn => NameVsn
|
Item = #{
|
||||||
, enable => State
|
name_vsn => NameVsn,
|
||||||
},
|
enable => State
|
||||||
|
},
|
||||||
tryit("ensure_state", fun() -> ensure_configured(Item, Position) end);
|
tryit("ensure_state", fun() -> ensure_configured(Item, Position) end);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{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,
|
SplitFun = fun(#{name_vsn := Nv}) -> bin(Nv) =/= bin(NameVsn) end,
|
||||||
{Front, Rear} = lists:splitwith(SplitFun, Configured),
|
{Front, Rear} = lists:splitwith(SplitFun, Configured),
|
||||||
Rear =:= [] andalso
|
Rear =:= [] andalso
|
||||||
throw(#{error => "position_anchor_plugin_not_configured",
|
throw(#{
|
||||||
hint => "maybe_install_and_configure",
|
error => "position_anchor_plugin_not_configured",
|
||||||
name_vsn => NameVsn
|
hint => "maybe_install_and_configure",
|
||||||
}),
|
name_vsn => NameVsn
|
||||||
|
}),
|
||||||
case Action of
|
case Action of
|
||||||
before -> Front ++ [Item | Rear];
|
before ->
|
||||||
|
Front ++ [Item | Rear];
|
||||||
behind ->
|
behind ->
|
||||||
[Anchor | Rear0] = Rear,
|
[Anchor | Rear0] = Rear,
|
||||||
Front ++ [Anchor, Item | Rear0]
|
Front ++ [Anchor, Item | Rear0]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
%% @doc Delete the package file.
|
%% @doc Delete the package file.
|
||||||
-spec delete_package(name_vsn()) -> ok.
|
-spec delete_package(name_vsn()) -> ok.
|
||||||
delete_package(NameVsn) ->
|
delete_package(NameVsn) ->
|
||||||
|
|
@ -198,9 +209,11 @@ delete_package(NameVsn) ->
|
||||||
{error, enoent} ->
|
{error, enoent} ->
|
||||||
ok;
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "failed_to_delete_package_file",
|
?SLOG(error, #{
|
||||||
path => File,
|
msg => "failed_to_delete_package_file",
|
||||||
reason => Reason}),
|
path => File,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -219,9 +232,11 @@ purge(NameVsn) ->
|
||||||
{error, enoent} ->
|
{error, enoent} ->
|
||||||
ok;
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "failed_to_purge_plugin_dir",
|
?SLOG(error, #{
|
||||||
dir => Dir,
|
msg => "failed_to_purge_plugin_dir",
|
||||||
reason => Reason}),
|
dir => Dir,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -235,10 +250,13 @@ ensure_started() ->
|
||||||
-spec ensure_started(name_vsn()) -> ok | {error, term()}.
|
-spec ensure_started(name_vsn()) -> ok | {error, term()}.
|
||||||
ensure_started(NameVsn) ->
|
ensure_started(NameVsn) ->
|
||||||
case do_ensure_started(NameVsn) of
|
case do_ensure_started(NameVsn) of
|
||||||
ok -> ok;
|
ok ->
|
||||||
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(alert, #{msg => "failed_to_start_plugin",
|
?SLOG(alert, #{
|
||||||
reason => Reason}),
|
msg => "failed_to_start_plugin",
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -250,11 +268,13 @@ ensure_stopped() ->
|
||||||
%% @doc Stop a plugin from Management API or CLI.
|
%% @doc Stop a plugin from Management API or CLI.
|
||||||
-spec ensure_stopped(name_vsn()) -> ok | {error, term()}.
|
-spec ensure_stopped(name_vsn()) -> ok | {error, term()}.
|
||||||
ensure_stopped(NameVsn) ->
|
ensure_stopped(NameVsn) ->
|
||||||
tryit("stop_plugin",
|
tryit(
|
||||||
fun() ->
|
"stop_plugin",
|
||||||
Plugin = do_read_plugin(NameVsn),
|
fun() ->
|
||||||
ensure_apps_stopped(Plugin)
|
Plugin = do_read_plugin(NameVsn),
|
||||||
end).
|
ensure_apps_stopped(Plugin)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%% @doc Stop and then start the plugin.
|
%% @doc Stop and then start the plugin.
|
||||||
restart(NameVsn) ->
|
restart(NameVsn) ->
|
||||||
|
|
@ -269,39 +289,45 @@ restart(NameVsn) ->
|
||||||
list() ->
|
list() ->
|
||||||
Pattern = filename:join([install_dir(), "*", "release.json"]),
|
Pattern = filename:join([install_dir(), "*", "release.json"]),
|
||||||
All = lists:filtermap(
|
All = lists:filtermap(
|
||||||
fun(JsonFile) ->
|
fun(JsonFile) ->
|
||||||
case read_plugin({file, JsonFile}, #{}) of
|
case read_plugin({file, JsonFile}, #{}) of
|
||||||
{ok, Info} ->
|
{ok, Info} ->
|
||||||
{true, Info};
|
{true, Info};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(warning, Reason),
|
?SLOG(warning, Reason),
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end, filelib:wildcard(Pattern)),
|
end,
|
||||||
|
filelib:wildcard(Pattern)
|
||||||
|
),
|
||||||
list(configured(), All).
|
list(configured(), All).
|
||||||
|
|
||||||
%% Make sure configured ones are ordered in front.
|
%% Make sure configured ones are ordered in front.
|
||||||
list([], All) -> All;
|
list([], All) ->
|
||||||
|
All;
|
||||||
list([#{name_vsn := NameVsn} | Rest], All) ->
|
list([#{name_vsn := NameVsn} | Rest], All) ->
|
||||||
SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
|
SplitF = fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
|
||||||
bin([Name, "-", Vsn]) =/= bin(NameVsn)
|
bin([Name, "-", Vsn]) =/= bin(NameVsn)
|
||||||
end,
|
end,
|
||||||
case lists:splitwith(SplitF, All) of
|
case lists:splitwith(SplitF, All) of
|
||||||
{_, []} ->
|
{_, []} ->
|
||||||
?SLOG(warning, #{msg => "configured_plugin_not_installed",
|
?SLOG(warning, #{
|
||||||
name_vsn => NameVsn
|
msg => "configured_plugin_not_installed",
|
||||||
}),
|
name_vsn => NameVsn
|
||||||
|
}),
|
||||||
list(Rest, All);
|
list(Rest, All);
|
||||||
{Front, [I | Rear]} ->
|
{Front, [I | Rear]} ->
|
||||||
[I | list(Rest, Front ++ Rear)]
|
[I | list(Rest, Front ++ Rear)]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_ensure_started(NameVsn) ->
|
do_ensure_started(NameVsn) ->
|
||||||
tryit("start_plugins",
|
tryit(
|
||||||
fun() ->
|
"start_plugins",
|
||||||
Plugin = do_read_plugin(NameVsn),
|
fun() ->
|
||||||
ok = load_code_start_apps(NameVsn, Plugin)
|
Plugin = do_read_plugin(NameVsn),
|
||||||
end).
|
ok = load_code_start_apps(NameVsn, Plugin)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%% try the function, catch 'throw' exceptions as normal 'error' return
|
%% try the function, catch 'throw' exceptions as normal 'error' return
|
||||||
%% other exceptions with stacktrace returned.
|
%% other exceptions with stacktrace returned.
|
||||||
|
|
@ -309,25 +335,28 @@ tryit(WhichOp, F) ->
|
||||||
try
|
try
|
||||||
F()
|
F()
|
||||||
catch
|
catch
|
||||||
throw : Reason ->
|
throw:Reason ->
|
||||||
%% thrown exceptions are known errors
|
%% thrown exceptions are known errors
|
||||||
%% translate to a return value without stacktrace
|
%% translate to a return value without stacktrace
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
error : Reason : Stacktrace ->
|
error:Reason:Stacktrace ->
|
||||||
%% unexpected errors, log stacktrace
|
%% unexpected errors, log stacktrace
|
||||||
?SLOG(warning, #{ msg => "plugin_op_failed"
|
?SLOG(warning, #{
|
||||||
, which_op => WhichOp
|
msg => "plugin_op_failed",
|
||||||
, exception => Reason
|
which_op => WhichOp,
|
||||||
, stacktrace => Stacktrace
|
exception => Reason,
|
||||||
}),
|
stacktrace => Stacktrace
|
||||||
|
}),
|
||||||
{error, {failed, WhichOp}}
|
{error, {failed, WhichOp}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% read plugin info from the JSON file
|
%% read plugin info from the JSON file
|
||||||
%% returns {ok, Info} or {error, Reason}
|
%% returns {ok, Info} or {error, Reason}
|
||||||
read_plugin(NameVsn, Options) ->
|
read_plugin(NameVsn, Options) ->
|
||||||
tryit("read_plugin_info",
|
tryit(
|
||||||
fun() -> {ok, do_read_plugin(NameVsn, Options)} end).
|
"read_plugin_info",
|
||||||
|
fun() -> {ok, do_read_plugin(NameVsn, Options)} end
|
||||||
|
).
|
||||||
|
|
||||||
do_read_plugin(Plugin) -> do_read_plugin(Plugin, #{}).
|
do_read_plugin(Plugin) -> do_read_plugin(Plugin, #{}).
|
||||||
|
|
||||||
|
|
@ -339,10 +368,11 @@ do_read_plugin({file, InfoFile}, Options) ->
|
||||||
Info1 = plugins_readme(NameVsn, Options, Info0),
|
Info1 = plugins_readme(NameVsn, Options, Info0),
|
||||||
plugin_status(NameVsn, Info1);
|
plugin_status(NameVsn, Info1);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
throw(#{error => "bad_info_file",
|
throw(#{
|
||||||
path => InfoFile,
|
error => "bad_info_file",
|
||||||
return => Reason
|
path => InfoFile,
|
||||||
})
|
return => Reason
|
||||||
|
})
|
||||||
end;
|
end;
|
||||||
do_read_plugin(NameVsn, Options) ->
|
do_read_plugin(NameVsn, Options) ->
|
||||||
do_read_plugin({file, info_file(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};
|
{ok, Bin} -> Info#{readme => Bin};
|
||||||
_ -> Info#{readme => <<>>}
|
_ -> Info#{readme => <<>>}
|
||||||
end;
|
end;
|
||||||
plugins_readme(_NameVsn, _Options, Info) -> Info.
|
plugins_readme(_NameVsn, _Options, Info) ->
|
||||||
|
Info.
|
||||||
|
|
||||||
plugin_status(NameVsn, Info) ->
|
plugin_status(NameVsn, Info) ->
|
||||||
{AppName, _AppVsn} = parse_name_vsn(NameVsn),
|
{AppName, _AppVsn} = parse_name_vsn(NameVsn),
|
||||||
|
|
@ -368,74 +399,91 @@ plugin_status(NameVsn, Info) ->
|
||||||
end,
|
end,
|
||||||
Configured = lists:filtermap(
|
Configured = lists:filtermap(
|
||||||
fun(#{name_vsn := Nv, enable := St}) ->
|
fun(#{name_vsn := Nv, enable := St}) ->
|
||||||
case bin(Nv) =:= bin(NameVsn) of
|
case bin(Nv) =:= bin(NameVsn) of
|
||||||
true -> {true, St};
|
true -> {true, St};
|
||||||
false -> false
|
false -> false
|
||||||
end
|
end
|
||||||
end, configured()),
|
end,
|
||||||
ConfSt = case Configured of
|
configured()
|
||||||
[] -> not_configured;
|
),
|
||||||
[true] -> enabled;
|
ConfSt =
|
||||||
[false] -> disabled
|
case Configured of
|
||||||
end,
|
[] -> not_configured;
|
||||||
Info#{ running_status => RunningSt
|
[true] -> enabled;
|
||||||
, config_status => ConfSt
|
[false] -> disabled
|
||||||
|
end,
|
||||||
|
Info#{
|
||||||
|
running_status => RunningSt,
|
||||||
|
config_status => ConfSt
|
||||||
}.
|
}.
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||||
bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
|
bin(L) when is_list(L) -> unicode:characters_to_binary(L, utf8);
|
||||||
bin(B) when is_binary(B) -> B.
|
bin(B) when is_binary(B) -> B.
|
||||||
|
|
||||||
check_plugin(#{ <<"name">> := Name
|
check_plugin(
|
||||||
, <<"rel_vsn">> := Vsn
|
#{
|
||||||
, <<"rel_apps">> := Apps
|
<<"name">> := Name,
|
||||||
, <<"description">> := _
|
<<"rel_vsn">> := Vsn,
|
||||||
} = Info, NameVsn, File) ->
|
<<"rel_apps">> := Apps,
|
||||||
|
<<"description">> := _
|
||||||
|
} = Info,
|
||||||
|
NameVsn,
|
||||||
|
File
|
||||||
|
) ->
|
||||||
case bin(NameVsn) =:= bin([Name, "-", Vsn]) of
|
case bin(NameVsn) =:= bin([Name, "-", Vsn]) of
|
||||||
true ->
|
true ->
|
||||||
try
|
try
|
||||||
[_ | _ ] = Apps, %% assert
|
%% assert
|
||||||
|
[_ | _] = Apps,
|
||||||
%% validate if the list is all <app>-<vsn> strings
|
%% validate if the list is all <app>-<vsn> strings
|
||||||
lists:foreach(fun parse_name_vsn/1, Apps)
|
lists:foreach(fun parse_name_vsn/1, Apps)
|
||||||
catch
|
catch
|
||||||
_ : _ ->
|
_:_ ->
|
||||||
throw(#{ error => "bad_rel_apps"
|
throw(#{
|
||||||
, rel_apps => Apps
|
error => "bad_rel_apps",
|
||||||
, hint => "A non-empty string list of app_name-app_vsn format"
|
rel_apps => Apps,
|
||||||
})
|
hint => "A non-empty string list of app_name-app_vsn format"
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
Info;
|
Info;
|
||||||
false ->
|
false ->
|
||||||
throw(#{ error => "name_vsn_mismatch"
|
throw(#{
|
||||||
, name_vsn => NameVsn
|
error => "name_vsn_mismatch",
|
||||||
, path => File
|
name_vsn => NameVsn,
|
||||||
, name => Name
|
path => File,
|
||||||
, rel_vsn => Vsn
|
name => Name,
|
||||||
})
|
rel_vsn => Vsn
|
||||||
|
})
|
||||||
end;
|
end;
|
||||||
check_plugin(_What, NameVsn, File) ->
|
check_plugin(_What, NameVsn, File) ->
|
||||||
throw(#{ error => "bad_info_file_content"
|
throw(#{
|
||||||
, mandatory_fields => [rel_vsn, name, rel_apps, description]
|
error => "bad_info_file_content",
|
||||||
, name_vsn => NameVsn
|
mandatory_fields => [rel_vsn, name, rel_apps, description],
|
||||||
, path => File
|
name_vsn => NameVsn,
|
||||||
}).
|
path => File
|
||||||
|
}).
|
||||||
|
|
||||||
load_code_start_apps(RelNameVsn, #{<<"rel_apps">> := Apps}) ->
|
load_code_start_apps(RelNameVsn, #{<<"rel_apps">> := Apps}) ->
|
||||||
LibDir = filename:join([install_dir(), RelNameVsn]),
|
LibDir = filename:join([install_dir(), RelNameVsn]),
|
||||||
RunningApps = running_apps(),
|
RunningApps = running_apps(),
|
||||||
%% load plugin apps and beam code
|
%% load plugin apps and beam code
|
||||||
AppNames =
|
AppNames =
|
||||||
lists:map(fun(AppNameVsn) ->
|
lists:map(
|
||||||
{AppName, AppVsn} = parse_name_vsn(AppNameVsn),
|
fun(AppNameVsn) ->
|
||||||
EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]),
|
{AppName, AppVsn} = parse_name_vsn(AppNameVsn),
|
||||||
ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps),
|
EbinDir = filename:join([LibDir, AppNameVsn, "ebin"]),
|
||||||
AppName
|
ok = load_plugin_app(AppName, AppVsn, EbinDir, RunningApps),
|
||||||
end, Apps),
|
AppName
|
||||||
|
end,
|
||||||
|
Apps
|
||||||
|
),
|
||||||
lists:foreach(fun start_app/1, AppNames).
|
lists:foreach(fun start_app/1, AppNames).
|
||||||
|
|
||||||
load_plugin_app(AppName, AppVsn, Ebin, RunningApps) ->
|
load_plugin_app(AppName, AppVsn, Ebin, RunningApps) ->
|
||||||
case lists:keyfind(AppName, 1, RunningApps) of
|
case lists:keyfind(AppName, 1, RunningApps) of
|
||||||
false -> do_load_plugin_app(AppName, Ebin);
|
false ->
|
||||||
|
do_load_plugin_app(AppName, Ebin);
|
||||||
{_, Vsn} ->
|
{_, Vsn} ->
|
||||||
case bin(Vsn) =:= bin(AppVsn) of
|
case bin(Vsn) =:= bin(AppVsn) of
|
||||||
true ->
|
true ->
|
||||||
|
|
@ -443,10 +491,12 @@ load_plugin_app(AppName, AppVsn, Ebin, RunningApps) ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
%% running but a different version
|
%% running but a different version
|
||||||
?SLOG(warning, #{msg => "plugin_app_already_running", name => AppName,
|
?SLOG(warning, #{
|
||||||
running_vsn => Vsn,
|
msg => "plugin_app_already_running",
|
||||||
loading_vsn => AppVsn
|
name => AppName,
|
||||||
})
|
running_vsn => Vsn,
|
||||||
|
loading_vsn => AppVsn
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -457,21 +507,31 @@ do_load_plugin_app(AppName, Ebin) ->
|
||||||
Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])),
|
Modules = filelib:wildcard(filename:join([Ebin, "*.beam"])),
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(BeamFile) ->
|
fun(BeamFile) ->
|
||||||
Module = list_to_atom(filename:basename(BeamFile, ".beam")),
|
Module = list_to_atom(filename:basename(BeamFile, ".beam")),
|
||||||
case code:load_file(Module) of
|
case code:load_file(Module) of
|
||||||
{module, _} -> ok;
|
{module, _} ->
|
||||||
{error, Reason} -> throw(#{error => "failed_to_load_plugin_beam",
|
ok;
|
||||||
path => BeamFile,
|
{error, Reason} ->
|
||||||
reason => Reason
|
throw(#{
|
||||||
})
|
error => "failed_to_load_plugin_beam",
|
||||||
end
|
path => BeamFile,
|
||||||
end, Modules),
|
reason => Reason
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Modules
|
||||||
|
),
|
||||||
case application:load(AppName) of
|
case application:load(AppName) of
|
||||||
ok -> ok;
|
ok ->
|
||||||
{error, {already_loaded, _}} -> ok;
|
ok;
|
||||||
{error, Reason} -> throw(#{error => "failed_to_load_plugin_app",
|
{error, {already_loaded, _}} ->
|
||||||
name => AppName,
|
ok;
|
||||||
reason => Reason})
|
{error, Reason} ->
|
||||||
|
throw(#{
|
||||||
|
error => "failed_to_load_plugin_app",
|
||||||
|
name => AppName,
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
start_app(App) ->
|
start_app(App) ->
|
||||||
|
|
@ -484,11 +544,12 @@ start_app(App) ->
|
||||||
?SLOG(debug, #{msg => "started_plugin_app", app => App}),
|
?SLOG(debug, #{msg => "started_plugin_app", app => App}),
|
||||||
ok;
|
ok;
|
||||||
{error, {ErrApp, Reason}} ->
|
{error, {ErrApp, Reason}} ->
|
||||||
throw(#{error => "failed_to_start_plugin_app",
|
throw(#{
|
||||||
app => App,
|
error => "failed_to_start_plugin_app",
|
||||||
err_app => ErrApp,
|
app => App,
|
||||||
reason => Reason
|
err_app => ErrApp,
|
||||||
})
|
reason => Reason
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Stop all apps installed by the plugin package,
|
%% Stop all apps installed by the plugin package,
|
||||||
|
|
@ -496,18 +557,22 @@ start_app(App) ->
|
||||||
ensure_apps_stopped(#{<<"rel_apps">> := Apps}) ->
|
ensure_apps_stopped(#{<<"rel_apps">> := Apps}) ->
|
||||||
%% load plugin apps and beam code
|
%% load plugin apps and beam code
|
||||||
AppsToStop =
|
AppsToStop =
|
||||||
lists:map(fun(NameVsn) ->
|
lists:map(
|
||||||
{AppName, _AppVsn} = parse_name_vsn(NameVsn),
|
fun(NameVsn) ->
|
||||||
AppName
|
{AppName, _AppVsn} = parse_name_vsn(NameVsn),
|
||||||
end, Apps),
|
AppName
|
||||||
|
end,
|
||||||
|
Apps
|
||||||
|
),
|
||||||
case tryit("stop_apps", fun() -> stop_apps(AppsToStop) end) of
|
case tryit("stop_apps", fun() -> stop_apps(AppsToStop) end) of
|
||||||
{ok, []} ->
|
{ok, []} ->
|
||||||
%% all apps stopped
|
%% all apps stopped
|
||||||
ok;
|
ok;
|
||||||
{ok, Left} ->
|
{ok, Left} ->
|
||||||
?SLOG(warning, #{msg => "unabled_to_stop_plugin_apps",
|
?SLOG(warning, #{
|
||||||
apps => Left
|
msg => "unabled_to_stop_plugin_apps",
|
||||||
}),
|
apps => Left
|
||||||
|
}),
|
||||||
ok;
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
|
@ -516,9 +581,12 @@ ensure_apps_stopped(#{<<"rel_apps">> := Apps}) ->
|
||||||
stop_apps(Apps) ->
|
stop_apps(Apps) ->
|
||||||
RunningApps = running_apps(),
|
RunningApps = running_apps(),
|
||||||
case do_stop_apps(Apps, [], RunningApps) of
|
case do_stop_apps(Apps, [], RunningApps) of
|
||||||
{ok, []} -> {ok, []}; %% all stopped
|
%% all stopped
|
||||||
{ok, Remain} when Remain =:= Apps -> {ok, Apps}; %% no progress
|
{ok, []} -> {ok, []};
|
||||||
{ok, Remain} -> stop_apps(Remain) %% try again
|
%% no progress
|
||||||
|
{ok, Remain} when Remain =:= Apps -> {ok, Apps};
|
||||||
|
%% try again
|
||||||
|
{ok, Remain} -> stop_apps(Remain)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_stop_apps([], Remain, _AllApps) ->
|
do_stop_apps([], Remain, _AllApps) ->
|
||||||
|
|
@ -553,11 +621,15 @@ unload_moudle_and_app(App) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
is_needed_by_any(AppToStop, RunningApps) ->
|
is_needed_by_any(AppToStop, RunningApps) ->
|
||||||
lists:any(fun({RunningApp, _RunningAppVsn}) ->
|
lists:any(
|
||||||
is_needed_by(AppToStop, RunningApp)
|
fun({RunningApp, _RunningAppVsn}) ->
|
||||||
end, RunningApps).
|
is_needed_by(AppToStop, RunningApp)
|
||||||
|
end,
|
||||||
|
RunningApps
|
||||||
|
).
|
||||||
|
|
||||||
is_needed_by(AppToStop, AppToStop) -> false;
|
is_needed_by(AppToStop, AppToStop) ->
|
||||||
|
false;
|
||||||
is_needed_by(AppToStop, RunningApp) ->
|
is_needed_by(AppToStop, RunningApp) ->
|
||||||
case application:get_key(RunningApp, applications) of
|
case application:get_key(RunningApp, applications) of
|
||||||
{ok, Deps} -> lists:member(AppToStop, Deps);
|
{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);
|
maps:fold(fun(K, V, Acc) -> Acc#{bin(K) => V} end, #{}, Map);
|
||||||
bin_key(List = [#{} | _]) ->
|
bin_key(List = [#{} | _]) ->
|
||||||
lists:map(fun(M) -> bin_key(M) end, 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) when is_atom(Key) ->
|
||||||
get_config([Key], Default);
|
get_config([Key], Default);
|
||||||
|
|
@ -604,8 +677,10 @@ for_plugin(#{name_vsn := NameVsn, enable := true}, Fun) ->
|
||||||
{error, Reason} -> [{NameVsn, Reason}]
|
{error, Reason} -> [{NameVsn, Reason}]
|
||||||
end;
|
end;
|
||||||
for_plugin(#{name_vsn := NameVsn, enable := false}, _Fun) ->
|
for_plugin(#{name_vsn := NameVsn, enable := false}, _Fun) ->
|
||||||
?SLOG(debug, #{msg => "plugin_disabled",
|
?SLOG(debug, #{
|
||||||
name_vsn => NameVsn}),
|
msg => "plugin_disabled",
|
||||||
|
name_vsn => NameVsn
|
||||||
|
}),
|
||||||
[].
|
[].
|
||||||
|
|
||||||
parse_name_vsn(NameVsn) when is_binary(NameVsn) ->
|
parse_name_vsn(NameVsn) when is_binary(NameVsn) ->
|
||||||
|
|
@ -627,6 +702,9 @@ readme_file(NameVsn) ->
|
||||||
filename:join([dir(NameVsn), "README.md"]).
|
filename:join([dir(NameVsn), "README.md"]).
|
||||||
|
|
||||||
running_apps() ->
|
running_apps() ->
|
||||||
lists:map(fun({N, _, V}) ->
|
lists:map(
|
||||||
{N, V}
|
fun({N, _, V}) ->
|
||||||
end, application:which_applications(infinity)).
|
{N, V}
|
||||||
|
end,
|
||||||
|
application:which_applications(infinity)
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@
|
||||||
|
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
-export([ start/2
|
-export([
|
||||||
, stop/1
|
start/2,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
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} = emqx_plugins_sup:start_link(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,23 @@
|
||||||
|
|
||||||
-module(emqx_plugins_cli).
|
-module(emqx_plugins_cli).
|
||||||
|
|
||||||
-export([ list/1
|
-export([
|
||||||
, describe/2
|
list/1,
|
||||||
, ensure_installed/2
|
describe/2,
|
||||||
, ensure_uninstalled/2
|
ensure_installed/2,
|
||||||
, ensure_started/2
|
ensure_uninstalled/2,
|
||||||
, ensure_stopped/2
|
ensure_started/2,
|
||||||
, restart/2
|
ensure_stopped/2,
|
||||||
, ensure_disabled/2
|
restart/2,
|
||||||
, ensure_enabled/3
|
ensure_disabled/2,
|
||||||
]).
|
ensure_enabled/3
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-define(PRINT(EXPR, LOG_FUN),
|
-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) ->
|
list(LogFun) ->
|
||||||
LogFun("~ts~n", [to_json(emqx_plugins:list())]).
|
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
|
%% this should not happen unless the package is manually installed
|
||||||
%% corrupted packages installed from emqx_plugins:ensure_installed
|
%% corrupted packages installed from emqx_plugins:ensure_installed
|
||||||
%% should not leave behind corrupted files
|
%% should not leave behind corrupted files
|
||||||
?SLOG(error, #{msg => "failed_to_describe_plugin",
|
?SLOG(error, #{
|
||||||
name_vsn => NameVsn,
|
msg => "failed_to_describe_plugin",
|
||||||
cause => Reason}),
|
name_vsn => NameVsn,
|
||||||
|
cause => Reason
|
||||||
|
}),
|
||||||
%% do nothing to the CLI console
|
%% do nothing to the CLI console
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
@ -75,14 +79,18 @@ to_json(Input) ->
|
||||||
emqx_logger_jsonfmt:best_effort_json(Input).
|
emqx_logger_jsonfmt:best_effort_json(Input).
|
||||||
|
|
||||||
print(NameVsn, Res, LogFun, Action) ->
|
print(NameVsn, Res, LogFun, Action) ->
|
||||||
Obj = #{action => Action,
|
Obj = #{
|
||||||
name_vsn => NameVsn},
|
action => Action,
|
||||||
|
name_vsn => NameVsn
|
||||||
|
},
|
||||||
JsonReady =
|
JsonReady =
|
||||||
case Res of
|
case Res of
|
||||||
ok ->
|
ok ->
|
||||||
Obj#{result => ok};
|
Obj#{result => ok};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
Obj#{result => not_ok,
|
Obj#{
|
||||||
cause => Reason}
|
result => not_ok,
|
||||||
|
cause => Reason
|
||||||
|
}
|
||||||
end,
|
end,
|
||||||
LogFun("~ts~n", [to_json(JsonReady)]).
|
LogFun("~ts~n", [to_json(JsonReady)]).
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
, namespace/0
|
fields/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include("emqx_plugins.hrl").
|
-include("emqx_plugins.hrl").
|
||||||
|
|
@ -31,31 +32,41 @@ namespace() -> "plugin".
|
||||||
roots() -> [?CONF_ROOT].
|
roots() -> [?CONF_ROOT].
|
||||||
|
|
||||||
fields(?CONF_ROOT) ->
|
fields(?CONF_ROOT) ->
|
||||||
#{fields => root_fields(),
|
#{
|
||||||
desc => ?DESC(?CONF_ROOT)
|
fields => root_fields(),
|
||||||
};
|
desc => ?DESC(?CONF_ROOT)
|
||||||
|
};
|
||||||
fields(state) ->
|
fields(state) ->
|
||||||
#{ fields => state_fields(),
|
#{
|
||||||
desc => ?DESC(state)
|
fields => state_fields(),
|
||||||
}.
|
desc => ?DESC(state)
|
||||||
|
}.
|
||||||
|
|
||||||
state_fields() ->
|
state_fields() ->
|
||||||
[ {name_vsn,
|
[
|
||||||
hoconsc:mk(string(),
|
{name_vsn,
|
||||||
#{ desc => ?DESC(name_vsn)
|
hoconsc:mk(
|
||||||
, required => true
|
string(),
|
||||||
})}
|
#{
|
||||||
, {enable,
|
desc => ?DESC(name_vsn),
|
||||||
hoconsc:mk(boolean(),
|
required => true
|
||||||
#{ desc => ?DESC(enable)
|
}
|
||||||
, required => true
|
)},
|
||||||
})}
|
{enable,
|
||||||
|
hoconsc:mk(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
desc => ?DESC(enable),
|
||||||
|
required => true
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
root_fields() ->
|
root_fields() ->
|
||||||
[ {states, fun states/1}
|
[
|
||||||
, {install_dir, fun install_dir/1}
|
{states, fun states/1},
|
||||||
, {check_interval, fun check_interval/1}
|
{install_dir, fun install_dir/1},
|
||||||
|
{check_interval, fun check_interval/1}
|
||||||
].
|
].
|
||||||
|
|
||||||
states(type) -> hoconsc:array(hoconsc:ref(?MODULE, state));
|
states(type) -> hoconsc:array(hoconsc:ref(?MODULE, state));
|
||||||
|
|
@ -66,7 +77,8 @@ states(_) -> undefined.
|
||||||
|
|
||||||
install_dir(type) -> string();
|
install_dir(type) -> string();
|
||||||
install_dir(required) -> false;
|
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(T) when T =/= desc -> undefined;
|
||||||
install_dir(desc) -> ?DESC(install_dir).
|
install_dir(desc) -> ?DESC(install_dir).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ init([]) ->
|
||||||
%% TODO: Add monitor plugins change.
|
%% TODO: Add monitor plugins change.
|
||||||
Monitor = emqx_plugins_monitor,
|
Monitor = emqx_plugins_monitor,
|
||||||
_Children = [
|
_Children = [
|
||||||
#{id => Monitor,
|
#{
|
||||||
|
id => Monitor,
|
||||||
start => {Monitor, start_link, []},
|
start => {Monitor, start_link, []},
|
||||||
restart => permanent,
|
restart => permanent,
|
||||||
shutdown => brutal_kill,
|
shutdown => brutal_kill,
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,12 @@ end_per_suite(Config) ->
|
||||||
|
|
||||||
init_per_testcase(TestCase, Config) ->
|
init_per_testcase(TestCase, Config) ->
|
||||||
emqx_plugins:put_configured([]),
|
emqx_plugins:put_configured([]),
|
||||||
lists:foreach(fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
|
lists:foreach(
|
||||||
emqx_plugins:purge(bin([Name, "-", Vsn]))
|
fun(#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
|
||||||
end, emqx_plugins:list()),
|
emqx_plugins:purge(bin([Name, "-", Vsn]))
|
||||||
|
end,
|
||||||
|
emqx_plugins:list()
|
||||||
|
),
|
||||||
?MODULE:TestCase({init, Config}).
|
?MODULE:TestCase({init, Config}).
|
||||||
|
|
||||||
end_per_testcase(TestCase, Config) ->
|
end_per_testcase(TestCase, Config) ->
|
||||||
|
|
@ -59,35 +62,46 @@ end_per_testcase(TestCase, Config) ->
|
||||||
|
|
||||||
build_demo_plugin_package() ->
|
build_demo_plugin_package() ->
|
||||||
build_demo_plugin_package(
|
build_demo_plugin_package(
|
||||||
#{ target_path => "_build/default/emqx_plugrel"
|
#{
|
||||||
, release_name => "emqx_plugin_template"
|
target_path => "_build/default/emqx_plugrel",
|
||||||
, git_url => "https://github.com/emqx/emqx-plugin-template.git"
|
release_name => "emqx_plugin_template",
|
||||||
, vsn => ?EMQX_PLUGIN_TEMPLATE_VSN
|
git_url => "https://github.com/emqx/emqx-plugin-template.git",
|
||||||
, workdir => "demo_src"
|
vsn => ?EMQX_PLUGIN_TEMPLATE_VSN,
|
||||||
, shdir => emqx_plugins:install_dir()
|
workdir => "demo_src",
|
||||||
}).
|
shdir => emqx_plugins:install_dir()
|
||||||
|
}
|
||||||
|
).
|
||||||
|
|
||||||
build_demo_plugin_package(#{ target_path := TargetPath
|
build_demo_plugin_package(
|
||||||
, release_name := ReleaseName
|
#{
|
||||||
, git_url := GitUrl
|
target_path := TargetPath,
|
||||||
, vsn := PluginVsn
|
release_name := ReleaseName,
|
||||||
, workdir := DemoWorkDir
|
git_url := GitUrl,
|
||||||
, shdir := WorkDir
|
vsn := PluginVsn,
|
||||||
} = Opts) ->
|
workdir := DemoWorkDir,
|
||||||
|
shdir := WorkDir
|
||||||
|
} = Opts
|
||||||
|
) ->
|
||||||
BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]),
|
BuildSh = filename:join([WorkDir, "build-demo-plugin.sh"]),
|
||||||
Cmd = string:join([ BuildSh
|
Cmd = string:join(
|
||||||
, PluginVsn
|
[
|
||||||
, TargetPath
|
BuildSh,
|
||||||
, ReleaseName
|
PluginVsn,
|
||||||
, GitUrl
|
TargetPath,
|
||||||
, DemoWorkDir
|
ReleaseName,
|
||||||
],
|
GitUrl,
|
||||||
" "),
|
DemoWorkDir
|
||||||
|
],
|
||||||
|
" "
|
||||||
|
),
|
||||||
case emqx_run_sh:do(Cmd, [{cd, WorkDir}]) of
|
case emqx_run_sh:do(Cmd, [{cd, WorkDir}]) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
Pkg = filename:join([WorkDir, ReleaseName ++ "-" ++
|
Pkg = filename:join([
|
||||||
PluginVsn ++
|
WorkDir,
|
||||||
?PACKAGE_SUFFIX]),
|
ReleaseName ++ "-" ++
|
||||||
|
PluginVsn ++
|
||||||
|
?PACKAGE_SUFFIX
|
||||||
|
]),
|
||||||
case filelib:is_regular(Pkg) of
|
case filelib:is_regular(Pkg) of
|
||||||
true -> Opts#{package => Pkg};
|
true -> Opts#{package => Pkg};
|
||||||
false -> error(#{reason => unexpected_build_result, not_found => 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}) ->
|
t_demo_install_start_stop_uninstall({init, Config}) ->
|
||||||
Opts = #{package := Package} = build_demo_plugin_package(),
|
Opts = #{package := Package} = build_demo_plugin_package(),
|
||||||
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
||||||
[ {name_vsn, NameVsn}
|
[
|
||||||
, {plugin_opts, Opts}
|
{name_vsn, NameVsn},
|
||||||
| Config
|
{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) ->
|
t_demo_install_start_stop_uninstall(Config) ->
|
||||||
NameVsn = proplists:get_value(name_vsn, Config),
|
NameVsn = proplists:get_value(name_vsn, Config),
|
||||||
#{ release_name := ReleaseName
|
#{
|
||||||
, vsn := PluginVsn
|
release_name := ReleaseName,
|
||||||
} = proplists:get_value(plugin_opts, Config),
|
vsn := PluginVsn
|
||||||
|
} = proplists:get_value(plugin_opts, Config),
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
%% idempotent
|
%% idempotent
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
|
|
@ -129,8 +146,10 @@ t_demo_install_start_stop_uninstall(Config) ->
|
||||||
ok = assert_app_running(map_sets, true),
|
ok = assert_app_running(map_sets, true),
|
||||||
|
|
||||||
%% running app can not be un-installed
|
%% running app can not be un-installed
|
||||||
?assertMatch({error, _},
|
?assertMatch(
|
||||||
emqx_plugins:ensure_uninstalled(NameVsn)),
|
{error, _},
|
||||||
|
emqx_plugins:ensure_uninstalled(NameVsn)
|
||||||
|
),
|
||||||
|
|
||||||
%% stop
|
%% stop
|
||||||
ok = emqx_plugins:ensure_stopped(NameVsn),
|
ok = emqx_plugins:ensure_stopped(NameVsn),
|
||||||
|
|
@ -143,9 +162,15 @@ t_demo_install_start_stop_uninstall(Config) ->
|
||||||
%% still listed after stopped
|
%% still listed after stopped
|
||||||
ReleaseNameBin = list_to_binary(ReleaseName),
|
ReleaseNameBin = list_to_binary(ReleaseName),
|
||||||
PluginVsnBin = list_to_binary(PluginVsn),
|
PluginVsnBin = list_to_binary(PluginVsn),
|
||||||
?assertMatch([#{<<"name">> := ReleaseNameBin,
|
?assertMatch(
|
||||||
<<"rel_vsn">> := PluginVsnBin
|
[
|
||||||
}], emqx_plugins:list()),
|
#{
|
||||||
|
<<"name">> := ReleaseNameBin,
|
||||||
|
<<"rel_vsn">> := PluginVsnBin
|
||||||
|
}
|
||||||
|
],
|
||||||
|
emqx_plugins:list()
|
||||||
|
),
|
||||||
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
||||||
?assertEqual([], emqx_plugins:list()),
|
?assertEqual([], emqx_plugins:list()),
|
||||||
ok.
|
ok.
|
||||||
|
|
@ -164,23 +189,29 @@ t_position({init, Config}) ->
|
||||||
#{package := Package} = build_demo_plugin_package(),
|
#{package := Package} = build_demo_plugin_package(),
|
||||||
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
||||||
[{name_vsn, NameVsn} | Config];
|
[{name_vsn, NameVsn} | Config];
|
||||||
t_position({'end', _Config}) -> ok;
|
t_position({'end', _Config}) ->
|
||||||
|
ok;
|
||||||
t_position(Config) ->
|
t_position(Config) ->
|
||||||
NameVsn = proplists:get_value(name_vsn, Config),
|
NameVsn = proplists:get_value(name_vsn, Config),
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
ok = emqx_plugins:ensure_enabled(NameVsn),
|
ok = emqx_plugins:ensure_enabled(NameVsn),
|
||||||
FakeInfo = "name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
|
FakeInfo =
|
||||||
"description=\"desc fake position app\"",
|
"name=position, rel_vsn=\"2\", rel_apps=[\"position-9\"],"
|
||||||
|
"description=\"desc fake position app\"",
|
||||||
PosApp2 = <<"position-2">>,
|
PosApp2 = <<"position-2">>,
|
||||||
ok = write_info_file(Config, PosApp2, FakeInfo),
|
ok = write_info_file(Config, PosApp2, FakeInfo),
|
||||||
%% fake a disabled plugin in config
|
%% fake a disabled plugin in config
|
||||||
ok = emqx_plugins:ensure_state(PosApp2, {before, NameVsn}, false),
|
ok = emqx_plugins:ensure_state(PosApp2, {before, NameVsn}, false),
|
||||||
ListFun = fun() ->
|
ListFun = fun() ->
|
||||||
lists:map(fun(
|
lists:map(
|
||||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn}) ->
|
fun(
|
||||||
<<Name/binary, "-", Vsn/binary>>
|
#{<<"name">> := Name, <<"rel_vsn">> := Vsn}
|
||||||
end, emqx_plugins:list())
|
) ->
|
||||||
end,
|
<<Name/binary, "-", Vsn/binary>>
|
||||||
|
end,
|
||||||
|
emqx_plugins:list()
|
||||||
|
)
|
||||||
|
end,
|
||||||
?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
|
?assertEqual([PosApp2, list_to_binary(NameVsn)], ListFun()),
|
||||||
emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
|
emqx_plugins:ensure_enabled(PosApp2, {behind, NameVsn}),
|
||||||
?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
|
?assertEqual([list_to_binary(NameVsn), PosApp2], ListFun()),
|
||||||
|
|
@ -197,13 +228,15 @@ t_start_restart_and_stop({init, Config}) ->
|
||||||
#{package := Package} = build_demo_plugin_package(),
|
#{package := Package} = build_demo_plugin_package(),
|
||||||
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
||||||
[{name_vsn, NameVsn} | Config];
|
[{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) ->
|
t_start_restart_and_stop(Config) ->
|
||||||
NameVsn = proplists:get_value(name_vsn, Config),
|
NameVsn = proplists:get_value(name_vsn, Config),
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
ok = emqx_plugins:ensure_enabled(NameVsn),
|
ok = emqx_plugins:ensure_enabled(NameVsn),
|
||||||
FakeInfo = "name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"],"
|
FakeInfo =
|
||||||
"description=\"desc bar\"",
|
"name=bar, rel_vsn=\"2\", rel_apps=[\"bar-9\"],"
|
||||||
|
"description=\"desc bar\"",
|
||||||
Bar2 = <<"bar-2">>,
|
Bar2 = <<"bar-2">>,
|
||||||
ok = write_info_file(Config, Bar2, FakeInfo),
|
ok = write_info_file(Config, Bar2, FakeInfo),
|
||||||
%% fake a disabled plugin in config
|
%% fake a disabled plugin in config
|
||||||
|
|
@ -216,8 +249,10 @@ t_start_restart_and_stop(Config) ->
|
||||||
%% fake enable bar-2
|
%% fake enable bar-2
|
||||||
ok = emqx_plugins:ensure_state(Bar2, rear, true),
|
ok = emqx_plugins:ensure_state(Bar2, rear, true),
|
||||||
%% should cause an error
|
%% should cause an error
|
||||||
?assertError(#{function := _, errors := [_ | _]},
|
?assertError(
|
||||||
emqx_plugins:ensure_started()),
|
#{function := _, errors := [_ | _]},
|
||||||
|
emqx_plugins:ensure_started()
|
||||||
|
),
|
||||||
%% but demo plugin should still be running
|
%% but demo plugin should still be running
|
||||||
assert_app_running(emqx_plugin_template, true),
|
assert_app_running(emqx_plugin_template, true),
|
||||||
|
|
||||||
|
|
@ -255,9 +290,13 @@ t_enable_disable(Config) ->
|
||||||
?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()),
|
?assertEqual([#{name_vsn => NameVsn, enable => false}], emqx_plugins:configured()),
|
||||||
ok = emqx_plugins:ensure_enabled(bin(NameVsn)),
|
ok = emqx_plugins:ensure_enabled(bin(NameVsn)),
|
||||||
?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
|
?assertEqual([#{name_vsn => NameVsn, enable => true}], emqx_plugins:configured()),
|
||||||
?assertMatch({error, #{reason := "bad_plugin_config_status",
|
?assertMatch(
|
||||||
hint := "disable_the_plugin_first"
|
{error, #{
|
||||||
}}, emqx_plugins:ensure_uninstalled(NameVsn)),
|
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_disabled(bin(NameVsn)),
|
||||||
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
||||||
?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)),
|
?assertMatch({error, _}, emqx_plugins:ensure_enabled(NameVsn)),
|
||||||
|
|
@ -271,20 +310,28 @@ assert_app_running(Name, false) ->
|
||||||
AllApps = application:which_applications(),
|
AllApps = application:which_applications(),
|
||||||
?assertEqual(false, lists:keyfind(Name, 1, AllApps)).
|
?assertEqual(false, lists:keyfind(Name, 1, AllApps)).
|
||||||
|
|
||||||
t_bad_tar_gz({init, Config}) -> Config;
|
t_bad_tar_gz({init, Config}) ->
|
||||||
t_bad_tar_gz({'end', _Config}) -> ok;
|
Config;
|
||||||
|
t_bad_tar_gz({'end', _Config}) ->
|
||||||
|
ok;
|
||||||
t_bad_tar_gz(Config) ->
|
t_bad_tar_gz(Config) ->
|
||||||
WorkDir = proplists:get_value(data_dir, Config),
|
WorkDir = proplists:get_value(data_dir, Config),
|
||||||
FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]),
|
FakeTarTz = filename:join([WorkDir, "fake-vsn.tar.gz"]),
|
||||||
ok = file:write_file(FakeTarTz, "a\n"),
|
ok = file:write_file(FakeTarTz, "a\n"),
|
||||||
?assertMatch({error, #{reason := "bad_plugin_package",
|
?assertMatch(
|
||||||
return := eof
|
{error, #{
|
||||||
}},
|
reason := "bad_plugin_package",
|
||||||
emqx_plugins:ensure_installed("fake-vsn")),
|
return := eof
|
||||||
?assertMatch({error, #{reason := "failed_to_extract_plugin_package",
|
}},
|
||||||
return := not_found
|
emqx_plugins:ensure_installed("fake-vsn")
|
||||||
}},
|
),
|
||||||
emqx_plugins:ensure_installed("nonexisting")),
|
?assertMatch(
|
||||||
|
{error, #{
|
||||||
|
reason := "failed_to_extract_plugin_package",
|
||||||
|
return := not_found
|
||||||
|
}},
|
||||||
|
emqx_plugins:ensure_installed("nonexisting")
|
||||||
|
),
|
||||||
?assertEqual([], emqx_plugins:list()),
|
?assertEqual([], emqx_plugins:list()),
|
||||||
ok = emqx_plugins:delete_package("fake-vsn"),
|
ok = emqx_plugins:delete_package("fake-vsn"),
|
||||||
%% idempotent
|
%% idempotent
|
||||||
|
|
@ -292,8 +339,10 @@ t_bad_tar_gz(Config) ->
|
||||||
|
|
||||||
%% create a corrupted .tar.gz
|
%% create a corrupted .tar.gz
|
||||||
%% failed install attempts should not leave behind extracted dir
|
%% failed install attempts should not leave behind extracted dir
|
||||||
t_bad_tar_gz2({init, Config}) -> Config;
|
t_bad_tar_gz2({init, Config}) ->
|
||||||
t_bad_tar_gz2({'end', _Config}) -> ok;
|
Config;
|
||||||
|
t_bad_tar_gz2({'end', _Config}) ->
|
||||||
|
ok;
|
||||||
t_bad_tar_gz2(Config) ->
|
t_bad_tar_gz2(Config) ->
|
||||||
WorkDir = proplists:get_value(data_dir, Config),
|
WorkDir = proplists:get_value(data_dir, Config),
|
||||||
NameVsn = "foo-0.2",
|
NameVsn = "foo-0.2",
|
||||||
|
|
@ -310,45 +359,57 @@ t_bad_tar_gz2(Config) ->
|
||||||
?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
|
?assertEqual({error, enoent}, file:read_file_info(emqx_plugins:dir(NameVsn))),
|
||||||
ok = emqx_plugins:delete_package(NameVsn).
|
ok = emqx_plugins:delete_package(NameVsn).
|
||||||
|
|
||||||
t_bad_info_json({init, Config}) -> Config;
|
t_bad_info_json({init, Config}) ->
|
||||||
t_bad_info_json({'end', _}) -> ok;
|
Config;
|
||||||
|
t_bad_info_json({'end', _}) ->
|
||||||
|
ok;
|
||||||
t_bad_info_json(Config) ->
|
t_bad_info_json(Config) ->
|
||||||
NameVsn = "test-2",
|
NameVsn = "test-2",
|
||||||
ok = write_info_file(Config, NameVsn, "bad-syntax"),
|
ok = write_info_file(Config, NameVsn, "bad-syntax"),
|
||||||
?assertMatch({error, #{error := "bad_info_file",
|
?assertMatch(
|
||||||
return := {parse_error, _}
|
{error, #{
|
||||||
}},
|
error := "bad_info_file",
|
||||||
emqx_plugins:describe(NameVsn)),
|
return := {parse_error, _}
|
||||||
|
}},
|
||||||
|
emqx_plugins:describe(NameVsn)
|
||||||
|
),
|
||||||
ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
|
ok = write_info_file(Config, NameVsn, "{\"bad\": \"obj\"}"),
|
||||||
?assertMatch({error, #{error := "bad_info_file_content",
|
?assertMatch(
|
||||||
mandatory_fields := _
|
{error, #{
|
||||||
}},
|
error := "bad_info_file_content",
|
||||||
emqx_plugins:describe(NameVsn)),
|
mandatory_fields := _
|
||||||
|
}},
|
||||||
|
emqx_plugins:describe(NameVsn)
|
||||||
|
),
|
||||||
?assertEqual([], emqx_plugins:list()),
|
?assertEqual([], emqx_plugins:list()),
|
||||||
emqx_plugins:purge(NameVsn),
|
emqx_plugins:purge(NameVsn),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_elixir_plugin({init, Config}) ->
|
t_elixir_plugin({init, Config}) ->
|
||||||
Opts0 =
|
Opts0 =
|
||||||
#{ target_path => "_build/prod/plugrelex/elixir_plugin_template"
|
#{
|
||||||
, release_name => "elixir_plugin_template"
|
target_path => "_build/prod/plugrelex/elixir_plugin_template",
|
||||||
, git_url => "https://github.com/emqx/emqx-elixir-plugin.git"
|
release_name => "elixir_plugin_template",
|
||||||
, vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN
|
git_url => "https://github.com/emqx/emqx-elixir-plugin.git",
|
||||||
, workdir => "demo_src_elixir"
|
vsn => ?EMQX_ELIXIR_PLUGIN_TEMPLATE_VSN,
|
||||||
, shdir => emqx_plugins:install_dir()
|
workdir => "demo_src_elixir",
|
||||||
},
|
shdir => emqx_plugins:install_dir()
|
||||||
|
},
|
||||||
Opts = #{package := Package} = build_demo_plugin_package(Opts0),
|
Opts = #{package := Package} = build_demo_plugin_package(Opts0),
|
||||||
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
NameVsn = filename:basename(Package, ?PACKAGE_SUFFIX),
|
||||||
[ {name_vsn, NameVsn}
|
[
|
||||||
, {plugin_opts, Opts}
|
{name_vsn, NameVsn},
|
||||||
| Config
|
{plugin_opts, Opts}
|
||||||
|
| Config
|
||||||
];
|
];
|
||||||
t_elixir_plugin({'end', _Config}) -> ok;
|
t_elixir_plugin({'end', _Config}) ->
|
||||||
|
ok;
|
||||||
t_elixir_plugin(Config) ->
|
t_elixir_plugin(Config) ->
|
||||||
NameVsn = proplists:get_value(name_vsn, Config),
|
NameVsn = proplists:get_value(name_vsn, Config),
|
||||||
#{ release_name := ReleaseName
|
#{
|
||||||
, vsn := PluginVsn
|
release_name := ReleaseName,
|
||||||
} = proplists:get_value(plugin_opts, Config),
|
vsn := PluginVsn
|
||||||
|
} = proplists:get_value(plugin_opts, Config),
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
%% idempotent
|
%% idempotent
|
||||||
ok = emqx_plugins:ensure_installed(NameVsn),
|
ok = emqx_plugins:ensure_installed(NameVsn),
|
||||||
|
|
@ -368,8 +429,10 @@ t_elixir_plugin(Config) ->
|
||||||
3 = 'Elixir.Kernel':'+'(1, 2),
|
3 = 'Elixir.Kernel':'+'(1, 2),
|
||||||
|
|
||||||
%% running app can not be un-installed
|
%% running app can not be un-installed
|
||||||
?assertMatch({error, _},
|
?assertMatch(
|
||||||
emqx_plugins:ensure_uninstalled(NameVsn)),
|
{error, _},
|
||||||
|
emqx_plugins:ensure_uninstalled(NameVsn)
|
||||||
|
),
|
||||||
|
|
||||||
%% stop
|
%% stop
|
||||||
ok = emqx_plugins:ensure_stopped(NameVsn),
|
ok = emqx_plugins:ensure_stopped(NameVsn),
|
||||||
|
|
@ -382,9 +445,15 @@ t_elixir_plugin(Config) ->
|
||||||
%% still listed after stopped
|
%% still listed after stopped
|
||||||
ReleaseNameBin = list_to_binary(ReleaseName),
|
ReleaseNameBin = list_to_binary(ReleaseName),
|
||||||
PluginVsnBin = list_to_binary(PluginVsn),
|
PluginVsnBin = list_to_binary(PluginVsn),
|
||||||
?assertMatch([#{<<"name">> := ReleaseNameBin,
|
?assertMatch(
|
||||||
<<"rel_vsn">> := PluginVsnBin
|
[
|
||||||
}], emqx_plugins:list()),
|
#{
|
||||||
|
<<"name">> := ReleaseNameBin,
|
||||||
|
<<"rel_vsn">> := PluginVsnBin
|
||||||
|
}
|
||||||
|
],
|
||||||
|
emqx_plugins:list()
|
||||||
|
),
|
||||||
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
ok = emqx_plugins:ensure_uninstalled(NameVsn),
|
||||||
?assertEqual([], emqx_plugins:list()),
|
?assertEqual([], emqx_plugins:list()),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,26 @@
|
||||||
|
|
||||||
ensure_configured_test_todo() ->
|
ensure_configured_test_todo() ->
|
||||||
meck_emqx(),
|
meck_emqx(),
|
||||||
try test_ensure_configured()
|
try
|
||||||
after emqx_plugins:put_configured([])
|
test_ensure_configured()
|
||||||
|
after
|
||||||
|
emqx_plugins:put_configured([])
|
||||||
end,
|
end,
|
||||||
meck:unload(emqx).
|
meck:unload(emqx).
|
||||||
|
|
||||||
|
|
||||||
test_ensure_configured() ->
|
test_ensure_configured() ->
|
||||||
ok = emqx_plugins:put_configured([]),
|
ok = emqx_plugins:put_configured([]),
|
||||||
P1 =#{name_vsn => "p-1", enable => true},
|
P1 = #{name_vsn => "p-1", enable => true},
|
||||||
P2 =#{name_vsn => "p-2", enable => true},
|
P2 = #{name_vsn => "p-2", enable => true},
|
||||||
P3 =#{name_vsn => "p-3", enable => false},
|
P3 = #{name_vsn => "p-3", enable => false},
|
||||||
emqx_plugins:ensure_configured(P1, front),
|
emqx_plugins:ensure_configured(P1, front),
|
||||||
emqx_plugins:ensure_configured(P2, {before, <<"p-1">>}),
|
emqx_plugins:ensure_configured(P2, {before, <<"p-1">>}),
|
||||||
emqx_plugins:ensure_configured(P3, {before, <<"p-1">>}),
|
emqx_plugins:ensure_configured(P3, {before, <<"p-1">>}),
|
||||||
?assertEqual([P2, P3, P1], emqx_plugins:configured()),
|
?assertEqual([P2, P3, P1], emqx_plugins:configured()),
|
||||||
?assertThrow(#{error := "position_anchor_plugin_not_configured"},
|
?assertThrow(
|
||||||
emqx_plugins:ensure_configured(P3, {before, <<"unknown-x">>})).
|
#{error := "position_anchor_plugin_not_configured"},
|
||||||
|
emqx_plugins:ensure_configured(P3, {before, <<"unknown-x">>})
|
||||||
|
).
|
||||||
|
|
||||||
read_plugin_test() ->
|
read_plugin_test() ->
|
||||||
meck_emqx(),
|
meck_emqx(),
|
||||||
|
|
@ -47,16 +50,20 @@ read_plugin_test() ->
|
||||||
fun(_Dir) ->
|
fun(_Dir) ->
|
||||||
NameVsn = "bar-5",
|
NameVsn = "bar-5",
|
||||||
InfoFile = emqx_plugins:info_file(NameVsn),
|
InfoFile = emqx_plugins:info_file(NameVsn),
|
||||||
FakeInfo = "name=bar, rel_vsn=\"5\", rel_apps=[justname_no_vsn],"
|
FakeInfo =
|
||||||
"description=\"desc bar\"",
|
"name=bar, rel_vsn=\"5\", rel_apps=[justname_no_vsn],"
|
||||||
|
"description=\"desc bar\"",
|
||||||
try
|
try
|
||||||
ok = write_file(InfoFile, FakeInfo),
|
ok = write_file(InfoFile, FakeInfo),
|
||||||
?assertMatch({error, #{error := "bad_rel_apps"}},
|
?assertMatch(
|
||||||
emqx_plugins:read_plugin(NameVsn, #{}))
|
{error, #{error := "bad_rel_apps"}},
|
||||||
|
emqx_plugins:read_plugin(NameVsn, #{})
|
||||||
|
)
|
||||||
after
|
after
|
||||||
emqx_plugins:purge(NameVsn)
|
emqx_plugins:purge(NameVsn)
|
||||||
end
|
end
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
meck:unload(emqx).
|
meck:unload(emqx).
|
||||||
|
|
||||||
with_rand_install_dir(F) ->
|
with_rand_install_dir(F) ->
|
||||||
|
|
@ -91,7 +98,8 @@ delete_package_test() ->
|
||||||
Dir = File,
|
Dir = File,
|
||||||
ok = filelib:ensure_dir(filename:join([Dir, "foo"])),
|
ok = filelib:ensure_dir(filename:join([Dir, "foo"])),
|
||||||
?assertMatch({error, _}, emqx_plugins:delete_package("a-1"))
|
?assertMatch({error, _}, emqx_plugins:delete_package("a-1"))
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
meck:unload(emqx).
|
meck:unload(emqx).
|
||||||
|
|
||||||
%% purge plugin's install dir should mostly work and return ok
|
%% purge plugin's install dir should mostly work and return ok
|
||||||
|
|
@ -110,15 +118,19 @@ purge_test() ->
|
||||||
%% write a file for the dir path
|
%% write a file for the dir path
|
||||||
ok = file:write_file(Dir, "a"),
|
ok = file:write_file(Dir, "a"),
|
||||||
?assertEqual(ok, emqx_plugins:purge("a-1"))
|
?assertEqual(ok, emqx_plugins:purge("a-1"))
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
meck:unload(emqx).
|
meck:unload(emqx).
|
||||||
|
|
||||||
meck_emqx() ->
|
meck_emqx() ->
|
||||||
meck:new(emqx, [unstick, passthrough]),
|
meck:new(emqx, [unstick, passthrough]),
|
||||||
meck:expect(emqx, update_config,
|
meck:expect(
|
||||||
|
emqx,
|
||||||
|
update_config,
|
||||||
fun(Path, Values, _Opts) ->
|
fun(Path, Values, _Opts) ->
|
||||||
emqx_config:put(Path, Values)
|
emqx_config:put(Path, Values)
|
||||||
end),
|
end
|
||||||
|
),
|
||||||
%meck:expect(emqx, get_config,
|
%meck:expect(emqx, get_config,
|
||||||
% fun(KeyPath, Default) ->
|
% fun(KeyPath, Default) ->
|
||||||
% Map = emqx:get_raw_config(KeyPath, Default),
|
% Map = emqx:get_raw_config(KeyPath, Default),
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps,
|
{deps, [
|
||||||
[ {emqx, {path, "../emqx"}},
|
{emqx, {path, "../emqx"}},
|
||||||
%% FIXME: tag this as v3.1.3
|
%% FIXME: tag this as v3.1.3
|
||||||
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
|
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
|
||||||
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.4"}}}
|
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.4"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
{erl_opts, [warn_unused_vars,
|
{erl_opts, [
|
||||||
warn_shadow_vars,
|
warn_unused_vars,
|
||||||
warn_unused_import,
|
warn_shadow_vars,
|
||||||
warn_obsolete_guard,
|
warn_unused_import,
|
||||||
debug_info,
|
warn_obsolete_guard,
|
||||||
{parse_transform}]}.
|
debug_info,
|
||||||
|
{parse_transform}
|
||||||
|
]}.
|
||||||
|
|
||||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
{xref_checks, [
|
||||||
locals_not_used, deprecated_function_calls,
|
undefined_function_calls,
|
||||||
warnings_as_errors, deprecated_functions]}.
|
undefined_functions,
|
||||||
|
locals_not_used,
|
||||||
|
deprecated_function_calls,
|
||||||
|
warnings_as_errors,
|
||||||
|
deprecated_functions
|
||||||
|
]}.
|
||||||
{cover_enabled, true}.
|
{cover_enabled, true}.
|
||||||
{cover_opts, [verbose]}.
|
{cover_opts, [verbose]}.
|
||||||
{cover_export_enabled, true}.
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_prometheus,
|
{application, emqx_prometheus, [
|
||||||
[{description, "Prometheus for EMQX"},
|
{description, "Prometheus for EMQX"},
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
% strict semver, bump manually!
|
||||||
{modules, []},
|
{vsn, "5.0.0"},
|
||||||
{registered, [emqx_prometheus_sup]},
|
{modules, []},
|
||||||
{applications, [kernel,stdlib,prometheus,emqx]},
|
{registered, [emqx_prometheus_sup]},
|
||||||
{mod, {emqx_prometheus_app,[]}},
|
{applications, [kernel, stdlib, prometheus, emqx]},
|
||||||
{env, []},
|
{mod, {emqx_prometheus_app, []}},
|
||||||
{licenses, ["Apache-2.0"]},
|
{env, []},
|
||||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||||
{"Github", "https://github.com/emqx/emqx-prometheus"}
|
{links, [
|
||||||
]}
|
{"Homepage", "https://emqx.io/"},
|
||||||
]}.
|
{"Github", "https://github.com/emqx/emqx-prometheus"}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -28,38 +28,44 @@
|
||||||
-include_lib("prometheus/include/prometheus_model.hrl").
|
-include_lib("prometheus/include/prometheus_model.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-import(prometheus_model_helpers,
|
-import(
|
||||||
[ create_mf/5
|
prometheus_model_helpers,
|
||||||
, gauge_metric/1
|
[
|
||||||
, counter_metric/1
|
create_mf/5,
|
||||||
]).
|
gauge_metric/1,
|
||||||
|
counter_metric/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
-export([ update/1
|
-export([
|
||||||
, start/0
|
update/1,
|
||||||
, stop/0
|
start/0,
|
||||||
, restart/0
|
stop/0,
|
||||||
% for rpc
|
restart/0,
|
||||||
, do_start/0
|
% for rpc
|
||||||
, do_stop/0
|
do_start/0,
|
||||||
]).
|
do_stop/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([start_link/1]).
|
-export([start_link/1]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([ init/1
|
-export([
|
||||||
, handle_call/3
|
init/1,
|
||||||
, handle_cast/2
|
handle_call/3,
|
||||||
, handle_info/2
|
handle_cast/2,
|
||||||
, code_change/3
|
handle_info/2,
|
||||||
, terminate/2
|
code_change/3,
|
||||||
]).
|
terminate/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% prometheus_collector callback
|
%% prometheus_collector callback
|
||||||
-export([ deregister_cleanup/1
|
-export([
|
||||||
, collect_mf/2
|
deregister_cleanup/1,
|
||||||
, collect_metrics/2
|
collect_mf/2,
|
||||||
]).
|
collect_metrics/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([collect/1]).
|
-export([collect/1]).
|
||||||
|
|
||||||
|
|
@ -72,8 +78,13 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% update new config
|
%% update new config
|
||||||
update(Config) ->
|
update(Config) ->
|
||||||
case emqx_conf:update([prometheus], Config,
|
case
|
||||||
#{rawconf_with_defaults => true, override_to => cluster}) of
|
emqx_conf:update(
|
||||||
|
[prometheus],
|
||||||
|
Config,
|
||||||
|
#{rawconf_with_defaults => true, override_to => cluster}
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, #{raw_config := NewConfigRows}} ->
|
{ok, #{raw_config := NewConfigRows}} ->
|
||||||
case maps:get(<<"enable">>, Config, true) of
|
case maps:get(<<"enable">>, Config, true) of
|
||||||
true ->
|
true ->
|
||||||
|
|
@ -131,13 +142,12 @@ handle_call(_Msg, _From, State) ->
|
||||||
handle_cast(_Msg, State) ->
|
handle_cast(_Msg, State) ->
|
||||||
{noreply, 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()), "@"),
|
[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(),
|
Data = prometheus_text_format:format(),
|
||||||
httpc:request(post, {Url, [], "text/plain", Data}, [{autoredirect, true}], []),
|
httpc:request(post, {Url, [], "text/plain", Data}, [{autoredirect, true}], []),
|
||||||
{noreply, ensure_timer(State)};
|
{noreply, ensure_timer(State)};
|
||||||
|
|
||||||
handle_info(_Msg, State) ->
|
handle_info(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
@ -176,14 +186,15 @@ collect(<<"json">>) ->
|
||||||
Metrics = emqx_metrics:all(),
|
Metrics = emqx_metrics:all(),
|
||||||
Stats = emqx_stats:getstats(),
|
Stats = emqx_stats:getstats(),
|
||||||
VMData = emqx_vm_data(),
|
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()]),
|
stats => maps:from_list([collect_stats(Name, Stats) || Name <- emqx_stats()]),
|
||||||
packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]),
|
metrics => maps:from_list([collect_stats(Name, VMData) || Name <- emqx_vm()]),
|
||||||
messages => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]),
|
packets => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_packets()]),
|
||||||
delivery => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]),
|
messages => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_messages()]),
|
||||||
client => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_client()]),
|
delivery => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_delivery()]),
|
||||||
session => maps:from_list([collect_stats(Name, Metrics) || Name <- emqx_metrics_session()])};
|
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">>) ->
|
collect(<<"prometheus">>) ->
|
||||||
prometheus_text_format:format().
|
prometheus_text_format:format().
|
||||||
|
|
||||||
|
|
@ -219,13 +230,11 @@ emqx_collect(emqx_connections_count, Stats) ->
|
||||||
gauge_metric(?C('connections.count', Stats));
|
gauge_metric(?C('connections.count', Stats));
|
||||||
emqx_collect(emqx_connections_max, Stats) ->
|
emqx_collect(emqx_connections_max, Stats) ->
|
||||||
gauge_metric(?C('connections.max', Stats));
|
gauge_metric(?C('connections.max', Stats));
|
||||||
|
|
||||||
%% sessions
|
%% sessions
|
||||||
emqx_collect(emqx_sessions_count, Stats) ->
|
emqx_collect(emqx_sessions_count, Stats) ->
|
||||||
gauge_metric(?C('sessions.count', Stats));
|
gauge_metric(?C('sessions.count', Stats));
|
||||||
emqx_collect(emqx_sessions_max, Stats) ->
|
emqx_collect(emqx_sessions_max, Stats) ->
|
||||||
gauge_metric(?C('sessions.max', Stats));
|
gauge_metric(?C('sessions.max', Stats));
|
||||||
|
|
||||||
%% pub/sub stats
|
%% pub/sub stats
|
||||||
emqx_collect(emqx_topics_count, Stats) ->
|
emqx_collect(emqx_topics_count, Stats) ->
|
||||||
gauge_metric(?C('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));
|
gauge_metric(?C('subscriptions.shared.count', Stats));
|
||||||
emqx_collect(emqx_subscriptions_shared_max, Stats) ->
|
emqx_collect(emqx_subscriptions_shared_max, Stats) ->
|
||||||
gauge_metric(?C('subscriptions.shared.max', Stats));
|
gauge_metric(?C('subscriptions.shared.max', Stats));
|
||||||
|
|
||||||
%% retained
|
%% retained
|
||||||
emqx_collect(emqx_retained_count, Stats) ->
|
emqx_collect(emqx_retained_count, Stats) ->
|
||||||
gauge_metric(?C('retained.count', Stats));
|
gauge_metric(?C('retained.count', Stats));
|
||||||
emqx_collect(emqx_retained_max, Stats) ->
|
emqx_collect(emqx_retained_max, Stats) ->
|
||||||
gauge_metric(?C('retained.max', Stats));
|
gauge_metric(?C('retained.max', Stats));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Metrics - packets & bytes
|
%% Metrics - packets & bytes
|
||||||
|
|
||||||
|
|
@ -262,13 +269,11 @@ emqx_collect(emqx_bytes_received, Metrics) ->
|
||||||
counter_metric(?C('bytes.received', Metrics));
|
counter_metric(?C('bytes.received', Metrics));
|
||||||
emqx_collect(emqx_bytes_sent, Metrics) ->
|
emqx_collect(emqx_bytes_sent, Metrics) ->
|
||||||
counter_metric(?C('bytes.sent', Metrics));
|
counter_metric(?C('bytes.sent', Metrics));
|
||||||
|
|
||||||
%% received.sent
|
%% received.sent
|
||||||
emqx_collect(emqx_packets_received, Metrics) ->
|
emqx_collect(emqx_packets_received, Metrics) ->
|
||||||
counter_metric(?C('packets.received', Metrics));
|
counter_metric(?C('packets.received', Metrics));
|
||||||
emqx_collect(emqx_packets_sent, Metrics) ->
|
emqx_collect(emqx_packets_sent, Metrics) ->
|
||||||
counter_metric(?C('packets.sent', Metrics));
|
counter_metric(?C('packets.sent', Metrics));
|
||||||
|
|
||||||
%% connect
|
%% connect
|
||||||
emqx_collect(emqx_packets_connect, Metrics) ->
|
emqx_collect(emqx_packets_connect, Metrics) ->
|
||||||
counter_metric(?C('packets.connect.received', 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));
|
counter_metric(?C('packets.connack.error', Metrics));
|
||||||
emqx_collect(emqx_packets_connack_auth_error, Metrics) ->
|
emqx_collect(emqx_packets_connack_auth_error, Metrics) ->
|
||||||
counter_metric(?C('packets.connack.auth_error', Metrics));
|
counter_metric(?C('packets.connack.auth_error', Metrics));
|
||||||
|
|
||||||
%% sub.unsub
|
%% sub.unsub
|
||||||
emqx_collect(emqx_packets_subscribe_received, Metrics) ->
|
emqx_collect(emqx_packets_subscribe_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.unsubscribe.error', Metrics));
|
||||||
emqx_collect(emqx_packets_unsuback_sent, Metrics) ->
|
emqx_collect(emqx_packets_unsuback_sent, Metrics) ->
|
||||||
counter_metric(?C('packets.unsuback.sent', Metrics));
|
counter_metric(?C('packets.unsuback.sent', Metrics));
|
||||||
|
|
||||||
%% publish.puback
|
%% publish.puback
|
||||||
emqx_collect(emqx_packets_publish_received, Metrics) ->
|
emqx_collect(emqx_packets_publish_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.publish.auth_error', Metrics));
|
||||||
emqx_collect(emqx_packets_publish_dropped, Metrics) ->
|
emqx_collect(emqx_packets_publish_dropped, Metrics) ->
|
||||||
counter_metric(?C('packets.publish.dropped', Metrics));
|
counter_metric(?C('packets.publish.dropped', Metrics));
|
||||||
|
|
||||||
%% puback
|
%% puback
|
||||||
emqx_collect(emqx_packets_puback_received, Metrics) ->
|
emqx_collect(emqx_packets_puback_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.puback.inuse', Metrics));
|
||||||
emqx_collect(emqx_packets_puback_missed, Metrics) ->
|
emqx_collect(emqx_packets_puback_missed, Metrics) ->
|
||||||
counter_metric(?C('packets.puback.missed', Metrics));
|
counter_metric(?C('packets.puback.missed', Metrics));
|
||||||
|
|
||||||
%% pubrec
|
%% pubrec
|
||||||
emqx_collect(emqx_packets_pubrec_received, Metrics) ->
|
emqx_collect(emqx_packets_pubrec_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.pubrec.inuse', Metrics));
|
||||||
emqx_collect(emqx_packets_pubrec_missed, Metrics) ->
|
emqx_collect(emqx_packets_pubrec_missed, Metrics) ->
|
||||||
counter_metric(?C('packets.pubrec.missed', Metrics));
|
counter_metric(?C('packets.pubrec.missed', Metrics));
|
||||||
|
|
||||||
%% pubrel
|
%% pubrel
|
||||||
emqx_collect(emqx_packets_pubrel_received, Metrics) ->
|
emqx_collect(emqx_packets_pubrel_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.pubrel.sent', Metrics));
|
||||||
emqx_collect(emqx_packets_pubrel_missed, Metrics) ->
|
emqx_collect(emqx_packets_pubrel_missed, Metrics) ->
|
||||||
counter_metric(?C('packets.pubrel.missed', Metrics));
|
counter_metric(?C('packets.pubrel.missed', Metrics));
|
||||||
|
|
||||||
%% pubcomp
|
%% pubcomp
|
||||||
emqx_collect(emqx_packets_pubcomp_received, Metrics) ->
|
emqx_collect(emqx_packets_pubcomp_received, Metrics) ->
|
||||||
counter_metric(?C('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));
|
counter_metric(?C('packets.pubcomp.inuse', Metrics));
|
||||||
emqx_collect(emqx_packets_pubcomp_missed, Metrics) ->
|
emqx_collect(emqx_packets_pubcomp_missed, Metrics) ->
|
||||||
counter_metric(?C('packets.pubcomp.missed', Metrics));
|
counter_metric(?C('packets.pubcomp.missed', Metrics));
|
||||||
|
|
||||||
%% pingreq
|
%% pingreq
|
||||||
emqx_collect(emqx_packets_pingreq_received, Metrics) ->
|
emqx_collect(emqx_packets_pingreq_received, Metrics) ->
|
||||||
counter_metric(?C('packets.pingreq.received', Metrics));
|
counter_metric(?C('packets.pingreq.received', Metrics));
|
||||||
emqx_collect(emqx_packets_pingresp_sent, Metrics) ->
|
emqx_collect(emqx_packets_pingresp_sent, Metrics) ->
|
||||||
counter_metric(?C('packets.pingresp.sent', Metrics));
|
counter_metric(?C('packets.pingresp.sent', Metrics));
|
||||||
|
|
||||||
%% disconnect
|
%% disconnect
|
||||||
emqx_collect(emqx_packets_disconnect_received, Metrics) ->
|
emqx_collect(emqx_packets_disconnect_received, Metrics) ->
|
||||||
counter_metric(?C('packets.disconnect.received', Metrics));
|
counter_metric(?C('packets.disconnect.received', Metrics));
|
||||||
emqx_collect(emqx_packets_disconnect_sent, Metrics) ->
|
emqx_collect(emqx_packets_disconnect_sent, Metrics) ->
|
||||||
counter_metric(?C('packets.disconnect.sent', Metrics));
|
counter_metric(?C('packets.disconnect.sent', Metrics));
|
||||||
|
|
||||||
%% auth
|
%% auth
|
||||||
emqx_collect(emqx_packets_auth_received, Metrics) ->
|
emqx_collect(emqx_packets_auth_received, Metrics) ->
|
||||||
counter_metric(?C('packets.auth.received', Metrics));
|
counter_metric(?C('packets.auth.received', Metrics));
|
||||||
emqx_collect(emqx_packets_auth_sent, Metrics) ->
|
emqx_collect(emqx_packets_auth_sent, Metrics) ->
|
||||||
counter_metric(?C('packets.auth.sent', Metrics));
|
counter_metric(?C('packets.auth.sent', Metrics));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Metrics - messages
|
%% Metrics - messages
|
||||||
|
|
||||||
%% messages
|
%% messages
|
||||||
emqx_collect(emqx_messages_received, Metrics) ->
|
emqx_collect(emqx_messages_received, Metrics) ->
|
||||||
counter_metric(?C('messages.received', Metrics));
|
counter_metric(?C('messages.received', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_sent, Metrics) ->
|
emqx_collect(emqx_messages_sent, Metrics) ->
|
||||||
counter_metric(?C('messages.sent', Metrics));
|
counter_metric(?C('messages.sent', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_qos0_received, Metrics) ->
|
emqx_collect(emqx_messages_qos0_received, Metrics) ->
|
||||||
counter_metric(?C('messages.qos0.received', Metrics));
|
counter_metric(?C('messages.qos0.received', Metrics));
|
||||||
emqx_collect(emqx_messages_qos0_sent, Metrics) ->
|
emqx_collect(emqx_messages_qos0_sent, Metrics) ->
|
||||||
counter_metric(?C('messages.qos0.sent', Metrics));
|
counter_metric(?C('messages.qos0.sent', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_qos1_received, Metrics) ->
|
emqx_collect(emqx_messages_qos1_received, Metrics) ->
|
||||||
counter_metric(?C('messages.qos1.received', Metrics));
|
counter_metric(?C('messages.qos1.received', Metrics));
|
||||||
emqx_collect(emqx_messages_qos1_sent, Metrics) ->
|
emqx_collect(emqx_messages_qos1_sent, Metrics) ->
|
||||||
counter_metric(?C('messages.qos1.sent', Metrics));
|
counter_metric(?C('messages.qos1.sent', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_qos2_received, Metrics) ->
|
emqx_collect(emqx_messages_qos2_received, Metrics) ->
|
||||||
counter_metric(?C('messages.qos2.received', Metrics));
|
counter_metric(?C('messages.qos2.received', Metrics));
|
||||||
emqx_collect(emqx_messages_qos2_sent, Metrics) ->
|
emqx_collect(emqx_messages_qos2_sent, Metrics) ->
|
||||||
counter_metric(?C('messages.qos2.sent', Metrics));
|
counter_metric(?C('messages.qos2.sent', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_publish, Metrics) ->
|
emqx_collect(emqx_messages_publish, Metrics) ->
|
||||||
counter_metric(?C('messages.publish', Metrics));
|
counter_metric(?C('messages.publish', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_dropped, Metrics) ->
|
emqx_collect(emqx_messages_dropped, Metrics) ->
|
||||||
counter_metric(?C('messages.dropped', Metrics));
|
counter_metric(?C('messages.dropped', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_dropped_expired, Metrics) ->
|
emqx_collect(emqx_messages_dropped_expired, Metrics) ->
|
||||||
counter_metric(?C('messages.dropped.await_pubrel_timeout', Metrics));
|
counter_metric(?C('messages.dropped.await_pubrel_timeout', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_dropped_no_subscribers, Metrics) ->
|
emqx_collect(emqx_messages_dropped_no_subscribers, Metrics) ->
|
||||||
counter_metric(?C('messages.dropped.no_subscribers', Metrics));
|
counter_metric(?C('messages.dropped.no_subscribers', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_forward, Metrics) ->
|
emqx_collect(emqx_messages_forward, Metrics) ->
|
||||||
counter_metric(?C('messages.forward', Metrics));
|
counter_metric(?C('messages.forward', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_retained, Metrics) ->
|
emqx_collect(emqx_messages_retained, Metrics) ->
|
||||||
counter_metric(?C('messages.retained', Metrics));
|
counter_metric(?C('messages.retained', Metrics));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_delayed, Stats) ->
|
emqx_collect(emqx_messages_delayed, Stats) ->
|
||||||
counter_metric(?C('messages.delayed', Stats));
|
counter_metric(?C('messages.delayed', Stats));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_delivered, Stats) ->
|
emqx_collect(emqx_messages_delivered, Stats) ->
|
||||||
counter_metric(?C('messages.delivered', Stats));
|
counter_metric(?C('messages.delivered', Stats));
|
||||||
|
|
||||||
emqx_collect(emqx_messages_acked, Stats) ->
|
emqx_collect(emqx_messages_acked, Stats) ->
|
||||||
counter_metric(?C('messages.acked', Stats));
|
counter_metric(?C('messages.acked', Stats));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Metrics - delivery
|
%% Metrics - delivery
|
||||||
|
|
||||||
|
|
@ -432,7 +413,6 @@ emqx_collect(emqx_delivery_dropped_queue_full, Stats) ->
|
||||||
counter_metric(?C('delivery.dropped.queue_full', Stats));
|
counter_metric(?C('delivery.dropped.queue_full', Stats));
|
||||||
emqx_collect(emqx_delivery_dropped_expired, Stats) ->
|
emqx_collect(emqx_delivery_dropped_expired, Stats) ->
|
||||||
counter_metric(?C('delivery.dropped.expired', Stats));
|
counter_metric(?C('delivery.dropped.expired', Stats));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Metrics - client
|
%% Metrics - client
|
||||||
|
|
||||||
|
|
@ -450,7 +430,6 @@ emqx_collect(emqx_client_unsubscribe, Stats) ->
|
||||||
counter_metric(?C('client.unsubscribe', Stats));
|
counter_metric(?C('client.unsubscribe', Stats));
|
||||||
emqx_collect(emqx_client_disconnected, Stats) ->
|
emqx_collect(emqx_client_disconnected, Stats) ->
|
||||||
counter_metric(?C('client.disconnected', Stats));
|
counter_metric(?C('client.disconnected', Stats));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Metrics - session
|
%% Metrics - session
|
||||||
|
|
||||||
|
|
@ -464,31 +443,23 @@ emqx_collect(emqx_session_discarded, Stats) ->
|
||||||
counter_metric(?C('session.discarded', Stats));
|
counter_metric(?C('session.discarded', Stats));
|
||||||
emqx_collect(emqx_session_terminated, Stats) ->
|
emqx_collect(emqx_session_terminated, Stats) ->
|
||||||
counter_metric(?C('session.terminated', Stats));
|
counter_metric(?C('session.terminated', Stats));
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% VM
|
%% VM
|
||||||
|
|
||||||
emqx_collect(emqx_vm_cpu_use, VMData) ->
|
emqx_collect(emqx_vm_cpu_use, VMData) ->
|
||||||
gauge_metric(?C(cpu_use, VMData));
|
gauge_metric(?C(cpu_use, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_vm_cpu_idle, VMData) ->
|
emqx_collect(emqx_vm_cpu_idle, VMData) ->
|
||||||
gauge_metric(?C(cpu_idle, VMData));
|
gauge_metric(?C(cpu_idle, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_vm_run_queue, VMData) ->
|
emqx_collect(emqx_vm_run_queue, VMData) ->
|
||||||
gauge_metric(?C(run_queue, VMData));
|
gauge_metric(?C(run_queue, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_vm_process_messages_in_queues, VMData) ->
|
emqx_collect(emqx_vm_process_messages_in_queues, VMData) ->
|
||||||
gauge_metric(?C(process_total_messages, VMData));
|
gauge_metric(?C(process_total_messages, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_vm_total_memory, VMData) ->
|
emqx_collect(emqx_vm_total_memory, VMData) ->
|
||||||
gauge_metric(?C(total_memory, VMData));
|
gauge_metric(?C(total_memory, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_vm_used_memory, VMData) ->
|
emqx_collect(emqx_vm_used_memory, VMData) ->
|
||||||
gauge_metric(?C(used_memory, VMData));
|
gauge_metric(?C(used_memory, VMData));
|
||||||
|
|
||||||
emqx_collect(emqx_cluster_nodes_running, ClusterData) ->
|
emqx_collect(emqx_cluster_nodes_running, ClusterData) ->
|
||||||
gauge_metric(?C(nodes_running, ClusterData));
|
gauge_metric(?C(nodes_running, ClusterData));
|
||||||
|
|
||||||
emqx_collect(emqx_cluster_nodes_stopped, ClusterData) ->
|
emqx_collect(emqx_cluster_nodes_stopped, ClusterData) ->
|
||||||
gauge_metric(?C(nodes_stopped, ClusterData)).
|
gauge_metric(?C(nodes_stopped, ClusterData)).
|
||||||
|
|
||||||
|
|
@ -497,142 +468,157 @@ emqx_collect(emqx_cluster_nodes_stopped, ClusterData) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
emqx_stats() ->
|
emqx_stats() ->
|
||||||
[ emqx_connections_count
|
[
|
||||||
, emqx_connections_max
|
emqx_connections_count,
|
||||||
, emqx_sessions_count
|
emqx_connections_max,
|
||||||
, emqx_sessions_max
|
emqx_sessions_count,
|
||||||
, emqx_topics_count
|
emqx_sessions_max,
|
||||||
, emqx_topics_max
|
emqx_topics_count,
|
||||||
, emqx_suboptions_count
|
emqx_topics_max,
|
||||||
, emqx_suboptions_max
|
emqx_suboptions_count,
|
||||||
, emqx_subscribers_count
|
emqx_suboptions_max,
|
||||||
, emqx_subscribers_max
|
emqx_subscribers_count,
|
||||||
, emqx_subscriptions_count
|
emqx_subscribers_max,
|
||||||
, emqx_subscriptions_max
|
emqx_subscriptions_count,
|
||||||
, emqx_subscriptions_shared_count
|
emqx_subscriptions_max,
|
||||||
, emqx_subscriptions_shared_max
|
emqx_subscriptions_shared_count,
|
||||||
, emqx_retained_count
|
emqx_subscriptions_shared_max,
|
||||||
, emqx_retained_max
|
emqx_retained_count,
|
||||||
|
emqx_retained_max
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_metrics_packets() ->
|
emqx_metrics_packets() ->
|
||||||
[ emqx_bytes_received
|
[
|
||||||
, emqx_bytes_sent
|
emqx_bytes_received,
|
||||||
, emqx_packets_received
|
emqx_bytes_sent,
|
||||||
, emqx_packets_sent
|
emqx_packets_received,
|
||||||
, emqx_packets_connect
|
emqx_packets_sent,
|
||||||
, emqx_packets_connack_sent
|
emqx_packets_connect,
|
||||||
, emqx_packets_connack_error
|
emqx_packets_connack_sent,
|
||||||
, emqx_packets_connack_auth_error
|
emqx_packets_connack_error,
|
||||||
, emqx_packets_publish_received
|
emqx_packets_connack_auth_error,
|
||||||
, emqx_packets_publish_sent
|
emqx_packets_publish_received,
|
||||||
, emqx_packets_publish_inuse
|
emqx_packets_publish_sent,
|
||||||
, emqx_packets_publish_error
|
emqx_packets_publish_inuse,
|
||||||
, emqx_packets_publish_auth_error
|
emqx_packets_publish_error,
|
||||||
, emqx_packets_publish_dropped
|
emqx_packets_publish_auth_error,
|
||||||
, emqx_packets_puback_received
|
emqx_packets_publish_dropped,
|
||||||
, emqx_packets_puback_sent
|
emqx_packets_puback_received,
|
||||||
, emqx_packets_puback_inuse
|
emqx_packets_puback_sent,
|
||||||
, emqx_packets_puback_missed
|
emqx_packets_puback_inuse,
|
||||||
, emqx_packets_pubrec_received
|
emqx_packets_puback_missed,
|
||||||
, emqx_packets_pubrec_sent
|
emqx_packets_pubrec_received,
|
||||||
, emqx_packets_pubrec_inuse
|
emqx_packets_pubrec_sent,
|
||||||
, emqx_packets_pubrec_missed
|
emqx_packets_pubrec_inuse,
|
||||||
, emqx_packets_pubrel_received
|
emqx_packets_pubrec_missed,
|
||||||
, emqx_packets_pubrel_sent
|
emqx_packets_pubrel_received,
|
||||||
, emqx_packets_pubrel_missed
|
emqx_packets_pubrel_sent,
|
||||||
, emqx_packets_pubcomp_received
|
emqx_packets_pubrel_missed,
|
||||||
, emqx_packets_pubcomp_sent
|
emqx_packets_pubcomp_received,
|
||||||
, emqx_packets_pubcomp_inuse
|
emqx_packets_pubcomp_sent,
|
||||||
, emqx_packets_pubcomp_missed
|
emqx_packets_pubcomp_inuse,
|
||||||
, emqx_packets_subscribe_received
|
emqx_packets_pubcomp_missed,
|
||||||
, emqx_packets_subscribe_error
|
emqx_packets_subscribe_received,
|
||||||
, emqx_packets_subscribe_auth_error
|
emqx_packets_subscribe_error,
|
||||||
, emqx_packets_suback_sent
|
emqx_packets_subscribe_auth_error,
|
||||||
, emqx_packets_unsubscribe_received
|
emqx_packets_suback_sent,
|
||||||
, emqx_packets_unsubscribe_error
|
emqx_packets_unsubscribe_received,
|
||||||
, emqx_packets_unsuback_sent
|
emqx_packets_unsubscribe_error,
|
||||||
, emqx_packets_pingreq_received
|
emqx_packets_unsuback_sent,
|
||||||
, emqx_packets_pingresp_sent
|
emqx_packets_pingreq_received,
|
||||||
, emqx_packets_disconnect_received
|
emqx_packets_pingresp_sent,
|
||||||
, emqx_packets_disconnect_sent
|
emqx_packets_disconnect_received,
|
||||||
, emqx_packets_auth_received
|
emqx_packets_disconnect_sent,
|
||||||
, emqx_packets_auth_sent
|
emqx_packets_auth_received,
|
||||||
|
emqx_packets_auth_sent
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_metrics_messages() ->
|
emqx_metrics_messages() ->
|
||||||
[ emqx_messages_received
|
[
|
||||||
, emqx_messages_sent
|
emqx_messages_received,
|
||||||
, emqx_messages_qos0_received
|
emqx_messages_sent,
|
||||||
, emqx_messages_qos0_sent
|
emqx_messages_qos0_received,
|
||||||
, emqx_messages_qos1_received
|
emqx_messages_qos0_sent,
|
||||||
, emqx_messages_qos1_sent
|
emqx_messages_qos1_received,
|
||||||
, emqx_messages_qos2_received
|
emqx_messages_qos1_sent,
|
||||||
, emqx_messages_qos2_sent
|
emqx_messages_qos2_received,
|
||||||
, emqx_messages_publish
|
emqx_messages_qos2_sent,
|
||||||
, emqx_messages_dropped
|
emqx_messages_publish,
|
||||||
, emqx_messages_dropped_expired
|
emqx_messages_dropped,
|
||||||
, emqx_messages_dropped_no_subscribers
|
emqx_messages_dropped_expired,
|
||||||
, emqx_messages_forward
|
emqx_messages_dropped_no_subscribers,
|
||||||
, emqx_messages_retained
|
emqx_messages_forward,
|
||||||
, emqx_messages_delayed
|
emqx_messages_retained,
|
||||||
, emqx_messages_delivered
|
emqx_messages_delayed,
|
||||||
, emqx_messages_acked
|
emqx_messages_delivered,
|
||||||
|
emqx_messages_acked
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_metrics_delivery() ->
|
emqx_metrics_delivery() ->
|
||||||
[ emqx_delivery_dropped
|
[
|
||||||
, emqx_delivery_dropped_no_local
|
emqx_delivery_dropped,
|
||||||
, emqx_delivery_dropped_too_large
|
emqx_delivery_dropped_no_local,
|
||||||
, emqx_delivery_dropped_qos0_msg
|
emqx_delivery_dropped_too_large,
|
||||||
, emqx_delivery_dropped_queue_full
|
emqx_delivery_dropped_qos0_msg,
|
||||||
, emqx_delivery_dropped_expired
|
emqx_delivery_dropped_queue_full,
|
||||||
|
emqx_delivery_dropped_expired
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_metrics_client() ->
|
emqx_metrics_client() ->
|
||||||
[ emqx_client_connected
|
[
|
||||||
, emqx_client_authenticate
|
emqx_client_connected,
|
||||||
, emqx_client_auth_anonymous
|
emqx_client_authenticate,
|
||||||
, emqx_client_authorize
|
emqx_client_auth_anonymous,
|
||||||
, emqx_client_subscribe
|
emqx_client_authorize,
|
||||||
, emqx_client_unsubscribe
|
emqx_client_subscribe,
|
||||||
, emqx_client_disconnected
|
emqx_client_unsubscribe,
|
||||||
|
emqx_client_disconnected
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_metrics_session() ->
|
emqx_metrics_session() ->
|
||||||
[ emqx_session_created
|
[
|
||||||
, emqx_session_resumed
|
emqx_session_created,
|
||||||
, emqx_session_takenover
|
emqx_session_resumed,
|
||||||
, emqx_session_discarded
|
emqx_session_takenover,
|
||||||
, emqx_session_terminated
|
emqx_session_discarded,
|
||||||
|
emqx_session_terminated
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_vm() ->
|
emqx_vm() ->
|
||||||
[ emqx_vm_cpu_use
|
[
|
||||||
, emqx_vm_cpu_idle
|
emqx_vm_cpu_use,
|
||||||
, emqx_vm_run_queue
|
emqx_vm_cpu_idle,
|
||||||
, emqx_vm_process_messages_in_queues
|
emqx_vm_run_queue,
|
||||||
, emqx_vm_total_memory
|
emqx_vm_process_messages_in_queues,
|
||||||
, emqx_vm_used_memory
|
emqx_vm_total_memory,
|
||||||
|
emqx_vm_used_memory
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_vm_data() ->
|
emqx_vm_data() ->
|
||||||
Idle = case cpu_sup:util([detailed]) of
|
Idle =
|
||||||
{_, 0, 0, _} -> 0; %% Not support for Windows
|
case cpu_sup:util([detailed]) of
|
||||||
{_Num, _Use, IdleList, _} -> ?C(idle, IdleList)
|
%% Not support for Windows
|
||||||
end,
|
{_, 0, 0, _} -> 0;
|
||||||
|
{_Num, _Use, IdleList, _} -> ?C(idle, IdleList)
|
||||||
|
end,
|
||||||
RunQueue = erlang:statistics(run_queue),
|
RunQueue = erlang:statistics(run_queue),
|
||||||
[{run_queue, RunQueue},
|
[
|
||||||
{process_total_messages, 0}, %% XXX: Plan removed at v5.0
|
{run_queue, RunQueue},
|
||||||
{cpu_idle, Idle},
|
%% XXX: Plan removed at v5.0
|
||||||
{cpu_use, 100 - Idle}] ++ emqx_vm:mem_info().
|
{process_total_messages, 0},
|
||||||
|
{cpu_idle, Idle},
|
||||||
|
{cpu_use, 100 - Idle}
|
||||||
|
] ++ emqx_vm:mem_info().
|
||||||
|
|
||||||
emqx_cluster() ->
|
emqx_cluster() ->
|
||||||
[ emqx_cluster_nodes_running
|
[
|
||||||
, emqx_cluster_nodes_stopped
|
emqx_cluster_nodes_running,
|
||||||
|
emqx_cluster_nodes_stopped
|
||||||
].
|
].
|
||||||
|
|
||||||
emqx_cluster_data() ->
|
emqx_cluster_data() ->
|
||||||
#{running_nodes := Running, stopped_nodes := Stopped} = mria_mnesia:cluster_info(),
|
#{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)}
|
||||||
|
].
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,16 @@
|
||||||
|
|
||||||
-import(hoconsc, [ref/2]).
|
-import(hoconsc, [ref/2]).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ prometheus/2
|
-export([
|
||||||
, stats/2
|
prometheus/2,
|
||||||
]).
|
stats/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(SCHEMA_MODULE, emqx_prometheus_schema).
|
-define(SCHEMA_MODULE, emqx_prometheus_schema).
|
||||||
|
|
||||||
|
|
@ -37,32 +39,38 @@ api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/prometheus"
|
[
|
||||||
, "/prometheus/stats"
|
"/prometheus",
|
||||||
|
"/prometheus/stats"
|
||||||
].
|
].
|
||||||
|
|
||||||
schema("/prometheus") ->
|
schema("/prometheus") ->
|
||||||
#{ 'operationId' => prometheus
|
#{
|
||||||
, get =>
|
'operationId' => prometheus,
|
||||||
#{ description => <<"Get Prometheus config info">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{200 => prometheus_config_schema()}
|
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") ->
|
schema("/prometheus/stats") ->
|
||||||
#{ 'operationId' => stats
|
#{
|
||||||
, get =>
|
'operationId' => stats,
|
||||||
#{ description => <<"Get Prometheus Data">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{200 => prometheus_data_schema()}
|
description => <<"Get Prometheus Data">>,
|
||||||
|
responses =>
|
||||||
|
#{200 => prometheus_data_schema()}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API Handler funcs
|
%% API Handler funcs
|
||||||
|
|
@ -70,7 +78,6 @@ schema("/prometheus/stats") ->
|
||||||
|
|
||||||
prometheus(get, _Params) ->
|
prometheus(get, _Params) ->
|
||||||
{200, emqx:get_raw_config([<<"prometheus">>], #{})};
|
{200, emqx:get_raw_config([<<"prometheus">>], #{})};
|
||||||
|
|
||||||
prometheus(put, #{body := Body}) ->
|
prometheus(put, #{body := Body}) ->
|
||||||
case emqx_prometheus:update(Body) of
|
case emqx_prometheus:update(Body) of
|
||||||
{ok, NewConfig} ->
|
{ok, NewConfig} ->
|
||||||
|
|
@ -100,21 +107,25 @@ stats(get, #{headers := Headers}) ->
|
||||||
|
|
||||||
prometheus_config_schema() ->
|
prometheus_config_schema() ->
|
||||||
emqx_dashboard_swagger:schema_with_example(
|
emqx_dashboard_swagger:schema_with_example(
|
||||||
ref(?SCHEMA_MODULE, "prometheus"),
|
ref(?SCHEMA_MODULE, "prometheus"),
|
||||||
prometheus_config_example()).
|
prometheus_config_example()
|
||||||
|
).
|
||||||
|
|
||||||
prometheus_config_example() ->
|
prometheus_config_example() ->
|
||||||
#{ enable => true
|
#{
|
||||||
, interval => "15s"
|
enable => true,
|
||||||
, push_gateway_server => <<"http://127.0.0.1:9091">>
|
interval => "15s",
|
||||||
}.
|
push_gateway_server => <<"http://127.0.0.1:9091">>
|
||||||
|
}.
|
||||||
|
|
||||||
prometheus_data_schema() ->
|
prometheus_data_schema() ->
|
||||||
#{ description => <<"Get Prometheus Data">>
|
#{
|
||||||
, content =>
|
description => <<"Get Prometheus Data">>,
|
||||||
#{ 'application/json' =>
|
content =>
|
||||||
#{schema => #{type => object}}
|
#{
|
||||||
, 'text/plain' =>
|
'application/json' =>
|
||||||
#{schema => #{type => string}}
|
#{schema => #{type => object}},
|
||||||
|
'text/plain' =>
|
||||||
|
#{schema => #{type => string}}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@
|
||||||
-include("emqx_prometheus.hrl").
|
-include("emqx_prometheus.hrl").
|
||||||
|
|
||||||
%% Application callbacks
|
%% Application callbacks
|
||||||
-export([ start/2
|
-export([
|
||||||
, stop/1
|
start/2,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
{ok, Sup} = emqx_prometheus_sup:start_link(),
|
{ok, Sup} = emqx_prometheus_sup:start_link(),
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_prometheus_mria).
|
-module(emqx_prometheus_mria).
|
||||||
|
|
||||||
-export([deregister_cleanup/1,
|
-export([
|
||||||
collect_mf/2
|
deregister_cleanup/1,
|
||||||
]).
|
collect_mf/2
|
||||||
|
]).
|
||||||
|
|
||||||
-include_lib("prometheus/include/prometheus.hrl").
|
-include_lib("prometheus/include/prometheus.hrl").
|
||||||
|
|
||||||
|
|
@ -43,39 +44,45 @@ deregister_cleanup(_) -> ok.
|
||||||
_Registry :: prometheus_registry:registry(),
|
_Registry :: prometheus_registry:registry(),
|
||||||
Callback :: prometheus_collector:callback().
|
Callback :: prometheus_collector:callback().
|
||||||
collect_mf(_Registry, Callback) ->
|
collect_mf(_Registry, Callback) ->
|
||||||
case mria_rlog:backend() of
|
case mria_rlog:backend() of
|
||||||
rlog ->
|
rlog ->
|
||||||
Metrics = metrics(),
|
Metrics = metrics(),
|
||||||
_ = [add_metric_family(Metric, Callback) || Metric <- Metrics],
|
_ = [add_metric_family(Metric, Callback) || Metric <- Metrics],
|
||||||
ok;
|
ok;
|
||||||
mnesia ->
|
mnesia ->
|
||||||
ok
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_metric_family({Name, Metrics}, Callback) ->
|
add_metric_family({Name, Metrics}, Callback) ->
|
||||||
Callback(prometheus_model_helpers:create_mf( ?METRIC_NAME(Name)
|
Callback(
|
||||||
, <<"">>
|
prometheus_model_helpers:create_mf(
|
||||||
, gauge
|
?METRIC_NAME(Name),
|
||||||
, catch_all(Metrics)
|
<<"">>,
|
||||||
)).
|
gauge,
|
||||||
|
catch_all(Metrics)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%====================================================================
|
%%====================================================================
|
||||||
|
|
||||||
metrics() ->
|
metrics() ->
|
||||||
Metrics = case mria_rlog:role() of
|
Metrics =
|
||||||
replicant ->
|
case mria_rlog:role() of
|
||||||
[lag, bootstrap_time, bootstrap_num_keys, message_queue_len, replayq_len];
|
replicant ->
|
||||||
core ->
|
[lag, bootstrap_time, bootstrap_num_keys, message_queue_len, replayq_len];
|
||||||
[last_intercepted_trans, weight, replicants, server_mql]
|
core ->
|
||||||
end,
|
[last_intercepted_trans, weight, replicants, server_mql]
|
||||||
|
end,
|
||||||
[{MetricId, fun() -> get_shard_metric(MetricId) end} || MetricId <- Metrics].
|
[{MetricId, fun() -> get_shard_metric(MetricId) end} || MetricId <- Metrics].
|
||||||
|
|
||||||
get_shard_metric(Metric) ->
|
get_shard_metric(Metric) ->
|
||||||
%% TODO: only report shards that are up
|
%% 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) ->
|
get_shard_metric(replicants, Shard) ->
|
||||||
length(mria_status:agents(Shard));
|
length(mria_status:agents(Shard));
|
||||||
|
|
@ -88,6 +95,8 @@ get_shard_metric(Metric, Shard) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
catch_all(DataFun) ->
|
catch_all(DataFun) ->
|
||||||
try DataFun()
|
try
|
||||||
catch _:_ -> undefined
|
DataFun()
|
||||||
|
catch
|
||||||
|
_:_ -> undefined
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, desc/1
|
fields/1,
|
||||||
]).
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
namespace() -> "prometheus".
|
namespace() -> "prometheus".
|
||||||
|
|
||||||
|
|
@ -32,25 +33,36 @@ roots() -> ["prometheus"].
|
||||||
|
|
||||||
fields("prometheus") ->
|
fields("prometheus") ->
|
||||||
[
|
[
|
||||||
{push_gateway_server, sc(string(),
|
{push_gateway_server,
|
||||||
#{ default => "http://127.0.0.1:9091"
|
sc(
|
||||||
, required => true
|
string(),
|
||||||
, desc => ?DESC(push_gateway_server)
|
#{
|
||||||
})},
|
default => "http://127.0.0.1:9091",
|
||||||
{interval, sc(emqx_schema:duration_ms(),
|
required => true,
|
||||||
#{ default => "15s"
|
desc => ?DESC(push_gateway_server)
|
||||||
, required => true
|
}
|
||||||
, desc => ?DESC(interval)
|
)},
|
||||||
})},
|
{interval,
|
||||||
{enable, sc(boolean(),
|
sc(
|
||||||
#{ default => false
|
emqx_schema:duration_ms(),
|
||||||
, required => true
|
#{
|
||||||
, desc => ?DESC(enable)
|
default => "15s",
|
||||||
})}
|
required => true,
|
||||||
|
desc => ?DESC(interval)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{enable,
|
||||||
|
sc(
|
||||||
|
boolean(),
|
||||||
|
#{
|
||||||
|
default => false,
|
||||||
|
required => true,
|
||||||
|
desc => ?DESC(enable)
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
desc("prometheus") -> ?DESC(prometheus);
|
desc("prometheus") -> ?DESC(prometheus);
|
||||||
desc(_) ->
|
desc(_) -> undefined.
|
||||||
undefined.
|
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,24 @@
|
||||||
|
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
|
|
||||||
-export([ start_link/0
|
-export([
|
||||||
, start_child/1
|
start_link/0,
|
||||||
, start_child/2
|
start_child/1,
|
||||||
, stop_child/1
|
start_child/2,
|
||||||
]).
|
stop_child/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
%% Helper macro for declaring children of supervisor
|
%% Helper macro for declaring children of supervisor
|
||||||
-define(CHILD(Mod, Opts), #{id => Mod,
|
-define(CHILD(Mod, Opts), #{
|
||||||
start => {Mod, start_link, [Opts]},
|
id => Mod,
|
||||||
restart => permanent,
|
start => {Mod, start_link, [Opts]},
|
||||||
shutdown => 5000,
|
restart => permanent,
|
||||||
type => worker,
|
shutdown => 5000,
|
||||||
modules => [Mod]}).
|
type => worker,
|
||||||
|
modules => [Mod]
|
||||||
|
}).
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
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) ->
|
start_child(Mod, Opts) when is_atom(Mod) andalso is_map(Opts) ->
|
||||||
assert_started(supervisor:start_child(?MODULE, ?CHILD(Mod, 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) ->
|
stop_child(ChildId) ->
|
||||||
case supervisor:terminate_child(?MODULE, ChildId) of
|
case supervisor:terminate_child(?MODULE, ChildId) of
|
||||||
ok -> supervisor:delete_child(?MODULE, ChildId);
|
ok -> supervisor:delete_child(?MODULE, ChildId);
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
|
introduced_in/0,
|
||||||
|
|
||||||
, start/1
|
start/1,
|
||||||
, stop/1
|
stop/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,14 @@
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
|
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
|
||||||
-define(CONF_DEFAULT, <<"
|
-define(CONF_DEFAULT,
|
||||||
prometheus {
|
<<"\n"
|
||||||
push_gateway_server = \"http://127.0.0.1:9091\"
|
"prometheus {\n"
|
||||||
interval = \"1s\"
|
" push_gateway_server = \"http://127.0.0.1:9091\"\n"
|
||||||
enable = true
|
" interval = \"1s\"\n"
|
||||||
}
|
" enable = true\n"
|
||||||
">>).
|
"}\n">>
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Setups
|
%% Setups
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,14 @@ t_prometheus_api(_) ->
|
||||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, "", Auth),
|
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, "", Auth),
|
||||||
|
|
||||||
Conf = emqx_json:decode(Response, [return_maps]),
|
Conf = emqx_json:decode(Response, [return_maps]),
|
||||||
?assertMatch(#{<<"push_gateway_server">> := _,
|
?assertMatch(
|
||||||
<<"interval">> := _,
|
#{
|
||||||
<<"enable">> := _}, Conf),
|
<<"push_gateway_server">> := _,
|
||||||
|
<<"interval">> := _,
|
||||||
|
<<"enable">> := _
|
||||||
|
},
|
||||||
|
Conf
|
||||||
|
),
|
||||||
|
|
||||||
NewConf = Conf#{<<"interval">> := <<"2s">>},
|
NewConf = Conf#{<<"interval">> := <<"2s">>},
|
||||||
{ok, Response2} = emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, NewConf),
|
{ok, Response2} = emqx_mgmt_api_test_util:request_api(put, Path, "", Auth, NewConf),
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@
|
||||||
}.
|
}.
|
||||||
-type resource_group() :: binary().
|
-type resource_group() :: binary().
|
||||||
-type create_opts() :: #{
|
-type create_opts() :: #{
|
||||||
health_check_interval => integer(),
|
health_check_interval => integer(),
|
||||||
health_check_timeout => integer(),
|
health_check_timeout => integer(),
|
||||||
waiting_connect_complete => integer()
|
waiting_connect_complete => integer()
|
||||||
}.
|
}.
|
||||||
-type after_query() :: {[OnSuccess :: after_query_fun()], [OnFailed :: after_query_fun()]} |
|
-type after_query() ::
|
||||||
undefined.
|
{[OnSuccess :: after_query_fun()], [OnFailed :: after_query_fun()]}
|
||||||
|
| undefined.
|
||||||
|
|
||||||
%% the `after_query_fun()` is mainly for callbacks that increment counters or do some fallback
|
%% the `after_query_fun()` is mainly for callbacks that increment counters or do some fallback
|
||||||
%% actions upon query failure
|
%% actions upon query failure
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,17 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-define(SAFE_CALL(_EXP_),
|
-define(SAFE_CALL(_EXP_),
|
||||||
?SAFE_CALL(_EXP_, ok)).
|
?SAFE_CALL(_EXP_, ok)
|
||||||
|
).
|
||||||
|
|
||||||
-define(SAFE_CALL(_EXP_, _EXP_ON_FAIL_),
|
-define(SAFE_CALL(_EXP_, _EXP_ON_FAIL_),
|
||||||
fun() ->
|
fun() ->
|
||||||
try (_EXP_)
|
try
|
||||||
catch _EXCLASS_:_EXCPTION_:_ST_ ->
|
(_EXP_)
|
||||||
|
catch
|
||||||
|
_EXCLASS_:_EXCPTION_:_ST_ ->
|
||||||
_EXP_ON_FAIL_,
|
_EXP_ON_FAIL_,
|
||||||
{error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
{error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||||
end
|
end
|
||||||
end()).
|
end()
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{erl_opts, [ debug_info
|
{erl_opts, [
|
||||||
, nowarn_unused_import
|
debug_info,
|
||||||
%, {d, 'RESOURCE_DEBUG'}
|
nowarn_unused_import
|
||||||
]}.
|
%, {d, 'RESOURCE_DEBUG'}
|
||||||
|
]}.
|
||||||
|
|
||||||
{erl_first_files, ["src/emqx_resource_transform.erl"]}.
|
{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,
|
%% try to override the dialyzer 'race_conditions' defined in the top-level dir,
|
||||||
%% but it doesn't work
|
%% 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"}}}
|
{deps, [
|
||||||
, {emqx, {path, "../emqx"}}
|
{jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}},
|
||||||
]}.
|
{emqx, {path, "../emqx"}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_resource,
|
{application, emqx_resource, [
|
||||||
[{description, "An OTP application"},
|
{description, "An OTP application"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.1.0"},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{mod, {emqx_resource_app, []}},
|
{mod, {emqx_resource_app, []}},
|
||||||
{applications,
|
{applications, [
|
||||||
[kernel,
|
kernel,
|
||||||
stdlib,
|
stdlib,
|
||||||
gproc,
|
gproc,
|
||||||
jsx,
|
jsx,
|
||||||
emqx
|
emqx
|
||||||
]},
|
]},
|
||||||
{env,[]},
|
{env, []},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache 2.0"]},
|
{licenses, ["Apache 2.0"]},
|
||||||
{links, []}
|
{links, []}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -25,66 +25,93 @@
|
||||||
|
|
||||||
%% APIs for behaviour implementations
|
%% APIs for behaviour implementations
|
||||||
|
|
||||||
-export([ query_success/1
|
-export([
|
||||||
, query_failed/1
|
query_success/1,
|
||||||
]).
|
query_failed/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% APIs for instances
|
%% APIs for instances
|
||||||
|
|
||||||
-export([ check_config/2
|
-export([
|
||||||
, check_and_create/4
|
check_config/2,
|
||||||
, check_and_create/5
|
check_and_create/4,
|
||||||
, check_and_create_local/4
|
check_and_create/5,
|
||||||
, check_and_create_local/5
|
check_and_create_local/4,
|
||||||
, check_and_recreate/4
|
check_and_create_local/5,
|
||||||
, check_and_recreate_local/4
|
check_and_recreate/4,
|
||||||
]).
|
check_and_recreate_local/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% Sync resource instances and files
|
%% Sync resource instances and files
|
||||||
%% provisional solution: rpc:multicall to all the nodes for creating/updating/removing
|
%% provisional solution: rpc:multicall to all the nodes for creating/updating/removing
|
||||||
%% todo: replicate operations
|
%% todo: replicate operations
|
||||||
-export([ create/4 %% store the config and start the instance
|
|
||||||
, create/5
|
%% store the config and start the instance
|
||||||
, create_local/4
|
-export([
|
||||||
, create_local/5
|
create/4,
|
||||||
, create_dry_run/2 %% run start/2, health_check/2 and stop/1 sequentially
|
create/5,
|
||||||
, create_dry_run_local/2
|
create_local/4,
|
||||||
, recreate/4 %% this will do create_dry_run, stop the old instance and start a new one
|
create_local/5,
|
||||||
, recreate_local/4
|
%% run start/2, health_check/2 and stop/1 sequentially
|
||||||
, remove/1 %% remove the config and stop the instance
|
create_dry_run/2,
|
||||||
, remove_local/1
|
create_dry_run_local/2,
|
||||||
, reset_metrics/1
|
%% this will do create_dry_run, stop the old instance and start a new one
|
||||||
, reset_metrics_local/1
|
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
|
%% Calls to the callback module with current resource state
|
||||||
%% They also save the state after the call finished (except query/2,3).
|
%% They also save the state after the call finished (except query/2,3).
|
||||||
-export([ restart/1 %% restart the instance.
|
|
||||||
, restart/2
|
%% restart the instance.
|
||||||
, health_check/1 %% verify if the resource is working normally
|
-export([
|
||||||
, set_resource_status_connecting/1 %% set resource status to disconnected
|
restart/1,
|
||||||
, stop/1 %% stop the instance
|
restart/2,
|
||||||
, query/2 %% query the instance
|
%% verify if the resource is working normally
|
||||||
, query/3 %% query the instance with after_query()
|
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
|
%% 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.
|
%% start the instance
|
||||||
, list_instances_verbose/0 %% list all the instances
|
-export([
|
||||||
, get_instance/1 %% return the data of the instance
|
call_start/3,
|
||||||
, list_instances_by_type/1 %% return all the instances of the same resource type
|
%% verify if the resource is working normally
|
||||||
, generate_id/1
|
call_health_check/3,
|
||||||
, list_group_instances/1
|
%% stop the instance
|
||||||
]).
|
call_stop/3
|
||||||
|
]).
|
||||||
|
|
||||||
-optional_callbacks([ on_query/4
|
%% list all the instances, id only.
|
||||||
, on_health_check/2
|
-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
|
%% when calling emqx_resource:start/1
|
||||||
-callback on_start(instance_id(), resource_config()) ->
|
-callback on_start(instance_id(), resource_config()) ->
|
||||||
|
|
@ -98,7 +125,7 @@
|
||||||
|
|
||||||
%% when calling emqx_resource:health_check/2
|
%% when calling emqx_resource:health_check/2
|
||||||
-callback on_health_check(instance_id(), resource_state()) ->
|
-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()].
|
-spec list_types() -> [module()].
|
||||||
list_types() ->
|
list_types() ->
|
||||||
|
|
@ -111,24 +138,26 @@ discover_resource_mods() ->
|
||||||
-spec is_resource_mod(module()) -> boolean().
|
-spec is_resource_mod(module()) -> boolean().
|
||||||
is_resource_mod(Module) ->
|
is_resource_mod(Module) ->
|
||||||
Info = Module:module_info(attributes),
|
Info = Module:module_info(attributes),
|
||||||
Behaviour = proplists:get_value(behavior, Info, []) ++
|
Behaviour =
|
||||||
proplists:get_value(behaviour, Info, []),
|
proplists:get_value(behavior, Info, []) ++
|
||||||
|
proplists:get_value(behaviour, Info, []),
|
||||||
lists:member(?MODULE, Behaviour).
|
lists:member(?MODULE, Behaviour).
|
||||||
|
|
||||||
-spec query_success(after_query()) -> ok.
|
-spec query_success(after_query()) -> ok.
|
||||||
query_success(undefined) -> ok;
|
query_success(undefined) -> ok;
|
||||||
query_success({OnSucc, _}) ->
|
query_success({OnSucc, _}) -> apply_query_after_calls(OnSucc).
|
||||||
apply_query_after_calls(OnSucc).
|
|
||||||
|
|
||||||
-spec query_failed(after_query()) -> ok.
|
-spec query_failed(after_query()) -> ok.
|
||||||
query_failed(undefined) -> ok;
|
query_failed(undefined) -> ok;
|
||||||
query_failed({_, OnFailed}) ->
|
query_failed({_, OnFailed}) -> apply_query_after_calls(OnFailed).
|
||||||
apply_query_after_calls(OnFailed).
|
|
||||||
|
|
||||||
apply_query_after_calls(Funcs) ->
|
apply_query_after_calls(Funcs) ->
|
||||||
lists:foreach(fun({Fun, Args}) ->
|
lists:foreach(
|
||||||
|
fun({Fun, Args}) ->
|
||||||
safe_apply(Fun, Args)
|
safe_apply(Fun, Args)
|
||||||
end, Funcs).
|
end,
|
||||||
|
Funcs
|
||||||
|
).
|
||||||
|
|
||||||
%% =================================================================================
|
%% =================================================================================
|
||||||
%% APIs for resource instances
|
%% 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) ->
|
||||||
create_local(InstId, Group, ResourceType, Config, #{}).
|
create_local(InstId, Group, ResourceType, Config, #{}).
|
||||||
|
|
||||||
-spec create_local(instance_id(),
|
-spec create_local(
|
||||||
resource_group(),
|
instance_id(),
|
||||||
resource_type(),
|
resource_group(),
|
||||||
resource_config(),
|
resource_type(),
|
||||||
create_opts()) ->
|
resource_config(),
|
||||||
|
create_opts()
|
||||||
|
) ->
|
||||||
{ok, resource_data() | 'already_created'} | {error, Reason :: term()}.
|
{ok, resource_data() | 'already_created'} | {error, Reason :: term()}.
|
||||||
create_local(InstId, Group, ResourceType, Config, Opts) ->
|
create_local(InstId, Group, ResourceType, Config, Opts) ->
|
||||||
call_instance(InstId, {create, 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) ->
|
query(InstId, Request, AfterQuery) ->
|
||||||
case get_instance(InstId) of
|
case get_instance(InstId) of
|
||||||
{ok, _Group, #{status := connecting}} ->
|
{ok, _Group, #{status := connecting}} ->
|
||||||
query_error(connecting, <<"cannot serve query when the resource "
|
query_error(connecting, <<
|
||||||
"instance is still connecting">>);
|
"cannot serve query when the resource "
|
||||||
|
"instance is still connecting"
|
||||||
|
>>);
|
||||||
{ok, _Group, #{status := disconnected}} ->
|
{ok, _Group, #{status := disconnected}} ->
|
||||||
query_error(disconnected, <<"cannot serve query when the resource "
|
query_error(disconnected, <<
|
||||||
"instance is disconnected">>);
|
"cannot serve query when the resource "
|
||||||
|
"instance is disconnected"
|
||||||
|
>>);
|
||||||
{ok, _Group, #{mod := Mod, state := ResourceState, status := connected}} ->
|
{ok, _Group, #{mod := Mod, state := ResourceState, status := connected}} ->
|
||||||
%% the resource state is readonly to Module:on_query/4
|
%% the resource state is readonly to Module:on_query/4
|
||||||
%% and the `after_query()` functions should be thread safe
|
%% and the `after_query()` functions should be thread safe
|
||||||
ok = emqx_plugin_libs_metrics:inc(resource_metrics, InstId, matched),
|
ok = emqx_plugin_libs_metrics:inc(resource_metrics, InstId, matched),
|
||||||
try Mod:on_query(InstId, Request, AfterQuery, ResourceState)
|
try
|
||||||
catch Err:Reason:ST ->
|
Mod:on_query(InstId, Request, AfterQuery, ResourceState)
|
||||||
emqx_plugin_libs_metrics:inc(resource_metrics, InstId, exception),
|
catch
|
||||||
erlang:raise(Err, Reason, ST)
|
Err:Reason:ST ->
|
||||||
|
emqx_plugin_libs_metrics:inc(resource_metrics, InstId, exception),
|
||||||
|
erlang:raise(Err, Reason, ST)
|
||||||
end;
|
end;
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
query_error(not_found, <<"the resource id not exists">>)
|
query_error(not_found, <<"the resource id not exists">>)
|
||||||
|
|
@ -258,9 +295,10 @@ list_instances_verbose() ->
|
||||||
|
|
||||||
-spec list_instances_by_type(module()) -> [instance_id()].
|
-spec list_instances_by_type(module()) -> [instance_id()].
|
||||||
list_instances_by_type(ResourceType) ->
|
list_instances_by_type(ResourceType) ->
|
||||||
filter_instances(fun(_, RT) when RT =:= ResourceType -> true;
|
filter_instances(fun
|
||||||
(_, _) -> false
|
(_, RT) when RT =:= ResourceType -> true;
|
||||||
end).
|
(_, _) -> false
|
||||||
|
end).
|
||||||
|
|
||||||
-spec generate_id(term()) -> instance_id().
|
-spec generate_id(term()) -> instance_id().
|
||||||
generate_id(Name) when is_binary(Name) ->
|
generate_id(Name) when is_binary(Name) ->
|
||||||
|
|
@ -276,7 +314,9 @@ call_start(InstId, Mod, Config) ->
|
||||||
?SAFE_CALL(Mod:on_start(InstId, Config)).
|
?SAFE_CALL(Mod:on_start(InstId, Config)).
|
||||||
|
|
||||||
-spec call_health_check(instance_id(), module(), resource_state()) ->
|
-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) ->
|
call_health_check(InstId, Mod, ResourceState) ->
|
||||||
?SAFE_CALL(Mod:on_health_check(InstId, ResourceState)).
|
?SAFE_CALL(Mod:on_health_check(InstId, ResourceState)).
|
||||||
|
|
||||||
|
|
@ -289,58 +329,82 @@ call_stop(InstId, Mod, ResourceState) ->
|
||||||
check_config(ResourceType, Conf) ->
|
check_config(ResourceType, Conf) ->
|
||||||
emqx_hocon:check(ResourceType, Conf).
|
emqx_hocon:check(ResourceType, Conf).
|
||||||
|
|
||||||
-spec check_and_create(instance_id(),
|
-spec check_and_create(
|
||||||
resource_group(),
|
instance_id(),
|
||||||
resource_type(),
|
resource_group(),
|
||||||
raw_resource_config()) ->
|
resource_type(),
|
||||||
|
raw_resource_config()
|
||||||
|
) ->
|
||||||
{ok, resource_data() | 'already_created'} | {error, term()}.
|
{ok, resource_data() | 'already_created'} | {error, term()}.
|
||||||
check_and_create(InstId, Group, ResourceType, RawConfig) ->
|
check_and_create(InstId, Group, ResourceType, RawConfig) ->
|
||||||
check_and_create(InstId, Group, ResourceType, RawConfig, #{}).
|
check_and_create(InstId, Group, ResourceType, RawConfig, #{}).
|
||||||
|
|
||||||
-spec check_and_create(instance_id(),
|
-spec check_and_create(
|
||||||
resource_group(),
|
instance_id(),
|
||||||
resource_type(),
|
resource_group(),
|
||||||
raw_resource_config(),
|
resource_type(),
|
||||||
create_opts()) ->
|
raw_resource_config(),
|
||||||
|
create_opts()
|
||||||
|
) ->
|
||||||
{ok, resource_data() | 'already_created'} | {error, term()}.
|
{ok, resource_data() | 'already_created'} | {error, term()}.
|
||||||
check_and_create(InstId, Group, ResourceType, RawConfig, Opts) ->
|
check_and_create(InstId, Group, ResourceType, RawConfig, Opts) ->
|
||||||
check_and_do(ResourceType, RawConfig,
|
check_and_do(
|
||||||
fun(InstConf) -> create(InstId, Group, ResourceType, InstConf, Opts) end).
|
ResourceType,
|
||||||
|
RawConfig,
|
||||||
|
fun(InstConf) -> create(InstId, Group, ResourceType, InstConf, Opts) end
|
||||||
|
).
|
||||||
|
|
||||||
-spec check_and_create_local(instance_id(),
|
-spec check_and_create_local(
|
||||||
resource_group(),
|
instance_id(),
|
||||||
resource_type(),
|
resource_group(),
|
||||||
raw_resource_config()) ->
|
resource_type(),
|
||||||
|
raw_resource_config()
|
||||||
|
) ->
|
||||||
{ok, resource_data()} | {error, term()}.
|
{ok, resource_data()} | {error, term()}.
|
||||||
check_and_create_local(InstId, Group, ResourceType, RawConfig) ->
|
check_and_create_local(InstId, Group, ResourceType, RawConfig) ->
|
||||||
check_and_create_local(InstId, Group, ResourceType, RawConfig, #{}).
|
check_and_create_local(InstId, Group, ResourceType, RawConfig, #{}).
|
||||||
|
|
||||||
-spec check_and_create_local(instance_id(),
|
-spec check_and_create_local(
|
||||||
resource_group(),
|
instance_id(),
|
||||||
resource_type(),
|
resource_group(),
|
||||||
raw_resource_config(),
|
resource_type(),
|
||||||
create_opts()) -> {ok, resource_data()} | {error, term()}.
|
raw_resource_config(),
|
||||||
|
create_opts()
|
||||||
|
) -> {ok, resource_data()} | {error, term()}.
|
||||||
check_and_create_local(InstId, Group, ResourceType, RawConfig, Opts) ->
|
check_and_create_local(InstId, Group, ResourceType, RawConfig, Opts) ->
|
||||||
check_and_do(ResourceType, RawConfig,
|
check_and_do(
|
||||||
fun(InstConf) -> create_local(InstId, Group, ResourceType, InstConf, Opts) end).
|
ResourceType,
|
||||||
|
RawConfig,
|
||||||
|
fun(InstConf) -> create_local(InstId, Group, ResourceType, InstConf, Opts) end
|
||||||
|
).
|
||||||
|
|
||||||
-spec check_and_recreate(instance_id(),
|
-spec check_and_recreate(
|
||||||
resource_type(),
|
instance_id(),
|
||||||
raw_resource_config(),
|
resource_type(),
|
||||||
create_opts()) ->
|
raw_resource_config(),
|
||||||
|
create_opts()
|
||||||
|
) ->
|
||||||
{ok, resource_data()} | {error, term()}.
|
{ok, resource_data()} | {error, term()}.
|
||||||
check_and_recreate(InstId, ResourceType, RawConfig, Opts) ->
|
check_and_recreate(InstId, ResourceType, RawConfig, Opts) ->
|
||||||
check_and_do(ResourceType, RawConfig,
|
check_and_do(
|
||||||
fun(InstConf) -> recreate(InstId, ResourceType, InstConf, Opts) end).
|
ResourceType,
|
||||||
|
RawConfig,
|
||||||
|
fun(InstConf) -> recreate(InstId, ResourceType, InstConf, Opts) end
|
||||||
|
).
|
||||||
|
|
||||||
-spec check_and_recreate_local(instance_id(),
|
-spec check_and_recreate_local(
|
||||||
resource_type(),
|
instance_id(),
|
||||||
raw_resource_config(),
|
resource_type(),
|
||||||
create_opts()) ->
|
raw_resource_config(),
|
||||||
|
create_opts()
|
||||||
|
) ->
|
||||||
{ok, resource_data()} | {error, term()}.
|
{ok, resource_data()} | {error, term()}.
|
||||||
check_and_recreate_local(InstId, ResourceType, RawConfig, Opts) ->
|
check_and_recreate_local(InstId, ResourceType, RawConfig, Opts) ->
|
||||||
check_and_do(ResourceType, RawConfig,
|
check_and_do(
|
||||||
fun(InstConf) -> recreate_local(InstId, ResourceType, InstConf, Opts) end).
|
ResourceType,
|
||||||
|
RawConfig,
|
||||||
|
fun(InstConf) -> recreate_local(InstId, ResourceType, InstConf, Opts) end
|
||||||
|
).
|
||||||
|
|
||||||
check_and_do(ResourceType, RawConfig, Do) when is_function(Do) ->
|
check_and_do(ResourceType, RawConfig, Do) when is_function(Do) ->
|
||||||
case check_config(ResourceType, RawConfig) of
|
case check_config(ResourceType, RawConfig) of
|
||||||
|
|
@ -355,8 +419,7 @@ filter_instances(Filter) ->
|
||||||
|
|
||||||
inc_metrics_funcs(InstId) ->
|
inc_metrics_funcs(InstId) ->
|
||||||
OnFailed = [{fun emqx_plugin_libs_metrics:inc/3, [resource_metrics, InstId, failed]}],
|
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}.
|
{OnSucc, OnFailed}.
|
||||||
|
|
||||||
call_instance(InstId, Query) ->
|
call_instance(InstId, Query) ->
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,29 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_resource_health_check).
|
-module(emqx_resource_health_check).
|
||||||
|
|
||||||
-export([ start_link/3
|
-export([
|
||||||
, create_checker/3
|
start_link/3,
|
||||||
, delete_checker/1
|
create_checker/3,
|
||||||
]).
|
delete_checker/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ start_health_check/3
|
-export([
|
||||||
, health_check_timeout_checker/4
|
start_health_check/3,
|
||||||
]).
|
health_check_timeout_checker/4
|
||||||
|
]).
|
||||||
|
|
||||||
-define(SUP, emqx_resource_health_check_sup).
|
-define(SUP, emqx_resource_health_check_sup).
|
||||||
-define(ID(NAME), {resource_health_check, NAME}).
|
-define(ID(NAME), {resource_health_check, NAME}).
|
||||||
|
|
||||||
child_spec(Name, Sleep, Timeout) ->
|
child_spec(Name, Sleep, Timeout) ->
|
||||||
#{id => ?ID(Name),
|
#{
|
||||||
start => {?MODULE, start_link, [Name, Sleep, Timeout]},
|
id => ?ID(Name),
|
||||||
restart => transient,
|
start => {?MODULE, start_link, [Name, Sleep, Timeout]},
|
||||||
shutdown => 5000, type => worker, modules => [?MODULE]}.
|
restart => transient,
|
||||||
|
shutdown => 5000,
|
||||||
|
type => worker,
|
||||||
|
modules => [?MODULE]
|
||||||
|
}.
|
||||||
|
|
||||||
start_link(Name, Sleep, Timeout) ->
|
start_link(Name, Sleep, Timeout) ->
|
||||||
Pid = proc_lib:spawn_link(?MODULE, start_health_check, [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) ->
|
create_checker(Name, Sleep, Retry, Timeout) ->
|
||||||
case supervisor:start_child(?SUP, child_spec(Name, Sleep, Timeout)) of
|
case supervisor:start_child(?SUP, child_spec(Name, Sleep, Timeout)) of
|
||||||
{ok, _} -> ok;
|
{ok, _} ->
|
||||||
{error, already_present} -> ok;
|
ok;
|
||||||
|
{error, already_present} ->
|
||||||
|
ok;
|
||||||
{error, {already_started, _}} when Retry == false ->
|
{error, {already_started, _}} when Retry == false ->
|
||||||
ok = delete_checker(Name),
|
ok = delete_checker(Name),
|
||||||
create_checker(Name, Sleep, true, Timeout);
|
create_checker(Name, Sleep, true, Timeout);
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete_checker(Name) ->
|
delete_checker(Name) ->
|
||||||
case supervisor:terminate_child(?SUP, ?ID(Name)) of
|
case supervisor:terminate_child(?SUP, ?ID(Name)) of
|
||||||
ok -> supervisor:delete_child(?SUP, ?ID(Name));
|
ok -> supervisor:delete_child(?SUP, ?ID(Name));
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
start_health_check(Name, Sleep, Timeout) ->
|
start_health_check(Name, Sleep, Timeout) ->
|
||||||
Pid = self(),
|
Pid = self(),
|
||||||
|
|
@ -63,13 +72,16 @@ start_health_check(Name, Sleep, Timeout) ->
|
||||||
|
|
||||||
health_check(Name) ->
|
health_check(Name) ->
|
||||||
receive
|
receive
|
||||||
{Pid, begin_health_check} ->
|
{Pid, begin_health_check} ->
|
||||||
case emqx_resource:health_check(Name) of
|
case emqx_resource:health_check(Name) of
|
||||||
ok ->
|
ok ->
|
||||||
emqx_alarm:deactivate(Name);
|
emqx_alarm:deactivate(Name);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
emqx_alarm:activate(Name, #{name => Name},
|
emqx_alarm:activate(
|
||||||
<<Name/binary, " health check failed">>)
|
Name,
|
||||||
|
#{name => Name},
|
||||||
|
<<Name/binary, " health check failed">>
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
Pid ! health_check_finish
|
Pid ! health_check_finish
|
||||||
end,
|
end,
|
||||||
|
|
@ -81,8 +93,11 @@ health_check_timeout_checker(Pid, Name, SleepTime, Timeout) ->
|
||||||
receive
|
receive
|
||||||
health_check_finish -> timer:sleep(SleepTime)
|
health_check_finish -> timer:sleep(SleepTime)
|
||||||
after Timeout ->
|
after Timeout ->
|
||||||
emqx_alarm:activate(Name, #{name => Name},
|
emqx_alarm:activate(
|
||||||
<<Name/binary, " health check timeout">>),
|
Name,
|
||||||
|
#{name => Name},
|
||||||
|
<<Name/binary, " health check timeout">>
|
||||||
|
),
|
||||||
emqx_resource:set_resource_status_connecting(Name),
|
emqx_resource:set_resource_status_connecting(Name),
|
||||||
receive
|
receive
|
||||||
health_check_finish -> timer:sleep(SleepTime)
|
health_check_finish -> timer:sleep(SleepTime)
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,28 @@
|
||||||
-export([start_link/2]).
|
-export([start_link/2]).
|
||||||
|
|
||||||
%% load resource instances from *.conf files
|
%% load resource instances from *.conf files
|
||||||
-export([ lookup/1
|
-export([
|
||||||
, get_metrics/1
|
lookup/1,
|
||||||
, reset_metrics/1
|
get_metrics/1,
|
||||||
, list_all/0
|
reset_metrics/1,
|
||||||
, list_group/1
|
list_all/0,
|
||||||
]).
|
list_group/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ hash_call/2
|
-export([
|
||||||
, hash_call/3
|
hash_call/2,
|
||||||
]).
|
hash_call/3
|
||||||
|
]).
|
||||||
|
|
||||||
%% gen_server Callbacks
|
%% gen_server Callbacks
|
||||||
-export([ init/1
|
-export([
|
||||||
, handle_call/3
|
init/1,
|
||||||
, handle_cast/2
|
handle_call/3,
|
||||||
, handle_info/2
|
handle_cast/2,
|
||||||
, terminate/2
|
handle_info/2,
|
||||||
, code_change/3
|
terminate/2,
|
||||||
]).
|
code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
-record(state, {worker_pool, worker_id}).
|
-record(state, {worker_pool, worker_id}).
|
||||||
|
|
||||||
|
|
@ -52,8 +55,12 @@
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
start_link(Pool, Id) ->
|
start_link(Pool, Id) ->
|
||||||
gen_server:start_link({local, proc_name(?MODULE, Id)},
|
gen_server:start_link(
|
||||||
?MODULE, {Pool, Id}, []).
|
{local, proc_name(?MODULE, Id)},
|
||||||
|
?MODULE,
|
||||||
|
{Pool, Id},
|
||||||
|
[]
|
||||||
|
).
|
||||||
|
|
||||||
%% call the worker by the hash of resource-instance-id, to make sure we always handle
|
%% 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.
|
%% operations on the same instance in the same worker.
|
||||||
|
|
@ -67,8 +74,7 @@ hash_call(InstId, Request, Timeout) ->
|
||||||
lookup(InstId) ->
|
lookup(InstId) ->
|
||||||
case ets:lookup(emqx_resource_instance, InstId) of
|
case ets:lookup(emqx_resource_instance, InstId) of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[{_, Group, Data}] ->
|
[{_, Group, Data}] -> {ok, Group, Data#{id => InstId, metrics => get_metrics(InstId)}}
|
||||||
{ok, Group, Data#{id => InstId, metrics => get_metrics(InstId)}}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
make_test_id() ->
|
make_test_id() ->
|
||||||
|
|
@ -103,39 +109,32 @@ list_group(Group) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-spec init({atom(), integer()}) ->
|
-spec init({atom(), integer()}) ->
|
||||||
{ok, State :: state()} | {ok, State :: state(), timeout() | hibernate | {continue, term()}} |
|
{ok, State :: state()}
|
||||||
{stop, Reason :: term()} | ignore.
|
| {ok, State :: state(), timeout() | hibernate | {continue, term()}}
|
||||||
|
| {stop, Reason :: term()}
|
||||||
|
| ignore.
|
||||||
init({Pool, Id}) ->
|
init({Pool, Id}) ->
|
||||||
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
|
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
|
||||||
{ok, #state{worker_pool = Pool, worker_id = Id}}.
|
{ok, #state{worker_pool = Pool, worker_id = Id}}.
|
||||||
|
|
||||||
handle_call({create, InstId, Group, ResourceType, Config, Opts}, _From, State) ->
|
handle_call({create, InstId, Group, ResourceType, Config, Opts}, _From, State) ->
|
||||||
{reply, do_create(InstId, Group, ResourceType, Config, Opts), State};
|
{reply, do_create(InstId, Group, ResourceType, Config, Opts), State};
|
||||||
|
|
||||||
handle_call({create_dry_run, ResourceType, Config}, _From, State) ->
|
handle_call({create_dry_run, ResourceType, Config}, _From, State) ->
|
||||||
{reply, do_create_dry_run(ResourceType, Config), State};
|
{reply, do_create_dry_run(ResourceType, Config), State};
|
||||||
|
|
||||||
handle_call({recreate, InstId, ResourceType, Config, Opts}, _From, State) ->
|
handle_call({recreate, InstId, ResourceType, Config, Opts}, _From, State) ->
|
||||||
{reply, do_recreate(InstId, ResourceType, Config, Opts), State};
|
{reply, do_recreate(InstId, ResourceType, Config, Opts), State};
|
||||||
|
|
||||||
handle_call({reset_metrics, InstId}, _From, State) ->
|
handle_call({reset_metrics, InstId}, _From, State) ->
|
||||||
{reply, do_reset_metrics(InstId), State};
|
{reply, do_reset_metrics(InstId), State};
|
||||||
|
|
||||||
handle_call({remove, InstId}, _From, State) ->
|
handle_call({remove, InstId}, _From, State) ->
|
||||||
{reply, do_remove(InstId), State};
|
{reply, do_remove(InstId), State};
|
||||||
|
|
||||||
handle_call({restart, InstId, Opts}, _From, State) ->
|
handle_call({restart, InstId, Opts}, _From, State) ->
|
||||||
{reply, do_restart(InstId, Opts), State};
|
{reply, do_restart(InstId, Opts), State};
|
||||||
|
|
||||||
handle_call({stop, InstId}, _From, State) ->
|
handle_call({stop, InstId}, _From, State) ->
|
||||||
{reply, do_stop(InstId), State};
|
{reply, do_stop(InstId), State};
|
||||||
|
|
||||||
handle_call({health_check, InstId}, _From, State) ->
|
handle_call({health_check, InstId}, _From, State) ->
|
||||||
{reply, do_health_check(InstId), State};
|
{reply, do_health_check(InstId), State};
|
||||||
|
|
||||||
handle_call({set_resource_status_connecting, InstId}, _From, State) ->
|
handle_call({set_resource_status_connecting, InstId}, _From, State) ->
|
||||||
{reply, do_set_resource_status_connecting(InstId), State};
|
{reply, do_set_resource_status_connecting(InstId), State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
logger:error("Received unexpected call: ~p", [Req]),
|
logger:error("Received unexpected call: ~p", [Req]),
|
||||||
{reply, ignored, State}.
|
{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
|
%% suppress the race condition check, as these functions are protected in gproc workers
|
||||||
-dialyzer({nowarn_function, [ do_recreate/4
|
-dialyzer(
|
||||||
, do_create/5
|
{nowarn_function, [
|
||||||
, do_restart/2
|
do_recreate/4,
|
||||||
, do_start/5
|
do_create/5,
|
||||||
, do_stop/1
|
do_restart/2,
|
||||||
, do_health_check/1
|
do_start/5,
|
||||||
, start_and_check/6
|
do_stop/1,
|
||||||
]}).
|
do_health_check/1,
|
||||||
|
start_and_check/6
|
||||||
|
]}
|
||||||
|
).
|
||||||
|
|
||||||
do_recreate(InstId, ResourceType, NewConfig, Opts) ->
|
do_recreate(InstId, ResourceType, NewConfig, Opts) ->
|
||||||
case lookup(InstId) of
|
case lookup(InstId) of
|
||||||
|
|
@ -185,10 +187,11 @@ do_wait_for_resource_ready(_InstId, 0) ->
|
||||||
timeout;
|
timeout;
|
||||||
do_wait_for_resource_ready(InstId, Retry) ->
|
do_wait_for_resource_ready(InstId, Retry) ->
|
||||||
case force_lookup(InstId) of
|
case force_lookup(InstId) of
|
||||||
#{status := connected} -> ok;
|
#{status := connected} ->
|
||||||
|
ok;
|
||||||
_ ->
|
_ ->
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
do_wait_for_resource_ready(InstId, Retry-1)
|
do_wait_for_resource_ready(InstId, Retry - 1)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_create(InstId, Group, ResourceType, Config, Opts) ->
|
do_create(InstId, Group, ResourceType, Config, Opts) ->
|
||||||
|
|
@ -197,8 +200,12 @@ do_create(InstId, Group, ResourceType, Config, Opts) ->
|
||||||
{ok, already_created};
|
{ok, already_created};
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
ok = do_start(InstId, Group, ResourceType, Config, Opts),
|
ok = do_start(InstId, Group, ResourceType, Config, Opts),
|
||||||
ok = emqx_plugin_libs_metrics:create_metrics(resource_metrics, InstId,
|
ok = emqx_plugin_libs_metrics:create_metrics(
|
||||||
[matched, success, failed, exception], [matched]),
|
resource_metrics,
|
||||||
|
InstId,
|
||||||
|
[matched, success, failed, exception],
|
||||||
|
[matched]
|
||||||
|
),
|
||||||
{ok, force_lookup(InstId)}
|
{ok, force_lookup(InstId)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -212,7 +219,8 @@ do_create_dry_run(ResourceType, Config) ->
|
||||||
{error, _} = Error -> Error;
|
{error, _} = Error -> Error;
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
{error, Reason, _} -> {error, Reason}
|
{error, Reason, _} ->
|
||||||
|
{error, Reason}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
|
|
@ -246,13 +254,18 @@ do_restart(InstId, Opts) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_start(InstId, Group, ResourceType, Config, Opts) when is_binary(InstId) ->
|
do_start(InstId, Group, ResourceType, Config, Opts) when is_binary(InstId) ->
|
||||||
InitData = #{id => InstId, mod => ResourceType, config => Config,
|
InitData = #{
|
||||||
status => connecting, state => undefined},
|
id => InstId,
|
||||||
|
mod => ResourceType,
|
||||||
|
config => Config,
|
||||||
|
status => connecting,
|
||||||
|
state => undefined
|
||||||
|
},
|
||||||
%% The `emqx_resource:call_start/3` need the instance exist beforehand
|
%% The `emqx_resource:call_start/3` need the instance exist beforehand
|
||||||
ets:insert(emqx_resource_instance, {InstId, Group, InitData}),
|
ets:insert(emqx_resource_instance, {InstId, Group, InitData}),
|
||||||
spawn(fun() ->
|
spawn(fun() ->
|
||||||
start_and_check(InstId, Group, ResourceType, Config, Opts, InitData)
|
start_and_check(InstId, Group, ResourceType, Config, Opts, InitData)
|
||||||
end),
|
end),
|
||||||
_ = wait_for_resource_ready(InstId, maps:get(wait_for_resource_ready, Opts, 5000)),
|
_ = wait_for_resource_ready(InstId, maps:get(wait_for_resource_ready, Opts, 5000)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
@ -268,9 +281,11 @@ start_and_check(InstId, Group, ResourceType, Config, Opts, Data) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
create_default_checker(InstId, Opts) ->
|
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_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_stop(InstId) when is_binary(InstId) ->
|
||||||
do_with_group_and_instance_data(InstId, fun do_stop/2, []).
|
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) ->
|
do_health_check(Group, #{id := InstId, mod := Mod, state := ResourceState0} = Data) ->
|
||||||
case emqx_resource:call_health_check(InstId, Mod, ResourceState0) of
|
case emqx_resource:call_health_check(InstId, Mod, ResourceState0) of
|
||||||
{ok, ResourceState1} ->
|
{ok, ResourceState1} ->
|
||||||
ets:insert(emqx_resource_instance,
|
ets:insert(
|
||||||
{InstId, Group, Data#{status => connected, state => ResourceState1}}),
|
emqx_resource_instance,
|
||||||
|
{InstId, Group, Data#{status => connected, state => ResourceState1}}
|
||||||
|
),
|
||||||
ok;
|
ok;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
logger:error("health check for ~p failed: ~p", [InstId, Reason]),
|
logger:error("health check for ~p failed: ~p", [InstId, Reason]),
|
||||||
ets:insert(emqx_resource_instance,
|
ets:insert(
|
||||||
{InstId, Group, Data#{status => connecting}}),
|
emqx_resource_instance,
|
||||||
|
{InstId, Group, Data#{status => connecting}}
|
||||||
|
),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
{error, Reason, ResourceState1} ->
|
{error, Reason, ResourceState1} ->
|
||||||
logger:error("health check for ~p failed: ~p", [InstId, Reason]),
|
logger:error("health check for ~p failed: ~p", [InstId, Reason]),
|
||||||
ets:insert(emqx_resource_instance,
|
ets:insert(
|
||||||
{InstId, Group, Data#{status => connecting, state => ResourceState1}}),
|
emqx_resource_instance,
|
||||||
|
{InstId, Group, Data#{status => connecting, state => ResourceState1}}
|
||||||
|
),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -311,7 +332,8 @@ do_set_resource_status_connecting(InstId) ->
|
||||||
{ok, Group, #{id := InstId} = Data} ->
|
{ok, Group, #{id := InstId} = Data} ->
|
||||||
logger:error("health check for ~p failed: timeout", [InstId]),
|
logger:error("health check for ~p failed: timeout", [InstId]),
|
||||||
ets:insert(emqx_resource_instance, {InstId, Group, Data#{status => connecting}});
|
ets:insert(emqx_resource_instance, {InstId, Group, Data#{status => connecting}});
|
||||||
Error -> {error, Error}
|
Error ->
|
||||||
|
{error, Error}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
-define(RESOURCE_INST_MOD, emqx_resource_instance).
|
-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() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
@ -40,27 +41,39 @@ init([]) ->
|
||||||
ResourceInsts = [
|
ResourceInsts = [
|
||||||
begin
|
begin
|
||||||
ensure_pool_worker(Pool, {Pool, Idx}, Idx),
|
ensure_pool_worker(Pool, {Pool, Idx}, Idx),
|
||||||
#{id => {Mod, Idx},
|
#{
|
||||||
start => {Mod, start_link, [Pool, Idx]},
|
id => {Mod, Idx},
|
||||||
restart => transient,
|
start => {Mod, start_link, [Pool, Idx]},
|
||||||
shutdown => 5000, type => worker, modules => [Mod]}
|
restart => transient,
|
||||||
end || Idx <- lists:seq(1, ?POOL_SIZE)],
|
shutdown => 5000,
|
||||||
HealthCheck =
|
type => worker,
|
||||||
#{id => emqx_resource_health_check_sup,
|
modules => [Mod]
|
||||||
start => {emqx_resource_health_check_sup, start_link, []},
|
}
|
||||||
restart => transient,
|
end
|
||||||
shutdown => infinity, type => supervisor, modules => [emqx_resource_health_check_sup]},
|
|| 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]}}.
|
{ok, {SupFlags, [HealthCheck, Metrics | ResourceInsts]}}.
|
||||||
|
|
||||||
%% internal functions
|
%% internal functions
|
||||||
ensure_pool(Pool, Type, Opts) ->
|
ensure_pool(Pool, Type, Opts) ->
|
||||||
try gproc_pool:new(Pool, Type, Opts)
|
try
|
||||||
|
gproc_pool:new(Pool, Type, Opts)
|
||||||
catch
|
catch
|
||||||
error:exists -> ok
|
error:exists -> ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
ensure_pool_worker(Pool, Name, Slot) ->
|
ensure_pool_worker(Pool, Name, Slot) ->
|
||||||
try gproc_pool:add_worker(Pool, Name, Slot)
|
try
|
||||||
|
gproc_pool:add_worker(Pool, Name, Slot)
|
||||||
catch
|
catch
|
||||||
error:exists -> ok
|
error:exists -> ok
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
|
|
||||||
-module(emqx_resource_validator).
|
-module(emqx_resource_validator).
|
||||||
|
|
||||||
-export([ min/2
|
-export([
|
||||||
, max/2
|
min/2,
|
||||||
, not_empty/1
|
max/2,
|
||||||
]).
|
not_empty/1
|
||||||
|
]).
|
||||||
|
|
||||||
max(Type, Max) ->
|
max(Type, Max) ->
|
||||||
limit(Type, '=<', Max).
|
limit(Type, '=<', Max).
|
||||||
|
|
@ -28,16 +29,19 @@ min(Type, Min) ->
|
||||||
limit(Type, '>=', Min).
|
limit(Type, '>=', Min).
|
||||||
|
|
||||||
not_empty(ErrMsg) ->
|
not_empty(ErrMsg) ->
|
||||||
fun(<<>>) -> {error, ErrMsg};
|
fun
|
||||||
(_) -> ok
|
(<<>>) -> {error, ErrMsg};
|
||||||
|
(_) -> ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
limit(Type, Op, Expected) ->
|
limit(Type, Op, Expected) ->
|
||||||
L = len(Type),
|
L = len(Type),
|
||||||
fun(Value) ->
|
fun(Value) ->
|
||||||
Got = L(Value),
|
Got = L(Value),
|
||||||
return(erlang:Op(Got, Expected),
|
return(
|
||||||
err_limit({Type, {Op, Expected}, {got, Got}}))
|
erlang:Op(Got, Expected),
|
||||||
|
err_limit({Type, {Op, Expected}, {got, Got}})
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
len(array) -> fun erlang:length/1;
|
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]).
|
io_lib:format("Expect the ~ts value ~ts ~p but got: ~p", [Type, Op, Expected, Got]).
|
||||||
|
|
||||||
return(true, _) -> ok;
|
return(true, _) -> ok;
|
||||||
return(false, Error) ->
|
return(false, Error) -> {error, Error}.
|
||||||
{error, Error}.
|
|
||||||
|
|
|
||||||
|
|
@ -18,52 +18,58 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
|
introduced_in/0,
|
||||||
|
|
||||||
, create/5
|
create/5,
|
||||||
, create_dry_run/2
|
create_dry_run/2,
|
||||||
, recreate/4
|
recreate/4,
|
||||||
, remove/1
|
remove/1,
|
||||||
, reset_metrics/1
|
reset_metrics/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
introduced_in() ->
|
introduced_in() ->
|
||||||
"5.0.0".
|
"5.0.0".
|
||||||
|
|
||||||
-spec create( emqx_resource:instance_id()
|
-spec create(
|
||||||
, emqx_resource:resource_group()
|
emqx_resource:instance_id(),
|
||||||
, emqx_resource:resource_type()
|
emqx_resource:resource_group(),
|
||||||
, emqx_resource:resource_config()
|
emqx_resource:resource_type(),
|
||||||
, emqx_resource:create_opts()
|
emqx_resource:resource_config(),
|
||||||
) ->
|
emqx_resource:create_opts()
|
||||||
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
) ->
|
||||||
|
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
||||||
create(InstId, Group, ResourceType, Config, Opts) ->
|
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()
|
-spec create_dry_run(
|
||||||
, emqx_resource:resource_config()
|
emqx_resource:resource_type(),
|
||||||
) ->
|
emqx_resource:resource_config()
|
||||||
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
) ->
|
||||||
|
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
||||||
create_dry_run(ResourceType, Config) ->
|
create_dry_run(ResourceType, Config) ->
|
||||||
emqx_cluster_rpc:multicall(emqx_resource, create_dry_run_local, [ResourceType, Config]).
|
emqx_cluster_rpc:multicall(emqx_resource, create_dry_run_local, [ResourceType, Config]).
|
||||||
|
|
||||||
-spec recreate( emqx_resource:instance_id()
|
-spec recreate(
|
||||||
, emqx_resource:resource_type()
|
emqx_resource:instance_id(),
|
||||||
, emqx_resource:resource_config()
|
emqx_resource:resource_type(),
|
||||||
, emqx_resource:create_opts()
|
emqx_resource:resource_config(),
|
||||||
) ->
|
emqx_resource:create_opts()
|
||||||
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
) ->
|
||||||
|
emqx_cluster_rpc:multicall_return(emqx_resource:resource_data()).
|
||||||
recreate(InstId, ResourceType, Config, Opts) ->
|
recreate(InstId, ResourceType, Config, Opts) ->
|
||||||
emqx_cluster_rpc:multicall(emqx_resource, recreate_local, [InstId, ResourceType, Config, Opts]).
|
emqx_cluster_rpc:multicall(emqx_resource, recreate_local, [InstId, ResourceType, Config, Opts]).
|
||||||
|
|
||||||
-spec remove(emqx_resource:instance_id()) ->
|
-spec remove(emqx_resource:instance_id()) ->
|
||||||
emqx_cluster_rpc:multicall_return(ok).
|
emqx_cluster_rpc:multicall_return(ok).
|
||||||
remove(InstId) ->
|
remove(InstId) ->
|
||||||
emqx_cluster_rpc:multicall(emqx_resource, remove_local, [InstId]).
|
emqx_cluster_rpc:multicall(emqx_resource, remove_local, [InstId]).
|
||||||
|
|
||||||
-spec reset_metrics(emqx_resource:instance_id()) ->
|
-spec reset_metrics(emqx_resource:instance_id()) ->
|
||||||
emqx_cluster_rpc:multicall_return(ok).
|
emqx_cluster_rpc:multicall_return(ok).
|
||||||
reset_metrics(InstId) ->
|
reset_metrics(InstId) ->
|
||||||
emqx_cluster_rpc:multicall(emqx_resource, reset_metrics_local, [InstId]).
|
emqx_cluster_rpc:multicall(emqx_resource, reset_metrics_local, [InstId]).
|
||||||
|
|
|
||||||
|
|
@ -60,22 +60,25 @@ t_check_config(_) ->
|
||||||
|
|
||||||
t_create_remove(_) ->
|
t_create_remove(_) ->
|
||||||
{error, _} = emqx_resource:check_and_create_local(
|
{error, _} = emqx_resource:check_and_create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{unknown => test_resource}),
|
#{unknown => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:create(
|
{ok, _} = emqx_resource:create(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource}),
|
#{name => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
emqx_resource:recreate(
|
emqx_resource:recreate(
|
||||||
?ID,
|
?ID,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource},
|
#{name => test_resource},
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
||||||
|
|
||||||
?assert(is_process_alive(Pid)),
|
?assert(is_process_alive(Pid)),
|
||||||
|
|
@ -87,22 +90,25 @@ t_create_remove(_) ->
|
||||||
|
|
||||||
t_create_remove_local(_) ->
|
t_create_remove_local(_) ->
|
||||||
{error, _} = emqx_resource:check_and_create_local(
|
{error, _} = emqx_resource:check_and_create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{unknown => test_resource}),
|
#{unknown => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource}),
|
#{name => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
emqx_resource:recreate_local(
|
emqx_resource:recreate_local(
|
||||||
?ID,
|
?ID,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource},
|
#{name => test_resource},
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
||||||
|
|
||||||
?assert(is_process_alive(Pid)),
|
?assert(is_process_alive(Pid)),
|
||||||
|
|
@ -110,10 +116,11 @@ t_create_remove_local(_) ->
|
||||||
emqx_resource:set_resource_status_connecting(?ID),
|
emqx_resource:set_resource_status_connecting(?ID),
|
||||||
|
|
||||||
emqx_resource:recreate_local(
|
emqx_resource:recreate_local(
|
||||||
?ID,
|
?ID,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource},
|
#{name => test_resource},
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_resource:remove_local(?ID),
|
ok = emqx_resource:remove_local(?ID),
|
||||||
{error, _} = emqx_resource:remove_local(?ID),
|
{error, _} = emqx_resource:remove_local(?ID),
|
||||||
|
|
@ -122,10 +129,11 @@ t_create_remove_local(_) ->
|
||||||
|
|
||||||
t_query(_) ->
|
t_query(_) ->
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource}),
|
#{name => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
Pid = self(),
|
Pid = self(),
|
||||||
Success = fun() -> Pid ! success end,
|
Success = fun() -> Pid ! success end,
|
||||||
|
|
@ -142,28 +150,32 @@ t_query(_) ->
|
||||||
?assert(false)
|
?assert(false)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
?assertMatch({error, {emqx_resource, #{reason := not_found}}},
|
?assertMatch(
|
||||||
emqx_resource:query(<<"unknown">>, get_state)),
|
{error, {emqx_resource, #{reason := not_found}}},
|
||||||
|
emqx_resource:query(<<"unknown">>, get_state)
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_resource:remove_local(?ID).
|
ok = emqx_resource:remove_local(?ID).
|
||||||
|
|
||||||
t_healthy_timeout(_) ->
|
t_healthy_timeout(_) ->
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => <<"test_resource">>},
|
#{name => <<"test_resource">>},
|
||||||
#{health_check_timeout => 200}),
|
#{health_check_timeout => 200}
|
||||||
|
),
|
||||||
timer:sleep(500),
|
timer:sleep(500),
|
||||||
|
|
||||||
ok = emqx_resource:remove_local(?ID).
|
ok = emqx_resource:remove_local(?ID).
|
||||||
|
|
||||||
t_healthy(_) ->
|
t_healthy(_) ->
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => <<"test_resource">>}),
|
#{name => <<"test_resource">>}
|
||||||
|
),
|
||||||
timer:sleep(400),
|
timer:sleep(400),
|
||||||
|
|
||||||
emqx_resource_health_check:create_checker(?ID, 15000, 10000),
|
emqx_resource_health_check:create_checker(?ID, 15000, 10000),
|
||||||
|
|
@ -175,38 +187,44 @@ t_healthy(_) ->
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
[#{status := connected}],
|
[#{status := connected}],
|
||||||
emqx_resource:list_instances_verbose()),
|
emqx_resource:list_instances_verbose()
|
||||||
|
),
|
||||||
|
|
||||||
erlang:exit(Pid, shutdown),
|
erlang:exit(Pid, shutdown),
|
||||||
|
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
{error, dead},
|
{error, dead},
|
||||||
emqx_resource:health_check(?ID)),
|
emqx_resource:health_check(?ID)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
[#{status := connecting}],
|
[#{status := connecting}],
|
||||||
emqx_resource:list_instances_verbose()),
|
emqx_resource:list_instances_verbose()
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_resource:remove_local(?ID).
|
ok = emqx_resource:remove_local(?ID).
|
||||||
|
|
||||||
t_stop_start(_) ->
|
t_stop_start(_) ->
|
||||||
{error, _} = emqx_resource:check_and_create(
|
{error, _} = emqx_resource:check_and_create(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{unknown => test_resource}),
|
#{unknown => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:check_and_create(
|
{ok, _} = emqx_resource:check_and_create(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{<<"name">> => <<"test_resource">>}),
|
#{<<"name">> => <<"test_resource">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:check_and_recreate(
|
{ok, _} = emqx_resource:check_and_recreate(
|
||||||
?ID,
|
?ID,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{<<"name">> => <<"test_resource">>},
|
#{<<"name">> => <<"test_resource">>},
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
#{pid := Pid0} = emqx_resource:query(?ID, get_state),
|
#{pid := Pid0} = emqx_resource:query(?ID, get_state),
|
||||||
|
|
||||||
|
|
@ -216,8 +234,10 @@ t_stop_start(_) ->
|
||||||
|
|
||||||
?assertNot(is_process_alive(Pid0)),
|
?assertNot(is_process_alive(Pid0)),
|
||||||
|
|
||||||
?assertMatch({error, {emqx_resource, #{reason := disconnected}}},
|
?assertMatch(
|
||||||
emqx_resource:query(?ID, get_state)),
|
{error, {emqx_resource, #{reason := disconnected}}},
|
||||||
|
emqx_resource:query(?ID, get_state)
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_resource:restart(?ID),
|
ok = emqx_resource:restart(?ID),
|
||||||
|
|
||||||
|
|
@ -229,22 +249,25 @@ t_stop_start(_) ->
|
||||||
|
|
||||||
t_stop_start_local(_) ->
|
t_stop_start_local(_) ->
|
||||||
{error, _} = emqx_resource:check_and_create_local(
|
{error, _} = emqx_resource:check_and_create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{unknown => test_resource}),
|
#{unknown => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:check_and_create_local(
|
{ok, _} = emqx_resource:check_and_create_local(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{<<"name">> => <<"test_resource">>}),
|
#{<<"name">> => <<"test_resource">>}
|
||||||
|
),
|
||||||
|
|
||||||
{ok, _} = emqx_resource:check_and_recreate_local(
|
{ok, _} = emqx_resource:check_and_recreate_local(
|
||||||
?ID,
|
?ID,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{<<"name">> => <<"test_resource">>},
|
#{<<"name">> => <<"test_resource">>},
|
||||||
#{}),
|
#{}
|
||||||
|
),
|
||||||
|
|
||||||
#{pid := Pid0} = emqx_resource:query(?ID, get_state),
|
#{pid := Pid0} = emqx_resource:query(?ID, get_state),
|
||||||
|
|
||||||
|
|
@ -254,8 +277,10 @@ t_stop_start_local(_) ->
|
||||||
|
|
||||||
?assertNot(is_process_alive(Pid0)),
|
?assertNot(is_process_alive(Pid0)),
|
||||||
|
|
||||||
?assertMatch({error, {emqx_resource, #{reason := disconnected}}},
|
?assertMatch(
|
||||||
emqx_resource:query(?ID, get_state)),
|
{error, {emqx_resource, #{reason := disconnected}}},
|
||||||
|
emqx_resource:query(?ID, get_state)
|
||||||
|
),
|
||||||
|
|
||||||
ok = emqx_resource:restart(?ID),
|
ok = emqx_resource:restart(?ID),
|
||||||
|
|
||||||
|
|
@ -265,60 +290,73 @@ t_stop_start_local(_) ->
|
||||||
|
|
||||||
t_list_filter(_) ->
|
t_list_filter(_) ->
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
emqx_resource:generate_id(<<"a">>),
|
emqx_resource:generate_id(<<"a">>),
|
||||||
<<"group1">>,
|
<<"group1">>,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => a}),
|
#{name => a}
|
||||||
|
),
|
||||||
{ok, _} = emqx_resource:create_local(
|
{ok, _} = emqx_resource:create_local(
|
||||||
emqx_resource:generate_id(<<"a">>),
|
emqx_resource:generate_id(<<"a">>),
|
||||||
<<"group2">>,
|
<<"group2">>,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => grouped_a}),
|
#{name => grouped_a}
|
||||||
|
),
|
||||||
|
|
||||||
[Id1] = emqx_resource:list_group_instances(<<"group1">>),
|
[Id1] = emqx_resource:list_group_instances(<<"group1">>),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, <<"group1">>, #{config := #{name := a}}},
|
{ok, <<"group1">>, #{config := #{name := a}}},
|
||||||
emqx_resource:get_instance(Id1)),
|
emqx_resource:get_instance(Id1)
|
||||||
|
),
|
||||||
|
|
||||||
[Id2] = emqx_resource:list_group_instances(<<"group2">>),
|
[Id2] = emqx_resource:list_group_instances(<<"group2">>),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, <<"group2">>, #{config := #{name := grouped_a}}},
|
{ok, <<"group2">>, #{config := #{name := grouped_a}}},
|
||||||
emqx_resource:get_instance(Id2)).
|
emqx_resource:get_instance(Id2)
|
||||||
|
).
|
||||||
|
|
||||||
t_create_dry_run_local(_) ->
|
t_create_dry_run_local(_) ->
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
ok,
|
ok,
|
||||||
emqx_resource:create_dry_run_local(
|
emqx_resource:create_dry_run_local(
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource, register => true})),
|
#{name => test_resource, register => true}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(undefined, whereis(test_resource)).
|
?assertEqual(undefined, whereis(test_resource)).
|
||||||
|
|
||||||
t_create_dry_run_local_failed(_) ->
|
t_create_dry_run_local_failed(_) ->
|
||||||
{Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE,
|
{Res, _} = emqx_resource:create_dry_run_local(
|
||||||
#{cteate_error => true}),
|
?TEST_RESOURCE,
|
||||||
|
#{cteate_error => true}
|
||||||
|
),
|
||||||
?assertEqual(error, Res),
|
?assertEqual(error, Res),
|
||||||
|
|
||||||
{Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE,
|
{Res, _} = emqx_resource:create_dry_run_local(
|
||||||
#{name => test_resource, health_check_error => true}),
|
?TEST_RESOURCE,
|
||||||
|
#{name => test_resource, health_check_error => true}
|
||||||
|
),
|
||||||
?assertEqual(error, Res),
|
?assertEqual(error, Res),
|
||||||
|
|
||||||
{Res, _} = emqx_resource:create_dry_run_local(?TEST_RESOURCE,
|
{Res, _} = emqx_resource:create_dry_run_local(
|
||||||
#{name => test_resource, stop_error => true}),
|
?TEST_RESOURCE,
|
||||||
|
#{name => test_resource, stop_error => true}
|
||||||
|
),
|
||||||
?assertEqual(error, Res).
|
?assertEqual(error, Res).
|
||||||
|
|
||||||
t_test_func(_) ->
|
t_test_func(_) ->
|
||||||
?assertEqual(ok, erlang:apply(emqx_resource_validator:not_empty("not_empty"), [<<"someval">>])),
|
?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: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"])).
|
?assertEqual(ok, erlang:apply(emqx_resource_validator:max(string, 10), ["less10"])).
|
||||||
|
|
||||||
t_reset_metrics(_) ->
|
t_reset_metrics(_) ->
|
||||||
{ok, _} = emqx_resource:create(
|
{ok, _} = emqx_resource:create(
|
||||||
?ID,
|
?ID,
|
||||||
?DEFAULT_RESOURCE_GROUP,
|
?DEFAULT_RESOURCE_GROUP,
|
||||||
?TEST_RESOURCE,
|
?TEST_RESOURCE,
|
||||||
#{name => test_resource}),
|
#{name => test_resource}
|
||||||
|
),
|
||||||
|
|
||||||
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
#{pid := Pid} = emqx_resource:query(?ID, get_state),
|
||||||
emqx_resource:reset_metrics(?ID),
|
emqx_resource:reset_metrics(?ID),
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,21 @@
|
||||||
-behaviour(emqx_resource).
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
%% callbacks of behaviour emqx_resource
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([ on_start/2
|
-export([
|
||||||
, on_stop/2
|
on_start/2,
|
||||||
, on_query/4
|
on_stop/2,
|
||||||
, on_health_check/2
|
on_query/4,
|
||||||
]).
|
on_health_check/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% callbacks for emqx_resource config schema
|
%% callbacks for emqx_resource config schema
|
||||||
-export([roots/0]).
|
-export([roots/0]).
|
||||||
|
|
||||||
roots() -> [{name, fun name/1},
|
roots() ->
|
||||||
{register, fun register/1}].
|
[
|
||||||
|
{name, fun name/1},
|
||||||
|
{register, fun register/1}
|
||||||
|
].
|
||||||
|
|
||||||
name(type) -> atom();
|
name(type) -> atom();
|
||||||
name(required) -> true;
|
name(required) -> true;
|
||||||
|
|
@ -46,21 +50,27 @@ on_start(_InstId, #{create_error := true}) ->
|
||||||
error("some error");
|
error("some error");
|
||||||
on_start(InstId, #{name := Name, stop_error := true} = Opts) ->
|
on_start(InstId, #{name := Name, stop_error := true} = Opts) ->
|
||||||
Register = maps:get(register, Opts, false),
|
Register = maps:get(register, Opts, false),
|
||||||
{ok, #{name => Name,
|
{ok, #{
|
||||||
id => InstId,
|
name => Name,
|
||||||
stop_error => true,
|
id => InstId,
|
||||||
pid => spawn_dummy_process(Name, Register)}};
|
stop_error => true,
|
||||||
|
pid => spawn_dummy_process(Name, Register)
|
||||||
|
}};
|
||||||
on_start(InstId, #{name := Name, health_check_error := true} = Opts) ->
|
on_start(InstId, #{name := Name, health_check_error := true} = Opts) ->
|
||||||
Register = maps:get(register, Opts, false),
|
Register = maps:get(register, Opts, false),
|
||||||
{ok, #{name => Name,
|
{ok, #{
|
||||||
id => InstId,
|
name => Name,
|
||||||
health_check_error => true,
|
id => InstId,
|
||||||
pid => spawn_dummy_process(Name, Register)}};
|
health_check_error => true,
|
||||||
|
pid => spawn_dummy_process(Name, Register)
|
||||||
|
}};
|
||||||
on_start(InstId, #{name := Name} = Opts) ->
|
on_start(InstId, #{name := Name} = Opts) ->
|
||||||
Register = maps:get(register, Opts, false),
|
Register = maps:get(register, Opts, false),
|
||||||
{ok, #{name => Name,
|
{ok, #{
|
||||||
id => InstId,
|
name => Name,
|
||||||
pid => spawn_dummy_process(Name, Register)}}.
|
id => InstId,
|
||||||
|
pid => spawn_dummy_process(Name, Register)
|
||||||
|
}}.
|
||||||
|
|
||||||
on_stop(_InstId, #{stop_error := true}) ->
|
on_stop(_InstId, #{stop_error := true}) ->
|
||||||
{error, stop_error};
|
{error, stop_error};
|
||||||
|
|
@ -86,13 +96,15 @@ on_health_check(_InstId, State = #{pid := Pid}) ->
|
||||||
|
|
||||||
spawn_dummy_process(Name, Register) ->
|
spawn_dummy_process(Name, Register) ->
|
||||||
spawn(
|
spawn(
|
||||||
fun() ->
|
fun() ->
|
||||||
true = case Register of
|
true =
|
||||||
true -> register(Name, self());
|
case Register of
|
||||||
_ -> true
|
true -> register(Name, self());
|
||||||
end,
|
_ -> true
|
||||||
Ref = make_ref(),
|
end,
|
||||||
receive
|
Ref = make_ref(),
|
||||||
Ref -> ok
|
receive
|
||||||
end
|
Ref -> ok
|
||||||
end).
|
end
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
-type rule_id() :: binary().
|
-type rule_id() :: binary().
|
||||||
-type rule_name() :: binary().
|
-type rule_name() :: binary().
|
||||||
|
|
||||||
-type mf() :: {Module::atom(), Fun::atom()}.
|
-type mf() :: {Module :: atom(), Fun :: atom()}.
|
||||||
|
|
||||||
-type hook() :: atom() | 'any'.
|
-type hook() :: atom() | 'any'.
|
||||||
-type topic() :: binary().
|
-type topic() :: binary().
|
||||||
|
|
@ -36,60 +36,73 @@
|
||||||
-type bridge_channel_id() :: binary().
|
-type bridge_channel_id() :: binary().
|
||||||
-type output_fun_args() :: map().
|
-type output_fun_args() :: map().
|
||||||
|
|
||||||
-type output() :: #{
|
-type output() ::
|
||||||
mod := builtin_output_module() | module(),
|
#{
|
||||||
func := builtin_output_func() | atom(),
|
mod := builtin_output_module() | module(),
|
||||||
args => output_fun_args()
|
func := builtin_output_func() | atom(),
|
||||||
} | bridge_channel_id().
|
args => output_fun_args()
|
||||||
|
}
|
||||||
|
| bridge_channel_id().
|
||||||
|
|
||||||
-type rule() ::
|
-type rule() ::
|
||||||
#{ id := rule_id()
|
#{
|
||||||
, name := binary()
|
id := rule_id(),
|
||||||
, sql := binary()
|
name := binary(),
|
||||||
, outputs := [output()]
|
sql := binary(),
|
||||||
, enable := boolean()
|
outputs := [output()],
|
||||||
, description => binary()
|
enable := boolean(),
|
||||||
, created_at := integer() %% epoch in millisecond precision
|
description => binary(),
|
||||||
, updated_at := integer() %% epoch in millisecond precision
|
%% epoch in millisecond precision
|
||||||
, from := list(topic())
|
created_at := integer(),
|
||||||
, is_foreach := boolean()
|
%% epoch in millisecond precision
|
||||||
, fields := list()
|
updated_at := integer(),
|
||||||
, doeach := term()
|
from := list(topic()),
|
||||||
, incase := term()
|
is_foreach := boolean(),
|
||||||
, conditions := tuple()
|
fields := list(),
|
||||||
}.
|
doeach := term(),
|
||||||
|
incase := term(),
|
||||||
|
conditions := tuple()
|
||||||
|
}.
|
||||||
|
|
||||||
%% Arithmetic operators
|
%% Arithmetic operators
|
||||||
-define(is_arith(Op), (Op =:= '+' orelse
|
-define(is_arith(Op),
|
||||||
Op =:= '-' orelse
|
(Op =:= '+' orelse
|
||||||
Op =:= '*' orelse
|
Op =:= '-' orelse
|
||||||
Op =:= '/' orelse
|
Op =:= '*' orelse
|
||||||
Op =:= 'div')).
|
Op =:= '/' orelse
|
||||||
|
Op =:= 'div')
|
||||||
|
).
|
||||||
|
|
||||||
%% Compare operators
|
%% Compare operators
|
||||||
-define(is_comp(Op), (Op =:= '=' orelse
|
-define(is_comp(Op),
|
||||||
Op =:= '=~' orelse
|
(Op =:= '=' orelse
|
||||||
Op =:= '>' orelse
|
Op =:= '=~' orelse
|
||||||
Op =:= '<' orelse
|
Op =:= '>' orelse
|
||||||
Op =:= '<=' orelse
|
Op =:= '<' orelse
|
||||||
Op =:= '>=' orelse
|
Op =:= '<=' orelse
|
||||||
Op =:= '<>' orelse
|
Op =:= '>=' orelse
|
||||||
Op =:= '!=')).
|
Op =:= '<>' orelse
|
||||||
|
Op =:= '!=')
|
||||||
|
).
|
||||||
|
|
||||||
%% Logical operators
|
%% Logical operators
|
||||||
-define(is_logical(Op), (Op =:= 'and' orelse Op =:= 'or')).
|
-define(is_logical(Op), (Op =:= 'and' orelse Op =:= 'or')).
|
||||||
|
|
||||||
-define(RAISE(_EXP_, _ERROR_),
|
-define(RAISE(_EXP_, _ERROR_),
|
||||||
?RAISE(_EXP_, _ = do_nothing, _ERROR_)).
|
?RAISE(_EXP_, _ = do_nothing, _ERROR_)
|
||||||
|
).
|
||||||
|
|
||||||
-define(RAISE(_EXP_, _EXP_ON_FAIL_, _ERROR_),
|
-define(RAISE(_EXP_, _EXP_ON_FAIL_, _ERROR_),
|
||||||
fun() ->
|
fun() ->
|
||||||
try (_EXP_)
|
try
|
||||||
catch _EXCLASS_:_EXCPTION_:_ST_ ->
|
(_EXP_)
|
||||||
|
catch
|
||||||
|
_EXCLASS_:_EXCPTION_:_ST_ ->
|
||||||
_EXP_ON_FAIL_,
|
_EXP_ON_FAIL_,
|
||||||
throw(_ERROR_)
|
throw(_ERROR_)
|
||||||
end
|
end
|
||||||
end()).
|
end()
|
||||||
|
).
|
||||||
|
|
||||||
%% Tables
|
%% Tables
|
||||||
-define(RULE_TAB, emqx_rule_engine).
|
-define(RULE_TAB, emqx_rule_engine).
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
]}.
|
|
||||||
|
|
||||||
{erl_opts, [warn_unused_vars,
|
{erl_opts, [
|
||||||
warn_shadow_vars,
|
warn_unused_vars,
|
||||||
warn_unused_import,
|
warn_shadow_vars,
|
||||||
warn_obsolete_guard,
|
warn_unused_import,
|
||||||
no_debug_info,
|
warn_obsolete_guard,
|
||||||
compressed, %% for edge
|
no_debug_info,
|
||||||
{parse_transform}
|
%% for edge
|
||||||
]}.
|
compressed,
|
||||||
|
{parse_transform}
|
||||||
|
]}.
|
||||||
|
|
||||||
{overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}.
|
{overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}.
|
||||||
|
|
||||||
{edoc_opts, [{preprocess, true}]}.
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
|
||||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
{xref_checks, [
|
||||||
locals_not_used, deprecated_function_calls,
|
undefined_function_calls,
|
||||||
warnings_as_errors, deprecated_functions
|
undefined_functions,
|
||||||
]}.
|
locals_not_used,
|
||||||
|
deprecated_function_calls,
|
||||||
|
warnings_as_errors,
|
||||||
|
deprecated_functions
|
||||||
|
]}.
|
||||||
|
|
||||||
{cover_enabled, true}.
|
{cover_enabled, true}.
|
||||||
{cover_opts, [verbose]}.
|
{cover_opts, [verbose]}.
|
||||||
{cover_export_enabled, true}.
|
{cover_export_enabled, true}.
|
||||||
|
|
||||||
{plugins, [rebar3_proper]}.
|
{plugins, [rebar3_proper]}.
|
||||||
|
|
||||||
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ check_params/2
|
-export([check_params/2]).
|
||||||
]).
|
|
||||||
|
|
||||||
-export([roots/0, fields/1]).
|
-export([roots/0, fields/1]).
|
||||||
|
|
||||||
|
|
@ -20,10 +19,11 @@ check_params(Params, Tag) ->
|
||||||
try hocon_tconf:check_plain(?MODULE, #{BTag => Params}, Opts, [Tag]) of
|
try hocon_tconf:check_plain(?MODULE, #{BTag => Params}, Opts, [Tag]) of
|
||||||
#{Tag := Checked} -> {ok, Checked}
|
#{Tag := Checked} -> {ok, Checked}
|
||||||
catch
|
catch
|
||||||
throw : Reason ->
|
throw:Reason ->
|
||||||
?SLOG(error, #{msg => "check_rule_params_failed",
|
?SLOG(error, #{
|
||||||
reason => Reason
|
msg => "check_rule_params_failed",
|
||||||
}),
|
reason => Reason
|
||||||
|
}),
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
@ -31,226 +31,285 @@ check_params(Params, Tag) ->
|
||||||
%% Hocon Schema Definitions
|
%% Hocon Schema Definitions
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[ {"rule_creation", sc(ref("rule_creation"), #{desc => ?DESC("root_rule_creation")})}
|
[
|
||||||
, {"rule_info", sc(ref("rule_info"), #{desc => ?DESC("root_rule_info")})}
|
{"rule_creation", sc(ref("rule_creation"), #{desc => ?DESC("root_rule_creation")})},
|
||||||
, {"rule_events", sc(ref("rule_events"), #{desc => ?DESC("root_rule_events")})}
|
{"rule_info", sc(ref("rule_info"), #{desc => ?DESC("root_rule_info")})},
|
||||||
, {"rule_test", sc(ref("rule_test"), #{desc => ?DESC("root_rule_test")})}
|
{"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") ->
|
fields("rule_creation") ->
|
||||||
emqx_rule_engine_schema:fields("rules");
|
emqx_rule_engine_schema:fields("rules");
|
||||||
|
|
||||||
fields("rule_info") ->
|
fields("rule_info") ->
|
||||||
[ rule_id()
|
[
|
||||||
, {"metrics", sc(ref("metrics"), #{desc => ?DESC("ri_metrics")})}
|
rule_id(),
|
||||||
, {"node_metrics", sc(hoconsc:array(ref("node_metrics")),
|
{"metrics", sc(ref("metrics"), #{desc => ?DESC("ri_metrics")})},
|
||||||
#{ desc => ?DESC("ri_node_metrics")
|
{"node_metrics",
|
||||||
})}
|
sc(
|
||||||
, {"from", sc(hoconsc:array(binary()),
|
hoconsc:array(ref("node_metrics")),
|
||||||
#{desc => ?DESC("ri_from"), example => "t/#"})}
|
#{desc => ?DESC("ri_node_metrics")}
|
||||||
, {"created_at", sc(binary(),
|
)},
|
||||||
#{ desc => ?DESC("ri_created_at")
|
{"from",
|
||||||
, example => "2021-12-01T15:00:43.153+08:00"
|
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");
|
] ++ fields("rule_creation");
|
||||||
|
|
||||||
%% TODO: we can delete this API if the Dashboard not depends on it
|
%% TODO: we can delete this API if the Dashboard not depends on it
|
||||||
fields("rule_events") ->
|
fields("rule_events") ->
|
||||||
ETopics = [binary_to_atom(emqx_rule_events:event_topic(E)) || E <- emqx_rule_events:event_names()],
|
ETopics = [
|
||||||
[ {"event", sc(hoconsc:enum(ETopics), #{desc => ?DESC("rs_event"), required => true})}
|
binary_to_atom(emqx_rule_events:event_topic(E))
|
||||||
, {"title", sc(binary(), #{desc => ?DESC("rs_title"), example => "some title"})}
|
|| E <- emqx_rule_events:event_names()
|
||||||
, {"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")})}
|
{"event", sc(hoconsc:enum(ETopics), #{desc => ?DESC("rs_event"), required => true})},
|
||||||
, {"sql_example", sc(binary(), #{desc => ?DESC("rs_sql_example")})}
|
{"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") ->
|
fields("rule_test") ->
|
||||||
[ {"context", sc(hoconsc:union([ ref("ctx_pub")
|
[
|
||||||
, ref("ctx_sub")
|
{"context",
|
||||||
, ref("ctx_unsub")
|
sc(
|
||||||
, ref("ctx_delivered")
|
hoconsc:union([
|
||||||
, ref("ctx_acked")
|
ref("ctx_pub"),
|
||||||
, ref("ctx_dropped")
|
ref("ctx_sub"),
|
||||||
, ref("ctx_connected")
|
ref("ctx_unsub"),
|
||||||
, ref("ctx_disconnected")
|
ref("ctx_delivered"),
|
||||||
, ref("ctx_connack")
|
ref("ctx_acked"),
|
||||||
, ref("ctx_check_authz_complete")
|
ref("ctx_dropped"),
|
||||||
, ref("ctx_bridge_mqtt")
|
ref("ctx_connected"),
|
||||||
]),
|
ref("ctx_disconnected"),
|
||||||
#{desc => ?DESC("test_context"),
|
ref("ctx_connack"),
|
||||||
default => #{}})}
|
ref("ctx_check_authz_complete"),
|
||||||
, {"sql", sc(binary(), #{desc => ?DESC("test_sql"), required => true})}
|
ref("ctx_bridge_mqtt")
|
||||||
|
]),
|
||||||
|
#{
|
||||||
|
desc => ?DESC("test_context"),
|
||||||
|
default => #{}
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{"sql", sc(binary(), #{desc => ?DESC("test_sql"), required => true})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("metrics") ->
|
fields("metrics") ->
|
||||||
[ {"sql.matched", sc(non_neg_integer(), #{
|
[
|
||||||
desc => ?DESC("metrics_sql_matched")
|
{"sql.matched",
|
||||||
})}
|
sc(non_neg_integer(), #{
|
||||||
, {"sql.matched.rate", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate") })}
|
desc => ?DESC("metrics_sql_matched")
|
||||||
, {"sql.matched.rate.max", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate_max") })}
|
})},
|
||||||
, {"sql.matched.rate.last5m", sc(float(),
|
{"sql.matched.rate", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate")})},
|
||||||
#{desc => ?DESC("metrics_sql_matched_rate_last5m") })}
|
{"sql.matched.rate.max", sc(float(), #{desc => ?DESC("metrics_sql_matched_rate_max")})},
|
||||||
, {"sql.passed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_passed") })}
|
{"sql.matched.rate.last5m",
|
||||||
, {"sql.failed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_failed") })}
|
sc(
|
||||||
, {"sql.failed.exception", sc(non_neg_integer(), #{
|
float(),
|
||||||
desc => ?DESC("metrics_sql_failed_exception")
|
#{desc => ?DESC("metrics_sql_matched_rate_last5m")}
|
||||||
})}
|
)},
|
||||||
, {"sql.failed.unknown", sc(non_neg_integer(), #{
|
{"sql.passed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_passed")})},
|
||||||
desc => ?DESC("metrics_sql_failed_unknown")
|
{"sql.failed", sc(non_neg_integer(), #{desc => ?DESC("metrics_sql_failed")})},
|
||||||
})}
|
{"sql.failed.exception",
|
||||||
, {"outputs.total", sc(non_neg_integer(), #{
|
sc(non_neg_integer(), #{
|
||||||
desc => ?DESC("metrics_outputs_total")
|
desc => ?DESC("metrics_sql_failed_exception")
|
||||||
})}
|
})},
|
||||||
, {"outputs.success", sc(non_neg_integer(), #{
|
{"sql.failed.unknown",
|
||||||
desc => ?DESC("metrics_outputs_success")
|
sc(non_neg_integer(), #{
|
||||||
})}
|
desc => ?DESC("metrics_sql_failed_unknown")
|
||||||
, {"outputs.failed", sc(non_neg_integer(), #{
|
})},
|
||||||
desc => ?DESC("metrics_outputs_failed")
|
{"outputs.total",
|
||||||
})}
|
sc(non_neg_integer(), #{
|
||||||
, {"outputs.failed.out_of_service", sc(non_neg_integer(), #{
|
desc => ?DESC("metrics_outputs_total")
|
||||||
desc => ?DESC("metrics_outputs_failed_out_of_service")
|
})},
|
||||||
})}
|
{"outputs.success",
|
||||||
, {"outputs.failed.unknown", sc(non_neg_integer(), #{
|
sc(non_neg_integer(), #{
|
||||||
desc => ?DESC("metrics_outputs_failed_unknown")
|
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") ->
|
fields("node_metrics") ->
|
||||||
[ {"node", sc(binary(), #{desc => ?DESC("node_node"), example => "emqx@127.0.0.1"})}
|
[{"node", sc(binary(), #{desc => ?DESC("node_node"), example => "emqx@127.0.0.1"})}] ++
|
||||||
] ++ fields("metrics");
|
fields("metrics");
|
||||||
|
|
||||||
fields("ctx_pub") ->
|
fields("ctx_pub") ->
|
||||||
[ {"event_type", sc(message_publish, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"id", sc(binary(), #{desc => ?DESC("event_id")})}
|
{"event_type", sc(message_publish, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"id", sc(binary(), #{desc => ?DESC("event_id")})},
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}
|
{"payload", sc(binary(), #{desc => ?DESC("event_payload")})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})},
|
||||||
, {"publish_received_at", sc(integer(), #{
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
desc => ?DESC("event_publish_received_at")})}
|
{"publish_received_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_publish_received_at")
|
||||||
|
})}
|
||||||
] ++ [qos()];
|
] ++ [qos()];
|
||||||
|
|
||||||
fields("ctx_sub") ->
|
fields("ctx_sub") ->
|
||||||
[ {"event_type", sc(session_subscribed, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"event_type",
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
sc(session_subscribed, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"payload", sc(binary(), #{desc => ?DESC("event_payload")})},
|
||||||
, {"publish_received_at", sc(integer(), #{
|
{"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})},
|
||||||
desc => ?DESC("event_publish_received_at")})}
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
|
{"publish_received_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_publish_received_at")
|
||||||
|
})}
|
||||||
] ++ [qos()];
|
] ++ [qos()];
|
||||||
|
|
||||||
fields("ctx_unsub") ->
|
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") ->
|
fields("ctx_delivered") ->
|
||||||
[ {"event_type", sc(message_delivered, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"id", sc(binary(), #{desc => ?DESC("event_id")})}
|
{"event_type",
|
||||||
, {"from_clientid", sc(binary(), #{desc => ?DESC("event_from_clientid")})}
|
sc(message_delivered, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"from_username", sc(binary(), #{desc => ?DESC("event_from_username")})}
|
{"id", sc(binary(), #{desc => ?DESC("event_id")})},
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"from_clientid", sc(binary(), #{desc => ?DESC("event_from_clientid")})},
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
{"from_username", sc(binary(), #{desc => ?DESC("event_from_username")})},
|
||||||
, {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"payload", sc(binary(), #{desc => ?DESC("event_payload")})},
|
||||||
, {"publish_received_at", sc(integer(), #{
|
{"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})},
|
||||||
desc => ?DESC("event_publish_received_at")})}
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
|
{"publish_received_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_publish_received_at")
|
||||||
|
})}
|
||||||
] ++ [qos()];
|
] ++ [qos()];
|
||||||
|
|
||||||
fields("ctx_acked") ->
|
fields("ctx_acked") ->
|
||||||
[{"event_type", sc(message_acked, #{desc => ?DESC("event_event_type"), required => true})}] ++
|
[{"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") ->
|
fields("ctx_dropped") ->
|
||||||
[ {"event_type", sc(message_dropped, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"id", sc(binary(), #{desc => ?DESC("event_id")})}
|
{"event_type", sc(message_dropped, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"reason", sc(binary(), #{desc => ?DESC("event_ctx_dropped")})}
|
{"id", sc(binary(), #{desc => ?DESC("event_id")})},
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"reason", sc(binary(), #{desc => ?DESC("event_ctx_dropped")})},
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}
|
{"payload", sc(binary(), #{desc => ?DESC("event_payload")})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})},
|
||||||
, {"publish_received_at", sc(integer(), #{
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
desc => ?DESC("event_publish_received_at")})}
|
{"publish_received_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_publish_received_at")
|
||||||
|
})}
|
||||||
] ++ [qos()];
|
] ++ [qos()];
|
||||||
|
|
||||||
fields("ctx_connected") ->
|
fields("ctx_connected") ->
|
||||||
[ {"event_type", sc(client_connected, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"event_type",
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
sc(client_connected, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"mountpoint", sc(binary(), #{desc => ?DESC("event_mountpoint")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}
|
{"mountpoint", sc(binary(), #{desc => ?DESC("event_mountpoint")})},
|
||||||
, {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})}
|
{"peername", sc(binary(), #{desc => ?DESC("event_peername")})},
|
||||||
, {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})}
|
{"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})},
|
||||||
, {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})}
|
{"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})},
|
||||||
, {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})}
|
{"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})},
|
||||||
, {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})}
|
{"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})},
|
||||||
, {"is_bridge", sc(boolean(), #{desc => ?DESC("event_is_bridge"), default => false})}
|
{"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})},
|
||||||
, {"connected_at", sc(integer(), #{
|
{"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})},
|
||||||
desc => ?DESC("event_connected_at")})}
|
{"is_bridge", sc(boolean(), #{desc => ?DESC("event_is_bridge"), default => false})},
|
||||||
|
{"connected_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_connected_at")
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("ctx_disconnected") ->
|
fields("ctx_disconnected") ->
|
||||||
[ {"event_type", sc(client_disconnected, #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"event_type",
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
sc(client_disconnected, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"reason", sc(binary(), #{desc => ?DESC("event_ctx_disconnected_reason")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}
|
{"reason", sc(binary(), #{desc => ?DESC("event_ctx_disconnected_reason")})},
|
||||||
, {"disconnected_at", sc(integer(), #{
|
{"peername", sc(binary(), #{desc => ?DESC("event_peername")})},
|
||||||
desc => ?DESC("event_ctx_disconnected_da")})}
|
{"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})},
|
||||||
|
{"disconnected_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_ctx_disconnected_da")
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("ctx_connack") ->
|
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")})}
|
{"event_type", sc(client_connack, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})}
|
{"reason_code", sc(binary(), #{desc => ?DESC("event_ctx_connack_reason_code")})},
|
||||||
, {"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
{"clean_start", sc(boolean(), #{desc => ?DESC("event_clean_start"), default => true})},
|
||||||
, {"peername", sc(binary(), #{desc => ?DESC("event_peername")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})}
|
{"peername", sc(binary(), #{desc => ?DESC("event_peername")})},
|
||||||
, {"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})}
|
{"sockname", sc(binary(), #{desc => ?DESC("event_sockname")})},
|
||||||
, {"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})}
|
{"proto_name", sc(binary(), #{desc => ?DESC("event_proto_name")})},
|
||||||
, {"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})}
|
{"proto_ver", sc(binary(), #{desc => ?DESC("event_proto_ver")})},
|
||||||
, {"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})}
|
{"keepalive", sc(integer(), #{desc => ?DESC("event_keepalive")})},
|
||||||
, {"connected_at", sc(integer(), #{
|
{"expiry_interval", sc(integer(), #{desc => ?DESC("event_expiry_interval")})},
|
||||||
desc => ?DESC("event_connected_at")})}
|
{"connected_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_connected_at")
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields("ctx_check_authz_complete") ->
|
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")})}
|
{"event_type",
|
||||||
, {"username", sc(binary(), #{desc => ?DESC("event_username")})}
|
sc(client_check_authz_complete, #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})}
|
{"clientid", sc(binary(), #{desc => ?DESC("event_clientid")})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"username", sc(binary(), #{desc => ?DESC("event_username")})},
|
||||||
, {"action", sc(binary(), #{desc => ?DESC("event_action")})}
|
{"peerhost", sc(binary(), #{desc => ?DESC("event_peerhost")})},
|
||||||
, {"authz_source", sc(binary(), #{desc => ?DESC("event_authz_source")})}
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
, {"result", sc(binary(), #{desc => ?DESC("event_result")})}
|
{"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") ->
|
fields("ctx_bridge_mqtt") ->
|
||||||
[ {"event_type", sc('$bridges/mqtt:*', #{desc => ?DESC("event_event_type"), required => true})}
|
[
|
||||||
, {"id", sc(binary(), #{desc => ?DESC("event_id")})}
|
{"event_type",
|
||||||
, {"payload", sc(binary(), #{desc => ?DESC("event_payload")})}
|
sc('$bridges/mqtt:*', #{desc => ?DESC("event_event_type"), required => true})},
|
||||||
, {"topic", sc(binary(), #{desc => ?DESC("event_topic")})}
|
{"id", sc(binary(), #{desc => ?DESC("event_id")})},
|
||||||
, {"server", sc(binary(), #{desc => ?DESC("event_server")})}
|
{"payload", sc(binary(), #{desc => ?DESC("event_payload")})},
|
||||||
, {"dup", sc(binary(), #{desc => ?DESC("event_dup")})}
|
{"topic", sc(binary(), #{desc => ?DESC("event_topic")})},
|
||||||
, {"retain", sc(binary(), #{desc => ?DESC("event_retain")})}
|
{"server", sc(binary(), #{desc => ?DESC("event_server")})},
|
||||||
, {"message_received_at", sc(integer(), #{
|
{"dup", sc(binary(), #{desc => ?DESC("event_dup")})},
|
||||||
desc => ?DESC("event_publish_received_at")})}
|
{"retain", sc(binary(), #{desc => ?DESC("event_retain")})},
|
||||||
|
{"message_received_at",
|
||||||
|
sc(integer(), #{
|
||||||
|
desc => ?DESC("event_publish_received_at")
|
||||||
|
})}
|
||||||
] ++ [qos()].
|
] ++ [qos()].
|
||||||
|
|
||||||
qos() ->
|
qos() ->
|
||||||
{"qos", sc(emqx_schema:qos(), #{desc => ?DESC("event_qos")})}.
|
{"qos", sc(emqx_schema:qos(), #{desc => ?DESC("event_qos")})}.
|
||||||
|
|
||||||
rule_id() ->
|
rule_id() ->
|
||||||
{"id", sc(binary(),
|
{"id",
|
||||||
#{ desc => ?DESC("rule_id"), required => true
|
sc(
|
||||||
, example => "293fb66f"
|
binary(),
|
||||||
})}.
|
#{
|
||||||
|
desc => ?DESC("rule_id"),
|
||||||
|
required => true,
|
||||||
|
example => "293fb66f"
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
|
||||||
ref(Field) -> hoconsc:ref(?MODULE, Field).
|
ref(Field) -> hoconsc:ref(?MODULE, Field).
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,27 @@
|
||||||
|
|
||||||
-export([date/3, date/4, parse_date/4]).
|
-export([date/3, date/4, parse_date/4]).
|
||||||
|
|
||||||
-export([ is_int_char/1
|
-export([
|
||||||
, is_symbol_char/1
|
is_int_char/1,
|
||||||
, is_m_char/1
|
is_symbol_char/1,
|
||||||
]).
|
is_m_char/1
|
||||||
|
]).
|
||||||
|
|
||||||
-record(result, {
|
-record(result, {
|
||||||
year = "1970" :: string() %%year()
|
%%year()
|
||||||
, month = "1" :: string() %%month()
|
year = "1970" :: string(),
|
||||||
, day = "1" :: string() %%day()
|
%%month()
|
||||||
, hour = "0" :: string() %%hour()
|
month = "1" :: string(),
|
||||||
, minute = "0" :: string() %%minute() %% epoch in millisecond precision
|
%%day()
|
||||||
, second = "0" :: string() %%second() %% epoch in millisecond precision
|
day = "1" :: string(),
|
||||||
, zone = "+00:00" :: string() %%integer() %% zone maybe some value
|
%%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'
|
%% -type time_unit() :: 'microsecond'
|
||||||
|
|
@ -42,43 +50,59 @@ date(TimeUnit, Offset, FormatString) ->
|
||||||
date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)).
|
date(TimeUnit, Offset, FormatString, erlang:system_time(TimeUnit)).
|
||||||
|
|
||||||
date(TimeUnit, Offset, FormatString, TimeEpoch) ->
|
date(TimeUnit, Offset, FormatString, TimeEpoch) ->
|
||||||
[Head|Other] = string:split(FormatString, "%", all),
|
[Head | Other] = string:split(FormatString, "%", all),
|
||||||
R = create_tag([{st, Head}], Other),
|
R = create_tag([{st, Head}], Other),
|
||||||
Res = lists:map(fun(Expr) ->
|
Res = lists:map(
|
||||||
eval_tag(rmap(make_time(TimeUnit, Offset, TimeEpoch)), Expr) end, R),
|
fun(Expr) ->
|
||||||
|
eval_tag(rmap(make_time(TimeUnit, Offset, TimeEpoch)), Expr)
|
||||||
|
end,
|
||||||
|
R
|
||||||
|
),
|
||||||
lists:concat(Res).
|
lists:concat(Res).
|
||||||
|
|
||||||
parse_date(TimeUnit, Offset, FormatString, InputString) ->
|
parse_date(TimeUnit, Offset, FormatString, InputString) ->
|
||||||
[Head|Other] = string:split(FormatString, "%", all),
|
[Head | Other] = string:split(FormatString, "%", all),
|
||||||
R = create_tag([{st, Head}], Other),
|
R = create_tag([{st, Head}], Other),
|
||||||
IsZ = fun(V) -> case V of
|
IsZ = fun(V) ->
|
||||||
{tag, $Z} -> true;
|
case V of
|
||||||
_ -> false
|
{tag, $Z} -> true;
|
||||||
end end,
|
_ -> false
|
||||||
|
end
|
||||||
|
end,
|
||||||
R1 = lists:filter(IsZ, R),
|
R1 = lists:filter(IsZ, R),
|
||||||
IfFun = fun(Con, A, B) ->
|
IfFun = fun(Con, A, B) ->
|
||||||
case Con of
|
case Con of
|
||||||
[] -> A;
|
[] -> A;
|
||||||
_ -> B
|
_ -> B
|
||||||
end end,
|
end
|
||||||
|
end,
|
||||||
Res = parse_input(FormatString, InputString),
|
Res = parse_input(FormatString, InputString),
|
||||||
Str = Res#result.year ++ "-"
|
Str =
|
||||||
++ Res#result.month ++ "-"
|
Res#result.year ++ "-" ++
|
||||||
++ Res#result.day ++ "T"
|
Res#result.month ++ "-" ++
|
||||||
++ Res#result.hour ++ ":"
|
Res#result.day ++ "T" ++
|
||||||
++ Res#result.minute ++ ":"
|
Res#result.hour ++ ":" ++
|
||||||
++ Res#result.second ++
|
Res#result.minute ++ ":" ++
|
||||||
IfFun(R1, Offset, Res#result.zone),
|
Res#result.second ++
|
||||||
|
IfFun(R1, Offset, Res#result.zone),
|
||||||
calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]).
|
calendar:rfc3339_to_system_time(Str, [{unit, TimeUnit}]).
|
||||||
|
|
||||||
mlist(R)->
|
mlist(R) ->
|
||||||
[ {$H, R#result.hour} %% %H Shows hour in 24-hour format [15]
|
%% %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]
|
{$H, R#result.hour},
|
||||||
, {$y, R#result.year} %% %y Displays year YYYY [2021]
|
%% %M Displays minutes [00-59]
|
||||||
, {$m, R#result.month} %% %m Displays the number of the month [01-12]
|
{$M, R#result.minute},
|
||||||
, {$d, R#result.day} %% %d Displays the number of the month [01-12]
|
%% %S Displays seconds [00-59]
|
||||||
, {$Z, R#result.zone} %% %Z Displays Time zone
|
{$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) ->
|
rmap(Result) ->
|
||||||
|
|
@ -88,69 +112,95 @@ support_char() -> "HMSymdZ".
|
||||||
|
|
||||||
create_tag(Head, []) ->
|
create_tag(Head, []) ->
|
||||||
Head;
|
Head;
|
||||||
create_tag(Head, [Val1|RVal]) ->
|
create_tag(Head, [Val1 | RVal]) ->
|
||||||
case Val1 of
|
case Val1 of
|
||||||
[] -> create_tag(Head ++ [{st, [$%]}], RVal);
|
[] ->
|
||||||
[H| Other] ->
|
create_tag(Head ++ [{st, [$%]}], RVal);
|
||||||
|
[H | Other] ->
|
||||||
case lists:member(H, support_char()) of
|
case lists:member(H, support_char()) of
|
||||||
true -> create_tag(Head ++ [{tag, H}, {st, Other}], RVal);
|
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
|
||||||
end.
|
end.
|
||||||
|
|
||||||
eval_tag(_,{st, Str}) ->
|
eval_tag(_, {st, Str}) ->
|
||||||
Str;
|
Str;
|
||||||
eval_tag(Map,{tag, Char}) ->
|
eval_tag(Map, {tag, Char}) ->
|
||||||
maps:get(Char, Map, "undefined").
|
maps:get(Char, Map, "undefined").
|
||||||
|
|
||||||
%% make_time(TimeUnit, Offset) ->
|
%% make_time(TimeUnit, Offset) ->
|
||||||
%% make_time(TimeUnit, Offset, erlang:system_time(TimeUnit)).
|
%% make_time(TimeUnit, Offset, erlang:system_time(TimeUnit)).
|
||||||
make_time(TimeUnit, Offset, TimeEpoch) ->
|
make_time(TimeUnit, Offset, TimeEpoch) ->
|
||||||
Res = calendar:system_time_to_rfc3339(TimeEpoch,
|
Res = calendar:system_time_to_rfc3339(
|
||||||
[{unit, TimeUnit}, {offset, Offset}]),
|
TimeEpoch,
|
||||||
[Y1, Y2, Y3, Y4, $-, Mon1, Mon2, $-, D1, D2, _T,
|
[{unit, TimeUnit}, {offset, Offset}]
|
||||||
H1, H2, $:, Min1, Min2, $:, S1, S2 | TimeStr] = Res,
|
),
|
||||||
|
[
|
||||||
|
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,
|
IsFractionChar = fun(C) -> C >= $0 andalso C =< $9 orelse C =:= $. end,
|
||||||
{FractionStr, UtcOffset} = lists:splitwith(IsFractionChar, TimeStr),
|
{FractionStr, UtcOffset} = lists:splitwith(IsFractionChar, TimeStr),
|
||||||
#result{
|
#result{
|
||||||
year = [Y1, Y2, Y3, Y4]
|
year = [Y1, Y2, Y3, Y4],
|
||||||
, month = [Mon1, Mon2]
|
month = [Mon1, Mon2],
|
||||||
, day = [D1, D2]
|
day = [D1, D2],
|
||||||
, hour = [H1, H2]
|
hour = [H1, H2],
|
||||||
, minute = [Min1, Min2]
|
minute = [Min1, Min2],
|
||||||
, second = [S1, S2] ++ FractionStr
|
second = [S1, S2] ++ FractionStr,
|
||||||
, zone = UtcOffset
|
zone = UtcOffset
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
|
||||||
is_int_char(C) ->
|
is_int_char(C) ->
|
||||||
C >= $0 andalso C =< $9 .
|
C >= $0 andalso C =< $9.
|
||||||
is_symbol_char(C) ->
|
is_symbol_char(C) ->
|
||||||
C =:= $- orelse C =:= $+ .
|
C =:= $- orelse C =:= $+.
|
||||||
is_m_char(C) ->
|
is_m_char(C) ->
|
||||||
C =:= $:.
|
C =:= $:.
|
||||||
|
|
||||||
parse_char_with_fun(_, []) -> error(null_input);
|
parse_char_with_fun(_, []) ->
|
||||||
parse_char_with_fun(ValidFun, [C|Other]) ->
|
error(null_input);
|
||||||
Res = case erlang:is_function(ValidFun) of
|
parse_char_with_fun(ValidFun, [C | Other]) ->
|
||||||
true -> ValidFun(C);
|
Res =
|
||||||
false -> erlang:apply(emqx_rule_date, ValidFun, [C])
|
case erlang:is_function(ValidFun) of
|
||||||
end,
|
true -> ValidFun(C);
|
||||||
|
false -> erlang:apply(emqx_rule_date, ValidFun, [C])
|
||||||
|
end,
|
||||||
case Res of
|
case Res of
|
||||||
true -> {C, Other};
|
true -> {C, Other};
|
||||||
false -> error({unexpected,[C|Other]})
|
false -> error({unexpected, [C | Other]})
|
||||||
end.
|
end.
|
||||||
parse_string([], Input) -> {[], Input};
|
parse_string([], Input) ->
|
||||||
parse_string([C|Other], Input) ->
|
{[], Input};
|
||||||
|
parse_string([C | Other], Input) ->
|
||||||
{C1, Input1} = parse_char_with_fun(fun(V) -> V =:= C end, Input),
|
{C1, Input1} = parse_char_with_fun(fun(V) -> V =:= C end, Input),
|
||||||
{Res, Input2} = parse_string(Other, Input1),
|
{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) ->
|
parse_times(Times, Fun, Input) ->
|
||||||
{C1, Input1} = parse_char_with_fun(Fun, Input),
|
{C1, Input1} = parse_char_with_fun(Fun, Input),
|
||||||
{Res, Input2} = parse_times((Times - 1), Fun, Input1),
|
{Res, Input2} = parse_times((Times - 1), Fun, Input1),
|
||||||
{[C1|Res], Input2}.
|
{[C1 | Res], Input2}.
|
||||||
|
|
||||||
parse_int_times(Times, Input) ->
|
parse_int_times(Times, Input) ->
|
||||||
parse_times(Times, is_int_char, Input).
|
parse_times(Times, is_int_char, Input).
|
||||||
|
|
@ -162,33 +212,42 @@ parse_fraction(Input) ->
|
||||||
parse_second(Input) ->
|
parse_second(Input) ->
|
||||||
{M, Input1} = parse_int_times(2, Input),
|
{M, Input1} = parse_int_times(2, Input),
|
||||||
{M1, Input2} = parse_fraction(Input1),
|
{M1, Input2} = parse_fraction(Input1),
|
||||||
{M++M1, Input2}.
|
{M ++ M1, Input2}.
|
||||||
|
|
||||||
parse_zone(Input) ->
|
parse_zone(Input) ->
|
||||||
{S, Input1} = parse_char_with_fun(is_symbol_char, Input),
|
{S, Input1} = parse_char_with_fun(is_symbol_char, Input),
|
||||||
{M, Input2} = parse_int_times(2, Input1),
|
{M, Input2} = parse_int_times(2, Input1),
|
||||||
{C, Input3} = parse_char_with_fun(is_m_char, Input2),
|
{C, Input3} = parse_char_with_fun(is_m_char, Input2),
|
||||||
{V, Input4} = parse_int_times(2, Input3),
|
{V, Input4} = parse_int_times(2, Input3),
|
||||||
{[S|M++[C|V]], Input4}.
|
{[S | M ++ [C | V]], Input4}.
|
||||||
|
|
||||||
mlist1()->
|
mlist1() ->
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
[ {$H, fun(Input) -> parse_int_times(2, Input) end} %% %H Shows hour in 24-hour format [15]
|
%% %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]
|
{$H, fun(Input) -> parse_int_times(2, Input) end},
|
||||||
, {$y, fun(Input) -> parse_int_times(4, Input) end} %% %y Displays year YYYY [2021]
|
%% %M Displays minutes [00-59]
|
||||||
, {$m, fun(Input) -> parse_int_times(2, Input) end} %% %m Displays the number of the month [01-12]
|
{$M, fun(Input) -> parse_int_times(2, Input) end},
|
||||||
, {$d, fun(Input) -> parse_int_times(2, Input) end} %% %d Displays the number of the month [01-12]
|
%% %S Displays seconds [00-59]
|
||||||
, {$Z, fun(Input) -> parse_zone(Input) end} %% %Z Displays Time zone
|
{$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($H, Res, Str) -> Res#result{hour = Str};
|
||||||
update_result($M, Res, Str) -> Res#result{minute=Str};
|
update_result($M, Res, Str) -> Res#result{minute = Str};
|
||||||
update_result($S, Res, Str) -> Res#result{second=Str};
|
update_result($S, Res, Str) -> Res#result{second = Str};
|
||||||
update_result($y, Res, Str) -> Res#result{year=Str};
|
update_result($y, Res, Str) -> Res#result{year = Str};
|
||||||
update_result($m, Res, Str) -> Res#result{month=Str};
|
update_result($m, Res, Str) -> Res#result{month = Str};
|
||||||
update_result($d, Res, Str) -> Res#result{day=Str};
|
update_result($d, Res, Str) -> Res#result{day = Str};
|
||||||
update_result($Z, Res, Str) -> Res#result{zone=Str}.
|
update_result($Z, Res, Str) -> Res#result{zone = Str}.
|
||||||
|
|
||||||
parse_tag(Res, {st, St}, InputString) ->
|
parse_tag(Res, {st, St}, InputString) ->
|
||||||
{_A, B} = parse_string(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 = update_result(St, Res, A),
|
||||||
{NRes, B}.
|
{NRes, B}.
|
||||||
|
|
||||||
parse_tags(Res, [], _) -> Res;
|
parse_tags(Res, [], _) ->
|
||||||
parse_tags(Res, [Tag|Others], InputString) ->
|
Res;
|
||||||
|
parse_tags(Res, [Tag | Others], InputString) ->
|
||||||
{NRes, B} = parse_tag(Res, Tag, InputString),
|
{NRes, B} = parse_tag(Res, Tag, InputString),
|
||||||
parse_tags(NRes, Others, B).
|
parse_tags(NRes, Others, B).
|
||||||
|
|
||||||
parse_input(FormatString, InputString) ->
|
parse_input(FormatString, InputString) ->
|
||||||
[Head|Other] = string:split(FormatString, "%", all),
|
[Head | Other] = string:split(FormatString, "%", all),
|
||||||
R = create_tag([{st, Head}], Other),
|
R = create_tag([{st, Head}], Other),
|
||||||
parse_tags(#result{}, R, InputString).
|
parse_tags(#result{}, R, InputString).
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_rule_engine,
|
{application, emqx_rule_engine, [
|
||||||
[{description, "EMQX Rule Engine"},
|
{description, "EMQX Rule Engine"},
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
% strict semver, bump manually!
|
||||||
{modules, []},
|
{vsn, "5.0.0"},
|
||||||
{registered, [emqx_rule_engine_sup, emqx_rule_engine]},
|
{modules, []},
|
||||||
{applications, [kernel,stdlib,rulesql,getopt]},
|
{registered, [emqx_rule_engine_sup, emqx_rule_engine]},
|
||||||
{mod, {emqx_rule_engine_app, []}},
|
{applications, [kernel, stdlib, rulesql, getopt]},
|
||||||
{env, []},
|
{mod, {emqx_rule_engine_app, []}},
|
||||||
{licenses, ["Apache-2.0"]},
|
{env, []},
|
||||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||||
{"Github", "https://github.com/emqx/emqx-rule-engine"}
|
{links, [
|
||||||
]}
|
{"Homepage", "https://emqx.io/"},
|
||||||
]}.
|
{"Github", "https://github.com/emqx/emqx-rule-engine"}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -25,51 +25,56 @@
|
||||||
|
|
||||||
-export([start_link/0]).
|
-export([start_link/0]).
|
||||||
|
|
||||||
-export([ post_config_update/5
|
-export([
|
||||||
, config_key_path/0
|
post_config_update/5,
|
||||||
]).
|
config_key_path/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% Rule Management
|
%% Rule Management
|
||||||
|
|
||||||
-export([ load_rules/0
|
-export([load_rules/0]).
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ create_rule/1
|
-export([
|
||||||
, insert_rule/1
|
create_rule/1,
|
||||||
, update_rule/1
|
insert_rule/1,
|
||||||
, delete_rule/1
|
update_rule/1,
|
||||||
, get_rule/1
|
delete_rule/1,
|
||||||
]).
|
get_rule/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ get_rules/0
|
-export([
|
||||||
, get_rules_for_topic/1
|
get_rules/0,
|
||||||
, get_rules_with_same_event/1
|
get_rules_for_topic/1,
|
||||||
, get_rules_ordered_by_ts/0
|
get_rules_with_same_event/1,
|
||||||
]).
|
get_rules_ordered_by_ts/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% exported for cluster_call
|
%% exported for cluster_call
|
||||||
-export([ do_delete_rule/1
|
-export([
|
||||||
, do_insert_rule/1
|
do_delete_rule/1,
|
||||||
]).
|
do_insert_rule/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ load_hooks_for_rule/1
|
-export([
|
||||||
, unload_hooks_for_rule/1
|
load_hooks_for_rule/1,
|
||||||
, maybe_add_metrics_for_rule/1
|
unload_hooks_for_rule/1,
|
||||||
, clear_metrics_for_rule/1
|
maybe_add_metrics_for_rule/1,
|
||||||
, reset_metrics_for_rule/1
|
clear_metrics_for_rule/1,
|
||||||
]).
|
reset_metrics_for_rule/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% exported for `emqx_telemetry'
|
%% exported for `emqx_telemetry'
|
||||||
-export([get_basic_usage_info/0]).
|
-export([get_basic_usage_info/0]).
|
||||||
|
|
||||||
%% gen_server Callbacks
|
%% gen_server Callbacks
|
||||||
-export([ init/1
|
-export([
|
||||||
, handle_call/3
|
init/1,
|
||||||
, handle_cast/2
|
handle_call/3,
|
||||||
, handle_info/2
|
handle_cast/2,
|
||||||
, terminate/2
|
handle_info/2,
|
||||||
, code_change/3
|
terminate/2,
|
||||||
]).
|
code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
-define(RULE_ENGINE, ?MODULE).
|
-define(RULE_ENGINE, ?MODULE).
|
||||||
|
|
||||||
|
|
@ -77,24 +82,25 @@
|
||||||
|
|
||||||
%% NOTE: This order cannot be changed! This is to make the metric working during relup.
|
%% NOTE: This order cannot be changed! This is to make the metric working during relup.
|
||||||
%% Append elements to this list to add new metrics.
|
%% Append elements to this list to add new metrics.
|
||||||
-define(METRICS, [ 'sql.matched'
|
-define(METRICS, [
|
||||||
, 'sql.passed'
|
'sql.matched',
|
||||||
, 'sql.failed'
|
'sql.passed',
|
||||||
, 'sql.failed.exception'
|
'sql.failed',
|
||||||
, 'sql.failed.no_result'
|
'sql.failed.exception',
|
||||||
, 'outputs.total'
|
'sql.failed.no_result',
|
||||||
, 'outputs.success'
|
'outputs.total',
|
||||||
, 'outputs.failed'
|
'outputs.success',
|
||||||
, 'outputs.failed.out_of_service'
|
'outputs.failed',
|
||||||
, 'outputs.failed.unknown'
|
'outputs.failed.out_of_service',
|
||||||
]).
|
'outputs.failed.unknown'
|
||||||
|
]).
|
||||||
|
|
||||||
-define(RATE_METRICS, ['sql.matched']).
|
-define(RATE_METRICS, ['sql.matched']).
|
||||||
|
|
||||||
config_key_path() ->
|
config_key_path() ->
|
||||||
[rule_engine, rules].
|
[rule_engine, rules].
|
||||||
|
|
||||||
-spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}).
|
-spec start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}.
|
||||||
start_link() ->
|
start_link() ->
|
||||||
gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []).
|
gen_server:start_link({local, ?RULE_ENGINE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
|
@ -102,17 +108,26 @@ start_link() ->
|
||||||
%% The config handler for emqx_rule_engine
|
%% The config handler for emqx_rule_engine
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) ->
|
post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) ->
|
||||||
#{added := Added, removed := Removed, changed := Updated}
|
#{added := Added, removed := Removed, changed := Updated} =
|
||||||
= emqx_map_lib:diff_maps(NewRules, OldRules),
|
emqx_map_lib:diff_maps(NewRules, OldRules),
|
||||||
maps_foreach(fun({Id, {_Old, New}}) ->
|
maps_foreach(
|
||||||
|
fun({Id, {_Old, New}}) ->
|
||||||
{ok, _} = update_rule(New#{id => bin(Id)})
|
{ok, _} = update_rule(New#{id => bin(Id)})
|
||||||
end, Updated),
|
end,
|
||||||
maps_foreach(fun({Id, _Rule}) ->
|
Updated
|
||||||
|
),
|
||||||
|
maps_foreach(
|
||||||
|
fun({Id, _Rule}) ->
|
||||||
ok = delete_rule(bin(Id))
|
ok = delete_rule(bin(Id))
|
||||||
end, Removed),
|
end,
|
||||||
maps_foreach(fun({Id, Rule}) ->
|
Removed
|
||||||
|
),
|
||||||
|
maps_foreach(
|
||||||
|
fun({Id, Rule}) ->
|
||||||
{ok, _} = create_rule(Rule#{id => bin(Id)})
|
{ok, _} = create_rule(Rule#{id => bin(Id)})
|
||||||
end, Added),
|
end,
|
||||||
|
Added
|
||||||
|
),
|
||||||
{ok, get_rules()}.
|
{ok, get_rules()}.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -121,9 +136,12 @@ post_config_update(_, _Req, NewRules, OldRules, _AppEnvs) ->
|
||||||
|
|
||||||
-spec load_rules() -> ok.
|
-spec load_rules() -> ok.
|
||||||
load_rules() ->
|
load_rules() ->
|
||||||
maps_foreach(fun({Id, Rule}) ->
|
maps_foreach(
|
||||||
|
fun({Id, Rule}) ->
|
||||||
{ok, _} = create_rule(Rule#{id => bin(Id)})
|
{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()}.
|
-spec create_rule(map()) -> {ok, rule()} | {error, term()}.
|
||||||
create_rule(Params = #{id := RuleId}) when is_binary(RuleId) ->
|
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)
|
parse_and_insert(Params, CreatedAt)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(delete_rule(RuleId :: rule_id()) -> ok).
|
-spec delete_rule(RuleId :: rule_id()) -> ok.
|
||||||
delete_rule(RuleId) when is_binary(RuleId) ->
|
delete_rule(RuleId) when is_binary(RuleId) ->
|
||||||
gen_server:call(?RULE_ENGINE, {delete_rule, RuleId}, ?T_CALL).
|
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) ->
|
insert_rule(Rule) ->
|
||||||
gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL).
|
gen_server:call(?RULE_ENGINE, {insert_rule, Rule}, ?T_CALL).
|
||||||
|
|
||||||
|
|
@ -153,30 +171,39 @@ insert_rule(Rule) ->
|
||||||
%% Rule Management
|
%% Rule Management
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-spec(get_rules() -> [rule()]).
|
-spec get_rules() -> [rule()].
|
||||||
get_rules() ->
|
get_rules() ->
|
||||||
get_all_records(?RULE_TAB).
|
get_all_records(?RULE_TAB).
|
||||||
|
|
||||||
get_rules_ordered_by_ts() ->
|
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
|
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) ->
|
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) ->
|
get_rules_with_same_event(Topic) ->
|
||||||
EventName = emqx_rule_events:event_name(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) ->
|
is_of_event_name(EventName, Topic) ->
|
||||||
EventName =:= emqx_rule_events:event_name(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) ->
|
get_rule(Id) ->
|
||||||
case ets:lookup(?RULE_TAB, Id) of
|
case ets:lookup(?RULE_TAB, Id) of
|
||||||
[{Id, Rule}] -> {ok, Rule#{id => Id}};
|
[{Id, Rule}] -> {ok, Rule#{id => Id}};
|
||||||
|
|
@ -188,7 +215,8 @@ load_hooks_for_rule(#{from := Topics}) ->
|
||||||
|
|
||||||
maybe_add_metrics_for_rule(Id) ->
|
maybe_add_metrics_for_rule(Id) ->
|
||||||
case emqx_plugin_libs_metrics:has_metrics(rule_metrics, Id) of
|
case emqx_plugin_libs_metrics:has_metrics(rule_metrics, Id) of
|
||||||
true -> ok;
|
true ->
|
||||||
|
ok;
|
||||||
false ->
|
false ->
|
||||||
ok = emqx_plugin_libs_metrics:create_metrics(rule_metrics, Id, ?METRICS, ?RATE_METRICS)
|
ok = emqx_plugin_libs_metrics:create_metrics(rule_metrics, Id, ?METRICS, ?RATE_METRICS)
|
||||||
end.
|
end.
|
||||||
|
|
@ -196,86 +224,101 @@ maybe_add_metrics_for_rule(Id) ->
|
||||||
clear_metrics_for_rule(Id) ->
|
clear_metrics_for_rule(Id) ->
|
||||||
ok = emqx_plugin_libs_metrics:clear_metrics(rule_metrics, 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) ->
|
reset_metrics_for_rule(Id) ->
|
||||||
emqx_plugin_libs_metrics:reset_metrics(rule_metrics, Id).
|
emqx_plugin_libs_metrics:reset_metrics(rule_metrics, Id).
|
||||||
|
|
||||||
unload_hooks_for_rule(#{id := Id, from := Topics}) ->
|
unload_hooks_for_rule(#{id := Id, from := Topics}) ->
|
||||||
lists:foreach(fun(Topic) ->
|
lists:foreach(
|
||||||
case get_rules_with_same_event(Topic) of
|
fun(Topic) ->
|
||||||
[#{id := Id0}] when Id0 == Id -> %% we are now deleting the last rule
|
case get_rules_with_same_event(Topic) of
|
||||||
emqx_rule_events:unload(Topic);
|
%% we are now deleting the last rule
|
||||||
_ -> ok
|
[#{id := Id0}] when Id0 == Id ->
|
||||||
end
|
emqx_rule_events:unload(Topic);
|
||||||
end, Topics).
|
_ ->
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Topics
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Telemetry helper functions
|
%% Telemetry helper functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-spec get_basic_usage_info() -> #{ num_rules => non_neg_integer()
|
-spec get_basic_usage_info() ->
|
||||||
, referenced_bridges =>
|
#{
|
||||||
#{ BridgeType => non_neg_integer()
|
num_rules => non_neg_integer(),
|
||||||
}
|
referenced_bridges =>
|
||||||
}
|
#{BridgeType => non_neg_integer()}
|
||||||
when BridgeType :: atom().
|
}
|
||||||
|
when
|
||||||
|
BridgeType :: atom().
|
||||||
get_basic_usage_info() ->
|
get_basic_usage_info() ->
|
||||||
try
|
try
|
||||||
Rules = get_rules(),
|
Rules = get_rules(),
|
||||||
EnabledRules =
|
EnabledRules =
|
||||||
lists:filter(
|
lists:filter(
|
||||||
fun(#{enable := Enabled}) -> Enabled end,
|
fun(#{enable := Enabled}) -> Enabled end,
|
||||||
Rules),
|
Rules
|
||||||
|
),
|
||||||
NumRules = length(EnabledRules),
|
NumRules = length(EnabledRules),
|
||||||
ReferencedBridges =
|
ReferencedBridges =
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun(#{outputs := Outputs}, Acc) ->
|
fun(#{outputs := Outputs}, Acc) ->
|
||||||
BridgeIDs = lists:filter(fun is_binary/1, Outputs),
|
BridgeIDs = lists:filter(fun is_binary/1, Outputs),
|
||||||
tally_referenced_bridges(BridgeIDs, Acc)
|
tally_referenced_bridges(BridgeIDs, Acc)
|
||||||
end,
|
end,
|
||||||
#{},
|
#{},
|
||||||
EnabledRules),
|
EnabledRules
|
||||||
#{ num_rules => NumRules
|
),
|
||||||
, referenced_bridges => ReferencedBridges
|
#{
|
||||||
}
|
num_rules => NumRules,
|
||||||
|
referenced_bridges => ReferencedBridges
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
_:_ ->
|
_:_ ->
|
||||||
#{ num_rules => 0
|
#{
|
||||||
, referenced_bridges => #{}
|
num_rules => 0,
|
||||||
}
|
referenced_bridges => #{}
|
||||||
|
}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
tally_referenced_bridges(BridgeIDs, Acc0) ->
|
tally_referenced_bridges(BridgeIDs, Acc0) ->
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun(BridgeID, Acc) ->
|
fun(BridgeID, Acc) ->
|
||||||
{BridgeType, _BridgeName} = emqx_bridge:parse_bridge_id(BridgeID),
|
{BridgeType, _BridgeName} = emqx_bridge:parse_bridge_id(BridgeID),
|
||||||
maps:update_with(
|
maps:update_with(
|
||||||
BridgeType,
|
BridgeType,
|
||||||
fun(X) -> X + 1 end,
|
fun(X) -> X + 1 end,
|
||||||
1,
|
1,
|
||||||
Acc)
|
Acc
|
||||||
end,
|
)
|
||||||
Acc0,
|
end,
|
||||||
BridgeIDs).
|
Acc0,
|
||||||
|
BridgeIDs
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
_TableId = ets:new(?KV_TAB, [named_table, set, public, {write_concurrency, true},
|
_TableId = ets:new(?KV_TAB, [
|
||||||
{read_concurrency, true}]),
|
named_table,
|
||||||
|
set,
|
||||||
|
public,
|
||||||
|
{write_concurrency, true},
|
||||||
|
{read_concurrency, true}
|
||||||
|
]),
|
||||||
{ok, #{}}.
|
{ok, #{}}.
|
||||||
|
|
||||||
handle_call({insert_rule, Rule}, _From, State) ->
|
handle_call({insert_rule, Rule}, _From, State) ->
|
||||||
do_insert_rule(Rule),
|
do_insert_rule(Rule),
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call({delete_rule, Rule}, _From, State) ->
|
handle_call({delete_rule, Rule}, _From, State) ->
|
||||||
do_delete_rule(Rule),
|
do_delete_rule(Rule),
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
|
||||||
handle_call(Req, _From, State) ->
|
handle_call(Req, _From, State) ->
|
||||||
?SLOG(error, #{msg => "unexpected_call", request => Req}),
|
?SLOG(error, #{msg => "unexpected_call", request => Req}),
|
||||||
{reply, ignored, State}.
|
{reply, ignored, State}.
|
||||||
|
|
@ -321,7 +364,8 @@ parse_and_insert(Params = #{id := RuleId, sql := Sql, outputs := Outputs}, Creat
|
||||||
},
|
},
|
||||||
ok = insert_rule(Rule),
|
ok = insert_rule(Rule),
|
||||||
{ok, Rule};
|
{ok, Rule};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_insert_rule(#{id := Id} = Rule) ->
|
do_insert_rule(#{id := Id} = Rule) ->
|
||||||
|
|
@ -337,7 +381,8 @@ do_delete_rule(RuleId) ->
|
||||||
ok = clear_metrics_for_rule(RuleId),
|
ok = clear_metrics_for_rule(RuleId),
|
||||||
true = ets:delete(?RULE_TAB, RuleId),
|
true = ets:delete(?RULE_TAB, RuleId),
|
||||||
ok;
|
ok;
|
||||||
not_found -> ok
|
not_found ->
|
||||||
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
parse_outputs(Outputs) ->
|
parse_outputs(Outputs) ->
|
||||||
|
|
|
||||||
|
|
@ -32,20 +32,33 @@
|
||||||
-export(['/rule_events'/2, '/rule_test'/2, '/rules'/2, '/rules/:id'/2, '/rules/:id/reset_metrics'/2]).
|
-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_NO_RULE(ID), list_to_binary(io_lib:format("Rule ~ts Not Found", [(ID)]))).
|
||||||
-define(ERR_BADARGS(REASON),
|
-define(ERR_BADARGS(REASON), begin
|
||||||
begin
|
R0 = err_msg(REASON),
|
||||||
R0 = err_msg(REASON),
|
<<"Bad Arguments: ", R0/binary>>
|
||||||
<<"Bad Arguments: ", R0/binary>>
|
end).
|
||||||
end).
|
|
||||||
-define(CHECK_PARAMS(PARAMS, TAG, EXPR),
|
-define(CHECK_PARAMS(PARAMS, TAG, EXPR),
|
||||||
case emqx_rule_api_schema:check_params(PARAMS, TAG) of
|
case emqx_rule_api_schema:check_params(PARAMS, TAG) of
|
||||||
{ok, CheckedParams} ->
|
{ok, CheckedParams} ->
|
||||||
EXPR;
|
EXPR;
|
||||||
{error, REASON} ->
|
{error, REASON} ->
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(REASON)}}
|
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(REASON)}}
|
||||||
end).
|
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),
|
-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.matched' => MATCH,
|
||||||
'sql.passed' => PASS,
|
'sql.passed' => PASS,
|
||||||
|
|
@ -60,9 +73,23 @@
|
||||||
'sql.matched.rate' => RATE,
|
'sql.matched.rate' => RATE,
|
||||||
'sql.matched.rate.max' => RATE_MAX,
|
'sql.matched.rate.max' => RATE_MAX,
|
||||||
'sql.matched.rate.last5m' => RATE_5
|
'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.matched' := MATCH,
|
||||||
'sql.passed' := PASS,
|
'sql.passed' := PASS,
|
||||||
|
|
@ -77,7 +104,8 @@
|
||||||
'sql.matched.rate' := RATE,
|
'sql.matched.rate' := RATE,
|
||||||
'sql.matched.rate.max' := RATE_MAX,
|
'sql.matched.rate.max' := RATE_MAX,
|
||||||
'sql.matched.rate.last5m' := RATE_5
|
'sql.matched.rate.last5m' := RATE_5
|
||||||
}).
|
}
|
||||||
|
).
|
||||||
|
|
||||||
namespace() -> "rule".
|
namespace() -> "rule".
|
||||||
|
|
||||||
|
|
@ -107,7 +135,8 @@ schema("/rules") ->
|
||||||
summary => <<"List Rules">>,
|
summary => <<"List Rules">>,
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => mk(array(rule_info_schema()), #{desc => ?DESC("desc9")})
|
200 => mk(array(rule_info_schema()), #{desc => ?DESC("desc9")})
|
||||||
}},
|
}
|
||||||
|
},
|
||||||
post => #{
|
post => #{
|
||||||
tags => [<<"rules">>],
|
tags => [<<"rules">>],
|
||||||
description => ?DESC("api2"),
|
description => ?DESC("api2"),
|
||||||
|
|
@ -116,9 +145,9 @@ schema("/rules") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
|
400 => error_schema('BAD_REQUEST', "Invalid Parameters"),
|
||||||
201 => rule_info_schema()
|
201 => rule_info_schema()
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/rule_events") ->
|
schema("/rule_events") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/rule_events',
|
'operationId' => '/rule_events',
|
||||||
|
|
@ -131,7 +160,6 @@ schema("/rule_events") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/rules/:id") ->
|
schema("/rules/:id") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/rules/:id',
|
'operationId' => '/rules/:id',
|
||||||
|
|
@ -166,7 +194,6 @@ schema("/rules/:id") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/rules/:id/reset_metrics") ->
|
schema("/rules/:id/reset_metrics") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/rules/:id/reset_metrics',
|
'operationId' => '/rules/:id/reset_metrics',
|
||||||
|
|
@ -181,7 +208,6 @@ schema("/rules/:id/reset_metrics") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/rule_test") ->
|
schema("/rule_test") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/rule_test',
|
'operationId' => '/rule_test',
|
||||||
|
|
@ -206,7 +232,7 @@ param_path_id() ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
%% To get around the hocon bug, we replace crlf with spaces
|
%% 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]),
|
NewSQL = re:replace(SQL, "[\r\n]", " ", [{return, binary}, global]),
|
||||||
Params#{<<"sql">> => NewSQL}.
|
Params#{<<"sql">> => NewSQL}.
|
||||||
|
|
||||||
|
|
@ -216,7 +242,6 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) ->
|
||||||
'/rules'(get, _Params) ->
|
'/rules'(get, _Params) ->
|
||||||
Records = emqx_rule_engine:get_rules_ordered_by_ts(),
|
Records = emqx_rule_engine:get_rules_ordered_by_ts(),
|
||||||
{200, format_rule_resp(Records)};
|
{200, format_rule_resp(Records)};
|
||||||
|
|
||||||
'/rules'(post, #{body := Params0}) ->
|
'/rules'(post, #{body := Params0}) ->
|
||||||
case maps:get(<<"id">>, Params0, list_to_binary(emqx_misc:gen_id(8))) of
|
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),
|
[Rule] = get_one_rule(AllRules, Id),
|
||||||
{201, format_rule_resp(Rule)};
|
{201, format_rule_resp(Rule)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "create_rule_failed",
|
?SLOG(error, #{
|
||||||
id => Id, reason => Reason}),
|
msg => "create_rule_failed",
|
||||||
|
id => Id,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}}
|
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
'/rule_test'(post, #{body := Params}) ->
|
'/rule_test'(post, #{body := Params}) ->
|
||||||
?CHECK_PARAMS(Params, rule_test, case emqx_rule_sqltester:test(CheckedParams) of
|
?CHECK_PARAMS(
|
||||||
{ok, Result} -> {200, Result};
|
Params,
|
||||||
{error, {parse_error, Reason}} ->
|
rule_test,
|
||||||
{400, #{code => 'BAD_REQUEST', message => err_msg(Reason)}};
|
case emqx_rule_sqltester:test(CheckedParams) of
|
||||||
{error, nomatch} -> {412, #{code => 'NOT_MATCH', message => <<"SQL Not Match">>}}
|
{ok, Result} ->
|
||||||
end).
|
{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}}) ->
|
'/rules/:id'(get, #{bindings := #{id := Id}}) ->
|
||||||
case emqx_rule_engine:get_rule(Id) of
|
case emqx_rule_engine:get_rule(Id) of
|
||||||
|
|
@ -255,7 +289,6 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) ->
|
||||||
not_found ->
|
not_found ->
|
||||||
{404, #{code => 'NOT_FOUND', message => <<"Rule Id Not Found">>}}
|
{404, #{code => 'NOT_FOUND', message => <<"Rule Id Not Found">>}}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
'/rules/:id'(put, #{bindings := #{id := Id}, body := Params0}) ->
|
'/rules/:id'(put, #{bindings := #{id := Id}, body := Params0}) ->
|
||||||
Params = filter_out_request_body(Params0),
|
Params = filter_out_request_body(Params0),
|
||||||
ConfPath = emqx_rule_engine:config_key_path() ++ [Id],
|
ConfPath = emqx_rule_engine:config_key_path() ++ [Id],
|
||||||
|
|
@ -264,25 +297,35 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) ->
|
||||||
[Rule] = get_one_rule(AllRules, Id),
|
[Rule] = get_one_rule(AllRules, Id),
|
||||||
{200, format_rule_resp(Rule)};
|
{200, format_rule_resp(Rule)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "update_rule_failed",
|
?SLOG(error, #{
|
||||||
id => Id, reason => Reason}),
|
msg => "update_rule_failed",
|
||||||
|
id => Id,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}}
|
{400, #{code => 'BAD_REQUEST', message => ?ERR_BADARGS(Reason)}}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
'/rules/:id'(delete, #{bindings := #{id := Id}}) ->
|
'/rules/:id'(delete, #{bindings := #{id := Id}}) ->
|
||||||
ConfPath = emqx_rule_engine:config_key_path() ++ [Id],
|
ConfPath = emqx_rule_engine:config_key_path() ++ [Id],
|
||||||
case emqx_conf:remove(ConfPath, #{override_to => cluster}) of
|
case emqx_conf:remove(ConfPath, #{override_to => cluster}) of
|
||||||
{ok, _} -> {204};
|
{ok, _} ->
|
||||||
|
{204};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "delete_rule_failed",
|
?SLOG(error, #{
|
||||||
id => Id, reason => Reason}),
|
msg => "delete_rule_failed",
|
||||||
|
id => Id,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
{500, #{code => 'INTERNAL_ERROR', message => ?ERR_BADARGS(Reason)}}
|
{500, #{code => 'INTERNAL_ERROR', message => ?ERR_BADARGS(Reason)}}
|
||||||
end.
|
end.
|
||||||
'/rules/:id/reset_metrics'(put, #{bindings := #{id := RuleId}}) ->
|
'/rules/:id/reset_metrics'(put, #{bindings := #{id := RuleId}}) ->
|
||||||
case emqx_rule_engine_proto_v1:reset_metrics(RuleId) of
|
case emqx_rule_engine_proto_v1:reset_metrics(RuleId) of
|
||||||
{ok, _TxnId, _Result} -> {200, <<"Reset Success">>};
|
{ok, _TxnId, _Result} ->
|
||||||
Failed -> {400, #{code => 'BAD_REQUEST',
|
{200, <<"Reset Success">>};
|
||||||
message => err_msg(Failed)}}
|
Failed ->
|
||||||
|
{400, #{
|
||||||
|
code => 'BAD_REQUEST',
|
||||||
|
message => err_msg(Failed)
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
@ -292,29 +335,31 @@ replace_sql_clrf(#{ <<"sql">> := SQL } = Params) ->
|
||||||
err_msg(Msg) ->
|
err_msg(Msg) ->
|
||||||
list_to_binary(io_lib:format("~0p", [Msg])).
|
list_to_binary(io_lib:format("~0p", [Msg])).
|
||||||
|
|
||||||
|
|
||||||
format_rule_resp(Rules) when is_list(Rules) ->
|
format_rule_resp(Rules) when is_list(Rules) ->
|
||||||
[format_rule_resp(R) || R <- Rules];
|
[format_rule_resp(R) || R <- Rules];
|
||||||
|
format_rule_resp(#{
|
||||||
format_rule_resp(#{ id := Id, name := Name,
|
id := Id,
|
||||||
created_at := CreatedAt,
|
name := Name,
|
||||||
from := Topics,
|
created_at := CreatedAt,
|
||||||
outputs := Output,
|
from := Topics,
|
||||||
sql := SQL,
|
outputs := Output,
|
||||||
enable := Enable,
|
sql := SQL,
|
||||||
description := Descr}) ->
|
enable := Enable,
|
||||||
|
description := Descr
|
||||||
|
}) ->
|
||||||
NodeMetrics = get_rule_metrics(Id),
|
NodeMetrics = get_rule_metrics(Id),
|
||||||
#{id => Id,
|
#{
|
||||||
name => Name,
|
id => Id,
|
||||||
from => Topics,
|
name => Name,
|
||||||
outputs => format_output(Output),
|
from => Topics,
|
||||||
sql => SQL,
|
outputs => format_output(Output),
|
||||||
metrics => aggregate_metrics(NodeMetrics),
|
sql => SQL,
|
||||||
node_metrics => NodeMetrics,
|
metrics => aggregate_metrics(NodeMetrics),
|
||||||
enable => Enable,
|
node_metrics => NodeMetrics,
|
||||||
created_at => format_datetime(CreatedAt, millisecond),
|
enable => Enable,
|
||||||
description => Descr
|
created_at => format_datetime(CreatedAt, millisecond),
|
||||||
}.
|
description => Descr
|
||||||
|
}.
|
||||||
|
|
||||||
format_datetime(Timestamp, Unit) ->
|
format_datetime(Timestamp, Unit) ->
|
||||||
list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, 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(Out) || Out <- Outputs].
|
||||||
|
|
||||||
do_format_output(#{mod := Mod, func := Func, args := Args}) ->
|
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) ->
|
do_format_output(BridgeChannelId) when is_binary(BridgeChannelId) ->
|
||||||
BridgeChannelId.
|
BridgeChannelId.
|
||||||
|
|
||||||
printable_function_name(emqx_rule_outputs, Func) ->
|
printable_function_name(emqx_rule_outputs, Func) ->
|
||||||
Func;
|
Func;
|
||||||
printable_function_name(Mod, Func) ->
|
printable_function_name(Mod, Func) ->
|
||||||
list_to_binary(lists:concat([Mod,":",Func])).
|
list_to_binary(lists:concat([Mod, ":", Func])).
|
||||||
|
|
||||||
get_rule_metrics(Id) ->
|
get_rule_metrics(Id) ->
|
||||||
Format = fun (Node, #{
|
Format = fun(
|
||||||
|
Node,
|
||||||
|
#{
|
||||||
counters :=
|
counters :=
|
||||||
#{'sql.matched' := Matched, 'sql.passed' := Passed, 'sql.failed' := Failed,
|
#{
|
||||||
'sql.failed.exception' := FailedEx,
|
'sql.matched' := Matched,
|
||||||
'sql.failed.no_result' := FailedNoRes,
|
'sql.passed' := Passed,
|
||||||
'outputs.total' := OTotal,
|
'sql.failed' := Failed,
|
||||||
'outputs.failed' := OFailed,
|
'sql.failed.exception' := FailedEx,
|
||||||
'outputs.failed.out_of_service' := OFailedOOS,
|
'sql.failed.no_result' := FailedNoRes,
|
||||||
'outputs.failed.unknown' := OFailedUnknown,
|
'outputs.total' := OTotal,
|
||||||
'outputs.success' := OFailedSucc
|
'outputs.failed' := OFailed,
|
||||||
},
|
'outputs.failed.out_of_service' := OFailedOOS,
|
||||||
|
'outputs.failed.unknown' := OFailedUnknown,
|
||||||
|
'outputs.success' := OFailedSucc
|
||||||
|
},
|
||||||
rate :=
|
rate :=
|
||||||
#{'sql.matched' :=
|
#{
|
||||||
#{current := Current, max := Max, last5m := Last5M}
|
'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
|
) ->
|
||||||
}
|
#{
|
||||||
|
metrics => ?METRICS(
|
||||||
|
Matched,
|
||||||
|
Passed,
|
||||||
|
Failed,
|
||||||
|
FailedEx,
|
||||||
|
FailedNoRes,
|
||||||
|
OTotal,
|
||||||
|
OFailed,
|
||||||
|
OFailedOOS,
|
||||||
|
OFailedUnknown,
|
||||||
|
OFailedSucc,
|
||||||
|
Current,
|
||||||
|
Max,
|
||||||
|
Last5M
|
||||||
|
),
|
||||||
|
node => Node
|
||||||
|
}
|
||||||
end,
|
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) ->
|
aggregate_metrics(AllMetrics) ->
|
||||||
InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
lists:foldl(fun
|
lists:foldl(
|
||||||
(#{metrics := ?metrics(Match1, Passed1, Failed1, FailedEx1, FailedNoRes1,
|
fun(
|
||||||
OTotal1, OFailed1, OFailedOOS1, OFailedUnknown1, OFailedSucc1,
|
#{
|
||||||
Rate1, RateMax1, Rate5m1)},
|
metrics := ?metrics(
|
||||||
?metrics(Match0, Passed0, Failed0, FailedEx0, FailedNoRes0,
|
Match1,
|
||||||
OTotal0, OFailed0, OFailedOOS0, OFailedUnknown0, OFailedSucc0,
|
Passed1,
|
||||||
Rate0, RateMax0, Rate5m0)) ->
|
Failed1,
|
||||||
?METRICS(Match1 + Match0, Passed1 + Passed0, Failed1 + Failed0,
|
FailedEx1,
|
||||||
FailedEx1 + FailedEx0, FailedNoRes1 + FailedNoRes0,
|
FailedNoRes1,
|
||||||
OTotal1 + OTotal0, OFailed1 + OFailed0,
|
OTotal1,
|
||||||
OFailedOOS1 + OFailedOOS0,
|
OFailed1,
|
||||||
OFailedUnknown1 + OFailedUnknown0,
|
OFailedOOS1,
|
||||||
OFailedSucc1 + OFailedSucc0,
|
OFailedUnknown1,
|
||||||
Rate1 + Rate0, RateMax1 + RateMax0, Rate5m1 + Rate5m0)
|
OFailedSucc1,
|
||||||
end, InitMetrics, AllMetrics).
|
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) ->
|
get_one_rule(AllRules, Id) ->
|
||||||
[R || R = #{id := Id0} <- AllRules, Id0 == Id].
|
[R || R = #{id := Id0} <- AllRules, Id0 == Id].
|
||||||
|
|
||||||
filter_out_request_body(Conf) ->
|
filter_out_request_body(Conf) ->
|
||||||
ExtraConfs = [<<"id">>, <<"status">>, <<"node_status">>, <<"node_metrics">>,
|
ExtraConfs = [
|
||||||
<<"metrics">>, <<"node">>],
|
<<"id">>,
|
||||||
|
<<"status">>,
|
||||||
|
<<"node_status">>,
|
||||||
|
<<"node_metrics">>,
|
||||||
|
<<"metrics">>,
|
||||||
|
<<"node">>
|
||||||
|
],
|
||||||
maps:without(ExtraConfs, Conf).
|
maps:without(ExtraConfs, Conf).
|
||||||
|
|
|
||||||
|
|
@ -21,96 +21,140 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1
|
roots/0,
|
||||||
, desc/1
|
fields/1,
|
||||||
]).
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ validate_sql/1
|
-export([validate_sql/1]).
|
||||||
]).
|
|
||||||
|
|
||||||
namespace() -> rule_engine.
|
namespace() -> rule_engine.
|
||||||
|
|
||||||
roots() -> ["rule_engine"].
|
roots() -> ["rule_engine"].
|
||||||
|
|
||||||
fields("rule_engine") ->
|
fields("rule_engine") ->
|
||||||
[ {ignore_sys_message, sc(boolean(), #{default => true, desc => ?DESC("rule_engine_ignore_sys_message")
|
[
|
||||||
})}
|
{ignore_sys_message,
|
||||||
, {rules, sc(hoconsc:map("id", ref("rules")), #{desc => ?DESC("rule_engine_rules"), default => #{}})}
|
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") ->
|
fields("rules") ->
|
||||||
[ rule_name()
|
[
|
||||||
, {"sql", sc(binary(),
|
rule_name(),
|
||||||
#{ desc => ?DESC("rules_sql")
|
{"sql",
|
||||||
, example => "SELECT * FROM \"test/topic\" WHERE payload.x = 1"
|
sc(
|
||||||
, required => true
|
binary(),
|
||||||
, validator => fun ?MODULE:validate_sql/1
|
#{
|
||||||
})}
|
desc => ?DESC("rules_sql"),
|
||||||
, {"outputs", sc(hoconsc:array(hoconsc:union(outputs())),
|
example => "SELECT * FROM \"test/topic\" WHERE payload.x = 1",
|
||||||
#{ desc => ?DESC("rules_outputs")
|
required => true,
|
||||||
, default => []
|
validator => fun ?MODULE:validate_sql/1
|
||||||
, example => [
|
}
|
||||||
<<"http:my_http_bridge">>,
|
)},
|
||||||
#{function => republish, args => #{
|
{"outputs",
|
||||||
topic => <<"t/1">>, payload => <<"${payload}">>}},
|
sc(
|
||||||
#{function => console}
|
hoconsc:array(hoconsc:union(outputs())),
|
||||||
]
|
#{
|
||||||
})}
|
desc => ?DESC("rules_outputs"),
|
||||||
, {"enable", sc(boolean(), #{desc => ?DESC("rules_enable"), default => true})}
|
default => [],
|
||||||
, {"description", sc(binary(),
|
example => [
|
||||||
#{ desc => ?DESC("rules_description")
|
<<"http:my_http_bridge">>,
|
||||||
, example => "Some description"
|
#{
|
||||||
, default => <<>>
|
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") ->
|
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") ->
|
fields("builtin_output_console") ->
|
||||||
[ {function, sc(console, #{desc => ?DESC("console_function")})}
|
[
|
||||||
%% we may support some args for the console output in the future
|
{function, sc(console, #{desc => ?DESC("console_function")})}
|
||||||
%, {args, sc(map(), #{desc => "The arguments of the built-in 'console' output",
|
%% we may support some args for the console output in the future
|
||||||
% default => #{}})}
|
%, {args, sc(map(), #{desc => "The arguments of the built-in 'console' output",
|
||||||
|
% default => #{}})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("user_provided_function") ->
|
fields("user_provided_function") ->
|
||||||
[ {function, sc(binary(),
|
[
|
||||||
#{ desc => ?DESC("user_provided_function_function")
|
{function,
|
||||||
, required => true
|
sc(
|
||||||
, example => "module:function"
|
binary(),
|
||||||
})}
|
#{
|
||||||
, {args, sc(map(),
|
desc => ?DESC("user_provided_function_function"),
|
||||||
#{ desc => ?DESC("user_provided_function_args")
|
required => true,
|
||||||
, default => #{}
|
example => "module:function"
|
||||||
})}
|
}
|
||||||
|
)},
|
||||||
|
{args,
|
||||||
|
sc(
|
||||||
|
map(),
|
||||||
|
#{
|
||||||
|
desc => ?DESC("user_provided_function_args"),
|
||||||
|
default => #{}
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields("republish_args") ->
|
fields("republish_args") ->
|
||||||
[ {topic, sc(binary(),
|
[
|
||||||
#{ desc => ?DESC("republish_args_topic")
|
{topic,
|
||||||
, required => true
|
sc(
|
||||||
, example => <<"a/1">>
|
binary(),
|
||||||
})}
|
#{
|
||||||
, {qos, sc(qos(),
|
desc => ?DESC("republish_args_topic"),
|
||||||
#{ desc => ?DESC("republish_args_qos")
|
required => true,
|
||||||
, default => <<"${qos}">>
|
example => <<"a/1">>
|
||||||
, example => <<"${qos}">>
|
}
|
||||||
})}
|
)},
|
||||||
, {retain, sc(hoconsc:union([binary(), boolean()]),
|
{qos,
|
||||||
#{ desc => ?DESC("republish_args_retain")
|
sc(
|
||||||
, default => <<"${retain}">>
|
qos(),
|
||||||
, example => <<"${retain}">>
|
#{
|
||||||
})}
|
desc => ?DESC("republish_args_qos"),
|
||||||
, {payload, sc(binary(),
|
default => <<"${qos}">>,
|
||||||
#{ desc => ?DESC("republish_args_payload")
|
example => <<"${qos}">>
|
||||||
, default => <<"${payload}">>
|
}
|
||||||
, example => <<"${payload}">>
|
)},
|
||||||
})}
|
{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") ->
|
desc("rule_engine") ->
|
||||||
|
|
@ -129,18 +173,23 @@ desc(_) ->
|
||||||
undefined.
|
undefined.
|
||||||
|
|
||||||
rule_name() ->
|
rule_name() ->
|
||||||
{"name", sc(binary(),
|
{"name",
|
||||||
#{ desc => ?DESC("rules_name")
|
sc(
|
||||||
, default => ""
|
binary(),
|
||||||
, required => true
|
#{
|
||||||
, example => "foo"
|
desc => ?DESC("rules_name"),
|
||||||
})}.
|
default => "",
|
||||||
|
required => true,
|
||||||
|
example => "foo"
|
||||||
|
}
|
||||||
|
)}.
|
||||||
|
|
||||||
outputs() ->
|
outputs() ->
|
||||||
[ binary()
|
[
|
||||||
, ref("builtin_output_republish")
|
binary(),
|
||||||
, ref("builtin_output_console")
|
ref("builtin_output_republish"),
|
||||||
, ref("user_provided_function")
|
ref("builtin_output_console"),
|
||||||
|
ref("user_provided_function")
|
||||||
].
|
].
|
||||||
|
|
||||||
qos() ->
|
qos() ->
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,13 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
Registry = #{id => emqx_rule_engine,
|
Registry = #{
|
||||||
start => {emqx_rule_engine, start_link, []},
|
id => emqx_rule_engine,
|
||||||
restart => permanent,
|
start => {emqx_rule_engine, start_link, []},
|
||||||
shutdown => 5000,
|
restart => permanent,
|
||||||
type => worker,
|
shutdown => 5000,
|
||||||
modules => [emqx_rule_engine]},
|
type => worker,
|
||||||
|
modules => [emqx_rule_engine]
|
||||||
|
},
|
||||||
Metrics = emqx_plugin_libs_metrics:child_spec(rule_metrics),
|
Metrics = emqx_plugin_libs_metrics:child_spec(rule_metrics),
|
||||||
{ok, {{one_for_one, 10, 10}, [Registry, Metrics]}}.
|
{ok, {{one_for_one, 10, 10}, [Registry, Metrics]}}.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,257 +21,303 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% IoT Funcs
|
%% IoT Funcs
|
||||||
-export([ msgid/0
|
-export([
|
||||||
, qos/0
|
msgid/0,
|
||||||
, flags/0
|
qos/0,
|
||||||
, flag/1
|
flags/0,
|
||||||
, topic/0
|
flag/1,
|
||||||
, topic/1
|
topic/0,
|
||||||
, clientid/0
|
topic/1,
|
||||||
, clientip/0
|
clientid/0,
|
||||||
, peerhost/0
|
clientip/0,
|
||||||
, username/0
|
peerhost/0,
|
||||||
, payload/0
|
username/0,
|
||||||
, payload/1
|
payload/0,
|
||||||
, contains_topic/2
|
payload/1,
|
||||||
, contains_topic/3
|
contains_topic/2,
|
||||||
, contains_topic_match/2
|
contains_topic/3,
|
||||||
, contains_topic_match/3
|
contains_topic_match/2,
|
||||||
, null/0
|
contains_topic_match/3,
|
||||||
]).
|
null/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% Arithmetic Funcs
|
%% Arithmetic Funcs
|
||||||
-export([ '+'/2
|
-export([
|
||||||
, '-'/2
|
'+'/2,
|
||||||
, '*'/2
|
'-'/2,
|
||||||
, '/'/2
|
'*'/2,
|
||||||
, 'div'/2
|
'/'/2,
|
||||||
, mod/2
|
'div'/2,
|
||||||
, eq/2
|
mod/2,
|
||||||
]).
|
eq/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Math Funcs
|
%% Math Funcs
|
||||||
-export([ abs/1
|
-export([
|
||||||
, acos/1
|
abs/1,
|
||||||
, acosh/1
|
acos/1,
|
||||||
, asin/1
|
acosh/1,
|
||||||
, asinh/1
|
asin/1,
|
||||||
, atan/1
|
asinh/1,
|
||||||
, atanh/1
|
atan/1,
|
||||||
, ceil/1
|
atanh/1,
|
||||||
, cos/1
|
ceil/1,
|
||||||
, cosh/1
|
cos/1,
|
||||||
, exp/1
|
cosh/1,
|
||||||
, floor/1
|
exp/1,
|
||||||
, fmod/2
|
floor/1,
|
||||||
, log/1
|
fmod/2,
|
||||||
, log10/1
|
log/1,
|
||||||
, log2/1
|
log10/1,
|
||||||
, power/2
|
log2/1,
|
||||||
, round/1
|
power/2,
|
||||||
, sin/1
|
round/1,
|
||||||
, sinh/1
|
sin/1,
|
||||||
, sqrt/1
|
sinh/1,
|
||||||
, tan/1
|
sqrt/1,
|
||||||
, tanh/1
|
tan/1,
|
||||||
]).
|
tanh/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Bits Funcs
|
%% Bits Funcs
|
||||||
-export([ bitnot/1
|
-export([
|
||||||
, bitand/2
|
bitnot/1,
|
||||||
, bitor/2
|
bitand/2,
|
||||||
, bitxor/2
|
bitor/2,
|
||||||
, bitsl/2
|
bitxor/2,
|
||||||
, bitsr/2
|
bitsl/2,
|
||||||
, bitsize/1
|
bitsr/2,
|
||||||
, subbits/2
|
bitsize/1,
|
||||||
, subbits/3
|
subbits/2,
|
||||||
, subbits/6
|
subbits/3,
|
||||||
]).
|
subbits/6
|
||||||
|
]).
|
||||||
|
|
||||||
%% Data Type Conversion
|
%% Data Type Conversion
|
||||||
-export([ str/1
|
-export([
|
||||||
, str_utf8/1
|
str/1,
|
||||||
, bool/1
|
str_utf8/1,
|
||||||
, int/1
|
bool/1,
|
||||||
, float/1
|
int/1,
|
||||||
, float/2
|
float/1,
|
||||||
, map/1
|
float/2,
|
||||||
, bin2hexstr/1
|
map/1,
|
||||||
, hexstr2bin/1
|
bin2hexstr/1,
|
||||||
]).
|
hexstr2bin/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Data Type Validation Funcs
|
%% Data Type Validation Funcs
|
||||||
-export([ is_null/1
|
-export([
|
||||||
, is_not_null/1
|
is_null/1,
|
||||||
, is_str/1
|
is_not_null/1,
|
||||||
, is_bool/1
|
is_str/1,
|
||||||
, is_int/1
|
is_bool/1,
|
||||||
, is_float/1
|
is_int/1,
|
||||||
, is_num/1
|
is_float/1,
|
||||||
, is_map/1
|
is_num/1,
|
||||||
, is_array/1
|
is_map/1,
|
||||||
]).
|
is_array/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% String Funcs
|
%% String Funcs
|
||||||
-export([ lower/1
|
-export([
|
||||||
, ltrim/1
|
lower/1,
|
||||||
, reverse/1
|
ltrim/1,
|
||||||
, rtrim/1
|
reverse/1,
|
||||||
, strlen/1
|
rtrim/1,
|
||||||
, substr/2
|
strlen/1,
|
||||||
, substr/3
|
substr/2,
|
||||||
, trim/1
|
substr/3,
|
||||||
, upper/1
|
trim/1,
|
||||||
, split/2
|
upper/1,
|
||||||
, split/3
|
split/2,
|
||||||
, concat/2
|
split/3,
|
||||||
, tokens/2
|
concat/2,
|
||||||
, tokens/3
|
tokens/2,
|
||||||
, sprintf_s/2
|
tokens/3,
|
||||||
, pad/2
|
sprintf_s/2,
|
||||||
, pad/3
|
pad/2,
|
||||||
, pad/4
|
pad/3,
|
||||||
, replace/3
|
pad/4,
|
||||||
, replace/4
|
replace/3,
|
||||||
, regex_match/2
|
replace/4,
|
||||||
, regex_replace/3
|
regex_match/2,
|
||||||
, ascii/1
|
regex_replace/3,
|
||||||
, find/2
|
ascii/1,
|
||||||
, find/3
|
find/2,
|
||||||
]).
|
find/3
|
||||||
|
]).
|
||||||
|
|
||||||
%% Map Funcs
|
%% Map Funcs
|
||||||
-export([ map_new/0
|
-export([map_new/0]).
|
||||||
]).
|
|
||||||
|
|
||||||
-export([ map_get/2
|
-export([
|
||||||
, map_get/3
|
map_get/2,
|
||||||
, map_put/3
|
map_get/3,
|
||||||
]).
|
map_put/3
|
||||||
|
]).
|
||||||
|
|
||||||
%% For backward compatibility
|
%% For backward compatibility
|
||||||
-export([ mget/2
|
-export([
|
||||||
, mget/3
|
mget/2,
|
||||||
, mput/3
|
mget/3,
|
||||||
]).
|
mput/3
|
||||||
|
]).
|
||||||
|
|
||||||
%% Array Funcs
|
%% Array Funcs
|
||||||
-export([ nth/2
|
-export([
|
||||||
, length/1
|
nth/2,
|
||||||
, sublist/2
|
length/1,
|
||||||
, sublist/3
|
sublist/2,
|
||||||
, first/1
|
sublist/3,
|
||||||
, last/1
|
first/1,
|
||||||
, contains/2
|
last/1,
|
||||||
]).
|
contains/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Hash Funcs
|
%% Hash Funcs
|
||||||
-export([ md5/1
|
-export([
|
||||||
, sha/1
|
md5/1,
|
||||||
, sha256/1
|
sha/1,
|
||||||
]).
|
sha256/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Data encode and decode
|
%% Data encode and decode
|
||||||
-export([ base64_encode/1
|
-export([
|
||||||
, base64_decode/1
|
base64_encode/1,
|
||||||
, json_decode/1
|
base64_decode/1,
|
||||||
, json_encode/1
|
json_decode/1,
|
||||||
, term_decode/1
|
json_encode/1,
|
||||||
, term_encode/1
|
term_decode/1,
|
||||||
]).
|
term_encode/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Date functions
|
%% Date functions
|
||||||
-export([ now_rfc3339/0
|
-export([
|
||||||
, now_rfc3339/1
|
now_rfc3339/0,
|
||||||
, unix_ts_to_rfc3339/1
|
now_rfc3339/1,
|
||||||
, unix_ts_to_rfc3339/2
|
unix_ts_to_rfc3339/1,
|
||||||
, rfc3339_to_unix_ts/1
|
unix_ts_to_rfc3339/2,
|
||||||
, rfc3339_to_unix_ts/2
|
rfc3339_to_unix_ts/1,
|
||||||
, now_timestamp/0
|
rfc3339_to_unix_ts/2,
|
||||||
, now_timestamp/1
|
now_timestamp/0,
|
||||||
, format_date/3
|
now_timestamp/1,
|
||||||
, format_date/4
|
format_date/3,
|
||||||
, date_to_unix_ts/4
|
format_date/4,
|
||||||
]).
|
date_to_unix_ts/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% Proc Dict Func
|
%% Proc Dict Func
|
||||||
-export([ proc_dict_get/1
|
-export([
|
||||||
, proc_dict_put/2
|
proc_dict_get/1,
|
||||||
, proc_dict_del/1
|
proc_dict_put/2,
|
||||||
, kv_store_get/1
|
proc_dict_del/1,
|
||||||
, kv_store_get/2
|
kv_store_get/1,
|
||||||
, kv_store_put/2
|
kv_store_get/2,
|
||||||
, kv_store_del/1
|
kv_store_put/2,
|
||||||
]).
|
kv_store_del/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export(['$handle_undefined_function'/2]).
|
-export(['$handle_undefined_function'/2]).
|
||||||
|
|
||||||
-compile({no_auto_import,
|
-compile(
|
||||||
[ abs/1
|
{no_auto_import, [
|
||||||
, ceil/1
|
abs/1,
|
||||||
, floor/1
|
ceil/1,
|
||||||
, round/1
|
floor/1,
|
||||||
, map_get/2
|
round/1,
|
||||||
]}).
|
map_get/2
|
||||||
|
]}
|
||||||
|
).
|
||||||
|
|
||||||
-define(is_var(X), is_binary(X)).
|
-define(is_var(X), is_binary(X)).
|
||||||
|
|
||||||
%% @doc "msgid()" Func
|
%% @doc "msgid()" Func
|
||||||
msgid() ->
|
msgid() ->
|
||||||
fun(#{id := MsgId}) -> MsgId; (_) -> undefined end.
|
fun
|
||||||
|
(#{id := MsgId}) -> MsgId;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "qos()" Func
|
%% @doc "qos()" Func
|
||||||
qos() ->
|
qos() ->
|
||||||
fun(#{qos := QoS}) -> QoS; (_) -> undefined end.
|
fun
|
||||||
|
(#{qos := QoS}) -> QoS;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "topic()" Func
|
%% @doc "topic()" Func
|
||||||
topic() ->
|
topic() ->
|
||||||
fun(#{topic := Topic}) -> Topic; (_) -> undefined end.
|
fun
|
||||||
|
(#{topic := Topic}) -> Topic;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "topic(N)" Func
|
%% @doc "topic(N)" Func
|
||||||
topic(I) when is_integer(I) ->
|
topic(I) when is_integer(I) ->
|
||||||
fun(#{topic := Topic}) ->
|
fun
|
||||||
|
(#{topic := Topic}) ->
|
||||||
lists:nth(I, emqx_topic:tokens(Topic));
|
lists:nth(I, emqx_topic:tokens(Topic));
|
||||||
(_) -> undefined
|
(_) ->
|
||||||
|
undefined
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc "flags()" Func
|
%% @doc "flags()" Func
|
||||||
flags() ->
|
flags() ->
|
||||||
fun(#{flags := Flags}) -> Flags; (_) -> #{} end.
|
fun
|
||||||
|
(#{flags := Flags}) -> Flags;
|
||||||
|
(_) -> #{}
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "flags(Name)" Func
|
%% @doc "flags(Name)" Func
|
||||||
flag(Name) ->
|
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
|
%% @doc "clientid()" Func
|
||||||
clientid() ->
|
clientid() ->
|
||||||
fun(#{from := ClientId}) -> ClientId; (_) -> undefined end.
|
fun
|
||||||
|
(#{from := ClientId}) -> ClientId;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "username()" Func
|
%% @doc "username()" Func
|
||||||
username() ->
|
username() ->
|
||||||
fun(#{username := Username}) -> Username; (_) -> undefined end.
|
fun
|
||||||
|
(#{username := Username}) -> Username;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
%% @doc "clientip()" Func
|
%% @doc "clientip()" Func
|
||||||
clientip() ->
|
clientip() ->
|
||||||
peerhost().
|
peerhost().
|
||||||
|
|
||||||
peerhost() ->
|
peerhost() ->
|
||||||
fun(#{peerhost := Addr}) -> Addr; (_) -> undefined end.
|
fun
|
||||||
|
(#{peerhost := Addr}) -> Addr;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
payload() ->
|
payload() ->
|
||||||
fun(#{payload := Payload}) -> Payload; (_) -> undefined end.
|
fun
|
||||||
|
(#{payload := Payload}) -> Payload;
|
||||||
|
(_) -> undefined
|
||||||
|
end.
|
||||||
|
|
||||||
payload(Path) ->
|
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);
|
emqx_rule_maps:nested_get(map_path(Path), Payload);
|
||||||
(_) -> undefined
|
(_) ->
|
||||||
|
undefined
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Check if a topic_filter contains a specific topic
|
%% @doc Check if a topic_filter contains a specific topic
|
||||||
%% TopicFilters = [{<<"t/a">>, #{qos => 0}].
|
%% TopicFilters = [{<<"t/a">>, #{qos => 0}].
|
||||||
-spec(contains_topic(emqx_types:topic_filters(), emqx_types:topic())
|
-spec contains_topic(emqx_types:topic_filters(), emqx_types:topic()) ->
|
||||||
-> true | false).
|
true | false.
|
||||||
contains_topic(TopicFilters, Topic) ->
|
contains_topic(TopicFilters, Topic) ->
|
||||||
case find_topic_filter(Topic, TopicFilters, fun eq/2) of
|
case find_topic_filter(Topic, TopicFilters, fun eq/2) of
|
||||||
not_found -> false;
|
not_found -> false;
|
||||||
|
|
@ -283,8 +329,8 @@ contains_topic(TopicFilters, Topic, QoS) ->
|
||||||
_ -> false
|
_ -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(contains_topic_match(emqx_types:topic_filters(), emqx_types:topic())
|
-spec contains_topic_match(emqx_types:topic_filters(), emqx_types:topic()) ->
|
||||||
-> true | false).
|
true | false.
|
||||||
contains_topic_match(TopicFilters, Topic) ->
|
contains_topic_match(TopicFilters, Topic) ->
|
||||||
case find_topic_filter(Topic, TopicFilters, fun emqx_topic:match/2) of
|
case find_topic_filter(Topic, TopicFilters, fun emqx_topic:match/2) of
|
||||||
not_found -> false;
|
not_found -> false;
|
||||||
|
|
@ -298,10 +344,13 @@ contains_topic_match(TopicFilters, Topic, QoS) ->
|
||||||
|
|
||||||
find_topic_filter(Filter, TopicFilters, Func) ->
|
find_topic_filter(Filter, TopicFilters, Func) ->
|
||||||
try
|
try
|
||||||
[case Func(Topic, Filter) of
|
[
|
||||||
true -> throw(Result);
|
case Func(Topic, Filter) of
|
||||||
false -> ok
|
true -> throw(Result);
|
||||||
end || Result = #{topic := Topic} <- TopicFilters],
|
false -> ok
|
||||||
|
end
|
||||||
|
|| Result = #{topic := Topic} <- TopicFilters
|
||||||
|
],
|
||||||
not_found
|
not_found
|
||||||
catch
|
catch
|
||||||
throw:Result -> Result
|
throw:Result -> Result
|
||||||
|
|
@ -317,7 +366,6 @@ null() ->
|
||||||
%% plus 2 numbers
|
%% plus 2 numbers
|
||||||
'+'(X, Y) when is_number(X), is_number(Y) ->
|
'+'(X, Y) when is_number(X), is_number(Y) ->
|
||||||
X + Y;
|
X + Y;
|
||||||
|
|
||||||
%% string concatenation
|
%% string concatenation
|
||||||
%% this requires one of the arguments is string, the other argument will be converted
|
%% this requires one of the arguments is string, the other argument will be converted
|
||||||
%% to string automatically (implicit conversion)
|
%% to string automatically (implicit conversion)
|
||||||
|
|
@ -355,7 +403,7 @@ acos(N) when is_number(N) ->
|
||||||
acosh(N) when is_number(N) ->
|
acosh(N) when is_number(N) ->
|
||||||
math:acosh(N).
|
math:acosh(N).
|
||||||
|
|
||||||
asin(N) when is_number(N)->
|
asin(N) when is_number(N) ->
|
||||||
math:asin(N).
|
math:asin(N).
|
||||||
|
|
||||||
asinh(N) when is_number(N) ->
|
asinh(N) when is_number(N) ->
|
||||||
|
|
@ -364,19 +412,19 @@ asinh(N) when is_number(N) ->
|
||||||
atan(N) when is_number(N) ->
|
atan(N) when is_number(N) ->
|
||||||
math:atan(N).
|
math:atan(N).
|
||||||
|
|
||||||
atanh(N) when is_number(N)->
|
atanh(N) when is_number(N) ->
|
||||||
math:atanh(N).
|
math:atanh(N).
|
||||||
|
|
||||||
ceil(N) when is_number(N) ->
|
ceil(N) when is_number(N) ->
|
||||||
erlang:ceil(N).
|
erlang:ceil(N).
|
||||||
|
|
||||||
cos(N) when is_number(N)->
|
cos(N) when is_number(N) ->
|
||||||
math:cos(N).
|
math:cos(N).
|
||||||
|
|
||||||
cosh(N) when is_number(N) ->
|
cosh(N) when is_number(N) ->
|
||||||
math:cosh(N).
|
math:cosh(N).
|
||||||
|
|
||||||
exp(N) when is_number(N)->
|
exp(N) when is_number(N) ->
|
||||||
math:exp(N).
|
math:exp(N).
|
||||||
|
|
||||||
floor(N) when is_number(N) ->
|
floor(N) when is_number(N) ->
|
||||||
|
|
@ -391,7 +439,7 @@ log(N) when is_number(N) ->
|
||||||
log10(N) when is_number(N) ->
|
log10(N) when is_number(N) ->
|
||||||
math:log10(N).
|
math:log10(N).
|
||||||
|
|
||||||
log2(N) when is_number(N)->
|
log2(N) when is_number(N) ->
|
||||||
math:log2(N).
|
math:log2(N).
|
||||||
|
|
||||||
power(X, Y) when is_number(X), is_number(Y) ->
|
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) ->
|
subbits(Bits, Start, Len) when is_integer(Start), is_integer(Len), is_bitstring(Bits) ->
|
||||||
get_subbits(Bits, Start, Len, <<"integer">>, <<"unsigned">>, <<"big">>).
|
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).
|
||||||
|
|
||||||
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 =/= <<>> ->
|
<<_:Begin, Rem/bits>> when Rem =/= <<>> ->
|
||||||
Sz = bit_size(Rem),
|
Sz = bit_size(Rem),
|
||||||
do_get_subbits(Rem, Sz, Len, Type, Signedness, Endianness);
|
do_get_subbits(Rem, Sz, Len, Type, Signedness, Endianness);
|
||||||
_ -> undefined
|
_ ->
|
||||||
|
undefined
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-define(match_bits(Bits0, Pattern, ElesePattern),
|
-define(match_bits(Bits0, Pattern, ElesePattern),
|
||||||
|
|
@ -464,46 +515,80 @@ get_subbits(Bits, Start, Len, Type, Signedness, Endianness) ->
|
||||||
SubBits;
|
SubBits;
|
||||||
ElesePattern ->
|
ElesePattern ->
|
||||||
SubBits
|
SubBits
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/integer-unsigned-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/integer-unsigned-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/integer-unsigned-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/integer-unsigned-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/float-unsigned-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/float-unsigned-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/float-unsigned-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/float-unsigned-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/bits-unsigned-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/bits-unsigned-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/bits-unsigned-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/bits-unsigned-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/integer-signed-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/integer-signed-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/integer-signed-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/integer-signed-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/float-signed-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/float-signed-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/float-signed-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/float-signed-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"big">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"big">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/bits-signed-big-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/bits-signed-big-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/bits-signed-big-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/bits-signed-big-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"unsigned">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/integer-unsigned-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/integer-unsigned-little-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/integer-unsigned-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/integer-unsigned-little-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"unsigned">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/float-unsigned-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/float-unsigned-little-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/float-unsigned-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/float-unsigned-little-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"unsigned">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/bits-unsigned-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/bits-unsigned-little-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/bits-unsigned-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/bits-unsigned-little-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"integer">>, <<"signed">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/integer-signed-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/integer-signed-little-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/integer-signed-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/integer-signed-little-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"float">>, <<"signed">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/float-signed-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/float-signed-little-unit:1>>);
|
Bits,
|
||||||
|
<<SubBits:Len/float-signed-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/float-signed-little-unit:1>>
|
||||||
|
);
|
||||||
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"little">>) ->
|
do_get_subbits(Bits, Sz, Len, <<"bits">>, <<"signed">>, <<"little">>) ->
|
||||||
?match_bits(Bits, <<SubBits:Len/bits-signed-little-unit:1, _/bits>>,
|
?match_bits(
|
||||||
<<SubBits:Sz/bits-signed-little-unit:1>>).
|
Bits,
|
||||||
|
<<SubBits:Len/bits-signed-little-unit:1, _/bits>>,
|
||||||
|
<<SubBits:Sz/bits-signed-little-unit:1>>
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Data Type Conversion Funcs
|
%% Data Type Conversion Funcs
|
||||||
|
|
@ -590,9 +675,11 @@ strlen(S) when is_binary(S) ->
|
||||||
substr(S, Start) when is_binary(S), is_integer(Start) ->
|
substr(S, Start) when is_binary(S), is_integer(Start) ->
|
||||||
string:slice(S, Start).
|
string:slice(S, Start).
|
||||||
|
|
||||||
substr(S, Start, Length) when is_binary(S),
|
substr(S, Start, Length) when
|
||||||
is_integer(Start),
|
is_binary(S),
|
||||||
is_integer(Length) ->
|
is_integer(Start),
|
||||||
|
is_integer(Length)
|
||||||
|
->
|
||||||
string:slice(S, Start, Length).
|
string:slice(S, Start, Length).
|
||||||
|
|
||||||
trim(S) when is_binary(S) ->
|
trim(S) when is_binary(S) ->
|
||||||
|
|
@ -601,26 +688,28 @@ trim(S) when is_binary(S) ->
|
||||||
upper(S) when is_binary(S) ->
|
upper(S) when is_binary(S) ->
|
||||||
string:uppercase(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 =/= ""].
|
[R || R <- string:split(S, P, all), R =/= <<>> andalso R =/= ""].
|
||||||
|
|
||||||
split(S, P, <<"notrim">>) ->
|
split(S, P, <<"notrim">>) ->
|
||||||
string:split(S, P, all);
|
string:split(S, P, all);
|
||||||
|
|
||||||
split(S, P, <<"leading_notrim">>) ->
|
split(S, P, <<"leading_notrim">>) ->
|
||||||
string:split(S, P, leading);
|
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 =/= ""];
|
[R || R <- string:split(S, P, leading), R =/= <<>> andalso R =/= ""];
|
||||||
split(S, P, <<"trailing_notrim">>) ->
|
split(S, P, <<"trailing_notrim">>) ->
|
||||||
string:split(S, P, trailing);
|
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 =/= ""].
|
[R || R <- string:split(S, P, trailing), R =/= <<>> andalso R =/= ""].
|
||||||
|
|
||||||
tokens(S, Separators) ->
|
tokens(S, Separators) ->
|
||||||
[list_to_binary(R) || R <- string:lexemes(binary_to_list(S), binary_to_list(Separators))].
|
[list_to_binary(R) || R <- string:lexemes(binary_to_list(S), binary_to_list(Separators))].
|
||||||
|
|
||||||
tokens(S, Separators, <<"nocrlf">>) ->
|
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
|
%% implicit convert args to strings, and then do concatenation
|
||||||
concat(S1, S2) ->
|
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) ->
|
pad(S, Len, <<"trailing">>) when is_binary(S), is_integer(Len) ->
|
||||||
iolist_to_binary(string:pad(S, Len, trailing));
|
iolist_to_binary(string:pad(S, Len, trailing));
|
||||||
|
|
||||||
pad(S, Len, <<"both">>) when is_binary(S), is_integer(Len) ->
|
pad(S, Len, <<"both">>) when is_binary(S), is_integer(Len) ->
|
||||||
iolist_to_binary(string:pad(S, Len, both));
|
iolist_to_binary(string:pad(S, Len, both));
|
||||||
|
|
||||||
pad(S, Len, <<"leading">>) when is_binary(S), is_integer(Len) ->
|
pad(S, Len, <<"leading">>) when is_binary(S), is_integer(Len) ->
|
||||||
iolist_to_binary(string:pad(S, Len, leading)).
|
iolist_to_binary(string:pad(S, Len, leading)).
|
||||||
|
|
||||||
pad(S, Len, <<"trailing">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
pad(S, Len, <<"trailing">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
||||||
Chars = unicode:characters_to_list(Char, utf8),
|
Chars = unicode:characters_to_list(Char, utf8),
|
||||||
iolist_to_binary(string:pad(S, Len, trailing, Chars));
|
iolist_to_binary(string:pad(S, Len, trailing, Chars));
|
||||||
|
|
||||||
pad(S, Len, <<"both">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
pad(S, Len, <<"both">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
||||||
Chars = unicode:characters_to_list(Char, utf8),
|
Chars = unicode:characters_to_list(Char, utf8),
|
||||||
iolist_to_binary(string:pad(S, Len, both, Chars));
|
iolist_to_binary(string:pad(S, Len, both, Chars));
|
||||||
|
|
||||||
pad(S, Len, <<"leading">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
pad(S, Len, <<"leading">>, Char) when is_binary(S), is_integer(Len), is_binary(Char) ->
|
||||||
Chars = unicode:characters_to_list(Char, utf8),
|
Chars = unicode:characters_to_list(Char, utf8),
|
||||||
iolist_to_binary(string:pad(S, Len, leading, Chars)).
|
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) ->
|
replace(SrcStr, P, RepStr, <<"all">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) ->
|
||||||
iolist_to_binary(string:replace(SrcStr, P, RepStr, all));
|
iolist_to_binary(string:replace(SrcStr, P, RepStr, all));
|
||||||
|
replace(SrcStr, P, RepStr, <<"trailing">>) when
|
||||||
replace(SrcStr, P, RepStr, <<"trailing">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) ->
|
is_binary(SrcStr), is_binary(P), is_binary(RepStr)
|
||||||
|
->
|
||||||
iolist_to_binary(string:replace(SrcStr, P, RepStr, trailing));
|
iolist_to_binary(string:replace(SrcStr, P, RepStr, trailing));
|
||||||
|
|
||||||
replace(SrcStr, P, RepStr, <<"leading">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) ->
|
replace(SrcStr, P, RepStr, <<"leading">>) when is_binary(SrcStr), is_binary(P), is_binary(RepStr) ->
|
||||||
iolist_to_binary(string:replace(SrcStr, P, RepStr, leading)).
|
iolist_to_binary(string:replace(SrcStr, P, RepStr, leading)).
|
||||||
|
|
||||||
regex_match(Str, RE) ->
|
regex_match(Str, RE) ->
|
||||||
case re:run(Str, RE, [global,{capture,none}]) of
|
case re:run(Str, RE, [global, {capture, none}]) of
|
||||||
match -> true;
|
match -> true;
|
||||||
nomatch -> false
|
nomatch -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
regex_replace(SrcStr, RE, RepStr) ->
|
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) ->
|
ascii(Char) when is_binary(Char) ->
|
||||||
[FirstC| _] = binary_to_list(Char),
|
[FirstC | _] = binary_to_list(Char),
|
||||||
FirstC.
|
FirstC.
|
||||||
|
|
||||||
find(S, P) when is_binary(S), is_binary(P) ->
|
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, P, <<"trailing">>) when is_binary(S), is_binary(P) ->
|
||||||
find_s(S, P, trailing);
|
find_s(S, P, trailing);
|
||||||
|
|
||||||
find(S, P, <<"leading">>) when is_binary(S), is_binary(P) ->
|
find(S, P, <<"leading">>) when is_binary(S), is_binary(P) ->
|
||||||
find_s(S, P, leading).
|
find_s(S, P, leading).
|
||||||
|
|
||||||
|
|
@ -735,7 +819,8 @@ mget(Key, Map) ->
|
||||||
|
|
||||||
mget(Key, Map, Default) ->
|
mget(Key, Map, Default) ->
|
||||||
case maps:find(Key, Map) of
|
case maps:find(Key, Map) of
|
||||||
{ok, Val} -> Val;
|
{ok, Val} ->
|
||||||
|
Val;
|
||||||
error when is_atom(Key) ->
|
error when is_atom(Key) ->
|
||||||
%% the map may have an equivalent binary-form key
|
%% the map may have an equivalent binary-form key
|
||||||
BinKey = emqx_plugin_libs_rule:bin(Key),
|
BinKey = emqx_plugin_libs_rule:bin(Key),
|
||||||
|
|
@ -744,14 +829,16 @@ mget(Key, Map, Default) ->
|
||||||
error -> Default
|
error -> Default
|
||||||
end;
|
end;
|
||||||
error when is_binary(Key) ->
|
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)),
|
AtomKey = list_to_existing_atom(binary_to_list(Key)),
|
||||||
case maps:find(AtomKey, Map) of
|
case maps:find(AtomKey, Map) of
|
||||||
{ok, Val} -> Val;
|
{ok, Val} -> Val;
|
||||||
error -> Default
|
error -> Default
|
||||||
end
|
end
|
||||||
catch error:badarg ->
|
catch
|
||||||
Default
|
error:badarg ->
|
||||||
|
Default
|
||||||
end;
|
end;
|
||||||
error ->
|
error ->
|
||||||
Default
|
Default
|
||||||
|
|
@ -759,7 +846,8 @@ mget(Key, Map, Default) ->
|
||||||
|
|
||||||
mput(Key, Val, Map) ->
|
mput(Key, Val, Map) ->
|
||||||
case maps:find(Key, Map) of
|
case maps:find(Key, Map) of
|
||||||
{ok, _} -> maps:put(Key, Val, Map);
|
{ok, _} ->
|
||||||
|
maps:put(Key, Val, Map);
|
||||||
error when is_atom(Key) ->
|
error when is_atom(Key) ->
|
||||||
%% the map may have an equivalent binary-form key
|
%% the map may have an equivalent binary-form key
|
||||||
BinKey = emqx_plugin_libs_rule:bin(Key),
|
BinKey = emqx_plugin_libs_rule:bin(Key),
|
||||||
|
|
@ -768,14 +856,16 @@ mput(Key, Val, Map) ->
|
||||||
error -> maps:put(Key, Val, Map)
|
error -> maps:put(Key, Val, Map)
|
||||||
end;
|
end;
|
||||||
error when is_binary(Key) ->
|
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)),
|
AtomKey = list_to_existing_atom(binary_to_list(Key)),
|
||||||
case maps:find(AtomKey, Map) of
|
case maps:find(AtomKey, Map) of
|
||||||
{ok, _} -> maps:put(AtomKey, Val, Map);
|
{ok, _} -> maps:put(AtomKey, Val, Map);
|
||||||
error -> maps:put(Key, Val, Map)
|
error -> maps:put(Key, Val, Map)
|
||||||
end
|
end
|
||||||
catch error:badarg ->
|
catch
|
||||||
maps:put(Key, Val, Map)
|
error:badarg ->
|
||||||
|
maps:put(Key, Val, Map)
|
||||||
end;
|
end;
|
||||||
error ->
|
error ->
|
||||||
maps:put(Key, Val, Map)
|
maps:put(Key, Val, Map)
|
||||||
|
|
@ -863,14 +953,18 @@ unix_ts_to_rfc3339(Epoch) ->
|
||||||
unix_ts_to_rfc3339(Epoch, Unit) when is_integer(Epoch) ->
|
unix_ts_to_rfc3339(Epoch, Unit) when is_integer(Epoch) ->
|
||||||
emqx_plugin_libs_rule:bin(
|
emqx_plugin_libs_rule:bin(
|
||||||
calendar:system_time_to_rfc3339(
|
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) ->
|
||||||
rfc3339_to_unix_ts(DateTime, <<"second">>).
|
rfc3339_to_unix_ts(DateTime, <<"second">>).
|
||||||
|
|
||||||
rfc3339_to_unix_ts(DateTime, Unit) when is_binary(DateTime) ->
|
rfc3339_to_unix_ts(DateTime, Unit) when is_binary(DateTime) ->
|
||||||
calendar:rfc3339_to_system_time(binary_to_list(DateTime),
|
calendar:rfc3339_to_system_time(
|
||||||
[{unit, time_unit(Unit)}]).
|
binary_to_list(DateTime),
|
||||||
|
[{unit, time_unit(Unit)}]
|
||||||
|
).
|
||||||
|
|
||||||
now_timestamp() ->
|
now_timestamp() ->
|
||||||
erlang:system_time(second).
|
erlang:system_time(second).
|
||||||
|
|
@ -885,22 +979,30 @@ time_unit(<<"nanosecond">>) -> nanosecond.
|
||||||
|
|
||||||
format_date(TimeUnit, Offset, FormatString) ->
|
format_date(TimeUnit, Offset, FormatString) ->
|
||||||
emqx_plugin_libs_rule:bin(
|
emqx_plugin_libs_rule:bin(
|
||||||
emqx_rule_date:date(time_unit(TimeUnit),
|
emqx_rule_date:date(
|
||||||
emqx_plugin_libs_rule:str(Offset),
|
time_unit(TimeUnit),
|
||||||
emqx_plugin_libs_rule:str(FormatString))).
|
emqx_plugin_libs_rule:str(Offset),
|
||||||
|
emqx_plugin_libs_rule:str(FormatString)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
|
format_date(TimeUnit, Offset, FormatString, TimeEpoch) ->
|
||||||
emqx_plugin_libs_rule:bin(
|
emqx_plugin_libs_rule:bin(
|
||||||
emqx_rule_date:date(time_unit(TimeUnit),
|
emqx_rule_date:date(
|
||||||
emqx_plugin_libs_rule:str(Offset),
|
time_unit(TimeUnit),
|
||||||
emqx_plugin_libs_rule:str(FormatString),
|
emqx_plugin_libs_rule:str(Offset),
|
||||||
TimeEpoch)).
|
emqx_plugin_libs_rule:str(FormatString),
|
||||||
|
TimeEpoch
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
|
date_to_unix_ts(TimeUnit, Offset, FormatString, InputString) ->
|
||||||
emqx_rule_date:parse_date(time_unit(TimeUnit),
|
emqx_rule_date:parse_date(
|
||||||
emqx_plugin_libs_rule:str(Offset),
|
time_unit(TimeUnit),
|
||||||
emqx_plugin_libs_rule:str(FormatString),
|
emqx_plugin_libs_rule:str(Offset),
|
||||||
emqx_plugin_libs_rule:str(InputString)).
|
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.
|
%% @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
|
%% 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) ->
|
% '$handle_undefined_function'(Fun, Args) ->
|
||||||
% error({sql_function_not_supported, function_literal(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]);
|
erlang:apply(fun sprintf_s/2, [Format, Args]);
|
||||||
|
|
||||||
'$handle_undefined_function'(Fun, Args) ->
|
'$handle_undefined_function'(Fun, Args) ->
|
||||||
error({sql_function_not_supported, function_literal(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) ++ "()";
|
atom_to_list(Fun) ++ "()";
|
||||||
function_literal(Fun, [FArg | Args]) when is_atom(Fun), is_list(Args) ->
|
function_literal(Fun, [FArg | Args]) when is_atom(Fun), is_list(Args) ->
|
||||||
WithFirstArg = io_lib:format("~ts(~0p", [atom_to_list(Fun), FArg]),
|
WithFirstArg = io_lib:format("~ts(~0p", [atom_to_list(Fun), FArg]),
|
||||||
lists:foldl(fun(Arg, Literal) ->
|
lists:foldl(
|
||||||
io_lib:format("~ts, ~0p", [Literal, Arg])
|
fun(Arg, Literal) ->
|
||||||
end, WithFirstArg, Args) ++ ")";
|
io_lib:format("~ts, ~0p", [Literal, Arg])
|
||||||
|
end,
|
||||||
|
WithFirstArg,
|
||||||
|
Args
|
||||||
|
) ++ ")";
|
||||||
function_literal(Fun, Args) ->
|
function_literal(Fun, Args) ->
|
||||||
{invalid_func, {Fun, Args}}.
|
{invalid_func, {Fun, Args}}.
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@
|
||||||
|
|
||||||
-module(emqx_rule_maps).
|
-module(emqx_rule_maps).
|
||||||
|
|
||||||
-export([ nested_get/2
|
-export([
|
||||||
, nested_get/3
|
nested_get/2,
|
||||||
, nested_put/3
|
nested_get/3,
|
||||||
, range_gen/2
|
nested_put/3,
|
||||||
, range_get/3
|
range_gen/2,
|
||||||
, atom_key_map/1
|
range_get/3,
|
||||||
, unsafe_atom_key_map/1
|
atom_key_map/1,
|
||||||
]).
|
unsafe_atom_key_map/1
|
||||||
|
]).
|
||||||
|
|
||||||
nested_get(Key, Data) ->
|
nested_get(Key, Data) ->
|
||||||
nested_get(Key, Data, undefined).
|
nested_get(Key, Data, undefined).
|
||||||
|
|
@ -41,8 +42,10 @@ do_nested_get([Key | More], Data, OrgData, Default) ->
|
||||||
do_nested_get([], Val, _OrgData, _Default) ->
|
do_nested_get([], Val, _OrgData, _Default) ->
|
||||||
Val.
|
Val.
|
||||||
|
|
||||||
nested_put(Key, Val, Data) when not is_map(Data),
|
nested_put(Key, Val, Data) when
|
||||||
not is_list(Data) ->
|
not is_map(Data),
|
||||||
|
not is_list(Data)
|
||||||
|
->
|
||||||
nested_put(Key, Val, #{});
|
nested_put(Key, Val, #{});
|
||||||
nested_put({var, Key}, Val, Map) ->
|
nested_put({var, Key}, Val, Map) ->
|
||||||
general_map_put({key, Key}, Val, Map, Map);
|
general_map_put({key, Key}, Val, Map, Map);
|
||||||
|
|
@ -56,19 +59,27 @@ do_nested_put([], Val, _Map, _OrgData) ->
|
||||||
Val.
|
Val.
|
||||||
|
|
||||||
general_map_get(Key, Map, OrgData, Default) ->
|
general_map_get(Key, Map, OrgData, Default) ->
|
||||||
general_find(Key, Map, OrgData,
|
general_find(
|
||||||
|
Key,
|
||||||
|
Map,
|
||||||
|
OrgData,
|
||||||
fun
|
fun
|
||||||
({equivalent, {_EquiKey, Val}}) -> Val;
|
({equivalent, {_EquiKey, Val}}) -> Val;
|
||||||
({found, {_Key, Val}}) -> Val;
|
({found, {_Key, Val}}) -> Val;
|
||||||
(not_found) -> Default
|
(not_found) -> Default
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
general_map_put(Key, Val, Map, OrgData) ->
|
general_map_put(Key, Val, Map, OrgData) ->
|
||||||
general_find(Key, Map, OrgData,
|
general_find(
|
||||||
|
Key,
|
||||||
|
Map,
|
||||||
|
OrgData,
|
||||||
fun
|
fun
|
||||||
({equivalent, {EquiKey, _Val}}) -> do_put(EquiKey, Val, Map, OrgData);
|
({equivalent, {EquiKey, _Val}}) -> do_put(EquiKey, Val, Map, OrgData);
|
||||||
(_) -> do_put(Key, Val, Map, OrgData)
|
(_) -> do_put(Key, Val, Map, OrgData)
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
general_find(KeyOrIndex, Data, OrgData, Handler) when is_binary(Data) ->
|
general_find(KeyOrIndex, Data, OrgData, Handler) when is_binary(Data) ->
|
||||||
try emqx_json:decode(Data, [return_maps]) of
|
try emqx_json:decode(Data, [return_maps]) of
|
||||||
|
|
@ -78,7 +89,8 @@ general_find(KeyOrIndex, Data, OrgData, Handler) when is_binary(Data) ->
|
||||||
end;
|
end;
|
||||||
general_find({key, Key}, Map, _OrgData, Handler) when is_map(Map) ->
|
general_find({key, Key}, Map, _OrgData, Handler) when is_map(Map) ->
|
||||||
case maps:find(Key, Map) of
|
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) ->
|
error when is_atom(Key) ->
|
||||||
%% the map may have an equivalent binary-form key
|
%% the map may have an equivalent binary-form key
|
||||||
BinKey = emqx_plugin_libs_rule:bin(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)
|
error -> Handler(not_found)
|
||||||
end;
|
end;
|
||||||
error when is_binary(Key) ->
|
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)),
|
AtomKey = list_to_existing_atom(binary_to_list(Key)),
|
||||||
case maps:find(AtomKey, Map) of
|
case maps:find(AtomKey, Map) of
|
||||||
{ok, Val} -> Handler({equivalent, {{key, AtomKey}, Val}});
|
{ok, Val} -> Handler({equivalent, {{key, AtomKey}, Val}});
|
||||||
error -> Handler(not_found)
|
error -> Handler(not_found)
|
||||||
end
|
end
|
||||||
catch error:badarg ->
|
catch
|
||||||
Handler(not_found)
|
error:badarg ->
|
||||||
|
Handler(not_found)
|
||||||
end;
|
end;
|
||||||
error ->
|
error ->
|
||||||
Handler(not_found)
|
Handler(not_found)
|
||||||
|
|
@ -122,18 +136,21 @@ do_put({index, Index0}, Val, List, OrgData) ->
|
||||||
setnth(_, Data, Val) when not is_list(Data) ->
|
setnth(_, Data, Val) when not is_list(Data) ->
|
||||||
setnth(head, [], Val);
|
setnth(head, [], Val);
|
||||||
setnth(head, List, Val) when is_list(List) -> [Val | List];
|
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) 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(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 ->
|
setnth(I, List, Val) when is_integer(I), I > 0 ->
|
||||||
do_setnth(I, List, Val);
|
do_setnth(I, List, Val);
|
||||||
setnth(I, List, Val) when is_integer(I), I < 0 ->
|
setnth(I, List, Val) when is_integer(I), I < 0 ->
|
||||||
lists:reverse(do_setnth(-I, lists:reverse(List), Val)).
|
lists:reverse(do_setnth(-I, lists:reverse(List), Val)).
|
||||||
|
|
||||||
do_setnth(1, [_ | Rest], Val) -> [Val | Rest];
|
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) -> [].
|
do_setnth(_, [], _Val) -> [].
|
||||||
|
|
||||||
getnth(0, _) ->
|
getnth(0, _) ->
|
||||||
|
|
@ -144,8 +161,10 @@ getnth(I, L) when I < 0 ->
|
||||||
do_getnth(-I, lists:reverse(L)).
|
do_getnth(-I, lists:reverse(L)).
|
||||||
|
|
||||||
do_getnth(I, L) ->
|
do_getnth(I, L) ->
|
||||||
try {ok, lists:nth(I, L)}
|
try
|
||||||
catch error:_ -> {error, not_found}
|
{ok, lists:nth(I, L)}
|
||||||
|
catch
|
||||||
|
error:_ -> {error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle_getnth(Index, List, IndexPattern, Handler) ->
|
handle_getnth(Index, List, IndexPattern, Handler) ->
|
||||||
|
|
@ -170,7 +189,8 @@ do_range_get(Begin, End, List) ->
|
||||||
EndIndex = index(End, TotalLen),
|
EndIndex = index(End, TotalLen),
|
||||||
lists:sublist(List, BeginIndex, (EndIndex - BeginIndex + 1)).
|
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, _) when Index > 0 -> Index;
|
||||||
index(Index, Len) when Index < 0 ->
|
index(Index, Len) when Index < 0 ->
|
||||||
Len + Index + 1.
|
Len + Index + 1.
|
||||||
|
|
@ -180,26 +200,36 @@ index(Index, Len) when Index < 0 ->
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
atom_key_map(BinKeyMap) when is_map(BinKeyMap) ->
|
atom_key_map(BinKeyMap) when is_map(BinKeyMap) ->
|
||||||
maps:fold(
|
maps:fold(
|
||||||
fun(K, V, Acc) when is_binary(K) ->
|
fun
|
||||||
Acc#{binary_to_existing_atom(K, utf8) => atom_key_map(V)};
|
(K, V, Acc) when is_binary(K) ->
|
||||||
(K, V, Acc) when is_list(K) ->
|
Acc#{binary_to_existing_atom(K, utf8) => atom_key_map(V)};
|
||||||
Acc#{list_to_existing_atom(K) => atom_key_map(V)};
|
(K, V, Acc) when is_list(K) ->
|
||||||
(K, V, Acc) when is_atom(K) ->
|
Acc#{list_to_existing_atom(K) => atom_key_map(V)};
|
||||||
Acc#{K => atom_key_map(V)}
|
(K, V, Acc) when is_atom(K) ->
|
||||||
end, #{}, BinKeyMap);
|
Acc#{K => atom_key_map(V)}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
BinKeyMap
|
||||||
|
);
|
||||||
atom_key_map(ListV) when is_list(ListV) ->
|
atom_key_map(ListV) when is_list(ListV) ->
|
||||||
[atom_key_map(V) || V <- 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) ->
|
unsafe_atom_key_map(BinKeyMap) when is_map(BinKeyMap) ->
|
||||||
maps:fold(
|
maps:fold(
|
||||||
fun(K, V, Acc) when is_binary(K) ->
|
fun
|
||||||
Acc#{binary_to_atom(K, utf8) => unsafe_atom_key_map(V)};
|
(K, V, Acc) when is_binary(K) ->
|
||||||
(K, V, Acc) when is_list(K) ->
|
Acc#{binary_to_atom(K, utf8) => unsafe_atom_key_map(V)};
|
||||||
Acc#{list_to_atom(K) => unsafe_atom_key_map(V)};
|
(K, V, Acc) when is_list(K) ->
|
||||||
(K, V, Acc) when is_atom(K) ->
|
Acc#{list_to_atom(K) => unsafe_atom_key_map(V)};
|
||||||
Acc#{K => unsafe_atom_key_map(V)}
|
(K, V, Acc) when is_atom(K) ->
|
||||||
end, #{}, BinKeyMap);
|
Acc#{K => unsafe_atom_key_map(V)}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
BinKeyMap
|
||||||
|
);
|
||||||
unsafe_atom_key_map(ListV) when is_list(ListV) ->
|
unsafe_atom_key_map(ListV) when is_list(ListV) ->
|
||||||
[unsafe_atom_key_map(V) || V <- ListV];
|
[unsafe_atom_key_map(V) || V <- ListV];
|
||||||
unsafe_atom_key_map(Val) -> Val.
|
unsafe_atom_key_map(Val) ->
|
||||||
|
Val.
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,18 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
|
||||||
%% APIs
|
%% APIs
|
||||||
-export([ parse_output/1
|
-export([parse_output/1]).
|
||||||
]).
|
|
||||||
|
|
||||||
%% callbacks of emqx_rule_output
|
%% callbacks of emqx_rule_output
|
||||||
-export([ pre_process_output_args/2
|
-export([pre_process_output_args/2]).
|
||||||
]).
|
|
||||||
|
|
||||||
%% output functions
|
%% output functions
|
||||||
-export([ console/3
|
-export([
|
||||||
, republish/3
|
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().
|
-callback pre_process_output_args(FuncName :: atom(), output_fun_args()) -> output_fun_args().
|
||||||
|
|
||||||
|
|
@ -44,20 +42,32 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
parse_output(#{function := OutputFunc} = Output) ->
|
parse_output(#{function := OutputFunc} = Output) ->
|
||||||
{Mod, Func} = parse_output_func(OutputFunc),
|
{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
|
%% callbacks of emqx_rule_output
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
pre_process_output_args(republish, #{topic := Topic, qos := QoS, retain := Retain,
|
pre_process_output_args(
|
||||||
payload := Payload} = Args) ->
|
republish,
|
||||||
Args#{preprocessed_tmpl => #{
|
#{
|
||||||
|
topic := Topic,
|
||||||
|
qos := QoS,
|
||||||
|
retain := Retain,
|
||||||
|
payload := Payload
|
||||||
|
} = Args
|
||||||
|
) ->
|
||||||
|
Args#{
|
||||||
|
preprocessed_tmpl => #{
|
||||||
topic => emqx_plugin_libs_rule:preproc_tmpl(Topic),
|
topic => emqx_plugin_libs_rule:preproc_tmpl(Topic),
|
||||||
qos => preproc_vars(QoS),
|
qos => preproc_vars(QoS),
|
||||||
retain => preproc_vars(Retain),
|
retain => preproc_vars(Retain),
|
||||||
payload => emqx_plugin_libs_rule:preproc_tmpl(Payload)
|
payload => emqx_plugin_libs_rule:preproc_tmpl(Payload)
|
||||||
}};
|
}
|
||||||
|
};
|
||||||
pre_process_output_args(_, Args) ->
|
pre_process_output_args(_, Args) ->
|
||||||
Args.
|
Args.
|
||||||
|
|
||||||
|
|
@ -66,35 +76,55 @@ pre_process_output_args(_, Args) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-spec console(map(), map(), map()) -> any().
|
-spec console(map(), map(), map()) -> any().
|
||||||
console(Selected, #{metadata := #{rule_id := RuleId}} = Envs, _Args) ->
|
console(Selected, #{metadata := #{rule_id := RuleId}} = Envs, _Args) ->
|
||||||
?ULOG("[rule output] ~ts~n"
|
?ULOG(
|
||||||
"\tOutput Data: ~p~n"
|
"[rule output] ~ts~n"
|
||||||
"\tEnvs: ~p~n", [RuleId, Selected, Envs]).
|
"\tOutput Data: ~p~n"
|
||||||
|
"\tEnvs: ~p~n",
|
||||||
|
[RuleId, Selected, Envs]
|
||||||
|
).
|
||||||
|
|
||||||
republish(_Selected, #{topic := Topic, headers := #{republish_by := RuleId},
|
republish(
|
||||||
metadata := #{rule_id := RuleId}}, _Args) ->
|
_Selected,
|
||||||
|
#{
|
||||||
|
topic := Topic,
|
||||||
|
headers := #{republish_by := RuleId},
|
||||||
|
metadata := #{rule_id := RuleId}
|
||||||
|
},
|
||||||
|
_Args
|
||||||
|
) ->
|
||||||
?SLOG(error, #{msg => "recursive_republish_detected", topic => Topic});
|
?SLOG(error, #{msg => "recursive_republish_detected", topic => Topic});
|
||||||
|
|
||||||
%% republish a PUBLISH message
|
%% republish a PUBLISH message
|
||||||
republish(Selected, #{flags := Flags, metadata := #{rule_id := RuleId}},
|
republish(
|
||||||
#{preprocessed_tmpl := #{
|
Selected,
|
||||||
|
#{flags := Flags, metadata := #{rule_id := RuleId}},
|
||||||
|
#{
|
||||||
|
preprocessed_tmpl := #{
|
||||||
qos := QoSTks,
|
qos := QoSTks,
|
||||||
retain := RetainTks,
|
retain := RetainTks,
|
||||||
topic := TopicTks,
|
topic := TopicTks,
|
||||||
payload := PayloadTks}}) ->
|
payload := PayloadTks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected),
|
Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected),
|
||||||
Payload = format_msg(PayloadTks, Selected),
|
Payload = format_msg(PayloadTks, Selected),
|
||||||
QoS = replace_simple_var(QoSTks, Selected, 0),
|
QoS = replace_simple_var(QoSTks, Selected, 0),
|
||||||
Retain = replace_simple_var(RetainTks, Selected, false),
|
Retain = replace_simple_var(RetainTks, Selected, false),
|
||||||
?TRACE("RULE", "republish_message", #{topic => Topic, payload => Payload}),
|
?TRACE("RULE", "republish_message", #{topic => Topic, payload => Payload}),
|
||||||
safe_publish(RuleId, Topic, QoS, Flags#{retain => Retain}, Payload);
|
safe_publish(RuleId, Topic, QoS, Flags#{retain => Retain}, Payload);
|
||||||
|
|
||||||
%% in case this is a "$events/" event
|
%% in case this is a "$events/" event
|
||||||
republish(Selected, #{metadata := #{rule_id := RuleId}},
|
republish(
|
||||||
#{preprocessed_tmpl := #{
|
Selected,
|
||||||
qos := QoSTks,
|
#{metadata := #{rule_id := RuleId}},
|
||||||
retain := RetainTks,
|
#{
|
||||||
topic := TopicTks,
|
preprocessed_tmpl := #{
|
||||||
payload := PayloadTks}}) ->
|
qos := QoSTks,
|
||||||
|
retain := RetainTks,
|
||||||
|
topic := TopicTks,
|
||||||
|
payload := PayloadTks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected),
|
Topic = emqx_plugin_libs_rule:proc_tmpl(TopicTks, Selected),
|
||||||
Payload = format_msg(PayloadTks, Selected),
|
Payload = format_msg(PayloadTks, Selected),
|
||||||
QoS = replace_simple_var(QoSTks, Selected, 0),
|
QoS = replace_simple_var(QoSTks, Selected, 0),
|
||||||
|
|
@ -114,8 +144,10 @@ get_output_mod_func(OutputFunc) when is_atom(OutputFunc) ->
|
||||||
{emqx_rule_outputs, OutputFunc};
|
{emqx_rule_outputs, OutputFunc};
|
||||||
get_output_mod_func(OutputFunc) when is_binary(OutputFunc) ->
|
get_output_mod_func(OutputFunc) when is_binary(OutputFunc) ->
|
||||||
ToAtom = fun(Bin) ->
|
ToAtom = fun(Bin) ->
|
||||||
try binary_to_existing_atom(Bin) of Atom -> Atom
|
try binary_to_existing_atom(Bin) of
|
||||||
catch error:badarg -> error({unknown_output_function, OutputFunc})
|
Atom -> Atom
|
||||||
|
catch
|
||||||
|
error:badarg -> error({unknown_output_function, OutputFunc})
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
case string:split(OutputFunc, ":", all) of
|
case string:split(OutputFunc, ":", all) of
|
||||||
|
|
@ -158,7 +190,8 @@ preproc_vars(Data) ->
|
||||||
replace_simple_var(Tokens, Data, Default) when is_list(Tokens) ->
|
replace_simple_var(Tokens, Data, Default) when is_list(Tokens) ->
|
||||||
[Var] = emqx_plugin_libs_rule:proc_tmpl(Tokens, Data, #{return => rawlist}),
|
[Var] = emqx_plugin_libs_rule:proc_tmpl(Tokens, Data, #{return => rawlist}),
|
||||||
case Var of
|
case Var of
|
||||||
undefined -> Default; %% cannot find the variable from Data
|
%% cannot find the variable from Data
|
||||||
|
undefined -> Default;
|
||||||
_ -> Var
|
_ -> Var
|
||||||
end;
|
end;
|
||||||
replace_simple_var(Val, _Data, _Default) ->
|
replace_simple_var(Val, _Data, _Default) ->
|
||||||
|
|
|
||||||
|
|
@ -20,32 +20,37 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ apply_rule/2
|
-export([
|
||||||
, apply_rules/2
|
apply_rule/2,
|
||||||
, clear_rule_payload/0
|
apply_rules/2,
|
||||||
]).
|
clear_rule_payload/0
|
||||||
|
]).
|
||||||
|
|
||||||
-import(emqx_rule_maps,
|
-import(
|
||||||
[ nested_get/2
|
emqx_rule_maps,
|
||||||
, range_gen/2
|
[
|
||||||
, range_get/3
|
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 input() :: map().
|
||||||
-type alias() :: atom().
|
-type alias() :: atom().
|
||||||
-type collection() :: {alias(), [term()]}.
|
-type collection() :: {alias(), [term()]}.
|
||||||
|
|
||||||
-define(ephemeral_alias(TYPE, NAME),
|
-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).
|
-define(ActionMaxRetry, 3).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Apply rules
|
%% Apply rules
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
-spec(apply_rules(list(rule()), input()) -> ok).
|
-spec apply_rules(list(rule()), input()) -> ok.
|
||||||
apply_rules([], _Input) ->
|
apply_rules([], _Input) ->
|
||||||
ok;
|
ok;
|
||||||
apply_rules([#{enable := false} | More], Input) ->
|
apply_rules([#{enable := false} | More], Input) ->
|
||||||
|
|
@ -61,54 +66,77 @@ apply_rule_discard_result(Rule, Input) ->
|
||||||
apply_rule(Rule = #{id := RuleID}, Input) ->
|
apply_rule(Rule = #{id := RuleID}, Input) ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.matched'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.matched'),
|
||||||
clear_rule_payload(),
|
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
|
catch
|
||||||
%% ignore the errors if select or match failed
|
%% ignore the errors if select or match failed
|
||||||
_:Reason = {select_and_transform_error, Error} ->
|
_:Reason = {select_and_transform_error, Error} ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
||||||
?SLOG(warning, #{msg => "SELECT_clause_exception",
|
?SLOG(warning, #{
|
||||||
rule_id => RuleID, reason => Error}),
|
msg => "SELECT_clause_exception",
|
||||||
|
rule_id => RuleID,
|
||||||
|
reason => Error
|
||||||
|
}),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
_:Reason = {match_conditions_error, Error} ->
|
_:Reason = {match_conditions_error, Error} ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
||||||
?SLOG(warning, #{msg => "WHERE_clause_exception",
|
?SLOG(warning, #{
|
||||||
rule_id => RuleID, reason => Error}),
|
msg => "WHERE_clause_exception",
|
||||||
|
rule_id => RuleID,
|
||||||
|
reason => Error
|
||||||
|
}),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
_:Reason = {select_and_collect_error, Error} ->
|
_:Reason = {select_and_collect_error, Error} ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
||||||
?SLOG(warning, #{msg => "FOREACH_clause_exception",
|
?SLOG(warning, #{
|
||||||
rule_id => RuleID, reason => Error}),
|
msg => "FOREACH_clause_exception",
|
||||||
|
rule_id => RuleID,
|
||||||
|
reason => Error
|
||||||
|
}),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
_:Reason = {match_incase_error, Error} ->
|
_:Reason = {match_incase_error, Error} ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
||||||
?SLOG(warning, #{msg => "INCASE_clause_exception",
|
?SLOG(warning, #{
|
||||||
rule_id => RuleID, reason => Error}),
|
msg => "INCASE_clause_exception",
|
||||||
|
rule_id => RuleID,
|
||||||
|
reason => Error
|
||||||
|
}),
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
Class:Error:StkTrace ->
|
Class:Error:StkTrace ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleID, 'sql.failed.exception'),
|
||||||
?SLOG(error, #{msg => "apply_rule_failed",
|
?SLOG(error, #{
|
||||||
rule_id => RuleID,
|
msg => "apply_rule_failed",
|
||||||
exception => Class,
|
rule_id => RuleID,
|
||||||
reason => Error,
|
exception => Class,
|
||||||
stacktrace => StkTrace
|
reason => Error,
|
||||||
}),
|
stacktrace => StkTrace
|
||||||
|
}),
|
||||||
{error, {Error, StkTrace}}
|
{error, {Error, StkTrace}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_apply_rule(#{
|
do_apply_rule(
|
||||||
id := RuleId,
|
#{
|
||||||
is_foreach := true,
|
id := RuleId,
|
||||||
fields := Fields,
|
is_foreach := true,
|
||||||
doeach := DoEach,
|
fields := Fields,
|
||||||
incase := InCase,
|
doeach := DoEach,
|
||||||
conditions := Conditions,
|
incase := InCase,
|
||||||
outputs := Outputs
|
conditions := Conditions,
|
||||||
}, Input) ->
|
outputs := Outputs
|
||||||
{Selected, Collection} = ?RAISE(select_and_collect(Fields, Input),
|
},
|
||||||
{select_and_collect_error, {_EXCLASS_,_EXCPTION_,_ST_}}),
|
Input
|
||||||
|
) ->
|
||||||
|
{Selected, Collection} = ?RAISE(
|
||||||
|
select_and_collect(Fields, Input),
|
||||||
|
{select_and_collect_error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||||
|
),
|
||||||
ColumnsAndSelected = maps:merge(Input, Selected),
|
ColumnsAndSelected = maps:merge(Input, Selected),
|
||||||
case ?RAISE(match_conditions(Conditions, ColumnsAndSelected),
|
case
|
||||||
{match_conditions_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of
|
?RAISE(
|
||||||
|
match_conditions(Conditions, ColumnsAndSelected),
|
||||||
|
{match_conditions_error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||||
|
)
|
||||||
|
of
|
||||||
true ->
|
true ->
|
||||||
Collection2 = filter_collection(Input, InCase, DoEach, Collection),
|
Collection2 = filter_collection(Input, InCase, DoEach, Collection),
|
||||||
case Collection2 of
|
case Collection2 of
|
||||||
|
|
@ -122,17 +150,26 @@ do_apply_rule(#{
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.failed.no_result'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.failed.no_result'),
|
||||||
{error, nomatch}
|
{error, nomatch}
|
||||||
end;
|
end;
|
||||||
|
do_apply_rule(
|
||||||
do_apply_rule(#{id := RuleId,
|
#{
|
||||||
is_foreach := false,
|
id := RuleId,
|
||||||
fields := Fields,
|
is_foreach := false,
|
||||||
conditions := Conditions,
|
fields := Fields,
|
||||||
outputs := Outputs
|
conditions := Conditions,
|
||||||
}, Input) ->
|
outputs := Outputs
|
||||||
Selected = ?RAISE(select_and_transform(Fields, Input),
|
},
|
||||||
{select_and_transform_error, {_EXCLASS_,_EXCPTION_,_ST_}}),
|
Input
|
||||||
case ?RAISE(match_conditions(Conditions, maps:merge(Input, Selected)),
|
) ->
|
||||||
{match_conditions_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of
|
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 ->
|
true ->
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.passed'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'sql.passed'),
|
||||||
{ok, handle_output_list(RuleId, Outputs, Selected, Input)};
|
{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(More, Input, maps:merge(Output, Input));
|
||||||
select_and_transform([{as, Field, Alias} | More], Input, Output) ->
|
select_and_transform([{as, Field, Alias} | More], Input, Output) ->
|
||||||
Val = eval(Field, Input),
|
Val = eval(Field, Input),
|
||||||
select_and_transform(More,
|
select_and_transform(
|
||||||
|
More,
|
||||||
nested_put(Alias, Val, Input),
|
nested_put(Alias, Val, Input),
|
||||||
nested_put(Alias, Val, Output));
|
nested_put(Alias, Val, Output)
|
||||||
|
);
|
||||||
select_and_transform([Field | More], Input, Output) ->
|
select_and_transform([Field | More], Input, Output) ->
|
||||||
Val = eval(Field, Input),
|
Val = eval(Field, Input),
|
||||||
Key = alias(Field),
|
Key = alias(Field),
|
||||||
select_and_transform(More,
|
select_and_transform(
|
||||||
|
More,
|
||||||
nested_put(Key, Val, Input),
|
nested_put(Key, Val, Input),
|
||||||
nested_put(Key, Val, Output)).
|
nested_put(Key, Val, Output)
|
||||||
|
).
|
||||||
|
|
||||||
%% FOREACH Clause
|
%% FOREACH Clause
|
||||||
-spec select_and_collect(list(), input()) -> {input(), collection()}.
|
-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)}};
|
{nested_put(Alias, Val, Output), {A, ensure_list(Val)}};
|
||||||
select_and_collect([{as, Field, Alias} | More], Input, {Output, LastKV}) ->
|
select_and_collect([{as, Field, Alias} | More], Input, {Output, LastKV}) ->
|
||||||
Val = eval(Field, Input),
|
Val = eval(Field, Input),
|
||||||
select_and_collect(More,
|
select_and_collect(
|
||||||
|
More,
|
||||||
nested_put(Alias, Val, Input),
|
nested_put(Alias, Val, Input),
|
||||||
{nested_put(Alias, Val, Output), LastKV});
|
{nested_put(Alias, Val, Output), LastKV}
|
||||||
|
);
|
||||||
select_and_collect([Field], Input, {Output, _}) ->
|
select_and_collect([Field], Input, {Output, _}) ->
|
||||||
Val = eval(Field, Input),
|
Val = eval(Field, Input),
|
||||||
Key = alias(Field),
|
Key = alias(Field),
|
||||||
|
|
@ -184,24 +227,36 @@ select_and_collect([Field], Input, {Output, _}) ->
|
||||||
select_and_collect([Field | More], Input, {Output, LastKV}) ->
|
select_and_collect([Field | More], Input, {Output, LastKV}) ->
|
||||||
Val = eval(Field, Input),
|
Val = eval(Field, Input),
|
||||||
Key = alias(Field),
|
Key = alias(Field),
|
||||||
select_and_collect(More,
|
select_and_collect(
|
||||||
|
More,
|
||||||
nested_put(Key, Val, Input),
|
nested_put(Key, Val, Input),
|
||||||
{nested_put(Key, Val, Output), LastKV}).
|
{nested_put(Key, Val, Output), LastKV}
|
||||||
|
).
|
||||||
|
|
||||||
%% Filter each item got from FOREACH
|
%% Filter each item got from FOREACH
|
||||||
filter_collection(Input, InCase, DoEach, {CollKey, CollVal}) ->
|
filter_collection(Input, InCase, DoEach, {CollKey, CollVal}) ->
|
||||||
lists:filtermap(
|
lists:filtermap(
|
||||||
fun(Item) ->
|
fun(Item) ->
|
||||||
InputAndItem = maps:merge(Input, #{CollKey => Item}),
|
InputAndItem = maps:merge(Input, #{CollKey => Item}),
|
||||||
case ?RAISE(match_conditions(InCase, InputAndItem),
|
case
|
||||||
{match_incase_error, {_EXCLASS_,_EXCPTION_,_ST_}}) of
|
?RAISE(
|
||||||
|
match_conditions(InCase, InputAndItem),
|
||||||
|
{match_incase_error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||||
|
)
|
||||||
|
of
|
||||||
true when DoEach == [] -> {true, InputAndItem};
|
true when DoEach == [] -> {true, InputAndItem};
|
||||||
true ->
|
true ->
|
||||||
{true, ?RAISE(select_and_transform(DoEach, InputAndItem),
|
{true,
|
||||||
{doeach_error, {_EXCLASS_,_EXCPTION_,_ST_}})};
|
?RAISE(
|
||||||
false -> false
|
select_and_transform(DoEach, InputAndItem),
|
||||||
|
{doeach_error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||||
|
)};
|
||||||
|
false ->
|
||||||
|
false
|
||||||
end
|
end
|
||||||
end, CollVal).
|
end,
|
||||||
|
CollVal
|
||||||
|
).
|
||||||
|
|
||||||
%% Conditional Clauses such as WHERE, WHEN.
|
%% Conditional Clauses such as WHERE, WHEN.
|
||||||
match_conditions({'and', L, R}, Data) ->
|
match_conditions({'and', L, R}, Data) ->
|
||||||
|
|
@ -212,7 +267,8 @@ match_conditions({'not', Var}, Data) ->
|
||||||
case eval(Var, Data) of
|
case eval(Var, Data) of
|
||||||
Bool when is_boolean(Bool) ->
|
Bool when is_boolean(Bool) ->
|
||||||
not Bool;
|
not Bool;
|
||||||
_other -> false
|
_other ->
|
||||||
|
false
|
||||||
end;
|
end;
|
||||||
match_conditions({in, Var, {list, Vals}}, Data) ->
|
match_conditions({in, Var, {list, Vals}}, Data) ->
|
||||||
lists:member(eval(Var, Data), [eval(V, Data) || V <- Vals]);
|
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).
|
do_compare('=~', T, F) -> emqx_topic:match(T, F).
|
||||||
|
|
||||||
number(Bin) ->
|
number(Bin) ->
|
||||||
try binary_to_integer(Bin)
|
try
|
||||||
catch error:badarg -> binary_to_float(Bin)
|
binary_to_integer(Bin)
|
||||||
|
catch
|
||||||
|
error:badarg -> binary_to_float(Bin)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle_output_list(RuleId, Outputs, Selected, Envs) ->
|
handle_output_list(RuleId, Outputs, Selected, Envs) ->
|
||||||
|
|
@ -266,13 +324,20 @@ handle_output(RuleId, OutId, Selected, Envs) ->
|
||||||
catch
|
catch
|
||||||
throw:out_of_service ->
|
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'),
|
||||||
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});
|
?SLOG(warning, #{msg => "out_of_service", output => OutId});
|
||||||
Err:Reason:ST ->
|
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'),
|
||||||
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed.unknown'),
|
ok = emqx_plugin_libs_metrics:inc(rule_metrics, RuleId, 'outputs.failed.unknown'),
|
||||||
?SLOG(error, #{msg => "output_failed", output => OutId, exception => Err,
|
?SLOG(error, #{
|
||||||
reason => Reason, stacktrace => ST})
|
msg => "output_failed",
|
||||||
|
output => OutId,
|
||||||
|
exception => Err,
|
||||||
|
reason => Reason,
|
||||||
|
stacktrace => ST
|
||||||
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_handle_output(BridgeId, Selected, _Envs) when is_binary(BridgeId) ->
|
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
|
case emqx_bridge:send_message(BridgeId, Selected) of
|
||||||
{error, {Err, _}} when Err == bridge_not_found; Err == bridge_stopped ->
|
{error, {Err, _}} when Err == bridge_not_found; Err == bridge_stopped ->
|
||||||
throw(out_of_service);
|
throw(out_of_service);
|
||||||
Result -> Result
|
Result ->
|
||||||
|
Result
|
||||||
end;
|
end;
|
||||||
do_handle_output(#{mod := Mod, func := Func, args := Args}, Selected, Envs) ->
|
do_handle_output(#{mod := Mod, func := Func, args := Args}, Selected, Envs) ->
|
||||||
%% the function can also throw 'out_of_service'
|
%% 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);
|
do_apply_func(Name, Args, Input);
|
||||||
apply_func(Name, Args, Input) when is_binary(Name) ->
|
apply_func(Name, Args, Input) when is_binary(Name) ->
|
||||||
FunName =
|
FunName =
|
||||||
try binary_to_existing_atom(Name, utf8)
|
try
|
||||||
catch error:badarg -> error({sql_function_not_supported, Name})
|
binary_to_existing_atom(Name, utf8)
|
||||||
|
catch
|
||||||
|
error:badarg -> error({sql_function_not_supported, Name})
|
||||||
end,
|
end,
|
||||||
do_apply_func(FunName, Args, Input).
|
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
|
case erlang:apply(emqx_rule_funcs, Name, Args) of
|
||||||
Func when is_function(Func) ->
|
Func when is_function(Func) ->
|
||||||
erlang:apply(Func, [Input]);
|
erlang:apply(Func, [Input]);
|
||||||
Result -> Result
|
Result ->
|
||||||
|
Result
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_metadata(Input, Metadata) when is_map(Input), is_map(Metadata) ->
|
add_metadata(Input, Metadata) when is_map(Input), is_map(Metadata) ->
|
||||||
|
|
@ -417,8 +486,10 @@ cache_payload(DecodedP) ->
|
||||||
DecodedP.
|
DecodedP.
|
||||||
|
|
||||||
safe_decode_and_cache(MaybeJson) ->
|
safe_decode_and_cache(MaybeJson) ->
|
||||||
try cache_payload(emqx_json:decode(MaybeJson, [return_maps]))
|
try
|
||||||
catch _:_ -> error({decode_json_failed, MaybeJson})
|
cache_payload(emqx_json:decode(MaybeJson, [return_maps]))
|
||||||
|
catch
|
||||||
|
_:_ -> error({decode_json_failed, MaybeJson})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
ensure_list(List) when is_list(List) -> List;
|
ensure_list(List) when is_list(List) -> List;
|
||||||
|
|
|
||||||
|
|
@ -20,84 +20,89 @@
|
||||||
|
|
||||||
-export([parse/1]).
|
-export([parse/1]).
|
||||||
|
|
||||||
-export([ select_fields/1
|
-export([
|
||||||
, select_is_foreach/1
|
select_fields/1,
|
||||||
, select_doeach/1
|
select_is_foreach/1,
|
||||||
, select_incase/1
|
select_doeach/1,
|
||||||
, select_from/1
|
select_incase/1,
|
||||||
, select_where/1
|
select_from/1,
|
||||||
]).
|
select_where/1
|
||||||
|
]).
|
||||||
|
|
||||||
-import(proplists, [ get_value/2
|
-import(proplists, [
|
||||||
, get_value/3
|
get_value/2,
|
||||||
]).
|
get_value/3
|
||||||
|
]).
|
||||||
|
|
||||||
-record(select, {fields, from, where, is_foreach, doeach, incase}).
|
-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 variable() :: binary() | list(binary()).
|
||||||
|
|
||||||
-type alias() :: binary() | list(binary()).
|
-type alias() :: binary() | list(binary()).
|
||||||
|
|
||||||
-type field() :: const() | variable()
|
-type field() ::
|
||||||
| {as, field(), alias()}
|
const()
|
||||||
| {'fun', atom(), list(field())}.
|
| variable()
|
||||||
|
| {as, field(), alias()}
|
||||||
|
| {'fun', atom(), list(field())}.
|
||||||
|
|
||||||
-export_type([select/0]).
|
-export_type([select/0]).
|
||||||
|
|
||||||
%% Parse one select statement.
|
%% Parse one select statement.
|
||||||
-spec(parse(string() | binary()) -> {ok, select()} | {error, term()}).
|
-spec parse(string() | binary()) -> {ok, select()} | {error, term()}.
|
||||||
parse(Sql) ->
|
parse(Sql) ->
|
||||||
try case rulesql:parsetree(Sql) of
|
try
|
||||||
|
case rulesql:parsetree(Sql) of
|
||||||
{ok, {select, Clauses}} ->
|
{ok, {select, Clauses}} ->
|
||||||
{ok, #select{
|
{ok, #select{
|
||||||
is_foreach = false,
|
is_foreach = false,
|
||||||
fields = get_value(fields, Clauses),
|
fields = get_value(fields, Clauses),
|
||||||
doeach = [],
|
doeach = [],
|
||||||
incase = {},
|
incase = {},
|
||||||
from = get_value(from, Clauses),
|
from = get_value(from, Clauses),
|
||||||
where = get_value(where, Clauses)
|
where = get_value(where, Clauses)
|
||||||
}};
|
}};
|
||||||
{ok, {foreach, Clauses}} ->
|
{ok, {foreach, Clauses}} ->
|
||||||
{ok, #select{
|
{ok, #select{
|
||||||
is_foreach = true,
|
is_foreach = true,
|
||||||
fields = get_value(fields, Clauses),
|
fields = get_value(fields, Clauses),
|
||||||
doeach = get_value(do, Clauses, []),
|
doeach = get_value(do, Clauses, []),
|
||||||
incase = get_value(incase, Clauses, {}),
|
incase = get_value(incase, Clauses, {}),
|
||||||
from = get_value(from, Clauses),
|
from = get_value(from, Clauses),
|
||||||
where = get_value(where, Clauses)
|
where = get_value(where, Clauses)
|
||||||
}};
|
}};
|
||||||
Error -> {error, Error}
|
Error ->
|
||||||
|
{error, Error}
|
||||||
end
|
end
|
||||||
catch
|
catch
|
||||||
_Error:Reason:StackTrace ->
|
_Error:Reason:StackTrace ->
|
||||||
{error, {Reason, StackTrace}}
|
{error, {Reason, StackTrace}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(select_fields(select()) -> list(field())).
|
-spec select_fields(select()) -> list(field()).
|
||||||
select_fields(#select{fields = Fields}) ->
|
select_fields(#select{fields = Fields}) ->
|
||||||
Fields.
|
Fields.
|
||||||
|
|
||||||
-spec(select_is_foreach(select()) -> boolean()).
|
-spec select_is_foreach(select()) -> boolean().
|
||||||
select_is_foreach(#select{is_foreach = IsForeach}) ->
|
select_is_foreach(#select{is_foreach = IsForeach}) ->
|
||||||
IsForeach.
|
IsForeach.
|
||||||
|
|
||||||
-spec(select_doeach(select()) -> list(field())).
|
-spec select_doeach(select()) -> list(field()).
|
||||||
select_doeach(#select{doeach = DoEach}) ->
|
select_doeach(#select{doeach = DoEach}) ->
|
||||||
DoEach.
|
DoEach.
|
||||||
|
|
||||||
-spec(select_incase(select()) -> list(field())).
|
-spec select_incase(select()) -> list(field()).
|
||||||
select_incase(#select{incase = InCase}) ->
|
select_incase(#select{incase = InCase}) ->
|
||||||
InCase.
|
InCase.
|
||||||
|
|
||||||
-spec(select_from(select()) -> list(binary())).
|
-spec select_from(select()) -> list(binary()).
|
||||||
select_from(#select{from = From}) ->
|
select_from(#select{from = From}) ->
|
||||||
From.
|
From.
|
||||||
|
|
||||||
-spec(select_where(select()) -> tuple()).
|
-spec select_where(select()) -> tuple().
|
||||||
select_where(#select{where = Where}) ->
|
select_where(#select{where = Where}) ->
|
||||||
Where.
|
Where.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,11 @@
|
||||||
-include("rule_engine.hrl").
|
-include("rule_engine.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ test/1
|
-export([
|
||||||
, echo_action/2
|
test/1,
|
||||||
, get_selected_data/3
|
echo_action/2,
|
||||||
]).
|
get_selected_data/3
|
||||||
|
]).
|
||||||
|
|
||||||
-spec test(#{sql := binary(), context := map()}) -> {ok, map() | list()} | {error, term()}.
|
-spec test(#{sql := binary(), context := map()}) -> {ok, map() | list()} | {error, term()}.
|
||||||
test(#{sql := Sql, context := Context}) ->
|
test(#{sql := Sql, context := Context}) ->
|
||||||
|
|
@ -60,9 +61,7 @@ test_rule(Sql, Select, Context, EventTopics) ->
|
||||||
created_at => erlang:system_time(millisecond)
|
created_at => erlang:system_time(millisecond)
|
||||||
},
|
},
|
||||||
FullContext = fill_default_values(hd(EventTopics), emqx_rule_maps:atom_key_map(Context)),
|
FullContext = fill_default_values(hd(EventTopics), emqx_rule_maps:atom_key_map(Context)),
|
||||||
try
|
try emqx_rule_runtime:apply_rule(Rule, FullContext) of
|
||||||
emqx_rule_runtime:apply_rule(Rule, FullContext)
|
|
||||||
of
|
|
||||||
{ok, Data} -> {ok, flatten(Data)};
|
{ok, Data} -> {ok, flatten(Data)};
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
after
|
after
|
||||||
|
|
@ -76,8 +75,10 @@ is_publish_topic(<<"$events/", _/binary>>) -> false;
|
||||||
is_publish_topic(<<"$bridges/", _/binary>>) -> false;
|
is_publish_topic(<<"$bridges/", _/binary>>) -> false;
|
||||||
is_publish_topic(_Topic) -> true.
|
is_publish_topic(_Topic) -> true.
|
||||||
|
|
||||||
flatten([]) -> [];
|
flatten([]) ->
|
||||||
flatten([D1]) -> D1;
|
[];
|
||||||
|
flatten([D1]) ->
|
||||||
|
D1;
|
||||||
flatten([D1 | L]) when is_list(D1) ->
|
flatten([D1 | L]) when is_list(D1) ->
|
||||||
D1 ++ flatten(L).
|
D1 ++ flatten(L).
|
||||||
|
|
||||||
|
|
@ -92,4 +93,6 @@ envs_examp(EventTopic) ->
|
||||||
EventName = emqx_rule_events:event_name(EventTopic),
|
EventName = emqx_rule_events:event_name(EventTopic),
|
||||||
emqx_rule_maps:atom_key_map(
|
emqx_rule_maps:atom_key_map(
|
||||||
maps:from_list(
|
maps:from_list(
|
||||||
emqx_rule_events:columns_with_exam(EventName))).
|
emqx_rule_events:columns_with_exam(EventName)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-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/include/bpapi.hrl").
|
||||||
-include_lib("emqx_rule_engine/include/rule_engine.hrl").
|
-include_lib("emqx_rule_engine/include/rule_engine.hrl").
|
||||||
|
|
@ -30,6 +31,6 @@ introduced_in() ->
|
||||||
"5.0.0".
|
"5.0.0".
|
||||||
|
|
||||||
-spec reset_metrics(rule_id()) ->
|
-spec reset_metrics(rule_id()) ->
|
||||||
emqx_cluster_rpc:multicall_return(ok).
|
emqx_cluster_rpc:multicall_return(ok).
|
||||||
reset_metrics(RuleId) ->
|
reset_metrics(RuleId) ->
|
||||||
emqx_cluster_rpc:multicall(emqx_rule_engine, reset_metrics_for_rule, [RuleId]).
|
emqx_cluster_rpc:multicall(emqx_rule_engine, reset_metrics_for_rule, [RuleId]).
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -38,15 +38,19 @@ t_crud_rule_api(_Config) ->
|
||||||
},
|
},
|
||||||
{201, Rule} = emqx_rule_engine_api:'/rules'(post, #{body => Params0}),
|
{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"
|
%% if we post again with the same params, it return with 400 "rule id already exists"
|
||||||
?assertMatch({400, #{code := _, message := _Message}},
|
?assertMatch(
|
||||||
emqx_rule_engine_api:'/rules'(post, #{body => Params0})),
|
{400, #{code := _, message := _Message}},
|
||||||
|
emqx_rule_engine_api:'/rules'(post, #{body => Params0})
|
||||||
|
),
|
||||||
|
|
||||||
?assertEqual(RuleID, maps:get(id, Rule)),
|
?assertEqual(RuleID, maps:get(id, Rule)),
|
||||||
{200, Rules} = emqx_rule_engine_api:'/rules'(get, #{}),
|
{200, Rules} = emqx_rule_engine_api:'/rules'(get, #{}),
|
||||||
ct:pal("RList : ~p", [Rules]),
|
ct:pal("RList : ~p", [Rules]),
|
||||||
?assert(length(Rules) > 0),
|
?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),
|
?assertEqual(<<"Reset Success">>, Rule0),
|
||||||
|
|
||||||
{200, Rule1} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}),
|
{200, Rule1} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}),
|
||||||
|
|
@ -54,19 +58,26 @@ t_crud_rule_api(_Config) ->
|
||||||
?assertEqual(Rule, Rule1),
|
?assertEqual(Rule, Rule1),
|
||||||
|
|
||||||
{200, Rule2} = emqx_rule_engine_api:'/rules/:id'(put, #{
|
{200, Rule2} = emqx_rule_engine_api:'/rules/:id'(put, #{
|
||||||
bindings => #{id => RuleID},
|
bindings => #{id => RuleID},
|
||||||
body => Params0#{<<"sql">> => <<"select * from \"t/b\"">>}
|
body => Params0#{<<"sql">> => <<"select * from \"t/b\"">>}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
{200, Rule3} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}),
|
{200, Rule3} = emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}}),
|
||||||
%ct:pal("RShow : ~p", [Rule3]),
|
%ct:pal("RShow : ~p", [Rule3]),
|
||||||
?assertEqual(Rule3, Rule2),
|
?assertEqual(Rule3, Rule2),
|
||||||
?assertEqual(<<"select * from \"t/b\"">>, maps:get(sql, Rule3)),
|
?assertEqual(<<"select * from \"t/b\"">>, maps:get(sql, Rule3)),
|
||||||
|
|
||||||
?assertMatch({204}, emqx_rule_engine_api:'/rules/:id'(delete,
|
?assertMatch(
|
||||||
#{bindings => #{id => RuleID}})),
|
{204},
|
||||||
|
emqx_rule_engine_api:'/rules/:id'(
|
||||||
|
delete,
|
||||||
|
#{bindings => #{id => RuleID}}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
%ct:pal("Show After Deleted: ~p", [NotFound]),
|
%ct:pal("Show After Deleted: ~p", [NotFound]),
|
||||||
?assertMatch({404, #{code := _, message := _Message}},
|
?assertMatch(
|
||||||
emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}})),
|
{404, #{code := _, message := _Message}},
|
||||||
|
emqx_rule_engine_api:'/rules/:id'(get, #{bindings => #{id => RuleID}})
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,30 @@ all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
t_mod_hook_fun(_) ->
|
t_mod_hook_fun(_) ->
|
||||||
Funcs = emqx_rule_events:module_info(exports),
|
Funcs = emqx_rule_events:module_info(exports),
|
||||||
[?assert(lists:keymember(emqx_rule_events:hook_fun(Event), 1, Funcs)) ||
|
[
|
||||||
Event <- ['client.connected',
|
?assert(lists:keymember(emqx_rule_events:hook_fun(Event), 1, Funcs))
|
||||||
'client.disconnected',
|
|| Event <- [
|
||||||
'session.subscribed',
|
'client.connected',
|
||||||
'session.unsubscribed',
|
'client.disconnected',
|
||||||
'message.acked',
|
'session.subscribed',
|
||||||
'message.dropped',
|
'session.unsubscribed',
|
||||||
'message.delivered'
|
'message.acked',
|
||||||
]].
|
'message.dropped',
|
||||||
|
'message.delivered'
|
||||||
|
]
|
||||||
|
].
|
||||||
|
|
||||||
t_printable_maps(_) ->
|
t_printable_maps(_) ->
|
||||||
Headers = #{peerhost => {127,0,0,1},
|
Headers = #{
|
||||||
peername => {{127,0,0,1}, 9980},
|
peerhost => {127, 0, 0, 1},
|
||||||
sockname => {{127,0,0,1}, 1883}
|
peername => {{127, 0, 0, 1}, 9980},
|
||||||
},
|
sockname => {{127, 0, 0, 1}, 1883}
|
||||||
|
},
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
#{peerhost := <<"127.0.0.1">>,
|
#{
|
||||||
peername := <<"127.0.0.1:9980">>,
|
peerhost := <<"127.0.0.1">>,
|
||||||
sockname := <<"127.0.0.1:1883">>
|
peername := <<"127.0.0.1:9980">>,
|
||||||
}, emqx_rule_events:printable_maps(Headers)).
|
sockname := <<"127.0.0.1:1883">>
|
||||||
|
},
|
||||||
|
emqx_rule_events:printable_maps(Headers)
|
||||||
|
).
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@
|
||||||
t_msgid(_) ->
|
t_msgid(_) ->
|
||||||
Msg = message(),
|
Msg = message(),
|
||||||
?assertEqual(undefined, apply_func(msgid, [], #{})),
|
?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(_) ->
|
t_qos(_) ->
|
||||||
?assertEqual(undefined, apply_func(qos, [], #{})),
|
?assertEqual(undefined, apply_func(qos, [], #{})),
|
||||||
|
|
@ -61,12 +63,12 @@ t_clientid(_) ->
|
||||||
?assertEqual(<<"clientid">>, apply_func(clientid, [], Msg)).
|
?assertEqual(<<"clientid">>, apply_func(clientid, [], Msg)).
|
||||||
|
|
||||||
t_clientip(_) ->
|
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(undefined, apply_func(clientip, [], #{})),
|
||||||
?assertEqual(<<"127.0.0.1">>, apply_func(clientip, [], eventmsg_publish(Msg))).
|
?assertEqual(<<"127.0.0.1">>, apply_func(clientip, [], eventmsg_publish(Msg))).
|
||||||
|
|
||||||
t_peerhost(_) ->
|
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(undefined, apply_func(peerhost, [], #{})),
|
||||||
?assertEqual(<<"127.0.0.1">>, apply_func(peerhost, [], eventmsg_publish(Msg))).
|
?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(<<"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}">>, 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(<<"1">>, emqx_rule_funcs:str(1)),
|
||||||
?assertEqual(<<"2.0">>, emqx_rule_funcs:str(2.0)),
|
?assertEqual(<<"2.0">>, emqx_rule_funcs:str(2.0)),
|
||||||
?assertEqual(<<"true">>, emqx_rule_funcs:str(true)),
|
?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 你好")),
|
||||||
?assertEqual(<<"abc 你好"/utf8>>, emqx_rule_funcs:str_utf8(<<"abc 你好"/utf8>>)),
|
?assertEqual(<<"abc 你好"/utf8>>, emqx_rule_funcs:str_utf8(<<"abc 你好"/utf8>>)),
|
||||||
?assertEqual(<<"abc">>, emqx_rule_funcs:str_utf8(abc)),
|
?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(<<"1">>, emqx_rule_funcs:str_utf8(1)),
|
||||||
?assertEqual(<<"2.0">>, emqx_rule_funcs:str_utf8(2.0)),
|
?assertEqual(<<"2.0">>, emqx_rule_funcs:str_utf8(2.0)),
|
||||||
?assertEqual(<<"true">>, emqx_rule_funcs:str_utf8(true)),
|
?assertEqual(<<"true">>, emqx_rule_funcs:str_utf8(true)),
|
||||||
|
|
@ -126,7 +130,9 @@ t_float(_) ->
|
||||||
?assertError(_, emqx_rule_funcs:float("a")).
|
?assertError(_, emqx_rule_funcs:float("a")).
|
||||||
|
|
||||||
t_map(_) ->
|
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}">>)),
|
?assertEqual(#{<<"a">> => 1}, emqx_rule_funcs:map(<<"{\"a\":1}">>)),
|
||||||
?assertError(_, emqx_rule_funcs:map(<<"a">>)),
|
?assertError(_, emqx_rule_funcs:map(<<"a">>)),
|
||||||
?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">>)).
|
?assertEqual(undefined, emqx_rule_funcs:proc_dict_get(<<"abc">>)).
|
||||||
|
|
||||||
t_term_encode(_) ->
|
t_term_encode(_) ->
|
||||||
TestData = [<<"abc">>, #{a => 1}, #{<<"3">> => [1,2,4]}],
|
TestData = [<<"abc">>, #{a => 1}, #{<<"3">> => [1, 2, 4]}],
|
||||||
lists:foreach(fun(Data) ->
|
lists:foreach(
|
||||||
?assertEqual(Data,
|
fun(Data) ->
|
||||||
|
?assertEqual(
|
||||||
|
Data,
|
||||||
emqx_rule_funcs:term_decode(
|
emqx_rule_funcs:term_decode(
|
||||||
emqx_rule_funcs:term_encode(Data)))
|
emqx_rule_funcs:term_encode(Data)
|
||||||
end, TestData).
|
)
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
TestData
|
||||||
|
).
|
||||||
|
|
||||||
t_hexstr2bin(_) ->
|
t_hexstr2bin(_) ->
|
||||||
?assertEqual(<<1,2>>, emqx_rule_funcs:hexstr2bin(<<"0102">>)),
|
?assertEqual(<<1, 2>>, emqx_rule_funcs:hexstr2bin(<<"0102">>)),
|
||||||
?assertEqual(<<17,33>>, emqx_rule_funcs:hexstr2bin(<<"1121">>)).
|
?assertEqual(<<17, 33>>, emqx_rule_funcs:hexstr2bin(<<"1121">>)).
|
||||||
|
|
||||||
t_bin2hexstr(_) ->
|
t_bin2hexstr(_) ->
|
||||||
?assertEqual(<<"0102">>, emqx_rule_funcs:bin2hexstr(<<1,2>>)),
|
?assertEqual(<<"0102">>, emqx_rule_funcs:bin2hexstr(<<1, 2>>)),
|
||||||
?assertEqual(<<"1121">>, emqx_rule_funcs:bin2hexstr(<<17,33>>)).
|
?assertEqual(<<"1121">>, emqx_rule_funcs:bin2hexstr(<<17, 33>>)).
|
||||||
|
|
||||||
t_hex_convert(_) ->
|
t_hex_convert(_) ->
|
||||||
?PROPTEST(hex_convert).
|
?PROPTEST(hex_convert).
|
||||||
|
|
||||||
hex_convert() ->
|
hex_convert() ->
|
||||||
?FORALL(L, list(range(0, 255)),
|
?FORALL(
|
||||||
begin
|
L,
|
||||||
AbitraryBin = list_to_binary(L),
|
list(range(0, 255)),
|
||||||
AbitraryBin == emqx_rule_funcs:hexstr2bin(
|
begin
|
||||||
emqx_rule_funcs:bin2hexstr(AbitraryBin))
|
AbitraryBin = list_to_binary(L),
|
||||||
end).
|
AbitraryBin ==
|
||||||
|
emqx_rule_funcs:hexstr2bin(
|
||||||
|
emqx_rule_funcs:bin2hexstr(AbitraryBin)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
t_is_null(_) ->
|
t_is_null(_) ->
|
||||||
?assertEqual(true, emqx_rule_funcs:is_null(undefined)),
|
?assertEqual(true, emqx_rule_funcs:is_null(undefined)),
|
||||||
|
|
@ -184,50 +201,80 @@ t_is_null(_) ->
|
||||||
?assertEqual(false, emqx_rule_funcs:is_null(<<"a">>)).
|
?assertEqual(false, emqx_rule_funcs:is_null(<<"a">>)).
|
||||||
|
|
||||||
t_is_not_null(_) ->
|
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(_) ->
|
t_is_str(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_str(T))
|
[
|
||||||
|| T <- [<<"a">>, <<>>, <<"abc">>]],
|
?assertEqual(true, emqx_rule_funcs:is_str(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_str(T))
|
|| T <- [<<"a">>, <<>>, <<"abc">>]
|
||||||
|| T <- ["a", a, 1]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_str(T))
|
||||||
|
|| T <- ["a", a, 1]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_bool(_) ->
|
t_is_bool(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_bool(T))
|
[
|
||||||
|| T <- [true, false]],
|
?assertEqual(true, emqx_rule_funcs:is_bool(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_bool(T))
|
|| T <- [true, false]
|
||||||
|| T <- ["a", <<>>, a, 2]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_bool(T))
|
||||||
|
|| T <- ["a", <<>>, a, 2]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_int(_) ->
|
t_is_int(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_int(T))
|
[
|
||||||
|| T <- [1, 2, -1]],
|
?assertEqual(true, emqx_rule_funcs:is_int(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_int(T))
|
|| T <- [1, 2, -1]
|
||||||
|| T <- [1.1, "a", a]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_int(T))
|
||||||
|
|| T <- [1.1, "a", a]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_float(_) ->
|
t_is_float(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_float(T))
|
[
|
||||||
|| T <- [1.1, 2.0, -1.2]],
|
?assertEqual(true, emqx_rule_funcs:is_float(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_float(T))
|
|| T <- [1.1, 2.0, -1.2]
|
||||||
|| T <- [1, "a", a, <<>>]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_float(T))
|
||||||
|
|| T <- [1, "a", a, <<>>]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_num(_) ->
|
t_is_num(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_num(T))
|
[
|
||||||
|| T <- [1.1, 2.0, -1.2, 1]],
|
?assertEqual(true, emqx_rule_funcs:is_num(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_num(T))
|
|| T <- [1.1, 2.0, -1.2, 1]
|
||||||
|| T <- ["a", a, <<>>]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_num(T))
|
||||||
|
|| T <- ["a", a, <<>>]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_map(_) ->
|
t_is_map(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_map(T))
|
[
|
||||||
|| T <- [#{}, #{a =>1}]],
|
?assertEqual(true, emqx_rule_funcs:is_map(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_map(T))
|
|| T <- [#{}, #{a => 1}]
|
||||||
|| T <- ["a", a, <<>>]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_map(T))
|
||||||
|
|| T <- ["a", a, <<>>]
|
||||||
|
].
|
||||||
|
|
||||||
t_is_array(_) ->
|
t_is_array(_) ->
|
||||||
[?assertEqual(true, emqx_rule_funcs:is_array(T))
|
[
|
||||||
|| T <- [[], [1,2]]],
|
?assertEqual(true, emqx_rule_funcs:is_array(T))
|
||||||
[?assertEqual(false, emqx_rule_funcs:is_array(T))
|
|| T <- [[], [1, 2]]
|
||||||
|| T <- [<<>>, a]].
|
],
|
||||||
|
[
|
||||||
|
?assertEqual(false, emqx_rule_funcs:is_array(T))
|
||||||
|
|| T <- [<<>>, a]
|
||||||
|
].
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Test cases for arith op
|
%% Test cases for arith op
|
||||||
|
|
@ -237,22 +284,30 @@ t_arith_op(_) ->
|
||||||
?PROPTEST(prop_arith_op).
|
?PROPTEST(prop_arith_op).
|
||||||
|
|
||||||
prop_arith_op() ->
|
prop_arith_op() ->
|
||||||
?FORALL({X, Y}, {number(), number()},
|
?FORALL(
|
||||||
begin
|
{X, Y},
|
||||||
(X + Y) == apply_func('+', [X, Y]) andalso
|
{number(), number()},
|
||||||
|
begin
|
||||||
|
(X + Y) == apply_func('+', [X, Y]) andalso
|
||||||
(X - Y) == apply_func('-', [X, Y]) andalso
|
(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]);
|
(X / Y) == apply_func('/', [X, Y]);
|
||||||
true -> true
|
true ->
|
||||||
end) andalso
|
true
|
||||||
(case is_integer(X)
|
end) andalso
|
||||||
andalso is_pos_integer(Y) of
|
(case
|
||||||
true ->
|
is_integer(X) andalso
|
||||||
(X rem Y) == apply_func('mod', [X, Y]);
|
is_pos_integer(Y)
|
||||||
false -> true
|
of
|
||||||
|
true ->
|
||||||
|
(X rem Y) == apply_func('mod', [X, Y]);
|
||||||
|
false ->
|
||||||
|
true
|
||||||
end)
|
end)
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
is_pos_integer(X) ->
|
is_pos_integer(X) ->
|
||||||
is_integer(X) andalso X > 0.
|
is_integer(X) andalso X > 0.
|
||||||
|
|
@ -266,29 +321,45 @@ t_math_fun(_) ->
|
||||||
|
|
||||||
prop_math_fun() ->
|
prop_math_fun() ->
|
||||||
Excluded = [module_info, atanh, asin, acos],
|
Excluded = [module_info, atanh, asin, acos],
|
||||||
MathFuns = [{F, A} || {F, A} <- math:module_info(exports),
|
MathFuns = [
|
||||||
not lists:member(F, Excluded),
|
{F, A}
|
||||||
erlang:function_exported(emqx_rule_funcs, F, A)],
|
|| {F, A} <- math:module_info(exports),
|
||||||
?FORALL({X, Y}, {pos_integer(), pos_integer()},
|
not lists:member(F, Excluded),
|
||||||
begin
|
erlang:function_exported(emqx_rule_funcs, F, A)
|
||||||
lists:foldl(fun({F, 1}, True) ->
|
],
|
||||||
True andalso comp_with_math(F, X);
|
?FORALL(
|
||||||
({F = fmod, 2}, True) ->
|
{X, Y},
|
||||||
True andalso (if Y =/= 0 ->
|
{pos_integer(), pos_integer()},
|
||||||
comp_with_math(F, X, Y);
|
begin
|
||||||
true -> true
|
lists:foldl(
|
||||||
end);
|
fun
|
||||||
({F, 2}, True) ->
|
({F, 1}, True) ->
|
||||||
True andalso comp_with_math(F, X, Y)
|
True andalso comp_with_math(F, X);
|
||||||
end, true, MathFuns)
|
({F = fmod, 2}, True) ->
|
||||||
end).
|
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)
|
comp_with_math(Fun, X) when
|
||||||
when Fun =:= exp;
|
Fun =:= exp;
|
||||||
Fun =:= sinh;
|
Fun =:= sinh;
|
||||||
Fun =:= cosh ->
|
Fun =:= cosh
|
||||||
if X < 710 -> math:Fun(X) == apply_func(Fun, [X]);
|
->
|
||||||
true -> true
|
if
|
||||||
|
X < 710 -> math:Fun(X) == apply_func(Fun, [X]);
|
||||||
|
true -> true
|
||||||
end;
|
end;
|
||||||
comp_with_math(F, X) ->
|
comp_with_math(F, X) ->
|
||||||
math:F(X) == apply_func(F, [X]).
|
math:F(X) == apply_func(F, [X]).
|
||||||
|
|
@ -304,15 +375,18 @@ t_bits_op(_) ->
|
||||||
?PROPTEST(prop_bits_op).
|
?PROPTEST(prop_bits_op).
|
||||||
|
|
||||||
prop_bits_op() ->
|
prop_bits_op() ->
|
||||||
?FORALL({X, Y}, {integer(), integer()},
|
?FORALL(
|
||||||
begin
|
{X, Y},
|
||||||
(bnot X) == apply_func(bitnot, [X]) andalso
|
{integer(), integer()},
|
||||||
|
begin
|
||||||
|
(bnot X) == apply_func(bitnot, [X]) andalso
|
||||||
(X band Y) == apply_func(bitand, [X, Y]) andalso
|
(X band Y) == apply_func(bitand, [X, Y]) andalso
|
||||||
(X bor Y) == apply_func(bitor, [X, Y]) andalso
|
(X bor Y) == apply_func(bitor, [X, Y]) andalso
|
||||||
(X bxor Y) == apply_func(bitxor, [X, Y]) andalso
|
(X bxor Y) == apply_func(bitxor, [X, Y]) andalso
|
||||||
(X bsl Y) == apply_func(bitsl, [X, Y]) andalso
|
(X bsl Y) == apply_func(bitsl, [X, Y]) andalso
|
||||||
(X bsr Y) == apply_func(bitsr, [X, Y])
|
(X bsr Y) == apply_func(bitsr, [X, Y])
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Test cases for string
|
%% Test cases for string
|
||||||
|
|
@ -346,77 +420,140 @@ t_trim(_) ->
|
||||||
t_split_all(_) ->
|
t_split_all(_) ->
|
||||||
?assertEqual([], apply_func(split, [<<>>, <<"/">>])),
|
?assertEqual([], apply_func(split, [<<>>, <<"/">>])),
|
||||||
?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">>], 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\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\r\n">>], apply_func(split, [<<"a,b,c\r\n">>, <<",">>])).
|
||||||
|
|
||||||
t_split_notrim_all(_) ->
|
t_split_notrim_all(_) ->
|
||||||
?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"notrim">>])),
|
?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"notrim">>])),
|
||||||
?assertEqual([<<>>,<<>>], apply_func(split, [<<"/">>, <<"/">>, <<"notrim">>])),
|
?assertEqual([<<>>, <<>>], apply_func(split, [<<"/">>, <<"/">>, <<"notrim">>])),
|
||||||
?assertEqual([<<>>, <<"a">>,<<"b">>,<<"c">>], apply_func(split, [<<"/a/b/c">>, <<"/">>, <<"notrim">>])),
|
?assertEqual(
|
||||||
?assertEqual([<<>>, <<"a">>,<<"b">>, <<>>, <<"c">>, <<>>], apply_func(split, [<<"/a/b//c/">>, <<"/">>, <<"notrim">>])),
|
[<<>>, <<"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(
|
||||||
?assertEqual([<<"哈哈"/utf8>>,<<" 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"notrim">>])).
|
[<<>>, <<"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(_) ->
|
t_split_leading(_) ->
|
||||||
?assertEqual([], apply_func(split, [<<>>, <<"/">>, <<"leading">>])),
|
?assertEqual([], apply_func(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/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading">>])),
|
?assertEqual(
|
||||||
?assertEqual([<<"a">>,<<"b,c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"leading">>])),
|
[<<"a">>, <<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"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\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(_) ->
|
t_split_leading_notrim(_) ->
|
||||||
?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"leading_notrim">>])),
|
?assertEqual([<<>>], apply_func(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(
|
||||||
?assertEqual([<<"a">>,<<"b//c/">>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"leading_notrim">>])),
|
[<<>>, <<"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(
|
||||||
?assertEqual([<<"哈哈"/utf8>>,<<" 你好, 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"leading_notrim">>])).
|
[<<"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(_) ->
|
t_split_trailing(_) ->
|
||||||
?assertEqual([], apply_func(split, [<<>>, <<"/">>, <<"trailing">>])),
|
?assertEqual([], apply_func(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">>], 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(
|
||||||
?assertEqual([<<"a b">>,<<"c\r\n">>], apply_func(split, [<<"a b,c\r\n">>, <<",">>, <<"trailing">>])),
|
[<<"a,b">>, <<"c\n">>], apply_func(split, [<<"a,b,c\n">>, <<",">>, <<"trailing">>])
|
||||||
?assertEqual([<<"哈哈, 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"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(_) ->
|
t_split_trailing_notrim(_) ->
|
||||||
?assertEqual([<<>>], apply_func(split, [<<>>, <<"/">>, <<"trailing_notrim">>])),
|
?assertEqual([<<>>], apply_func(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(
|
||||||
?assertEqual([<<"a/b//c">>, <<>>], apply_func(split, [<<"a/b//c/">>, <<"/">>, <<"trailing_notrim">>])),
|
[<<"/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(
|
||||||
?assertEqual([<<"哈哈, 你好"/utf8>>,<<" 是的\r\n"/utf8>>], apply_func(split, [<<"哈哈, 你好, 是的\r\n"/utf8>>, <<",">>, <<"trailing_notrim">>])).
|
[<<"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(_) ->
|
t_tokens(_) ->
|
||||||
?assertEqual([], apply_func(tokens, [<<>>, <<"/">>])),
|
?assertEqual([], apply_func(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">>, <<"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">>, <<"\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\r\n">>], apply_func(tokens, [<<"a ,b,c\r\n">>, <<", ">>])),
|
||||||
?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"a,b, c\n">>, <<", ">>, <<"nocrlf">>])),
|
?assertEqual(
|
||||||
?assertEqual([<<"a">>,<<"b">>,<<"c">>], apply_func(tokens, [<<"a,b,c\r\n">>, <<",">>, <<"nocrlf">>])),
|
[<<"a">>, <<"b">>, <<"c">>], apply_func(tokens, [<<"a,b, c\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\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([], 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(_) ->
|
t_concat(_) ->
|
||||||
?assertEqual(<<"ab">>, apply_func(concat, [<<"a">>, <<"b">>])),
|
?assertEqual(<<"ab">>, apply_func(concat, [<<"a">>, <<"b">>])),
|
||||||
?assertEqual(<<"ab">>, apply_func('+', [<<"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(<<"abc">>, apply_func(concat, [apply_func(concat, [<<"a">>, <<"b">>]), <<"c">>])),
|
||||||
?assertEqual(<<"a">>, apply_func(concat, [<<"">>, <<"a">>])),
|
?assertEqual(<<"a">>, apply_func(concat, [<<"">>, <<"a">>])),
|
||||||
?assertEqual(<<"a">>, apply_func(concat, [<<"a">>, <<"">>])),
|
?assertEqual(<<"a">>, apply_func(concat, [<<"a">>, <<"">>])),
|
||||||
|
|
@ -424,8 +561,13 @@ t_concat(_) ->
|
||||||
|
|
||||||
t_sprintf(_) ->
|
t_sprintf(_) ->
|
||||||
?assertEqual(<<"Hello Shawn!">>, apply_func(sprintf, [<<"Hello ~ts!">>, <<"Shawn">>])),
|
?assertEqual(<<"Hello Shawn!">>, apply_func(sprintf, [<<"Hello ~ts!">>, <<"Shawn">>])),
|
||||||
?assertEqual(<<"Name: ABC, Count: 2">>, apply_func(sprintf, [<<"Name: ~ts, Count: ~p">>, <<"ABC">>, 2])),
|
?assertEqual(
|
||||||
?assertEqual(<<"Name: ABC, Count: 2, Status: {ok,running}">>, apply_func(sprintf, [<<"Name: ~ts, Count: ~p, Status: ~p">>, <<"ABC">>, 2, {ok, running}])).
|
<<"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(_) ->
|
t_pad(_) ->
|
||||||
?assertEqual(<<"abc ">>, apply_func(pad, [<<"abc">>, 5])),
|
?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 ">>, <<" ">>, <<"::">>])),
|
?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 ">>, <<" ">>, <<"-">>, <<"all">>])),
|
||||||
?assertEqual(<<"ab-c ">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"leading">>])),
|
?assertEqual(
|
||||||
?assertEqual(<<"ab c -">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"trailing">>])).
|
<<"ab-c ">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"leading">>])
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
<<"ab c -">>, apply_func(replace, [<<"ab c ">>, <<" ">>, <<"-">>, <<"trailing">>])
|
||||||
|
).
|
||||||
|
|
||||||
t_ascii(_) ->
|
t_ascii(_) ->
|
||||||
?assertEqual(97, apply_func(ascii, [<<"a">>])),
|
?assertEqual(97, apply_func(ascii, [<<"a">>])),
|
||||||
|
|
@ -483,7 +629,7 @@ t_regex_replace(_) ->
|
||||||
?assertEqual(<<"aebed">>, apply_func(regex_replace, [<<"accbcd">>, <<"c+">>, <<"e">>])),
|
?assertEqual(<<"aebed">>, apply_func(regex_replace, [<<"accbcd">>, <<"c+">>, <<"e">>])),
|
||||||
?assertEqual(<<"a[cc]b[c]d">>, apply_func(regex_replace, [<<"accbcd">>, <<"c+">>, <<"[&]">>])).
|
?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).
|
bin(S) -> iolist_to_binary(S).
|
||||||
|
|
||||||
|
|
@ -492,34 +638,34 @@ bin(S) -> iolist_to_binary(S).
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_nth(_) ->
|
t_nth(_) ->
|
||||||
?assertEqual(2, apply_func(nth, [2, [1,2,3,4]])),
|
?assertEqual(2, apply_func(nth, [2, [1, 2, 3, 4]])),
|
||||||
?assertEqual(4, apply_func(nth, [4, [1,2,3,4]])).
|
?assertEqual(4, apply_func(nth, [4, [1, 2, 3, 4]])).
|
||||||
|
|
||||||
t_length(_) ->
|
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, [[]])).
|
?assertEqual(0, apply_func(length, [[]])).
|
||||||
|
|
||||||
t_slice(_) ->
|
t_slice(_) ->
|
||||||
?assertEqual([1,2,3,4], apply_func(sublist, [4, [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([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, 1, [1, 2, 3, 4]])),
|
||||||
?assertEqual([4], apply_func(sublist, [4, 2, [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([], apply_func(sublist, [5, 2, [1, 2, 3, 4]])),
|
||||||
?assertEqual([2,3], apply_func(sublist, [2, 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], apply_func(sublist, [1, 1, [1, 2, 3, 4]])).
|
||||||
|
|
||||||
t_first_last(_) ->
|
t_first_last(_) ->
|
||||||
?assertEqual(1, apply_func(first, [[1,2,3,4]])),
|
?assertEqual(1, apply_func(first, [[1, 2, 3, 4]])),
|
||||||
?assertEqual(4, apply_func(last, [[1,2,3,4]])).
|
?assertEqual(4, apply_func(last, [[1, 2, 3, 4]])).
|
||||||
|
|
||||||
t_contains(_) ->
|
t_contains(_) ->
|
||||||
?assertEqual(true, apply_func(contains, [1, [1,2,3,4]])),
|
?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, [3, [1, 2, 3, 4]])),
|
||||||
?assertEqual(true, apply_func(contains, [<<"a">>, [<<>>,<<"ab">>,3,<<"a">>]])),
|
?assertEqual(true, apply_func(contains, [<<"a">>, [<<>>, <<"ab">>, 3, <<"a">>]])),
|
||||||
?assertEqual(true, apply_func(contains, [#{a=>b}, [#{a=>1}, #{a=>b}]])),
|
?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, [#{a => b}, [#{a => 1}]])),
|
||||||
?assertEqual(false, apply_func(contains, [3, [1, 2]])),
|
?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(_) ->
|
t_map_get(_) ->
|
||||||
?assertEqual(1, apply_func(map_get, [<<"a">>, #{a => 1}])),
|
?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">> => 1}, apply_func(map_put, [<<"a">>, 1, #{}])),
|
||||||
?assertEqual(#{a => 2}, apply_func(map_put, [<<"a">>, 2, #{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}}, 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}])).
|
?assertEqual(#{a => 2}, apply_func(map_put, [<<"a">>, 2, #{a => 1}])).
|
||||||
|
|
||||||
t_mget(_) ->
|
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, 7])),
|
||||||
?assertEqual(127, apply_func(subbits, [<<255:8>>, 2, 8])).
|
?assertEqual(127, apply_func(subbits, [<<255:8>>, 2, 8])).
|
||||||
t_subbits2_integer(_) ->
|
t_subbits2_integer(_) ->
|
||||||
?assertEqual(456, apply_func(subbits, [<<456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>])),
|
?assertEqual(
|
||||||
?assertEqual(-456, apply_func(subbits, [<<-456:32/integer>>, 1, 32, <<"integer">>, <<"signed">>, <<"big">>])).
|
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(_) ->
|
t_subbits2_float(_) ->
|
||||||
R = apply_func(subbits, [<<5.3:64/float>>, 1, 64, <<"float">>, <<"unsigned">>, <<"big">>]),
|
R = apply_func(subbits, [<<5.3:64/float>>, 1, 64, <<"float">>, <<"unsigned">>, <<"big">>]),
|
||||||
RL = (5.3 - R),
|
RL = (5.3 - R),
|
||||||
ct:pal(";;;;~p", [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">>]),
|
R2 = apply_func(subbits, [<<-5.3:64/float>>, 1, 64, <<"float">>, <<"signed">>, <<"big">>]),
|
||||||
|
|
||||||
RL2 = (5.3 + R2),
|
RL2 = (5.3 + R2),
|
||||||
ct:pal(";;;;~p", [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
|
%% Test cases for Hash funcs
|
||||||
|
|
@ -602,12 +756,15 @@ t_hash_funcs(_) ->
|
||||||
?PROPTEST(prop_hash_fun).
|
?PROPTEST(prop_hash_fun).
|
||||||
|
|
||||||
prop_hash_fun() ->
|
prop_hash_fun() ->
|
||||||
?FORALL(S, binary(),
|
?FORALL(
|
||||||
begin
|
S,
|
||||||
(32 == byte_size(apply_func(md5, [S]))) andalso
|
binary(),
|
||||||
|
begin
|
||||||
|
(32 == byte_size(apply_func(md5, [S]))) andalso
|
||||||
(40 == byte_size(apply_func(sha, [S]))) andalso
|
(40 == byte_size(apply_func(sha, [S]))) andalso
|
||||||
(64 == byte_size(apply_func(sha256, [S])))
|
(64 == byte_size(apply_func(sha256, [S])))
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Test cases for base64
|
%% Test cases for base64
|
||||||
|
|
@ -617,72 +774,131 @@ t_base64_encode(_) ->
|
||||||
?PROPTEST(prop_base64_encode).
|
?PROPTEST(prop_base64_encode).
|
||||||
|
|
||||||
prop_base64_encode() ->
|
prop_base64_encode() ->
|
||||||
?FORALL(S, list(range(0, 255)),
|
?FORALL(
|
||||||
begin
|
S,
|
||||||
Bin = iolist_to_binary(S),
|
list(range(0, 255)),
|
||||||
Bin == base64:decode(apply_func(base64_encode, [Bin]))
|
begin
|
||||||
end).
|
Bin = iolist_to_binary(S),
|
||||||
|
Bin == base64:decode(apply_func(base64_encode, [Bin]))
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Date functions
|
%% Date functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
t_now_rfc3339(_) ->
|
t_now_rfc3339(_) ->
|
||||||
?assert(is_integer(
|
?assert(
|
||||||
calendar:rfc3339_to_system_time(
|
is_integer(
|
||||||
binary_to_list(apply_func(now_rfc3339, []))))).
|
calendar:rfc3339_to_system_time(
|
||||||
|
binary_to_list(apply_func(now_rfc3339, []))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
t_now_rfc3339_1(_) ->
|
t_now_rfc3339_1(_) ->
|
||||||
[?assert(is_integer(
|
[
|
||||||
calendar:rfc3339_to_system_time(
|
?assert(
|
||||||
binary_to_list(apply_func(now_rfc3339, [atom_to_binary(Unit, utf8)])),
|
is_integer(
|
||||||
[{unit, Unit}])))
|
calendar:rfc3339_to_system_time(
|
||||||
|| Unit <- [second,millisecond,microsecond,nanosecond]].
|
binary_to_list(apply_func(now_rfc3339, [atom_to_binary(Unit, utf8)])),
|
||||||
|
[{unit, Unit}]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|| Unit <- [second, millisecond, microsecond, nanosecond]
|
||||||
|
].
|
||||||
|
|
||||||
t_now_timestamp(_) ->
|
t_now_timestamp(_) ->
|
||||||
?assert(is_integer(apply_func(now_timestamp, []))).
|
?assert(is_integer(apply_func(now_timestamp, []))).
|
||||||
|
|
||||||
t_now_timestamp_1(_) ->
|
t_now_timestamp_1(_) ->
|
||||||
[?assert(is_integer(
|
[
|
||||||
apply_func(now_timestamp, [atom_to_binary(Unit, utf8)])))
|
?assert(
|
||||||
|| Unit <- [second,millisecond,microsecond,nanosecond]].
|
is_integer(
|
||||||
|
apply_func(now_timestamp, [atom_to_binary(Unit, utf8)])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|| Unit <- [second, millisecond, microsecond, nanosecond]
|
||||||
|
].
|
||||||
|
|
||||||
t_unix_ts_to_rfc3339(_) ->
|
t_unix_ts_to_rfc3339(_) ->
|
||||||
[begin
|
[
|
||||||
BUnit = atom_to_binary(Unit, utf8),
|
begin
|
||||||
Epoch = apply_func(now_timestamp, [BUnit]),
|
BUnit = atom_to_binary(Unit, utf8),
|
||||||
DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]),
|
Epoch = apply_func(now_timestamp, [BUnit]),
|
||||||
?assertEqual(Epoch,
|
DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]),
|
||||||
calendar:rfc3339_to_system_time(binary_to_list(DateTime), [{unit, Unit}]))
|
?assertEqual(
|
||||||
end || Unit <- [second,millisecond,microsecond,nanosecond]].
|
Epoch,
|
||||||
|
calendar:rfc3339_to_system_time(binary_to_list(DateTime), [{unit, Unit}])
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|| Unit <- [second, millisecond, microsecond, nanosecond]
|
||||||
|
].
|
||||||
|
|
||||||
t_rfc3339_to_unix_ts(_) ->
|
t_rfc3339_to_unix_ts(_) ->
|
||||||
[begin
|
[
|
||||||
BUnit = atom_to_binary(Unit, utf8),
|
begin
|
||||||
Epoch = apply_func(now_timestamp, [BUnit]),
|
BUnit = atom_to_binary(Unit, utf8),
|
||||||
DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]),
|
Epoch = apply_func(now_timestamp, [BUnit]),
|
||||||
?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
|
DateTime = apply_func(unix_ts_to_rfc3339, [Epoch, BUnit]),
|
||||||
end || Unit <- [second,millisecond,microsecond,nanosecond]].
|
?assertEqual(Epoch, emqx_rule_funcs:rfc3339_to_unix_ts(DateTime, BUnit))
|
||||||
|
end
|
||||||
|
|| Unit <- [second, millisecond, microsecond, nanosecond]
|
||||||
|
].
|
||||||
|
|
||||||
t_format_date_funcs(_) ->
|
t_format_date_funcs(_) ->
|
||||||
?PROPTEST(prop_format_date_fun).
|
?PROPTEST(prop_format_date_fun).
|
||||||
|
|
||||||
prop_format_date_fun() ->
|
prop_format_date_fun() ->
|
||||||
Args1 = [<<"second">>, <<"+07:00">>, <<"%m--%d--%y---%H:%M:%S%Z">>],
|
Args1 = [<<"second">>, <<"+07:00">>, <<"%m--%d--%y---%H:%M:%S%Z">>],
|
||||||
?FORALL(S, erlang:system_time(second),
|
?FORALL(
|
||||||
S == apply_func(date_to_unix_ts,
|
S,
|
||||||
Args1 ++ [apply_func(format_date,
|
erlang:system_time(second),
|
||||||
Args1 ++ [S])])),
|
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">>],
|
Args2 = [<<"millisecond">>, <<"+04:00">>, <<"--%m--%d--%y---%H:%M:%S%Z">>],
|
||||||
?FORALL(S, erlang:system_time(millisecond),
|
?FORALL(
|
||||||
S == apply_func(date_to_unix_ts,
|
S,
|
||||||
Args2 ++ [apply_func(format_date,
|
erlang:system_time(millisecond),
|
||||||
Args2 ++ [S])])),
|
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">>],
|
Args = [<<"second">>, <<"+08:00">>, <<"%y-%m-%d-%H:%M:%S%Z">>],
|
||||||
?FORALL(S, erlang:system_time(second),
|
?FORALL(
|
||||||
S == apply_func(date_to_unix_ts,
|
S,
|
||||||
Args ++ [apply_func(format_date,
|
erlang:system_time(second),
|
||||||
Args ++ [S])])).
|
S ==
|
||||||
|
apply_func(
|
||||||
|
date_to_unix_ts,
|
||||||
|
Args ++
|
||||||
|
[
|
||||||
|
apply_func(
|
||||||
|
format_date,
|
||||||
|
Args ++ [S]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Utility functions
|
%% Utility functions
|
||||||
|
|
@ -699,8 +915,10 @@ apply_func(Name, Args, Msg) ->
|
||||||
apply_func(Name, Args, emqx_message:to_map(Msg)).
|
apply_func(Name, Args, emqx_message:to_map(Msg)).
|
||||||
|
|
||||||
message() ->
|
message() ->
|
||||||
emqx_message:set_flags(#{dup => false},
|
emqx_message:set_flags(
|
||||||
emqx_message:make(<<"clientid">>, 1, <<"topic/#">>, <<"payload">>)).
|
#{dup => false},
|
||||||
|
emqx_message:make(<<"clientid">>, 1, <<"topic/#">>, <<"payload">>)
|
||||||
|
).
|
||||||
|
|
||||||
% t_contains_topic(_) ->
|
% t_contains_topic(_) ->
|
||||||
% error('TODO').
|
% error('TODO').
|
||||||
|
|
@ -831,13 +1049,15 @@ message() ->
|
||||||
% t_json_decode(_) ->
|
% t_json_decode(_) ->
|
||||||
% error('TODO').
|
% error('TODO').
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% CT functions
|
%% CT functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
IsTestCase = fun("t_" ++ _) -> true; (_) -> false end,
|
IsTestCase = fun
|
||||||
|
("t_" ++ _) -> true;
|
||||||
|
(_) -> false
|
||||||
|
end,
|
||||||
[F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))].
|
[F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))].
|
||||||
|
|
||||||
suite() ->
|
suite() ->
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,27 @@
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
-import(emqx_rule_maps,
|
-import(
|
||||||
[ nested_get/2
|
emqx_rule_maps,
|
||||||
, nested_get/3
|
[
|
||||||
, nested_put/3
|
nested_get/2,
|
||||||
, atom_key_map/1
|
nested_get/3,
|
||||||
]).
|
nested_put/3,
|
||||||
|
atom_key_map/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
-define(path(Path), {path,
|
-define(path(Path),
|
||||||
[case K of
|
{path, [
|
||||||
{ic, Key} -> {index, {const, Key}};
|
case K of
|
||||||
{iv, Key} -> {index, {var, Key}};
|
{ic, Key} -> {index, {const, Key}};
|
||||||
{i, Path1} -> {index, Path1};
|
{iv, Key} -> {index, {var, Key}};
|
||||||
_ -> {key, K}
|
{i, Path1} -> {index, Path1};
|
||||||
end || K <- Path]}).
|
_ -> {key, K}
|
||||||
|
end
|
||||||
|
|| K <- Path
|
||||||
|
]}
|
||||||
|
).
|
||||||
|
|
||||||
-define(PROPTEST(Prop), true = proper:quickcheck(Prop)).
|
-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 => a}, nested_put(?path([a]), a, #{})),
|
||||||
?assertEqual(#{a => undefined}, nested_put(?path([a]), undefined, #{})),
|
?assertEqual(#{a => undefined}, nested_put(?path([a]), undefined, #{})),
|
||||||
?assertEqual(#{a => 1}, nested_put(?path([a]), 1, not_map)),
|
?assertEqual(#{a => 1}, nested_put(?path([a]), 1, not_map)),
|
||||||
?assertEqual(#{a => #{b => b}}, nested_put(?path([a,b]), b, #{})),
|
?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 => #{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}, 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})),
|
?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}, 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">> => 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(
|
||||||
?assertEqual(#{<<"k">> => #{t => v1}}, nested_put(?path([k,t]), v1, #{<<"k">> => #{t => v0}})),
|
#{<<"k">> => #{<<"t">> => v1}},
|
||||||
?assertEqual(#{k => #{<<"t">> => #{a => v1}}}, nested_put(?path([k,t,a]), v1, #{k => #{<<"t">> => v0}})),
|
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 => 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(_) ->
|
t_nested_put_index(_) ->
|
||||||
?assertEqual([1,a,3], nested_put(?path([{ic,2}]), a, [1,2,3])),
|
?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, 0}]), a, [1, 2, 3])),
|
||||||
?assertEqual([1,2,3], nested_put(?path([{ic,4}]), 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}]), a, [1, [2], 3])),
|
||||||
?assertEqual([1,[[a]],3], nested_put(?path([{ic,2}, {ic,1}, {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, [[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], 1], nested_put(?path([{ic, 2}, {i, ?path([{ic, 3}])}]), a, [1, [2], 1])),
|
||||||
%% nested_put to the first or tail of a list:
|
%% 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, not_list)),
|
||||||
?assertEqual([a], nested_put(?path([{ic,head}]), a, [])),
|
?assertEqual([a], nested_put(?path([{ic, head}]), a, [])),
|
||||||
?assertEqual([a,1,2,3], nested_put(?path([{ic,head}]), a, [1,2,3])),
|
?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, not_list)),
|
||||||
?assertEqual([a], nested_put(?path([{ic,tail}]), a, [])),
|
?assertEqual([a], nested_put(?path([{ic, tail}]), a, [])),
|
||||||
?assertEqual([1,2,3,a], nested_put(?path([{ic,tail}]), a, [1,2,3])).
|
?assertEqual([1, 2, 3, a], nested_put(?path([{ic, tail}]), a, [1, 2, 3])).
|
||||||
|
|
||||||
t_nested_put_negative_index(_) ->
|
t_nested_put_negative_index(_) ->
|
||||||
?assertEqual([1,2,a], nested_put(?path([{ic,-1}]), 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([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([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, 3], nested_put(?path([{ic, -4}]), a, [1, 2, 3])).
|
||||||
|
|
||||||
t_nested_put_mix_map_index(_) ->
|
t_nested_put_mix_map_index(_) ->
|
||||||
?assertEqual(#{a => [a]}, nested_put(?path([a, {ic,2}]), a, #{})),
|
?assertEqual(#{a => [a]}, nested_put(?path([a, {ic, 2}]), a, #{})),
|
||||||
?assertEqual(#{a => [#{b => 0}]}, nested_put(?path([a, {ic,2}, b]), 0, #{})),
|
?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(#{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]), c, [1, #{a => b}, 3])),
|
||||||
?assertEqual([1,#{a => [c]},3], nested_put(?path([{ic,2}, a, {ic, 1}]), c, [1,#{a => [b]},3])),
|
?assertEqual(
|
||||||
?assertEqual(#{a => [1,a,3], b => 2}, nested_put(?path([a, {iv,b}]), a, #{a => [1,2,3], b => 2})),
|
[1, #{a => [c]}, 3], nested_put(?path([{ic, 2}, a, {ic, 1}]), c, [1, #{a => [b]}, 3])
|
||||||
?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 => [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(_) ->
|
t_nested_get_map(_) ->
|
||||||
?assertEqual(undefined, nested_get(?path([a]), not_map)),
|
?assertEqual(undefined, nested_get(?path([a]), not_map)),
|
||||||
?assertEqual(#{a => 1}, nested_get(?path([]), #{a => 1})),
|
?assertEqual(#{a => 1}, nested_get(?path([]), #{a => 1})),
|
||||||
?assertEqual(#{b => c}, nested_get(?path([a]), #{a => #{b => c}})),
|
?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]), not_map)),
|
||||||
?assertEqual(undefined, nested_get(?path([a,b,c]), #{})),
|
?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 => #{}})),
|
||||||
?assertEqual(undefined, nested_get(?path([a,b,c]), #{a => #{b => #{}}})),
|
?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(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(c, nested_get(?path([a, b, c]), #{a => #{b => #{c => c}}})).
|
||||||
|
|
||||||
t_nested_get_map_1(_) ->
|
t_nested_get_map_1(_) ->
|
||||||
?assertEqual(1, nested_get(?path([a]), <<"{\"a\": 1}">>)),
|
?assertEqual(1, nested_get(?path([a]), <<"{\"a\": 1}">>)),
|
||||||
?assertEqual(<<"{\"b\": 1}">>, nested_get(?path([a]), #{a => <<"{\"b\": 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(_) ->
|
t_nested_get_index(_) ->
|
||||||
%% single index get
|
%% single index get
|
||||||
?assertEqual(1, nested_get(?path([{ic,1}]), [1,2,3])),
|
?assertEqual(1, nested_get(?path([{ic, 1}]), [1, 2, 3])),
|
||||||
?assertEqual(2, nested_get(?path([{ic,2}]), [1,2,3])),
|
?assertEqual(2, nested_get(?path([{ic, 2}]), [1, 2, 3])),
|
||||||
?assertEqual(3, nested_get(?path([{ic,3}]), [1,2,3])),
|
?assertEqual(3, nested_get(?path([{ic, 3}]), [1, 2, 3])),
|
||||||
?assertEqual(undefined, nested_get(?path([{ic,0}]), [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("not_found", nested_get(?path([{ic, 0}]), [1, 2, 3], "not_found")),
|
||||||
?assertEqual(undefined, nested_get(?path([{ic,4}]), [1,2,3])),
|
?assertEqual(undefined, nested_get(?path([{ic, 4}]), [1, 2, 3])),
|
||||||
?assertEqual("not_found", nested_get(?path([{ic,4}]), [1,2,3], "not_found")),
|
?assertEqual("not_found", nested_get(?path([{ic, 4}]), [1, 2, 3], "not_found")),
|
||||||
%% multiple index get
|
%% multiple index get
|
||||||
?assertEqual(c, nested_get(?path([{ic,2}, {ic,3}]), [1,[a,b,c],3])),
|
?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(
|
||||||
?assertEqual(undefined, nested_get(?path([{ic,2}, {ic,1}, {ic,1}]), [1,[a,b,["I","II","III"]],3])),
|
"I", nested_get(?path([{ic, 2}, {ic, 3}, {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(
|
||||||
|
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(_) ->
|
t_nested_get_negative_index(_) ->
|
||||||
?assertEqual(3, nested_get(?path([{ic,-1}]), [1,2,3])),
|
?assertEqual(3, nested_get(?path([{ic, -1}]), [1, 2, 3])),
|
||||||
?assertEqual(2, nested_get(?path([{ic,-2}]), [1,2,3])),
|
?assertEqual(2, nested_get(?path([{ic, -2}]), [1, 2, 3])),
|
||||||
?assertEqual(1, nested_get(?path([{ic,-3}]), [1,2,3])),
|
?assertEqual(1, nested_get(?path([{ic, -3}]), [1, 2, 3])),
|
||||||
?assertEqual(undefined, nested_get(?path([{ic,-4}]), [1,2,3])).
|
?assertEqual(undefined, nested_get(?path([{ic, -4}]), [1, 2, 3])).
|
||||||
|
|
||||||
t_nested_get_mix_map_index(_) ->
|
t_nested_get_mix_map_index(_) ->
|
||||||
%% index const
|
%% index const
|
||||||
?assertEqual(1, nested_get(?path([a, {ic,1}]), #{a => [1,2,3]})),
|
?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(2, nested_get(?path([{ic, 2}, a]), [1, #{a => 2}, 3])),
|
||||||
?assertEqual(undefined, nested_get(?path([a, {ic,0}]), #{a => [1,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([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("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(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("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(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(
|
||||||
?assertEqual("I", nested_get(?path([{ic,2}, c, d]), [1,#{a => a, b => b, c => #{d => "I"}},3])),
|
"I",
|
||||||
?assertEqual(undefined, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3])),
|
nested_get(?path([{ic, 2}, c, {ic, 1}]), [1, #{a => a, b => b, c => ["I", "II", "III"]}, 3])
|
||||||
?assertEqual(default, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3], default)),
|
),
|
||||||
|
?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
|
%% 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(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, c}]), #{a => [1, 2, 3], b => 1})),
|
||||||
?assertEqual(undefined, nested_get(?path([a, {iv,b}]), #{a => [1,2,3], b => 4})),
|
?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]),
|
?assertEqual(
|
||||||
[1,#{a => a, b => b, c => #{d => "I"}},2], default)),
|
"I",
|
||||||
?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]),
|
nested_get(
|
||||||
#{a => [1,2,3], b => [#{c => 3}]})),
|
?path([{i, ?path([{ic, 3}])}, c, d]),
|
||||||
?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]),
|
[1, #{a => a, b => b, c => #{d => "I"}}, 2],
|
||||||
#{a => [1,2,3], b => [#{c => 3}]}, default)),
|
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])}]),
|
?assertEqual(
|
||||||
#{a => [1,2,3], b => [#{c => 3}]}, default)).
|
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(_) ->
|
t_atom_key_map(_) ->
|
||||||
?assertEqual(#{a => 1}, atom_key_map(#{<<"a">> => 1})),
|
?assertEqual(#{a => 1}, atom_key_map(#{<<"a">> => 1})),
|
||||||
?assertEqual(#{a => 1, b => #{a => 2}},
|
?assertEqual(
|
||||||
atom_key_map(#{<<"a">> => 1, <<"b">> => #{<<"a">> => 2}})),
|
#{a => 1, b => #{a => 2}},
|
||||||
?assertEqual([#{a => 1}, #{b => #{a => 2}}],
|
atom_key_map(#{<<"a">> => 1, <<"b">> => #{<<"a">> => 2}})
|
||||||
atom_key_map([#{<<"a">> => 1}, #{<<"b">> => #{<<"a">> => 2}}])),
|
),
|
||||||
?assertEqual(#{a => 1, b => [#{a => 2}, #{c => 2}]},
|
?assertEqual(
|
||||||
atom_key_map(#{<<"a">> => 1, <<"b">> => [#{<<"a">> => 2}, #{<<"c">> => 2}]})).
|
[#{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() ->
|
all() ->
|
||||||
IsTestCase = fun("t_" ++ _) -> true; (_) -> false end,
|
IsTestCase = fun
|
||||||
|
("t_" ++ _) -> true;
|
||||||
|
(_) -> false
|
||||||
|
end,
|
||||||
[F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))].
|
[F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))].
|
||||||
|
|
||||||
suite() ->
|
suite() ->
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
-include_lib("proper/include/proper.hrl").
|
-include_lib("proper/include/proper.hrl").
|
||||||
|
|
||||||
prop_get_put_single_key() ->
|
prop_get_put_single_key() ->
|
||||||
?FORALL({Key, Val}, {term(), term()},
|
?FORALL(
|
||||||
begin
|
{Key, Val},
|
||||||
Val =:= emqx_rule_maps:nested_get({var, Key},
|
{term(), term()},
|
||||||
emqx_rule_maps:nested_put({var, Key}, Val, #{}))
|
begin
|
||||||
end).
|
Val =:=
|
||||||
|
emqx_rule_maps:nested_get(
|
||||||
|
{var, Key},
|
||||||
|
emqx_rule_maps:nested_put({var, Key}, Val, #{})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue