chore: reformat mgmt code.
This commit is contained in:
parent
5661948a86
commit
aa7807baeb
|
@ -1,20 +1,28 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
|
|
||||||
{deps, [ {emqx, {path, "../emqx"}}
|
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||||
]}.
|
|
||||||
|
|
||||||
{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,
|
||||||
warnings_as_errors,
|
warn_obsolete_guard,
|
||||||
debug_info,
|
warnings_as_errors,
|
||||||
{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_management,
|
{application, emqx_management, [
|
||||||
[{description, "EMQX Management API and CLI"},
|
{description, "EMQX Management API and CLI"},
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
% strict semver, bump manually!
|
||||||
{modules, []},
|
{vsn, "5.0.0"},
|
||||||
{registered, [emqx_management_sup]},
|
{modules, []},
|
||||||
{applications, [kernel,stdlib,emqx_plugins,minirest,emqx]},
|
{registered, [emqx_management_sup]},
|
||||||
{mod, {emqx_mgmt_app,[]}},
|
{applications, [kernel, stdlib, emqx_plugins, minirest, emqx]},
|
||||||
{env, []},
|
{mod, {emqx_mgmt_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-management"}
|
{links, [
|
||||||
]}
|
{"Homepage", "https://emqx.io/"},
|
||||||
]}.
|
{"Github", "https://github.com/emqx/emqx-management"}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
|
@ -19,9 +19,11 @@
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
-export([ namespace/0
|
-export([
|
||||||
, roots/0
|
namespace/0,
|
||||||
, fields/1]).
|
roots/0,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
namespace() -> management.
|
namespace() -> management.
|
||||||
|
|
||||||
|
|
|
@ -25,76 +25,82 @@
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
%% Nodes and Brokers API
|
%% Nodes and Brokers API
|
||||||
-export([ list_nodes/0
|
-export([
|
||||||
, lookup_node/1
|
list_nodes/0,
|
||||||
, list_brokers/0
|
lookup_node/1,
|
||||||
, lookup_broker/1
|
list_brokers/0,
|
||||||
, node_info/0
|
lookup_broker/1,
|
||||||
, node_info/1
|
node_info/0,
|
||||||
, broker_info/0
|
node_info/1,
|
||||||
, broker_info/1
|
broker_info/0,
|
||||||
]).
|
broker_info/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Metrics and Stats
|
%% Metrics and Stats
|
||||||
-export([ get_metrics/0
|
-export([
|
||||||
, get_metrics/1
|
get_metrics/0,
|
||||||
, get_stats/0
|
get_metrics/1,
|
||||||
, get_stats/1
|
get_stats/0,
|
||||||
]).
|
get_stats/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Clients, Sessions
|
%% Clients, Sessions
|
||||||
-export([ lookup_client/2
|
-export([
|
||||||
, lookup_client/3
|
lookup_client/2,
|
||||||
, kickout_client/1
|
lookup_client/3,
|
||||||
, list_authz_cache/1
|
kickout_client/1,
|
||||||
, list_client_subscriptions/1
|
list_authz_cache/1,
|
||||||
, client_subscriptions/2
|
list_client_subscriptions/1,
|
||||||
, clean_authz_cache/1
|
client_subscriptions/2,
|
||||||
, clean_authz_cache/2
|
clean_authz_cache/1,
|
||||||
, clean_authz_cache_all/0
|
clean_authz_cache/2,
|
||||||
, clean_authz_cache_all/1
|
clean_authz_cache_all/0,
|
||||||
, set_ratelimit_policy/2
|
clean_authz_cache_all/1,
|
||||||
, set_quota_policy/2
|
set_ratelimit_policy/2,
|
||||||
, set_keepalive/2
|
set_quota_policy/2,
|
||||||
]).
|
set_keepalive/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Internal funcs
|
%% Internal funcs
|
||||||
-export([do_call_client/2]).
|
-export([do_call_client/2]).
|
||||||
|
|
||||||
%% Subscriptions
|
%% Subscriptions
|
||||||
-export([ list_subscriptions/1
|
-export([
|
||||||
, list_subscriptions_via_topic/2
|
list_subscriptions/1,
|
||||||
, list_subscriptions_via_topic/3
|
list_subscriptions_via_topic/2,
|
||||||
, lookup_subscriptions/1
|
list_subscriptions_via_topic/3,
|
||||||
, lookup_subscriptions/2
|
lookup_subscriptions/1,
|
||||||
|
lookup_subscriptions/2,
|
||||||
|
|
||||||
, do_list_subscriptions/0
|
do_list_subscriptions/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% PubSub
|
%% PubSub
|
||||||
-export([ subscribe/2
|
-export([
|
||||||
, do_subscribe/2
|
subscribe/2,
|
||||||
, publish/1
|
do_subscribe/2,
|
||||||
, unsubscribe/2
|
publish/1,
|
||||||
, do_unsubscribe/2
|
unsubscribe/2,
|
||||||
]).
|
do_unsubscribe/2
|
||||||
|
]).
|
||||||
|
|
||||||
%% Alarms
|
%% Alarms
|
||||||
-export([ get_alarms/1
|
-export([
|
||||||
, get_alarms/2
|
get_alarms/1,
|
||||||
, deactivate/2
|
get_alarms/2,
|
||||||
, delete_all_deactivated_alarms/0
|
deactivate/2,
|
||||||
, delete_all_deactivated_alarms/1
|
delete_all_deactivated_alarms/0,
|
||||||
]).
|
delete_all_deactivated_alarms/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Banned
|
%% Banned
|
||||||
-export([ create_banned/1
|
-export([
|
||||||
, delete_banned/1
|
create_banned/1,
|
||||||
]).
|
delete_banned/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% Common Table API
|
%% Common Table API
|
||||||
-export([ max_row_limit/0
|
-export([max_row_limit/0]).
|
||||||
]).
|
|
||||||
|
|
||||||
-define(APP, emqx_management).
|
-define(APP, emqx_management).
|
||||||
|
|
||||||
|
@ -113,24 +119,26 @@ list_nodes() ->
|
||||||
lookup_node(Node) -> node_info(Node).
|
lookup_node(Node) -> node_info(Node).
|
||||||
|
|
||||||
node_info() ->
|
node_info() ->
|
||||||
Memory = emqx_vm:get_memory(),
|
Memory = emqx_vm:get_memory(),
|
||||||
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
|
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
|
||||||
BrokerInfo = emqx_sys:info(),
|
BrokerInfo = emqx_sys:info(),
|
||||||
Info#{node => node(),
|
Info#{
|
||||||
otp_release => otp_rel(),
|
node => node(),
|
||||||
memory_total => proplists:get_value(allocated, Memory),
|
otp_release => otp_rel(),
|
||||||
memory_used => proplists:get_value(used, Memory),
|
memory_total => proplists:get_value(allocated, Memory),
|
||||||
process_available => erlang:system_info(process_limit),
|
memory_used => proplists:get_value(used, Memory),
|
||||||
process_used => erlang:system_info(process_count),
|
process_available => erlang:system_info(process_limit),
|
||||||
|
process_used => erlang:system_info(process_count),
|
||||||
|
|
||||||
max_fds => proplists:get_value(
|
max_fds => proplists:get_value(
|
||||||
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
|
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))
|
||||||
connections => ets:info(emqx_channel, size),
|
),
|
||||||
node_status => 'Running',
|
connections => ets:info(emqx_channel, size),
|
||||||
uptime => proplists:get_value(uptime, BrokerInfo),
|
node_status => 'Running',
|
||||||
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
uptime => proplists:get_value(uptime, BrokerInfo),
|
||||||
role => mria_rlog:role()
|
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
||||||
}.
|
role => mria_rlog:role()
|
||||||
|
}.
|
||||||
|
|
||||||
node_info(Node) ->
|
node_info(Node) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
|
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
|
||||||
|
@ -167,18 +175,21 @@ get_metrics(Node) ->
|
||||||
|
|
||||||
get_stats() ->
|
get_stats() ->
|
||||||
GlobalStatsKeys =
|
GlobalStatsKeys =
|
||||||
[ 'retained.count'
|
[
|
||||||
, 'retained.max'
|
'retained.count',
|
||||||
, 'topics.count'
|
'retained.max',
|
||||||
, 'topics.max'
|
'topics.count',
|
||||||
, 'subscriptions.shared.count'
|
'topics.max',
|
||||||
, 'subscriptions.shared.max'
|
'subscriptions.shared.count',
|
||||||
|
'subscriptions.shared.max'
|
||||||
],
|
],
|
||||||
CountStats = nodes_info_count([
|
CountStats = nodes_info_count([
|
||||||
begin
|
begin
|
||||||
Stats = get_stats(Node),
|
Stats = get_stats(Node),
|
||||||
delete_keys(Stats, GlobalStatsKeys)
|
delete_keys(Stats, GlobalStatsKeys)
|
||||||
end || Node <- mria_mnesia:running_nodes()]),
|
end
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]),
|
||||||
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
|
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
|
||||||
maps:merge(CountStats, GlobalStats).
|
maps:merge(CountStats, GlobalStats).
|
||||||
|
|
||||||
|
@ -207,21 +218,28 @@ nodes_info_count(PropList) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
lookup_client({clientid, ClientId}, FormatFun) ->
|
lookup_client({clientid, ClientId}, FormatFun) ->
|
||||||
lists:append([lookup_client(Node, {clientid, ClientId}, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]);
|
lookup_client(Node, {clientid, ClientId}, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]);
|
||||||
lookup_client({username, Username}, FormatFun) ->
|
lookup_client({username, Username}, FormatFun) ->
|
||||||
lists:append([lookup_client(Node, {username, Username}, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]).
|
lookup_client(Node, {username, Username}, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]).
|
||||||
|
|
||||||
lookup_client(Node, Key, {M, F}) ->
|
lookup_client(Node, Key, {M, F}) ->
|
||||||
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
|
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
|
||||||
{error, Err} -> {error, Err};
|
{error, Err} ->
|
||||||
L -> lists:map(fun({Chan, Info0, Stats}) ->
|
{error, Err};
|
||||||
Info = Info0#{node => Node},
|
L ->
|
||||||
M:F({Chan, Info, Stats})
|
lists:map(
|
||||||
end,
|
fun({Chan, Info0, Stats}) ->
|
||||||
L)
|
Info = Info0#{node => Node},
|
||||||
|
M:F({Chan, Info, Stats})
|
||||||
|
end,
|
||||||
|
L
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
kickout_client({ClientID, FormatFun}) ->
|
kickout_client({ClientID, FormatFun}) ->
|
||||||
|
@ -266,7 +284,7 @@ clean_authz_cache(Node, ClientId) ->
|
||||||
clean_authz_cache_all() ->
|
clean_authz_cache_all() ->
|
||||||
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
|
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
|
||||||
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
|
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
|
||||||
[] -> ok;
|
[] -> ok;
|
||||||
BadNodes -> {error, BadNodes}
|
BadNodes -> {error, BadNodes}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -287,9 +305,13 @@ set_keepalive(_ClientId, _Interval) ->
|
||||||
%% @private
|
%% @private
|
||||||
call_client(ClientId, Req) ->
|
call_client(ClientId, Req) ->
|
||||||
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
|
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
|
||||||
Expected = lists:filter(fun({error, _}) -> false;
|
Expected = lists:filter(
|
||||||
(_) -> true
|
fun
|
||||||
end, Results),
|
({error, _}) -> false;
|
||||||
|
(_) -> true
|
||||||
|
end,
|
||||||
|
Results
|
||||||
|
),
|
||||||
case Expected of
|
case Expected of
|
||||||
[] -> {error, not_found};
|
[] -> {error, not_found};
|
||||||
[Result | _] -> Result
|
[Result | _] -> Result
|
||||||
|
@ -299,13 +321,15 @@ call_client(ClientId, Req) ->
|
||||||
-spec do_call_client(emqx_types:clientid(), term()) -> term().
|
-spec do_call_client(emqx_types:clientid(), term()) -> term().
|
||||||
do_call_client(ClientId, Req) ->
|
do_call_client(ClientId, Req) ->
|
||||||
case emqx_cm:lookup_channels(ClientId) of
|
case emqx_cm:lookup_channels(ClientId) of
|
||||||
[] -> {error, not_found};
|
[] ->
|
||||||
|
{error, not_found};
|
||||||
Pids when is_list(Pids) ->
|
Pids when is_list(Pids) ->
|
||||||
Pid = lists:last(Pids),
|
Pid = lists:last(Pids),
|
||||||
case emqx_cm:get_chan_info(ClientId, Pid) of
|
case emqx_cm:get_chan_info(ClientId, Pid) of
|
||||||
#{conninfo := #{conn_mod := ConnMod}} ->
|
#{conninfo := #{conn_mod := ConnMod}} ->
|
||||||
erlang:apply(ConnMod, call, [Pid, Req]);
|
erlang:apply(ConnMod, call, [Pid, Req]);
|
||||||
undefined -> {error, not_found}
|
undefined ->
|
||||||
|
{error, not_found}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -320,22 +344,28 @@ call_client(Node, ClientId, Req) ->
|
||||||
-spec do_list_subscriptions() -> [map()].
|
-spec do_list_subscriptions() -> [map()].
|
||||||
do_list_subscriptions() ->
|
do_list_subscriptions() ->
|
||||||
case check_row_limit([mqtt_subproperty]) of
|
case check_row_limit([mqtt_subproperty]) of
|
||||||
false -> throw(max_row_limit);
|
false ->
|
||||||
ok -> [#{topic => Topic, clientid => ClientId, options => Options}
|
throw(max_row_limit);
|
||||||
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)]
|
ok ->
|
||||||
|
[
|
||||||
|
#{topic => Topic, clientid => ClientId, options => Options}
|
||||||
|
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)
|
||||||
|
]
|
||||||
end.
|
end.
|
||||||
|
|
||||||
list_subscriptions(Node) ->
|
list_subscriptions(Node) ->
|
||||||
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
|
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
|
||||||
|
|
||||||
list_subscriptions_via_topic(Topic, FormatFun) ->
|
list_subscriptions_via_topic(Topic, FormatFun) ->
|
||||||
lists:append([list_subscriptions_via_topic(Node, Topic, FormatFun)
|
lists:append([
|
||||||
|| Node <- mria_mnesia:running_nodes()]).
|
list_subscriptions_via_topic(Node, Topic, FormatFun)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
]).
|
||||||
|
|
||||||
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
|
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
|
||||||
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
|
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
|
||||||
{error, Reason} -> {error, Reason};
|
{error, Reason} -> {error, Reason};
|
||||||
Result -> M:F(Result)
|
Result -> M:F(Result)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
lookup_subscriptions(ClientId) ->
|
lookup_subscriptions(ClientId) ->
|
||||||
|
@ -354,20 +384,17 @@ subscribe(ClientId, TopicTables) ->
|
||||||
subscribe([Node | Nodes], ClientId, TopicTables) ->
|
subscribe([Node | Nodes], ClientId, TopicTables) ->
|
||||||
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
|
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
|
||||||
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
|
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
|
||||||
{subscribe, Res} ->
|
{subscribe, Res} -> {subscribe, Res, Node}
|
||||||
{subscribe, Res, Node}
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
subscribe([], _ClientId, _TopicTables) ->
|
subscribe([], _ClientId, _TopicTables) ->
|
||||||
{error, channel_not_found}.
|
{error, channel_not_found}.
|
||||||
|
|
||||||
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
|
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
|
||||||
{subscribe, _} | {error, atom()}.
|
{subscribe, _} | {error, atom()}.
|
||||||
do_subscribe(ClientId, TopicTables) ->
|
do_subscribe(ClientId, TopicTables) ->
|
||||||
case ets:lookup(emqx_channel, ClientId) of
|
case ets:lookup(emqx_channel, ClientId) of
|
||||||
[] -> {error, channel_not_found};
|
[] -> {error, channel_not_found};
|
||||||
[{_, Pid}] ->
|
[{_, Pid}] -> Pid ! {subscribe, TopicTables}
|
||||||
Pid ! {subscribe, TopicTables}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%TODO: ???
|
%%TODO: ???
|
||||||
|
@ -376,12 +403,12 @@ publish(Msg) ->
|
||||||
emqx:publish(Msg).
|
emqx:publish(Msg).
|
||||||
|
|
||||||
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
unsubscribe(ClientId, Topic) ->
|
unsubscribe(ClientId, Topic) ->
|
||||||
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
|
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
|
||||||
|
|
||||||
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
|
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
unsubscribe([Node | Nodes], ClientId, Topic) ->
|
unsubscribe([Node | Nodes], ClientId, Topic) ->
|
||||||
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
|
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
|
||||||
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
|
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
|
||||||
|
@ -391,12 +418,11 @@ unsubscribe([], _ClientId, _Topic) ->
|
||||||
{error, channel_not_found}.
|
{error, channel_not_found}.
|
||||||
|
|
||||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, _}.
|
{unsubscribe, _} | {error, _}.
|
||||||
do_unsubscribe(ClientId, Topic) ->
|
do_unsubscribe(ClientId, Topic) ->
|
||||||
case ets:lookup(emqx_channel, ClientId) of
|
case ets:lookup(emqx_channel, ClientId) of
|
||||||
[] -> {error, channel_not_found};
|
[] -> {error, channel_not_found};
|
||||||
[{_, Pid}] ->
|
[{_, Pid}] -> Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
||||||
Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -426,11 +452,18 @@ add_duration_field([], _Now, Acc) ->
|
||||||
Acc;
|
Acc;
|
||||||
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
|
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
|
||||||
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
|
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
|
||||||
|
add_duration_field(
|
||||||
add_duration_field( [Alarm = #{ activated := false
|
[
|
||||||
, activate_at := ActivateAt
|
Alarm = #{
|
||||||
, deactivate_at := DeactivateAt} | Rest]
|
activated := false,
|
||||||
, Now, Acc) ->
|
activate_at := ActivateAt,
|
||||||
|
deactivate_at := DeactivateAt
|
||||||
|
}
|
||||||
|
| Rest
|
||||||
|
],
|
||||||
|
Now,
|
||||||
|
Acc
|
||||||
|
) ->
|
||||||
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
|
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -462,13 +495,13 @@ check_row_limit([], _Limit) ->
|
||||||
ok;
|
ok;
|
||||||
check_row_limit([Tab | Tables], Limit) ->
|
check_row_limit([Tab | Tables], Limit) ->
|
||||||
case table_size(Tab) > Limit of
|
case table_size(Tab) > Limit of
|
||||||
true -> false;
|
true -> false;
|
||||||
false -> check_row_limit(Tables, Limit)
|
false -> check_row_limit(Tables, Limit)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_results(Results) ->
|
check_results(Results) ->
|
||||||
case lists:any(fun(Item) -> Item =:= ok end, Results) of
|
case lists:any(fun(Item) -> Item =:= ok end, Results) of
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> wrap_rpc(lists:last(Results))
|
false -> wrap_rpc(lists:last(Results))
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,18 @@
|
||||||
|
|
||||||
-define(FRESH_SELECT, fresh_select).
|
-define(FRESH_SELECT, fresh_select).
|
||||||
|
|
||||||
-export([ paginate/3
|
-export([
|
||||||
, paginate/4
|
paginate/3,
|
||||||
]).
|
paginate/4
|
||||||
|
]).
|
||||||
|
|
||||||
%% first_next query APIs
|
%% first_next query APIs
|
||||||
-export([ node_query/5
|
-export([
|
||||||
, cluster_query/4
|
node_query/5,
|
||||||
, select_table_with_count/5
|
cluster_query/4,
|
||||||
, b2i/1
|
select_table_with_count/5,
|
||||||
]).
|
b2i/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([do_query/6]).
|
-export([do_query/6]).
|
||||||
|
|
||||||
|
@ -50,30 +52,30 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
||||||
Limit = b2i(limit(Params)),
|
Limit = b2i(limit(Params)),
|
||||||
Cursor = qlc:cursor(Qh),
|
Cursor = qlc:cursor(Qh),
|
||||||
case Page > 1 of
|
case Page > 1 of
|
||||||
true ->
|
true ->
|
||||||
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
||||||
ok;
|
ok;
|
||||||
false -> ok
|
false ->
|
||||||
|
ok
|
||||||
end,
|
end,
|
||||||
Rows = qlc:next_answers(Cursor, Limit),
|
Rows = qlc:next_answers(Cursor, Limit),
|
||||||
qlc:delete_cursor(Cursor),
|
qlc:delete_cursor(Cursor),
|
||||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
#{
|
||||||
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]}.
|
meta => #{page => Page, limit => Limit, count => Count},
|
||||||
|
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
|
||||||
|
}.
|
||||||
|
|
||||||
query_handle(Table) when is_atom(Table) ->
|
query_handle(Table) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table)]);
|
qlc:q([R || R <- ets:table(Table)]);
|
||||||
|
|
||||||
query_handle({Table, Opts}) when is_atom(Table) ->
|
query_handle({Table, Opts}) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||||
|
|
||||||
query_handle([Table]) when is_atom(Table) ->
|
query_handle([Table]) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table)]);
|
qlc:q([R || R <- ets:table(Table)]);
|
||||||
|
|
||||||
query_handle([{Table, Opts}]) when is_atom(Table) ->
|
query_handle([{Table, Opts}]) when is_atom(Table) ->
|
||||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||||
|
|
||||||
query_handle(Tables) ->
|
query_handle(Tables) ->
|
||||||
qlc:append([query_handle(T) || T <- Tables]). %
|
%
|
||||||
|
qlc:append([query_handle(T) || T <- Tables]).
|
||||||
|
|
||||||
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||||
Options = {traverse, {select, MatchSpec}},
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
@ -87,16 +89,12 @@ query_handle(Tables, MatchSpec) ->
|
||||||
|
|
||||||
count(Table) when is_atom(Table) ->
|
count(Table) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count({Table, _}) when is_atom(Table) ->
|
count({Table, _}) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count([Table]) when is_atom(Table) ->
|
count([Table]) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count([{Table, _}]) when is_atom(Table) ->
|
count([{Table, _}]) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
ets:info(Table, size);
|
||||||
|
|
||||||
count(Tables) ->
|
count(Tables) ->
|
||||||
lists:sum([count(T) || T <- Tables]).
|
lists:sum([count(T) || T <- Tables]).
|
||||||
|
|
||||||
|
@ -121,7 +119,7 @@ limit(Params) ->
|
||||||
|
|
||||||
init_meta(Params) ->
|
init_meta(Params) ->
|
||||||
Limit = b2i(limit(Params)),
|
Limit = b2i(limit(Params)),
|
||||||
Page = b2i(page(Params)),
|
Page = b2i(page(Params)),
|
||||||
#{
|
#{
|
||||||
page => Page,
|
page => Page,
|
||||||
limit => Limit,
|
limit => Limit,
|
||||||
|
@ -134,17 +132,24 @@ init_meta(Params) ->
|
||||||
|
|
||||||
node_query(Node, QString, Tab, QSchema, QueryFun) ->
|
node_query(Node, QString, Tab, QSchema, QueryFun) ->
|
||||||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||||
page_limit_check_query( init_meta(QString)
|
page_limit_check_query(
|
||||||
, { fun do_node_query/5
|
init_meta(QString),
|
||||||
, [Node, Tab, NQString, QueryFun, init_meta(QString)]}).
|
{fun do_node_query/5, [Node, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||||
|
).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
|
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
|
||||||
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
||||||
|
|
||||||
do_node_query( Node, Tab, QString, QueryFun, Continuation
|
do_node_query(
|
||||||
, Meta = #{limit := Limit}
|
Node,
|
||||||
, Results) ->
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
Continuation,
|
||||||
|
Meta = #{limit := Limit},
|
||||||
|
Results
|
||||||
|
) ->
|
||||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||||
{error, {badrpc, R}} ->
|
{error, {badrpc, R}} ->
|
||||||
{error, Node, {badrpc, R}};
|
{error, Node, {badrpc, R}};
|
||||||
|
@ -164,18 +169,33 @@ cluster_query(QString, Tab, QSchema, QueryFun) ->
|
||||||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
page_limit_check_query(
|
page_limit_check_query(
|
||||||
init_meta(QString)
|
init_meta(QString),
|
||||||
, {fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}).
|
{fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||||
|
).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
|
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
|
||||||
do_cluster_query( Nodes, Tab, QString, QueryFun
|
do_cluster_query(
|
||||||
, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
Nodes,
|
||||||
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
_Continuation = ?FRESH_SELECT,
|
||||||
|
Meta,
|
||||||
|
_Results = []
|
||||||
|
).
|
||||||
|
|
||||||
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
|
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
|
||||||
#{meta => Meta, data => Results};
|
#{meta => Meta, data => Results};
|
||||||
do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
do_cluster_query(
|
||||||
Meta = #{limit := Limit}, Results) ->
|
[Node | Tail] = Nodes,
|
||||||
|
Tab,
|
||||||
|
QString,
|
||||||
|
QueryFun,
|
||||||
|
Continuation,
|
||||||
|
Meta = #{limit := Limit},
|
||||||
|
Results
|
||||||
|
) ->
|
||||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||||
{error, {badrpc, R}} ->
|
{error, {badrpc, R}} ->
|
||||||
{error, Node, {bar_rpc, R}};
|
{error, Node, {bar_rpc, R}};
|
||||||
|
@ -192,11 +212,18 @@ do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% @private This function is exempt from BPAPI
|
%% @private This function is exempt from BPAPI
|
||||||
do_query(Node, Tab, QString, {M,F}, Continuation, Limit) when Node =:= node() ->
|
do_query(Node, Tab, QString, {M, F}, Continuation, Limit) when Node =:= node() ->
|
||||||
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
|
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
|
||||||
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
|
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
|
||||||
case rpc:call(Node, ?MODULE, do_query,
|
case
|
||||||
[Node, Tab, QString, QueryFun, Continuation, Limit], 50000) of
|
rpc:call(
|
||||||
|
Node,
|
||||||
|
?MODULE,
|
||||||
|
do_query,
|
||||||
|
[Node, Tab, QString, QueryFun, Continuation, Limit],
|
||||||
|
50000
|
||||||
|
)
|
||||||
|
of
|
||||||
{badrpc, _} = R -> {error, R};
|
{badrpc, _} = R -> {error, R};
|
||||||
Ret -> Ret
|
Ret -> Ret
|
||||||
end.
|
end.
|
||||||
|
@ -220,8 +247,9 @@ sub_query_result(Len, Rows, Limit, Results, Meta) ->
|
||||||
%% Table Select
|
%% Table Select
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun) when
|
||||||
when is_function(FuzzyFilterFun) andalso Limit > 0 ->
|
is_function(FuzzyFilterFun) andalso Limit > 0
|
||||||
|
->
|
||||||
case ets:select(Tab, Ms, Limit) of
|
case ets:select(Tab, Ms, Limit) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
@ -229,8 +257,9 @@ select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
||||||
Rows = FuzzyFilterFun(RawResult),
|
Rows = FuzzyFilterFun(RawResult),
|
||||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||||
end;
|
end;
|
||||||
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun)
|
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun) when
|
||||||
when is_function(FuzzyFilterFun) ->
|
is_function(FuzzyFilterFun)
|
||||||
|
->
|
||||||
case ets:select(ets:repair_continuation(Continuation, Ms)) of
|
case ets:select(ets:repair_continuation(Continuation, Ms)) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
@ -238,8 +267,9 @@ select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun
|
||||||
Rows = FuzzyFilterFun(RawResult),
|
Rows = FuzzyFilterFun(RawResult),
|
||||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||||
end;
|
end;
|
||||||
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun)
|
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun) when
|
||||||
when Limit > 0 ->
|
Limit > 0
|
||||||
|
->
|
||||||
case ets:select(Tab, Ms, Limit) of
|
case ets:select(Tab, Ms, Limit) of
|
||||||
'$end_of_table' ->
|
'$end_of_table' ->
|
||||||
{0, [], ?FRESH_SELECT};
|
{0, [], ?FRESH_SELECT};
|
||||||
|
@ -267,36 +297,53 @@ parse_qstring(QString, QSchema) ->
|
||||||
do_parse_qstring([], _, Acc1, Acc2) ->
|
do_parse_qstring([], _, Acc1, Acc2) ->
|
||||||
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
|
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
|
||||||
{lists:reverse(Acc1), lists:reverse(NAcc2)};
|
{lists:reverse(Acc1), lists:reverse(NAcc2)};
|
||||||
|
|
||||||
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
|
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
|
||||||
case proplists:get_value(Key, QSchema) of
|
case proplists:get_value(Key, QSchema) of
|
||||||
undefined -> do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
undefined ->
|
||||||
|
do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
||||||
Type ->
|
Type ->
|
||||||
case Key of
|
case Key of
|
||||||
<<Prefix:4/binary, NKey/binary>>
|
<<Prefix:4/binary, NKey/binary>> when
|
||||||
when Prefix =:= <<"gte_">>;
|
Prefix =:= <<"gte_">>;
|
||||||
Prefix =:= <<"lte_">> ->
|
Prefix =:= <<"lte_">>
|
||||||
OpposeKey = case Prefix of
|
->
|
||||||
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
OpposeKey =
|
||||||
<<"lte_">> -> <<"gte_", NKey/binary>>
|
case Prefix of
|
||||||
end,
|
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
||||||
|
<<"lte_">> -> <<"gte_", NKey/binary>>
|
||||||
|
end,
|
||||||
case lists:keytake(OpposeKey, 1, RestQString) of
|
case lists:keytake(OpposeKey, 1, RestQString) of
|
||||||
false ->
|
false ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, Type) | Acc1], Acc2);
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
);
|
||||||
{value, {K2, V2}, NParams} ->
|
{value, {K2, V2}, NParams} ->
|
||||||
do_parse_qstring( NParams, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, K2, V2, Type) | Acc1], Acc2)
|
NParams,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, K2, V2, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
)
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
case is_fuzzy_key(Key) of
|
case is_fuzzy_key(Key) of
|
||||||
true ->
|
true ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, Acc1, [qs(Key, Value, Type) | Acc2]);
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
Acc1,
|
||||||
|
[qs(Key, Value, Type) | Acc2]
|
||||||
|
);
|
||||||
_ ->
|
_ ->
|
||||||
do_parse_qstring( RestQString, QSchema
|
do_parse_qstring(
|
||||||
, [qs(Key, Value, Type) | Acc1], Acc2)
|
RestQString,
|
||||||
|
QSchema,
|
||||||
|
[qs(Key, Value, Type) | Acc1],
|
||||||
|
Acc2
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
@ -310,7 +357,7 @@ qs(K, Value0, Type) ->
|
||||||
try
|
try
|
||||||
qs(K, to_type(Value0, Type))
|
qs(K, to_type(Value0, Type))
|
||||||
catch
|
catch
|
||||||
throw : bad_value_type ->
|
throw:bad_value_type ->
|
||||||
throw({bad_value_type, {K, Type, Value0}})
|
throw({bad_value_type, {K, Type, Value0}})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -333,12 +380,11 @@ is_fuzzy_key(_) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
page_start(1, _) -> 1;
|
page_start(1, _) -> 1;
|
||||||
page_start(Page, Limit) -> (Page-1) * Limit + 1.
|
page_start(Page, Limit) -> (Page - 1) * Limit + 1.
|
||||||
|
|
||||||
|
|
||||||
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
|
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
PageStart = page_start(Page, Limit),
|
PageStart = page_start(Page, Limit),
|
||||||
PageEnd = Page * Limit,
|
PageEnd = Page * Limit,
|
||||||
case Count + Len of
|
case Count + Len of
|
||||||
NCount when NCount < PageStart ->
|
NCount when NCount < PageStart ->
|
||||||
{more, Meta#{count => NCount}};
|
{more, Meta#{count => NCount}};
|
||||||
|
@ -353,7 +399,7 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
case (Count - Len) < PageStart of
|
case (Count - Len) < PageStart of
|
||||||
true ->
|
true ->
|
||||||
NeedNowNum = Count - PageStart + 1,
|
NeedNowNum = Count - PageStart + 1,
|
||||||
SubStart = Len - NeedNowNum + 1,
|
SubStart = Len - NeedNowNum + 1,
|
||||||
{SubStart, NeedNowNum};
|
{SubStart, NeedNowNum};
|
||||||
false ->
|
false ->
|
||||||
{_SubStart = 1, _NeedNowNum = Len}
|
{_SubStart = 1, _NeedNowNum = Len}
|
||||||
|
@ -361,8 +407,9 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||||
|
|
||||||
page_limit_check_query(Meta, {F, A}) ->
|
page_limit_check_query(Meta, {F, A}) ->
|
||||||
case Meta of
|
case Meta of
|
||||||
#{page := Page, limit := Limit}
|
#{page := Page, limit := Limit} when
|
||||||
when Page < 1; Limit < 1 ->
|
Page < 1; Limit < 1
|
||||||
|
->
|
||||||
{error, page_limit_invalid};
|
{error, page_limit_invalid};
|
||||||
_ ->
|
_ ->
|
||||||
erlang:apply(F, A)
|
erlang:apply(F, A)
|
||||||
|
@ -376,7 +423,7 @@ to_type(V, TargetType) ->
|
||||||
try
|
try
|
||||||
to_type_(V, TargetType)
|
to_type_(V, TargetType)
|
||||||
catch
|
catch
|
||||||
_ : _ ->
|
_:_ ->
|
||||||
throw(bad_value_type)
|
throw(bad_value_type)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -419,37 +466,43 @@ to_ip_port(IPAddress) ->
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
params2qs_test() ->
|
params2qs_test() ->
|
||||||
QSchema = [{<<"str">>, binary},
|
QSchema = [
|
||||||
{<<"int">>, integer},
|
{<<"str">>, binary},
|
||||||
{<<"atom">>, atom},
|
{<<"int">>, integer},
|
||||||
{<<"ts">>, timestamp},
|
{<<"atom">>, atom},
|
||||||
{<<"gte_range">>, integer},
|
{<<"ts">>, timestamp},
|
||||||
{<<"lte_range">>, integer},
|
{<<"gte_range">>, integer},
|
||||||
{<<"like_fuzzy">>, binary},
|
{<<"lte_range">>, integer},
|
||||||
{<<"match_topic">>, binary}],
|
{<<"like_fuzzy">>, binary},
|
||||||
QString = [{<<"str">>, <<"abc">>},
|
{<<"match_topic">>, binary}
|
||||||
{<<"int">>, <<"123">>},
|
],
|
||||||
{<<"atom">>, <<"connected">>},
|
QString = [
|
||||||
{<<"ts">>, <<"156000">>},
|
{<<"str">>, <<"abc">>},
|
||||||
{<<"gte_range">>, <<"1">>},
|
{<<"int">>, <<"123">>},
|
||||||
{<<"lte_range">>, <<"5">>},
|
{<<"atom">>, <<"connected">>},
|
||||||
{<<"like_fuzzy">>, <<"user">>},
|
{<<"ts">>, <<"156000">>},
|
||||||
{<<"match_topic">>, <<"t/#">>}],
|
{<<"gte_range">>, <<"1">>},
|
||||||
ExpectedQs = [{str, '=:=', <<"abc">>},
|
{<<"lte_range">>, <<"5">>},
|
||||||
{int, '=:=', 123},
|
{<<"like_fuzzy">>, <<"user">>},
|
||||||
{atom, '=:=', connected},
|
{<<"match_topic">>, <<"t/#">>}
|
||||||
{ts, '=:=', 156000},
|
],
|
||||||
{range, '>=', 1, '=<', 5}
|
ExpectedQs = [
|
||||||
],
|
{str, '=:=', <<"abc">>},
|
||||||
FuzzyNQString = [{fuzzy, like, <<"user">>},
|
{int, '=:=', 123},
|
||||||
{topic, match, <<"t/#">>}],
|
{atom, '=:=', connected},
|
||||||
|
{ts, '=:=', 156000},
|
||||||
|
{range, '>=', 1, '=<', 5}
|
||||||
|
],
|
||||||
|
FuzzyNQString = [
|
||||||
|
{fuzzy, like, <<"user">>},
|
||||||
|
{topic, match, <<"t/#">>}
|
||||||
|
],
|
||||||
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
|
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
|
||||||
|
|
||||||
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
|
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
|
||||||
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
|
|
||||||
b2i(Bin) when is_binary(Bin) ->
|
b2i(Bin) when is_binary(Bin) ->
|
||||||
binary_to_integer(Bin);
|
binary_to_integer(Bin);
|
||||||
b2i(Any) ->
|
b2i(Any) ->
|
||||||
|
|
|
@ -43,9 +43,12 @@ schema("/alarms") ->
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{activated, hoconsc:mk(boolean(), #{in => query,
|
{activated,
|
||||||
desc => ?DESC(get_alarms_qs_activated),
|
hoconsc:mk(boolean(), #{
|
||||||
required => false})}
|
in => query,
|
||||||
|
desc => ?DESC(get_alarms_qs_activated),
|
||||||
|
required => false
|
||||||
|
})}
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [
|
200 => [
|
||||||
|
@ -54,7 +57,7 @@ schema("/alarms") ->
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
description => ?DESC(delete_alarms_api),
|
description => ?DESC(delete_alarms_api),
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => ?DESC(delete_alarms_api_response204)
|
204 => ?DESC(delete_alarms_api_response204)
|
||||||
|
@ -64,21 +67,38 @@ schema("/alarms") ->
|
||||||
|
|
||||||
fields(alarm) ->
|
fields(alarm) ->
|
||||||
[
|
[
|
||||||
{node, hoconsc:mk(binary(),
|
{node,
|
||||||
#{desc => ?DESC(node), example => atom_to_list(node())})},
|
hoconsc:mk(
|
||||||
{name, hoconsc:mk(binary(),
|
binary(),
|
||||||
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>})},
|
#{desc => ?DESC(node), example => atom_to_list(node())}
|
||||||
{message, hoconsc:mk(binary(), #{desc => ?DESC(message),
|
)},
|
||||||
example => <<"System memory usage is higher than 70%">>})},
|
{name,
|
||||||
{details, hoconsc:mk(map(), #{desc => ?DESC(details),
|
hoconsc:mk(
|
||||||
example => #{<<"high_watermark">> => 70}})},
|
binary(),
|
||||||
|
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>}
|
||||||
|
)},
|
||||||
|
{message,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => ?DESC(message),
|
||||||
|
example => <<"System memory usage is higher than 70%">>
|
||||||
|
})},
|
||||||
|
{details,
|
||||||
|
hoconsc:mk(map(), #{
|
||||||
|
desc => ?DESC(details),
|
||||||
|
example => #{<<"high_watermark">> => 70}
|
||||||
|
})},
|
||||||
{duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
|
{duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
|
||||||
{activate_at, hoconsc:mk(binary(), #{desc => ?DESC(activate_at),
|
{activate_at,
|
||||||
example => <<"2021-10-25T11:52:52.548+08:00">>})},
|
hoconsc:mk(binary(), #{
|
||||||
{deactivate_at, hoconsc:mk(binary(), #{desc => ?DESC(deactivate_at),
|
desc => ?DESC(activate_at),
|
||||||
example => <<"2021-10-31T10:52:52.548+08:00">>})}
|
example => <<"2021-10-25T11:52:52.548+08:00">>
|
||||||
|
})},
|
||||||
|
{deactivate_at,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => ?DESC(deactivate_at),
|
||||||
|
example => <<"2021-10-31T10:52:52.548+08:00">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
emqx_dashboard_swagger:fields(limit) ++
|
emqx_dashboard_swagger:fields(limit) ++
|
||||||
|
@ -100,7 +120,6 @@ alarms(get, #{query_string := QString}) ->
|
||||||
Response ->
|
Response ->
|
||||||
{200, Response}
|
{200, Response}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
alarms(delete, _Params) ->
|
alarms(delete, _Params) ->
|
||||||
_ = emqx_mgmt:delete_all_deactivated_alarms(),
|
_ = emqx_mgmt:delete_all_deactivated_alarms(),
|
||||||
{204}.
|
{204}.
|
||||||
|
@ -109,11 +128,10 @@ alarms(delete, _Params) ->
|
||||||
%% internal
|
%% internal
|
||||||
|
|
||||||
query(Table, _QsSpec, Continuation, Limit) ->
|
query(Table, _QsSpec, Continuation, Limit) ->
|
||||||
Ms = [{'$1',[],['$1']}],
|
Ms = [{'$1', [], ['$1']}],
|
||||||
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
|
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
|
||||||
|
|
||||||
format_alarm(Alarms) when is_list(Alarms) ->
|
format_alarm(Alarms) when is_list(Alarms) ->
|
||||||
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
|
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
|
||||||
|
|
||||||
format_alarm(Alarm) ->
|
format_alarm(Alarm) ->
|
||||||
emqx_alarm:format(Alarm).
|
emqx_alarm:format(Alarm).
|
||||||
|
|
|
@ -31,7 +31,6 @@ api_spec() ->
|
||||||
paths() ->
|
paths() ->
|
||||||
["/api_key", "/api_key/:name"].
|
["/api_key", "/api_key/:name"].
|
||||||
|
|
||||||
|
|
||||||
schema("/api_key") ->
|
schema("/api_key") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => api_key,
|
'operationId' => api_key,
|
||||||
|
@ -82,41 +81,80 @@ schema("/api_key/:name") ->
|
||||||
|
|
||||||
fields(app) ->
|
fields(app) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
hoconsc:mk(
|
||||||
validator => fun ?MODULE:validate_name/1,
|
binary(),
|
||||||
example => <<"EMQX-API-KEY-1">>})},
|
#{
|
||||||
{api_key, hoconsc:mk(binary(),
|
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||||
#{desc => """TODO:uses HMAC-SHA256 for signing.""",
|
validator => fun ?MODULE:validate_name/1,
|
||||||
example => <<"a4697a5c75a769f6">>})},
|
example => <<"EMQX-API-KEY-1">>
|
||||||
{api_secret, hoconsc:mk(binary(),
|
}
|
||||||
#{desc => """An API secret is a simple encrypted string that identifies"""
|
)},
|
||||||
"""an application without any principal."""
|
{api_key,
|
||||||
"""They are useful for accessing public data anonymously,"""
|
hoconsc:mk(
|
||||||
"""and are used to associate API requests.""",
|
binary(),
|
||||||
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>})},
|
#{
|
||||||
{expired_at, hoconsc:mk(hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
desc => "" "TODO:uses HMAC-SHA256 for signing." "",
|
||||||
#{desc => "No longer valid datetime",
|
example => <<"a4697a5c75a769f6">>
|
||||||
example => <<"2021-12-05T02:01:34.186Z">>,
|
}
|
||||||
required => false,
|
)},
|
||||||
default => undefined
|
{api_secret,
|
||||||
})},
|
hoconsc:mk(
|
||||||
{created_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
binary(),
|
||||||
#{desc => "ApiKey create datetime",
|
#{
|
||||||
example => <<"2021-12-01T00:00:00.000Z">>
|
desc =>
|
||||||
})},
|
""
|
||||||
{desc, hoconsc:mk(binary(),
|
"An API secret is a simple encrypted string that identifies"
|
||||||
#{example => <<"Note">>, required => false})},
|
""
|
||||||
|
""
|
||||||
|
"an application without any principal."
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"They are useful for accessing public data anonymously,"
|
||||||
|
""
|
||||||
|
""
|
||||||
|
"and are used to associate API requests."
|
||||||
|
"",
|
||||||
|
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{expired_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
||||||
|
#{
|
||||||
|
desc => "No longer valid datetime",
|
||||||
|
example => <<"2021-12-05T02:01:34.186Z">>,
|
||||||
|
required => false,
|
||||||
|
default => undefined
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{created_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
emqx_datetime:epoch_second(),
|
||||||
|
#{
|
||||||
|
desc => "ApiKey create datetime",
|
||||||
|
example => <<"2021-12-01T00:00:00.000Z">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{desc,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{example => <<"Note">>, required => false}
|
||||||
|
)},
|
||||||
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
|
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
hoconsc:mk(
|
||||||
example => <<"EMQX-API-KEY-1">>,
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
||||||
})}
|
example => <<"EMQX-API-KEY-1">>,
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||||
|
@ -129,7 +167,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete(Keys, Fields) ->
|
delete(Keys, Fields) ->
|
||||||
|
@ -146,10 +185,13 @@ api_key(post, #{body := App}) ->
|
||||||
ExpiredAt = ensure_expired_at(App),
|
ExpiredAt = ensure_expired_at(App),
|
||||||
Desc = unicode:characters_to_binary(Desc0, unicode),
|
Desc = unicode:characters_to_binary(Desc0, unicode),
|
||||||
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
|
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
|
||||||
{ok, NewApp} -> {200, format(NewApp)};
|
{ok, NewApp} ->
|
||||||
|
{200, format(NewApp)};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => 'BAD_REQUEST',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'BAD_REQUEST',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
|
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
|
||||||
|
@ -184,5 +226,5 @@ format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
|
||||||
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
|
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
|
||||||
}.
|
}.
|
||||||
|
|
||||||
ensure_expired_at(#{<<"expired_at">> := ExpiredAt})when is_integer(ExpiredAt) -> ExpiredAt;
|
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
|
||||||
ensure_expired_at(_) -> undefined.
|
ensure_expired_at(_) -> undefined.
|
||||||
|
|
|
@ -24,16 +24,19 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([format/1]).
|
-export([format/1]).
|
||||||
|
|
||||||
-export([ banned/2
|
-export([
|
||||||
, delete_banned/2
|
banned/2,
|
||||||
]).
|
delete_banned/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TAB, emqx_banned).
|
-define(TAB, emqx_banned).
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ paths() ->
|
||||||
|
|
||||||
schema("/banned") ->
|
schema("/banned") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => banned,
|
'operationId' => banned,
|
||||||
get => #{
|
get => #{
|
||||||
description => ?DESC(list_banned_api),
|
description => ?DESC(list_banned_api),
|
||||||
parameters => [
|
parameters => [
|
||||||
|
@ -57,7 +60,7 @@ schema("/banned") ->
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit)
|
hoconsc:ref(emqx_dashboard_swagger, limit)
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>[
|
200 => [
|
||||||
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
|
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
|
||||||
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
|
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
|
||||||
]
|
]
|
||||||
|
@ -69,8 +72,9 @@ schema("/banned") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
|
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
|
||||||
400 => emqx_dashboard_swagger:error_codes(
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
||||||
?DESC(create_banned_api_response400))
|
?DESC(create_banned_api_response400)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -80,51 +84,67 @@ schema("/banned/:as/:who") ->
|
||||||
delete => #{
|
delete => #{
|
||||||
description => ?DESC(delete_banned_api),
|
description => ?DESC(delete_banned_api),
|
||||||
parameters => [
|
parameters => [
|
||||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
{as,
|
||||||
desc => ?DESC(as),
|
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||||
in => path,
|
desc => ?DESC(as),
|
||||||
example => username})},
|
in => path,
|
||||||
{who, hoconsc:mk(binary(), #{
|
example => username
|
||||||
desc => ?DESC(who),
|
})},
|
||||||
in => path,
|
{who,
|
||||||
example => <<"Badass">>})}
|
hoconsc:mk(binary(), #{
|
||||||
],
|
desc => ?DESC(who),
|
||||||
|
in => path,
|
||||||
|
example => <<"Badass">>
|
||||||
|
})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Delete banned success">>,
|
204 => <<"Delete banned success">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['NOT_FOUND'],
|
['NOT_FOUND'],
|
||||||
?DESC(delete_banned_api_response404))
|
?DESC(delete_banned_api_response404)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(ban) ->
|
fields(ban) ->
|
||||||
[
|
[
|
||||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
{as,
|
||||||
desc => ?DESC(as),
|
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||||
required => true,
|
desc => ?DESC(as),
|
||||||
example => username})},
|
required => true,
|
||||||
{who, hoconsc:mk(binary(), #{
|
example => username
|
||||||
desc => ?DESC(who),
|
})},
|
||||||
required => true,
|
{who,
|
||||||
example => <<"Banned name"/utf8>>})},
|
hoconsc:mk(binary(), #{
|
||||||
{by, hoconsc:mk(binary(), #{
|
desc => ?DESC(who),
|
||||||
desc => ?DESC(by),
|
required => true,
|
||||||
required => false,
|
example => <<"Banned name"/utf8>>
|
||||||
example => <<"mgmt_api">>})},
|
})},
|
||||||
{reason, hoconsc:mk(binary(), #{
|
{by,
|
||||||
desc => ?DESC(reason),
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
desc => ?DESC(by),
|
||||||
example => <<"Too many requests">>})},
|
required => false,
|
||||||
{at, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
example => <<"mgmt_api">>
|
||||||
desc => ?DESC(at),
|
})},
|
||||||
required => false,
|
{reason,
|
||||||
example => <<"2021-10-25T21:48:47+08:00">>})},
|
hoconsc:mk(binary(), #{
|
||||||
{until, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
desc => ?DESC(reason),
|
||||||
desc => ?DESC(until),
|
required => false,
|
||||||
required => false,
|
example => <<"Too many requests">>
|
||||||
example => <<"2021-10-25T21:53:47+08:00">>})
|
})},
|
||||||
}
|
{at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||||
|
desc => ?DESC(at),
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-10-25T21:48:47+08:00">>
|
||||||
|
})},
|
||||||
|
{until,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||||
|
desc => ?DESC(until),
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-10-25T21:53:47+08:00">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
|
@ -141,8 +161,7 @@ banned(post, #{body := Body}) ->
|
||||||
Ban ->
|
Ban ->
|
||||||
case emqx_banned:create(Ban) of
|
case emqx_banned:create(Ban) of
|
||||||
{ok, Banned} -> {200, format(Banned)};
|
{ok, Banned} -> {200, format(Banned)};
|
||||||
{error, {already_exist, Old}} ->
|
{error, {already_exist, Old}} -> {400, 'ALREADY_EXISTS', format(Old)}
|
||||||
{400, 'ALREADY_EXISTS', format(Old)}
|
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
|
@ -26,63 +26,70 @@
|
||||||
-include("emqx_mgmt.hrl").
|
-include("emqx_mgmt.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ clients/2
|
-export([
|
||||||
, client/2
|
clients/2,
|
||||||
, subscriptions/2
|
client/2,
|
||||||
, authz_cache/2
|
subscriptions/2,
|
||||||
, subscribe/2
|
authz_cache/2,
|
||||||
, unsubscribe/2
|
subscribe/2,
|
||||||
, subscribe_batch/2
|
unsubscribe/2,
|
||||||
, set_keepalive/2
|
subscribe_batch/2,
|
||||||
]).
|
set_keepalive/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format_channel_info/1
|
query/4,
|
||||||
]).
|
format_channel_info/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% for batch operation
|
%% for batch operation
|
||||||
-export([do_subscribe/3]).
|
-export([do_subscribe/3]).
|
||||||
|
|
||||||
-define(CLIENT_QTAB, emqx_channel_info).
|
-define(CLIENT_QTAB, emqx_channel_info).
|
||||||
|
|
||||||
-define(CLIENT_QSCHEMA,
|
-define(CLIENT_QSCHEMA, [
|
||||||
[ {<<"node">>, atom}
|
{<<"node">>, atom},
|
||||||
, {<<"username">>, binary}
|
{<<"username">>, binary},
|
||||||
, {<<"zone">>, atom}
|
{<<"zone">>, atom},
|
||||||
, {<<"ip_address">>, ip}
|
{<<"ip_address">>, ip},
|
||||||
, {<<"conn_state">>, atom}
|
{<<"conn_state">>, atom},
|
||||||
, {<<"clean_start">>, atom}
|
{<<"clean_start">>, atom},
|
||||||
, {<<"proto_name">>, binary}
|
{<<"proto_name">>, binary},
|
||||||
, {<<"proto_ver">>, integer}
|
{<<"proto_ver">>, integer},
|
||||||
, {<<"like_clientid">>, binary}
|
{<<"like_clientid">>, binary},
|
||||||
, {<<"like_username">>, binary}
|
{<<"like_username">>, binary},
|
||||||
, {<<"gte_created_at">>, timestamp}
|
{<<"gte_created_at">>, timestamp},
|
||||||
, {<<"lte_created_at">>, timestamp}
|
{<<"lte_created_at">>, timestamp},
|
||||||
, {<<"gte_connected_at">>, timestamp}
|
{<<"gte_connected_at">>, timestamp},
|
||||||
, {<<"lte_connected_at">>, timestamp}]).
|
{<<"lte_connected_at">>, timestamp}
|
||||||
|
]).
|
||||||
|
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
|
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
|
||||||
|
|
||||||
-define(CLIENT_ID_NOT_FOUND,
|
-define(CLIENT_ID_NOT_FOUND,
|
||||||
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>).
|
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>
|
||||||
|
).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/clients"
|
[
|
||||||
, "/clients/:clientid"
|
"/clients",
|
||||||
, "/clients/:clientid/authorization/cache"
|
"/clients/:clientid",
|
||||||
, "/clients/:clientid/subscriptions"
|
"/clients/:clientid/authorization/cache",
|
||||||
, "/clients/:clientid/subscribe"
|
"/clients/:clientid/subscriptions",
|
||||||
, "/clients/:clientid/unsubscribe"
|
"/clients/:clientid/subscribe",
|
||||||
, "/clients/:clientid/keepalive"
|
"/clients/:clientid/unsubscribe",
|
||||||
|
"/clients/:clientid/keepalive"
|
||||||
].
|
].
|
||||||
|
|
||||||
schema("/clients") ->
|
schema("/clients") ->
|
||||||
|
@ -93,69 +100,105 @@ schema("/clients") ->
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{node, hoconsc:mk(binary(), #{
|
{node,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Node name">>,
|
required => false,
|
||||||
example => atom_to_list(node())})},
|
desc => <<"Node name">>,
|
||||||
{username, hoconsc:mk(binary(), #{
|
example => atom_to_list(node())
|
||||||
in => query,
|
})},
|
||||||
required => false,
|
{username,
|
||||||
desc => <<"User name">>})},
|
hoconsc:mk(binary(), #{
|
||||||
{zone, hoconsc:mk(binary(), #{
|
in => query,
|
||||||
in => query,
|
required => false,
|
||||||
required => false})},
|
desc => <<"User name">>
|
||||||
{ip_address, hoconsc:mk(binary(), #{
|
})},
|
||||||
in => query,
|
{zone,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Client's IP address">>,
|
in => query,
|
||||||
example => <<"127.0.0.1">>})},
|
required => false
|
||||||
{conn_state, hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
})},
|
||||||
in => query,
|
{ip_address,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"The current connection status of the client, ",
|
in => query,
|
||||||
"the possible values are connected,idle,disconnected">>})},
|
required => false,
|
||||||
{clean_start, hoconsc:mk(boolean(), #{
|
desc => <<"Client's IP address">>,
|
||||||
in => query,
|
example => <<"127.0.0.1">>
|
||||||
required => false,
|
})},
|
||||||
description => <<"Whether the client uses a new session">>})},
|
{conn_state,
|
||||||
{proto_name, hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
description => <<"Client protocol name, ",
|
desc =>
|
||||||
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>})},
|
<<"The current connection status of the client, ",
|
||||||
{proto_ver, hoconsc:mk(binary(), #{
|
"the possible values are connected,idle,disconnected">>
|
||||||
in => query,
|
})},
|
||||||
required => false,
|
{clean_start,
|
||||||
desc => <<"Client protocol version">>})},
|
hoconsc:mk(boolean(), #{
|
||||||
{like_clientid, hoconsc:mk(binary(), #{
|
in => query,
|
||||||
in => query,
|
required => false,
|
||||||
required => false,
|
description => <<"Whether the client uses a new session">>
|
||||||
desc => <<"Fuzzy search `clientid` as substring">>})},
|
})},
|
||||||
{like_username, hoconsc:mk(binary(), #{
|
{proto_name,
|
||||||
in => query,
|
hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Fuzzy search `username` as substring">>})},
|
required => false,
|
||||||
{gte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
description =>
|
||||||
in => query,
|
<<"Client protocol name, ",
|
||||||
required => false,
|
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>
|
||||||
desc => <<"Search client session creation time by greater",
|
})},
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
{proto_ver,
|
||||||
{lte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
hoconsc:mk(binary(), #{
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
desc => <<"Search client session creation time by less",
|
desc => <<"Client protocol version">>
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
})},
|
||||||
{gte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
{like_clientid,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Search client connection creation time by greater"
|
required => false,
|
||||||
" than or equal method, rfc3339 or timestamp(epoch millisecond)">>})},
|
desc => <<"Fuzzy search `clientid` as substring">>
|
||||||
{lte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
})},
|
||||||
in => query,
|
{like_username,
|
||||||
required => false,
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Search client connection creation time by less"
|
in => query,
|
||||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})}
|
required => false,
|
||||||
|
desc => <<"Fuzzy search `username` as substring">>
|
||||||
|
})},
|
||||||
|
{gte_created_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc =>
|
||||||
|
<<"Search client session creation time by greater",
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||||
|
})},
|
||||||
|
{lte_created_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc =>
|
||||||
|
<<"Search client session creation time by less",
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||||
|
})},
|
||||||
|
{gte_connected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc => <<
|
||||||
|
"Search client connection creation time by greater"
|
||||||
|
" than or equal method, rfc3339 or timestamp(epoch millisecond)"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{lte_connected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
desc => <<
|
||||||
|
"Search client connection creation time by less"
|
||||||
|
" than or equal method, rfc3339 or timestamp(millisecond)"
|
||||||
|
>>
|
||||||
|
})}
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => [
|
200 => [
|
||||||
|
@ -164,10 +207,11 @@ schema("/clients") ->
|
||||||
],
|
],
|
||||||
400 =>
|
400 =>
|
||||||
emqx_dashboard_swagger:error_codes(
|
emqx_dashboard_swagger:error_codes(
|
||||||
['INVALID_PARAMETER'], <<"Invalid parameters">>)}
|
['INVALID_PARAMETER'], <<"Invalid parameters">>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid") ->
|
schema("/clients/:clientid") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => client,
|
'operationId' => client,
|
||||||
|
@ -177,19 +221,23 @@ schema("/clients/:clientid") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)}},
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
description => <<"Kick out client by client ID">>,
|
description => <<"Kick out client by client ID">>,
|
||||||
parameters => [
|
parameters => [
|
||||||
{clientid, hoconsc:mk(binary(), #{in => path})}],
|
{clientid, hoconsc:mk(binary(), #{in => path})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Kick out client successfully">>,
|
204 => <<"Kick out client successfully">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/authorization/cache") ->
|
schema("/clients/:clientid/authorization/cache") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => authz_cache,
|
'operationId' => authz_cache,
|
||||||
|
@ -199,7 +247,8 @@ schema("/clients/:clientid/authorization/cache") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
|
@ -208,11 +257,11 @@ schema("/clients/:clientid/authorization/cache") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Kick out client successfully">>,
|
204 => <<"Kick out client successfully">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/subscriptions") ->
|
schema("/clients/:clientid/subscriptions") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => subscriptions,
|
'operationId' => subscriptions,
|
||||||
|
@ -220,13 +269,15 @@ schema("/clients/:clientid/subscriptions") ->
|
||||||
description => <<"Get client subscriptions">>,
|
description => <<"Get client subscriptions">>,
|
||||||
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}),
|
200 => hoconsc:mk(
|
||||||
|
hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}
|
||||||
|
),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/subscribe") ->
|
schema("/clients/:clientid/subscribe") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => subscribe,
|
'operationId' => subscribe,
|
||||||
|
@ -237,11 +288,11 @@ schema("/clients/:clientid/subscribe") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
|
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/unsubscribe") ->
|
schema("/clients/:clientid/unsubscribe") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => unsubscribe,
|
'operationId' => unsubscribe,
|
||||||
|
@ -252,11 +303,11 @@ schema("/clients/:clientid/unsubscribe") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
204 => <<"Unsubscribe OK">>,
|
204 => <<"Unsubscribe OK">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/clients/:clientid/keepalive") ->
|
schema("/clients/:clientid/keepalive") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => set_keepalive,
|
'operationId' => set_keepalive,
|
||||||
|
@ -267,96 +318,187 @@ schema("/clients/:clientid/keepalive") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||||
404 => emqx_dashboard_swagger:error_codes(
|
404 => emqx_dashboard_swagger:error_codes(
|
||||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(client) ->
|
fields(client) ->
|
||||||
[
|
[
|
||||||
{awaiting_rel_cnt, hoconsc:mk(integer(), #{desc =>
|
{awaiting_rel_cnt,
|
||||||
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{awaiting_rel_max, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"v4 api name [max_awaiting_rel]. "
|
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>
|
||||||
"Maximum allowed number of awaiting PUBREC packet">>})},
|
})},
|
||||||
{clean_start, hoconsc:mk(boolean(), #{desc =>
|
{awaiting_rel_max,
|
||||||
<<"Indicate whether the client is using a brand new session">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<
|
||||||
|
"v4 api name [max_awaiting_rel]. "
|
||||||
|
"Maximum allowed number of awaiting PUBREC packet"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{clean_start,
|
||||||
|
hoconsc:mk(boolean(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Indicate whether the client is using a brand new session">>
|
||||||
|
})},
|
||||||
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
|
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
|
||||||
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
|
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
|
||||||
{connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
{connected_at,
|
||||||
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>})},
|
hoconsc:mk(
|
||||||
{created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
emqx_datetime:epoch_millisecond(),
|
||||||
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>})},
|
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>}
|
||||||
{disconnected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{desc =>
|
)},
|
||||||
<<"Client offline time."
|
{created_at,
|
||||||
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)">>})},
|
hoconsc:mk(
|
||||||
{expiry_interval, hoconsc:mk(integer(), #{desc =>
|
emqx_datetime:epoch_millisecond(),
|
||||||
<<"Session expiration interval, with the unit of second">>})},
|
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>}
|
||||||
{heap_size, hoconsc:mk(integer(), #{desc =>
|
)},
|
||||||
<<"Process heap size with the unit of byte">>})},
|
{disconnected_at,
|
||||||
|
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||||
|
desc =>
|
||||||
|
<<
|
||||||
|
"Client offline time."
|
||||||
|
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)"
|
||||||
|
>>
|
||||||
|
})},
|
||||||
|
{expiry_interval,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Session expiration interval, with the unit of second">>
|
||||||
|
})},
|
||||||
|
{heap_size,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Process heap size with the unit of byte">>
|
||||||
|
})},
|
||||||
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
|
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
|
||||||
{inflight_max, hoconsc:mk(integer(), #{desc =>
|
{inflight_max,
|
||||||
<<"v4 api name [max_inflight]. Maximum length of inflight">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"v4 api name [max_inflight]. Maximum length of inflight">>
|
||||||
|
})},
|
||||||
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
|
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
|
||||||
{is_bridge, hoconsc:mk(boolean(), #{desc =>
|
{is_bridge,
|
||||||
<<"Indicates whether the client is connectedvia bridge">>})},
|
hoconsc:mk(boolean(), #{
|
||||||
{keepalive, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"keepalive time, with the unit of second">>})},
|
<<"Indicates whether the client is connectedvia bridge">>
|
||||||
|
})},
|
||||||
|
{keepalive,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"keepalive time, with the unit of second">>
|
||||||
|
})},
|
||||||
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
|
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
|
||||||
{mqueue_dropped, hoconsc:mk(integer(), #{desc =>
|
{mqueue_dropped,
|
||||||
<<"Number of messages dropped by the message queue due to exceeding the length">>})},
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of messages dropped by the message queue due to exceeding the length">>
|
||||||
|
})},
|
||||||
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
|
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
|
||||||
{mqueue_max, hoconsc:mk(integer(), #{desc =>
|
{mqueue_max,
|
||||||
<<"v4 api name [max_mqueue]. Maximum length of message queue">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{node, hoconsc:mk(binary(), #{desc =>
|
desc =>
|
||||||
<<"Name of the node to which the client is connected">>})},
|
<<"v4 api name [max_mqueue]. Maximum length of message queue">>
|
||||||
|
})},
|
||||||
|
{node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Name of the node to which the client is connected">>
|
||||||
|
})},
|
||||||
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
|
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
|
||||||
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
|
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
|
||||||
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
|
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
|
||||||
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
|
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
|
||||||
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
|
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
|
||||||
{'recv_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
{'recv_msg.dropped',
|
||||||
<<"Number of dropped PUBLISH packets">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'recv_msg.dropped.await_pubrel_timeout', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
<<"Number of dropped PUBLISH packets">>
|
||||||
{'recv_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
})},
|
||||||
<<"Number of PUBLISH QoS0 packets received">>})},
|
{'recv_msg.dropped.await_pubrel_timeout',
|
||||||
{'recv_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
hoconsc:mk(integer(), #{
|
||||||
<<"Number of PUBLISH QoS1 packets received">>})},
|
desc =>
|
||||||
{'recv_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
<<"Number of dropped PUBLISH packets due to expired">>
|
||||||
<<"Number of PUBLISH QoS2 packets received">>})},
|
})},
|
||||||
|
{'recv_msg.qos0',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS0 packets received">>
|
||||||
|
})},
|
||||||
|
{'recv_msg.qos1',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS1 packets received">>
|
||||||
|
})},
|
||||||
|
{'recv_msg.qos2',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS2 packets received">>
|
||||||
|
})},
|
||||||
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
|
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
|
||||||
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
|
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
|
||||||
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
|
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
|
||||||
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
|
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
|
||||||
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
|
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
|
||||||
{'send_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
{'send_msg.dropped',
|
||||||
<<"Number of dropped PUBLISH packets">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'send_msg.dropped.expired', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
<<"Number of dropped PUBLISH packets">>
|
||||||
{'send_msg.dropped.queue_full', hoconsc:mk(integer(), #{desc =>
|
})},
|
||||||
<<"Number of dropped PUBLISH packets due to queue full">>})},
|
{'send_msg.dropped.expired',
|
||||||
{'send_msg.dropped.too_large', hoconsc:mk(integer(), #{desc =>
|
hoconsc:mk(integer(), #{
|
||||||
<<"Number of dropped PUBLISH packets due to packet length too large">>})},
|
desc =>
|
||||||
{'send_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
<<"Number of dropped PUBLISH packets due to expired">>
|
||||||
<<"Number of PUBLISH QoS0 packets sent">>})},
|
})},
|
||||||
{'send_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
{'send_msg.dropped.queue_full',
|
||||||
<<"Number of PUBLISH QoS1 packets sent">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{'send_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"Number of PUBLISH QoS2 packets sent">>})},
|
<<"Number of dropped PUBLISH packets due to queue full">>
|
||||||
|
})},
|
||||||
|
{'send_msg.dropped.too_large',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of dropped PUBLISH packets due to packet length too large">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos0',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS0 packets sent">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos1',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS1 packets sent">>
|
||||||
|
})},
|
||||||
|
{'send_msg.qos2',
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Number of PUBLISH QoS2 packets sent">>
|
||||||
|
})},
|
||||||
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
|
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
|
||||||
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
|
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
|
||||||
{subscriptions_cnt, hoconsc:mk(integer(), #{desc =>
|
{subscriptions_cnt,
|
||||||
<<"Number of subscriptions established by this client.">>})},
|
hoconsc:mk(integer(), #{
|
||||||
{subscriptions_max, hoconsc:mk(integer(), #{desc =>
|
desc =>
|
||||||
<<"v4 api name [max_subscriptions]",
|
<<"Number of subscriptions established by this client.">>
|
||||||
" Maximum number of subscriptions allowed by this client">>})},
|
})},
|
||||||
|
{subscriptions_max,
|
||||||
|
hoconsc:mk(integer(), #{
|
||||||
|
desc =>
|
||||||
|
<<"v4 api name [max_subscriptions]",
|
||||||
|
" Maximum number of subscriptions allowed by this client">>
|
||||||
|
})},
|
||||||
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
|
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
|
||||||
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
|
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
|
||||||
{zone, hoconsc:mk(binary(), #{desc =>
|
{zone,
|
||||||
<<"Indicate the configuration group used by the client">>})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc =>
|
||||||
|
<<"Indicate the configuration group used by the client">>
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(authz_cache) ->
|
fields(authz_cache) ->
|
||||||
[
|
[
|
||||||
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
|
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
|
||||||
|
@ -364,23 +506,19 @@ fields(authz_cache) ->
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
|
||||||
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
|
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(keepalive) ->
|
fields(keepalive) ->
|
||||||
[
|
[
|
||||||
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
|
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(subscribe) ->
|
fields(subscribe) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
|
||||||
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
|
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(unsubscribe) ->
|
fields(unsubscribe) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
|
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
emqx_dashboard_swagger:fields(limit) ++
|
emqx_dashboard_swagger:fields(limit) ++
|
||||||
|
@ -393,13 +531,11 @@ clients(get, #{query_string := QString}) ->
|
||||||
|
|
||||||
client(get, #{bindings := Bindings}) ->
|
client(get, #{bindings := Bindings}) ->
|
||||||
lookup(Bindings);
|
lookup(Bindings);
|
||||||
|
|
||||||
client(delete, #{bindings := Bindings}) ->
|
client(delete, #{bindings := Bindings}) ->
|
||||||
kickout(Bindings).
|
kickout(Bindings).
|
||||||
|
|
||||||
authz_cache(get, #{bindings := Bindings}) ->
|
authz_cache(get, #{bindings := Bindings}) ->
|
||||||
get_authz_cache(Bindings);
|
get_authz_cache(Bindings);
|
||||||
|
|
||||||
authz_cache(delete, #{bindings := Bindings}) ->
|
authz_cache(delete, #{bindings := Bindings}) ->
|
||||||
clean_authz_cache(Bindings).
|
clean_authz_cache(Bindings).
|
||||||
|
|
||||||
|
@ -415,11 +551,14 @@ unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) ->
|
||||||
%% TODO: batch
|
%% TODO: batch
|
||||||
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
|
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
|
||||||
Topics =
|
Topics =
|
||||||
[begin
|
[
|
||||||
Topic = maps:get(<<"topic">>, TopicInfo),
|
begin
|
||||||
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
Topic = maps:get(<<"topic">>, TopicInfo),
|
||||||
#{topic => Topic, qos => Qos}
|
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
||||||
end || TopicInfo <- TopicInfos],
|
#{topic => Topic, qos => Qos}
|
||||||
|
end
|
||||||
|
|| TopicInfo <- TopicInfos
|
||||||
|
],
|
||||||
subscribe_batch(#{clientid => ClientID, topics => Topics}).
|
subscribe_batch(#{clientid => ClientID, topics => Topics}).
|
||||||
|
|
||||||
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
||||||
|
@ -436,16 +575,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
||||||
qos => maps:get(qos, SubOpts)
|
qos => maps:get(qos, SubOpts)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
{200, lists:map(Formatter, Subs)}
|
{200, lists:map(Formatter, Subs)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
||||||
case maps:find(<<"interval">>, Body) of
|
case maps:find(<<"interval">>, Body) of
|
||||||
error -> {400, 'BAD_REQUEST',"Interval Not Found"};
|
error ->
|
||||||
|
{400, 'BAD_REQUEST', "Interval Not Found"};
|
||||||
{ok, Interval} ->
|
{ok, Interval} ->
|
||||||
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
|
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
|
||||||
ok -> lookup(#{clientid => ClientID});
|
ok -> lookup(#{clientid => ClientID});
|
||||||
{error, not_found} ->{404, ?CLIENT_ID_NOT_FOUND};
|
{error, not_found} -> {404, ?CLIENT_ID_NOT_FOUND};
|
||||||
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
|
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
@ -454,16 +594,26 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
||||||
%% api apply
|
%% api apply
|
||||||
|
|
||||||
list_clients(QString) ->
|
list_clients(QString) ->
|
||||||
Result = case maps:get(<<"node">>, QString, undefined) of
|
Result =
|
||||||
undefined ->
|
case maps:get(<<"node">>, QString, undefined) of
|
||||||
emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB,
|
undefined ->
|
||||||
?CLIENT_QSCHEMA, ?QUERY_FUN);
|
emqx_mgmt_api:cluster_query(
|
||||||
Node0 ->
|
QString,
|
||||||
Node1 = binary_to_atom(Node0, utf8),
|
?CLIENT_QTAB,
|
||||||
QStringWithoutNode = maps:without([<<"node">>], QString),
|
?CLIENT_QSCHEMA,
|
||||||
emqx_mgmt_api:node_query(Node1, QStringWithoutNode,
|
?QUERY_FUN
|
||||||
?CLIENT_QTAB, ?CLIENT_QSCHEMA, ?QUERY_FUN)
|
);
|
||||||
end,
|
Node0 ->
|
||||||
|
Node1 = binary_to_atom(Node0, utf8),
|
||||||
|
QStringWithoutNode = maps:without([<<"node">>], QString),
|
||||||
|
emqx_mgmt_api:node_query(
|
||||||
|
Node1,
|
||||||
|
QStringWithoutNode,
|
||||||
|
?CLIENT_QTAB,
|
||||||
|
?CLIENT_QSCHEMA,
|
||||||
|
?QUERY_FUN
|
||||||
|
)
|
||||||
|
end,
|
||||||
case Result of
|
case Result of
|
||||||
{error, page_limit_invalid} ->
|
{error, page_limit_invalid} ->
|
||||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
|
@ -490,7 +640,7 @@ kickout(#{clientid := ClientID}) ->
|
||||||
{204}
|
{204}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_authz_cache(#{clientid := ClientID})->
|
get_authz_cache(#{clientid := ClientID}) ->
|
||||||
case emqx_mgmt:list_authz_cache(ClientID) of
|
case emqx_mgmt:list_authz_cache(ClientID) of
|
||||||
{error, not_found} ->
|
{error, not_found} ->
|
||||||
{404, ?CLIENT_ID_NOT_FOUND};
|
{404, ?CLIENT_ID_NOT_FOUND};
|
||||||
|
@ -524,9 +674,9 @@ subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) ->
|
||||||
Response =
|
Response =
|
||||||
#{
|
#{
|
||||||
clientid => ClientID,
|
clientid => ClientID,
|
||||||
topic => Topic,
|
topic => Topic,
|
||||||
qos => Qos,
|
qos => Qos,
|
||||||
node => Node
|
node => Node
|
||||||
},
|
},
|
||||||
{200, Response}
|
{200, Response}
|
||||||
end.
|
end.
|
||||||
|
@ -562,7 +712,7 @@ do_subscribe(ClientID, Topic0, Qos) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||||
{unsubscribe, _} | {error, channel_not_found}.
|
{unsubscribe, _} | {error, channel_not_found}.
|
||||||
do_unsubscribe(ClientID, Topic) ->
|
do_unsubscribe(ClientID, Topic) ->
|
||||||
case emqx_mgmt:unsubscribe(ClientID, Topic) of
|
case emqx_mgmt:unsubscribe(ClientID, Topic) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -576,14 +726,23 @@ do_unsubscribe(ClientID, Topic) ->
|
||||||
|
|
||||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(QString),
|
Ms = qs2ms(QString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_channel_info/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_channel_info/1
|
||||||
|
);
|
||||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(QString),
|
Ms = qs2ms(QString),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
emqx_mgmt_api:select_table_with_count(
|
||||||
fun format_channel_info/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format_channel_info/1
|
||||||
|
).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% QueryString to Match Spec
|
%% QueryString to Match Spec
|
||||||
|
@ -595,7 +754,6 @@ qs2ms(Qs) ->
|
||||||
|
|
||||||
qs2ms([], _, {MtchHead, Conds}) ->
|
qs2ms([], _, {MtchHead, Conds}) ->
|
||||||
{MtchHead, lists:reverse(Conds)};
|
{MtchHead, lists:reverse(Conds)};
|
||||||
|
|
||||||
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
|
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
|
||||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
|
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
|
||||||
qs2ms(Rest, N, {NMtchHead, Conds});
|
qs2ms(Rest, N, {NMtchHead, Conds});
|
||||||
|
@ -603,13 +761,16 @@ qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
|
||||||
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
|
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
|
||||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
|
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
|
||||||
NConds = put_conds(Qs, Holder, Conds),
|
NConds = put_conds(Qs, Holder, Conds),
|
||||||
qs2ms(Rest, N+1, {NMtchHead, NConds}).
|
qs2ms(Rest, N + 1, {NMtchHead, NConds}).
|
||||||
|
|
||||||
put_conds({_, Op, V}, Holder, Conds) ->
|
put_conds({_, Op, V}, Holder, Conds) ->
|
||||||
[{Op, Holder, V} | Conds];
|
[{Op, Holder, V} | Conds];
|
||||||
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
|
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
|
||||||
[{Op2, Holder, V2},
|
[
|
||||||
{Op1, Holder, V1} | Conds].
|
{Op2, Holder, V2},
|
||||||
|
{Op1, Holder, V1}
|
||||||
|
| Conds
|
||||||
|
].
|
||||||
|
|
||||||
ms(clientid, X) ->
|
ms(clientid, X) ->
|
||||||
#{clientinfo => #{clientid => X}};
|
#{clientinfo => #{clientid => X}};
|
||||||
|
@ -637,68 +798,78 @@ ms(created_at, X) ->
|
||||||
|
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
true;
|
true;
|
||||||
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
|
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
|
||||||
Val = case maps:get(Key, ClientInfo, <<>>) of
|
Val =
|
||||||
undefined -> <<>>;
|
case maps:get(Key, ClientInfo, <<>>) of
|
||||||
V -> V
|
undefined -> <<>>;
|
||||||
end,
|
V -> V
|
||||||
|
end,
|
||||||
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% format funcs
|
%% format funcs
|
||||||
|
|
||||||
format_channel_info({_, ClientInfo, ClientStats}) ->
|
format_channel_info({_, ClientInfo, ClientStats}) ->
|
||||||
Node = case ClientInfo of
|
Node =
|
||||||
#{node := N} -> N;
|
case ClientInfo of
|
||||||
_ -> node()
|
#{node := N} -> N;
|
||||||
end,
|
_ -> node()
|
||||||
StatsMap = maps:without([memory, next_pkt_id, total_heap_size],
|
end,
|
||||||
maps:from_list(ClientStats)),
|
StatsMap = maps:without(
|
||||||
|
[memory, next_pkt_id, total_heap_size],
|
||||||
|
maps:from_list(ClientStats)
|
||||||
|
),
|
||||||
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
|
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
|
||||||
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
|
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
|
||||||
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
||||||
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
|
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
|
||||||
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
|
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
|
||||||
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
|
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
|
||||||
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
|
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
|
||||||
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
||||||
RemoveList =
|
RemoveList =
|
||||||
[ auth_result
|
[
|
||||||
, peername
|
auth_result,
|
||||||
, sockname
|
peername,
|
||||||
, peerhost
|
sockname,
|
||||||
, conn_state
|
peerhost,
|
||||||
, send_pend
|
conn_state,
|
||||||
, conn_props
|
send_pend,
|
||||||
, peercert
|
conn_props,
|
||||||
, sockstate
|
peercert,
|
||||||
, subscriptions
|
sockstate,
|
||||||
, receive_maximum
|
subscriptions,
|
||||||
, protocol
|
receive_maximum,
|
||||||
, is_superuser
|
protocol,
|
||||||
, sockport
|
is_superuser,
|
||||||
, anonymous
|
sockport,
|
||||||
, mountpoint
|
anonymous,
|
||||||
, socktype
|
mountpoint,
|
||||||
, active_n
|
socktype,
|
||||||
, await_rel_timeout
|
active_n,
|
||||||
, conn_mod
|
await_rel_timeout,
|
||||||
, sockname
|
conn_mod,
|
||||||
, retry_interval
|
sockname,
|
||||||
, upgrade_qos
|
retry_interval,
|
||||||
, id %% sessionID, defined in emqx_session.erl
|
upgrade_qos,
|
||||||
],
|
%% sessionID, defined in emqx_session.erl
|
||||||
|
id
|
||||||
|
],
|
||||||
TimesKeys = [created_at, connected_at, disconnected_at],
|
TimesKeys = [created_at, connected_at, disconnected_at],
|
||||||
%% format timestamp to rfc3339
|
%% format timestamp to rfc3339
|
||||||
lists:foldl(fun result_format_time_fun/2
|
lists:foldl(
|
||||||
, maps:without(RemoveList, ClientInfoMap)
|
fun result_format_time_fun/2,
|
||||||
, TimesKeys).
|
maps:without(RemoveList, ClientInfoMap),
|
||||||
|
TimesKeys
|
||||||
|
).
|
||||||
|
|
||||||
%% format func helpers
|
%% format func helpers
|
||||||
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
|
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
|
||||||
|
@ -710,20 +881,22 @@ result_format_time_fun(Key, NClientInfoMap) ->
|
||||||
case NClientInfoMap of
|
case NClientInfoMap of
|
||||||
#{Key := TimeStamp} ->
|
#{Key := TimeStamp} ->
|
||||||
NClientInfoMap#{
|
NClientInfoMap#{
|
||||||
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)};
|
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)
|
||||||
|
};
|
||||||
#{} ->
|
#{} ->
|
||||||
NClientInfoMap
|
NClientInfoMap
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}).
|
-spec peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}.
|
||||||
peername_dispart({Addr, Port}) ->
|
peername_dispart({Addr, Port}) ->
|
||||||
AddrBinary = list_to_binary(inet:ntoa(Addr)),
|
AddrBinary = list_to_binary(inet:ntoa(Addr)),
|
||||||
%% PortBinary = integer_to_binary(Port),
|
%% PortBinary = integer_to_binary(Port),
|
||||||
{AddrBinary, Port}.
|
{AddrBinary, Port}.
|
||||||
|
|
||||||
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
|
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
|
||||||
#{ access => PubSub,
|
#{
|
||||||
topic => Topic,
|
access => PubSub,
|
||||||
result => AuthzResult,
|
topic => Topic,
|
||||||
updated_time => Timestamp
|
result => AuthzResult,
|
||||||
}.
|
updated_time => Timestamp
|
||||||
|
}.
|
||||||
|
|
|
@ -125,7 +125,7 @@ force_leave(delete, #{bindings := #{node := Node0}}) ->
|
||||||
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
|
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec(join(node()) -> ok | ignore | {error, term()}).
|
-spec join(node()) -> ok | ignore | {error, term()}.
|
||||||
join(Node) ->
|
join(Node) ->
|
||||||
ekka:join(Node).
|
ekka:join(Node).
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,13 @@
|
||||||
-export([api_spec/0, namespace/0]).
|
-export([api_spec/0, namespace/0]).
|
||||||
-export([paths/0, schema/1, fields/1]).
|
-export([paths/0, schema/1, fields/1]).
|
||||||
|
|
||||||
-export([ config/3
|
-export([
|
||||||
, config_reset/3
|
config/3,
|
||||||
, configs/3
|
config_reset/3,
|
||||||
, get_full_config/0
|
configs/3,
|
||||||
, global_zone_configs/3]).
|
get_full_config/0,
|
||||||
|
global_zone_configs/3
|
||||||
|
]).
|
||||||
|
|
||||||
-export([gen_schema/1]).
|
-export([gen_schema/1]).
|
||||||
|
|
||||||
|
@ -36,28 +38,29 @@
|
||||||
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
|
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
|
||||||
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
|
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
|
||||||
|
|
||||||
-define(EXCLUDES, [
|
-define(EXCLUDES,
|
||||||
<<"exhook">>,
|
[
|
||||||
<<"gateway">>,
|
<<"exhook">>,
|
||||||
<<"plugins">>,
|
<<"gateway">>,
|
||||||
<<"bridges">>,
|
<<"plugins">>,
|
||||||
<<"rule_engine">>,
|
<<"bridges">>,
|
||||||
<<"authorization">>,
|
<<"rule_engine">>,
|
||||||
<<"authentication">>,
|
<<"authorization">>,
|
||||||
<<"rpc">>,
|
<<"authentication">>,
|
||||||
<<"db">>,
|
<<"rpc">>,
|
||||||
<<"connectors">>,
|
<<"db">>,
|
||||||
<<"slow_subs">>,
|
<<"connectors">>,
|
||||||
<<"psk_authentication">>,
|
<<"slow_subs">>,
|
||||||
<<"topic_metrics">>,
|
<<"psk_authentication">>,
|
||||||
<<"rewrite">>,
|
<<"topic_metrics">>,
|
||||||
<<"auto_subscribe">>,
|
<<"rewrite">>,
|
||||||
<<"retainer">>,
|
<<"auto_subscribe">>,
|
||||||
<<"statsd">>,
|
<<"retainer">>,
|
||||||
<<"delayed">>,
|
<<"statsd">>,
|
||||||
<<"event_message">>,
|
<<"delayed">>,
|
||||||
<<"prometheus">>,
|
<<"event_message">>,
|
||||||
<<"telemetry">>
|
<<"prometheus">>,
|
||||||
|
<<"telemetry">>
|
||||||
] ++ global_zone_roots()
|
] ++ global_zone_roots()
|
||||||
).
|
).
|
||||||
|
|
||||||
|
@ -68,7 +71,7 @@ namespace() -> "configuration".
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
|
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
|
||||||
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
||||||
|
|
||||||
schema("/configs") ->
|
schema("/configs") ->
|
||||||
#{
|
#{
|
||||||
|
@ -76,12 +79,20 @@ schema("/configs") ->
|
||||||
get => #{
|
get => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description =>
|
description =>
|
||||||
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
||||||
parameters => [
|
parameters => [
|
||||||
{node, hoconsc:mk(typerefl:atom(),
|
{node,
|
||||||
#{in => query, required => false, example => <<"emqx@127.0.0.1">>,
|
hoconsc:mk(
|
||||||
desc =>
|
typerefl:atom(),
|
||||||
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
|
#{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
example => <<"emqx@127.0.0.1">>,
|
||||||
|
desc =>
|
||||||
|
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
|
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
|
||||||
}
|
}
|
||||||
|
@ -94,18 +105,29 @@ schema("/configs_reset/:rootname") ->
|
||||||
post => #{
|
post => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description =>
|
description =>
|
||||||
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>
|
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>\n"
|
||||||
- For a config entry that has default value, this resets it to the default value;
|
"- For a config entry that has default value, this resets it to the default value;\n"
|
||||||
- For a config entry that has no default value, an error 400 will be returned">>,
|
"- For a config entry that has no default value, an error 400 will be returned">>,
|
||||||
%% We only return "200" rather than the new configs that has been changed, as
|
%% We only return "200" rather than the new configs that has been changed, as
|
||||||
%% the schema of the changed configs is depends on the request parameter
|
%% the schema of the changed configs is depends on the request parameter
|
||||||
%% `conf_path`, it cannot be defined here.
|
%% `conf_path`, it cannot be defined here.
|
||||||
parameters => [
|
parameters => [
|
||||||
{rootname, hoconsc:mk( hoconsc:enum(Paths)
|
{rootname,
|
||||||
, #{in => path, example => <<"sysmon">>})},
|
hoconsc:mk(
|
||||||
{conf_path, hoconsc:mk(typerefl:binary(),
|
hoconsc:enum(Paths),
|
||||||
#{in => query, required => false, example => <<"os.sysmem_high_watermark">>,
|
#{in => path, example => <<"sysmon">>}
|
||||||
desc => <<"The config path separated by '.' character">>})}],
|
)},
|
||||||
|
{conf_path,
|
||||||
|
hoconsc:mk(
|
||||||
|
typerefl:binary(),
|
||||||
|
#{
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
example => <<"os.sysmem_high_watermark">>,
|
||||||
|
desc => <<"The config path separated by '.' character">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"Rest config successfully">>,
|
200 => <<"Rest config successfully">>,
|
||||||
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
|
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
|
||||||
|
@ -137,9 +159,11 @@ schema(Path) ->
|
||||||
'operationId' => config,
|
'operationId' => config,
|
||||||
get => #{
|
get => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description => iolist_to_binary([ <<"Get the sub-configurations under *">>
|
description => iolist_to_binary([
|
||||||
, RootKey
|
<<"Get the sub-configurations under *">>,
|
||||||
, <<"*">>]),
|
RootKey,
|
||||||
|
<<"*">>
|
||||||
|
]),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => Schema,
|
200 => Schema,
|
||||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
||||||
|
@ -147,9 +171,11 @@ schema(Path) ->
|
||||||
},
|
},
|
||||||
put => #{
|
put => #{
|
||||||
tags => [conf],
|
tags => [conf],
|
||||||
description => iolist_to_binary([ <<"Update the sub-configurations under *">>
|
description => iolist_to_binary([
|
||||||
, RootKey
|
<<"Update the sub-configurations under *">>,
|
||||||
, <<"*">>]),
|
RootKey,
|
||||||
|
<<"*">>
|
||||||
|
]),
|
||||||
'requestBody' => Schema,
|
'requestBody' => Schema,
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => Schema,
|
200 => Schema,
|
||||||
|
@ -176,7 +202,6 @@ config(get, _Params, Req) ->
|
||||||
Path = conf_path(Req),
|
Path = conf_path(Req),
|
||||||
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
|
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
|
||||||
{200, Conf};
|
{200, Conf};
|
||||||
|
|
||||||
config(put, #{body := Body}, Req) ->
|
config(put, #{body := Body}, Req) ->
|
||||||
Path = conf_path(Req),
|
Path = conf_path(Req),
|
||||||
case emqx_conf:update(Path, Body, ?OPTS) of
|
case emqx_conf:update(Path, Body, ?OPTS) of
|
||||||
|
@ -188,21 +213,32 @@ config(put, #{body := Body}, Req) ->
|
||||||
|
|
||||||
global_zone_configs(get, _Params, _Req) ->
|
global_zone_configs(get, _Params, _Req) ->
|
||||||
Paths = global_zone_roots(),
|
Paths = global_zone_roots(),
|
||||||
Zones = lists:foldl(fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
Zones = lists:foldl(
|
||||||
#{}, Paths),
|
fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
||||||
|
#{},
|
||||||
|
Paths
|
||||||
|
),
|
||||||
{200, Zones};
|
{200, Zones};
|
||||||
global_zone_configs(put, #{body := Body}, _Req) ->
|
global_zone_configs(put, #{body := Body}, _Req) ->
|
||||||
Res =
|
Res =
|
||||||
maps:fold(fun(Path, Value, Acc) ->
|
maps:fold(
|
||||||
case emqx_conf:update([Path], Value, ?OPTS) of
|
fun(Path, Value, Acc) ->
|
||||||
{ok, #{raw_config := RawConf}} ->
|
case emqx_conf:update([Path], Value, ?OPTS) of
|
||||||
Acc#{Path => RawConf};
|
{ok, #{raw_config := RawConf}} ->
|
||||||
{error, Reason} ->
|
Acc#{Path => RawConf};
|
||||||
?SLOG(error, #{msg => "update global zone failed", reason => Reason,
|
{error, Reason} ->
|
||||||
path => Path, value => Value}),
|
?SLOG(error, #{
|
||||||
Acc
|
msg => "update global zone failed",
|
||||||
end
|
reason => Reason,
|
||||||
end, #{}, Body),
|
path => Path,
|
||||||
|
value => Value
|
||||||
|
}),
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Body
|
||||||
|
),
|
||||||
case maps:size(Res) =:= maps:size(Body) of
|
case maps:size(Res) =:= maps:size(Body) of
|
||||||
true -> {200, Res};
|
true -> {200, Res};
|
||||||
false -> {400, #{code => 'UPDATE_FAILED'}}
|
false -> {400, #{code => 'UPDATE_FAILED'}}
|
||||||
|
@ -212,7 +248,8 @@ config_reset(post, _Params, Req) ->
|
||||||
%% reset the config specified by the query string param 'conf_path'
|
%% reset the config specified by the query string param 'conf_path'
|
||||||
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
|
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
|
||||||
case emqx:reset_config(Path, #{}) of
|
case emqx:reset_config(Path, #{}) of
|
||||||
{ok, _} -> {200};
|
{ok, _} ->
|
||||||
|
{200};
|
||||||
{error, no_default_value} ->
|
{error, no_default_value} ->
|
||||||
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
|
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -222,9 +259,8 @@ config_reset(post, _Params, Req) ->
|
||||||
configs(get, Params, _Req) ->
|
configs(get, Params, _Req) ->
|
||||||
Node = maps:get(node, Params, node()),
|
Node = maps:get(node, Params, node()),
|
||||||
case
|
case
|
||||||
lists:member(Node, mria_mnesia:running_nodes())
|
lists:member(Node, mria_mnesia:running_nodes()) andalso
|
||||||
andalso
|
emqx_management_proto_v1:get_full_config(Node)
|
||||||
emqx_management_proto_v1:get_full_config(Node)
|
|
||||||
of
|
of
|
||||||
false ->
|
false ->
|
||||||
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
|
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
|
||||||
|
@ -242,8 +278,11 @@ conf_path_reset(Req) ->
|
||||||
|
|
||||||
get_full_config() ->
|
get_full_config() ->
|
||||||
emqx_config:fill_defaults(
|
emqx_config:fill_defaults(
|
||||||
maps:without(?EXCLUDES,
|
maps:without(
|
||||||
emqx:get_raw_config([]))).
|
?EXCLUDES,
|
||||||
|
emqx:get_raw_config([])
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
get_config_with_default(Path) ->
|
get_config_with_default(Path) ->
|
||||||
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
|
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
|
||||||
|
@ -278,8 +317,11 @@ gen_schema(Conf) when is_list(Conf) ->
|
||||||
#{type => array, items => gen_schema(hd(Conf))}
|
#{type => array, items => gen_schema(hd(Conf))}
|
||||||
end;
|
end;
|
||||||
gen_schema(Conf) when is_map(Conf) ->
|
gen_schema(Conf) when is_map(Conf) ->
|
||||||
#{type => object, properties =>
|
#{
|
||||||
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)};
|
type => object,
|
||||||
|
properties =>
|
||||||
|
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)
|
||||||
|
};
|
||||||
gen_schema(_Conf) ->
|
gen_schema(_Conf) ->
|
||||||
%% the conf is not of JSON supported type, it may have been converted
|
%% the conf is not of JSON supported type, it may have been converted
|
||||||
%% by the hocon schema
|
%% by the hocon schema
|
||||||
|
|
|
@ -23,14 +23,16 @@
|
||||||
-import(hoconsc, [mk/2, ref/2]).
|
-import(hoconsc, [mk/2, ref/2]).
|
||||||
|
|
||||||
%% minirest/dashbaord_swagger behaviour callbacks
|
%% minirest/dashbaord_swagger behaviour callbacks
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ roots/0
|
-export([
|
||||||
, fields/1
|
roots/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% http handlers
|
%% http handlers
|
||||||
-export([metrics/2]).
|
-export([metrics/2]).
|
||||||
|
@ -53,9 +55,12 @@ metrics(get, #{query_string := Qs}) ->
|
||||||
true ->
|
true ->
|
||||||
{200, emqx_mgmt:get_metrics()};
|
{200, emqx_mgmt:get_metrics()};
|
||||||
false ->
|
false ->
|
||||||
Data = [maps:from_list(
|
Data = [
|
||||||
emqx_mgmt:get_metrics(Node) ++ [{node, Node}])
|
maps:from_list(
|
||||||
|| Node <- mria_mnesia:running_nodes()],
|
emqx_mgmt:get_metrics(Node) ++ [{node, Node}]
|
||||||
|
)
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
],
|
||||||
{200, Data}
|
{200, Data}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -64,23 +69,34 @@ metrics(get, #{query_string := Qs}) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
schema("/metrics") ->
|
schema("/metrics") ->
|
||||||
#{ 'operationId' => metrics
|
#{
|
||||||
, get =>
|
'operationId' => metrics,
|
||||||
#{ description => <<"EMQX metrics">>
|
get =>
|
||||||
, parameters =>
|
#{
|
||||||
[{ aggregate
|
description => <<"EMQX metrics">>,
|
||||||
, mk( boolean()
|
parameters =>
|
||||||
, #{ in => query
|
[
|
||||||
, required => false
|
{aggregate,
|
||||||
, desc => <<"Whether to aggregate all nodes Metrics">>})
|
mk(
|
||||||
}]
|
boolean(),
|
||||||
, responses =>
|
#{
|
||||||
#{ 200 => hoconsc:union(
|
in => query,
|
||||||
[ref(?MODULE, aggregated_metrics),
|
required => false,
|
||||||
hoconsc:array(ref(?MODULE, node_metrics))])
|
desc => <<"Whether to aggregate all nodes Metrics">>
|
||||||
}
|
}
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => hoconsc:union(
|
||||||
|
[
|
||||||
|
ref(?MODULE, aggregated_metrics),
|
||||||
|
hoconsc:array(ref(?MODULE, node_metrics))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
roots() ->
|
roots() ->
|
||||||
[].
|
[].
|
||||||
|
@ -91,176 +107,353 @@ fields(node_metrics) ->
|
||||||
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
|
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
|
||||||
|
|
||||||
properties() ->
|
properties() ->
|
||||||
[ m('actions.failure',
|
[
|
||||||
<<"Number of failure executions of the rule engine action">>)
|
m(
|
||||||
, m('actions.success',
|
'actions.failure',
|
||||||
<<"Number of successful executions of the rule engine action">>)
|
<<"Number of failure executions of the rule engine action">>
|
||||||
, m('bytes.received',
|
),
|
||||||
<<"Number of bytes received ">>)
|
m(
|
||||||
, m('bytes.sent',
|
'actions.success',
|
||||||
<<"Number of bytes sent on this connection">>)
|
<<"Number of successful executions of the rule engine action">>
|
||||||
, m('client.auth.anonymous',
|
),
|
||||||
<<"Number of clients who log in anonymously">>)
|
m(
|
||||||
, m('client.authenticate',
|
'bytes.received',
|
||||||
<<"Number of client authentications">>)
|
<<"Number of bytes received ">>
|
||||||
, m('client.check_authz',
|
),
|
||||||
<<"Number of Authorization rule checks">>)
|
m(
|
||||||
, m('client.connack',
|
'bytes.sent',
|
||||||
<<"Number of CONNACK packet sent">>)
|
<<"Number of bytes sent on this connection">>
|
||||||
, m('client.connect',
|
),
|
||||||
<<"Number of client connections">>)
|
m(
|
||||||
, m('client.connected',
|
'client.auth.anonymous',
|
||||||
<<"Number of successful client connections">>)
|
<<"Number of clients who log in anonymously">>
|
||||||
, m('client.disconnected',
|
),
|
||||||
<<"Number of client disconnects">>)
|
m(
|
||||||
, m('client.subscribe',
|
'client.authenticate',
|
||||||
<<"Number of client subscriptions">>)
|
<<"Number of client authentications">>
|
||||||
, m('client.unsubscribe',
|
),
|
||||||
<<"Number of client unsubscriptions">>)
|
m(
|
||||||
, m('delivery.dropped',
|
'client.check_authz',
|
||||||
<<"Total number of discarded messages when sending">>)
|
<<"Number of Authorization rule checks">>
|
||||||
, m('delivery.dropped.expired',
|
),
|
||||||
<<"Number of messages dropped due to message expiration on sending">>)
|
m(
|
||||||
, m('delivery.dropped.no_local',
|
'client.connack',
|
||||||
<<"Number of messages that were dropped due to the No Local subscription "
|
<<"Number of CONNACK packet sent">>
|
||||||
"option when sending">>)
|
),
|
||||||
, m('delivery.dropped.qos0_msg',
|
m(
|
||||||
<<"Number of messages with QoS 0 that were dropped because the message "
|
'client.connect',
|
||||||
"queue was full when sending">>)
|
<<"Number of client connections">>
|
||||||
, m('delivery.dropped.queue_full',
|
),
|
||||||
<<"Number of messages with a non-zero QoS that were dropped because the "
|
m(
|
||||||
"message queue was full when sending">>)
|
'client.connected',
|
||||||
, m('delivery.dropped.too_large',
|
<<"Number of successful client connections">>
|
||||||
<<"The number of messages that were dropped because the length exceeded "
|
),
|
||||||
"the limit when sending">>)
|
m(
|
||||||
, m('messages.acked',
|
'client.disconnected',
|
||||||
<<"Number of received PUBACK and PUBREC packet">>)
|
<<"Number of client disconnects">>
|
||||||
, m('messages.delayed',
|
),
|
||||||
<<"Number of delay-published messages">>)
|
m(
|
||||||
, m('messages.delivered',
|
'client.subscribe',
|
||||||
<<"Number of messages forwarded to the subscription process internally">>)
|
<<"Number of client subscriptions">>
|
||||||
, m('messages.dropped',
|
),
|
||||||
<<"Total number of messages dropped before forwarding to the subscription process">>)
|
m(
|
||||||
, m('messages.dropped.await_pubrel_timeout',
|
'client.unsubscribe',
|
||||||
<<"Number of messages dropped due to waiting PUBREL timeout">>)
|
<<"Number of client unsubscriptions">>
|
||||||
, m('messages.dropped.no_subscribers',
|
),
|
||||||
<<"Number of messages dropped due to no subscribers">>)
|
m(
|
||||||
, m('messages.forward',
|
'delivery.dropped',
|
||||||
<<"Number of messages forwarded to other nodes">>)
|
<<"Total number of discarded messages when sending">>
|
||||||
, m('messages.publish',
|
),
|
||||||
<<"Number of messages published in addition to system messages">>)
|
m(
|
||||||
, m('messages.qos0.received',
|
'delivery.dropped.expired',
|
||||||
<<"Number of QoS 0 messages received from clients">>)
|
<<"Number of messages dropped due to message expiration on sending">>
|
||||||
, m('messages.qos0.sent',
|
),
|
||||||
<<"Number of QoS 0 messages sent to clients">>)
|
m(
|
||||||
, m('messages.qos1.received',
|
'delivery.dropped.no_local',
|
||||||
<<"Number of QoS 1 messages received from clients">>)
|
<<
|
||||||
, m('messages.qos1.sent',
|
"Number of messages that were dropped due to the No Local subscription "
|
||||||
<<"Number of QoS 1 messages sent to clients">>)
|
"option when sending"
|
||||||
, m('messages.qos2.received',
|
>>
|
||||||
<<"Number of QoS 2 messages received from clients">>)
|
),
|
||||||
, m('messages.qos2.sent',
|
m(
|
||||||
<<"Number of QoS 2 messages sent to clients">>)
|
'delivery.dropped.qos0_msg',
|
||||||
, m('messages.received',
|
<<
|
||||||
<<"Number of messages received from the client, equal to the sum of "
|
"Number of messages with QoS 0 that were dropped because the message "
|
||||||
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received">>)
|
"queue was full when sending"
|
||||||
, m('messages.retained',
|
>>
|
||||||
<<"Number of retained messages">>)
|
),
|
||||||
, m('messages.sent',
|
m(
|
||||||
<<"Number of messages sent to the client, equal to the sum of "
|
'delivery.dropped.queue_full',
|
||||||
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent">>)
|
<<
|
||||||
, m('packets.auth.received',
|
"Number of messages with a non-zero QoS that were dropped because the "
|
||||||
<<"Number of received AUTH packet">>)
|
"message queue was full when sending"
|
||||||
, m('packets.auth.sent',
|
>>
|
||||||
<<"Number of sent AUTH packet">>)
|
),
|
||||||
, m('packets.connack.auth_error',
|
m(
|
||||||
<<"Number of received CONNECT packet with failed authentication">>)
|
'delivery.dropped.too_large',
|
||||||
, m('packets.connack.error',
|
<<
|
||||||
<<"Number of received CONNECT packet with unsuccessful connections">>)
|
"The number of messages that were dropped because the length exceeded "
|
||||||
, m('packets.connack.sent',
|
"the limit when sending"
|
||||||
<<"Number of sent CONNACK packet">>)
|
>>
|
||||||
, m('packets.connect.received',
|
),
|
||||||
<<"Number of received CONNECT packet">>)
|
m(
|
||||||
, m('packets.disconnect.received',
|
'messages.acked',
|
||||||
<<"Number of received DISCONNECT packet">>)
|
<<"Number of received PUBACK and PUBREC packet">>
|
||||||
, m('packets.disconnect.sent',
|
),
|
||||||
<<"Number of sent DISCONNECT packet">>)
|
m(
|
||||||
, m('packets.pingreq.received',
|
'messages.delayed',
|
||||||
<<"Number of received PINGREQ packet">>)
|
<<"Number of delay-published messages">>
|
||||||
, m('packets.pingresp.sent',
|
),
|
||||||
<<"Number of sent PUBRESP packet">>)
|
m(
|
||||||
, m('packets.puback.inuse',
|
'messages.delivered',
|
||||||
<<"Number of received PUBACK packet with occupied identifiers">>)
|
<<"Number of messages forwarded to the subscription process internally">>
|
||||||
, m('packets.puback.missed',
|
),
|
||||||
<<"Number of received packet with identifiers.">>)
|
m(
|
||||||
, m('packets.puback.received',
|
'messages.dropped',
|
||||||
<<"Number of received PUBACK packet">>)
|
<<"Total number of messages dropped before forwarding to the subscription process">>
|
||||||
, m('packets.puback.sent',
|
),
|
||||||
<<"Number of sent PUBACK packet">>)
|
m(
|
||||||
, m('packets.pubcomp.inuse',
|
'messages.dropped.await_pubrel_timeout',
|
||||||
<<"Number of received PUBCOMP packet with occupied identifiers">>)
|
<<"Number of messages dropped due to waiting PUBREL timeout">>
|
||||||
, m('packets.pubcomp.missed',
|
),
|
||||||
<<"Number of missed PUBCOMP packet">>)
|
m(
|
||||||
, m('packets.pubcomp.received',
|
'messages.dropped.no_subscribers',
|
||||||
<<"Number of received PUBCOMP packet">>)
|
<<"Number of messages dropped due to no subscribers">>
|
||||||
, m('packets.pubcomp.sent',
|
),
|
||||||
<<"Number of sent PUBCOMP packet">>)
|
m(
|
||||||
, m('packets.publish.auth_error',
|
'messages.forward',
|
||||||
<<"Number of received PUBLISH packets with failed the Authorization check">>)
|
<<"Number of messages forwarded to other nodes">>
|
||||||
, m('packets.publish.dropped',
|
),
|
||||||
<<"Number of messages discarded due to the receiving limit">>)
|
m(
|
||||||
, m('packets.publish.error',
|
'messages.publish',
|
||||||
<<"Number of received PUBLISH packet that cannot be published">>)
|
<<"Number of messages published in addition to system messages">>
|
||||||
, m('packets.publish.inuse',
|
),
|
||||||
<<"Number of received PUBLISH packet with occupied identifiers">>)
|
m(
|
||||||
, m('packets.publish.received',
|
'messages.qos0.received',
|
||||||
<<"Number of received PUBLISH packet">>)
|
<<"Number of QoS 0 messages received from clients">>
|
||||||
, m('packets.publish.sent',
|
),
|
||||||
<<"Number of sent PUBLISH packet">>)
|
m(
|
||||||
, m('packets.pubrec.inuse',
|
'messages.qos0.sent',
|
||||||
<<"Number of received PUBREC packet with occupied identifiers">>)
|
<<"Number of QoS 0 messages sent to clients">>
|
||||||
, m('packets.pubrec.missed',
|
),
|
||||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
m(
|
||||||
, m('packets.pubrec.received',
|
'messages.qos1.received',
|
||||||
<<"Number of received PUBREC packet">>)
|
<<"Number of QoS 1 messages received from clients">>
|
||||||
, m('packets.pubrec.sent',
|
),
|
||||||
<<"Number of sent PUBREC packet">>)
|
m(
|
||||||
, m('packets.pubrel.missed',
|
'messages.qos1.sent',
|
||||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
<<"Number of QoS 1 messages sent to clients">>
|
||||||
, m('packets.pubrel.received',
|
),
|
||||||
<<"Number of received PUBREL packet">>)
|
m(
|
||||||
, m('packets.pubrel.sent',
|
'messages.qos2.received',
|
||||||
<<"Number of sent PUBREL packet">>)
|
<<"Number of QoS 2 messages received from clients">>
|
||||||
, m('packets.received',
|
),
|
||||||
<<"Number of received packet">>)
|
m(
|
||||||
, m('packets.sent',
|
'messages.qos2.sent',
|
||||||
<<"Number of sent packet">>)
|
<<"Number of QoS 2 messages sent to clients">>
|
||||||
, m('packets.suback.sent',
|
),
|
||||||
<<"Number of sent SUBACK packet">>)
|
m(
|
||||||
, m('packets.subscribe.auth_error',
|
'messages.received',
|
||||||
<<"Number of received SUBACK packet with failed Authorization check">>)
|
<<
|
||||||
, m('packets.subscribe.error',
|
"Number of messages received from the client, equal to the sum of "
|
||||||
<<"Number of received SUBSCRIBE packet with failed subscriptions">>)
|
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received"
|
||||||
, m('packets.subscribe.received',
|
>>
|
||||||
<<"Number of received SUBSCRIBE packet">>)
|
),
|
||||||
, m('packets.unsuback.sent',
|
m(
|
||||||
<<"Number of sent UNSUBACK packet">>)
|
'messages.retained',
|
||||||
, m('packets.unsubscribe.error',
|
<<"Number of retained messages">>
|
||||||
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>)
|
),
|
||||||
, m('packets.unsubscribe.received',
|
m(
|
||||||
<<"Number of received UNSUBSCRIBE packet">>)
|
'messages.sent',
|
||||||
, m('rules.matched',
|
<<
|
||||||
<<"Number of rule matched">>)
|
"Number of messages sent to the client, equal to the sum of "
|
||||||
, m('session.created',
|
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent"
|
||||||
<<"Number of sessions created">>)
|
>>
|
||||||
, m('session.discarded',
|
),
|
||||||
<<"Number of sessions dropped because Clean Session or Clean Start is true">>)
|
m(
|
||||||
, m('session.resumed',
|
'packets.auth.received',
|
||||||
<<"Number of sessions resumed because Clean Session or Clean Start is false">>)
|
<<"Number of received AUTH packet">>
|
||||||
, m('session.takenover',
|
),
|
||||||
<<"Number of sessions takenover because Clean Session or Clean Start is false">>)
|
m(
|
||||||
, m('session.terminated',
|
'packets.auth.sent',
|
||||||
<<"Number of terminated sessions">>)
|
<<"Number of sent AUTH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.auth_error',
|
||||||
|
<<"Number of received CONNECT packet with failed authentication">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.error',
|
||||||
|
<<"Number of received CONNECT packet with unsuccessful connections">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connack.sent',
|
||||||
|
<<"Number of sent CONNACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.connect.received',
|
||||||
|
<<"Number of received CONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.disconnect.received',
|
||||||
|
<<"Number of received DISCONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.disconnect.sent',
|
||||||
|
<<"Number of sent DISCONNECT packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pingreq.received',
|
||||||
|
<<"Number of received PINGREQ packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pingresp.sent',
|
||||||
|
<<"Number of sent PUBRESP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.inuse',
|
||||||
|
<<"Number of received PUBACK packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.missed',
|
||||||
|
<<"Number of received packet with identifiers.">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.received',
|
||||||
|
<<"Number of received PUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.puback.sent',
|
||||||
|
<<"Number of sent PUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.inuse',
|
||||||
|
<<"Number of received PUBCOMP packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.missed',
|
||||||
|
<<"Number of missed PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.received',
|
||||||
|
<<"Number of received PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubcomp.sent',
|
||||||
|
<<"Number of sent PUBCOMP packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.auth_error',
|
||||||
|
<<"Number of received PUBLISH packets with failed the Authorization check">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.dropped',
|
||||||
|
<<"Number of messages discarded due to the receiving limit">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.error',
|
||||||
|
<<"Number of received PUBLISH packet that cannot be published">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.inuse',
|
||||||
|
<<"Number of received PUBLISH packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.received',
|
||||||
|
<<"Number of received PUBLISH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.publish.sent',
|
||||||
|
<<"Number of sent PUBLISH packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.inuse',
|
||||||
|
<<"Number of received PUBREC packet with occupied identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.missed',
|
||||||
|
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.received',
|
||||||
|
<<"Number of received PUBREC packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrec.sent',
|
||||||
|
<<"Number of sent PUBREC packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.missed',
|
||||||
|
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.received',
|
||||||
|
<<"Number of received PUBREL packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.pubrel.sent',
|
||||||
|
<<"Number of sent PUBREL packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.received',
|
||||||
|
<<"Number of received packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.sent',
|
||||||
|
<<"Number of sent packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.suback.sent',
|
||||||
|
<<"Number of sent SUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.auth_error',
|
||||||
|
<<"Number of received SUBACK packet with failed Authorization check">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.error',
|
||||||
|
<<"Number of received SUBSCRIBE packet with failed subscriptions">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.subscribe.received',
|
||||||
|
<<"Number of received SUBSCRIBE packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsuback.sent',
|
||||||
|
<<"Number of sent UNSUBACK packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsubscribe.error',
|
||||||
|
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'packets.unsubscribe.received',
|
||||||
|
<<"Number of received UNSUBSCRIBE packet">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'rules.matched',
|
||||||
|
<<"Number of rule matched">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.created',
|
||||||
|
<<"Number of sessions created">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.discarded',
|
||||||
|
<<"Number of sessions dropped because Clean Session or Clean Start is true">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.resumed',
|
||||||
|
<<"Number of sessions resumed because Clean Session or Clean Start is false">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.takenover',
|
||||||
|
<<"Number of sessions takenover because Clean Session or Clean Start is false">>
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
'session.terminated',
|
||||||
|
<<"Number of terminated sessions">>
|
||||||
|
)
|
||||||
].
|
].
|
||||||
|
|
||||||
m(K, Desc) ->
|
m(K, Desc) ->
|
||||||
|
|
|
@ -28,18 +28,20 @@
|
||||||
-define(SOURCE_ERROR, 'SOURCE_ERROR').
|
-define(SOURCE_ERROR, 'SOURCE_ERROR').
|
||||||
|
|
||||||
%% Swagger specs from hocon schema
|
%% Swagger specs from hocon schema
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, schema/1
|
api_spec/0,
|
||||||
, paths/0
|
schema/1,
|
||||||
, fields/1
|
paths/0,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
%% API callbacks
|
%% API callbacks
|
||||||
-export([ nodes/2
|
-export([
|
||||||
, node/2
|
nodes/2,
|
||||||
, node_metrics/2
|
node/2,
|
||||||
, node_stats/2
|
node_metrics/2,
|
||||||
]).
|
node_stats/2
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API spec funcs
|
%% API spec funcs
|
||||||
|
@ -49,123 +51,183 @@ api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[ "/nodes"
|
[
|
||||||
, "/nodes/:node"
|
"/nodes",
|
||||||
, "/nodes/:node/metrics"
|
"/nodes/:node",
|
||||||
, "/nodes/:node/stats"
|
"/nodes/:node/metrics",
|
||||||
|
"/nodes/:node/stats"
|
||||||
].
|
].
|
||||||
|
|
||||||
schema("/nodes") ->
|
schema("/nodes") ->
|
||||||
#{ 'operationId' => nodes
|
#{
|
||||||
, get =>
|
'operationId' => nodes,
|
||||||
#{ description => <<"List EMQX nodes">>
|
get =>
|
||||||
, responses =>
|
#{
|
||||||
#{200 => mk( array(ref(node_info))
|
description => <<"List EMQX nodes">>,
|
||||||
, #{desc => <<"List all EMQX nodes">>})}
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => mk(
|
||||||
|
array(ref(node_info)),
|
||||||
|
#{desc => <<"List all EMQX nodes">>}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node") ->
|
schema("/nodes/:node") ->
|
||||||
#{ 'operationId' => node
|
#{
|
||||||
, get =>
|
'operationId' => node,
|
||||||
#{ description => <<"Get node info">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node info">>,
|
||||||
#{ 200 => mk( ref(node_info)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node info successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(node_info),
|
||||||
|
#{desc => <<"Get node info successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node/metrics") ->
|
schema("/nodes/:node/metrics") ->
|
||||||
#{ 'operationId' => node_metrics
|
#{
|
||||||
, get =>
|
'operationId' => node_metrics,
|
||||||
#{ description => <<"Get node metrics">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node metrics">>,
|
||||||
#{ 200 => mk( ref(?NODE_METRICS_MODULE, node_metrics)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node metrics successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(?NODE_METRICS_MODULE, node_metrics),
|
||||||
|
#{desc => <<"Get node metrics successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
schema("/nodes/:node/stats") ->
|
schema("/nodes/:node/stats") ->
|
||||||
#{ 'operationId' => node_stats
|
#{
|
||||||
, get =>
|
'operationId' => node_stats,
|
||||||
#{ description => <<"Get node stats">>
|
get =>
|
||||||
, parameters => [ref(node_name)]
|
#{
|
||||||
, responses =>
|
description => <<"Get node stats">>,
|
||||||
#{ 200 => mk( ref(?NODE_STATS_MODULE, node_stats_data)
|
parameters => [ref(node_name)],
|
||||||
, #{desc => <<"Get node stats successfully">>})
|
responses =>
|
||||||
, 400 => node_error()
|
#{
|
||||||
}
|
200 => mk(
|
||||||
|
ref(?NODE_STATS_MODULE, node_stats_data),
|
||||||
|
#{desc => <<"Get node stats successfully">>}
|
||||||
|
),
|
||||||
|
400 => node_error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Fields
|
%% Fields
|
||||||
|
|
||||||
fields(node_name) ->
|
fields(node_name) ->
|
||||||
[ { node
|
[
|
||||||
, mk(atom()
|
{node,
|
||||||
, #{ in => path
|
mk(
|
||||||
, description => <<"Node name">>
|
atom(),
|
||||||
, required => true
|
#{
|
||||||
, example => <<"emqx@127.0.0.1">>
|
in => path,
|
||||||
})
|
description => <<"Node name">>,
|
||||||
}
|
required => true,
|
||||||
|
example => <<"emqx@127.0.0.1">>
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node_info) ->
|
fields(node_info) ->
|
||||||
[ { node
|
[
|
||||||
, mk( atom()
|
{node,
|
||||||
, #{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>})}
|
mk(
|
||||||
, { connections
|
atom(),
|
||||||
, mk( non_neg_integer()
|
#{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>}
|
||||||
, #{desc => <<"Number of clients currently connected to this node">>, example => 0})}
|
)},
|
||||||
, { load1
|
{connections,
|
||||||
, mk( string()
|
mk(
|
||||||
, #{desc => <<"CPU average load in 1 minute">>, example => "2.66"})}
|
non_neg_integer(),
|
||||||
, { load5
|
#{desc => <<"Number of clients currently connected to this node">>, example => 0}
|
||||||
, mk( string()
|
)},
|
||||||
, #{desc => <<"CPU average load in 5 minute">>, example => "2.66"})}
|
{load1,
|
||||||
, { load15
|
mk(
|
||||||
, mk( string()
|
string(),
|
||||||
, #{desc => <<"CPU average load in 15 minute">>, example => "2.66"})}
|
#{desc => <<"CPU average load in 1 minute">>, example => "2.66"}
|
||||||
, { max_fds
|
)},
|
||||||
, mk( non_neg_integer()
|
{load5,
|
||||||
, #{desc => <<"File descriptors limit">>, example => 1024})}
|
mk(
|
||||||
, { memory_total
|
string(),
|
||||||
, mk( emqx_schema:bytesize()
|
#{desc => <<"CPU average load in 5 minute">>, example => "2.66"}
|
||||||
, #{desc => <<"Allocated memory">>, example => "512.00M"})}
|
)},
|
||||||
, { memory_used
|
{load15,
|
||||||
, mk( emqx_schema:bytesize()
|
mk(
|
||||||
, #{desc => <<"Used memory">>, example => "256.00M"})}
|
string(),
|
||||||
, { node_status
|
#{desc => <<"CPU average load in 15 minute">>, example => "2.66"}
|
||||||
, mk( enum(['Running', 'Stopped'])
|
)},
|
||||||
, #{desc => <<"Node status">>, example => "Running"})}
|
{max_fds,
|
||||||
, { otp_release
|
mk(
|
||||||
, mk( string()
|
non_neg_integer(),
|
||||||
, #{ desc => <<"Erlang/OTP version">>, example => "24.2/12.2"})}
|
#{desc => <<"File descriptors limit">>, example => 1024}
|
||||||
, { process_available
|
)},
|
||||||
, mk( non_neg_integer()
|
{memory_total,
|
||||||
, #{desc => <<"Erlang processes limit">>, example => 2097152})}
|
mk(
|
||||||
, { process_used
|
emqx_schema:bytesize(),
|
||||||
, mk( non_neg_integer()
|
#{desc => <<"Allocated memory">>, example => "512.00M"}
|
||||||
, #{desc => <<"Running Erlang processes">>, example => 1024})}
|
)},
|
||||||
, { uptime
|
{memory_used,
|
||||||
, mk( non_neg_integer()
|
mk(
|
||||||
, #{desc => <<"System uptime, milliseconds">>, example => 5120000})}
|
emqx_schema:bytesize(),
|
||||||
, { version
|
#{desc => <<"Used memory">>, example => "256.00M"}
|
||||||
, mk( string()
|
)},
|
||||||
, #{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"})}
|
{node_status,
|
||||||
, { sys_path
|
mk(
|
||||||
, mk( string()
|
enum(['Running', 'Stopped']),
|
||||||
, #{desc => <<"Path to system files">>, example => "path/to/emqx"})}
|
#{desc => <<"Node status">>, example => "Running"}
|
||||||
, { log_path
|
)},
|
||||||
, mk( string()
|
{otp_release,
|
||||||
, #{desc => <<"Path to log files">>, example => "path/to/log | not found"})}
|
mk(
|
||||||
, { role
|
string(),
|
||||||
, mk( enum([core, replicant])
|
#{desc => <<"Erlang/OTP version">>, example => "24.2/12.2"}
|
||||||
, #{desc => <<"Node role">>, example => "core"})}
|
)},
|
||||||
|
{process_available,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"Erlang processes limit">>, example => 2097152}
|
||||||
|
)},
|
||||||
|
{process_used,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"Running Erlang processes">>, example => 1024}
|
||||||
|
)},
|
||||||
|
{uptime,
|
||||||
|
mk(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{desc => <<"System uptime, milliseconds">>, example => 5120000}
|
||||||
|
)},
|
||||||
|
{version,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"}
|
||||||
|
)},
|
||||||
|
{sys_path,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Path to system files">>, example => "path/to/emqx"}
|
||||||
|
)},
|
||||||
|
{log_path,
|
||||||
|
mk(
|
||||||
|
string(),
|
||||||
|
#{desc => <<"Path to log files">>, example => "path/to/log | not found"}
|
||||||
|
)},
|
||||||
|
{role,
|
||||||
|
mk(
|
||||||
|
enum([core, replicant]),
|
||||||
|
#{desc => <<"Node role">>, example => "core"}
|
||||||
|
)}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -221,17 +283,20 @@ get_stats(Node) ->
|
||||||
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
|
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
|
||||||
{ok, SysPathBinary} = file:get_cwd(),
|
{ok, SysPathBinary} = file:get_cwd(),
|
||||||
SysPath = list_to_binary(SysPathBinary),
|
SysPath = list_to_binary(SysPathBinary),
|
||||||
LogPath = case log_path() of
|
LogPath =
|
||||||
undefined ->
|
case log_path() of
|
||||||
<<"not found">>;
|
undefined ->
|
||||||
Path0 ->
|
<<"not found">>;
|
||||||
Path = list_to_binary(Path0),
|
Path0 ->
|
||||||
<<SysPath/binary, Path/binary>>
|
Path = list_to_binary(Path0),
|
||||||
end,
|
<<SysPath/binary, Path/binary>>
|
||||||
Info#{ memory_total := emqx_mgmt_util:kmg(Total)
|
end,
|
||||||
, memory_used := emqx_mgmt_util:kmg(Used)
|
Info#{
|
||||||
, sys_path => SysPath
|
memory_total := emqx_mgmt_util:kmg(Total),
|
||||||
, log_path => LogPath}.
|
memory_used := emqx_mgmt_util:kmg(Used),
|
||||||
|
sys_path => SysPath,
|
||||||
|
log_path => LogPath
|
||||||
|
}.
|
||||||
|
|
||||||
log_path() ->
|
log_path() ->
|
||||||
Configs = logger:get_handler_config(),
|
Configs = logger:get_handler_config(),
|
||||||
|
|
|
@ -22,27 +22,30 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
|
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, fields/1
|
api_spec/0,
|
||||||
, paths/0
|
fields/1,
|
||||||
, schema/1
|
paths/0,
|
||||||
, namespace/0
|
schema/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ list_plugins/2
|
-export([
|
||||||
, upload_install/2
|
list_plugins/2,
|
||||||
, plugin/2
|
upload_install/2,
|
||||||
, update_plugin/2
|
plugin/2,
|
||||||
, update_boot_order/2
|
update_plugin/2,
|
||||||
]).
|
update_boot_order/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ validate_name/1
|
-export([
|
||||||
, get_plugins/0
|
validate_name/1,
|
||||||
, install_package/2
|
get_plugins/0,
|
||||||
, delete_package/1
|
install_package/2,
|
||||||
, describe_package/1
|
delete_package/1,
|
||||||
, ensure_action/2
|
describe_package/1,
|
||||||
]).
|
ensure_action/2
|
||||||
|
]).
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
|
||||||
|
|
||||||
|
@ -65,9 +68,10 @@ schema("/plugins") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => list_plugins,
|
'operationId' => list_plugins,
|
||||||
get => #{
|
get => #{
|
||||||
description => "List all install plugins.<br>"
|
description =>
|
||||||
"Plugins are launched in top-down order.<br>"
|
"List all install plugins.<br>"
|
||||||
"Using `POST /plugins/{name}/move` to change the boot order.",
|
"Plugins are launched in top-down order.<br>"
|
||||||
|
"Using `POST /plugins/{name}/move` to change the boot order.",
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:array(hoconsc:ref(plugin))
|
200 => hoconsc:array(hoconsc:ref(plugin))
|
||||||
}
|
}
|
||||||
|
@ -77,20 +81,26 @@ schema("/plugins/install") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => upload_install,
|
'operationId' => upload_install,
|
||||||
post => #{
|
post => #{
|
||||||
description => "Install a plugin(plugin-vsn.tar.gz)."
|
description =>
|
||||||
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
"Install a plugin(plugin-vsn.tar.gz)."
|
||||||
"to develop plugin.",
|
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
||||||
|
"to develop plugin.",
|
||||||
'requestBody' => #{
|
'requestBody' => #{
|
||||||
content => #{
|
content => #{
|
||||||
'multipart/form-data' => #{
|
'multipart/form-data' => #{
|
||||||
schema => #{
|
schema => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => #{
|
properties => #{
|
||||||
plugin => #{type => string, format => binary}}},
|
plugin => #{type => string, format => binary}
|
||||||
encoding => #{plugin => #{'contentType' => 'application/gzip'}}}}},
|
}
|
||||||
|
},
|
||||||
|
encoding => #{plugin => #{'contentType' => 'application/gzip'}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"OK">>,
|
200 => <<"OK">>,
|
||||||
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR','ALREADY_INSTALLED'])
|
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR', 'ALREADY_INSTALLED'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -118,12 +128,14 @@ schema("/plugins/:name/:action") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => update_plugin,
|
'operationId' => update_plugin,
|
||||||
put => #{
|
put => #{
|
||||||
description => "start/stop a installed plugin.<br>"
|
description =>
|
||||||
"- **start**: start the plugin.<br>"
|
"start/stop a installed plugin.<br>"
|
||||||
"- **stop**: stop the plugin.<br>",
|
"- **start**: start the plugin.<br>"
|
||||||
|
"- **stop**: stop the plugin.<br>",
|
||||||
parameters => [
|
parameters => [
|
||||||
hoconsc:ref(name),
|
hoconsc:ref(name),
|
||||||
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}],
|
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}
|
||||||
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => <<"OK">>,
|
200 => <<"OK">>,
|
||||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
|
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
|
||||||
|
@ -143,57 +155,83 @@ schema("/plugins/:name/move") ->
|
||||||
|
|
||||||
fields(plugin) ->
|
fields(plugin) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{
|
hoconsc:mk(
|
||||||
desc => "Name-Vsn: without .tar.gz",
|
binary(),
|
||||||
validator => fun ?MODULE:validate_name/1,
|
#{
|
||||||
required => true,
|
desc => "Name-Vsn: without .tar.gz",
|
||||||
example => "emqx_plugin_template-5.0-rc.1"})
|
validator => fun ?MODULE:validate_name/1,
|
||||||
},
|
required => true,
|
||||||
|
example => "emqx_plugin_template-5.0-rc.1"
|
||||||
|
}
|
||||||
|
)},
|
||||||
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
|
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
|
||||||
{builder, hoconsc:ref(?MODULE, builder)},
|
{builder, hoconsc:ref(?MODULE, builder)},
|
||||||
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
|
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
|
||||||
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
|
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
|
||||||
{git_commit_or_build_date, hoconsc:mk(string(), #{
|
{git_commit_or_build_date,
|
||||||
example => "2021-12-25",
|
hoconsc:mk(string(), #{
|
||||||
desc => "Last git commit date by `git log -1 --pretty=format:'%cd' "
|
example => "2021-12-25",
|
||||||
"--date=format:'%Y-%m-%d`.\n"
|
desc =>
|
||||||
" If the last commit date is not available, the build date will be presented."
|
"Last git commit date by `git log -1 --pretty=format:'%cd' "
|
||||||
})},
|
"--date=format:'%Y-%m-%d`.\n"
|
||||||
|
" If the last commit date is not available, the build date will be presented."
|
||||||
|
})},
|
||||||
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
|
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
|
||||||
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
|
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
|
||||||
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
|
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
|
||||||
{rel_vsn, hoconsc:mk(binary(),
|
{rel_vsn,
|
||||||
#{desc => "Plugins release version",
|
hoconsc:mk(
|
||||||
required => true,
|
binary(),
|
||||||
example => <<"5.0-rc.1">>})
|
#{
|
||||||
},
|
desc => "Plugins release version",
|
||||||
{rel_apps, hoconsc:mk(hoconsc:array(binary()),
|
required => true,
|
||||||
#{desc => "Aplications in plugin.",
|
example => <<"5.0-rc.1">>
|
||||||
required => true,
|
}
|
||||||
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]})
|
)},
|
||||||
},
|
{rel_apps,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:array(binary()),
|
||||||
|
#{
|
||||||
|
desc => "Aplications in plugin.",
|
||||||
|
required => true,
|
||||||
|
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]
|
||||||
|
}
|
||||||
|
)},
|
||||||
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
|
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
|
||||||
{description, hoconsc:mk(binary(),
|
{description,
|
||||||
#{desc => "Plugin description.",
|
hoconsc:mk(
|
||||||
required => true,
|
binary(),
|
||||||
example => "This is an demo plugin description"})
|
#{
|
||||||
},
|
desc => "Plugin description.",
|
||||||
{running_status, hoconsc:mk(hoconsc:array(hoconsc:ref(running_status)),
|
required => true,
|
||||||
#{required => true})},
|
example => "This is an demo plugin description"
|
||||||
{readme, hoconsc:mk(binary(), #{
|
}
|
||||||
example => "This is an demo plugin.",
|
)},
|
||||||
desc => "only return when `GET /plugins/{name}`.",
|
{running_status,
|
||||||
required => false})}
|
hoconsc:mk(
|
||||||
|
hoconsc:array(hoconsc:ref(running_status)),
|
||||||
|
#{required => true}
|
||||||
|
)},
|
||||||
|
{readme,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
example => "This is an demo plugin.",
|
||||||
|
desc => "only return when `GET /plugins/{name}`.",
|
||||||
|
required => false
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => list_to_binary(?NAME_RE),
|
hoconsc:mk(
|
||||||
example => "emqx_plugin_template-5.0-rc.1",
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => list_to_binary(?NAME_RE),
|
||||||
})}
|
example => "emqx_plugin_template-5.0-rc.1",
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(builder) ->
|
fields(builder) ->
|
||||||
[
|
[
|
||||||
|
@ -202,27 +240,38 @@ fields(builder) ->
|
||||||
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
|
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
|
||||||
];
|
];
|
||||||
fields(position) ->
|
fields(position) ->
|
||||||
[{position, hoconsc:mk(hoconsc:union([front, rear, binary()]),
|
[
|
||||||
#{
|
{position,
|
||||||
desc => """
|
hoconsc:mk(
|
||||||
Enable auto-boot at position in the boot list, where Position could be
|
hoconsc:union([front, rear, binary()]),
|
||||||
'front', 'rear', or 'before:other-vsn', 'after:other-vsn'
|
#{
|
||||||
to specify a relative position.
|
desc =>
|
||||||
""",
|
""
|
||||||
required => false
|
"\n"
|
||||||
})}];
|
" Enable auto-boot at position in the boot list, where Position could be\n"
|
||||||
|
" 'front', 'rear', or 'before:other-vsn', 'after:other-vsn'\n"
|
||||||
|
" to specify a relative position.\n"
|
||||||
|
" "
|
||||||
|
"",
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(running_status) ->
|
fields(running_status) ->
|
||||||
[
|
[
|
||||||
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
|
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
|
||||||
{status, hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
{status,
|
||||||
desc => "Install plugin status at runtime</br>"
|
hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
||||||
"1. running: plugin is running.<br>"
|
desc =>
|
||||||
"2. stopped: plugin is stopped.<br>"
|
"Install plugin status at runtime</br>"
|
||||||
})}
|
"1. running: plugin is running.<br>"
|
||||||
|
"2. stopped: plugin is stopped.<br>"
|
||||||
|
})}
|
||||||
].
|
].
|
||||||
|
|
||||||
move_request_body() ->
|
move_request_body() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(hoconsc:ref(?MODULE, position),
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
|
hoconsc:ref(?MODULE, position),
|
||||||
#{
|
#{
|
||||||
move_to_front => #{
|
move_to_front => #{
|
||||||
summary => <<"move plugin on the front">>,
|
summary => <<"move plugin on the front">>,
|
||||||
|
@ -240,7 +289,8 @@ move_request_body() ->
|
||||||
summary => <<"move plugin after other plugins">>,
|
summary => <<"move plugin after other plugins">>,
|
||||||
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
|
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
|
||||||
}
|
}
|
||||||
}).
|
}
|
||||||
|
).
|
||||||
|
|
||||||
validate_name(Name) ->
|
validate_name(Name) ->
|
||||||
NameLen = byte_size(Name),
|
NameLen = byte_size(Name),
|
||||||
|
@ -250,7 +300,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% API CallBack Begin
|
%% API CallBack Begin
|
||||||
|
@ -271,29 +322,42 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) -
|
||||||
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
|
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
|
||||||
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
|
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
|
||||||
case filelib:wildcard(AppDir ++ "*.tar.gz") of
|
case filelib:wildcard(AppDir ++ "*.tar.gz") of
|
||||||
[] -> do_install_package(FileName, Bin);
|
[] ->
|
||||||
|
do_install_package(FileName, Bin);
|
||||||
OtherVsn ->
|
OtherVsn ->
|
||||||
{400, #{code => 'ALREADY_INSTALLED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p already installed",
|
code => 'ALREADY_INSTALLED',
|
||||||
[OtherVsn]))}}
|
message => iolist_to_binary(
|
||||||
|
io_lib:format(
|
||||||
|
"~p already installed",
|
||||||
|
[OtherVsn]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{400, #{code => 'ALREADY_INSTALLED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))}}
|
code => 'ALREADY_INSTALLED',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))
|
||||||
|
}}
|
||||||
end;
|
end;
|
||||||
upload_install(post, #{}) ->
|
upload_install(post, #{}) ->
|
||||||
{400, #{code => 'BAD_FORM_DATA',
|
{400, #{
|
||||||
|
code => 'BAD_FORM_DATA',
|
||||||
message =>
|
message =>
|
||||||
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>}
|
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>
|
||||||
}.
|
}}.
|
||||||
|
|
||||||
do_install_package(FileName, Bin) ->
|
do_install_package(FileName, Bin) ->
|
||||||
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
|
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
|
||||||
case lists:filter(fun(R) -> R =/= ok end, Res) of
|
case lists:filter(fun(R) -> R =/= ok end, Res) of
|
||||||
[] -> {200};
|
[] ->
|
||||||
|
{200};
|
||||||
[{error, Reason} | _] ->
|
[{error, Reason} | _] ->
|
||||||
{400, #{code => 'UNEXPECTED_ERROR',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'UNEXPECTED_ERROR',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
plugin(get, #{bindings := #{name := Name}}) ->
|
plugin(get, #{bindings := #{name := Name}}) ->
|
||||||
|
@ -302,7 +366,6 @@ plugin(get, #{bindings := #{name := Name}}) ->
|
||||||
[Plugin] -> {200, Plugin};
|
[Plugin] -> {200, Plugin};
|
||||||
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
|
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
plugin(delete, #{bindings := #{name := Name}}) ->
|
plugin(delete, #{bindings := #{name := Name}}) ->
|
||||||
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
|
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
|
||||||
return(204, Res).
|
return(204, Res).
|
||||||
|
@ -313,13 +376,17 @@ update_plugin(put, #{bindings := #{name := Name, action := Action}}) ->
|
||||||
|
|
||||||
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
|
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
|
||||||
case parse_position(Body, Name) of
|
case parse_position(Body, Name) of
|
||||||
{error, Reason} -> {400, #{code => 'BAD_POSITION', message => Reason}};
|
{error, Reason} ->
|
||||||
|
{400, #{code => 'BAD_POSITION', message => Reason}};
|
||||||
Position ->
|
Position ->
|
||||||
case emqx_plugins:ensure_enabled(Name, Position) of
|
case emqx_plugins:ensure_enabled(Name, Position) of
|
||||||
ok -> {200};
|
ok ->
|
||||||
|
{200};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{400, #{code => 'MOVE_FAILED',
|
{400, #{
|
||||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
code => 'MOVE_FAILED',
|
||||||
|
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||||
|
}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -347,7 +414,8 @@ delete_package(Name) ->
|
||||||
_ = emqx_plugins:ensure_disabled(Name),
|
_ = emqx_plugins:ensure_disabled(Name),
|
||||||
_ = emqx_plugins:purge(Name),
|
_ = emqx_plugins:purge(Name),
|
||||||
_ = emqx_plugins:delete_package(Name);
|
_ = emqx_plugins:delete_package(Name);
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% for RPC plugin update
|
%% for RPC plugin update
|
||||||
|
@ -361,15 +429,19 @@ ensure_action(Name, restart) ->
|
||||||
_ = emqx_plugins:ensure_enabled(Name),
|
_ = emqx_plugins:ensure_enabled(Name),
|
||||||
_ = emqx_plugins:restart(Name).
|
_ = emqx_plugins:restart(Name).
|
||||||
|
|
||||||
return(Code, ok) -> {Code};
|
return(Code, ok) ->
|
||||||
return(Code, {ok, Result}) -> {Code, Result};
|
{Code};
|
||||||
|
return(Code, {ok, Result}) ->
|
||||||
|
{Code, Result};
|
||||||
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
|
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
|
||||||
{404, #{code => 'NOT_FOUND', message => Path}};
|
{404, #{code => 'NOT_FOUND', message => Path}};
|
||||||
return(_, {error, Reason}) ->
|
return(_, {error, Reason}) ->
|
||||||
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
|
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
|
||||||
|
|
||||||
parse_position(#{<<"position">> := <<"front">>}, _) -> front;
|
parse_position(#{<<"position">> := <<"front">>}, _) ->
|
||||||
parse_position(#{<<"position">> := <<"rear">>}, _) -> rear;
|
front;
|
||||||
|
parse_position(#{<<"position">> := <<"rear">>}, _) ->
|
||||||
|
rear;
|
||||||
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
|
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
|
||||||
{error, <<"Invalid parameter. Cannot be placed before itself">>};
|
{error, <<"Invalid parameter. Cannot be placed before itself">>};
|
||||||
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
|
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
|
||||||
|
@ -382,7 +454,8 @@ parse_position(#{<<"position">> := <<"before:", Before/binary>>}, _Name) ->
|
||||||
{before, binary_to_list(Before)};
|
{before, binary_to_list(Before)};
|
||||||
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
|
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
|
||||||
{behind, binary_to_list(After)};
|
{behind, binary_to_list(After)};
|
||||||
parse_position(Position, _) -> {error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
parse_position(Position, _) ->
|
||||||
|
{error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
||||||
|
|
||||||
format_plugins(List) ->
|
format_plugins(List) ->
|
||||||
StatusMap = aggregate_status(List),
|
StatusMap = aggregate_status(List),
|
||||||
|
@ -392,13 +465,18 @@ format_plugins(List) ->
|
||||||
|
|
||||||
pack_status_in_order(List, StatusMap) ->
|
pack_status_in_order(List, StatusMap) ->
|
||||||
{Plugins, _} =
|
{Plugins, _} =
|
||||||
lists:foldl(fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
lists:foldl(
|
||||||
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
||||||
end, {[], StatusMap}, List),
|
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
||||||
|
end,
|
||||||
|
{[], StatusMap},
|
||||||
|
List
|
||||||
|
),
|
||||||
lists:reverse(Plugins).
|
lists:reverse(Plugins).
|
||||||
|
|
||||||
pack_plugin_in_order([], Acc, StatusAcc) -> {Acc, StatusAcc};
|
pack_plugin_in_order([], Acc, StatusAcc) ->
|
||||||
pack_plugin_in_order(_, Acc, StatusAcc)when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
{Acc, StatusAcc};
|
||||||
|
pack_plugin_in_order(_, Acc, StatusAcc) when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
||||||
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
||||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
|
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
|
||||||
case maps:find({Name, Vsn}, StatusAcc) of
|
case maps:find({Name, Vsn}, StatusAcc) of
|
||||||
|
@ -413,15 +491,20 @@ pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
||||||
|
|
||||||
aggregate_status(List) -> aggregate_status(List, #{}).
|
aggregate_status(List) -> aggregate_status(List, #{}).
|
||||||
|
|
||||||
aggregate_status([], Acc) -> Acc;
|
aggregate_status([], Acc) ->
|
||||||
|
Acc;
|
||||||
aggregate_status([{Node, Plugins} | List], Acc) ->
|
aggregate_status([{Node, Plugins} | List], Acc) ->
|
||||||
NewAcc =
|
NewAcc =
|
||||||
lists:foldl(fun(Plugin, SubAcc) ->
|
lists:foldl(
|
||||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
fun(Plugin, SubAcc) ->
|
||||||
Key = {Name, Vsn},
|
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
||||||
Value = #{node => Node, status => plugin_status(Plugin)},
|
Key = {Name, Vsn},
|
||||||
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
Value = #{node => Node, status => plugin_status(Plugin)},
|
||||||
end, Acc, Plugins),
|
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
||||||
|
end,
|
||||||
|
Acc,
|
||||||
|
Plugins
|
||||||
|
),
|
||||||
aggregate_status(List, NewAcc).
|
aggregate_status(List, NewAcc).
|
||||||
|
|
||||||
% running_status: running loaded, stopped
|
% running_status: running loaded, stopped
|
||||||
|
|
|
@ -20,14 +20,17 @@
|
||||||
|
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ publish/2
|
-export([
|
||||||
, publish_batch/2]).
|
publish/2,
|
||||||
|
publish_batch/2
|
||||||
|
]).
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
@ -46,7 +49,6 @@ schema("/publish") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
schema("/publish/bulk") ->
|
schema("/publish/bulk") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => publish_batch,
|
'operationId' => publish_batch,
|
||||||
|
@ -61,32 +63,43 @@ schema("/publish/bulk") ->
|
||||||
|
|
||||||
fields(publish_message) ->
|
fields(publish_message) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{
|
{topic,
|
||||||
desc => <<"Topic Name">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true,
|
desc => <<"Topic Name">>,
|
||||||
example => <<"api/example/topic">>})},
|
required => true,
|
||||||
{qos, hoconsc:mk(emqx_schema:qos(), #{
|
example => <<"api/example/topic">>
|
||||||
desc => <<"MQTT QoS">>,
|
})},
|
||||||
required => false,
|
{qos,
|
||||||
default => 0})},
|
hoconsc:mk(emqx_schema:qos(), #{
|
||||||
{from, hoconsc:mk(binary(), #{
|
desc => <<"MQTT QoS">>,
|
||||||
desc => <<"From client ID">>,
|
required => false,
|
||||||
required => false,
|
default => 0
|
||||||
example => <<"api_example_client">>})},
|
})},
|
||||||
{payload, hoconsc:mk(binary(), #{
|
{from,
|
||||||
desc => <<"MQTT Payload">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true,
|
desc => <<"From client ID">>,
|
||||||
example => <<"hello emqx api">>})},
|
required => false,
|
||||||
{retain, hoconsc:mk(boolean(), #{
|
example => <<"api_example_client">>
|
||||||
desc => <<"MQTT Retain Message">>,
|
})},
|
||||||
required => false,
|
{payload,
|
||||||
default => false})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"MQTT Payload">>,
|
||||||
|
required => true,
|
||||||
|
example => <<"hello emqx api">>
|
||||||
|
})},
|
||||||
|
{retain,
|
||||||
|
hoconsc:mk(boolean(), #{
|
||||||
|
desc => <<"MQTT Retain Message">>,
|
||||||
|
required => false,
|
||||||
|
default => false
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(publish_message_info) ->
|
fields(publish_message_info) ->
|
||||||
[
|
[
|
||||||
{id, hoconsc:mk(binary(), #{
|
{id,
|
||||||
desc => <<"Internal Message ID">>})}
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"Internal Message ID">>
|
||||||
|
})}
|
||||||
] ++ fields(publish_message).
|
] ++ fields(publish_message).
|
||||||
|
|
||||||
publish(post, #{body := Body}) ->
|
publish(post, #{body := Body}) ->
|
||||||
|
@ -100,19 +113,21 @@ publish_batch(post, #{body := Body}) ->
|
||||||
{200, format_message(Messages)}.
|
{200, format_message(Messages)}.
|
||||||
|
|
||||||
message(Map) ->
|
message(Map) ->
|
||||||
From = maps:get(<<"from">>, Map, http_api),
|
From = maps:get(<<"from">>, Map, http_api),
|
||||||
QoS = maps:get(<<"qos">>, Map, 0),
|
QoS = maps:get(<<"qos">>, Map, 0),
|
||||||
Topic = maps:get(<<"topic">>, Map),
|
Topic = maps:get(<<"topic">>, Map),
|
||||||
Payload = maps:get(<<"payload">>, Map),
|
Payload = maps:get(<<"payload">>, Map),
|
||||||
Retain = maps:get(<<"retain">>, Map, false),
|
Retain = maps:get(<<"retain">>, Map, false),
|
||||||
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
|
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
|
||||||
|
|
||||||
messages(List) ->
|
messages(List) ->
|
||||||
[message(MessageMap) || MessageMap <- List].
|
[message(MessageMap) || MessageMap <- List].
|
||||||
|
|
||||||
format_message(Messages) when is_list(Messages)->
|
format_message(Messages) when is_list(Messages) ->
|
||||||
[format_message(Message) || Message <- Messages];
|
[format_message(Message) || Message <- Messages];
|
||||||
format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags}) ->
|
format_message(#message{
|
||||||
|
id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags
|
||||||
|
}) ->
|
||||||
#{
|
#{
|
||||||
id => emqx_guid:to_hexstr(ID),
|
id => emqx_guid:to_hexstr(ID),
|
||||||
qos => Qos,
|
qos => Qos,
|
||||||
|
@ -124,5 +139,5 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload
|
||||||
|
|
||||||
to_binary(Data) when is_binary(Data) ->
|
to_binary(Data) when is_binary(Data) ->
|
||||||
Data;
|
Data;
|
||||||
to_binary(Data) ->
|
to_binary(Data) ->
|
||||||
list_to_binary(io_lib:format("~p", [Data])).
|
list_to_binary(io_lib:format("~p", [Data])).
|
||||||
|
|
|
@ -19,17 +19,22 @@
|
||||||
|
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-import( hoconsc
|
-import(
|
||||||
, [ mk/2
|
hoconsc,
|
||||||
, ref/1
|
[
|
||||||
, ref/2
|
mk/2,
|
||||||
, array/1]).
|
ref/1,
|
||||||
|
ref/2,
|
||||||
|
array/1
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([list/2]).
|
-export([list/2]).
|
||||||
|
|
||||||
|
@ -40,27 +45,38 @@ paths() ->
|
||||||
["/stats"].
|
["/stats"].
|
||||||
|
|
||||||
schema("/stats") ->
|
schema("/stats") ->
|
||||||
#{ 'operationId' => list
|
#{
|
||||||
, get =>
|
'operationId' => list,
|
||||||
#{ description => <<"EMQX stats">>
|
get =>
|
||||||
, tags => [<<"stats">>]
|
#{
|
||||||
, parameters => [ref(aggregate)]
|
description => <<"EMQX stats">>,
|
||||||
, responses =>
|
tags => [<<"stats">>],
|
||||||
#{ 200 => mk( hoconsc:union([ ref(?MODULE, node_stats_data)
|
parameters => [ref(aggregate)],
|
||||||
, array(ref(?MODULE, aggergate_data))
|
responses =>
|
||||||
])
|
#{
|
||||||
, #{ desc => <<"List stats ok">> })
|
200 => mk(
|
||||||
}
|
hoconsc:union([
|
||||||
|
ref(?MODULE, node_stats_data),
|
||||||
|
array(ref(?MODULE, aggergate_data))
|
||||||
|
]),
|
||||||
|
#{desc => <<"List stats ok">>}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(aggregate) ->
|
fields(aggregate) ->
|
||||||
[ { aggregate
|
[
|
||||||
, mk( boolean()
|
{aggregate,
|
||||||
, #{ desc => <<"Calculation aggregate for all nodes">>
|
mk(
|
||||||
, in => query
|
boolean(),
|
||||||
, required => false
|
#{
|
||||||
, example => false})}
|
desc => <<"Calculation aggregate for all nodes">>,
|
||||||
|
in => query,
|
||||||
|
required => false,
|
||||||
|
example => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node_stats_data) ->
|
fields(node_stats_data) ->
|
||||||
[
|
[
|
||||||
|
@ -80,17 +96,25 @@ fields(node_stats_data) ->
|
||||||
stats_schema('suboptions.max', <<"subscriptions.max">>),
|
stats_schema('suboptions.max', <<"subscriptions.max">>),
|
||||||
stats_schema('subscribers.count', <<"Number of current subscribers">>),
|
stats_schema('subscribers.count', <<"Number of current subscribers">>),
|
||||||
stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
|
stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
|
||||||
stats_schema('subscriptions.count', <<"Number of current subscriptions, including shared subscriptions">>),
|
stats_schema(
|
||||||
|
'subscriptions.count',
|
||||||
|
<<"Number of current subscriptions, including shared subscriptions">>
|
||||||
|
),
|
||||||
stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
|
stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
|
||||||
stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
|
stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
|
||||||
stats_schema('subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>),
|
stats_schema(
|
||||||
|
'subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>
|
||||||
|
),
|
||||||
stats_schema('topics.count', <<"Number of current topics">>),
|
stats_schema('topics.count', <<"Number of current topics">>),
|
||||||
stats_schema('topics.max', <<"Historical maximum number of topics">>)
|
stats_schema('topics.max', <<"Historical maximum number of topics">>)
|
||||||
];
|
];
|
||||||
fields(aggergate_data) ->
|
fields(aggergate_data) ->
|
||||||
[ { node
|
[
|
||||||
, mk( string(), #{ desc => <<"Node name">>
|
{node,
|
||||||
, example => <<"emqx@127.0.0.1">>})}
|
mk(string(), #{
|
||||||
|
desc => <<"Node name">>,
|
||||||
|
example => <<"emqx@127.0.0.1">>
|
||||||
|
})}
|
||||||
] ++ fields(node_stats_data).
|
] ++ fields(node_stats_data).
|
||||||
|
|
||||||
stats_schema(Name, Desc) ->
|
stats_schema(Name, Desc) ->
|
||||||
|
@ -103,7 +127,9 @@ list(get, #{query_string := Qs}) ->
|
||||||
true ->
|
true ->
|
||||||
{200, emqx_mgmt:get_stats()};
|
{200, emqx_mgmt:get_stats()};
|
||||||
_ ->
|
_ ->
|
||||||
Data = [maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}]) ||
|
Data = [
|
||||||
Node <- mria_mnesia:running_nodes()],
|
maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}])
|
||||||
|
|| Node <- mria_mnesia:running_nodes()
|
||||||
|
],
|
||||||
{200, Data}
|
{200, Data}
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
%% API
|
%% API
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
]).
|
schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([running_status/2]).
|
-export([running_status/2]).
|
||||||
|
|
||||||
|
@ -31,22 +32,30 @@ paths() ->
|
||||||
["/status"].
|
["/status"].
|
||||||
|
|
||||||
schema("/status") ->
|
schema("/status") ->
|
||||||
#{ 'operationId' => running_status
|
#{
|
||||||
, get =>
|
'operationId' => running_status,
|
||||||
#{ description => <<"Node running status">>
|
get =>
|
||||||
, security => []
|
#{
|
||||||
, responses =>
|
description => <<"Node running status">>,
|
||||||
#{200 =>
|
security => [],
|
||||||
#{ description => <<"Node is running">>
|
responses =>
|
||||||
, content =>
|
#{
|
||||||
#{ 'text/plain' =>
|
200 =>
|
||||||
#{ schema => #{type => string}
|
#{
|
||||||
, example => <<"Node emqx@127.0.0.1 is started\nemqx is running">>}
|
description => <<"Node is running">>,
|
||||||
}
|
content =>
|
||||||
}
|
#{
|
||||||
}
|
'text/plain' =>
|
||||||
|
#{
|
||||||
|
schema => #{type => string},
|
||||||
|
example =>
|
||||||
|
<<"Node emqx@127.0.0.1 is started\nemqx is running">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% API Handler funcs
|
%% API Handler funcs
|
||||||
|
@ -62,7 +71,7 @@ running_status(get, _Params) ->
|
||||||
end,
|
end,
|
||||||
AppStatus =
|
AppStatus =
|
||||||
case lists:keysearch(emqx, 1, application:which_applications()) of
|
case lists:keysearch(emqx, 1, application:which_applications()) of
|
||||||
false -> not_running;
|
false -> not_running;
|
||||||
{value, _Val} -> running
|
{value, _Val} -> running
|
||||||
end,
|
end,
|
||||||
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
|
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
|
||||||
|
|
|
@ -22,26 +22,30 @@
|
||||||
-include_lib("emqx/include/emqx.hrl").
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1]).
|
schema/1,
|
||||||
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([subscriptions/2]).
|
-export([subscriptions/2]).
|
||||||
|
|
||||||
-export([ query/4
|
-export([
|
||||||
, format/1
|
query/4,
|
||||||
]).
|
format/1
|
||||||
|
]).
|
||||||
|
|
||||||
-define(SUBS_QTABLE, emqx_suboption).
|
-define(SUBS_QTABLE, emqx_suboption).
|
||||||
|
|
||||||
-define(SUBS_QSCHEMA,
|
-define(SUBS_QSCHEMA, [
|
||||||
[ {<<"clientid">>, binary}
|
{<<"clientid">>, binary},
|
||||||
, {<<"topic">>, binary}
|
{<<"topic">>, binary},
|
||||||
, {<<"share">>, binary}
|
{<<"share">>, binary},
|
||||||
, {<<"share_group">>, binary}
|
{<<"share_group">>, binary},
|
||||||
, {<<"qos">>, integer}
|
{<<"qos">>, integer},
|
||||||
, {<<"match_topic">>, binary}]).
|
{<<"match_topic">>, binary}
|
||||||
|
]).
|
||||||
|
|
||||||
-define(QUERY_FUN, {?MODULE, query}).
|
-define(QUERY_FUN, {?MODULE, query}).
|
||||||
|
|
||||||
|
@ -58,7 +62,9 @@ schema("/subscriptions") ->
|
||||||
description => <<"List subscriptions">>,
|
description => <<"List subscriptions">>,
|
||||||
parameters => parameters(),
|
parameters => parameters(),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})}}
|
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})
|
||||||
|
}
|
||||||
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(subscription) ->
|
fields(subscription) ->
|
||||||
|
@ -74,41 +80,53 @@ parameters() ->
|
||||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||||
{
|
{
|
||||||
node, hoconsc:mk(binary(), #{
|
node,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Node name">>,
|
required => false,
|
||||||
example => atom_to_list(node())})
|
desc => <<"Node name">>,
|
||||||
|
example => atom_to_list(node())
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
clientid, hoconsc:mk(binary(), #{
|
clientid,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Client ID">>})
|
required => false,
|
||||||
|
desc => <<"Client ID">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
qos, hoconsc:mk(emqx_schema:qos(), #{
|
qos,
|
||||||
in => query,
|
hoconsc:mk(emqx_schema:qos(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"QoS">>})
|
required => false,
|
||||||
|
desc => <<"QoS">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic, hoconsc:mk(binary(), #{
|
topic,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Topic, url encoding">>})
|
required => false,
|
||||||
|
desc => <<"Topic, url encoding">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match_topic, hoconsc:mk(binary(), #{
|
match_topic,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Match topic string, url encoding">>})
|
required => false,
|
||||||
|
desc => <<"Match topic string, url encoding">>
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
share_group, hoconsc:mk(binary(), #{
|
share_group,
|
||||||
in => query,
|
hoconsc:mk(binary(), #{
|
||||||
required => false,
|
in => query,
|
||||||
desc => <<"Shared subscription group name">>})
|
required => false,
|
||||||
|
desc => <<"Shared subscription group name">>
|
||||||
|
})
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
@ -116,11 +134,20 @@ subscriptions(get, #{query_string := QString}) ->
|
||||||
Response =
|
Response =
|
||||||
case maps:get(<<"node">>, QString, undefined) of
|
case maps:get(<<"node">>, QString, undefined) of
|
||||||
undefined ->
|
undefined ->
|
||||||
emqx_mgmt_api:cluster_query(QString, ?SUBS_QTABLE,
|
emqx_mgmt_api:cluster_query(
|
||||||
?SUBS_QSCHEMA, ?QUERY_FUN);
|
QString,
|
||||||
|
?SUBS_QTABLE,
|
||||||
|
?SUBS_QSCHEMA,
|
||||||
|
?QUERY_FUN
|
||||||
|
);
|
||||||
Node0 ->
|
Node0 ->
|
||||||
emqx_mgmt_api:node_query(binary_to_atom(Node0, utf8), QString,
|
emqx_mgmt_api:node_query(
|
||||||
?SUBS_QTABLE, ?SUBS_QSCHEMA, ?QUERY_FUN)
|
binary_to_atom(Node0, utf8),
|
||||||
|
QString,
|
||||||
|
?SUBS_QTABLE,
|
||||||
|
?SUBS_QSCHEMA,
|
||||||
|
?QUERY_FUN
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
case Response of
|
case Response of
|
||||||
{error, page_limit_invalid} ->
|
{error, page_limit_invalid} ->
|
||||||
|
@ -134,10 +161,8 @@ subscriptions(get, #{query_string := QString}) ->
|
||||||
|
|
||||||
format(Items) when is_list(Items) ->
|
format(Items) when is_list(Items) ->
|
||||||
[format(Item) || Item <- Items];
|
[format(Item) || Item <- Items];
|
||||||
|
|
||||||
format({{Subscriber, Topic}, Options}) ->
|
format({{Subscriber, Topic}, Options}) ->
|
||||||
format({Subscriber, Topic, Options});
|
format({Subscriber, Topic, Options});
|
||||||
|
|
||||||
format({_Subscriber, Topic, Options = #{share := Group}}) ->
|
format({_Subscriber, Topic, Options = #{share := Group}}) ->
|
||||||
QoS = maps:get(qos, Options),
|
QoS = maps:get(qos, Options),
|
||||||
#{
|
#{
|
||||||
|
@ -161,19 +186,30 @@ format({_Subscriber, Topic, Options}) ->
|
||||||
|
|
||||||
query(Tab, {Qs, []}, Continuation, Limit) ->
|
query(Tab, {Qs, []}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs),
|
Ms = qs2ms(Qs),
|
||||||
emqx_mgmt_api:select_table_with_count( Tab, Ms
|
emqx_mgmt_api:select_table_with_count(
|
||||||
, Continuation, Limit, fun format/1);
|
Tab,
|
||||||
|
Ms,
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format/1
|
||||||
|
);
|
||||||
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
|
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs),
|
Ms = qs2ms(Qs),
|
||||||
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
|
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
|
||||||
emqx_mgmt_api:select_table_with_count( Tab, {Ms, FuzzyFilterFun}
|
emqx_mgmt_api:select_table_with_count(
|
||||||
, Continuation, Limit, fun format/1).
|
Tab,
|
||||||
|
{Ms, FuzzyFilterFun},
|
||||||
|
Continuation,
|
||||||
|
Limit,
|
||||||
|
fun format/1
|
||||||
|
).
|
||||||
|
|
||||||
fuzzy_filter_fun(Fuzzy) ->
|
fuzzy_filter_fun(Fuzzy) ->
|
||||||
fun(MsRaws) when is_list(MsRaws) ->
|
fun(MsRaws) when is_list(MsRaws) ->
|
||||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
lists:filter(
|
||||||
, MsRaws)
|
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||||
|
MsRaws
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_fuzzy_filter(_, []) ->
|
run_fuzzy_filter(_, []) ->
|
||||||
|
|
|
@ -22,23 +22,24 @@
|
||||||
%% API
|
%% API
|
||||||
-behaviour(minirest_api).
|
-behaviour(minirest_api).
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, paths/0
|
api_spec/0,
|
||||||
, schema/1
|
paths/0,
|
||||||
, fields/1
|
schema/1,
|
||||||
]).
|
fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ topics/2
|
-export([
|
||||||
, topic/2
|
topics/2,
|
||||||
]).
|
topic/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ query/4]).
|
-export([query/4]).
|
||||||
|
|
||||||
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
|
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
|
||||||
|
|
||||||
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
|
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
|
||||||
|
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
|
@ -73,19 +74,23 @@ schema("/topics/:topic") ->
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
|
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
|
||||||
404 =>
|
404 =>
|
||||||
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'],<<"Topic not found">>)
|
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'], <<"Topic not found">>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(topic) ->
|
fields(topic) ->
|
||||||
[
|
[
|
||||||
{topic, hoconsc:mk(binary(), #{
|
{topic,
|
||||||
desc => <<"Topic Name">>,
|
hoconsc:mk(binary(), #{
|
||||||
required => true})},
|
desc => <<"Topic Name">>,
|
||||||
{node, hoconsc:mk(binary(), #{
|
required => true
|
||||||
desc => <<"Node">>,
|
})},
|
||||||
required => true})}
|
{node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
|
desc => <<"Node">>,
|
||||||
|
required => true
|
||||||
|
})}
|
||||||
];
|
];
|
||||||
fields(meta) ->
|
fields(meta) ->
|
||||||
emqx_dashboard_swagger:fields(page) ++
|
emqx_dashboard_swagger:fields(page) ++
|
||||||
|
@ -103,8 +108,11 @@ topic(get, #{bindings := Bindings}) ->
|
||||||
%%%==============================================================================================
|
%%%==============================================================================================
|
||||||
%% api apply
|
%% api apply
|
||||||
do_list(Params) ->
|
do_list(Params) ->
|
||||||
case emqx_mgmt_api:node_query(
|
case
|
||||||
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}) of
|
emqx_mgmt_api:node_query(
|
||||||
|
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}
|
||||||
|
)
|
||||||
|
of
|
||||||
{error, page_limit_invalid} ->
|
{error, page_limit_invalid} ->
|
||||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||||
{error, Node, {badrpc, R}} ->
|
{error, Node, {badrpc, R}} ->
|
||||||
|
@ -128,16 +136,18 @@ generate_topic(Params = #{<<"topic">> := Topic}) ->
|
||||||
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
|
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
|
||||||
generate_topic(Params = #{topic := Topic}) ->
|
generate_topic(Params = #{topic := Topic}) ->
|
||||||
Params#{topic => uri_string:percent_decode(Topic)};
|
Params#{topic => uri_string:percent_decode(Topic)};
|
||||||
generate_topic(Params) -> Params.
|
generate_topic(Params) ->
|
||||||
|
Params.
|
||||||
|
|
||||||
query(Tab, {Qs, _}, Continuation, Limit) ->
|
query(Tab, {Qs, _}, Continuation, Limit) ->
|
||||||
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
|
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
|
||||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
|
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
|
||||||
|
|
||||||
qs2ms([], Res) -> Res;
|
qs2ms([], Res) ->
|
||||||
qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
Res;
|
||||||
|
qs2ms([{topic, '=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
||||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
|
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
|
||||||
qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
qs2ms([{node, '=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
||||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
|
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
|
||||||
|
|
||||||
format(#route{topic = Topic, dest = {_, Node}}) ->
|
format(#route{topic = Topic, dest = {_, Node}}) ->
|
||||||
|
@ -147,7 +157,8 @@ format(#route{topic = Topic, dest = Node}) ->
|
||||||
|
|
||||||
topic_param(In) ->
|
topic_param(In) ->
|
||||||
{
|
{
|
||||||
topic, hoconsc:mk(binary(), #{
|
topic,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Topic Name">>,
|
desc => <<"Topic Name">>,
|
||||||
in => In,
|
in => In,
|
||||||
required => (In == path),
|
required => (In == path),
|
||||||
|
@ -155,9 +166,10 @@ topic_param(In) ->
|
||||||
})
|
})
|
||||||
}.
|
}.
|
||||||
|
|
||||||
node_param()->
|
node_param() ->
|
||||||
{
|
{
|
||||||
node, hoconsc:mk(binary(), #{
|
node,
|
||||||
|
hoconsc:mk(binary(), #{
|
||||||
desc => <<"Node Name">>,
|
desc => <<"Node Name">>,
|
||||||
in => query,
|
in => query,
|
||||||
required => false,
|
required => false,
|
||||||
|
|
|
@ -21,26 +21,29 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([ api_spec/0
|
-export([
|
||||||
, fields/1
|
api_spec/0,
|
||||||
, paths/0
|
fields/1,
|
||||||
, schema/1
|
paths/0,
|
||||||
, namespace/0
|
schema/1,
|
||||||
]).
|
namespace/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ trace/2
|
-export([
|
||||||
, delete_trace/2
|
trace/2,
|
||||||
, update_trace/2
|
delete_trace/2,
|
||||||
, download_trace_log/2
|
update_trace/2,
|
||||||
, stream_log_file/2
|
download_trace_log/2,
|
||||||
]).
|
stream_log_file/2
|
||||||
|
]).
|
||||||
|
|
||||||
-export([validate_name/1]).
|
-export([validate_name/1]).
|
||||||
|
|
||||||
%% for rpc
|
%% for rpc
|
||||||
-export([ read_trace_file/3
|
-export([
|
||||||
, get_trace_size/0
|
read_trace_file/3,
|
||||||
]).
|
get_trace_size/0
|
||||||
|
]).
|
||||||
|
|
||||||
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
|
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
|
||||||
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
|
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
|
||||||
|
@ -53,7 +56,6 @@ api_spec() ->
|
||||||
paths() ->
|
paths() ->
|
||||||
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
|
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
|
||||||
|
|
||||||
|
|
||||||
schema("/trace") ->
|
schema("/trace") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => trace,
|
'operationId' => trace,
|
||||||
|
@ -68,9 +70,14 @@ schema("/trace") ->
|
||||||
'requestBody' => delete([status, log_size], fields(trace)),
|
'requestBody' => delete([status, log_size], fields(trace)),
|
||||||
responses => #{
|
responses => #{
|
||||||
200 => hoconsc:ref(trace),
|
200 => hoconsc:ref(trace),
|
||||||
400 => emqx_dashboard_swagger:error_codes(['ALREADY_EXISTS',
|
400 => emqx_dashboard_swagger:error_codes(
|
||||||
'DUPLICATE_CONDITION', 'INVALID_PARAMS'],
|
[
|
||||||
<<"trace name already exists">>)
|
'ALREADY_EXISTS',
|
||||||
|
'DUPLICATE_CONDITION',
|
||||||
|
'INVALID_PARAMS'
|
||||||
|
],
|
||||||
|
<<"trace name already exists">>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
delete => #{
|
delete => #{
|
||||||
|
@ -112,12 +119,13 @@ schema("/trace/:name/download") ->
|
||||||
parameters => [hoconsc:ref(name)],
|
parameters => [hoconsc:ref(name)],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>
|
200 =>
|
||||||
#{description => "A trace zip file",
|
#{
|
||||||
content => #{
|
description => "A trace zip file",
|
||||||
'application/octet-stream' =>
|
content => #{
|
||||||
#{schema => #{type => "string", format => "binary"}}
|
'application/octet-stream' =>
|
||||||
|
#{schema => #{type => "string", format => "binary"}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -134,92 +142,151 @@ schema("/trace/:name/log") ->
|
||||||
],
|
],
|
||||||
responses => #{
|
responses => #{
|
||||||
200 =>
|
200 =>
|
||||||
[
|
[
|
||||||
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
||||||
| fields(bytes) ++ fields(position)
|
| fields(bytes) ++ fields(position)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
fields(trace) ->
|
fields(trace) ->
|
||||||
[
|
[
|
||||||
{name, hoconsc:mk(binary(),
|
{name,
|
||||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
hoconsc:mk(
|
||||||
validator => fun ?MODULE:validate_name/1,
|
binary(),
|
||||||
required => true,
|
#{
|
||||||
example => <<"EMQX-TRACE-1">>})},
|
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||||
{type, hoconsc:mk(hoconsc:enum([clientid, topic, ip_address]),
|
validator => fun ?MODULE:validate_name/1,
|
||||||
#{desc => """Filter type""",
|
required => true,
|
||||||
required => true,
|
example => <<"EMQX-TRACE-1">>
|
||||||
example => <<"clientid">>})},
|
}
|
||||||
{topic, hoconsc:mk(binary(),
|
)},
|
||||||
#{desc => """support mqtt wildcard topic.""",
|
{type,
|
||||||
required => false,
|
hoconsc:mk(
|
||||||
example => <<"/dev/#">>})},
|
hoconsc:enum([clientid, topic, ip_address]),
|
||||||
{clientid, hoconsc:mk(binary(),
|
#{
|
||||||
#{desc => """mqtt clientid.""",
|
desc => "" "Filter type" "",
|
||||||
required => false,
|
required => true,
|
||||||
example => <<"dev-001">>})},
|
example => <<"clientid">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{topic,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
desc => "" "support mqtt wildcard topic." "",
|
||||||
|
required => false,
|
||||||
|
example => <<"/dev/#">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{clientid,
|
||||||
|
hoconsc:mk(
|
||||||
|
binary(),
|
||||||
|
#{
|
||||||
|
desc => "" "mqtt clientid." "",
|
||||||
|
required => false,
|
||||||
|
example => <<"dev-001">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
%% TODO add ip_address type in emqx_schema.erl
|
%% TODO add ip_address type in emqx_schema.erl
|
||||||
{ip_address, hoconsc:mk(binary(),
|
{ip_address,
|
||||||
#{desc => "client ip address",
|
hoconsc:mk(
|
||||||
required => false,
|
binary(),
|
||||||
example => <<"127.0.0.1">>
|
#{
|
||||||
})},
|
desc => "client ip address",
|
||||||
{status, hoconsc:mk(hoconsc:enum([running, stopped, waiting]),
|
required => false,
|
||||||
#{desc => "trace status",
|
example => <<"127.0.0.1">>
|
||||||
required => false,
|
}
|
||||||
example => running
|
)},
|
||||||
})},
|
{status,
|
||||||
{start_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
hoconsc:mk(
|
||||||
#{desc => "rfc3339 timestamp or epoch second",
|
hoconsc:enum([running, stopped, waiting]),
|
||||||
required => false,
|
#{
|
||||||
example => <<"2021-11-04T18:17:38+08:00">>
|
desc => "trace status",
|
||||||
})},
|
required => false,
|
||||||
{end_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
example => running
|
||||||
#{desc => "rfc3339 timestamp or epoch second",
|
}
|
||||||
required => false,
|
)},
|
||||||
example => <<"2021-11-05T18:17:38+08:00">>
|
{start_at,
|
||||||
})},
|
hoconsc:mk(
|
||||||
{log_size, hoconsc:mk(hoconsc:array(map()),
|
emqx_datetime:epoch_second(),
|
||||||
#{desc => "trace log size",
|
#{
|
||||||
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
desc => "rfc3339 timestamp or epoch second",
|
||||||
required => false})}
|
required => false,
|
||||||
|
example => <<"2021-11-04T18:17:38+08:00">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{end_at,
|
||||||
|
hoconsc:mk(
|
||||||
|
emqx_datetime:epoch_second(),
|
||||||
|
#{
|
||||||
|
desc => "rfc3339 timestamp or epoch second",
|
||||||
|
required => false,
|
||||||
|
example => <<"2021-11-05T18:17:38+08:00">>
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{log_size,
|
||||||
|
hoconsc:mk(
|
||||||
|
hoconsc:array(map()),
|
||||||
|
#{
|
||||||
|
desc => "trace log size",
|
||||||
|
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(name) ->
|
fields(name) ->
|
||||||
[{name, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{name,
|
||||||
desc => <<"[a-zA-Z0-9-_]">>,
|
hoconsc:mk(
|
||||||
example => <<"EMQX-TRACE-1">>,
|
binary(),
|
||||||
in => path,
|
#{
|
||||||
validator => fun ?MODULE:validate_name/1
|
desc => <<"[a-zA-Z0-9-_]">>,
|
||||||
})}
|
example => <<"EMQX-TRACE-1">>,
|
||||||
|
in => path,
|
||||||
|
validator => fun ?MODULE:validate_name/1
|
||||||
|
}
|
||||||
|
)}
|
||||||
];
|
];
|
||||||
fields(node) ->
|
fields(node) ->
|
||||||
[{node, hoconsc:mk(binary(),
|
[
|
||||||
#{
|
{node,
|
||||||
desc => "Node name",
|
hoconsc:mk(
|
||||||
in => query,
|
binary(),
|
||||||
required => false
|
#{
|
||||||
})}];
|
desc => "Node name",
|
||||||
|
in => query,
|
||||||
|
required => false
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(bytes) ->
|
fields(bytes) ->
|
||||||
[{bytes, hoconsc:mk(integer(),
|
[
|
||||||
#{
|
{bytes,
|
||||||
desc => "Maximum number of bytes to store in request",
|
hoconsc:mk(
|
||||||
in => query,
|
integer(),
|
||||||
required => false,
|
#{
|
||||||
default => 1000
|
desc => "Maximum number of bytes to store in request",
|
||||||
})}];
|
in => query,
|
||||||
|
required => false,
|
||||||
|
default => 1000
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(position) ->
|
fields(position) ->
|
||||||
[{position, hoconsc:mk(integer(),
|
[
|
||||||
#{
|
{position,
|
||||||
desc => "Offset from the current trace position.",
|
hoconsc:mk(
|
||||||
in => query,
|
integer(),
|
||||||
required => false,
|
#{
|
||||||
default => 0
|
desc => "Offset from the current trace position.",
|
||||||
})}].
|
in => query,
|
||||||
|
required => false,
|
||||||
|
default => 0
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
].
|
||||||
|
|
||||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||||
|
|
||||||
|
@ -231,7 +298,8 @@ validate_name(Name) ->
|
||||||
nomatch -> {error, "Name should be " ?NAME_RE};
|
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||||
_ -> ok
|
_ -> ok
|
||||||
end;
|
end;
|
||||||
false -> {error, "Name Length must =< 256"}
|
false ->
|
||||||
|
{error, "Name Length must =< 256"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete(Keys, Fields) ->
|
delete(Keys, Fields) ->
|
||||||
|
@ -239,32 +307,48 @@ delete(Keys, Fields) ->
|
||||||
|
|
||||||
trace(get, _Params) ->
|
trace(get, _Params) ->
|
||||||
case emqx_trace:list() of
|
case emqx_trace:list() of
|
||||||
[] -> {200, []};
|
[] ->
|
||||||
|
{200, []};
|
||||||
List0 ->
|
List0 ->
|
||||||
List = lists:sort(fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
List = lists:sort(
|
||||||
emqx_trace:format(List0)),
|
fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
||||||
|
emqx_trace:format(List0)
|
||||||
|
),
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
|
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
|
||||||
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
|
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
Traces =
|
Traces =
|
||||||
lists:map(fun(Trace = #{name := Name, start_at := Start,
|
lists:map(
|
||||||
end_at := End, enable := Enable, type := Type, filter := Filter}) ->
|
fun(
|
||||||
FileName = emqx_trace:filename(Name, Start),
|
Trace = #{
|
||||||
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
name := Name,
|
||||||
Trace0 = maps:without([enable, filter], Trace),
|
start_at := Start,
|
||||||
Trace0#{log_size => LogSize
|
end_at := End,
|
||||||
, Type => iolist_to_binary(Filter)
|
enable := Enable,
|
||||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
type := Type,
|
||||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
filter := Filter
|
||||||
, status => status(Enable, Start, End, Now)
|
}
|
||||||
}
|
) ->
|
||||||
end, List),
|
FileName = emqx_trace:filename(Name, Start),
|
||||||
|
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
||||||
|
Trace0 = maps:without([enable, filter], Trace),
|
||||||
|
Trace0#{
|
||||||
|
log_size => LogSize,
|
||||||
|
Type => iolist_to_binary(Filter),
|
||||||
|
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||||
|
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||||
|
status => status(Enable, Start, End, Now)
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
List
|
||||||
|
),
|
||||||
{200, Traces}
|
{200, Traces}
|
||||||
end;
|
end;
|
||||||
trace(post, #{body := Param}) ->
|
trace(post, #{body := Param}) ->
|
||||||
case emqx_trace:create(Param) of
|
case emqx_trace:create(Param) of
|
||||||
{ok, Trace0} -> {200, format_trace(Trace0)};
|
{ok, Trace0} ->
|
||||||
|
{200, format_trace(Trace0)};
|
||||||
{error, {already_existed, Name}} ->
|
{error, {already_existed, Name}} ->
|
||||||
{400, #{
|
{400, #{
|
||||||
code => 'ALREADY_EXISTS',
|
code => 'ALREADY_EXISTS',
|
||||||
|
@ -287,18 +371,27 @@ trace(delete, _Param) ->
|
||||||
|
|
||||||
format_trace(Trace0) ->
|
format_trace(Trace0) ->
|
||||||
[
|
[
|
||||||
#{start_at := Start, end_at := End,
|
#{
|
||||||
enable := Enable, type := Type, filter := Filter} = Trace1
|
start_at := Start,
|
||||||
|
end_at := End,
|
||||||
|
enable := Enable,
|
||||||
|
type := Type,
|
||||||
|
filter := Filter
|
||||||
|
} = Trace1
|
||||||
] = emqx_trace:format([Trace0]),
|
] = emqx_trace:format([Trace0]),
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
LogSize = lists:foldl(fun(Node, Acc) -> Acc#{Node => 0} end, #{},
|
LogSize = lists:foldl(
|
||||||
mria_mnesia:running_nodes()),
|
fun(Node, Acc) -> Acc#{Node => 0} end,
|
||||||
|
#{},
|
||||||
|
mria_mnesia:running_nodes()
|
||||||
|
),
|
||||||
Trace2 = maps:without([enable, filter], Trace1),
|
Trace2 = maps:without([enable, filter], Trace1),
|
||||||
Trace2#{log_size => LogSize
|
Trace2#{
|
||||||
, Type => iolist_to_binary(Filter)
|
log_size => LogSize,
|
||||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
Type => iolist_to_binary(Filter),
|
||||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||||
, status => status(Enable, Start, End, Now)
|
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||||
|
status => status(Enable, Start, End, Now)
|
||||||
}.
|
}.
|
||||||
|
|
||||||
delete_trace(delete, #{bindings := #{name := Name}}) ->
|
delete_trace(delete, #{bindings := #{name := Name}}) ->
|
||||||
|
@ -334,25 +427,34 @@ download_trace_log(get, #{bindings := #{name := Name}}) ->
|
||||||
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
|
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
|
||||||
},
|
},
|
||||||
{200, Headers, {file_binary, ZipName, Binary}};
|
{200, Headers, {file_binary, ZipName, Binary}};
|
||||||
{error, not_found} -> ?NOT_FOUND(Name)
|
{error, not_found} ->
|
||||||
|
?NOT_FOUND(Name)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
|
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
|
||||||
lists:foldl(fun(Res, Acc) ->
|
lists:foldl(
|
||||||
case Res of
|
fun(Res, Acc) ->
|
||||||
{ok, Node, Bin} ->
|
case Res of
|
||||||
FileName = Node ++ "-" ++ TraceLog,
|
{ok, Node, Bin} ->
|
||||||
ZipName = filename:join([ZipDir, FileName]),
|
FileName = Node ++ "-" ++ TraceLog,
|
||||||
case file:write_file(ZipName, Bin) of
|
ZipName = filename:join([ZipDir, FileName]),
|
||||||
ok -> [FileName | Acc];
|
case file:write_file(ZipName, Bin) of
|
||||||
_ -> Acc
|
ok -> [FileName | Acc];
|
||||||
end;
|
_ -> Acc
|
||||||
{error, Node, Reason} ->
|
end;
|
||||||
?SLOG(error, #{msg => "download_trace_log_error", node => Node,
|
{error, Node, Reason} ->
|
||||||
log => TraceLog, reason => Reason}),
|
?SLOG(error, #{
|
||||||
Acc
|
msg => "download_trace_log_error",
|
||||||
end
|
node => Node,
|
||||||
end, [], TraceFiles).
|
log => TraceLog,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
TraceFiles
|
||||||
|
).
|
||||||
|
|
||||||
collect_trace_file(TraceLog) ->
|
collect_trace_file(TraceLog) ->
|
||||||
Nodes = mria_mnesia:running_nodes(),
|
Nodes = mria_mnesia:running_nodes(),
|
||||||
|
@ -376,18 +478,25 @@ stream_log_file(get, #{bindings := #{name := Name}, query_string := Query}) ->
|
||||||
{eof, Size} ->
|
{eof, Size} ->
|
||||||
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
|
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
|
||||||
{200, #{meta => Meta, items => <<"">>}};
|
{200, #{meta => Meta, items => <<"">>}};
|
||||||
{error, enoent} -> %% the waiting trace should return "" not error.
|
%% the waiting trace should return "" not error.
|
||||||
|
{error, enoent} ->
|
||||||
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
|
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
|
||||||
{200, #{meta => Meta, items => <<"">>}};
|
{200, #{meta => Meta, items => <<"">>}};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, #{msg => "read_file_failed",
|
?SLOG(error, #{
|
||||||
node => Node, name => Name, reason => Reason,
|
msg => "read_file_failed",
|
||||||
position => Position, bytes => Bytes}),
|
node => Node,
|
||||||
|
name => Name,
|
||||||
|
reason => Reason,
|
||||||
|
position => Position,
|
||||||
|
bytes => Bytes
|
||||||
|
}),
|
||||||
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
|
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
|
||||||
{badrpc, nodedown} ->
|
{badrpc, nodedown} ->
|
||||||
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
|
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
|
||||||
end;
|
end;
|
||||||
{error, not_found} -> {400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
{error, not_found} ->
|
||||||
|
{400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
|
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
|
||||||
|
@ -396,23 +505,31 @@ get_trace_size() ->
|
||||||
Node = node(),
|
Node = node(),
|
||||||
case file:list_dir(TraceDir) of
|
case file:list_dir(TraceDir) of
|
||||||
{ok, AllFiles} ->
|
{ok, AllFiles} ->
|
||||||
lists:foldl(fun(File, Acc) ->
|
lists:foldl(
|
||||||
FullFileName = filename:join(TraceDir, File),
|
fun(File, Acc) ->
|
||||||
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
FullFileName = filename:join(TraceDir, File),
|
||||||
end, #{}, lists:delete("zip", AllFiles));
|
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
||||||
_ -> #{}
|
end,
|
||||||
|
#{},
|
||||||
|
lists:delete("zip", AllFiles)
|
||||||
|
);
|
||||||
|
_ ->
|
||||||
|
#{}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% this is an rpc call for stream_log_file/2
|
%% this is an rpc call for stream_log_file/2
|
||||||
-spec read_trace_file( binary()
|
-spec read_trace_file(
|
||||||
, non_neg_integer()
|
binary(),
|
||||||
, non_neg_integer()
|
non_neg_integer(),
|
||||||
) -> {ok, binary()}
|
non_neg_integer()
|
||||||
| {error, _}
|
) ->
|
||||||
| {eof, non_neg_integer()}.
|
{ok, binary()}
|
||||||
|
| {error, _}
|
||||||
|
| {eof, non_neg_integer()}.
|
||||||
read_trace_file(Name, Position, Limit) ->
|
read_trace_file(Name, Position, Limit) ->
|
||||||
case emqx_trace:get_trace_filename(Name) of
|
case emqx_trace:get_trace_filename(Name) of
|
||||||
{error, _} = Error -> Error;
|
{error, _} = Error ->
|
||||||
|
Error;
|
||||||
{ok, TraceFile} ->
|
{ok, TraceFile} ->
|
||||||
TraceDir = emqx_trace:trace_dir(),
|
TraceDir = emqx_trace:trace_dir(),
|
||||||
TracePath = filename:join([TraceDir, TraceFile]),
|
TracePath = filename:join([TraceDir, TraceFile]),
|
||||||
|
@ -423,13 +540,16 @@ read_file(Path, Offset, Bytes) ->
|
||||||
case file:open(Path, [read, raw, binary]) of
|
case file:open(Path, [read, raw, binary]) of
|
||||||
{ok, IoDevice} ->
|
{ok, IoDevice} ->
|
||||||
try
|
try
|
||||||
_ = case Offset of
|
_ =
|
||||||
|
case Offset of
|
||||||
0 -> ok;
|
0 -> ok;
|
||||||
_ -> file:position(IoDevice, {bof, Offset})
|
_ -> file:position(IoDevice, {bof, Offset})
|
||||||
end,
|
end,
|
||||||
case file:read(IoDevice, Bytes) of
|
case file:read(IoDevice, Bytes) of
|
||||||
{ok, Bin} -> {ok, Bin};
|
{ok, Bin} ->
|
||||||
{error, Reason} -> {error, Reason};
|
{ok, Bin};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason};
|
||||||
eof ->
|
eof ->
|
||||||
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
|
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
|
||||||
{eof, Size}
|
{eof, Size}
|
||||||
|
@ -437,20 +557,27 @@ read_file(Path, Offset, Bytes) ->
|
||||||
after
|
after
|
||||||
file:close(IoDevice)
|
file:close(IoDevice)
|
||||||
end;
|
end;
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
to_node(Node) ->
|
to_node(Node) ->
|
||||||
try {ok, binary_to_existing_atom(Node)}
|
try
|
||||||
catch _:_ ->
|
{ok, binary_to_existing_atom(Node)}
|
||||||
{error, not_found}
|
catch
|
||||||
|
_:_ ->
|
||||||
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
collect_file_size(Nodes, FileName, AllFiles) ->
|
collect_file_size(Nodes, FileName, AllFiles) ->
|
||||||
lists:foldl(fun(Node, Acc) ->
|
lists:foldl(
|
||||||
Size = maps:get({Node, FileName}, AllFiles, 0),
|
fun(Node, Acc) ->
|
||||||
Acc#{Node => Size}
|
Size = maps:get({Node, FileName}, AllFiles, 0),
|
||||||
end, #{}, Nodes).
|
Acc#{Node => Size}
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Nodes
|
||||||
|
).
|
||||||
|
|
||||||
status(false, _Start, _End, _Now) -> <<"stopped">>;
|
status(false, _Start, _End, _Now) -> <<"stopped">>;
|
||||||
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
|
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
|
||||||
|
|
|
@ -20,9 +20,10 @@
|
||||||
|
|
||||||
-define(APP, emqx_management).
|
-define(APP, emqx_management).
|
||||||
|
|
||||||
-export([ start/2
|
-export([
|
||||||
, stop/1
|
start/2,
|
||||||
]).
|
stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
-include("emqx_mgmt.hrl").
|
-include("emqx_mgmt.hrl").
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,15 @@
|
||||||
-export([mnesia/1]).
|
-export([mnesia/1]).
|
||||||
-boot_mnesia({mnesia, [boot]}).
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
|
||||||
-export([ create/4
|
-export([
|
||||||
, read/1
|
create/4,
|
||||||
, update/4
|
read/1,
|
||||||
, delete/1
|
update/4,
|
||||||
, list/0
|
delete/1,
|
||||||
]).
|
list/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ authorize/3 ]).
|
-export([authorize/3]).
|
||||||
|
|
||||||
-define(APP, emqx_app).
|
-define(APP, emqx_app).
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
desc = <<>> :: binary() | '_',
|
desc = <<>> :: binary() | '_',
|
||||||
expired_at = 0 :: integer() | undefined | '_',
|
expired_at = 0 :: integer() | undefined | '_',
|
||||||
created_at = 0 :: integer() | '_'
|
created_at = 0 :: integer() | '_'
|
||||||
}).
|
}).
|
||||||
|
|
||||||
mnesia(boot) ->
|
mnesia(boot) ->
|
||||||
ok = mria:create_table(?APP, [
|
ok = mria:create_table(?APP, [
|
||||||
|
@ -47,7 +48,8 @@ mnesia(boot) ->
|
||||||
{rlog_shard, ?COMMON_SHARD},
|
{rlog_shard, ?COMMON_SHARD},
|
||||||
{storage, disc_copies},
|
{storage, disc_copies},
|
||||||
{record_name, ?APP},
|
{record_name, ?APP},
|
||||||
{attributes, record_info(fields, ?APP)}]).
|
{attributes, record_info(fields, ?APP)}
|
||||||
|
]).
|
||||||
|
|
||||||
create(Name, Enable, ExpiredAt, Desc) ->
|
create(Name, Enable, ExpiredAt, Desc) ->
|
||||||
case mnesia:table_info(?APP, size) < 30 of
|
case mnesia:table_info(?APP, size) < 30 of
|
||||||
|
@ -61,13 +63,14 @@ read(Name) ->
|
||||||
[] -> mnesia:abort(not_found);
|
[] -> mnesia:abort(not_found);
|
||||||
[App] -> to_map(App)
|
[App] -> to_map(App)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
update(Name, Enable, ExpiredAt, Desc) ->
|
update(Name, Enable, ExpiredAt, Desc) ->
|
||||||
Fun = fun() ->
|
Fun = fun() ->
|
||||||
case mnesia:read(?APP, Name, write) of
|
case mnesia:read(?APP, Name, write) of
|
||||||
[] -> mnesia:abort(not_found);
|
[] ->
|
||||||
|
mnesia:abort(not_found);
|
||||||
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
|
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
|
||||||
App =
|
App =
|
||||||
App0#?APP{
|
App0#?APP{
|
||||||
|
@ -78,22 +81,25 @@ update(Name, Enable, ExpiredAt, Desc) ->
|
||||||
ok = mnesia:write(App),
|
ok = mnesia:write(App),
|
||||||
to_map(App)
|
to_map(App)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
delete(Name) ->
|
delete(Name) ->
|
||||||
Fun = fun() ->
|
Fun = fun() ->
|
||||||
case mnesia:read(?APP, Name) of
|
case mnesia:read(?APP, Name) of
|
||||||
[] -> mnesia:abort(not_found);
|
[] -> mnesia:abort(not_found);
|
||||||
[_App] -> mnesia:delete({?APP, Name}) end
|
[_App] -> mnesia:delete({?APP, Name})
|
||||||
end,
|
end
|
||||||
|
end,
|
||||||
trans(Fun).
|
trans(Fun).
|
||||||
|
|
||||||
list() ->
|
list() ->
|
||||||
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
||||||
|
|
||||||
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
{error, <<"not_allowed">>};
|
||||||
|
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||||
|
{error, <<"not_allowed">>};
|
||||||
authorize(_Path, ApiKey, ApiSecret) ->
|
authorize(_Path, ApiKey, ApiSecret) ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
case find_by_api_key(ApiKey) of
|
case find_by_api_key(ApiKey) of
|
||||||
|
@ -102,28 +108,35 @@ authorize(_Path, ApiKey, ApiSecret) ->
|
||||||
ok -> ok;
|
ok -> ok;
|
||||||
error -> {error, "secret_error"}
|
error -> {error, "secret_error"}
|
||||||
end;
|
end;
|
||||||
{ok, true, _ExpiredAt, _SecretHash} -> {error, "secret_expired"};
|
{ok, true, _ExpiredAt, _SecretHash} ->
|
||||||
{ok, false, _ExpiredAt, _SecretHash} -> {error, "secret_disable"};
|
{error, "secret_expired"};
|
||||||
{error, Reason} -> {error, Reason}
|
{ok, false, _ExpiredAt, _SecretHash} ->
|
||||||
|
{error, "secret_disable"};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
find_by_api_key(ApiKey) ->
|
find_by_api_key(ApiKey) ->
|
||||||
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
||||||
case trans(Fun) of
|
case trans(Fun) of
|
||||||
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
||||||
{ok, Enable, ExpiredAt, SecretHash};
|
{ok, Enable, ExpiredAt, SecretHash};
|
||||||
_ -> {error, "not_found"}
|
_ ->
|
||||||
|
{error, "not_found"}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
ensure_not_undefined(undefined, Old) -> Old;
|
ensure_not_undefined(undefined, Old) -> Old;
|
||||||
ensure_not_undefined(New, _Old) -> New.
|
ensure_not_undefined(New, _Old) -> New.
|
||||||
|
|
||||||
to_map(Apps)when is_list(Apps) ->
|
to_map(Apps) when is_list(Apps) ->
|
||||||
Fields = record_info(fields, ?APP),
|
Fields = record_info(fields, ?APP),
|
||||||
lists:map(fun(Trace0 = #?APP{}) ->
|
lists:map(
|
||||||
[_ | Values] = tuple_to_list(Trace0),
|
fun(Trace0 = #?APP{}) ->
|
||||||
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
[_ | Values] = tuple_to_list(Trace0),
|
||||||
end, Apps);
|
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
||||||
|
end,
|
||||||
|
Apps
|
||||||
|
);
|
||||||
to_map(App0) ->
|
to_map(App0) ->
|
||||||
[App] = to_map([App0]),
|
[App] = to_map([App0]),
|
||||||
App.
|
App.
|
||||||
|
@ -149,16 +162,18 @@ create_app(Name, Enable, ExpiredAt, Desc) ->
|
||||||
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
||||||
trans(fun() ->
|
trans(fun() ->
|
||||||
case mnesia:read(?APP, Name) of
|
case mnesia:read(?APP, Name) of
|
||||||
[_] -> mnesia:abort(name_already_existed);
|
[_] ->
|
||||||
|
mnesia:abort(name_already_existed);
|
||||||
[] ->
|
[] ->
|
||||||
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
|
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
|
||||||
[] ->
|
[] ->
|
||||||
ok = mnesia:write(App),
|
ok = mnesia:write(App),
|
||||||
to_map(App);
|
to_map(App);
|
||||||
_ -> mnesia:abort(api_key_already_existed)
|
_ ->
|
||||||
|
mnesia:abort(api_key_already_existed)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end).
|
end).
|
||||||
|
|
||||||
trans(Fun) ->
|
trans(Fun) ->
|
||||||
case mria:transaction(?COMMON_SHARD, Fun) of
|
case mria:transaction(?COMMON_SHARD, Fun) of
|
||||||
|
|
|
@ -23,8 +23,7 @@
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok, {{one_for_one, 1, 5}, []}}.
|
{ok, {{one_for_one, 1, 5}, []}}.
|
||||||
|
|
||||||
|
|
|
@ -16,38 +16,40 @@
|
||||||
|
|
||||||
-module(emqx_mgmt_util).
|
-module(emqx_mgmt_util).
|
||||||
|
|
||||||
-export([ strftime/1
|
-export([
|
||||||
, datetime/1
|
strftime/1,
|
||||||
, kmg/1
|
datetime/1,
|
||||||
, ntoa/1
|
kmg/1,
|
||||||
, merge_maps/2
|
ntoa/1,
|
||||||
, batch_operation/3
|
merge_maps/2,
|
||||||
]).
|
batch_operation/3
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ bad_request/0
|
-export([
|
||||||
, bad_request/1
|
bad_request/0,
|
||||||
, properties/1
|
bad_request/1,
|
||||||
, page_params/0
|
properties/1,
|
||||||
, schema/1
|
page_params/0,
|
||||||
, schema/2
|
schema/1,
|
||||||
, object_schema/1
|
schema/2,
|
||||||
, object_schema/2
|
object_schema/1,
|
||||||
, array_schema/1
|
object_schema/2,
|
||||||
, array_schema/2
|
array_schema/1,
|
||||||
, object_array_schema/1
|
array_schema/2,
|
||||||
, object_array_schema/2
|
object_array_schema/1,
|
||||||
, page_schema/1
|
object_array_schema/2,
|
||||||
, page_object_schema/1
|
page_schema/1,
|
||||||
, error_schema/1
|
page_object_schema/1,
|
||||||
, error_schema/2
|
error_schema/1,
|
||||||
, batch_schema/1
|
error_schema/2,
|
||||||
]).
|
batch_schema/1
|
||||||
|
]).
|
||||||
|
|
||||||
-export([urldecode/1]).
|
-export([urldecode/1]).
|
||||||
|
|
||||||
-define(KB, 1024).
|
-define(KB, 1024).
|
||||||
-define(MB, (1024*1024)).
|
-define(MB, (1024 * 1024)).
|
||||||
-define(GB, (1024*1024*1024)).
|
-define(GB, (1024 * 1024 * 1024)).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Strftime
|
%% Strftime
|
||||||
|
@ -55,17 +57,17 @@
|
||||||
|
|
||||||
strftime({MegaSecs, Secs, _MicroSecs}) ->
|
strftime({MegaSecs, Secs, _MicroSecs}) ->
|
||||||
strftime(datetime(MegaSecs * 1000000 + Secs));
|
strftime(datetime(MegaSecs * 1000000 + Secs));
|
||||||
|
|
||||||
strftime(Secs) when is_integer(Secs) ->
|
strftime(Secs) when is_integer(Secs) ->
|
||||||
strftime(datetime(Secs));
|
strftime(datetime(Secs));
|
||||||
|
strftime({{Y, M, D}, {H, MM, S}}) ->
|
||||||
strftime({{Y,M,D}, {H,MM,S}}) ->
|
|
||||||
lists:flatten(
|
lists:flatten(
|
||||||
io_lib:format(
|
io_lib:format(
|
||||||
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
|
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S]
|
||||||
|
)
|
||||||
|
).
|
||||||
|
|
||||||
datetime(Timestamp) when is_integer(Timestamp) ->
|
datetime(Timestamp) when is_integer(Timestamp) ->
|
||||||
Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}),
|
Epoch = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
||||||
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
|
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
|
||||||
calendar:universal_time_to_local_time(Universal).
|
calendar:universal_time_to_local_time(Universal).
|
||||||
|
|
||||||
|
@ -80,19 +82,27 @@ kmg(Byte) ->
|
||||||
kmg(F, S) ->
|
kmg(F, S) ->
|
||||||
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
|
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
|
||||||
|
|
||||||
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
|
ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
|
||||||
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
||||||
ntoa(IP) ->
|
ntoa(IP) ->
|
||||||
inet_parse:ntoa(IP).
|
inet_parse:ntoa(IP).
|
||||||
|
|
||||||
merge_maps(Default, New) ->
|
merge_maps(Default, New) ->
|
||||||
maps:fold(fun(K, V, Acc) ->
|
maps:fold(
|
||||||
case maps:get(K, Acc, undefined) of
|
fun(K, V, Acc) ->
|
||||||
OldV when is_map(OldV),
|
case maps:get(K, Acc, undefined) of
|
||||||
is_map(V) -> Acc#{K => merge_maps(OldV, V)};
|
OldV when
|
||||||
_ -> Acc#{K => V}
|
is_map(OldV),
|
||||||
end
|
is_map(V)
|
||||||
end, Default, New).
|
->
|
||||||
|
Acc#{K => merge_maps(OldV, V)};
|
||||||
|
_ ->
|
||||||
|
Acc#{K => V}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Default,
|
||||||
|
New
|
||||||
|
).
|
||||||
|
|
||||||
urldecode(S) ->
|
urldecode(S) ->
|
||||||
emqx_http_lib:uri_decode(S).
|
emqx_http_lib:uri_decode(S).
|
||||||
|
@ -123,8 +133,13 @@ array_schema(Schema, Desc) ->
|
||||||
object_array_schema(Properties) when is_map(Properties) ->
|
object_array_schema(Properties) when is_map(Properties) ->
|
||||||
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
|
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
|
||||||
object_array_schema(Properties, Desc) ->
|
object_array_schema(Properties, Desc) ->
|
||||||
json_content_schema(#{type => array,
|
json_content_schema(
|
||||||
items => #{type => object, properties => Properties}}, Desc).
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object, properties => Properties}
|
||||||
|
},
|
||||||
|
Desc
|
||||||
|
).
|
||||||
|
|
||||||
page_schema(Ref) when is_atom(Ref) ->
|
page_schema(Ref) when is_atom(Ref) ->
|
||||||
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
|
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
|
||||||
|
@ -134,9 +149,11 @@ page_schema(Schema) ->
|
||||||
properties => #{
|
properties => #{
|
||||||
meta => #{
|
meta => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => properties([{page, integer},
|
properties => properties([
|
||||||
{limit, integer},
|
{page, integer},
|
||||||
{count, integer}])
|
{limit, integer},
|
||||||
|
{count, integer}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
data => #{
|
data => #{
|
||||||
type => array,
|
type => array,
|
||||||
|
@ -155,8 +172,10 @@ error_schema(Description) ->
|
||||||
error_schema(Description, Enum) ->
|
error_schema(Description, Enum) ->
|
||||||
Schema = #{
|
Schema = #{
|
||||||
type => object,
|
type => object,
|
||||||
properties => properties([{code, string, <<>>, Enum},
|
properties => properties([
|
||||||
{message, string}])
|
{code, string, <<>>, Enum},
|
||||||
|
{message, string}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
json_content_schema(Schema, Description).
|
json_content_schema(Schema, Description).
|
||||||
|
|
||||||
|
@ -168,20 +187,28 @@ batch_schema(DefName) when is_binary(DefName) ->
|
||||||
properties => #{
|
properties => #{
|
||||||
success => #{
|
success => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
description => <<"Success count">>},
|
description => <<"Success count">>
|
||||||
|
},
|
||||||
failed => #{
|
failed => #{
|
||||||
type => integer,
|
type => integer,
|
||||||
description => <<"Failed count">>},
|
description => <<"Failed count">>
|
||||||
|
},
|
||||||
detail => #{
|
detail => #{
|
||||||
type => array,
|
type => array,
|
||||||
description => <<"Failed object & reason">>,
|
description => <<"Failed object & reason">>,
|
||||||
items => #{
|
items => #{
|
||||||
type => object,
|
type => object,
|
||||||
properties =>
|
properties =>
|
||||||
#{
|
#{
|
||||||
data => minirest:ref(DefName),
|
data => minirest:ref(DefName),
|
||||||
reason => #{
|
reason => #{
|
||||||
type => <<"string">>}}}}}},
|
type => <<"string">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
json_content_schema(Schema).
|
json_content_schema(Schema).
|
||||||
|
|
||||||
json_content_schema(Schema) when is_map(Schema) ->
|
json_content_schema(Schema) when is_map(Schema) ->
|
||||||
|
@ -211,7 +238,7 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) ->
|
||||||
case erlang:apply(Module, Function, Args) of
|
case erlang:apply(Module, Function, Args) of
|
||||||
ok ->
|
ok ->
|
||||||
batch_operation(Module, Function, ArgsList, Failed);
|
batch_operation(Module, Function, ArgsList, Failed);
|
||||||
{error ,Reason} ->
|
{error, Reason} ->
|
||||||
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
|
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -224,36 +251,75 @@ properties([Key | Props], Acc) when is_atom(Key) ->
|
||||||
properties([{Key, Type} | Props], Acc) ->
|
properties([{Key, Type} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type}, Acc));
|
properties(Props, maps:put(Key, #{type => Type}, Acc));
|
||||||
properties([{Key, object, Props1} | Props], Acc) ->
|
properties([{Key, object, Props1} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => object,
|
properties(
|
||||||
properties => properties(Props1)}, Acc));
|
Props,
|
||||||
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => properties(Props1)
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, {array, object}, Props1} | Props], Acc) ->
|
properties([{Key, {array, object}, Props1} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => array,
|
properties(
|
||||||
items => #{type => object,
|
Props,
|
||||||
properties => properties(Props1)
|
maps:put(
|
||||||
}}, Acc));
|
Key,
|
||||||
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{
|
||||||
|
type => object,
|
||||||
|
properties => properties(Props1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
|
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => array,
|
properties(
|
||||||
items => #{type => Type},
|
Props,
|
||||||
description => Desc}, Acc));
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => array,
|
||||||
|
items => #{type => Type},
|
||||||
|
description => Desc
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
);
|
||||||
properties([{Key, Type, Desc} | Props], Acc) ->
|
properties([{Key, Type, Desc} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
|
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
|
||||||
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
|
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
|
||||||
properties(Props, maps:put(Key, #{type => Type,
|
properties(
|
||||||
description => Desc,
|
Props,
|
||||||
enum => Enum}, Acc)).
|
maps:put(
|
||||||
|
Key,
|
||||||
|
#{
|
||||||
|
type => Type,
|
||||||
|
description => Desc,
|
||||||
|
enum => Enum
|
||||||
|
},
|
||||||
|
Acc
|
||||||
|
)
|
||||||
|
).
|
||||||
page_params() ->
|
page_params() ->
|
||||||
[#{
|
[
|
||||||
name => page,
|
#{
|
||||||
in => query,
|
name => page,
|
||||||
description => <<"Page">>,
|
in => query,
|
||||||
schema => #{type => integer, default => 1}
|
description => <<"Page">>,
|
||||||
},
|
schema => #{type => integer, default => 1}
|
||||||
#{
|
},
|
||||||
name => limit,
|
#{
|
||||||
in => query,
|
name => limit,
|
||||||
description => <<"Page size">>,
|
in => query,
|
||||||
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
description => <<"Page size">>,
|
||||||
}].
|
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
bad_request() ->
|
bad_request() ->
|
||||||
bad_request(<<"Bad Request">>).
|
bad_request(<<"Bad Request">>).
|
||||||
|
|
|
@ -17,12 +17,13 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
, get_plugins/0
|
introduced_in/0,
|
||||||
, install_package/2
|
get_plugins/0,
|
||||||
, describe_package/1
|
install_package/2,
|
||||||
, delete_package/1
|
describe_package/1,
|
||||||
, ensure_action/2
|
delete_package/1,
|
||||||
|
ensure_action/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
|
@ -18,12 +18,13 @@
|
||||||
|
|
||||||
-behaviour(emqx_bpapi).
|
-behaviour(emqx_bpapi).
|
||||||
|
|
||||||
-export([ introduced_in/0
|
-export([
|
||||||
|
introduced_in/0,
|
||||||
|
|
||||||
, trace_file/2
|
trace_file/2,
|
||||||
, get_trace_size/1
|
get_trace_size/1,
|
||||||
, read_trace_file/4
|
read_trace_file/4
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("emqx/include/bpapi.hrl").
|
-include_lib("emqx/include/bpapi.hrl").
|
||||||
|
|
||||||
|
@ -31,21 +32,22 @@ introduced_in() ->
|
||||||
"5.0.0".
|
"5.0.0".
|
||||||
|
|
||||||
-spec get_trace_size([node()]) ->
|
-spec get_trace_size([node()]) ->
|
||||||
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
||||||
get_trace_size(Nodes) ->
|
get_trace_size(Nodes) ->
|
||||||
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
|
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
|
||||||
|
|
||||||
-spec trace_file([node()], file:name_all()) ->
|
-spec trace_file([node()], file:name_all()) ->
|
||||||
emqx_rpc:multicall_result(
|
emqx_rpc:multicall_result(
|
||||||
{ok, Node :: list(), Binary :: binary()} |
|
{ok, Node :: list(), Binary :: binary()}
|
||||||
{error, Node :: list(), Reason :: term()}).
|
| {error, Node :: list(), Reason :: term()}
|
||||||
|
).
|
||||||
trace_file(Nodes, File) ->
|
trace_file(Nodes, File) ->
|
||||||
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
|
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
|
||||||
|
|
||||||
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
|
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
|
||||||
{ok, binary()}
|
{ok, binary()}
|
||||||
| {error, _}
|
| {error, _}
|
||||||
| {eof, non_neg_integer()}
|
| {eof, non_neg_integer()}
|
||||||
| {badrpc, _}.
|
| {badrpc, _}.
|
||||||
read_trace_file(Node, Name, Position, Limit) ->
|
read_trace_file(Node, Name, Position, Limit) ->
|
||||||
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
|
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_mgmt_api_alarms_SUITE).
|
-module(emqx_mgmt_api_alarms_SUITE).
|
||||||
|
|
||||||
|
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
@ -55,8 +54,8 @@ get_alarms(AssertCount, Activated) ->
|
||||||
Headers = emqx_mgmt_api_test_util:auth_header_(),
|
Headers = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
|
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
|
||||||
Data = emqx_json:decode(Response, [return_maps]),
|
Data = emqx_json:decode(Response, [return_maps]),
|
||||||
Meta = maps:get(<<"meta">>, Data),
|
Meta = maps:get(<<"meta">>, Data),
|
||||||
Page = maps:get(<<"page">>, Meta),
|
Page = maps:get(<<"page">>, Meta),
|
||||||
Limit = maps:get(<<"limit">>, Meta),
|
Limit = maps:get(<<"limit">>, Meta),
|
||||||
Count = maps:get(<<"count">>, Meta),
|
Count = maps:get(<<"count">>, Meta),
|
||||||
?assertEqual(Page, 1),
|
?assertEqual(Page, 1),
|
||||||
|
|
|
@ -22,10 +22,11 @@
|
||||||
|
|
||||||
all() -> [{group, parallel}, {group, sequence}].
|
all() -> [{group, parallel}, {group, sequence}].
|
||||||
suite() -> [{timetrap, {minutes, 1}}].
|
suite() -> [{timetrap, {minutes, 1}}].
|
||||||
groups() -> [
|
groups() ->
|
||||||
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
[
|
||||||
{sequence, [], [t_create_failed]}
|
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
||||||
].
|
{sequence, [], [t_create_failed]}
|
||||||
|
].
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
emqx_mgmt_api_test_util:init_suite(),
|
emqx_mgmt_api_test_util:init_suite(),
|
||||||
|
@ -37,15 +38,20 @@ end_per_suite(_) ->
|
||||||
t_create(_Config) ->
|
t_create(_Config) ->
|
||||||
Name = <<"EMQX-API-KEY-1">>,
|
Name = <<"EMQX-API-KEY-1">>,
|
||||||
{ok, Create} = create_app(Name),
|
{ok, Create} = create_app(Name),
|
||||||
?assertMatch(#{<<"api_key">> := _,
|
?assertMatch(
|
||||||
<<"api_secret">> := _,
|
#{
|
||||||
<<"created_at">> := _,
|
<<"api_key">> := _,
|
||||||
<<"desc">> := _,
|
<<"api_secret">> := _,
|
||||||
<<"enable">> := true,
|
<<"created_at">> := _,
|
||||||
<<"expired_at">> := _,
|
<<"desc">> := _,
|
||||||
<<"name">> := Name}, Create),
|
<<"enable">> := true,
|
||||||
|
<<"expired_at">> := _,
|
||||||
|
<<"name">> := Name
|
||||||
|
},
|
||||||
|
Create
|
||||||
|
),
|
||||||
{ok, List} = list_app(),
|
{ok, List} = list_app(),
|
||||||
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
||||||
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
|
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
|
||||||
{ok, App1} = read_app(Name),
|
{ok, App1} = read_app(Name),
|
||||||
?assertEqual(Name, maps:get(<<"name">>, App1)),
|
?assertEqual(Name, maps:get(<<"name">>, App1)),
|
||||||
|
@ -64,9 +70,12 @@ t_create_failed(_Config) ->
|
||||||
|
|
||||||
{ok, List} = list_app(),
|
{ok, List} = list_app(),
|
||||||
CreateNum = 30 - erlang:length(List),
|
CreateNum = 30 - erlang:length(List),
|
||||||
Names = lists:map(fun(Seq) ->
|
Names = lists:map(
|
||||||
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
fun(Seq) ->
|
||||||
end, lists:seq(1, CreateNum)),
|
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
||||||
|
end,
|
||||||
|
lists:seq(1, CreateNum)
|
||||||
|
),
|
||||||
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
|
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
|
||||||
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
|
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
|
||||||
|
|
||||||
|
@ -93,7 +102,8 @@ t_update(_Config) ->
|
||||||
?assertEqual(Name, maps:get(<<"name">>, Update1)),
|
?assertEqual(Name, maps:get(<<"name">>, Update1)),
|
||||||
?assertEqual(false, maps:get(<<"enable">>, Update1)),
|
?assertEqual(false, maps:get(<<"enable">>, Update1)),
|
||||||
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
|
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
|
||||||
?assertEqual(calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
?assertEqual(
|
||||||
|
calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
||||||
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
|
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
|
||||||
),
|
),
|
||||||
Unexpired1 = maps:without([expired_at], Change),
|
Unexpired1 = maps:without([expired_at], Change),
|
||||||
|
@ -117,10 +127,14 @@ t_delete(_Config) ->
|
||||||
t_authorize(_Config) ->
|
t_authorize(_Config) ->
|
||||||
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
|
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
|
||||||
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
|
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
|
||||||
BasicHeader = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
BasicHeader = emqx_common_test_http:auth_header(
|
||||||
binary_to_list(ApiSecret)),
|
binary_to_list(ApiKey),
|
||||||
SecretError = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
binary_to_list(ApiSecret)
|
||||||
binary_to_list(ApiKey)),
|
),
|
||||||
|
SecretError = emqx_common_test_http:auth_header(
|
||||||
|
binary_to_list(ApiKey),
|
||||||
|
binary_to_list(ApiKey)
|
||||||
|
),
|
||||||
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
|
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
|
||||||
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
|
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
|
||||||
|
|
||||||
|
@ -134,8 +148,10 @@ t_authorize(_Config) ->
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
|
||||||
|
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
?assertMatch(
|
||||||
update_app(Name, #{enable => false})),
|
{ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
||||||
|
update_app(Name, #{enable => false})
|
||||||
|
),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||||
|
|
||||||
Expired = #{
|
Expired = #{
|
||||||
|
@ -145,8 +161,10 @@ t_authorize(_Config) ->
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
|
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
|
||||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||||
UnExpired = #{expired_at => undefined},
|
UnExpired = #{expired_at => undefined},
|
||||||
?assertMatch({ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
?assertMatch(
|
||||||
update_app(Name, UnExpired)),
|
{ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
||||||
|
update_app(Name, UnExpired)
|
||||||
|
),
|
||||||
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
|
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -159,7 +177,6 @@ t_create_unexpired_app(_Config) ->
|
||||||
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
|
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
||||||
list_app() ->
|
list_app() ->
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
|
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
|
||||||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||||
|
|
|
@ -47,13 +47,17 @@ t_create(_Config) ->
|
||||||
until => Until
|
until => Until
|
||||||
},
|
},
|
||||||
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
|
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
|
||||||
?assertEqual(#{<<"as">> => As,
|
?assertEqual(
|
||||||
<<"at">> => At,
|
#{
|
||||||
<<"by">> => By,
|
<<"as">> => As,
|
||||||
<<"reason">> => Reason,
|
<<"at">> => At,
|
||||||
<<"until">> => Until,
|
<<"by">> => By,
|
||||||
<<"who">> => ClientId
|
<<"reason">> => Reason,
|
||||||
}, ClientIdBannedRes),
|
<<"until">> => Until,
|
||||||
|
<<"who">> => ClientId
|
||||||
|
},
|
||||||
|
ClientIdBannedRes
|
||||||
|
),
|
||||||
PeerHost = <<"192.168.2.13">>,
|
PeerHost = <<"192.168.2.13">>,
|
||||||
PeerHostBanned = #{
|
PeerHostBanned = #{
|
||||||
as => <<"peerhost">>,
|
as => <<"peerhost">>,
|
||||||
|
@ -64,15 +68,19 @@ t_create(_Config) ->
|
||||||
until => Until
|
until => Until
|
||||||
},
|
},
|
||||||
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
|
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
|
||||||
?assertEqual(#{<<"as">> => <<"peerhost">>,
|
?assertEqual(
|
||||||
<<"at">> => At,
|
#{
|
||||||
<<"by">> => By,
|
<<"as">> => <<"peerhost">>,
|
||||||
<<"reason">> => Reason,
|
<<"at">> => At,
|
||||||
<<"until">> => Until,
|
<<"by">> => By,
|
||||||
<<"who">> => PeerHost
|
<<"reason">> => Reason,
|
||||||
}, PeerHostBannedRes),
|
<<"until">> => Until,
|
||||||
|
<<"who">> => PeerHost
|
||||||
|
},
|
||||||
|
PeerHostBannedRes
|
||||||
|
),
|
||||||
{ok, #{<<"data">> := List}} = list_banned(),
|
{ok, #{<<"data">> := List}} = list_banned(),
|
||||||
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
||||||
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -94,8 +102,10 @@ t_create_failed(_Config) ->
|
||||||
},
|
},
|
||||||
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
|
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
|
||||||
?assertEqual(BadRequest, create_banned(BadPeerHost)),
|
?assertEqual(BadRequest, create_banned(BadPeerHost)),
|
||||||
Expired = BadPeerHost#{until => emqx_banned:to_rfc3339(Now - 1),
|
Expired = BadPeerHost#{
|
||||||
who => <<"127.0.0.1">>},
|
until => emqx_banned:to_rfc3339(Now - 1),
|
||||||
|
who => <<"127.0.0.1">>
|
||||||
|
},
|
||||||
?assertEqual(BadRequest, create_banned(Expired)),
|
?assertEqual(BadRequest, create_banned(Expired)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -117,8 +127,10 @@ t_delete(_Config) ->
|
||||||
},
|
},
|
||||||
{ok, _} = create_banned(Banned),
|
{ok, _} = create_banned(Banned),
|
||||||
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
|
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
|
||||||
?assertMatch({error,{"HTTP/1.1",404,"Not Found"}},
|
?assertMatch(
|
||||||
delete_banned(binary_to_list(As), binary_to_list(Who))),
|
{error, {"HTTP/1.1", 404, "Not Found"}},
|
||||||
|
delete_banned(binary_to_list(As), binary_to_list(Who))
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
list_banned() ->
|
list_banned() ->
|
||||||
|
|
|
@ -44,20 +44,20 @@ t_clients(_) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
|
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||||
{ok, _} = emqtt:connect(C2),
|
{ok, _} = emqtt:connect(C2),
|
||||||
|
|
||||||
timer:sleep(300),
|
timer:sleep(300),
|
||||||
|
|
||||||
%% get /clients
|
%% get /clients
|
||||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||||
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
||||||
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
|
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
|
||||||
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
||||||
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
||||||
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
||||||
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
||||||
?assertEqual(ClientsPage, 1),
|
?assertEqual(ClientsPage, 1),
|
||||||
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
|
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
|
||||||
?assertEqual(ClientsCount, 2),
|
?assertEqual(ClientsCount, 2),
|
||||||
|
@ -77,28 +77,48 @@ t_clients(_) ->
|
||||||
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
|
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
|
||||||
|
|
||||||
%% get /clients/:clientid/authorization/cache should has no authz cache
|
%% get /clients/:clientid/authorization/cache should has no authz cache
|
||||||
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path(["clients",
|
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "authorization", "cache"]),
|
"clients",
|
||||||
|
binary_to_list(ClientId1),
|
||||||
|
"authorization",
|
||||||
|
"cache"
|
||||||
|
]),
|
||||||
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
|
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
|
||||||
?assertEqual("[]", Client1AuthzCache),
|
?assertEqual("[]", Client1AuthzCache),
|
||||||
|
|
||||||
%% post /clients/:clientid/subscribe
|
%% post /clients/:clientid/subscribe
|
||||||
SubscribeBody = #{topic => Topic, qos => Qos},
|
SubscribeBody = #{topic => Topic, qos => Qos},
|
||||||
SubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
SubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "subscribe"]),
|
"clients",
|
||||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, SubscribePath,
|
binary_to_list(ClientId1),
|
||||||
"", AuthHeader, SubscribeBody),
|
"subscribe"
|
||||||
|
]),
|
||||||
|
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||||
|
post,
|
||||||
|
SubscribePath,
|
||||||
|
"",
|
||||||
|
AuthHeader,
|
||||||
|
SubscribeBody
|
||||||
|
),
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
|
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
|
||||||
?assertEqual(AfterSubTopic, Topic),
|
?assertEqual(AfterSubTopic, Topic),
|
||||||
?assertEqual(AfterSubQos, Qos),
|
?assertEqual(AfterSubQos, Qos),
|
||||||
|
|
||||||
%% post /clients/:clientid/unsubscribe
|
%% post /clients/:clientid/unsubscribe
|
||||||
UnSubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
UnSubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||||
binary_to_list(ClientId1), "unsubscribe"]),
|
"clients",
|
||||||
|
binary_to_list(ClientId1),
|
||||||
|
"unsubscribe"
|
||||||
|
]),
|
||||||
UnSubscribeBody = #{topic => Topic},
|
UnSubscribeBody = #{topic => Topic},
|
||||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, UnSubscribePath,
|
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||||
"", AuthHeader, UnSubscribeBody),
|
post,
|
||||||
|
UnSubscribePath,
|
||||||
|
"",
|
||||||
|
AuthHeader,
|
||||||
|
UnSubscribeBody
|
||||||
|
),
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
|
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
|
||||||
|
|
||||||
|
@ -118,44 +138,58 @@ t_query_clients_with_time(_) ->
|
||||||
ClientId2 = <<"client2">>,
|
ClientId2 = <<"client2">>,
|
||||||
|
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||||
{ok, _} = emqtt:connect(C2),
|
{ok, _} = emqtt:connect(C2),
|
||||||
|
|
||||||
timer:sleep(100),
|
timer:sleep(100),
|
||||||
|
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||||
%% get /clients with time(rfc3339)
|
%% get /clients with time(rfc3339)
|
||||||
NowTimeStampInt = erlang:system_time(millisecond),
|
NowTimeStampInt = erlang:system_time(millisecond),
|
||||||
%% Do not uri_encode `=` to `%3D`
|
%% Do not uri_encode `=` to `%3D`
|
||||||
Rfc3339String = emqx_http_lib:uri_encode(binary:bin_to_list(
|
Rfc3339String = emqx_http_lib:uri_encode(
|
||||||
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt))),
|
binary:bin_to_list(
|
||||||
|
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt)
|
||||||
|
)
|
||||||
|
),
|
||||||
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
|
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
|
||||||
|
|
||||||
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
||||||
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
||||||
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
||||||
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
||||||
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
||||||
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
||||||
|
|
||||||
RequestResults =
|
RequestResults =
|
||||||
[emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
[
|
||||||
|| Param <- LteParamRfc3339 ++ LteParamStamp
|
emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
||||||
++ GteParamRfc3339 ++ GteParamStamp],
|
|| Param <-
|
||||||
DecodedResults = [emqx_json:decode(Response, [return_maps])
|
LteParamRfc3339 ++ LteParamStamp ++
|
||||||
|| {ok, Response} <- RequestResults],
|
GteParamRfc3339 ++ GteParamStamp
|
||||||
|
],
|
||||||
|
DecodedResults = [
|
||||||
|
emqx_json:decode(Response, [return_maps])
|
||||||
|
|| {ok, Response} <- RequestResults
|
||||||
|
],
|
||||||
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
|
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
|
||||||
%% EachData :: list()
|
%% EachData :: list()
|
||||||
[?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
[
|
||||||
|
?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
||||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||||
#{<<"created_at">> := CreatedAt} <- EachData],
|
#{<<"created_at">> := CreatedAt} <- EachData
|
||||||
[?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
],
|
||||||
|
[
|
||||||
|
?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
||||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||||
#{<<"connected_at">> := ConnectedAt} <- EachData],
|
#{<<"connected_at">> := ConnectedAt} <- EachData
|
||||||
[?assertEqual(EachData, [])
|
],
|
||||||
|| #{<<"data">> := EachData} <- GteResponseDecodeds],
|
[
|
||||||
|
?assertEqual(EachData, [])
|
||||||
|
|| #{<<"data">> := EachData} <- GteResponseDecodeds
|
||||||
|
],
|
||||||
|
|
||||||
%% testcase cleanup, kickout client1 and client2
|
%% testcase cleanup, kickout client1 and client2
|
||||||
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
|
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
|
||||||
|
@ -169,7 +203,7 @@ t_keepalive(_Config) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
|
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
|
||||||
Body = #{interval => 11},
|
Body = #{interval => 11},
|
||||||
{error,{"HTTP/1.1",404,"Not Found"}} =
|
{error, {"HTTP/1.1", 404, "Not Found"}} =
|
||||||
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
|
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
|
||||||
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
|
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
|
@ -190,5 +224,6 @@ time_string_to_epoch(DateTime, Unit) when is_binary(DateTime) ->
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
calendar:rfc3339_to_system_time(
|
calendar:rfc3339_to_system_time(
|
||||||
binary_to_list(DateTime), [{unit, Unit}])
|
binary_to_list(DateTime), [{unit, Unit}]
|
||||||
|
)
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -32,10 +32,13 @@ end_per_suite(_) ->
|
||||||
|
|
||||||
t_get(_Config) ->
|
t_get(_Config) ->
|
||||||
{ok, Configs} = get_configs(),
|
{ok, Configs} = get_configs(),
|
||||||
maps:map(fun(Name, Value) ->
|
maps:map(
|
||||||
{ok, Config} = get_config(Name),
|
fun(Name, Value) ->
|
||||||
?assertEqual(Value, Config)
|
{ok, Config} = get_config(Name),
|
||||||
end, maps:remove(<<"license">>, Configs)),
|
?assertEqual(Value, Config)
|
||||||
|
end,
|
||||||
|
maps:remove(<<"license">>, Configs)
|
||||||
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_update(_Config) ->
|
t_update(_Config) ->
|
||||||
|
@ -50,8 +53,10 @@ t_update(_Config) ->
|
||||||
|
|
||||||
%% update failed
|
%% update failed
|
||||||
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
|
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}},
|
?assertMatch(
|
||||||
update_config(<<"sysmon">>, ErrorSysMon)),
|
{error, {"HTTP/1.1", 400, _}},
|
||||||
|
update_config(<<"sysmon">>, ErrorSysMon)
|
||||||
|
),
|
||||||
{ok, SysMon2} = get_config(<<"sysmon">>),
|
{ok, SysMon2} = get_config(<<"sysmon">>),
|
||||||
?assertEqual(SysMon1, SysMon2),
|
?assertEqual(SysMon1, SysMon2),
|
||||||
|
|
||||||
|
@ -101,8 +106,10 @@ t_global_zone(_Config) ->
|
||||||
{ok, Zones} = get_global_zone(),
|
{ok, Zones} = get_global_zone(),
|
||||||
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
|
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
|
||||||
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
|
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
|
||||||
?assertEqual(emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
?assertEqual(
|
||||||
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)),
|
emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
||||||
|
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)
|
||||||
|
),
|
||||||
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
|
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
|
||||||
{ok, #{}} = update_global_zone(NewZones),
|
{ok, #{}} = update_global_zone(NewZones),
|
||||||
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
|
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
|
||||||
|
@ -133,7 +140,8 @@ get_config(Name) ->
|
||||||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||||
{ok, Res} ->
|
{ok, Res} ->
|
||||||
{ok, emqx_json:decode(Res, [return_maps])};
|
{ok, emqx_json:decode(Res, [return_maps])};
|
||||||
Error -> Error
|
Error ->
|
||||||
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_configs() ->
|
get_configs() ->
|
||||||
|
@ -153,8 +161,11 @@ update_config(Name, Change) ->
|
||||||
|
|
||||||
reset_config(Name, Key) ->
|
reset_config(Name, Key) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Path = binary_to_list(iolist_to_binary(
|
Path = binary_to_list(
|
||||||
emqx_mgmt_api_test_util:api_path(["configs_reset", Name]))),
|
iolist_to_binary(
|
||||||
|
emqx_mgmt_api_test_util:api_path(["configs_reset", Name])
|
||||||
|
)
|
||||||
|
),
|
||||||
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
|
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
|
||||||
{ok, []} -> ok;
|
{ok, []} -> ok;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
|
|
|
@ -40,16 +40,17 @@ t_single_node_metrics_api(_) ->
|
||||||
{ok, MetricsResponse} = request_helper("metrics"),
|
{ok, MetricsResponse} = request_helper("metrics"),
|
||||||
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
|
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||||
LocalNodeMetrics = maps:from_list(
|
LocalNodeMetrics = maps:from_list(
|
||||||
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]),
|
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]
|
||||||
|
),
|
||||||
match_helper(LocalNodeMetrics, MetricsFromAPI).
|
match_helper(LocalNodeMetrics, MetricsFromAPI).
|
||||||
|
|
||||||
match_helper(SystemMetrics, MetricsFromAPI) ->
|
match_helper(SystemMetrics, MetricsFromAPI) ->
|
||||||
length_equal(SystemMetrics, MetricsFromAPI),
|
length_equal(SystemMetrics, MetricsFromAPI),
|
||||||
Fun =
|
Fun =
|
||||||
fun (Key, {SysMetrics, APIMetrics}) ->
|
fun(Key, {SysMetrics, APIMetrics}) ->
|
||||||
Value = maps:get(Key, SysMetrics),
|
Value = maps:get(Key, SysMetrics),
|
||||||
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
||||||
{Value, {SysMetrics, APIMetrics}}
|
{Value, {SysMetrics, APIMetrics}}
|
||||||
end,
|
end,
|
||||||
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
|
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
|
||||||
|
|
||||||
|
|
|
@ -67,19 +67,21 @@ t_nodes_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
||||||
t_log_path(_) ->
|
t_log_path(_) ->
|
||||||
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
|
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
|
||||||
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
|
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
|
||||||
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
|
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
|
||||||
?assertEqual(
|
?assertEqual(
|
||||||
<<"emqx-test.log">>,
|
<<"emqx-test.log">>,
|
||||||
filename:basename(Path)).
|
filename:basename(Path)
|
||||||
|
).
|
||||||
|
|
||||||
t_node_stats_api(_) ->
|
t_node_stats_api(_) ->
|
||||||
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
|
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
|
||||||
SystemStats= emqx_mgmt:get_stats(),
|
SystemStats = emqx_mgmt:get_stats(),
|
||||||
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
|
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
|
||||||
Stats = emqx_json:decode(StatsResponse, [return_maps]),
|
Stats = emqx_json:decode(StatsResponse, [return_maps]),
|
||||||
Fun =
|
Fun =
|
||||||
|
@ -91,12 +93,13 @@ t_node_stats_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
||||||
t_node_metrics_api(_) ->
|
t_node_metrics_api(_) ->
|
||||||
MetricsPath =
|
MetricsPath =
|
||||||
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
|
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
|
||||||
SystemMetrics= emqx_mgmt:get_metrics(),
|
SystemMetrics = emqx_mgmt:get_metrics(),
|
||||||
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
|
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
|
||||||
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
|
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||||
Fun =
|
Fun =
|
||||||
|
@ -108,4 +111,5 @@ t_node_metrics_api(_) ->
|
||||||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
|
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, {_, 400, _}},
|
{error, {_, 400, _}},
|
||||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||||
|
).
|
||||||
|
|
|
@ -56,17 +56,35 @@ todo_t_plugins(Config) ->
|
||||||
ok = emqx_plugins:delete_package(NameVsn),
|
ok = emqx_plugins:delete_package(NameVsn),
|
||||||
ok = install_plugin(PackagePath),
|
ok = install_plugin(PackagePath),
|
||||||
{ok, StopRes} = describe_plugins(NameVsn),
|
{ok, StopRes} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StopRes
|
||||||
|
),
|
||||||
{ok, StopRes1} = update_plugin(NameVsn, "start"),
|
{ok, StopRes1} = update_plugin(NameVsn, "start"),
|
||||||
?assertEqual([], StopRes1),
|
?assertEqual([], StopRes1),
|
||||||
{ok, StartRes} = describe_plugins(NameVsn),
|
{ok, StartRes} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}]}, StartRes),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StartRes
|
||||||
|
),
|
||||||
{ok, []} = update_plugin(NameVsn, "stop"),
|
{ok, []} = update_plugin(NameVsn, "stop"),
|
||||||
{ok, StopRes2} = describe_plugins(NameVsn),
|
{ok, StopRes2} = describe_plugins(NameVsn),
|
||||||
?assertMatch(#{<<"running_status">> := [
|
?assertMatch(
|
||||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes2),
|
#{
|
||||||
|
<<"running_status">> := [
|
||||||
|
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
StopRes2
|
||||||
|
),
|
||||||
{ok, []} = uninstall_plugin(NameVsn),
|
{ok, []} = uninstall_plugin(NameVsn),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -87,8 +105,16 @@ describe_plugins(Name) ->
|
||||||
install_plugin(FilePath) ->
|
install_plugin(FilePath) ->
|
||||||
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
||||||
case emqx_mgmt_api_test_util:upload_request(Path, FilePath, "plugin",
|
case
|
||||||
<<"application/gzip">>, [], Token) of
|
emqx_mgmt_api_test_util:upload_request(
|
||||||
|
Path,
|
||||||
|
FilePath,
|
||||||
|
"plugin",
|
||||||
|
<<"application/gzip">>,
|
||||||
|
[],
|
||||||
|
Token
|
||||||
|
)
|
||||||
|
of
|
||||||
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
|
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
@ -109,7 +135,6 @@ uninstall_plugin(Name) ->
|
||||||
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
|
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
|
||||||
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
|
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
|
||||||
|
|
||||||
|
|
||||||
build_demo_plugin_package(Dir) ->
|
build_demo_plugin_package(Dir) ->
|
||||||
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
|
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
|
||||||
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
|
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
|
||||||
|
|
|
@ -37,7 +37,9 @@ end_per_suite(_) ->
|
||||||
emqx_mgmt_api_test_util:end_suite().
|
emqx_mgmt_api_test_util:end_suite().
|
||||||
|
|
||||||
t_publish_api(_) ->
|
t_publish_api(_) ->
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||||
|
@ -50,14 +52,16 @@ t_publish_api(_) ->
|
||||||
emqtt:disconnect(Client).
|
emqtt:disconnect(Client).
|
||||||
|
|
||||||
t_publish_bulk_api(_) ->
|
t_publish_bulk_api(_) ->
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||||
Payload = <<"hello">>,
|
Payload = <<"hello">>,
|
||||||
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
|
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
|
||||||
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
Body =[#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
Body = [#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
||||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
|
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
|
||||||
ResponseMap = emqx_json:decode(Response, [return_maps]),
|
ResponseMap = emqx_json:decode(Response, [return_maps]),
|
||||||
?assertEqual(2, erlang:length(ResponseMap)),
|
?assertEqual(2, erlang:length(ResponseMap)),
|
||||||
|
@ -68,12 +72,12 @@ t_publish_bulk_api(_) ->
|
||||||
receive_assert(Topic, Qos, Payload) ->
|
receive_assert(Topic, Qos, Payload) ->
|
||||||
receive
|
receive
|
||||||
{publish, Message} ->
|
{publish, Message} ->
|
||||||
ReceiveTopic = maps:get(topic, Message),
|
ReceiveTopic = maps:get(topic, Message),
|
||||||
ReceiveQos = maps:get(qos, Message),
|
ReceiveQos = maps:get(qos, Message),
|
||||||
ReceivePayload = maps:get(payload, Message),
|
ReceivePayload = maps:get(payload, Message),
|
||||||
?assertEqual(ReceiveTopic , Topic),
|
?assertEqual(ReceiveTopic, Topic),
|
||||||
?assertEqual(ReceiveQos , Qos),
|
?assertEqual(ReceiveQos, Qos),
|
||||||
?assertEqual(ReceivePayload , Payload),
|
?assertEqual(ReceivePayload, Payload),
|
||||||
ok
|
ok
|
||||||
after 5000 ->
|
after 5000 ->
|
||||||
timeout
|
timeout
|
||||||
|
|
|
@ -37,7 +37,7 @@ t_stats_api(_) ->
|
||||||
SystemStats1 = emqx_mgmt:get_stats(),
|
SystemStats1 = emqx_mgmt:get_stats(),
|
||||||
Fun1 =
|
Fun1 =
|
||||||
fun(Key) ->
|
fun(Key) ->
|
||||||
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
||||||
end,
|
end,
|
||||||
lists:foreach(Fun1, maps:keys(SystemStats1)),
|
lists:foreach(Fun1, maps:keys(SystemStats1)),
|
||||||
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
|
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
|
||||||
|
|
|
@ -58,13 +58,13 @@ t_subscription_api(_) ->
|
||||||
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
|
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
|
||||||
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
|
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
|
||||||
end,
|
end,
|
||||||
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
||||||
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
|
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
|
||||||
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
|
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
|
||||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
|
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
|
||||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
|
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
|
||||||
|
|
||||||
QS = uri_string:compose_query([
|
QS = uri_string:compose_query([
|
||||||
{"clientid", ?CLIENTID},
|
{"clientid", ?CLIENTID},
|
||||||
{"topic", ?TOPIC2_TOPIC_ONLY},
|
{"topic", ?TOPIC2_TOPIC_ONLY},
|
||||||
{"node", atom_to_list(node())},
|
{"node", atom_to_list(node())},
|
||||||
|
@ -83,11 +83,11 @@ t_subscription_api(_) ->
|
||||||
?assertEqual(length(SubscriptionsList2), 1),
|
?assertEqual(length(SubscriptionsList2), 1),
|
||||||
|
|
||||||
MatchQs = uri_string:compose_query([
|
MatchQs = uri_string:compose_query([
|
||||||
{"clientid", ?CLIENTID},
|
{"clientid", ?CLIENTID},
|
||||||
{"node", atom_to_list(node())},
|
{"node", atom_to_list(node())},
|
||||||
{"qos", "0"},
|
{"qos", "0"},
|
||||||
{"match_topic", "t/#"}
|
{"match_topic", "t/#"}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
|
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
|
||||||
MatchData = emqx_json:decode(MatchRes, [return_maps]),
|
MatchData = emqx_json:decode(MatchRes, [return_maps]),
|
||||||
|
|
|
@ -28,7 +28,6 @@ init_suite(Apps) ->
|
||||||
application:load(emqx_management),
|
application:load(emqx_management),
|
||||||
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
|
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
|
||||||
|
|
||||||
|
|
||||||
end_suite() ->
|
end_suite() ->
|
||||||
end_suite([]).
|
end_suite([]).
|
||||||
|
|
||||||
|
@ -53,36 +52,44 @@ request_api(Method, Url, AuthOrHeaders) ->
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders) ->
|
request_api(Method, Url, QueryParams, AuthOrHeaders) ->
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, []).
|
request_api(Method, Url, QueryParams, AuthOrHeaders, []).
|
||||||
|
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, [])
|
request_api(Method, Url, QueryParams, AuthOrHeaders, []) when
|
||||||
when (Method =:= options) orelse
|
(Method =:= options) orelse
|
||||||
(Method =:= get) orelse
|
(Method =:= get) orelse
|
||||||
(Method =:= put) orelse
|
(Method =:= put) orelse
|
||||||
(Method =:= head) orelse
|
(Method =:= head) orelse
|
||||||
(Method =:= delete) orelse
|
(Method =:= delete) orelse
|
||||||
(Method =:= trace) ->
|
(Method =:= trace)
|
||||||
NewUrl = case QueryParams of
|
->
|
||||||
"" -> Url;
|
NewUrl =
|
||||||
_ -> Url ++ "?" ++ QueryParams
|
case QueryParams of
|
||||||
end,
|
"" -> Url;
|
||||||
|
_ -> Url ++ "?" ++ QueryParams
|
||||||
|
end,
|
||||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
|
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
|
||||||
request_api(Method, Url, QueryParams, AuthOrHeaders, Body)
|
request_api(Method, Url, QueryParams, AuthOrHeaders, Body) when
|
||||||
when (Method =:= post) orelse
|
(Method =:= post) orelse
|
||||||
(Method =:= patch) orelse
|
(Method =:= patch) orelse
|
||||||
(Method =:= put) orelse
|
(Method =:= put) orelse
|
||||||
(Method =:= delete) ->
|
(Method =:= delete)
|
||||||
NewUrl = case QueryParams of
|
->
|
||||||
"" -> Url;
|
NewUrl =
|
||||||
_ -> Url ++ "?" ++ QueryParams
|
case QueryParams of
|
||||||
end,
|
"" -> Url;
|
||||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}).
|
_ -> Url ++ "?" ++ QueryParams
|
||||||
|
end,
|
||||||
|
do_request_api(
|
||||||
|
Method,
|
||||||
|
{NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}
|
||||||
|
).
|
||||||
|
|
||||||
do_request_api(Method, Request)->
|
do_request_api(Method, Request) ->
|
||||||
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
||||||
case httpc:request(Method, Request, [], []) of
|
case httpc:request(Method, Request, [], []) of
|
||||||
{error, socket_closed_remotely} ->
|
{error, socket_closed_remotely} ->
|
||||||
{error, socket_closed_remotely};
|
{error, socket_closed_remotely};
|
||||||
{ok, {{"HTTP/1.1", Code, _}, _, Return} }
|
{ok, {{"HTTP/1.1", Code, _}, _, Return}} when
|
||||||
when Code >= 200 andalso Code =< 299 ->
|
Code >= 200 andalso Code =< 299
|
||||||
|
->
|
||||||
{ok, Return};
|
{ok, Return};
|
||||||
{ok, {Reason, _, _} = Error} ->
|
{ok, {Reason, _, _} = Error} ->
|
||||||
ct:pal("error: ~p~n", [Error]),
|
ct:pal("error: ~p~n", [Error]),
|
||||||
|
@ -97,11 +104,10 @@ auth_header_() ->
|
||||||
|
|
||||||
build_http_header(X) when is_list(X) ->
|
build_http_header(X) when is_list(X) ->
|
||||||
X;
|
X;
|
||||||
|
|
||||||
build_http_header(X) ->
|
build_http_header(X) ->
|
||||||
[X].
|
[X].
|
||||||
|
|
||||||
api_path(Parts)->
|
api_path(Parts) ->
|
||||||
?SERVER ++ filename:join([?BASE_PATH | Parts]).
|
?SERVER ++ filename:join([?BASE_PATH | Parts]).
|
||||||
|
|
||||||
%% Usage:
|
%% Usage:
|
||||||
|
@ -117,20 +123,27 @@ api_path(Parts)->
|
||||||
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
|
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
|
||||||
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
|
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
|
||||||
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||||
{ok, binary()} | {error, list()} when
|
{ok, binary()} | {error, list()}
|
||||||
URL:: binary(),
|
when
|
||||||
FilePath:: binary(),
|
URL :: binary(),
|
||||||
Name:: binary(),
|
FilePath :: binary(),
|
||||||
MimeType:: binary(),
|
Name :: binary(),
|
||||||
RequestData:: list(),
|
MimeType :: binary(),
|
||||||
AuthorizationToken:: binary().
|
RequestData :: list(),
|
||||||
|
AuthorizationToken :: binary().
|
||||||
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||||
Method = post,
|
Method = post,
|
||||||
Filename = filename:basename(FilePath),
|
Filename = filename:basename(FilePath),
|
||||||
{ok, Data} = file:read_file(FilePath),
|
{ok, Data} = file:read_file(FilePath),
|
||||||
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
|
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
|
||||||
RequestBody = format_multipart_formdata(Data, RequestData, Name,
|
RequestBody = format_multipart_formdata(
|
||||||
[Filename], MimeType, Boundary),
|
Data,
|
||||||
|
RequestData,
|
||||||
|
Name,
|
||||||
|
[Filename],
|
||||||
|
MimeType,
|
||||||
|
Boundary
|
||||||
|
),
|
||||||
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
|
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
|
||||||
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
|
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
|
||||||
Headers = [
|
Headers = [
|
||||||
|
@ -146,34 +159,56 @@ upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) -
|
||||||
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
|
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
|
||||||
|
|
||||||
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||||
binary() when
|
binary()
|
||||||
Data:: binary(),
|
when
|
||||||
Params:: list(),
|
Data :: binary(),
|
||||||
Name:: binary(),
|
Params :: list(),
|
||||||
FileNames:: list(),
|
Name :: binary(),
|
||||||
MimeType:: binary(),
|
FileNames :: list(),
|
||||||
Boundary:: binary().
|
MimeType :: binary(),
|
||||||
|
Boundary :: binary().
|
||||||
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||||
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
|
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
|
||||||
LineSeparator = <<"\r\n">>,
|
LineSeparator = <<"\r\n">>,
|
||||||
WithParams = lists:foldl(fun({Key, Value}, Acc) ->
|
WithParams = lists:foldl(
|
||||||
erlang:iolist_to_binary([
|
fun({Key, Value}, Acc) ->
|
||||||
Acc,
|
erlang:iolist_to_binary([
|
||||||
StartBoundary, LineSeparator,
|
Acc,
|
||||||
<<"Content-Disposition: form-data; name=\"">>, Key, <<"\"">>,
|
StartBoundary,
|
||||||
LineSeparator, LineSeparator,
|
LineSeparator,
|
||||||
Value, LineSeparator
|
<<"Content-Disposition: form-data; name=\"">>,
|
||||||
])
|
Key,
|
||||||
end, <<"">>, Params),
|
<<"\"">>,
|
||||||
WithPaths = lists:foldl(fun(FileName, Acc) ->
|
LineSeparator,
|
||||||
erlang:iolist_to_binary([
|
LineSeparator,
|
||||||
Acc,
|
Value,
|
||||||
StartBoundary, LineSeparator,
|
LineSeparator
|
||||||
<<"Content-Disposition: form-data; name=\"">>, Name, <<"\"; filename=\"">>,
|
])
|
||||||
FileName, <<"\"">>, LineSeparator,
|
end,
|
||||||
<<"Content-Type: ">>, MimeType, LineSeparator, LineSeparator,
|
<<"">>,
|
||||||
Data,
|
Params
|
||||||
LineSeparator
|
),
|
||||||
])
|
WithPaths = lists:foldl(
|
||||||
end, WithParams, FileNames),
|
fun(FileName, Acc) ->
|
||||||
|
erlang:iolist_to_binary([
|
||||||
|
Acc,
|
||||||
|
StartBoundary,
|
||||||
|
LineSeparator,
|
||||||
|
<<"Content-Disposition: form-data; name=\"">>,
|
||||||
|
Name,
|
||||||
|
<<"\"; filename=\"">>,
|
||||||
|
FileName,
|
||||||
|
<<"\"">>,
|
||||||
|
LineSeparator,
|
||||||
|
<<"Content-Type: ">>,
|
||||||
|
MimeType,
|
||||||
|
LineSeparator,
|
||||||
|
LineSeparator,
|
||||||
|
Data,
|
||||||
|
LineSeparator
|
||||||
|
])
|
||||||
|
end,
|
||||||
|
WithParams,
|
||||||
|
FileNames
|
||||||
|
),
|
||||||
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
|
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
|
||||||
|
|
|
@ -32,7 +32,9 @@ end_per_suite(_) ->
|
||||||
|
|
||||||
t_nodes_api(_) ->
|
t_nodes_api(_) ->
|
||||||
Topic = <<"test_topic">>,
|
Topic = <<"test_topic">>,
|
||||||
{ok, Client} = emqtt:start_link(#{username => <<"routes_username">>, clientid => <<"routes_cid">>}),
|
{ok, Client} = emqtt:start_link(#{
|
||||||
|
username => <<"routes_username">>, clientid => <<"routes_cid">>
|
||||||
|
}),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
{ok, _, _} = emqtt:subscribe(Client, Topic),
|
{ok, _, _} = emqtt:subscribe(Client, Topic),
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,9 @@ t_http_test(_Config) ->
|
||||||
#{
|
#{
|
||||||
<<"code">> => <<"BAD_REQUEST">>,
|
<<"code">> => <<"BAD_REQUEST">>,
|
||||||
<<"message">> => <<"name : mandatory_required_field">>
|
<<"message">> => <<"name : mandatory_required_field">>
|
||||||
}, json(Body)),
|
},
|
||||||
|
json(Body)
|
||||||
|
),
|
||||||
|
|
||||||
Name = <<"test-name">>,
|
Name = <<"test-name">>,
|
||||||
Trace = [
|
Trace = [
|
||||||
|
@ -77,32 +79,47 @@ t_http_test(_Config) ->
|
||||||
|
|
||||||
%% update
|
%% update
|
||||||
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
|
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
|
||||||
?assertEqual(#{<<"enable">> => false,
|
?assertEqual(
|
||||||
<<"name">> => <<"test-name">>}, json(Update)),
|
#{
|
||||||
|
<<"enable">> => false,
|
||||||
|
<<"name">> => <<"test-name">>
|
||||||
|
},
|
||||||
|
json(Update)
|
||||||
|
),
|
||||||
|
|
||||||
?assertMatch({error, {"HTTP/1.1", 404, _}, _},
|
?assertMatch(
|
||||||
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})),
|
{error, {"HTTP/1.1", 404, _}, _},
|
||||||
|
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})
|
||||||
|
),
|
||||||
{ok, List1} = request_api(get, api_path("trace"), Header),
|
{ok, List1} = request_api(get, api_path("trace"), Header),
|
||||||
[Data1] = json(List1),
|
[Data1] = json(List1),
|
||||||
Node = atom_to_binary(node()),
|
Node = atom_to_binary(node()),
|
||||||
?assertMatch(#{
|
?assertMatch(
|
||||||
<<"status">> := <<"stopped">>,
|
#{
|
||||||
<<"name">> := <<"test-name">>,
|
<<"status">> := <<"stopped">>,
|
||||||
<<"log_size">> := #{Node := _},
|
<<"name">> := <<"test-name">>,
|
||||||
<<"start_at">> := _,
|
<<"log_size">> := #{Node := _},
|
||||||
<<"end_at">> := _,
|
<<"start_at">> := _,
|
||||||
<<"type">> := <<"topic">>,
|
<<"end_at">> := _,
|
||||||
<<"topic">> := <<"/x/y/z">>
|
<<"type">> := <<"topic">>,
|
||||||
}, Data1),
|
<<"topic">> := <<"/x/y/z">>
|
||||||
|
},
|
||||||
|
Data1
|
||||||
|
),
|
||||||
|
|
||||||
%% delete
|
%% delete
|
||||||
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
|
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
|
||||||
?assertEqual(<<>>, Delete),
|
?assertEqual(<<>>, Delete),
|
||||||
|
|
||||||
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound}
|
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound} =
|
||||||
= request_api(delete, api_path("trace/test-name"), Header),
|
request_api(delete, api_path("trace/test-name"), Header),
|
||||||
?assertEqual(#{<<"code">> => <<"NOT_FOUND">>,
|
?assertEqual(
|
||||||
<<"message">> => <<"test-name NOT FOUND">>}, json(DeleteNotFound)),
|
#{
|
||||||
|
<<"code">> => <<"NOT_FOUND">>,
|
||||||
|
<<"message">> => <<"test-name NOT FOUND">>
|
||||||
|
},
|
||||||
|
json(DeleteNotFound)
|
||||||
|
),
|
||||||
|
|
||||||
{ok, List2} = request_api(get, api_path("trace"), Header),
|
{ok, List2} = request_api(get, api_path("trace"), Header),
|
||||||
?assertEqual([], json(List2)),
|
?assertEqual([], json(List2)),
|
||||||
|
@ -123,29 +140,43 @@ t_create_failed(_Config) ->
|
||||||
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
|
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
|
||||||
|
|
||||||
BadName1 = {<<"name">>, <<"test/bad">>},
|
BadName1 = {<<"name">>, <<"test/bad">>},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [BadName1 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [BadName1 | Trace])
|
||||||
|
),
|
||||||
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
|
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [BadName2 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [BadName2 | Trace])
|
||||||
|
),
|
||||||
|
|
||||||
%% already_exist
|
%% already_exist
|
||||||
GoodName = {<<"name">>, <<"test-name-0">>},
|
GoodName = {<<"name">>, <<"test-name-0">>},
|
||||||
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
|
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
|
||||||
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
|
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [GoodName | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [GoodName | Trace])
|
||||||
|
),
|
||||||
|
|
||||||
%% MAX Limited
|
%% MAX Limited
|
||||||
lists:map(fun(Seq) ->
|
lists:map(
|
||||||
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
fun(Seq) ->
|
||||||
Trace0 = [{name, Name0}, {type, topic},
|
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
||||||
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}],
|
Trace0 = [
|
||||||
{ok, _} = emqx_trace:create(Trace0)
|
{name, Name0},
|
||||||
end, lists:seq(1, 30 - ets:info(emqx_trace, size))),
|
{type, topic},
|
||||||
|
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}
|
||||||
|
],
|
||||||
|
{ok, _} = emqx_trace:create(Trace0)
|
||||||
|
end,
|
||||||
|
lists:seq(1, 30 - ets:info(emqx_trace, size))
|
||||||
|
),
|
||||||
GoodName1 = {<<"name">>, <<"test-name-1">>},
|
GoodName1 = {<<"name">>, <<"test-name-1">>},
|
||||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
?assertMatch(
|
||||||
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])),
|
{error, {"HTTP/1.1", 400, _}, _},
|
||||||
|
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])
|
||||||
|
),
|
||||||
unload(),
|
unload(),
|
||||||
emqx_trace:clear(),
|
emqx_trace:clear(),
|
||||||
ok.
|
ok.
|
||||||
|
@ -158,14 +189,23 @@ t_download_log(_Config) ->
|
||||||
create_trace(Name, ClientId, Now),
|
create_trace(Name, ClientId, Now),
|
||||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
[begin _ = emqtt:ping(Client) end ||_ <- lists:seq(1, 5)],
|
[
|
||||||
|
begin
|
||||||
|
_ = emqtt:ping(Client)
|
||||||
|
end
|
||||||
|
|| _ <- lists:seq(1, 5)
|
||||||
|
],
|
||||||
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
|
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
|
||||||
Header = auth_header_(),
|
Header = auth_header_(),
|
||||||
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
|
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
|
||||||
{ok, [_Comment,
|
{ok, [
|
||||||
#zip_file{name = ZipName,
|
_Comment,
|
||||||
info = #file_info{size = Size, type = regular, access = read_write}}]}
|
#zip_file{
|
||||||
= zip:table(Binary),
|
name = ZipName,
|
||||||
|
info = #file_info{size = Size, type = regular, access = read_write}
|
||||||
|
}
|
||||||
|
]} =
|
||||||
|
zip:table(Binary),
|
||||||
?assert(Size > 0),
|
?assert(Size > 0),
|
||||||
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
|
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
|
||||||
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
|
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
|
||||||
|
@ -176,13 +216,18 @@ create_trace(Name, ClientId, Start) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
#{timetrap => 900},
|
#{timetrap => 900},
|
||||||
begin
|
begin
|
||||||
{ok, _} = emqx_trace:create([{<<"name">>, Name},
|
{ok, _} = emqx_trace:create([
|
||||||
{<<"type">>, clientid}, {<<"clientid">>, ClientId}, {<<"start_at">>, Start}]),
|
{<<"name">>, Name},
|
||||||
|
{<<"type">>, clientid},
|
||||||
|
{<<"clientid">>, ClientId},
|
||||||
|
{<<"start_at">>, Start}
|
||||||
|
]),
|
||||||
?block_until(#{?snk_kind := update_trace_done})
|
?block_until(#{?snk_kind := update_trace_done})
|
||||||
end,
|
end,
|
||||||
fun(Trace) ->
|
fun(Trace) ->
|
||||||
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
|
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
|
||||||
end).
|
end
|
||||||
|
).
|
||||||
|
|
||||||
t_stream_log(_Config) ->
|
t_stream_log(_Config) ->
|
||||||
application:set_env(emqx, allow_anonymous, true),
|
application:set_env(emqx, allow_anonymous, true),
|
||||||
|
@ -194,7 +239,12 @@ t_stream_log(_Config) ->
|
||||||
create_trace(Name, ClientId, Now - 10),
|
create_trace(Name, ClientId, Now - 10),
|
||||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||||
{ok, _} = emqtt:connect(Client),
|
{ok, _} = emqtt:connect(Client),
|
||||||
[begin _ = emqtt:ping(Client) end || _ <- lists:seq(1, 5)],
|
[
|
||||||
|
begin
|
||||||
|
_ = emqtt:ping(Client)
|
||||||
|
end
|
||||||
|
|| _ <- lists:seq(1, 5)
|
||||||
|
],
|
||||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
|
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
|
||||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
|
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
|
||||||
ok = emqtt:disconnect(Client),
|
ok = emqtt:disconnect(Client),
|
||||||
|
@ -239,8 +289,9 @@ do_request_api(Method, Request) ->
|
||||||
{error, socket_closed_remotely};
|
{error, socket_closed_remotely};
|
||||||
{error, {shutdown, server_closed}} ->
|
{error, {shutdown, server_closed}} ->
|
||||||
{error, server_closed};
|
{error, server_closed};
|
||||||
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}}
|
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when
|
||||||
when Code =:= 200 orelse Code =:= 201 orelse Code =:= 204 ->
|
Code =:= 200 orelse Code =:= 201 orelse Code =:= 204
|
||||||
|
->
|
||||||
{ok, Return};
|
{ok, Return};
|
||||||
{ok, {Reason, _Header, Body}} ->
|
{ok, {Reason, _Header, Body}} ->
|
||||||
{error, Reason, Body}
|
{error, Reason, Body}
|
||||||
|
@ -250,7 +301,8 @@ api_path(Path) ->
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
|
||||||
|
|
||||||
json(Data) ->
|
json(Data) ->
|
||||||
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.
|
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]),
|
||||||
|
Jsx.
|
||||||
|
|
||||||
load() ->
|
load() ->
|
||||||
emqx_trace:start_link().
|
emqx_trace:start_link().
|
||||||
|
|
Loading…
Reference in New Issue