chore: reformat mgmt code.
This commit is contained in:
parent
5661948a86
commit
aa7807baeb
|
@ -1,20 +1,28 @@
|
|||
%% -*- mode: erlang -*-
|
||||
|
||||
{deps, [ {emqx, {path, "../emqx"}}
|
||||
]}.
|
||||
{deps, [{emqx, {path, "../emqx"}}]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
warnings_as_errors,
|
||||
debug_info,
|
||||
{parse_transform}]}.
|
||||
{erl_opts, [
|
||||
warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
warnings_as_errors,
|
||||
debug_info,
|
||||
{parse_transform}
|
||||
]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{xref_checks, [
|
||||
undefined_function_calls,
|
||||
undefined_functions,
|
||||
locals_not_used,
|
||||
deprecated_function_calls,
|
||||
warnings_as_errors,
|
||||
deprecated_functions
|
||||
]}.
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
|
||||
{project_plugins, [erlfmt]}.
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
%% -*- mode: erlang -*-
|
||||
{application, emqx_management,
|
||||
[{description, "EMQX Management API and CLI"},
|
||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_management_sup]},
|
||||
{applications, [kernel,stdlib,emqx_plugins,minirest,emqx]},
|
||||
{mod, {emqx_mgmt_app,[]}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://github.com/emqx/emqx-management"}
|
||||
]}
|
||||
]}.
|
||||
{application, emqx_management, [
|
||||
{description, "EMQX Management API and CLI"},
|
||||
% strict semver, bump manually!
|
||||
{vsn, "5.0.0"},
|
||||
{modules, []},
|
||||
{registered, [emqx_management_sup]},
|
||||
{applications, [kernel, stdlib, emqx_plugins, minirest, emqx]},
|
||||
{mod, {emqx_mgmt_app, []}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQX Team <contact@emqx.io>"]},
|
||||
{links, [
|
||||
{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://github.com/emqx/emqx-management"}
|
||||
]}
|
||||
]}.
|
||||
|
|
|
@ -19,9 +19,11 @@
|
|||
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-export([ namespace/0
|
||||
, roots/0
|
||||
, fields/1]).
|
||||
-export([
|
||||
namespace/0,
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
namespace() -> management.
|
||||
|
||||
|
|
|
@ -25,76 +25,82 @@
|
|||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
%% Nodes and Brokers API
|
||||
-export([ list_nodes/0
|
||||
, lookup_node/1
|
||||
, list_brokers/0
|
||||
, lookup_broker/1
|
||||
, node_info/0
|
||||
, node_info/1
|
||||
, broker_info/0
|
||||
, broker_info/1
|
||||
]).
|
||||
-export([
|
||||
list_nodes/0,
|
||||
lookup_node/1,
|
||||
list_brokers/0,
|
||||
lookup_broker/1,
|
||||
node_info/0,
|
||||
node_info/1,
|
||||
broker_info/0,
|
||||
broker_info/1
|
||||
]).
|
||||
|
||||
%% Metrics and Stats
|
||||
-export([ get_metrics/0
|
||||
, get_metrics/1
|
||||
, get_stats/0
|
||||
, get_stats/1
|
||||
]).
|
||||
-export([
|
||||
get_metrics/0,
|
||||
get_metrics/1,
|
||||
get_stats/0,
|
||||
get_stats/1
|
||||
]).
|
||||
|
||||
%% Clients, Sessions
|
||||
-export([ lookup_client/2
|
||||
, lookup_client/3
|
||||
, kickout_client/1
|
||||
, list_authz_cache/1
|
||||
, list_client_subscriptions/1
|
||||
, client_subscriptions/2
|
||||
, clean_authz_cache/1
|
||||
, clean_authz_cache/2
|
||||
, clean_authz_cache_all/0
|
||||
, clean_authz_cache_all/1
|
||||
, set_ratelimit_policy/2
|
||||
, set_quota_policy/2
|
||||
, set_keepalive/2
|
||||
]).
|
||||
-export([
|
||||
lookup_client/2,
|
||||
lookup_client/3,
|
||||
kickout_client/1,
|
||||
list_authz_cache/1,
|
||||
list_client_subscriptions/1,
|
||||
client_subscriptions/2,
|
||||
clean_authz_cache/1,
|
||||
clean_authz_cache/2,
|
||||
clean_authz_cache_all/0,
|
||||
clean_authz_cache_all/1,
|
||||
set_ratelimit_policy/2,
|
||||
set_quota_policy/2,
|
||||
set_keepalive/2
|
||||
]).
|
||||
|
||||
%% Internal funcs
|
||||
-export([do_call_client/2]).
|
||||
|
||||
%% Subscriptions
|
||||
-export([ list_subscriptions/1
|
||||
, list_subscriptions_via_topic/2
|
||||
, list_subscriptions_via_topic/3
|
||||
, lookup_subscriptions/1
|
||||
, lookup_subscriptions/2
|
||||
-export([
|
||||
list_subscriptions/1,
|
||||
list_subscriptions_via_topic/2,
|
||||
list_subscriptions_via_topic/3,
|
||||
lookup_subscriptions/1,
|
||||
lookup_subscriptions/2,
|
||||
|
||||
, do_list_subscriptions/0
|
||||
]).
|
||||
do_list_subscriptions/0
|
||||
]).
|
||||
|
||||
%% PubSub
|
||||
-export([ subscribe/2
|
||||
, do_subscribe/2
|
||||
, publish/1
|
||||
, unsubscribe/2
|
||||
, do_unsubscribe/2
|
||||
]).
|
||||
-export([
|
||||
subscribe/2,
|
||||
do_subscribe/2,
|
||||
publish/1,
|
||||
unsubscribe/2,
|
||||
do_unsubscribe/2
|
||||
]).
|
||||
|
||||
%% Alarms
|
||||
-export([ get_alarms/1
|
||||
, get_alarms/2
|
||||
, deactivate/2
|
||||
, delete_all_deactivated_alarms/0
|
||||
, delete_all_deactivated_alarms/1
|
||||
]).
|
||||
-export([
|
||||
get_alarms/1,
|
||||
get_alarms/2,
|
||||
deactivate/2,
|
||||
delete_all_deactivated_alarms/0,
|
||||
delete_all_deactivated_alarms/1
|
||||
]).
|
||||
|
||||
%% Banned
|
||||
-export([ create_banned/1
|
||||
, delete_banned/1
|
||||
]).
|
||||
-export([
|
||||
create_banned/1,
|
||||
delete_banned/1
|
||||
]).
|
||||
|
||||
%% Common Table API
|
||||
-export([ max_row_limit/0
|
||||
]).
|
||||
-export([max_row_limit/0]).
|
||||
|
||||
-define(APP, emqx_management).
|
||||
|
||||
|
@ -113,24 +119,26 @@ list_nodes() ->
|
|||
lookup_node(Node) -> node_info(Node).
|
||||
|
||||
node_info() ->
|
||||
Memory = emqx_vm:get_memory(),
|
||||
Memory = emqx_vm:get_memory(),
|
||||
Info = maps:from_list([{K, list_to_binary(V)} || {K, V} <- emqx_vm:loads()]),
|
||||
BrokerInfo = emqx_sys:info(),
|
||||
Info#{node => node(),
|
||||
otp_release => otp_rel(),
|
||||
memory_total => proplists:get_value(allocated, Memory),
|
||||
memory_used => proplists:get_value(used, Memory),
|
||||
process_available => erlang:system_info(process_limit),
|
||||
process_used => erlang:system_info(process_count),
|
||||
Info#{
|
||||
node => node(),
|
||||
otp_release => otp_rel(),
|
||||
memory_total => proplists:get_value(allocated, Memory),
|
||||
memory_used => proplists:get_value(used, Memory),
|
||||
process_available => erlang:system_info(process_limit),
|
||||
process_used => erlang:system_info(process_count),
|
||||
|
||||
max_fds => proplists:get_value(
|
||||
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
|
||||
connections => ets:info(emqx_channel, size),
|
||||
node_status => 'Running',
|
||||
uptime => proplists:get_value(uptime, BrokerInfo),
|
||||
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
||||
role => mria_rlog:role()
|
||||
}.
|
||||
max_fds => proplists:get_value(
|
||||
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))
|
||||
),
|
||||
connections => ets:info(emqx_channel, size),
|
||||
node_status => 'Running',
|
||||
uptime => proplists:get_value(uptime, BrokerInfo),
|
||||
version => iolist_to_binary(proplists:get_value(version, BrokerInfo)),
|
||||
role => mria_rlog:role()
|
||||
}.
|
||||
|
||||
node_info(Node) ->
|
||||
wrap_rpc(emqx_management_proto_v1:node_info(Node)).
|
||||
|
@ -167,18 +175,21 @@ get_metrics(Node) ->
|
|||
|
||||
get_stats() ->
|
||||
GlobalStatsKeys =
|
||||
[ 'retained.count'
|
||||
, 'retained.max'
|
||||
, 'topics.count'
|
||||
, 'topics.max'
|
||||
, 'subscriptions.shared.count'
|
||||
, 'subscriptions.shared.max'
|
||||
[
|
||||
'retained.count',
|
||||
'retained.max',
|
||||
'topics.count',
|
||||
'topics.max',
|
||||
'subscriptions.shared.count',
|
||||
'subscriptions.shared.max'
|
||||
],
|
||||
CountStats = nodes_info_count([
|
||||
begin
|
||||
Stats = get_stats(Node),
|
||||
delete_keys(Stats, GlobalStatsKeys)
|
||||
end || Node <- mria_mnesia:running_nodes()]),
|
||||
end
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
]),
|
||||
GlobalStats = maps:with(GlobalStatsKeys, maps:from_list(get_stats(node()))),
|
||||
maps:merge(CountStats, GlobalStats).
|
||||
|
||||
|
@ -207,21 +218,28 @@ nodes_info_count(PropList) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
lookup_client({clientid, ClientId}, FormatFun) ->
|
||||
lists:append([lookup_client(Node, {clientid, ClientId}, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()]);
|
||||
|
||||
lists:append([
|
||||
lookup_client(Node, {clientid, ClientId}, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
]);
|
||||
lookup_client({username, Username}, FormatFun) ->
|
||||
lists:append([lookup_client(Node, {username, Username}, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()]).
|
||||
lists:append([
|
||||
lookup_client(Node, {username, Username}, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
]).
|
||||
|
||||
lookup_client(Node, Key, {M, F}) ->
|
||||
case wrap_rpc(emqx_cm_proto_v1:lookup_client(Node, Key)) of
|
||||
{error, Err} -> {error, Err};
|
||||
L -> lists:map(fun({Chan, Info0, Stats}) ->
|
||||
Info = Info0#{node => Node},
|
||||
M:F({Chan, Info, Stats})
|
||||
end,
|
||||
L)
|
||||
{error, Err} ->
|
||||
{error, Err};
|
||||
L ->
|
||||
lists:map(
|
||||
fun({Chan, Info0, Stats}) ->
|
||||
Info = Info0#{node => Node},
|
||||
M:F({Chan, Info, Stats})
|
||||
end,
|
||||
L
|
||||
)
|
||||
end.
|
||||
|
||||
kickout_client({ClientID, FormatFun}) ->
|
||||
|
@ -266,7 +284,7 @@ clean_authz_cache(Node, ClientId) ->
|
|||
clean_authz_cache_all() ->
|
||||
Results = [{Node, clean_authz_cache_all(Node)} || Node <- mria_mnesia:running_nodes()],
|
||||
case lists:filter(fun({_Node, Item}) -> Item =/= ok end, Results) of
|
||||
[] -> ok;
|
||||
[] -> ok;
|
||||
BadNodes -> {error, BadNodes}
|
||||
end.
|
||||
|
||||
|
@ -287,9 +305,13 @@ set_keepalive(_ClientId, _Interval) ->
|
|||
%% @private
|
||||
call_client(ClientId, Req) ->
|
||||
Results = [call_client(Node, ClientId, Req) || Node <- mria_mnesia:running_nodes()],
|
||||
Expected = lists:filter(fun({error, _}) -> false;
|
||||
(_) -> true
|
||||
end, Results),
|
||||
Expected = lists:filter(
|
||||
fun
|
||||
({error, _}) -> false;
|
||||
(_) -> true
|
||||
end,
|
||||
Results
|
||||
),
|
||||
case Expected of
|
||||
[] -> {error, not_found};
|
||||
[Result | _] -> Result
|
||||
|
@ -299,13 +321,15 @@ call_client(ClientId, Req) ->
|
|||
-spec do_call_client(emqx_types:clientid(), term()) -> term().
|
||||
do_call_client(ClientId, Req) ->
|
||||
case emqx_cm:lookup_channels(ClientId) of
|
||||
[] -> {error, not_found};
|
||||
[] ->
|
||||
{error, not_found};
|
||||
Pids when is_list(Pids) ->
|
||||
Pid = lists:last(Pids),
|
||||
case emqx_cm:get_chan_info(ClientId, Pid) of
|
||||
#{conninfo := #{conn_mod := ConnMod}} ->
|
||||
erlang:apply(ConnMod, call, [Pid, Req]);
|
||||
undefined -> {error, not_found}
|
||||
undefined ->
|
||||
{error, not_found}
|
||||
end
|
||||
end.
|
||||
|
||||
|
@ -320,22 +344,28 @@ call_client(Node, ClientId, Req) ->
|
|||
-spec do_list_subscriptions() -> [map()].
|
||||
do_list_subscriptions() ->
|
||||
case check_row_limit([mqtt_subproperty]) of
|
||||
false -> throw(max_row_limit);
|
||||
ok -> [#{topic => Topic, clientid => ClientId, options => Options}
|
||||
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)]
|
||||
false ->
|
||||
throw(max_row_limit);
|
||||
ok ->
|
||||
[
|
||||
#{topic => Topic, clientid => ClientId, options => Options}
|
||||
|| {{Topic, ClientId}, Options} <- ets:tab2list(mqtt_subproperty)
|
||||
]
|
||||
end.
|
||||
|
||||
list_subscriptions(Node) ->
|
||||
wrap_rpc(emqx_management_proto_v1:list_subscriptions(Node)).
|
||||
|
||||
list_subscriptions_via_topic(Topic, FormatFun) ->
|
||||
lists:append([list_subscriptions_via_topic(Node, Topic, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()]).
|
||||
lists:append([
|
||||
list_subscriptions_via_topic(Node, Topic, FormatFun)
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
]).
|
||||
|
||||
list_subscriptions_via_topic(Node, Topic, _FormatFun = {M, F}) ->
|
||||
case wrap_rpc(emqx_broker_proto_v1:list_subscriptions_via_topic(Node, Topic)) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
Result -> M:F(Result)
|
||||
Result -> M:F(Result)
|
||||
end.
|
||||
|
||||
lookup_subscriptions(ClientId) ->
|
||||
|
@ -354,20 +384,17 @@ subscribe(ClientId, TopicTables) ->
|
|||
subscribe([Node | Nodes], ClientId, TopicTables) ->
|
||||
case wrap_rpc(emqx_management_proto_v1:subscribe(Node, ClientId, TopicTables)) of
|
||||
{error, _} -> subscribe(Nodes, ClientId, TopicTables);
|
||||
{subscribe, Res} ->
|
||||
{subscribe, Res, Node}
|
||||
{subscribe, Res} -> {subscribe, Res, Node}
|
||||
end;
|
||||
|
||||
subscribe([], _ClientId, _TopicTables) ->
|
||||
{error, channel_not_found}.
|
||||
|
||||
-spec do_subscribe(emqx_types:clientid(), emqx_types:topic_filters()) ->
|
||||
{subscribe, _} | {error, atom()}.
|
||||
{subscribe, _} | {error, atom()}.
|
||||
do_subscribe(ClientId, TopicTables) ->
|
||||
case ets:lookup(emqx_channel, ClientId) of
|
||||
[] -> {error, channel_not_found};
|
||||
[{_, Pid}] ->
|
||||
Pid ! {subscribe, TopicTables}
|
||||
[{_, Pid}] -> Pid ! {subscribe, TopicTables}
|
||||
end.
|
||||
|
||||
%%TODO: ???
|
||||
|
@ -376,12 +403,12 @@ publish(Msg) ->
|
|||
emqx:publish(Msg).
|
||||
|
||||
-spec unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
unsubscribe(ClientId, Topic) ->
|
||||
unsubscribe(mria_mnesia:running_nodes(), ClientId, Topic).
|
||||
|
||||
-spec unsubscribe([node()], emqx_types:clientid(), emqx_types:topic()) ->
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
unsubscribe([Node | Nodes], ClientId, Topic) ->
|
||||
case wrap_rpc(emqx_management_proto_v1:unsubscribe(Node, ClientId, Topic)) of
|
||||
{error, _} -> unsubscribe(Nodes, ClientId, Topic);
|
||||
|
@ -391,12 +418,11 @@ unsubscribe([], _ClientId, _Topic) ->
|
|||
{error, channel_not_found}.
|
||||
|
||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||
{unsubscribe, _} | {error, _}.
|
||||
{unsubscribe, _} | {error, _}.
|
||||
do_unsubscribe(ClientId, Topic) ->
|
||||
case ets:lookup(emqx_channel, ClientId) of
|
||||
[] -> {error, channel_not_found};
|
||||
[{_, Pid}] ->
|
||||
Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
||||
[{_, Pid}] -> Pid ! {unsubscribe, [emqx_topic:parse(Topic)]}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -426,11 +452,18 @@ add_duration_field([], _Now, Acc) ->
|
|||
Acc;
|
||||
add_duration_field([Alarm = #{activated := true, activate_at := ActivateAt} | Rest], Now, Acc) ->
|
||||
add_duration_field(Rest, Now, [Alarm#{duration => Now - ActivateAt} | Acc]);
|
||||
|
||||
add_duration_field( [Alarm = #{ activated := false
|
||||
, activate_at := ActivateAt
|
||||
, deactivate_at := DeactivateAt} | Rest]
|
||||
, Now, Acc) ->
|
||||
add_duration_field(
|
||||
[
|
||||
Alarm = #{
|
||||
activated := false,
|
||||
activate_at := ActivateAt,
|
||||
deactivate_at := DeactivateAt
|
||||
}
|
||||
| Rest
|
||||
],
|
||||
Now,
|
||||
Acc
|
||||
) ->
|
||||
add_duration_field(Rest, Now, [Alarm#{duration => DeactivateAt - ActivateAt} | Acc]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -462,13 +495,13 @@ check_row_limit([], _Limit) ->
|
|||
ok;
|
||||
check_row_limit([Tab | Tables], Limit) ->
|
||||
case table_size(Tab) > Limit of
|
||||
true -> false;
|
||||
true -> false;
|
||||
false -> check_row_limit(Tables, Limit)
|
||||
end.
|
||||
|
||||
check_results(Results) ->
|
||||
case lists:any(fun(Item) -> Item =:= ok end, Results) of
|
||||
true -> ok;
|
||||
true -> ok;
|
||||
false -> wrap_rpc(lists:last(Results))
|
||||
end.
|
||||
|
||||
|
|
|
@ -22,16 +22,18 @@
|
|||
|
||||
-define(FRESH_SELECT, fresh_select).
|
||||
|
||||
-export([ paginate/3
|
||||
, paginate/4
|
||||
]).
|
||||
-export([
|
||||
paginate/3,
|
||||
paginate/4
|
||||
]).
|
||||
|
||||
%% first_next query APIs
|
||||
-export([ node_query/5
|
||||
, cluster_query/4
|
||||
, select_table_with_count/5
|
||||
, b2i/1
|
||||
]).
|
||||
-export([
|
||||
node_query/5,
|
||||
cluster_query/4,
|
||||
select_table_with_count/5,
|
||||
b2i/1
|
||||
]).
|
||||
|
||||
-export([do_query/6]).
|
||||
|
||||
|
@ -50,30 +52,30 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
|
|||
Limit = b2i(limit(Params)),
|
||||
Cursor = qlc:cursor(Qh),
|
||||
case Page > 1 of
|
||||
true ->
|
||||
true ->
|
||||
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
||||
ok;
|
||||
false -> ok
|
||||
false ->
|
||||
ok
|
||||
end,
|
||||
Rows = qlc:next_answers(Cursor, Limit),
|
||||
qlc:delete_cursor(Cursor),
|
||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]}.
|
||||
#{
|
||||
meta => #{page => Page, limit => Limit, count => Count},
|
||||
data => [erlang:apply(Module, FormatFun, [Row]) || Row <- Rows]
|
||||
}.
|
||||
|
||||
query_handle(Table) when is_atom(Table) ->
|
||||
qlc:q([R || R <- ets:table(Table)]);
|
||||
|
||||
query_handle({Table, Opts}) when is_atom(Table) ->
|
||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||
|
||||
query_handle([Table]) when is_atom(Table) ->
|
||||
qlc:q([R || R <- ets:table(Table)]);
|
||||
|
||||
query_handle([{Table, Opts}]) when is_atom(Table) ->
|
||||
qlc:q([R || R <- ets:table(Table, Opts)]);
|
||||
|
||||
query_handle(Tables) ->
|
||||
qlc:append([query_handle(T) || T <- Tables]). %
|
||||
%
|
||||
qlc:append([query_handle(T) || T <- Tables]).
|
||||
|
||||
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||
Options = {traverse, {select, MatchSpec}},
|
||||
|
@ -87,16 +89,12 @@ query_handle(Tables, MatchSpec) ->
|
|||
|
||||
count(Table) when is_atom(Table) ->
|
||||
ets:info(Table, size);
|
||||
|
||||
count({Table, _}) when is_atom(Table) ->
|
||||
ets:info(Table, size);
|
||||
|
||||
count([Table]) when is_atom(Table) ->
|
||||
ets:info(Table, size);
|
||||
|
||||
count([{Table, _}]) when is_atom(Table) ->
|
||||
ets:info(Table, size);
|
||||
|
||||
count(Tables) ->
|
||||
lists:sum([count(T) || T <- Tables]).
|
||||
|
||||
|
@ -121,7 +119,7 @@ limit(Params) ->
|
|||
|
||||
init_meta(Params) ->
|
||||
Limit = b2i(limit(Params)),
|
||||
Page = b2i(page(Params)),
|
||||
Page = b2i(page(Params)),
|
||||
#{
|
||||
page => Page,
|
||||
limit => Limit,
|
||||
|
@ -134,17 +132,24 @@ init_meta(Params) ->
|
|||
|
||||
node_query(Node, QString, Tab, QSchema, QueryFun) ->
|
||||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||
page_limit_check_query( init_meta(QString)
|
||||
, { fun do_node_query/5
|
||||
, [Node, Tab, NQString, QueryFun, init_meta(QString)]}).
|
||||
page_limit_check_query(
|
||||
init_meta(QString),
|
||||
{fun do_node_query/5, [Node, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||
).
|
||||
|
||||
%% @private
|
||||
do_node_query(Node, Tab, QString, QueryFun, Meta) ->
|
||||
do_node_query(Node, Tab, QString, QueryFun, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
||||
|
||||
do_node_query( Node, Tab, QString, QueryFun, Continuation
|
||||
, Meta = #{limit := Limit}
|
||||
, Results) ->
|
||||
do_node_query(
|
||||
Node,
|
||||
Tab,
|
||||
QString,
|
||||
QueryFun,
|
||||
Continuation,
|
||||
Meta = #{limit := Limit},
|
||||
Results
|
||||
) ->
|
||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||
{error, {badrpc, R}} ->
|
||||
{error, Node, {badrpc, R}};
|
||||
|
@ -164,18 +169,33 @@ cluster_query(QString, Tab, QSchema, QueryFun) ->
|
|||
{_CodCnt, NQString} = parse_qstring(QString, QSchema),
|
||||
Nodes = mria_mnesia:running_nodes(),
|
||||
page_limit_check_query(
|
||||
init_meta(QString)
|
||||
, {fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}).
|
||||
init_meta(QString),
|
||||
{fun do_cluster_query/5, [Nodes, Tab, NQString, QueryFun, init_meta(QString)]}
|
||||
).
|
||||
|
||||
%% @private
|
||||
do_cluster_query(Nodes, Tab, QString, QueryFun, Meta) ->
|
||||
do_cluster_query( Nodes, Tab, QString, QueryFun
|
||||
, _Continuation = ?FRESH_SELECT, Meta, _Results = []).
|
||||
do_cluster_query(
|
||||
Nodes,
|
||||
Tab,
|
||||
QString,
|
||||
QueryFun,
|
||||
_Continuation = ?FRESH_SELECT,
|
||||
Meta,
|
||||
_Results = []
|
||||
).
|
||||
|
||||
do_cluster_query([], _Tab, _QString, _QueryFun, _Continuation, Meta, Results) ->
|
||||
#{meta => Meta, data => Results};
|
||||
do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
||||
Meta = #{limit := Limit}, Results) ->
|
||||
do_cluster_query(
|
||||
[Node | Tail] = Nodes,
|
||||
Tab,
|
||||
QString,
|
||||
QueryFun,
|
||||
Continuation,
|
||||
Meta = #{limit := Limit},
|
||||
Results
|
||||
) ->
|
||||
case do_query(Node, Tab, QString, QueryFun, Continuation, Limit) of
|
||||
{error, {badrpc, R}} ->
|
||||
{error, Node, {bar_rpc, R}};
|
||||
|
@ -192,11 +212,18 @@ do_cluster_query([Node | Tail] = Nodes, Tab, QString, QueryFun, Continuation,
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @private This function is exempt from BPAPI
|
||||
do_query(Node, Tab, QString, {M,F}, Continuation, Limit) when Node =:= node() ->
|
||||
do_query(Node, Tab, QString, {M, F}, Continuation, Limit) when Node =:= node() ->
|
||||
erlang:apply(M, F, [Tab, QString, Continuation, Limit]);
|
||||
do_query(Node, Tab, QString, QueryFun, Continuation, Limit) ->
|
||||
case rpc:call(Node, ?MODULE, do_query,
|
||||
[Node, Tab, QString, QueryFun, Continuation, Limit], 50000) of
|
||||
case
|
||||
rpc:call(
|
||||
Node,
|
||||
?MODULE,
|
||||
do_query,
|
||||
[Node, Tab, QString, QueryFun, Continuation, Limit],
|
||||
50000
|
||||
)
|
||||
of
|
||||
{badrpc, _} = R -> {error, R};
|
||||
Ret -> Ret
|
||||
end.
|
||||
|
@ -220,8 +247,9 @@ sub_query_result(Len, Rows, Limit, Results, Meta) ->
|
|||
%% Table Select
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
||||
when is_function(FuzzyFilterFun) andalso Limit > 0 ->
|
||||
select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun) when
|
||||
is_function(FuzzyFilterFun) andalso Limit > 0
|
||||
->
|
||||
case ets:select(Tab, Ms, Limit) of
|
||||
'$end_of_table' ->
|
||||
{0, [], ?FRESH_SELECT};
|
||||
|
@ -229,8 +257,9 @@ select_table_with_count(Tab, {Ms, FuzzyFilterFun}, ?FRESH_SELECT, Limit, FmtFun)
|
|||
Rows = FuzzyFilterFun(RawResult),
|
||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||
end;
|
||||
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun)
|
||||
when is_function(FuzzyFilterFun) ->
|
||||
select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun) when
|
||||
is_function(FuzzyFilterFun)
|
||||
->
|
||||
case ets:select(ets:repair_continuation(Continuation, Ms)) of
|
||||
'$end_of_table' ->
|
||||
{0, [], ?FRESH_SELECT};
|
||||
|
@ -238,8 +267,9 @@ select_table_with_count(_Tab, {Ms, FuzzyFilterFun}, Continuation, _Limit, FmtFun
|
|||
Rows = FuzzyFilterFun(RawResult),
|
||||
{length(Rows), lists:map(FmtFun, Rows), NContinuation}
|
||||
end;
|
||||
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun)
|
||||
when Limit > 0 ->
|
||||
select_table_with_count(Tab, Ms, ?FRESH_SELECT, Limit, FmtFun) when
|
||||
Limit > 0
|
||||
->
|
||||
case ets:select(Tab, Ms, Limit) of
|
||||
'$end_of_table' ->
|
||||
{0, [], ?FRESH_SELECT};
|
||||
|
@ -267,36 +297,53 @@ parse_qstring(QString, QSchema) ->
|
|||
do_parse_qstring([], _, Acc1, Acc2) ->
|
||||
NAcc2 = [E || E <- Acc2, not lists:keymember(element(1, E), 1, Acc1)],
|
||||
{lists:reverse(Acc1), lists:reverse(NAcc2)};
|
||||
|
||||
do_parse_qstring([{Key, Value} | RestQString], QSchema, Acc1, Acc2) ->
|
||||
case proplists:get_value(Key, QSchema) of
|
||||
undefined -> do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
||||
undefined ->
|
||||
do_parse_qstring(RestQString, QSchema, Acc1, Acc2);
|
||||
Type ->
|
||||
case Key of
|
||||
<<Prefix:4/binary, NKey/binary>>
|
||||
when Prefix =:= <<"gte_">>;
|
||||
Prefix =:= <<"lte_">> ->
|
||||
OpposeKey = case Prefix of
|
||||
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
||||
<<"lte_">> -> <<"gte_", NKey/binary>>
|
||||
end,
|
||||
<<Prefix:4/binary, NKey/binary>> when
|
||||
Prefix =:= <<"gte_">>;
|
||||
Prefix =:= <<"lte_">>
|
||||
->
|
||||
OpposeKey =
|
||||
case Prefix of
|
||||
<<"gte_">> -> <<"lte_", NKey/binary>>;
|
||||
<<"lte_">> -> <<"gte_", NKey/binary>>
|
||||
end,
|
||||
case lists:keytake(OpposeKey, 1, RestQString) of
|
||||
false ->
|
||||
do_parse_qstring( RestQString, QSchema
|
||||
, [qs(Key, Value, Type) | Acc1], Acc2);
|
||||
do_parse_qstring(
|
||||
RestQString,
|
||||
QSchema,
|
||||
[qs(Key, Value, Type) | Acc1],
|
||||
Acc2
|
||||
);
|
||||
{value, {K2, V2}, NParams} ->
|
||||
do_parse_qstring( NParams, QSchema
|
||||
, [qs(Key, Value, K2, V2, Type) | Acc1], Acc2)
|
||||
do_parse_qstring(
|
||||
NParams,
|
||||
QSchema,
|
||||
[qs(Key, Value, K2, V2, Type) | Acc1],
|
||||
Acc2
|
||||
)
|
||||
end;
|
||||
_ ->
|
||||
case is_fuzzy_key(Key) of
|
||||
true ->
|
||||
do_parse_qstring( RestQString, QSchema
|
||||
, Acc1, [qs(Key, Value, Type) | Acc2]);
|
||||
do_parse_qstring(
|
||||
RestQString,
|
||||
QSchema,
|
||||
Acc1,
|
||||
[qs(Key, Value, Type) | Acc2]
|
||||
);
|
||||
_ ->
|
||||
do_parse_qstring( RestQString, QSchema
|
||||
, [qs(Key, Value, Type) | Acc1], Acc2)
|
||||
|
||||
do_parse_qstring(
|
||||
RestQString,
|
||||
QSchema,
|
||||
[qs(Key, Value, Type) | Acc1],
|
||||
Acc2
|
||||
)
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
@ -310,7 +357,7 @@ qs(K, Value0, Type) ->
|
|||
try
|
||||
qs(K, to_type(Value0, Type))
|
||||
catch
|
||||
throw : bad_value_type ->
|
||||
throw:bad_value_type ->
|
||||
throw({bad_value_type, {K, Type, Value0}})
|
||||
end.
|
||||
|
||||
|
@ -333,12 +380,11 @@ is_fuzzy_key(_) ->
|
|||
false.
|
||||
|
||||
page_start(1, _) -> 1;
|
||||
page_start(Page, Limit) -> (Page-1) * Limit + 1.
|
||||
|
||||
page_start(Page, Limit) -> (Page - 1) * Limit + 1.
|
||||
|
||||
judge_page_with_counting(Len, Meta = #{page := Page, limit := Limit, count := Count}) ->
|
||||
PageStart = page_start(Page, Limit),
|
||||
PageEnd = Page * Limit,
|
||||
PageEnd = Page * Limit,
|
||||
case Count + Len of
|
||||
NCount when NCount < PageStart ->
|
||||
{more, Meta#{count => NCount}};
|
||||
|
@ -353,7 +399,7 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
|||
case (Count - Len) < PageStart of
|
||||
true ->
|
||||
NeedNowNum = Count - PageStart + 1,
|
||||
SubStart = Len - NeedNowNum + 1,
|
||||
SubStart = Len - NeedNowNum + 1,
|
||||
{SubStart, NeedNowNum};
|
||||
false ->
|
||||
{_SubStart = 1, _NeedNowNum = Len}
|
||||
|
@ -361,8 +407,9 @@ rows_sub_params(Len, _Meta = #{page := Page, limit := Limit, count := Count}) ->
|
|||
|
||||
page_limit_check_query(Meta, {F, A}) ->
|
||||
case Meta of
|
||||
#{page := Page, limit := Limit}
|
||||
when Page < 1; Limit < 1 ->
|
||||
#{page := Page, limit := Limit} when
|
||||
Page < 1; Limit < 1
|
||||
->
|
||||
{error, page_limit_invalid};
|
||||
_ ->
|
||||
erlang:apply(F, A)
|
||||
|
@ -376,7 +423,7 @@ to_type(V, TargetType) ->
|
|||
try
|
||||
to_type_(V, TargetType)
|
||||
catch
|
||||
_ : _ ->
|
||||
_:_ ->
|
||||
throw(bad_value_type)
|
||||
end.
|
||||
|
||||
|
@ -419,37 +466,43 @@ to_ip_port(IPAddress) ->
|
|||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
params2qs_test() ->
|
||||
QSchema = [{<<"str">>, binary},
|
||||
{<<"int">>, integer},
|
||||
{<<"atom">>, atom},
|
||||
{<<"ts">>, timestamp},
|
||||
{<<"gte_range">>, integer},
|
||||
{<<"lte_range">>, integer},
|
||||
{<<"like_fuzzy">>, binary},
|
||||
{<<"match_topic">>, binary}],
|
||||
QString = [{<<"str">>, <<"abc">>},
|
||||
{<<"int">>, <<"123">>},
|
||||
{<<"atom">>, <<"connected">>},
|
||||
{<<"ts">>, <<"156000">>},
|
||||
{<<"gte_range">>, <<"1">>},
|
||||
{<<"lte_range">>, <<"5">>},
|
||||
{<<"like_fuzzy">>, <<"user">>},
|
||||
{<<"match_topic">>, <<"t/#">>}],
|
||||
ExpectedQs = [{str, '=:=', <<"abc">>},
|
||||
{int, '=:=', 123},
|
||||
{atom, '=:=', connected},
|
||||
{ts, '=:=', 156000},
|
||||
{range, '>=', 1, '=<', 5}
|
||||
],
|
||||
FuzzyNQString = [{fuzzy, like, <<"user">>},
|
||||
{topic, match, <<"t/#">>}],
|
||||
QSchema = [
|
||||
{<<"str">>, binary},
|
||||
{<<"int">>, integer},
|
||||
{<<"atom">>, atom},
|
||||
{<<"ts">>, timestamp},
|
||||
{<<"gte_range">>, integer},
|
||||
{<<"lte_range">>, integer},
|
||||
{<<"like_fuzzy">>, binary},
|
||||
{<<"match_topic">>, binary}
|
||||
],
|
||||
QString = [
|
||||
{<<"str">>, <<"abc">>},
|
||||
{<<"int">>, <<"123">>},
|
||||
{<<"atom">>, <<"connected">>},
|
||||
{<<"ts">>, <<"156000">>},
|
||||
{<<"gte_range">>, <<"1">>},
|
||||
{<<"lte_range">>, <<"5">>},
|
||||
{<<"like_fuzzy">>, <<"user">>},
|
||||
{<<"match_topic">>, <<"t/#">>}
|
||||
],
|
||||
ExpectedQs = [
|
||||
{str, '=:=', <<"abc">>},
|
||||
{int, '=:=', 123},
|
||||
{atom, '=:=', connected},
|
||||
{ts, '=:=', 156000},
|
||||
{range, '>=', 1, '=<', 5}
|
||||
],
|
||||
FuzzyNQString = [
|
||||
{fuzzy, like, <<"user">>},
|
||||
{topic, match, <<"t/#">>}
|
||||
],
|
||||
?assertEqual({7, {ExpectedQs, FuzzyNQString}}, parse_qstring(QString, QSchema)),
|
||||
|
||||
{0, {[], []}} = parse_qstring([{not_a_predefined_params, val}], QSchema).
|
||||
|
||||
-endif.
|
||||
|
||||
|
||||
b2i(Bin) when is_binary(Bin) ->
|
||||
binary_to_integer(Bin);
|
||||
b2i(Any) ->
|
||||
|
|
|
@ -43,9 +43,12 @@ schema("/alarms") ->
|
|||
parameters => [
|
||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||
{activated, hoconsc:mk(boolean(), #{in => query,
|
||||
desc => ?DESC(get_alarms_qs_activated),
|
||||
required => false})}
|
||||
{activated,
|
||||
hoconsc:mk(boolean(), #{
|
||||
in => query,
|
||||
desc => ?DESC(get_alarms_qs_activated),
|
||||
required => false
|
||||
})}
|
||||
],
|
||||
responses => #{
|
||||
200 => [
|
||||
|
@ -54,7 +57,7 @@ schema("/alarms") ->
|
|||
]
|
||||
}
|
||||
},
|
||||
delete => #{
|
||||
delete => #{
|
||||
description => ?DESC(delete_alarms_api),
|
||||
responses => #{
|
||||
204 => ?DESC(delete_alarms_api_response204)
|
||||
|
@ -64,21 +67,38 @@ schema("/alarms") ->
|
|||
|
||||
fields(alarm) ->
|
||||
[
|
||||
{node, hoconsc:mk(binary(),
|
||||
#{desc => ?DESC(node), example => atom_to_list(node())})},
|
||||
{name, hoconsc:mk(binary(),
|
||||
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>})},
|
||||
{message, hoconsc:mk(binary(), #{desc => ?DESC(message),
|
||||
example => <<"System memory usage is higher than 70%">>})},
|
||||
{details, hoconsc:mk(map(), #{desc => ?DESC(details),
|
||||
example => #{<<"high_watermark">> => 70}})},
|
||||
{node,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{desc => ?DESC(node), example => atom_to_list(node())}
|
||||
)},
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{desc => ?DESC(node), example => <<"high_system_memory_usage">>}
|
||||
)},
|
||||
{message,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(message),
|
||||
example => <<"System memory usage is higher than 70%">>
|
||||
})},
|
||||
{details,
|
||||
hoconsc:mk(map(), #{
|
||||
desc => ?DESC(details),
|
||||
example => #{<<"high_watermark">> => 70}
|
||||
})},
|
||||
{duration, hoconsc:mk(integer(), #{desc => ?DESC(duration), example => 297056})},
|
||||
{activate_at, hoconsc:mk(binary(), #{desc => ?DESC(activate_at),
|
||||
example => <<"2021-10-25T11:52:52.548+08:00">>})},
|
||||
{deactivate_at, hoconsc:mk(binary(), #{desc => ?DESC(deactivate_at),
|
||||
example => <<"2021-10-31T10:52:52.548+08:00">>})}
|
||||
{activate_at,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(activate_at),
|
||||
example => <<"2021-10-25T11:52:52.548+08:00">>
|
||||
})},
|
||||
{deactivate_at,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(deactivate_at),
|
||||
example => <<"2021-10-31T10:52:52.548+08:00">>
|
||||
})}
|
||||
];
|
||||
|
||||
fields(meta) ->
|
||||
emqx_dashboard_swagger:fields(page) ++
|
||||
emqx_dashboard_swagger:fields(limit) ++
|
||||
|
@ -100,7 +120,6 @@ alarms(get, #{query_string := QString}) ->
|
|||
Response ->
|
||||
{200, Response}
|
||||
end;
|
||||
|
||||
alarms(delete, _Params) ->
|
||||
_ = emqx_mgmt:delete_all_deactivated_alarms(),
|
||||
{204}.
|
||||
|
@ -109,11 +128,10 @@ alarms(delete, _Params) ->
|
|||
%% internal
|
||||
|
||||
query(Table, _QsSpec, Continuation, Limit) ->
|
||||
Ms = [{'$1',[],['$1']}],
|
||||
Ms = [{'$1', [], ['$1']}],
|
||||
emqx_mgmt_api:select_table_with_count(Table, Ms, Continuation, Limit, fun format_alarm/1).
|
||||
|
||||
format_alarm(Alarms) when is_list(Alarms) ->
|
||||
[emqx_alarm:format(Alarm) || Alarm <- Alarms];
|
||||
|
||||
format_alarm(Alarm) ->
|
||||
emqx_alarm:format(Alarm).
|
||||
|
|
|
@ -31,7 +31,6 @@ api_spec() ->
|
|||
paths() ->
|
||||
["/api_key", "/api_key/:name"].
|
||||
|
||||
|
||||
schema("/api_key") ->
|
||||
#{
|
||||
'operationId' => api_key,
|
||||
|
@ -82,41 +81,80 @@ schema("/api_key/:name") ->
|
|||
|
||||
fields(app) ->
|
||||
[
|
||||
{name, hoconsc:mk(binary(),
|
||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
example => <<"EMQX-API-KEY-1">>})},
|
||||
{api_key, hoconsc:mk(binary(),
|
||||
#{desc => """TODO:uses HMAC-SHA256 for signing.""",
|
||||
example => <<"a4697a5c75a769f6">>})},
|
||||
{api_secret, hoconsc:mk(binary(),
|
||||
#{desc => """An API secret is a simple encrypted string that identifies"""
|
||||
"""an application without any principal."""
|
||||
"""They are useful for accessing public data anonymously,"""
|
||||
"""and are used to associate API requests.""",
|
||||
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>})},
|
||||
{expired_at, hoconsc:mk(hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
||||
#{desc => "No longer valid datetime",
|
||||
example => <<"2021-12-05T02:01:34.186Z">>,
|
||||
required => false,
|
||||
default => undefined
|
||||
})},
|
||||
{created_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
||||
#{desc => "ApiKey create datetime",
|
||||
example => <<"2021-12-01T00:00:00.000Z">>
|
||||
})},
|
||||
{desc, hoconsc:mk(binary(),
|
||||
#{example => <<"Note">>, required => false})},
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
example => <<"EMQX-API-KEY-1">>
|
||||
}
|
||||
)},
|
||||
{api_key,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "" "TODO:uses HMAC-SHA256 for signing." "",
|
||||
example => <<"a4697a5c75a769f6">>
|
||||
}
|
||||
)},
|
||||
{api_secret,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc =>
|
||||
""
|
||||
"An API secret is a simple encrypted string that identifies"
|
||||
""
|
||||
""
|
||||
"an application without any principal."
|
||||
""
|
||||
""
|
||||
"They are useful for accessing public data anonymously,"
|
||||
""
|
||||
""
|
||||
"and are used to associate API requests."
|
||||
"",
|
||||
example => <<"MzAyMjk3ODMwMDk0NjIzOTUxNjcwNzQ0NzQ3MTE2NDYyMDI">>
|
||||
}
|
||||
)},
|
||||
{expired_at,
|
||||
hoconsc:mk(
|
||||
hoconsc:union([undefined, emqx_datetime:epoch_second()]),
|
||||
#{
|
||||
desc => "No longer valid datetime",
|
||||
example => <<"2021-12-05T02:01:34.186Z">>,
|
||||
required => false,
|
||||
default => undefined
|
||||
}
|
||||
)},
|
||||
{created_at,
|
||||
hoconsc:mk(
|
||||
emqx_datetime:epoch_second(),
|
||||
#{
|
||||
desc => "ApiKey create datetime",
|
||||
example => <<"2021-12-01T00:00:00.000Z">>
|
||||
}
|
||||
)},
|
||||
{desc,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{example => <<"Note">>, required => false}
|
||||
)},
|
||||
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
|
||||
];
|
||||
fields(name) ->
|
||||
[{name, hoconsc:mk(binary(),
|
||||
#{
|
||||
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
||||
example => <<"EMQX-API-KEY-1">>,
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
})}
|
||||
[
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => <<"^[A-Za-z]+[A-Za-z0-9-_]*$">>,
|
||||
example => <<"EMQX-API-KEY-1">>,
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||
|
@ -129,7 +167,8 @@ validate_name(Name) ->
|
|||
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||
_ -> ok
|
||||
end;
|
||||
false -> {error, "Name Length must =< 256"}
|
||||
false ->
|
||||
{error, "Name Length must =< 256"}
|
||||
end.
|
||||
|
||||
delete(Keys, Fields) ->
|
||||
|
@ -146,10 +185,13 @@ api_key(post, #{body := App}) ->
|
|||
ExpiredAt = ensure_expired_at(App),
|
||||
Desc = unicode:characters_to_binary(Desc0, unicode),
|
||||
case emqx_mgmt_auth:create(Name, Enable, ExpiredAt, Desc) of
|
||||
{ok, NewApp} -> {200, format(NewApp)};
|
||||
{ok, NewApp} ->
|
||||
{200, format(NewApp)};
|
||||
{error, Reason} ->
|
||||
{400, #{code => 'BAD_REQUEST',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
||||
{400, #{
|
||||
code => 'BAD_REQUEST',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||
}}
|
||||
end.
|
||||
|
||||
-define(NOT_FOUND_RESPONSE, #{code => 'NOT_FOUND', message => <<"Name NOT FOUND">>}).
|
||||
|
@ -184,5 +226,5 @@ format(App = #{expired_at := ExpiredAt0, created_at := CreateAt}) ->
|
|||
created_at => list_to_binary(calendar:system_time_to_rfc3339(CreateAt))
|
||||
}.
|
||||
|
||||
ensure_expired_at(#{<<"expired_at">> := ExpiredAt})when is_integer(ExpiredAt) -> ExpiredAt;
|
||||
ensure_expired_at(#{<<"expired_at">> := ExpiredAt}) when is_integer(ExpiredAt) -> ExpiredAt;
|
||||
ensure_expired_at(_) -> undefined.
|
||||
|
|
|
@ -24,16 +24,19 @@
|
|||
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([format/1]).
|
||||
|
||||
-export([ banned/2
|
||||
, delete_banned/2
|
||||
]).
|
||||
-export([
|
||||
banned/2,
|
||||
delete_banned/2
|
||||
]).
|
||||
|
||||
-define(TAB, emqx_banned).
|
||||
|
||||
|
@ -49,7 +52,7 @@ paths() ->
|
|||
|
||||
schema("/banned") ->
|
||||
#{
|
||||
'operationId' => banned,
|
||||
'operationId' => banned,
|
||||
get => #{
|
||||
description => ?DESC(list_banned_api),
|
||||
parameters => [
|
||||
|
@ -57,7 +60,7 @@ schema("/banned") ->
|
|||
hoconsc:ref(emqx_dashboard_swagger, limit)
|
||||
],
|
||||
responses => #{
|
||||
200 =>[
|
||||
200 => [
|
||||
{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})},
|
||||
{meta, hoconsc:mk(hoconsc:ref(meta), #{})}
|
||||
]
|
||||
|
@ -69,8 +72,9 @@ schema("/banned") ->
|
|||
responses => #{
|
||||
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
|
||||
400 => emqx_dashboard_swagger:error_codes(
|
||||
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
||||
?DESC(create_banned_api_response400))
|
||||
['ALREADY_EXISTS', 'BAD_REQUEST'],
|
||||
?DESC(create_banned_api_response400)
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -80,51 +84,67 @@ schema("/banned/:as/:who") ->
|
|||
delete => #{
|
||||
description => ?DESC(delete_banned_api),
|
||||
parameters => [
|
||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||
desc => ?DESC(as),
|
||||
in => path,
|
||||
example => username})},
|
||||
{who, hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(who),
|
||||
in => path,
|
||||
example => <<"Badass">>})}
|
||||
],
|
||||
{as,
|
||||
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||
desc => ?DESC(as),
|
||||
in => path,
|
||||
example => username
|
||||
})},
|
||||
{who,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(who),
|
||||
in => path,
|
||||
example => <<"Badass">>
|
||||
})}
|
||||
],
|
||||
responses => #{
|
||||
204 => <<"Delete banned success">>,
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['NOT_FOUND'],
|
||||
?DESC(delete_banned_api_response404))
|
||||
['NOT_FOUND'],
|
||||
?DESC(delete_banned_api_response404)
|
||||
)
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(ban) ->
|
||||
[
|
||||
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||
desc => ?DESC(as),
|
||||
required => true,
|
||||
example => username})},
|
||||
{who, hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(who),
|
||||
required => true,
|
||||
example => <<"Banned name"/utf8>>})},
|
||||
{by, hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(by),
|
||||
required => false,
|
||||
example => <<"mgmt_api">>})},
|
||||
{reason, hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(reason),
|
||||
required => false,
|
||||
example => <<"Too many requests">>})},
|
||||
{at, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||
desc => ?DESC(at),
|
||||
required => false,
|
||||
example => <<"2021-10-25T21:48:47+08:00">>})},
|
||||
{until, hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||
desc => ?DESC(until),
|
||||
required => false,
|
||||
example => <<"2021-10-25T21:53:47+08:00">>})
|
||||
}
|
||||
{as,
|
||||
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
|
||||
desc => ?DESC(as),
|
||||
required => true,
|
||||
example => username
|
||||
})},
|
||||
{who,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(who),
|
||||
required => true,
|
||||
example => <<"Banned name"/utf8>>
|
||||
})},
|
||||
{by,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(by),
|
||||
required => false,
|
||||
example => <<"mgmt_api">>
|
||||
})},
|
||||
{reason,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => ?DESC(reason),
|
||||
required => false,
|
||||
example => <<"Too many requests">>
|
||||
})},
|
||||
{at,
|
||||
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||
desc => ?DESC(at),
|
||||
required => false,
|
||||
example => <<"2021-10-25T21:48:47+08:00">>
|
||||
})},
|
||||
{until,
|
||||
hoconsc:mk(emqx_datetime:epoch_second(), #{
|
||||
desc => ?DESC(until),
|
||||
required => false,
|
||||
example => <<"2021-10-25T21:53:47+08:00">>
|
||||
})}
|
||||
];
|
||||
fields(meta) ->
|
||||
emqx_dashboard_swagger:fields(page) ++
|
||||
|
@ -141,8 +161,7 @@ banned(post, #{body := Body}) ->
|
|||
Ban ->
|
||||
case emqx_banned:create(Ban) of
|
||||
{ok, Banned} -> {200, format(Banned)};
|
||||
{error, {already_exist, Old}} ->
|
||||
{400, 'ALREADY_EXISTS', format(Old)}
|
||||
{error, {already_exist, Old}} -> {400, 'ALREADY_EXISTS', format(Old)}
|
||||
end
|
||||
end.
|
||||
|
||||
|
|
|
@ -26,63 +26,70 @@
|
|||
-include("emqx_mgmt.hrl").
|
||||
|
||||
%% API
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ clients/2
|
||||
, client/2
|
||||
, subscriptions/2
|
||||
, authz_cache/2
|
||||
, subscribe/2
|
||||
, unsubscribe/2
|
||||
, subscribe_batch/2
|
||||
, set_keepalive/2
|
||||
]).
|
||||
-export([
|
||||
clients/2,
|
||||
client/2,
|
||||
subscriptions/2,
|
||||
authz_cache/2,
|
||||
subscribe/2,
|
||||
unsubscribe/2,
|
||||
subscribe_batch/2,
|
||||
set_keepalive/2
|
||||
]).
|
||||
|
||||
-export([ query/4
|
||||
, format_channel_info/1
|
||||
]).
|
||||
-export([
|
||||
query/4,
|
||||
format_channel_info/1
|
||||
]).
|
||||
|
||||
%% for batch operation
|
||||
-export([do_subscribe/3]).
|
||||
|
||||
-define(CLIENT_QTAB, emqx_channel_info).
|
||||
|
||||
-define(CLIENT_QSCHEMA,
|
||||
[ {<<"node">>, atom}
|
||||
, {<<"username">>, binary}
|
||||
, {<<"zone">>, atom}
|
||||
, {<<"ip_address">>, ip}
|
||||
, {<<"conn_state">>, atom}
|
||||
, {<<"clean_start">>, atom}
|
||||
, {<<"proto_name">>, binary}
|
||||
, {<<"proto_ver">>, integer}
|
||||
, {<<"like_clientid">>, binary}
|
||||
, {<<"like_username">>, binary}
|
||||
, {<<"gte_created_at">>, timestamp}
|
||||
, {<<"lte_created_at">>, timestamp}
|
||||
, {<<"gte_connected_at">>, timestamp}
|
||||
, {<<"lte_connected_at">>, timestamp}]).
|
||||
-define(CLIENT_QSCHEMA, [
|
||||
{<<"node">>, atom},
|
||||
{<<"username">>, binary},
|
||||
{<<"zone">>, atom},
|
||||
{<<"ip_address">>, ip},
|
||||
{<<"conn_state">>, atom},
|
||||
{<<"clean_start">>, atom},
|
||||
{<<"proto_name">>, binary},
|
||||
{<<"proto_ver">>, integer},
|
||||
{<<"like_clientid">>, binary},
|
||||
{<<"like_username">>, binary},
|
||||
{<<"gte_created_at">>, timestamp},
|
||||
{<<"lte_created_at">>, timestamp},
|
||||
{<<"gte_connected_at">>, timestamp},
|
||||
{<<"lte_connected_at">>, timestamp}
|
||||
]).
|
||||
|
||||
-define(QUERY_FUN, {?MODULE, query}).
|
||||
-define(FORMAT_FUN, {?MODULE, format_channel_info}).
|
||||
|
||||
-define(CLIENT_ID_NOT_FOUND,
|
||||
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>).
|
||||
<<"{\"code\": \"RESOURCE_NOT_FOUND\", \"reason\": \"Client id not found\"}">>
|
||||
).
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||
|
||||
paths() ->
|
||||
[ "/clients"
|
||||
, "/clients/:clientid"
|
||||
, "/clients/:clientid/authorization/cache"
|
||||
, "/clients/:clientid/subscriptions"
|
||||
, "/clients/:clientid/subscribe"
|
||||
, "/clients/:clientid/unsubscribe"
|
||||
, "/clients/:clientid/keepalive"
|
||||
[
|
||||
"/clients",
|
||||
"/clients/:clientid",
|
||||
"/clients/:clientid/authorization/cache",
|
||||
"/clients/:clientid/subscriptions",
|
||||
"/clients/:clientid/subscribe",
|
||||
"/clients/:clientid/unsubscribe",
|
||||
"/clients/:clientid/keepalive"
|
||||
].
|
||||
|
||||
schema("/clients") ->
|
||||
|
@ -93,69 +100,105 @@ schema("/clients") ->
|
|||
parameters => [
|
||||
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||
{node, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Node name">>,
|
||||
example => atom_to_list(node())})},
|
||||
{username, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"User name">>})},
|
||||
{zone, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false})},
|
||||
{ip_address, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client's IP address">>,
|
||||
example => <<"127.0.0.1">>})},
|
||||
{conn_state, hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"The current connection status of the client, ",
|
||||
"the possible values are connected,idle,disconnected">>})},
|
||||
{clean_start, hoconsc:mk(boolean(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
description => <<"Whether the client uses a new session">>})},
|
||||
{proto_name, hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
||||
in => query,
|
||||
required => false,
|
||||
description => <<"Client protocol name, ",
|
||||
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>})},
|
||||
{proto_ver, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client protocol version">>})},
|
||||
{like_clientid, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search `clientid` as substring">>})},
|
||||
{like_username, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search `username` as substring">>})},
|
||||
{gte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Search client session creation time by greater",
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
||||
{lte_created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Search client session creation time by less",
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})},
|
||||
{gte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Search client connection creation time by greater"
|
||||
" than or equal method, rfc3339 or timestamp(epoch millisecond)">>})},
|
||||
{lte_connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Search client connection creation time by less"
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)">>})}
|
||||
{node,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Node name">>,
|
||||
example => atom_to_list(node())
|
||||
})},
|
||||
{username,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"User name">>
|
||||
})},
|
||||
{zone,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false
|
||||
})},
|
||||
{ip_address,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client's IP address">>,
|
||||
example => <<"127.0.0.1">>
|
||||
})},
|
||||
{conn_state,
|
||||
hoconsc:mk(hoconsc:enum([connected, idle, disconnected]), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc =>
|
||||
<<"The current connection status of the client, ",
|
||||
"the possible values are connected,idle,disconnected">>
|
||||
})},
|
||||
{clean_start,
|
||||
hoconsc:mk(boolean(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
description => <<"Whether the client uses a new session">>
|
||||
})},
|
||||
{proto_name,
|
||||
hoconsc:mk(hoconsc:enum(['MQTT', 'CoAP', 'LwM2M', 'MQTT-SN']), #{
|
||||
in => query,
|
||||
required => false,
|
||||
description =>
|
||||
<<"Client protocol name, ",
|
||||
"the possible values are MQTT,CoAP,LwM2M,MQTT-SN">>
|
||||
})},
|
||||
{proto_ver,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client protocol version">>
|
||||
})},
|
||||
{like_clientid,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search `clientid` as substring">>
|
||||
})},
|
||||
{like_username,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Fuzzy search `username` as substring">>
|
||||
})},
|
||||
{gte_created_at,
|
||||
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc =>
|
||||
<<"Search client session creation time by greater",
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||
})},
|
||||
{lte_created_at,
|
||||
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc =>
|
||||
<<"Search client session creation time by less",
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)">>
|
||||
})},
|
||||
{gte_connected_at,
|
||||
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<
|
||||
"Search client connection creation time by greater"
|
||||
" than or equal method, rfc3339 or timestamp(epoch millisecond)"
|
||||
>>
|
||||
})},
|
||||
{lte_connected_at,
|
||||
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<
|
||||
"Search client connection creation time by less"
|
||||
" than or equal method, rfc3339 or timestamp(millisecond)"
|
||||
>>
|
||||
})}
|
||||
],
|
||||
responses => #{
|
||||
200 => [
|
||||
|
@ -164,10 +207,11 @@ schema("/clients") ->
|
|||
],
|
||||
400 =>
|
||||
emqx_dashboard_swagger:error_codes(
|
||||
['INVALID_PARAMETER'], <<"Invalid parameters">>)}
|
||||
['INVALID_PARAMETER'], <<"Invalid parameters">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid") ->
|
||||
#{
|
||||
'operationId' => client,
|
||||
|
@ -177,19 +221,23 @@ schema("/clients/:clientid") ->
|
|||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)}},
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
},
|
||||
delete => #{
|
||||
description => <<"Kick out client by client ID">>,
|
||||
parameters => [
|
||||
{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||
{clientid, hoconsc:mk(binary(), #{in => path})}
|
||||
],
|
||||
responses => #{
|
||||
204 => <<"Kick out client successfully">>,
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid/authorization/cache") ->
|
||||
#{
|
||||
'operationId' => authz_cache,
|
||||
|
@ -199,7 +247,8 @@ schema("/clients/:clientid/authorization/cache") ->
|
|||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, authz_cache), #{}),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
},
|
||||
delete => #{
|
||||
|
@ -208,11 +257,11 @@ schema("/clients/:clientid/authorization/cache") ->
|
|||
responses => #{
|
||||
204 => <<"Kick out client successfully">>,
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid/subscriptions") ->
|
||||
#{
|
||||
'operationId' => subscriptions,
|
||||
|
@ -220,13 +269,15 @@ schema("/clients/:clientid/subscriptions") ->
|
|||
description => <<"Get client subscriptions">>,
|
||||
parameters => [{clientid, hoconsc:mk(binary(), #{in => path})}],
|
||||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}),
|
||||
200 => hoconsc:mk(
|
||||
hoconsc:array(hoconsc:ref(emqx_mgmt_api_subscriptions, subscription)), #{}
|
||||
),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid/subscribe") ->
|
||||
#{
|
||||
'operationId' => subscribe,
|
||||
|
@ -237,11 +288,11 @@ schema("/clients/:clientid/subscribe") ->
|
|||
responses => #{
|
||||
200 => hoconsc:ref(emqx_mgmt_api_subscriptions, subscription),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid/unsubscribe") ->
|
||||
#{
|
||||
'operationId' => unsubscribe,
|
||||
|
@ -252,11 +303,11 @@ schema("/clients/:clientid/unsubscribe") ->
|
|||
responses => #{
|
||||
204 => <<"Unsubscribe OK">>,
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/clients/:clientid/keepalive") ->
|
||||
#{
|
||||
'operationId' => set_keepalive,
|
||||
|
@ -267,96 +318,187 @@ schema("/clients/:clientid/keepalive") ->
|
|||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:ref(?MODULE, client), #{}),
|
||||
404 => emqx_dashboard_swagger:error_codes(
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>)
|
||||
['CLIENTID_NOT_FOUND'], <<"Client id not found">>
|
||||
)
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(client) ->
|
||||
[
|
||||
{awaiting_rel_cnt, hoconsc:mk(integer(), #{desc =>
|
||||
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>})},
|
||||
{awaiting_rel_max, hoconsc:mk(integer(), #{desc =>
|
||||
<<"v4 api name [max_awaiting_rel]. "
|
||||
"Maximum allowed number of awaiting PUBREC packet">>})},
|
||||
{clean_start, hoconsc:mk(boolean(), #{desc =>
|
||||
<<"Indicate whether the client is using a brand new session">>})},
|
||||
{awaiting_rel_cnt,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>
|
||||
})},
|
||||
{awaiting_rel_max,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<
|
||||
"v4 api name [max_awaiting_rel]. "
|
||||
"Maximum allowed number of awaiting PUBREC packet"
|
||||
>>
|
||||
})},
|
||||
{clean_start,
|
||||
hoconsc:mk(boolean(), #{
|
||||
desc =>
|
||||
<<"Indicate whether the client is using a brand new session">>
|
||||
})},
|
||||
{clientid, hoconsc:mk(binary(), #{desc => <<"Client identifier">>})},
|
||||
{connected, hoconsc:mk(boolean(), #{desc => <<"Whether the client is connected">>})},
|
||||
{connected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
||||
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>})},
|
||||
{created_at, hoconsc:mk(emqx_datetime:epoch_millisecond(),
|
||||
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>})},
|
||||
{disconnected_at, hoconsc:mk(emqx_datetime:epoch_millisecond(), #{desc =>
|
||||
<<"Client offline time."
|
||||
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)">>})},
|
||||
{expiry_interval, hoconsc:mk(integer(), #{desc =>
|
||||
<<"Session expiration interval, with the unit of second">>})},
|
||||
{heap_size, hoconsc:mk(integer(), #{desc =>
|
||||
<<"Process heap size with the unit of byte">>})},
|
||||
{connected_at,
|
||||
hoconsc:mk(
|
||||
emqx_datetime:epoch_millisecond(),
|
||||
#{desc => <<"Client connection time, rfc3339 or timestamp(millisecond)">>}
|
||||
)},
|
||||
{created_at,
|
||||
hoconsc:mk(
|
||||
emqx_datetime:epoch_millisecond(),
|
||||
#{desc => <<"Session creation time, rfc3339 or timestamp(millisecond)">>}
|
||||
)},
|
||||
{disconnected_at,
|
||||
hoconsc:mk(emqx_datetime:epoch_millisecond(), #{
|
||||
desc =>
|
||||
<<
|
||||
"Client offline time."
|
||||
" It's Only valid and returned when connected is false, rfc3339 or timestamp(millisecond)"
|
||||
>>
|
||||
})},
|
||||
{expiry_interval,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Session expiration interval, with the unit of second">>
|
||||
})},
|
||||
{heap_size,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Process heap size with the unit of byte">>
|
||||
})},
|
||||
{inflight_cnt, hoconsc:mk(integer(), #{desc => <<"Current length of inflight">>})},
|
||||
{inflight_max, hoconsc:mk(integer(), #{desc =>
|
||||
<<"v4 api name [max_inflight]. Maximum length of inflight">>})},
|
||||
{inflight_max,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"v4 api name [max_inflight]. Maximum length of inflight">>
|
||||
})},
|
||||
{ip_address, hoconsc:mk(binary(), #{desc => <<"Client's IP address">>})},
|
||||
{is_bridge, hoconsc:mk(boolean(), #{desc =>
|
||||
<<"Indicates whether the client is connectedvia bridge">>})},
|
||||
{keepalive, hoconsc:mk(integer(), #{desc =>
|
||||
<<"keepalive time, with the unit of second">>})},
|
||||
{is_bridge,
|
||||
hoconsc:mk(boolean(), #{
|
||||
desc =>
|
||||
<<"Indicates whether the client is connectedvia bridge">>
|
||||
})},
|
||||
{keepalive,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"keepalive time, with the unit of second">>
|
||||
})},
|
||||
{mailbox_len, hoconsc:mk(integer(), #{desc => <<"Process mailbox size">>})},
|
||||
{mqueue_dropped, hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of messages dropped by the message queue due to exceeding the length">>})},
|
||||
{mqueue_dropped,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of messages dropped by the message queue due to exceeding the length">>
|
||||
})},
|
||||
{mqueue_len, hoconsc:mk(integer(), #{desc => <<"Current length of message queue">>})},
|
||||
{mqueue_max, hoconsc:mk(integer(), #{desc =>
|
||||
<<"v4 api name [max_mqueue]. Maximum length of message queue">>})},
|
||||
{node, hoconsc:mk(binary(), #{desc =>
|
||||
<<"Name of the node to which the client is connected">>})},
|
||||
{mqueue_max,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"v4 api name [max_mqueue]. Maximum length of message queue">>
|
||||
})},
|
||||
{node,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc =>
|
||||
<<"Name of the node to which the client is connected">>
|
||||
})},
|
||||
{port, hoconsc:mk(integer(), #{desc => <<"Client's port">>})},
|
||||
{proto_name, hoconsc:mk(binary(), #{desc => <<"Client protocol name">>})},
|
||||
{proto_ver, hoconsc:mk(integer(), #{desc => <<"Protocol version used by the client">>})},
|
||||
{recv_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets received">>})},
|
||||
{recv_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets received">>})},
|
||||
{'recv_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets">>})},
|
||||
{'recv_msg.dropped.await_pubrel_timeout', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
||||
{'recv_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS0 packets received">>})},
|
||||
{'recv_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS1 packets received">>})},
|
||||
{'recv_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS2 packets received">>})},
|
||||
{'recv_msg.dropped',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets">>
|
||||
})},
|
||||
{'recv_msg.dropped.await_pubrel_timeout',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets due to expired">>
|
||||
})},
|
||||
{'recv_msg.qos0',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS0 packets received">>
|
||||
})},
|
||||
{'recv_msg.qos1',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS1 packets received">>
|
||||
})},
|
||||
{'recv_msg.qos2',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS2 packets received">>
|
||||
})},
|
||||
{recv_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes received">>})},
|
||||
{recv_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets received">>})},
|
||||
{reductions, hoconsc:mk(integer(), #{desc => <<"Erlang reduction">>})},
|
||||
{send_cnt, hoconsc:mk(integer(), #{desc => <<"Number of TCP packets sent">>})},
|
||||
{send_msg, hoconsc:mk(integer(), #{desc => <<"Number of PUBLISH packets sent">>})},
|
||||
{'send_msg.dropped', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets">>})},
|
||||
{'send_msg.dropped.expired', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets due to expired">>})},
|
||||
{'send_msg.dropped.queue_full', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets due to queue full">>})},
|
||||
{'send_msg.dropped.too_large', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of dropped PUBLISH packets due to packet length too large">>})},
|
||||
{'send_msg.qos0', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS0 packets sent">>})},
|
||||
{'send_msg.qos1', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS1 packets sent">>})},
|
||||
{'send_msg.qos2', hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of PUBLISH QoS2 packets sent">>})},
|
||||
{'send_msg.dropped',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets">>
|
||||
})},
|
||||
{'send_msg.dropped.expired',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets due to expired">>
|
||||
})},
|
||||
{'send_msg.dropped.queue_full',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets due to queue full">>
|
||||
})},
|
||||
{'send_msg.dropped.too_large',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of dropped PUBLISH packets due to packet length too large">>
|
||||
})},
|
||||
{'send_msg.qos0',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS0 packets sent">>
|
||||
})},
|
||||
{'send_msg.qos1',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS1 packets sent">>
|
||||
})},
|
||||
{'send_msg.qos2',
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of PUBLISH QoS2 packets sent">>
|
||||
})},
|
||||
{send_oct, hoconsc:mk(integer(), #{desc => <<"Number of bytes sent">>})},
|
||||
{send_pkt, hoconsc:mk(integer(), #{desc => <<"Number of MQTT packets sent">>})},
|
||||
{subscriptions_cnt, hoconsc:mk(integer(), #{desc =>
|
||||
<<"Number of subscriptions established by this client.">>})},
|
||||
{subscriptions_max, hoconsc:mk(integer(), #{desc =>
|
||||
<<"v4 api name [max_subscriptions]",
|
||||
" Maximum number of subscriptions allowed by this client">>})},
|
||||
{subscriptions_cnt,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"Number of subscriptions established by this client.">>
|
||||
})},
|
||||
{subscriptions_max,
|
||||
hoconsc:mk(integer(), #{
|
||||
desc =>
|
||||
<<"v4 api name [max_subscriptions]",
|
||||
" Maximum number of subscriptions allowed by this client">>
|
||||
})},
|
||||
{username, hoconsc:mk(binary(), #{desc => <<"User name of client when connecting">>})},
|
||||
{will_msg, hoconsc:mk(binary(), #{desc => <<"Client will message">>})},
|
||||
{zone, hoconsc:mk(binary(), #{desc =>
|
||||
<<"Indicate the configuration group used by the client">>})}
|
||||
{zone,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc =>
|
||||
<<"Indicate the configuration group used by the client">>
|
||||
})}
|
||||
];
|
||||
|
||||
fields(authz_cache) ->
|
||||
[
|
||||
{access, hoconsc:mk(binary(), #{desc => <<"Access type">>})},
|
||||
|
@ -364,23 +506,19 @@ fields(authz_cache) ->
|
|||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic name">>})},
|
||||
{updated_time, hoconsc:mk(integer(), #{desc => <<"Update time">>})}
|
||||
];
|
||||
|
||||
fields(keepalive) ->
|
||||
[
|
||||
{interval, hoconsc:mk(integer(), #{desc => <<"Keepalive time, with the unit of second">>})}
|
||||
];
|
||||
|
||||
fields(subscribe) ->
|
||||
[
|
||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})},
|
||||
{qos, hoconsc:mk(emqx_schema:qos(), #{desc => <<"QoS">>})}
|
||||
];
|
||||
|
||||
fields(unsubscribe) ->
|
||||
[
|
||||
{topic, hoconsc:mk(binary(), #{desc => <<"Topic">>})}
|
||||
];
|
||||
|
||||
fields(meta) ->
|
||||
emqx_dashboard_swagger:fields(page) ++
|
||||
emqx_dashboard_swagger:fields(limit) ++
|
||||
|
@ -393,13 +531,11 @@ clients(get, #{query_string := QString}) ->
|
|||
|
||||
client(get, #{bindings := Bindings}) ->
|
||||
lookup(Bindings);
|
||||
|
||||
client(delete, #{bindings := Bindings}) ->
|
||||
kickout(Bindings).
|
||||
|
||||
authz_cache(get, #{bindings := Bindings}) ->
|
||||
get_authz_cache(Bindings);
|
||||
|
||||
authz_cache(delete, #{bindings := Bindings}) ->
|
||||
clean_authz_cache(Bindings).
|
||||
|
||||
|
@ -415,11 +551,14 @@ unsubscribe(post, #{bindings := #{clientid := ClientID}, body := TopicInfo}) ->
|
|||
%% TODO: batch
|
||||
subscribe_batch(post, #{bindings := #{clientid := ClientID}, body := TopicInfos}) ->
|
||||
Topics =
|
||||
[begin
|
||||
Topic = maps:get(<<"topic">>, TopicInfo),
|
||||
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
||||
#{topic => Topic, qos => Qos}
|
||||
end || TopicInfo <- TopicInfos],
|
||||
[
|
||||
begin
|
||||
Topic = maps:get(<<"topic">>, TopicInfo),
|
||||
Qos = maps:get(<<"qos">>, TopicInfo, 0),
|
||||
#{topic => Topic, qos => Qos}
|
||||
end
|
||||
|| TopicInfo <- TopicInfos
|
||||
],
|
||||
subscribe_batch(#{clientid => ClientID, topics => Topics}).
|
||||
|
||||
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
||||
|
@ -436,16 +575,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
|
|||
qos => maps:get(qos, SubOpts)
|
||||
}
|
||||
end,
|
||||
{200, lists:map(Formatter, Subs)}
|
||||
{200, lists:map(Formatter, Subs)}
|
||||
end.
|
||||
|
||||
set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
||||
case maps:find(<<"interval">>, Body) of
|
||||
error -> {400, 'BAD_REQUEST',"Interval Not Found"};
|
||||
error ->
|
||||
{400, 'BAD_REQUEST', "Interval Not Found"};
|
||||
{ok, Interval} ->
|
||||
case emqx_mgmt:set_keepalive(emqx_mgmt_util:urldecode(ClientID), Interval) of
|
||||
ok -> lookup(#{clientid => ClientID});
|
||||
{error, not_found} ->{404, ?CLIENT_ID_NOT_FOUND};
|
||||
{error, not_found} -> {404, ?CLIENT_ID_NOT_FOUND};
|
||||
{error, Reason} -> {400, #{code => 'PARAMS_ERROR', message => Reason}}
|
||||
end
|
||||
end.
|
||||
|
@ -454,16 +594,26 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
|
|||
%% api apply
|
||||
|
||||
list_clients(QString) ->
|
||||
Result = case maps:get(<<"node">>, QString, undefined) of
|
||||
undefined ->
|
||||
emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB,
|
||||
?CLIENT_QSCHEMA, ?QUERY_FUN);
|
||||
Node0 ->
|
||||
Node1 = binary_to_atom(Node0, utf8),
|
||||
QStringWithoutNode = maps:without([<<"node">>], QString),
|
||||
emqx_mgmt_api:node_query(Node1, QStringWithoutNode,
|
||||
?CLIENT_QTAB, ?CLIENT_QSCHEMA, ?QUERY_FUN)
|
||||
end,
|
||||
Result =
|
||||
case maps:get(<<"node">>, QString, undefined) of
|
||||
undefined ->
|
||||
emqx_mgmt_api:cluster_query(
|
||||
QString,
|
||||
?CLIENT_QTAB,
|
||||
?CLIENT_QSCHEMA,
|
||||
?QUERY_FUN
|
||||
);
|
||||
Node0 ->
|
||||
Node1 = binary_to_atom(Node0, utf8),
|
||||
QStringWithoutNode = maps:without([<<"node">>], QString),
|
||||
emqx_mgmt_api:node_query(
|
||||
Node1,
|
||||
QStringWithoutNode,
|
||||
?CLIENT_QTAB,
|
||||
?CLIENT_QSCHEMA,
|
||||
?QUERY_FUN
|
||||
)
|
||||
end,
|
||||
case Result of
|
||||
{error, page_limit_invalid} ->
|
||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||
|
@ -490,7 +640,7 @@ kickout(#{clientid := ClientID}) ->
|
|||
{204}
|
||||
end.
|
||||
|
||||
get_authz_cache(#{clientid := ClientID})->
|
||||
get_authz_cache(#{clientid := ClientID}) ->
|
||||
case emqx_mgmt:list_authz_cache(ClientID) of
|
||||
{error, not_found} ->
|
||||
{404, ?CLIENT_ID_NOT_FOUND};
|
||||
|
@ -524,9 +674,9 @@ subscribe(#{clientid := ClientID, topic := Topic, qos := Qos}) ->
|
|||
Response =
|
||||
#{
|
||||
clientid => ClientID,
|
||||
topic => Topic,
|
||||
qos => Qos,
|
||||
node => Node
|
||||
topic => Topic,
|
||||
qos => Qos,
|
||||
node => Node
|
||||
},
|
||||
{200, Response}
|
||||
end.
|
||||
|
@ -562,7 +712,7 @@ do_subscribe(ClientID, Topic0, Qos) ->
|
|||
end.
|
||||
|
||||
-spec do_unsubscribe(emqx_types:clientid(), emqx_types:topic()) ->
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
{unsubscribe, _} | {error, channel_not_found}.
|
||||
do_unsubscribe(ClientID, Topic) ->
|
||||
case emqx_mgmt:unsubscribe(ClientID, Topic) of
|
||||
{error, Reason} ->
|
||||
|
@ -576,14 +726,23 @@ do_unsubscribe(ClientID, Topic) ->
|
|||
|
||||
query(Tab, {QString, []}, Continuation, Limit) ->
|
||||
Ms = qs2ms(QString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit,
|
||||
fun format_channel_info/1);
|
||||
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
Ms,
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_channel_info/1
|
||||
);
|
||||
query(Tab, {QString, FuzzyQString}, Continuation, Limit) ->
|
||||
Ms = qs2ms(QString),
|
||||
FuzzyFilterFun = fuzzy_filter_fun(FuzzyQString),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, {Ms, FuzzyFilterFun}, Continuation, Limit,
|
||||
fun format_channel_info/1).
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
{Ms, FuzzyFilterFun},
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format_channel_info/1
|
||||
).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% QueryString to Match Spec
|
||||
|
@ -595,7 +754,6 @@ qs2ms(Qs) ->
|
|||
|
||||
qs2ms([], _, {MtchHead, Conds}) ->
|
||||
{MtchHead, lists:reverse(Conds)};
|
||||
|
||||
qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
|
||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
|
||||
qs2ms(Rest, N, {NMtchHead, Conds});
|
||||
|
@ -603,13 +761,16 @@ qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
|
|||
Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8),
|
||||
NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)),
|
||||
NConds = put_conds(Qs, Holder, Conds),
|
||||
qs2ms(Rest, N+1, {NMtchHead, NConds}).
|
||||
qs2ms(Rest, N + 1, {NMtchHead, NConds}).
|
||||
|
||||
put_conds({_, Op, V}, Holder, Conds) ->
|
||||
[{Op, Holder, V} | Conds];
|
||||
put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
|
||||
[{Op2, Holder, V2},
|
||||
{Op1, Holder, V1} | Conds].
|
||||
[
|
||||
{Op2, Holder, V2},
|
||||
{Op1, Holder, V1}
|
||||
| Conds
|
||||
].
|
||||
|
||||
ms(clientid, X) ->
|
||||
#{clientinfo => #{clientid => X}};
|
||||
|
@ -637,68 +798,78 @@ ms(created_at, X) ->
|
|||
|
||||
fuzzy_filter_fun(Fuzzy) ->
|
||||
fun(MsRaws) when is_list(MsRaws) ->
|
||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
||||
, MsRaws)
|
||||
lists:filter(
|
||||
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||
MsRaws
|
||||
)
|
||||
end.
|
||||
|
||||
run_fuzzy_filter(_, []) ->
|
||||
true;
|
||||
run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} | Fuzzy]) ->
|
||||
Val = case maps:get(Key, ClientInfo, <<>>) of
|
||||
undefined -> <<>>;
|
||||
V -> V
|
||||
end,
|
||||
Val =
|
||||
case maps:get(Key, ClientInfo, <<>>) of
|
||||
undefined -> <<>>;
|
||||
V -> V
|
||||
end,
|
||||
binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% format funcs
|
||||
|
||||
format_channel_info({_, ClientInfo, ClientStats}) ->
|
||||
Node = case ClientInfo of
|
||||
#{node := N} -> N;
|
||||
_ -> node()
|
||||
end,
|
||||
StatsMap = maps:without([memory, next_pkt_id, total_heap_size],
|
||||
maps:from_list(ClientStats)),
|
||||
Node =
|
||||
case ClientInfo of
|
||||
#{node := N} -> N;
|
||||
_ -> node()
|
||||
end,
|
||||
StatsMap = maps:without(
|
||||
[memory, next_pkt_id, total_heap_size],
|
||||
maps:from_list(ClientStats)
|
||||
),
|
||||
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
|
||||
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
|
||||
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
||||
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
|
||||
ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0),
|
||||
ClientInfoMap2 = maps:put(node, Node, ClientInfoMap1),
|
||||
ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2),
|
||||
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
|
||||
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
||||
ClientInfoMap = maps:put(connected, Connected, ClientInfoMap4),
|
||||
RemoveList =
|
||||
[ auth_result
|
||||
, peername
|
||||
, sockname
|
||||
, peerhost
|
||||
, conn_state
|
||||
, send_pend
|
||||
, conn_props
|
||||
, peercert
|
||||
, sockstate
|
||||
, subscriptions
|
||||
, receive_maximum
|
||||
, protocol
|
||||
, is_superuser
|
||||
, sockport
|
||||
, anonymous
|
||||
, mountpoint
|
||||
, socktype
|
||||
, active_n
|
||||
, await_rel_timeout
|
||||
, conn_mod
|
||||
, sockname
|
||||
, retry_interval
|
||||
, upgrade_qos
|
||||
, id %% sessionID, defined in emqx_session.erl
|
||||
],
|
||||
[
|
||||
auth_result,
|
||||
peername,
|
||||
sockname,
|
||||
peerhost,
|
||||
conn_state,
|
||||
send_pend,
|
||||
conn_props,
|
||||
peercert,
|
||||
sockstate,
|
||||
subscriptions,
|
||||
receive_maximum,
|
||||
protocol,
|
||||
is_superuser,
|
||||
sockport,
|
||||
anonymous,
|
||||
mountpoint,
|
||||
socktype,
|
||||
active_n,
|
||||
await_rel_timeout,
|
||||
conn_mod,
|
||||
sockname,
|
||||
retry_interval,
|
||||
upgrade_qos,
|
||||
%% sessionID, defined in emqx_session.erl
|
||||
id
|
||||
],
|
||||
TimesKeys = [created_at, connected_at, disconnected_at],
|
||||
%% format timestamp to rfc3339
|
||||
lists:foldl(fun result_format_time_fun/2
|
||||
, maps:without(RemoveList, ClientInfoMap)
|
||||
, TimesKeys).
|
||||
lists:foldl(
|
||||
fun result_format_time_fun/2,
|
||||
maps:without(RemoveList, ClientInfoMap),
|
||||
TimesKeys
|
||||
).
|
||||
|
||||
%% format func helpers
|
||||
take_maps_from_inner(_Key, Value, Current) when is_map(Value) ->
|
||||
|
@ -710,20 +881,22 @@ result_format_time_fun(Key, NClientInfoMap) ->
|
|||
case NClientInfoMap of
|
||||
#{Key := TimeStamp} ->
|
||||
NClientInfoMap#{
|
||||
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)};
|
||||
Key => emqx_datetime:epoch_to_rfc3339(TimeStamp)
|
||||
};
|
||||
#{} ->
|
||||
NClientInfoMap
|
||||
end.
|
||||
|
||||
-spec(peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}).
|
||||
-spec peername_dispart(emqx_types:peername()) -> {binary(), inet:port_number()}.
|
||||
peername_dispart({Addr, Port}) ->
|
||||
AddrBinary = list_to_binary(inet:ntoa(Addr)),
|
||||
%% PortBinary = integer_to_binary(Port),
|
||||
{AddrBinary, Port}.
|
||||
|
||||
format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) ->
|
||||
#{ access => PubSub,
|
||||
topic => Topic,
|
||||
result => AuthzResult,
|
||||
updated_time => Timestamp
|
||||
}.
|
||||
#{
|
||||
access => PubSub,
|
||||
topic => Topic,
|
||||
result => AuthzResult,
|
||||
updated_time => Timestamp
|
||||
}.
|
||||
|
|
|
@ -125,7 +125,7 @@ force_leave(delete, #{bindings := #{node := Node0}}) ->
|
|||
{400, #{code => 'BAD_REQUEST', message => error_message(Error)}}
|
||||
end.
|
||||
|
||||
-spec(join(node()) -> ok | ignore | {error, term()}).
|
||||
-spec join(node()) -> ok | ignore | {error, term()}.
|
||||
join(Node) ->
|
||||
ekka:join(Node).
|
||||
|
||||
|
|
|
@ -23,11 +23,13 @@
|
|||
-export([api_spec/0, namespace/0]).
|
||||
-export([paths/0, schema/1, fields/1]).
|
||||
|
||||
-export([ config/3
|
||||
, config_reset/3
|
||||
, configs/3
|
||||
, get_full_config/0
|
||||
, global_zone_configs/3]).
|
||||
-export([
|
||||
config/3,
|
||||
config_reset/3,
|
||||
configs/3,
|
||||
get_full_config/0,
|
||||
global_zone_configs/3
|
||||
]).
|
||||
|
||||
-export([gen_schema/1]).
|
||||
|
||||
|
@ -36,28 +38,29 @@
|
|||
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
|
||||
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
|
||||
|
||||
-define(EXCLUDES, [
|
||||
<<"exhook">>,
|
||||
<<"gateway">>,
|
||||
<<"plugins">>,
|
||||
<<"bridges">>,
|
||||
<<"rule_engine">>,
|
||||
<<"authorization">>,
|
||||
<<"authentication">>,
|
||||
<<"rpc">>,
|
||||
<<"db">>,
|
||||
<<"connectors">>,
|
||||
<<"slow_subs">>,
|
||||
<<"psk_authentication">>,
|
||||
<<"topic_metrics">>,
|
||||
<<"rewrite">>,
|
||||
<<"auto_subscribe">>,
|
||||
<<"retainer">>,
|
||||
<<"statsd">>,
|
||||
<<"delayed">>,
|
||||
<<"event_message">>,
|
||||
<<"prometheus">>,
|
||||
<<"telemetry">>
|
||||
-define(EXCLUDES,
|
||||
[
|
||||
<<"exhook">>,
|
||||
<<"gateway">>,
|
||||
<<"plugins">>,
|
||||
<<"bridges">>,
|
||||
<<"rule_engine">>,
|
||||
<<"authorization">>,
|
||||
<<"authentication">>,
|
||||
<<"rpc">>,
|
||||
<<"db">>,
|
||||
<<"connectors">>,
|
||||
<<"slow_subs">>,
|
||||
<<"psk_authentication">>,
|
||||
<<"topic_metrics">>,
|
||||
<<"rewrite">>,
|
||||
<<"auto_subscribe">>,
|
||||
<<"retainer">>,
|
||||
<<"statsd">>,
|
||||
<<"delayed">>,
|
||||
<<"event_message">>,
|
||||
<<"prometheus">>,
|
||||
<<"telemetry">>
|
||||
] ++ global_zone_roots()
|
||||
).
|
||||
|
||||
|
@ -68,7 +71,7 @@ namespace() -> "configuration".
|
|||
|
||||
paths() ->
|
||||
["/configs", "/configs_reset/:rootname", "/configs/global_zone"] ++
|
||||
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
||||
lists:map(fun({Name, _Type}) -> ?PREFIX ++ binary_to_list(Name) end, config_list()).
|
||||
|
||||
schema("/configs") ->
|
||||
#{
|
||||
|
@ -76,12 +79,20 @@ schema("/configs") ->
|
|||
get => #{
|
||||
tags => [conf],
|
||||
description =>
|
||||
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
||||
<<"Get all the configurations of the specified node, including hot and non-hot updatable items.">>,
|
||||
parameters => [
|
||||
{node, hoconsc:mk(typerefl:atom(),
|
||||
#{in => query, required => false, example => <<"emqx@127.0.0.1">>,
|
||||
desc =>
|
||||
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>})}],
|
||||
{node,
|
||||
hoconsc:mk(
|
||||
typerefl:atom(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
example => <<"emqx@127.0.0.1">>,
|
||||
desc =>
|
||||
<<"Node's name: If you do not fill in the fields, this node will be used by default.">>
|
||||
}
|
||||
)}
|
||||
],
|
||||
responses => #{
|
||||
200 => lists:map(fun({_, Schema}) -> Schema end, config_list())
|
||||
}
|
||||
|
@ -94,18 +105,29 @@ schema("/configs_reset/:rootname") ->
|
|||
post => #{
|
||||
tags => [conf],
|
||||
description =>
|
||||
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>
|
||||
- For a config entry that has default value, this resets it to the default value;
|
||||
- For a config entry that has no default value, an error 400 will be returned">>,
|
||||
<<"Reset the config entry specified by the query string parameter `conf_path`.<br/>\n"
|
||||
"- For a config entry that has default value, this resets it to the default value;\n"
|
||||
"- For a config entry that has no default value, an error 400 will be returned">>,
|
||||
%% We only return "200" rather than the new configs that has been changed, as
|
||||
%% the schema of the changed configs is depends on the request parameter
|
||||
%% `conf_path`, it cannot be defined here.
|
||||
parameters => [
|
||||
{rootname, hoconsc:mk( hoconsc:enum(Paths)
|
||||
, #{in => path, example => <<"sysmon">>})},
|
||||
{conf_path, hoconsc:mk(typerefl:binary(),
|
||||
#{in => query, required => false, example => <<"os.sysmem_high_watermark">>,
|
||||
desc => <<"The config path separated by '.' character">>})}],
|
||||
{rootname,
|
||||
hoconsc:mk(
|
||||
hoconsc:enum(Paths),
|
||||
#{in => path, example => <<"sysmon">>}
|
||||
)},
|
||||
{conf_path,
|
||||
hoconsc:mk(
|
||||
typerefl:binary(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
example => <<"os.sysmem_high_watermark">>,
|
||||
desc => <<"The config path separated by '.' character">>
|
||||
}
|
||||
)}
|
||||
],
|
||||
responses => #{
|
||||
200 => <<"Rest config successfully">>,
|
||||
400 => emqx_dashboard_swagger:error_codes(['NO_DEFAULT_VALUE', 'REST_FAILED'])
|
||||
|
@ -137,9 +159,11 @@ schema(Path) ->
|
|||
'operationId' => config,
|
||||
get => #{
|
||||
tags => [conf],
|
||||
description => iolist_to_binary([ <<"Get the sub-configurations under *">>
|
||||
, RootKey
|
||||
, <<"*">>]),
|
||||
description => iolist_to_binary([
|
||||
<<"Get the sub-configurations under *">>,
|
||||
RootKey,
|
||||
<<"*">>
|
||||
]),
|
||||
responses => #{
|
||||
200 => Schema,
|
||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"config not found">>)
|
||||
|
@ -147,9 +171,11 @@ schema(Path) ->
|
|||
},
|
||||
put => #{
|
||||
tags => [conf],
|
||||
description => iolist_to_binary([ <<"Update the sub-configurations under *">>
|
||||
, RootKey
|
||||
, <<"*">>]),
|
||||
description => iolist_to_binary([
|
||||
<<"Update the sub-configurations under *">>,
|
||||
RootKey,
|
||||
<<"*">>
|
||||
]),
|
||||
'requestBody' => Schema,
|
||||
responses => #{
|
||||
200 => Schema,
|
||||
|
@ -176,7 +202,6 @@ config(get, _Params, Req) ->
|
|||
Path = conf_path(Req),
|
||||
{ok, Conf} = emqx_map_lib:deep_find(Path, get_full_config()),
|
||||
{200, Conf};
|
||||
|
||||
config(put, #{body := Body}, Req) ->
|
||||
Path = conf_path(Req),
|
||||
case emqx_conf:update(Path, Body, ?OPTS) of
|
||||
|
@ -188,21 +213,32 @@ config(put, #{body := Body}, Req) ->
|
|||
|
||||
global_zone_configs(get, _Params, _Req) ->
|
||||
Paths = global_zone_roots(),
|
||||
Zones = lists:foldl(fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
||||
#{}, Paths),
|
||||
Zones = lists:foldl(
|
||||
fun(Path, Acc) -> Acc#{Path => get_config_with_default([Path])} end,
|
||||
#{},
|
||||
Paths
|
||||
),
|
||||
{200, Zones};
|
||||
global_zone_configs(put, #{body := Body}, _Req) ->
|
||||
Res =
|
||||
maps:fold(fun(Path, Value, Acc) ->
|
||||
case emqx_conf:update([Path], Value, ?OPTS) of
|
||||
{ok, #{raw_config := RawConf}} ->
|
||||
Acc#{Path => RawConf};
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "update global zone failed", reason => Reason,
|
||||
path => Path, value => Value}),
|
||||
Acc
|
||||
end
|
||||
end, #{}, Body),
|
||||
maps:fold(
|
||||
fun(Path, Value, Acc) ->
|
||||
case emqx_conf:update([Path], Value, ?OPTS) of
|
||||
{ok, #{raw_config := RawConf}} ->
|
||||
Acc#{Path => RawConf};
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{
|
||||
msg => "update global zone failed",
|
||||
reason => Reason,
|
||||
path => Path,
|
||||
value => Value
|
||||
}),
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
#{},
|
||||
Body
|
||||
),
|
||||
case maps:size(Res) =:= maps:size(Body) of
|
||||
true -> {200, Res};
|
||||
false -> {400, #{code => 'UPDATE_FAILED'}}
|
||||
|
@ -212,7 +248,8 @@ config_reset(post, _Params, Req) ->
|
|||
%% reset the config specified by the query string param 'conf_path'
|
||||
Path = conf_path_reset(Req) ++ conf_path_from_querystr(Req),
|
||||
case emqx:reset_config(Path, #{}) of
|
||||
{ok, _} -> {200};
|
||||
{ok, _} ->
|
||||
{200};
|
||||
{error, no_default_value} ->
|
||||
{400, #{code => 'NO_DEFAULT_VALUE', message => <<"No Default Value.">>}};
|
||||
{error, Reason} ->
|
||||
|
@ -222,9 +259,8 @@ config_reset(post, _Params, Req) ->
|
|||
configs(get, Params, _Req) ->
|
||||
Node = maps:get(node, Params, node()),
|
||||
case
|
||||
lists:member(Node, mria_mnesia:running_nodes())
|
||||
andalso
|
||||
emqx_management_proto_v1:get_full_config(Node)
|
||||
lists:member(Node, mria_mnesia:running_nodes()) andalso
|
||||
emqx_management_proto_v1:get_full_config(Node)
|
||||
of
|
||||
false ->
|
||||
Message = list_to_binary(io_lib:format("Bad node ~p, reason not found", [Node])),
|
||||
|
@ -242,8 +278,11 @@ conf_path_reset(Req) ->
|
|||
|
||||
get_full_config() ->
|
||||
emqx_config:fill_defaults(
|
||||
maps:without(?EXCLUDES,
|
||||
emqx:get_raw_config([]))).
|
||||
maps:without(
|
||||
?EXCLUDES,
|
||||
emqx:get_raw_config([])
|
||||
)
|
||||
).
|
||||
|
||||
get_config_with_default(Path) ->
|
||||
emqx_config:fill_defaults(emqx:get_raw_config(Path)).
|
||||
|
@ -278,8 +317,11 @@ gen_schema(Conf) when is_list(Conf) ->
|
|||
#{type => array, items => gen_schema(hd(Conf))}
|
||||
end;
|
||||
gen_schema(Conf) when is_map(Conf) ->
|
||||
#{type => object, properties =>
|
||||
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)};
|
||||
#{
|
||||
type => object,
|
||||
properties =>
|
||||
maps:map(fun(_K, V) -> gen_schema(V) end, Conf)
|
||||
};
|
||||
gen_schema(_Conf) ->
|
||||
%% the conf is not of JSON supported type, it may have been converted
|
||||
%% by the hocon schema
|
||||
|
|
|
@ -23,14 +23,16 @@
|
|||
-import(hoconsc, [mk/2, ref/2]).
|
||||
|
||||
%% minirest/dashbaord_swagger behaviour callbacks
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1
|
||||
]).
|
||||
|
||||
-export([ roots/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
roots/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
%% http handlers
|
||||
-export([metrics/2]).
|
||||
|
@ -53,9 +55,12 @@ metrics(get, #{query_string := Qs}) ->
|
|||
true ->
|
||||
{200, emqx_mgmt:get_metrics()};
|
||||
false ->
|
||||
Data = [maps:from_list(
|
||||
emqx_mgmt:get_metrics(Node) ++ [{node, Node}])
|
||||
|| Node <- mria_mnesia:running_nodes()],
|
||||
Data = [
|
||||
maps:from_list(
|
||||
emqx_mgmt:get_metrics(Node) ++ [{node, Node}]
|
||||
)
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
],
|
||||
{200, Data}
|
||||
end.
|
||||
|
||||
|
@ -64,23 +69,34 @@ metrics(get, #{query_string := Qs}) ->
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
schema("/metrics") ->
|
||||
#{ 'operationId' => metrics
|
||||
, get =>
|
||||
#{ description => <<"EMQX metrics">>
|
||||
, parameters =>
|
||||
[{ aggregate
|
||||
, mk( boolean()
|
||||
, #{ in => query
|
||||
, required => false
|
||||
, desc => <<"Whether to aggregate all nodes Metrics">>})
|
||||
}]
|
||||
, responses =>
|
||||
#{ 200 => hoconsc:union(
|
||||
[ref(?MODULE, aggregated_metrics),
|
||||
hoconsc:array(ref(?MODULE, node_metrics))])
|
||||
}
|
||||
#{
|
||||
'operationId' => metrics,
|
||||
get =>
|
||||
#{
|
||||
description => <<"EMQX metrics">>,
|
||||
parameters =>
|
||||
[
|
||||
{aggregate,
|
||||
mk(
|
||||
boolean(),
|
||||
#{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Whether to aggregate all nodes Metrics">>
|
||||
}
|
||||
)}
|
||||
],
|
||||
responses =>
|
||||
#{
|
||||
200 => hoconsc:union(
|
||||
[
|
||||
ref(?MODULE, aggregated_metrics),
|
||||
hoconsc:array(ref(?MODULE, node_metrics))
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}.
|
||||
}.
|
||||
|
||||
roots() ->
|
||||
[].
|
||||
|
@ -91,176 +107,353 @@ fields(node_metrics) ->
|
|||
[{node, mk(binary(), #{desc => <<"Node name">>})}] ++ properties().
|
||||
|
||||
properties() ->
|
||||
[ m('actions.failure',
|
||||
<<"Number of failure executions of the rule engine action">>)
|
||||
, m('actions.success',
|
||||
<<"Number of successful executions of the rule engine action">>)
|
||||
, m('bytes.received',
|
||||
<<"Number of bytes received ">>)
|
||||
, m('bytes.sent',
|
||||
<<"Number of bytes sent on this connection">>)
|
||||
, m('client.auth.anonymous',
|
||||
<<"Number of clients who log in anonymously">>)
|
||||
, m('client.authenticate',
|
||||
<<"Number of client authentications">>)
|
||||
, m('client.check_authz',
|
||||
<<"Number of Authorization rule checks">>)
|
||||
, m('client.connack',
|
||||
<<"Number of CONNACK packet sent">>)
|
||||
, m('client.connect',
|
||||
<<"Number of client connections">>)
|
||||
, m('client.connected',
|
||||
<<"Number of successful client connections">>)
|
||||
, m('client.disconnected',
|
||||
<<"Number of client disconnects">>)
|
||||
, m('client.subscribe',
|
||||
<<"Number of client subscriptions">>)
|
||||
, m('client.unsubscribe',
|
||||
<<"Number of client unsubscriptions">>)
|
||||
, m('delivery.dropped',
|
||||
<<"Total number of discarded messages when sending">>)
|
||||
, m('delivery.dropped.expired',
|
||||
<<"Number of messages dropped due to message expiration on sending">>)
|
||||
, m('delivery.dropped.no_local',
|
||||
<<"Number of messages that were dropped due to the No Local subscription "
|
||||
"option when sending">>)
|
||||
, m('delivery.dropped.qos0_msg',
|
||||
<<"Number of messages with QoS 0 that were dropped because the message "
|
||||
"queue was full when sending">>)
|
||||
, m('delivery.dropped.queue_full',
|
||||
<<"Number of messages with a non-zero QoS that were dropped because the "
|
||||
"message queue was full when sending">>)
|
||||
, m('delivery.dropped.too_large',
|
||||
<<"The number of messages that were dropped because the length exceeded "
|
||||
"the limit when sending">>)
|
||||
, m('messages.acked',
|
||||
<<"Number of received PUBACK and PUBREC packet">>)
|
||||
, m('messages.delayed',
|
||||
<<"Number of delay-published messages">>)
|
||||
, m('messages.delivered',
|
||||
<<"Number of messages forwarded to the subscription process internally">>)
|
||||
, m('messages.dropped',
|
||||
<<"Total number of messages dropped before forwarding to the subscription process">>)
|
||||
, m('messages.dropped.await_pubrel_timeout',
|
||||
<<"Number of messages dropped due to waiting PUBREL timeout">>)
|
||||
, m('messages.dropped.no_subscribers',
|
||||
<<"Number of messages dropped due to no subscribers">>)
|
||||
, m('messages.forward',
|
||||
<<"Number of messages forwarded to other nodes">>)
|
||||
, m('messages.publish',
|
||||
<<"Number of messages published in addition to system messages">>)
|
||||
, m('messages.qos0.received',
|
||||
<<"Number of QoS 0 messages received from clients">>)
|
||||
, m('messages.qos0.sent',
|
||||
<<"Number of QoS 0 messages sent to clients">>)
|
||||
, m('messages.qos1.received',
|
||||
<<"Number of QoS 1 messages received from clients">>)
|
||||
, m('messages.qos1.sent',
|
||||
<<"Number of QoS 1 messages sent to clients">>)
|
||||
, m('messages.qos2.received',
|
||||
<<"Number of QoS 2 messages received from clients">>)
|
||||
, m('messages.qos2.sent',
|
||||
<<"Number of QoS 2 messages sent to clients">>)
|
||||
, m('messages.received',
|
||||
<<"Number of messages received from the client, equal to the sum of "
|
||||
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received">>)
|
||||
, m('messages.retained',
|
||||
<<"Number of retained messages">>)
|
||||
, m('messages.sent',
|
||||
<<"Number of messages sent to the client, equal to the sum of "
|
||||
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent">>)
|
||||
, m('packets.auth.received',
|
||||
<<"Number of received AUTH packet">>)
|
||||
, m('packets.auth.sent',
|
||||
<<"Number of sent AUTH packet">>)
|
||||
, m('packets.connack.auth_error',
|
||||
<<"Number of received CONNECT packet with failed authentication">>)
|
||||
, m('packets.connack.error',
|
||||
<<"Number of received CONNECT packet with unsuccessful connections">>)
|
||||
, m('packets.connack.sent',
|
||||
<<"Number of sent CONNACK packet">>)
|
||||
, m('packets.connect.received',
|
||||
<<"Number of received CONNECT packet">>)
|
||||
, m('packets.disconnect.received',
|
||||
<<"Number of received DISCONNECT packet">>)
|
||||
, m('packets.disconnect.sent',
|
||||
<<"Number of sent DISCONNECT packet">>)
|
||||
, m('packets.pingreq.received',
|
||||
<<"Number of received PINGREQ packet">>)
|
||||
, m('packets.pingresp.sent',
|
||||
<<"Number of sent PUBRESP packet">>)
|
||||
, m('packets.puback.inuse',
|
||||
<<"Number of received PUBACK packet with occupied identifiers">>)
|
||||
, m('packets.puback.missed',
|
||||
<<"Number of received packet with identifiers.">>)
|
||||
, m('packets.puback.received',
|
||||
<<"Number of received PUBACK packet">>)
|
||||
, m('packets.puback.sent',
|
||||
<<"Number of sent PUBACK packet">>)
|
||||
, m('packets.pubcomp.inuse',
|
||||
<<"Number of received PUBCOMP packet with occupied identifiers">>)
|
||||
, m('packets.pubcomp.missed',
|
||||
<<"Number of missed PUBCOMP packet">>)
|
||||
, m('packets.pubcomp.received',
|
||||
<<"Number of received PUBCOMP packet">>)
|
||||
, m('packets.pubcomp.sent',
|
||||
<<"Number of sent PUBCOMP packet">>)
|
||||
, m('packets.publish.auth_error',
|
||||
<<"Number of received PUBLISH packets with failed the Authorization check">>)
|
||||
, m('packets.publish.dropped',
|
||||
<<"Number of messages discarded due to the receiving limit">>)
|
||||
, m('packets.publish.error',
|
||||
<<"Number of received PUBLISH packet that cannot be published">>)
|
||||
, m('packets.publish.inuse',
|
||||
<<"Number of received PUBLISH packet with occupied identifiers">>)
|
||||
, m('packets.publish.received',
|
||||
<<"Number of received PUBLISH packet">>)
|
||||
, m('packets.publish.sent',
|
||||
<<"Number of sent PUBLISH packet">>)
|
||||
, m('packets.pubrec.inuse',
|
||||
<<"Number of received PUBREC packet with occupied identifiers">>)
|
||||
, m('packets.pubrec.missed',
|
||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
||||
, m('packets.pubrec.received',
|
||||
<<"Number of received PUBREC packet">>)
|
||||
, m('packets.pubrec.sent',
|
||||
<<"Number of sent PUBREC packet">>)
|
||||
, m('packets.pubrel.missed',
|
||||
<<"Number of received PUBREC packet with unknown identifiers">>)
|
||||
, m('packets.pubrel.received',
|
||||
<<"Number of received PUBREL packet">>)
|
||||
, m('packets.pubrel.sent',
|
||||
<<"Number of sent PUBREL packet">>)
|
||||
, m('packets.received',
|
||||
<<"Number of received packet">>)
|
||||
, m('packets.sent',
|
||||
<<"Number of sent packet">>)
|
||||
, m('packets.suback.sent',
|
||||
<<"Number of sent SUBACK packet">>)
|
||||
, m('packets.subscribe.auth_error',
|
||||
<<"Number of received SUBACK packet with failed Authorization check">>)
|
||||
, m('packets.subscribe.error',
|
||||
<<"Number of received SUBSCRIBE packet with failed subscriptions">>)
|
||||
, m('packets.subscribe.received',
|
||||
<<"Number of received SUBSCRIBE packet">>)
|
||||
, m('packets.unsuback.sent',
|
||||
<<"Number of sent UNSUBACK packet">>)
|
||||
, m('packets.unsubscribe.error',
|
||||
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>)
|
||||
, m('packets.unsubscribe.received',
|
||||
<<"Number of received UNSUBSCRIBE packet">>)
|
||||
, m('rules.matched',
|
||||
<<"Number of rule matched">>)
|
||||
, m('session.created',
|
||||
<<"Number of sessions created">>)
|
||||
, m('session.discarded',
|
||||
<<"Number of sessions dropped because Clean Session or Clean Start is true">>)
|
||||
, m('session.resumed',
|
||||
<<"Number of sessions resumed because Clean Session or Clean Start is false">>)
|
||||
, m('session.takenover',
|
||||
<<"Number of sessions takenover because Clean Session or Clean Start is false">>)
|
||||
, m('session.terminated',
|
||||
<<"Number of terminated sessions">>)
|
||||
[
|
||||
m(
|
||||
'actions.failure',
|
||||
<<"Number of failure executions of the rule engine action">>
|
||||
),
|
||||
m(
|
||||
'actions.success',
|
||||
<<"Number of successful executions of the rule engine action">>
|
||||
),
|
||||
m(
|
||||
'bytes.received',
|
||||
<<"Number of bytes received ">>
|
||||
),
|
||||
m(
|
||||
'bytes.sent',
|
||||
<<"Number of bytes sent on this connection">>
|
||||
),
|
||||
m(
|
||||
'client.auth.anonymous',
|
||||
<<"Number of clients who log in anonymously">>
|
||||
),
|
||||
m(
|
||||
'client.authenticate',
|
||||
<<"Number of client authentications">>
|
||||
),
|
||||
m(
|
||||
'client.check_authz',
|
||||
<<"Number of Authorization rule checks">>
|
||||
),
|
||||
m(
|
||||
'client.connack',
|
||||
<<"Number of CONNACK packet sent">>
|
||||
),
|
||||
m(
|
||||
'client.connect',
|
||||
<<"Number of client connections">>
|
||||
),
|
||||
m(
|
||||
'client.connected',
|
||||
<<"Number of successful client connections">>
|
||||
),
|
||||
m(
|
||||
'client.disconnected',
|
||||
<<"Number of client disconnects">>
|
||||
),
|
||||
m(
|
||||
'client.subscribe',
|
||||
<<"Number of client subscriptions">>
|
||||
),
|
||||
m(
|
||||
'client.unsubscribe',
|
||||
<<"Number of client unsubscriptions">>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped',
|
||||
<<"Total number of discarded messages when sending">>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped.expired',
|
||||
<<"Number of messages dropped due to message expiration on sending">>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped.no_local',
|
||||
<<
|
||||
"Number of messages that were dropped due to the No Local subscription "
|
||||
"option when sending"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped.qos0_msg',
|
||||
<<
|
||||
"Number of messages with QoS 0 that were dropped because the message "
|
||||
"queue was full when sending"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped.queue_full',
|
||||
<<
|
||||
"Number of messages with a non-zero QoS that were dropped because the "
|
||||
"message queue was full when sending"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'delivery.dropped.too_large',
|
||||
<<
|
||||
"The number of messages that were dropped because the length exceeded "
|
||||
"the limit when sending"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'messages.acked',
|
||||
<<"Number of received PUBACK and PUBREC packet">>
|
||||
),
|
||||
m(
|
||||
'messages.delayed',
|
||||
<<"Number of delay-published messages">>
|
||||
),
|
||||
m(
|
||||
'messages.delivered',
|
||||
<<"Number of messages forwarded to the subscription process internally">>
|
||||
),
|
||||
m(
|
||||
'messages.dropped',
|
||||
<<"Total number of messages dropped before forwarding to the subscription process">>
|
||||
),
|
||||
m(
|
||||
'messages.dropped.await_pubrel_timeout',
|
||||
<<"Number of messages dropped due to waiting PUBREL timeout">>
|
||||
),
|
||||
m(
|
||||
'messages.dropped.no_subscribers',
|
||||
<<"Number of messages dropped due to no subscribers">>
|
||||
),
|
||||
m(
|
||||
'messages.forward',
|
||||
<<"Number of messages forwarded to other nodes">>
|
||||
),
|
||||
m(
|
||||
'messages.publish',
|
||||
<<"Number of messages published in addition to system messages">>
|
||||
),
|
||||
m(
|
||||
'messages.qos0.received',
|
||||
<<"Number of QoS 0 messages received from clients">>
|
||||
),
|
||||
m(
|
||||
'messages.qos0.sent',
|
||||
<<"Number of QoS 0 messages sent to clients">>
|
||||
),
|
||||
m(
|
||||
'messages.qos1.received',
|
||||
<<"Number of QoS 1 messages received from clients">>
|
||||
),
|
||||
m(
|
||||
'messages.qos1.sent',
|
||||
<<"Number of QoS 1 messages sent to clients">>
|
||||
),
|
||||
m(
|
||||
'messages.qos2.received',
|
||||
<<"Number of QoS 2 messages received from clients">>
|
||||
),
|
||||
m(
|
||||
'messages.qos2.sent',
|
||||
<<"Number of QoS 2 messages sent to clients">>
|
||||
),
|
||||
m(
|
||||
'messages.received',
|
||||
<<
|
||||
"Number of messages received from the client, equal to the sum of "
|
||||
"messages.qos0.received\fmessages.qos1.received and messages.qos2.received"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'messages.retained',
|
||||
<<"Number of retained messages">>
|
||||
),
|
||||
m(
|
||||
'messages.sent',
|
||||
<<
|
||||
"Number of messages sent to the client, equal to the sum of "
|
||||
"messages.qos0.sent\fmessages.qos1.sent and messages.qos2.sent"
|
||||
>>
|
||||
),
|
||||
m(
|
||||
'packets.auth.received',
|
||||
<<"Number of received AUTH packet">>
|
||||
),
|
||||
m(
|
||||
'packets.auth.sent',
|
||||
<<"Number of sent AUTH packet">>
|
||||
),
|
||||
m(
|
||||
'packets.connack.auth_error',
|
||||
<<"Number of received CONNECT packet with failed authentication">>
|
||||
),
|
||||
m(
|
||||
'packets.connack.error',
|
||||
<<"Number of received CONNECT packet with unsuccessful connections">>
|
||||
),
|
||||
m(
|
||||
'packets.connack.sent',
|
||||
<<"Number of sent CONNACK packet">>
|
||||
),
|
||||
m(
|
||||
'packets.connect.received',
|
||||
<<"Number of received CONNECT packet">>
|
||||
),
|
||||
m(
|
||||
'packets.disconnect.received',
|
||||
<<"Number of received DISCONNECT packet">>
|
||||
),
|
||||
m(
|
||||
'packets.disconnect.sent',
|
||||
<<"Number of sent DISCONNECT packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pingreq.received',
|
||||
<<"Number of received PINGREQ packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pingresp.sent',
|
||||
<<"Number of sent PUBRESP packet">>
|
||||
),
|
||||
m(
|
||||
'packets.puback.inuse',
|
||||
<<"Number of received PUBACK packet with occupied identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.puback.missed',
|
||||
<<"Number of received packet with identifiers.">>
|
||||
),
|
||||
m(
|
||||
'packets.puback.received',
|
||||
<<"Number of received PUBACK packet">>
|
||||
),
|
||||
m(
|
||||
'packets.puback.sent',
|
||||
<<"Number of sent PUBACK packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubcomp.inuse',
|
||||
<<"Number of received PUBCOMP packet with occupied identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.pubcomp.missed',
|
||||
<<"Number of missed PUBCOMP packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubcomp.received',
|
||||
<<"Number of received PUBCOMP packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubcomp.sent',
|
||||
<<"Number of sent PUBCOMP packet">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.auth_error',
|
||||
<<"Number of received PUBLISH packets with failed the Authorization check">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.dropped',
|
||||
<<"Number of messages discarded due to the receiving limit">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.error',
|
||||
<<"Number of received PUBLISH packet that cannot be published">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.inuse',
|
||||
<<"Number of received PUBLISH packet with occupied identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.received',
|
||||
<<"Number of received PUBLISH packet">>
|
||||
),
|
||||
m(
|
||||
'packets.publish.sent',
|
||||
<<"Number of sent PUBLISH packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrec.inuse',
|
||||
<<"Number of received PUBREC packet with occupied identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrec.missed',
|
||||
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrec.received',
|
||||
<<"Number of received PUBREC packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrec.sent',
|
||||
<<"Number of sent PUBREC packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrel.missed',
|
||||
<<"Number of received PUBREC packet with unknown identifiers">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrel.received',
|
||||
<<"Number of received PUBREL packet">>
|
||||
),
|
||||
m(
|
||||
'packets.pubrel.sent',
|
||||
<<"Number of sent PUBREL packet">>
|
||||
),
|
||||
m(
|
||||
'packets.received',
|
||||
<<"Number of received packet">>
|
||||
),
|
||||
m(
|
||||
'packets.sent',
|
||||
<<"Number of sent packet">>
|
||||
),
|
||||
m(
|
||||
'packets.suback.sent',
|
||||
<<"Number of sent SUBACK packet">>
|
||||
),
|
||||
m(
|
||||
'packets.subscribe.auth_error',
|
||||
<<"Number of received SUBACK packet with failed Authorization check">>
|
||||
),
|
||||
m(
|
||||
'packets.subscribe.error',
|
||||
<<"Number of received SUBSCRIBE packet with failed subscriptions">>
|
||||
),
|
||||
m(
|
||||
'packets.subscribe.received',
|
||||
<<"Number of received SUBSCRIBE packet">>
|
||||
),
|
||||
m(
|
||||
'packets.unsuback.sent',
|
||||
<<"Number of sent UNSUBACK packet">>
|
||||
),
|
||||
m(
|
||||
'packets.unsubscribe.error',
|
||||
<<"Number of received UNSUBSCRIBE packet with failed unsubscriptions">>
|
||||
),
|
||||
m(
|
||||
'packets.unsubscribe.received',
|
||||
<<"Number of received UNSUBSCRIBE packet">>
|
||||
),
|
||||
m(
|
||||
'rules.matched',
|
||||
<<"Number of rule matched">>
|
||||
),
|
||||
m(
|
||||
'session.created',
|
||||
<<"Number of sessions created">>
|
||||
),
|
||||
m(
|
||||
'session.discarded',
|
||||
<<"Number of sessions dropped because Clean Session or Clean Start is true">>
|
||||
),
|
||||
m(
|
||||
'session.resumed',
|
||||
<<"Number of sessions resumed because Clean Session or Clean Start is false">>
|
||||
),
|
||||
m(
|
||||
'session.takenover',
|
||||
<<"Number of sessions takenover because Clean Session or Clean Start is false">>
|
||||
),
|
||||
m(
|
||||
'session.terminated',
|
||||
<<"Number of terminated sessions">>
|
||||
)
|
||||
].
|
||||
|
||||
m(K, Desc) ->
|
||||
|
|
|
@ -28,18 +28,20 @@
|
|||
-define(SOURCE_ERROR, 'SOURCE_ERROR').
|
||||
|
||||
%% Swagger specs from hocon schema
|
||||
-export([ api_spec/0
|
||||
, schema/1
|
||||
, paths/0
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
schema/1,
|
||||
paths/0,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
%% API callbacks
|
||||
-export([ nodes/2
|
||||
, node/2
|
||||
, node_metrics/2
|
||||
, node_stats/2
|
||||
]).
|
||||
-export([
|
||||
nodes/2,
|
||||
node/2,
|
||||
node_metrics/2,
|
||||
node_stats/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API spec funcs
|
||||
|
@ -49,123 +51,183 @@ api_spec() ->
|
|||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
||||
|
||||
paths() ->
|
||||
[ "/nodes"
|
||||
, "/nodes/:node"
|
||||
, "/nodes/:node/metrics"
|
||||
, "/nodes/:node/stats"
|
||||
[
|
||||
"/nodes",
|
||||
"/nodes/:node",
|
||||
"/nodes/:node/metrics",
|
||||
"/nodes/:node/stats"
|
||||
].
|
||||
|
||||
schema("/nodes") ->
|
||||
#{ 'operationId' => nodes
|
||||
, get =>
|
||||
#{ description => <<"List EMQX nodes">>
|
||||
, responses =>
|
||||
#{200 => mk( array(ref(node_info))
|
||||
, #{desc => <<"List all EMQX nodes">>})}
|
||||
#{
|
||||
'operationId' => nodes,
|
||||
get =>
|
||||
#{
|
||||
description => <<"List EMQX nodes">>,
|
||||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
array(ref(node_info)),
|
||||
#{desc => <<"List all EMQX nodes">>}
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
schema("/nodes/:node") ->
|
||||
#{ 'operationId' => node
|
||||
, get =>
|
||||
#{ description => <<"Get node info">>
|
||||
, parameters => [ref(node_name)]
|
||||
, responses =>
|
||||
#{ 200 => mk( ref(node_info)
|
||||
, #{desc => <<"Get node info successfully">>})
|
||||
, 400 => node_error()
|
||||
}
|
||||
#{
|
||||
'operationId' => node,
|
||||
get =>
|
||||
#{
|
||||
description => <<"Get node info">>,
|
||||
parameters => [ref(node_name)],
|
||||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
ref(node_info),
|
||||
#{desc => <<"Get node info successfully">>}
|
||||
),
|
||||
400 => node_error()
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
schema("/nodes/:node/metrics") ->
|
||||
#{ 'operationId' => node_metrics
|
||||
, get =>
|
||||
#{ description => <<"Get node metrics">>
|
||||
, parameters => [ref(node_name)]
|
||||
, responses =>
|
||||
#{ 200 => mk( ref(?NODE_METRICS_MODULE, node_metrics)
|
||||
, #{desc => <<"Get node metrics successfully">>})
|
||||
, 400 => node_error()
|
||||
}
|
||||
#{
|
||||
'operationId' => node_metrics,
|
||||
get =>
|
||||
#{
|
||||
description => <<"Get node metrics">>,
|
||||
parameters => [ref(node_name)],
|
||||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
ref(?NODE_METRICS_MODULE, node_metrics),
|
||||
#{desc => <<"Get node metrics successfully">>}
|
||||
),
|
||||
400 => node_error()
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
schema("/nodes/:node/stats") ->
|
||||
#{ 'operationId' => node_stats
|
||||
, get =>
|
||||
#{ description => <<"Get node stats">>
|
||||
, parameters => [ref(node_name)]
|
||||
, responses =>
|
||||
#{ 200 => mk( ref(?NODE_STATS_MODULE, node_stats_data)
|
||||
, #{desc => <<"Get node stats successfully">>})
|
||||
, 400 => node_error()
|
||||
}
|
||||
#{
|
||||
'operationId' => node_stats,
|
||||
get =>
|
||||
#{
|
||||
description => <<"Get node stats">>,
|
||||
parameters => [ref(node_name)],
|
||||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
ref(?NODE_STATS_MODULE, node_stats_data),
|
||||
#{desc => <<"Get node stats successfully">>}
|
||||
),
|
||||
400 => node_error()
|
||||
}
|
||||
}
|
||||
}.
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Fields
|
||||
|
||||
fields(node_name) ->
|
||||
[ { node
|
||||
, mk(atom()
|
||||
, #{ in => path
|
||||
, description => <<"Node name">>
|
||||
, required => true
|
||||
, example => <<"emqx@127.0.0.1">>
|
||||
})
|
||||
}
|
||||
[
|
||||
{node,
|
||||
mk(
|
||||
atom(),
|
||||
#{
|
||||
in => path,
|
||||
description => <<"Node name">>,
|
||||
required => true,
|
||||
example => <<"emqx@127.0.0.1">>
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(node_info) ->
|
||||
[ { node
|
||||
, mk( atom()
|
||||
, #{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>})}
|
||||
, { connections
|
||||
, mk( non_neg_integer()
|
||||
, #{desc => <<"Number of clients currently connected to this node">>, example => 0})}
|
||||
, { load1
|
||||
, mk( string()
|
||||
, #{desc => <<"CPU average load in 1 minute">>, example => "2.66"})}
|
||||
, { load5
|
||||
, mk( string()
|
||||
, #{desc => <<"CPU average load in 5 minute">>, example => "2.66"})}
|
||||
, { load15
|
||||
, mk( string()
|
||||
, #{desc => <<"CPU average load in 15 minute">>, example => "2.66"})}
|
||||
, { max_fds
|
||||
, mk( non_neg_integer()
|
||||
, #{desc => <<"File descriptors limit">>, example => 1024})}
|
||||
, { memory_total
|
||||
, mk( emqx_schema:bytesize()
|
||||
, #{desc => <<"Allocated memory">>, example => "512.00M"})}
|
||||
, { memory_used
|
||||
, mk( emqx_schema:bytesize()
|
||||
, #{desc => <<"Used memory">>, example => "256.00M"})}
|
||||
, { node_status
|
||||
, mk( enum(['Running', 'Stopped'])
|
||||
, #{desc => <<"Node status">>, example => "Running"})}
|
||||
, { otp_release
|
||||
, mk( string()
|
||||
, #{ desc => <<"Erlang/OTP version">>, example => "24.2/12.2"})}
|
||||
, { process_available
|
||||
, mk( non_neg_integer()
|
||||
, #{desc => <<"Erlang processes limit">>, example => 2097152})}
|
||||
, { process_used
|
||||
, mk( non_neg_integer()
|
||||
, #{desc => <<"Running Erlang processes">>, example => 1024})}
|
||||
, { uptime
|
||||
, mk( non_neg_integer()
|
||||
, #{desc => <<"System uptime, milliseconds">>, example => 5120000})}
|
||||
, { version
|
||||
, mk( string()
|
||||
, #{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"})}
|
||||
, { sys_path
|
||||
, mk( string()
|
||||
, #{desc => <<"Path to system files">>, example => "path/to/emqx"})}
|
||||
, { log_path
|
||||
, mk( string()
|
||||
, #{desc => <<"Path to log files">>, example => "path/to/log | not found"})}
|
||||
, { role
|
||||
, mk( enum([core, replicant])
|
||||
, #{desc => <<"Node role">>, example => "core"})}
|
||||
[
|
||||
{node,
|
||||
mk(
|
||||
atom(),
|
||||
#{desc => <<"Node name">>, example => <<"emqx@127.0.0.1">>}
|
||||
)},
|
||||
{connections,
|
||||
mk(
|
||||
non_neg_integer(),
|
||||
#{desc => <<"Number of clients currently connected to this node">>, example => 0}
|
||||
)},
|
||||
{load1,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"CPU average load in 1 minute">>, example => "2.66"}
|
||||
)},
|
||||
{load5,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"CPU average load in 5 minute">>, example => "2.66"}
|
||||
)},
|
||||
{load15,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"CPU average load in 15 minute">>, example => "2.66"}
|
||||
)},
|
||||
{max_fds,
|
||||
mk(
|
||||
non_neg_integer(),
|
||||
#{desc => <<"File descriptors limit">>, example => 1024}
|
||||
)},
|
||||
{memory_total,
|
||||
mk(
|
||||
emqx_schema:bytesize(),
|
||||
#{desc => <<"Allocated memory">>, example => "512.00M"}
|
||||
)},
|
||||
{memory_used,
|
||||
mk(
|
||||
emqx_schema:bytesize(),
|
||||
#{desc => <<"Used memory">>, example => "256.00M"}
|
||||
)},
|
||||
{node_status,
|
||||
mk(
|
||||
enum(['Running', 'Stopped']),
|
||||
#{desc => <<"Node status">>, example => "Running"}
|
||||
)},
|
||||
{otp_release,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"Erlang/OTP version">>, example => "24.2/12.2"}
|
||||
)},
|
||||
{process_available,
|
||||
mk(
|
||||
non_neg_integer(),
|
||||
#{desc => <<"Erlang processes limit">>, example => 2097152}
|
||||
)},
|
||||
{process_used,
|
||||
mk(
|
||||
non_neg_integer(),
|
||||
#{desc => <<"Running Erlang processes">>, example => 1024}
|
||||
)},
|
||||
{uptime,
|
||||
mk(
|
||||
non_neg_integer(),
|
||||
#{desc => <<"System uptime, milliseconds">>, example => 5120000}
|
||||
)},
|
||||
{version,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"Release version">>, example => "5.0.0-beat.3-00000000"}
|
||||
)},
|
||||
{sys_path,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"Path to system files">>, example => "path/to/emqx"}
|
||||
)},
|
||||
{log_path,
|
||||
mk(
|
||||
string(),
|
||||
#{desc => <<"Path to log files">>, example => "path/to/log | not found"}
|
||||
)},
|
||||
{role,
|
||||
mk(
|
||||
enum([core, replicant]),
|
||||
#{desc => <<"Node role">>, example => "core"}
|
||||
)}
|
||||
].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -221,17 +283,20 @@ get_stats(Node) ->
|
|||
format(_Node, Info = #{memory_total := Total, memory_used := Used}) ->
|
||||
{ok, SysPathBinary} = file:get_cwd(),
|
||||
SysPath = list_to_binary(SysPathBinary),
|
||||
LogPath = case log_path() of
|
||||
undefined ->
|
||||
<<"not found">>;
|
||||
Path0 ->
|
||||
Path = list_to_binary(Path0),
|
||||
<<SysPath/binary, Path/binary>>
|
||||
end,
|
||||
Info#{ memory_total := emqx_mgmt_util:kmg(Total)
|
||||
, memory_used := emqx_mgmt_util:kmg(Used)
|
||||
, sys_path => SysPath
|
||||
, log_path => LogPath}.
|
||||
LogPath =
|
||||
case log_path() of
|
||||
undefined ->
|
||||
<<"not found">>;
|
||||
Path0 ->
|
||||
Path = list_to_binary(Path0),
|
||||
<<SysPath/binary, Path/binary>>
|
||||
end,
|
||||
Info#{
|
||||
memory_total := emqx_mgmt_util:kmg(Total),
|
||||
memory_used := emqx_mgmt_util:kmg(Used),
|
||||
sys_path => SysPath,
|
||||
log_path => LogPath
|
||||
}.
|
||||
|
||||
log_path() ->
|
||||
Configs = logger:get_handler_config(),
|
||||
|
|
|
@ -22,27 +22,30 @@
|
|||
-include_lib("emqx/include/logger.hrl").
|
||||
%%-include_lib("emqx_plugins/include/emqx_plugins.hrl").
|
||||
|
||||
-export([ api_spec/0
|
||||
, fields/1
|
||||
, paths/0
|
||||
, schema/1
|
||||
, namespace/0
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
fields/1,
|
||||
paths/0,
|
||||
schema/1,
|
||||
namespace/0
|
||||
]).
|
||||
|
||||
-export([ list_plugins/2
|
||||
, upload_install/2
|
||||
, plugin/2
|
||||
, update_plugin/2
|
||||
, update_boot_order/2
|
||||
]).
|
||||
-export([
|
||||
list_plugins/2,
|
||||
upload_install/2,
|
||||
plugin/2,
|
||||
update_plugin/2,
|
||||
update_boot_order/2
|
||||
]).
|
||||
|
||||
-export([ validate_name/1
|
||||
, get_plugins/0
|
||||
, install_package/2
|
||||
, delete_package/1
|
||||
, describe_package/1
|
||||
, ensure_action/2
|
||||
]).
|
||||
-export([
|
||||
validate_name/1,
|
||||
get_plugins/0,
|
||||
install_package/2,
|
||||
delete_package/1,
|
||||
describe_package/1,
|
||||
ensure_action/2
|
||||
]).
|
||||
|
||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_.]*$").
|
||||
|
||||
|
@ -65,9 +68,10 @@ schema("/plugins") ->
|
|||
#{
|
||||
'operationId' => list_plugins,
|
||||
get => #{
|
||||
description => "List all install plugins.<br>"
|
||||
"Plugins are launched in top-down order.<br>"
|
||||
"Using `POST /plugins/{name}/move` to change the boot order.",
|
||||
description =>
|
||||
"List all install plugins.<br>"
|
||||
"Plugins are launched in top-down order.<br>"
|
||||
"Using `POST /plugins/{name}/move` to change the boot order.",
|
||||
responses => #{
|
||||
200 => hoconsc:array(hoconsc:ref(plugin))
|
||||
}
|
||||
|
@ -77,20 +81,26 @@ schema("/plugins/install") ->
|
|||
#{
|
||||
'operationId' => upload_install,
|
||||
post => #{
|
||||
description => "Install a plugin(plugin-vsn.tar.gz)."
|
||||
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
||||
"to develop plugin.",
|
||||
description =>
|
||||
"Install a plugin(plugin-vsn.tar.gz)."
|
||||
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
|
||||
"to develop plugin.",
|
||||
'requestBody' => #{
|
||||
content => #{
|
||||
'multipart/form-data' => #{
|
||||
schema => #{
|
||||
type => object,
|
||||
properties => #{
|
||||
plugin => #{type => string, format => binary}}},
|
||||
encoding => #{plugin => #{'contentType' => 'application/gzip'}}}}},
|
||||
plugin => #{type => string, format => binary}
|
||||
}
|
||||
},
|
||||
encoding => #{plugin => #{'contentType' => 'application/gzip'}}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses => #{
|
||||
200 => <<"OK">>,
|
||||
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR','ALREADY_INSTALLED'])
|
||||
400 => emqx_dashboard_swagger:error_codes(['UNEXPECTED_ERROR', 'ALREADY_INSTALLED'])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -118,12 +128,14 @@ schema("/plugins/:name/:action") ->
|
|||
#{
|
||||
'operationId' => update_plugin,
|
||||
put => #{
|
||||
description => "start/stop a installed plugin.<br>"
|
||||
"- **start**: start the plugin.<br>"
|
||||
"- **stop**: stop the plugin.<br>",
|
||||
description =>
|
||||
"start/stop a installed plugin.<br>"
|
||||
"- **start**: start the plugin.<br>"
|
||||
"- **stop**: stop the plugin.<br>",
|
||||
parameters => [
|
||||
hoconsc:ref(name),
|
||||
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}],
|
||||
{action, hoconsc:mk(hoconsc:enum([start, stop]), #{desc => "Action", in => path})}
|
||||
],
|
||||
responses => #{
|
||||
200 => <<"OK">>,
|
||||
404 => emqx_dashboard_swagger:error_codes(['NOT_FOUND'], <<"Plugin Not Found">>)
|
||||
|
@ -143,57 +155,83 @@ schema("/plugins/:name/move") ->
|
|||
|
||||
fields(plugin) ->
|
||||
[
|
||||
{name, hoconsc:mk(binary(),
|
||||
#{
|
||||
desc => "Name-Vsn: without .tar.gz",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
required => true,
|
||||
example => "emqx_plugin_template-5.0-rc.1"})
|
||||
},
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Name-Vsn: without .tar.gz",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
required => true,
|
||||
example => "emqx_plugin_template-5.0-rc.1"
|
||||
}
|
||||
)},
|
||||
{author, hoconsc:mk(list(string()), #{example => [<<"EMQX Team">>]})},
|
||||
{builder, hoconsc:ref(?MODULE, builder)},
|
||||
{built_on_otp_release, hoconsc:mk(string(), #{example => "24"})},
|
||||
{compatibility, hoconsc:mk(map(), #{example => #{<<"emqx">> => <<"~>5.0">>}})},
|
||||
{git_commit_or_build_date, hoconsc:mk(string(), #{
|
||||
example => "2021-12-25",
|
||||
desc => "Last git commit date by `git log -1 --pretty=format:'%cd' "
|
||||
"--date=format:'%Y-%m-%d`.\n"
|
||||
" If the last commit date is not available, the build date will be presented."
|
||||
})},
|
||||
{git_commit_or_build_date,
|
||||
hoconsc:mk(string(), #{
|
||||
example => "2021-12-25",
|
||||
desc =>
|
||||
"Last git commit date by `git log -1 --pretty=format:'%cd' "
|
||||
"--date=format:'%Y-%m-%d`.\n"
|
||||
" If the last commit date is not available, the build date will be presented."
|
||||
})},
|
||||
{functionality, hoconsc:mk(hoconsc:array(string()), #{example => [<<"Demo">>]})},
|
||||
{git_ref, hoconsc:mk(string(), #{example => "ddab50fafeed6b1faea70fc9ffd8c700d7e26ec1"})},
|
||||
{metadata_vsn, hoconsc:mk(string(), #{example => "0.1.0"})},
|
||||
{rel_vsn, hoconsc:mk(binary(),
|
||||
#{desc => "Plugins release version",
|
||||
required => true,
|
||||
example => <<"5.0-rc.1">>})
|
||||
},
|
||||
{rel_apps, hoconsc:mk(hoconsc:array(binary()),
|
||||
#{desc => "Aplications in plugin.",
|
||||
required => true,
|
||||
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]})
|
||||
},
|
||||
{rel_vsn,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Plugins release version",
|
||||
required => true,
|
||||
example => <<"5.0-rc.1">>
|
||||
}
|
||||
)},
|
||||
{rel_apps,
|
||||
hoconsc:mk(
|
||||
hoconsc:array(binary()),
|
||||
#{
|
||||
desc => "Aplications in plugin.",
|
||||
required => true,
|
||||
example => [<<"emqx_plugin_template-5.0.0">>, <<"map_sets-1.1.0">>]
|
||||
}
|
||||
)},
|
||||
{repo, hoconsc:mk(string(), #{example => "https://github.com/emqx/emqx-plugin-template"})},
|
||||
{description, hoconsc:mk(binary(),
|
||||
#{desc => "Plugin description.",
|
||||
required => true,
|
||||
example => "This is an demo plugin description"})
|
||||
},
|
||||
{running_status, hoconsc:mk(hoconsc:array(hoconsc:ref(running_status)),
|
||||
#{required => true})},
|
||||
{readme, hoconsc:mk(binary(), #{
|
||||
example => "This is an demo plugin.",
|
||||
desc => "only return when `GET /plugins/{name}`.",
|
||||
required => false})}
|
||||
{description,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Plugin description.",
|
||||
required => true,
|
||||
example => "This is an demo plugin description"
|
||||
}
|
||||
)},
|
||||
{running_status,
|
||||
hoconsc:mk(
|
||||
hoconsc:array(hoconsc:ref(running_status)),
|
||||
#{required => true}
|
||||
)},
|
||||
{readme,
|
||||
hoconsc:mk(binary(), #{
|
||||
example => "This is an demo plugin.",
|
||||
desc => "only return when `GET /plugins/{name}`.",
|
||||
required => false
|
||||
})}
|
||||
];
|
||||
fields(name) ->
|
||||
[{name, hoconsc:mk(binary(),
|
||||
#{
|
||||
desc => list_to_binary(?NAME_RE),
|
||||
example => "emqx_plugin_template-5.0-rc.1",
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
})}
|
||||
[
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => list_to_binary(?NAME_RE),
|
||||
example => "emqx_plugin_template-5.0-rc.1",
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(builder) ->
|
||||
[
|
||||
|
@ -202,27 +240,38 @@ fields(builder) ->
|
|||
{website, hoconsc:mk(string(), #{example => "www.emqx.com"})}
|
||||
];
|
||||
fields(position) ->
|
||||
[{position, hoconsc:mk(hoconsc:union([front, rear, binary()]),
|
||||
#{
|
||||
desc => """
|
||||
Enable auto-boot at position in the boot list, where Position could be
|
||||
'front', 'rear', or 'before:other-vsn', 'after:other-vsn'
|
||||
to specify a relative position.
|
||||
""",
|
||||
required => false
|
||||
})}];
|
||||
[
|
||||
{position,
|
||||
hoconsc:mk(
|
||||
hoconsc:union([front, rear, binary()]),
|
||||
#{
|
||||
desc =>
|
||||
""
|
||||
"\n"
|
||||
" Enable auto-boot at position in the boot list, where Position could be\n"
|
||||
" 'front', 'rear', or 'before:other-vsn', 'after:other-vsn'\n"
|
||||
" to specify a relative position.\n"
|
||||
" "
|
||||
"",
|
||||
required => false
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(running_status) ->
|
||||
[
|
||||
{node, hoconsc:mk(string(), #{example => "emqx@127.0.0.1"})},
|
||||
{status, hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
||||
desc => "Install plugin status at runtime</br>"
|
||||
"1. running: plugin is running.<br>"
|
||||
"2. stopped: plugin is stopped.<br>"
|
||||
})}
|
||||
{status,
|
||||
hoconsc:mk(hoconsc:enum([running, stopped]), #{
|
||||
desc =>
|
||||
"Install plugin status at runtime</br>"
|
||||
"1. running: plugin is running.<br>"
|
||||
"2. stopped: plugin is stopped.<br>"
|
||||
})}
|
||||
].
|
||||
|
||||
move_request_body() ->
|
||||
emqx_dashboard_swagger:schema_with_examples(hoconsc:ref(?MODULE, position),
|
||||
emqx_dashboard_swagger:schema_with_examples(
|
||||
hoconsc:ref(?MODULE, position),
|
||||
#{
|
||||
move_to_front => #{
|
||||
summary => <<"move plugin on the front">>,
|
||||
|
@ -240,7 +289,8 @@ move_request_body() ->
|
|||
summary => <<"move plugin after other plugins">>,
|
||||
value => #{position => <<"after:emqx_plugin_demo-5.1-rc.2">>}
|
||||
}
|
||||
}).
|
||||
}
|
||||
).
|
||||
|
||||
validate_name(Name) ->
|
||||
NameLen = byte_size(Name),
|
||||
|
@ -250,7 +300,8 @@ validate_name(Name) ->
|
|||
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||
_ -> ok
|
||||
end;
|
||||
false -> {error, "Name Length must =< 256"}
|
||||
false ->
|
||||
{error, "Name Length must =< 256"}
|
||||
end.
|
||||
|
||||
%% API CallBack Begin
|
||||
|
@ -271,29 +322,42 @@ upload_install(post, #{body := #{<<"plugin">> := Plugin}}) when is_map(Plugin) -
|
|||
{AppName, _Vsn} = emqx_plugins:parse_name_vsn(FileName),
|
||||
AppDir = filename:join(emqx_plugins:install_dir(), AppName),
|
||||
case filelib:wildcard(AppDir ++ "*.tar.gz") of
|
||||
[] -> do_install_package(FileName, Bin);
|
||||
[] ->
|
||||
do_install_package(FileName, Bin);
|
||||
OtherVsn ->
|
||||
{400, #{code => 'ALREADY_INSTALLED',
|
||||
message => iolist_to_binary(io_lib:format("~p already installed",
|
||||
[OtherVsn]))}}
|
||||
{400, #{
|
||||
code => 'ALREADY_INSTALLED',
|
||||
message => iolist_to_binary(
|
||||
io_lib:format(
|
||||
"~p already installed",
|
||||
[OtherVsn]
|
||||
)
|
||||
)
|
||||
}}
|
||||
end;
|
||||
{ok, _} ->
|
||||
{400, #{code => 'ALREADY_INSTALLED',
|
||||
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))}}
|
||||
{400, #{
|
||||
code => 'ALREADY_INSTALLED',
|
||||
message => iolist_to_binary(io_lib:format("~p is already installed", [FileName]))
|
||||
}}
|
||||
end;
|
||||
upload_install(post, #{}) ->
|
||||
{400, #{code => 'BAD_FORM_DATA',
|
||||
{400, #{
|
||||
code => 'BAD_FORM_DATA',
|
||||
message =>
|
||||
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>}
|
||||
}.
|
||||
<<"form-data should be `plugin=@packagename-vsn.tar.gz;type=application/x-gzip`">>
|
||||
}}.
|
||||
|
||||
do_install_package(FileName, Bin) ->
|
||||
{Res, _} = emqx_mgmt_api_plugins_proto_v1:install_package(FileName, Bin),
|
||||
case lists:filter(fun(R) -> R =/= ok end, Res) of
|
||||
[] -> {200};
|
||||
[] ->
|
||||
{200};
|
||||
[{error, Reason} | _] ->
|
||||
{400, #{code => 'UNEXPECTED_ERROR',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
||||
{400, #{
|
||||
code => 'UNEXPECTED_ERROR',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||
}}
|
||||
end.
|
||||
|
||||
plugin(get, #{bindings := #{name := Name}}) ->
|
||||
|
@ -302,7 +366,6 @@ plugin(get, #{bindings := #{name := Name}}) ->
|
|||
[Plugin] -> {200, Plugin};
|
||||
[] -> {404, #{code => 'NOT_FOUND', message => Name}}
|
||||
end;
|
||||
|
||||
plugin(delete, #{bindings := #{name := Name}}) ->
|
||||
{ok, _TnxId, Res} = emqx_mgmt_api_plugins_proto_v1:delete_package(Name),
|
||||
return(204, Res).
|
||||
|
@ -313,13 +376,17 @@ update_plugin(put, #{bindings := #{name := Name, action := Action}}) ->
|
|||
|
||||
update_boot_order(post, #{bindings := #{name := Name}, body := Body}) ->
|
||||
case parse_position(Body, Name) of
|
||||
{error, Reason} -> {400, #{code => 'BAD_POSITION', message => Reason}};
|
||||
{error, Reason} ->
|
||||
{400, #{code => 'BAD_POSITION', message => Reason}};
|
||||
Position ->
|
||||
case emqx_plugins:ensure_enabled(Name, Position) of
|
||||
ok -> {200};
|
||||
ok ->
|
||||
{200};
|
||||
{error, Reason} ->
|
||||
{400, #{code => 'MOVE_FAILED',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))}}
|
||||
{400, #{
|
||||
code => 'MOVE_FAILED',
|
||||
message => iolist_to_binary(io_lib:format("~p", [Reason]))
|
||||
}}
|
||||
end
|
||||
end.
|
||||
|
||||
|
@ -347,7 +414,8 @@ delete_package(Name) ->
|
|||
_ = emqx_plugins:ensure_disabled(Name),
|
||||
_ = emqx_plugins:purge(Name),
|
||||
_ = emqx_plugins:delete_package(Name);
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% for RPC plugin update
|
||||
|
@ -361,15 +429,19 @@ ensure_action(Name, restart) ->
|
|||
_ = emqx_plugins:ensure_enabled(Name),
|
||||
_ = emqx_plugins:restart(Name).
|
||||
|
||||
return(Code, ok) -> {Code};
|
||||
return(Code, {ok, Result}) -> {Code, Result};
|
||||
return(Code, ok) ->
|
||||
{Code};
|
||||
return(Code, {ok, Result}) ->
|
||||
{Code, Result};
|
||||
return(_, {error, #{error := "bad_info_file", return := {enoent, _}, path := Path}}) ->
|
||||
{404, #{code => 'NOT_FOUND', message => Path}};
|
||||
return(_, {error, Reason}) ->
|
||||
{400, #{code => 'PARAM_ERROR', message => iolist_to_binary(io_lib:format("~p", [Reason]))}}.
|
||||
|
||||
parse_position(#{<<"position">> := <<"front">>}, _) -> front;
|
||||
parse_position(#{<<"position">> := <<"rear">>}, _) -> rear;
|
||||
parse_position(#{<<"position">> := <<"front">>}, _) ->
|
||||
front;
|
||||
parse_position(#{<<"position">> := <<"rear">>}, _) ->
|
||||
rear;
|
||||
parse_position(#{<<"position">> := <<"before:", Name/binary>>}, Name) ->
|
||||
{error, <<"Invalid parameter. Cannot be placed before itself">>};
|
||||
parse_position(#{<<"position">> := <<"after:", Name/binary>>}, Name) ->
|
||||
|
@ -382,7 +454,8 @@ parse_position(#{<<"position">> := <<"before:", Before/binary>>}, _Name) ->
|
|||
{before, binary_to_list(Before)};
|
||||
parse_position(#{<<"position">> := <<"after:", After/binary>>}, _Name) ->
|
||||
{behind, binary_to_list(After)};
|
||||
parse_position(Position, _) -> {error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
||||
parse_position(Position, _) ->
|
||||
{error, iolist_to_binary(io_lib:format("~p", [Position]))}.
|
||||
|
||||
format_plugins(List) ->
|
||||
StatusMap = aggregate_status(List),
|
||||
|
@ -392,13 +465,18 @@ format_plugins(List) ->
|
|||
|
||||
pack_status_in_order(List, StatusMap) ->
|
||||
{Plugins, _} =
|
||||
lists:foldl(fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
||||
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
||||
end, {[], StatusMap}, List),
|
||||
lists:foldl(
|
||||
fun({_Node, PluginList}, {Acc, StatusAcc}) ->
|
||||
pack_plugin_in_order(PluginList, Acc, StatusAcc)
|
||||
end,
|
||||
{[], StatusMap},
|
||||
List
|
||||
),
|
||||
lists:reverse(Plugins).
|
||||
|
||||
pack_plugin_in_order([], Acc, StatusAcc) -> {Acc, StatusAcc};
|
||||
pack_plugin_in_order(_, Acc, StatusAcc)when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
||||
pack_plugin_in_order([], Acc, StatusAcc) ->
|
||||
{Acc, StatusAcc};
|
||||
pack_plugin_in_order(_, Acc, StatusAcc) when map_size(StatusAcc) =:= 0 -> {Acc, StatusAcc};
|
||||
pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin0,
|
||||
case maps:find({Name, Vsn}, StatusAcc) of
|
||||
|
@ -413,15 +491,20 @@ pack_plugin_in_order([Plugin0 | Plugins], Acc, StatusAcc) ->
|
|||
|
||||
aggregate_status(List) -> aggregate_status(List, #{}).
|
||||
|
||||
aggregate_status([], Acc) -> Acc;
|
||||
aggregate_status([], Acc) ->
|
||||
Acc;
|
||||
aggregate_status([{Node, Plugins} | List], Acc) ->
|
||||
NewAcc =
|
||||
lists:foldl(fun(Plugin, SubAcc) ->
|
||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
||||
Key = {Name, Vsn},
|
||||
Value = #{node => Node, status => plugin_status(Plugin)},
|
||||
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
||||
end, Acc, Plugins),
|
||||
lists:foldl(
|
||||
fun(Plugin, SubAcc) ->
|
||||
#{<<"name">> := Name, <<"rel_vsn">> := Vsn} = Plugin,
|
||||
Key = {Name, Vsn},
|
||||
Value = #{node => Node, status => plugin_status(Plugin)},
|
||||
SubAcc#{Key => [Value | maps:get(Key, Acc, [])]}
|
||||
end,
|
||||
Acc,
|
||||
Plugins
|
||||
),
|
||||
aggregate_status(List, NewAcc).
|
||||
|
||||
% running_status: running loaded, stopped
|
||||
|
|
|
@ -20,14 +20,17 @@
|
|||
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ publish/2
|
||||
, publish_batch/2]).
|
||||
-export([
|
||||
publish/2,
|
||||
publish_batch/2
|
||||
]).
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||
|
@ -46,7 +49,6 @@ schema("/publish") ->
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema("/publish/bulk") ->
|
||||
#{
|
||||
'operationId' => publish_batch,
|
||||
|
@ -61,32 +63,43 @@ schema("/publish/bulk") ->
|
|||
|
||||
fields(publish_message) ->
|
||||
[
|
||||
{topic, hoconsc:mk(binary(), #{
|
||||
desc => <<"Topic Name">>,
|
||||
required => true,
|
||||
example => <<"api/example/topic">>})},
|
||||
{qos, hoconsc:mk(emqx_schema:qos(), #{
|
||||
desc => <<"MQTT QoS">>,
|
||||
required => false,
|
||||
default => 0})},
|
||||
{from, hoconsc:mk(binary(), #{
|
||||
desc => <<"From client ID">>,
|
||||
required => false,
|
||||
example => <<"api_example_client">>})},
|
||||
{payload, hoconsc:mk(binary(), #{
|
||||
desc => <<"MQTT Payload">>,
|
||||
required => true,
|
||||
example => <<"hello emqx api">>})},
|
||||
{retain, hoconsc:mk(boolean(), #{
|
||||
desc => <<"MQTT Retain Message">>,
|
||||
required => false,
|
||||
default => false})}
|
||||
{topic,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Topic Name">>,
|
||||
required => true,
|
||||
example => <<"api/example/topic">>
|
||||
})},
|
||||
{qos,
|
||||
hoconsc:mk(emqx_schema:qos(), #{
|
||||
desc => <<"MQTT QoS">>,
|
||||
required => false,
|
||||
default => 0
|
||||
})},
|
||||
{from,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"From client ID">>,
|
||||
required => false,
|
||||
example => <<"api_example_client">>
|
||||
})},
|
||||
{payload,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"MQTT Payload">>,
|
||||
required => true,
|
||||
example => <<"hello emqx api">>
|
||||
})},
|
||||
{retain,
|
||||
hoconsc:mk(boolean(), #{
|
||||
desc => <<"MQTT Retain Message">>,
|
||||
required => false,
|
||||
default => false
|
||||
})}
|
||||
];
|
||||
|
||||
fields(publish_message_info) ->
|
||||
[
|
||||
{id, hoconsc:mk(binary(), #{
|
||||
desc => <<"Internal Message ID">>})}
|
||||
{id,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Internal Message ID">>
|
||||
})}
|
||||
] ++ fields(publish_message).
|
||||
|
||||
publish(post, #{body := Body}) ->
|
||||
|
@ -100,19 +113,21 @@ publish_batch(post, #{body := Body}) ->
|
|||
{200, format_message(Messages)}.
|
||||
|
||||
message(Map) ->
|
||||
From = maps:get(<<"from">>, Map, http_api),
|
||||
QoS = maps:get(<<"qos">>, Map, 0),
|
||||
Topic = maps:get(<<"topic">>, Map),
|
||||
From = maps:get(<<"from">>, Map, http_api),
|
||||
QoS = maps:get(<<"qos">>, Map, 0),
|
||||
Topic = maps:get(<<"topic">>, Map),
|
||||
Payload = maps:get(<<"payload">>, Map),
|
||||
Retain = maps:get(<<"retain">>, Map, false),
|
||||
Retain = maps:get(<<"retain">>, Map, false),
|
||||
emqx_message:make(From, QoS, Topic, Payload, #{retain => Retain}, #{}).
|
||||
|
||||
messages(List) ->
|
||||
[message(MessageMap) || MessageMap <- List].
|
||||
|
||||
format_message(Messages) when is_list(Messages)->
|
||||
format_message(Messages) when is_list(Messages) ->
|
||||
[format_message(Message) || Message <- Messages];
|
||||
format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags}) ->
|
||||
format_message(#message{
|
||||
id = ID, qos = Qos, from = From, topic = Topic, payload = Payload, flags = Flags
|
||||
}) ->
|
||||
#{
|
||||
id => emqx_guid:to_hexstr(ID),
|
||||
qos => Qos,
|
||||
|
@ -124,5 +139,5 @@ format_message(#message{id = ID, qos = Qos, from = From, topic = Topic, payload
|
|||
|
||||
to_binary(Data) when is_binary(Data) ->
|
||||
Data;
|
||||
to_binary(Data) ->
|
||||
to_binary(Data) ->
|
||||
list_to_binary(io_lib:format("~p", [Data])).
|
||||
|
|
|
@ -19,17 +19,22 @@
|
|||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-import( hoconsc
|
||||
, [ mk/2
|
||||
, ref/1
|
||||
, ref/2
|
||||
, array/1]).
|
||||
-import(
|
||||
hoconsc,
|
||||
[
|
||||
mk/2,
|
||||
ref/1,
|
||||
ref/2,
|
||||
array/1
|
||||
]
|
||||
).
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([list/2]).
|
||||
|
||||
|
@ -40,27 +45,38 @@ paths() ->
|
|||
["/stats"].
|
||||
|
||||
schema("/stats") ->
|
||||
#{ 'operationId' => list
|
||||
, get =>
|
||||
#{ description => <<"EMQX stats">>
|
||||
, tags => [<<"stats">>]
|
||||
, parameters => [ref(aggregate)]
|
||||
, responses =>
|
||||
#{ 200 => mk( hoconsc:union([ ref(?MODULE, node_stats_data)
|
||||
, array(ref(?MODULE, aggergate_data))
|
||||
])
|
||||
, #{ desc => <<"List stats ok">> })
|
||||
}
|
||||
#{
|
||||
'operationId' => list,
|
||||
get =>
|
||||
#{
|
||||
description => <<"EMQX stats">>,
|
||||
tags => [<<"stats">>],
|
||||
parameters => [ref(aggregate)],
|
||||
responses =>
|
||||
#{
|
||||
200 => mk(
|
||||
hoconsc:union([
|
||||
ref(?MODULE, node_stats_data),
|
||||
array(ref(?MODULE, aggergate_data))
|
||||
]),
|
||||
#{desc => <<"List stats ok">>}
|
||||
)
|
||||
}
|
||||
}
|
||||
}.
|
||||
}.
|
||||
|
||||
fields(aggregate) ->
|
||||
[ { aggregate
|
||||
, mk( boolean()
|
||||
, #{ desc => <<"Calculation aggregate for all nodes">>
|
||||
, in => query
|
||||
, required => false
|
||||
, example => false})}
|
||||
[
|
||||
{aggregate,
|
||||
mk(
|
||||
boolean(),
|
||||
#{
|
||||
desc => <<"Calculation aggregate for all nodes">>,
|
||||
in => query,
|
||||
required => false,
|
||||
example => false
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(node_stats_data) ->
|
||||
[
|
||||
|
@ -80,17 +96,25 @@ fields(node_stats_data) ->
|
|||
stats_schema('suboptions.max', <<"subscriptions.max">>),
|
||||
stats_schema('subscribers.count', <<"Number of current subscribers">>),
|
||||
stats_schema('subscribers.max', <<"Historical maximum number of subscribers">>),
|
||||
stats_schema('subscriptions.count', <<"Number of current subscriptions, including shared subscriptions">>),
|
||||
stats_schema(
|
||||
'subscriptions.count',
|
||||
<<"Number of current subscriptions, including shared subscriptions">>
|
||||
),
|
||||
stats_schema('subscriptions.max', <<"Historical maximum number of subscriptions">>),
|
||||
stats_schema('subscriptions.shared.count', <<"Number of current shared subscriptions">>),
|
||||
stats_schema('subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>),
|
||||
stats_schema(
|
||||
'subscriptions.shared.max', <<"Historical maximum number of shared subscriptions">>
|
||||
),
|
||||
stats_schema('topics.count', <<"Number of current topics">>),
|
||||
stats_schema('topics.max', <<"Historical maximum number of topics">>)
|
||||
];
|
||||
fields(aggergate_data) ->
|
||||
[ { node
|
||||
, mk( string(), #{ desc => <<"Node name">>
|
||||
, example => <<"emqx@127.0.0.1">>})}
|
||||
[
|
||||
{node,
|
||||
mk(string(), #{
|
||||
desc => <<"Node name">>,
|
||||
example => <<"emqx@127.0.0.1">>
|
||||
})}
|
||||
] ++ fields(node_stats_data).
|
||||
|
||||
stats_schema(Name, Desc) ->
|
||||
|
@ -103,7 +127,9 @@ list(get, #{query_string := Qs}) ->
|
|||
true ->
|
||||
{200, emqx_mgmt:get_stats()};
|
||||
_ ->
|
||||
Data = [maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}]) ||
|
||||
Node <- mria_mnesia:running_nodes()],
|
||||
Data = [
|
||||
maps:from_list(emqx_mgmt:get_stats(Node) ++ [{node, Node}])
|
||||
|| Node <- mria_mnesia:running_nodes()
|
||||
],
|
||||
{200, Data}
|
||||
end.
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
%% API
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1
|
||||
]).
|
||||
|
||||
-export([running_status/2]).
|
||||
|
||||
|
@ -31,22 +32,30 @@ paths() ->
|
|||
["/status"].
|
||||
|
||||
schema("/status") ->
|
||||
#{ 'operationId' => running_status
|
||||
, get =>
|
||||
#{ description => <<"Node running status">>
|
||||
, security => []
|
||||
, responses =>
|
||||
#{200 =>
|
||||
#{ description => <<"Node is running">>
|
||||
, content =>
|
||||
#{ 'text/plain' =>
|
||||
#{ schema => #{type => string}
|
||||
, example => <<"Node emqx@127.0.0.1 is started\nemqx is running">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
#{
|
||||
'operationId' => running_status,
|
||||
get =>
|
||||
#{
|
||||
description => <<"Node running status">>,
|
||||
security => [],
|
||||
responses =>
|
||||
#{
|
||||
200 =>
|
||||
#{
|
||||
description => <<"Node is running">>,
|
||||
content =>
|
||||
#{
|
||||
'text/plain' =>
|
||||
#{
|
||||
schema => #{type => string},
|
||||
example =>
|
||||
<<"Node emqx@127.0.0.1 is started\nemqx is running">>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.
|
||||
}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API Handler funcs
|
||||
|
@ -62,7 +71,7 @@ running_status(get, _Params) ->
|
|||
end,
|
||||
AppStatus =
|
||||
case lists:keysearch(emqx, 1, application:which_applications()) of
|
||||
false -> not_running;
|
||||
false -> not_running;
|
||||
{value, _Val} -> running
|
||||
end,
|
||||
Status = io_lib:format("Node ~ts is ~ts~nemqx is ~ts", [node(), BrokerStatus, AppStatus]),
|
||||
|
|
|
@ -22,26 +22,30 @@
|
|||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([subscriptions/2]).
|
||||
|
||||
-export([ query/4
|
||||
, format/1
|
||||
]).
|
||||
-export([
|
||||
query/4,
|
||||
format/1
|
||||
]).
|
||||
|
||||
-define(SUBS_QTABLE, emqx_suboption).
|
||||
|
||||
-define(SUBS_QSCHEMA,
|
||||
[ {<<"clientid">>, binary}
|
||||
, {<<"topic">>, binary}
|
||||
, {<<"share">>, binary}
|
||||
, {<<"share_group">>, binary}
|
||||
, {<<"qos">>, integer}
|
||||
, {<<"match_topic">>, binary}]).
|
||||
-define(SUBS_QSCHEMA, [
|
||||
{<<"clientid">>, binary},
|
||||
{<<"topic">>, binary},
|
||||
{<<"share">>, binary},
|
||||
{<<"share_group">>, binary},
|
||||
{<<"qos">>, integer},
|
||||
{<<"match_topic">>, binary}
|
||||
]).
|
||||
|
||||
-define(QUERY_FUN, {?MODULE, query}).
|
||||
|
||||
|
@ -58,7 +62,9 @@ schema("/subscriptions") ->
|
|||
description => <<"List subscriptions">>,
|
||||
parameters => parameters(),
|
||||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})}}
|
||||
200 => hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, subscription)), #{})
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(subscription) ->
|
||||
|
@ -74,41 +80,53 @@ parameters() ->
|
|||
hoconsc:ref(emqx_dashboard_swagger, page),
|
||||
hoconsc:ref(emqx_dashboard_swagger, limit),
|
||||
{
|
||||
node, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Node name">>,
|
||||
example => atom_to_list(node())})
|
||||
node,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Node name">>,
|
||||
example => atom_to_list(node())
|
||||
})
|
||||
},
|
||||
{
|
||||
clientid, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client ID">>})
|
||||
clientid,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Client ID">>
|
||||
})
|
||||
},
|
||||
{
|
||||
qos, hoconsc:mk(emqx_schema:qos(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"QoS">>})
|
||||
qos,
|
||||
hoconsc:mk(emqx_schema:qos(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"QoS">>
|
||||
})
|
||||
},
|
||||
{
|
||||
topic, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Topic, url encoding">>})
|
||||
topic,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Topic, url encoding">>
|
||||
})
|
||||
},
|
||||
{
|
||||
match_topic, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Match topic string, url encoding">>})
|
||||
match_topic,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Match topic string, url encoding">>
|
||||
})
|
||||
},
|
||||
{
|
||||
share_group, hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Shared subscription group name">>})
|
||||
share_group,
|
||||
hoconsc:mk(binary(), #{
|
||||
in => query,
|
||||
required => false,
|
||||
desc => <<"Shared subscription group name">>
|
||||
})
|
||||
}
|
||||
].
|
||||
|
||||
|
@ -116,11 +134,20 @@ subscriptions(get, #{query_string := QString}) ->
|
|||
Response =
|
||||
case maps:get(<<"node">>, QString, undefined) of
|
||||
undefined ->
|
||||
emqx_mgmt_api:cluster_query(QString, ?SUBS_QTABLE,
|
||||
?SUBS_QSCHEMA, ?QUERY_FUN);
|
||||
emqx_mgmt_api:cluster_query(
|
||||
QString,
|
||||
?SUBS_QTABLE,
|
||||
?SUBS_QSCHEMA,
|
||||
?QUERY_FUN
|
||||
);
|
||||
Node0 ->
|
||||
emqx_mgmt_api:node_query(binary_to_atom(Node0, utf8), QString,
|
||||
?SUBS_QTABLE, ?SUBS_QSCHEMA, ?QUERY_FUN)
|
||||
emqx_mgmt_api:node_query(
|
||||
binary_to_atom(Node0, utf8),
|
||||
QString,
|
||||
?SUBS_QTABLE,
|
||||
?SUBS_QSCHEMA,
|
||||
?QUERY_FUN
|
||||
)
|
||||
end,
|
||||
case Response of
|
||||
{error, page_limit_invalid} ->
|
||||
|
@ -134,10 +161,8 @@ subscriptions(get, #{query_string := QString}) ->
|
|||
|
||||
format(Items) when is_list(Items) ->
|
||||
[format(Item) || Item <- Items];
|
||||
|
||||
format({{Subscriber, Topic}, Options}) ->
|
||||
format({Subscriber, Topic, Options});
|
||||
|
||||
format({_Subscriber, Topic, Options = #{share := Group}}) ->
|
||||
QoS = maps:get(qos, Options),
|
||||
#{
|
||||
|
@ -161,19 +186,30 @@ format({_Subscriber, Topic, Options}) ->
|
|||
|
||||
query(Tab, {Qs, []}, Continuation, Limit) ->
|
||||
Ms = qs2ms(Qs),
|
||||
emqx_mgmt_api:select_table_with_count( Tab, Ms
|
||||
, Continuation, Limit, fun format/1);
|
||||
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
Ms,
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format/1
|
||||
);
|
||||
query(Tab, {Qs, Fuzzy}, Continuation, Limit) ->
|
||||
Ms = qs2ms(Qs),
|
||||
FuzzyFilterFun = fuzzy_filter_fun(Fuzzy),
|
||||
emqx_mgmt_api:select_table_with_count( Tab, {Ms, FuzzyFilterFun}
|
||||
, Continuation, Limit, fun format/1).
|
||||
emqx_mgmt_api:select_table_with_count(
|
||||
Tab,
|
||||
{Ms, FuzzyFilterFun},
|
||||
Continuation,
|
||||
Limit,
|
||||
fun format/1
|
||||
).
|
||||
|
||||
fuzzy_filter_fun(Fuzzy) ->
|
||||
fun(MsRaws) when is_list(MsRaws) ->
|
||||
lists:filter( fun(E) -> run_fuzzy_filter(E, Fuzzy) end
|
||||
, MsRaws)
|
||||
lists:filter(
|
||||
fun(E) -> run_fuzzy_filter(E, Fuzzy) end,
|
||||
MsRaws
|
||||
)
|
||||
end.
|
||||
|
||||
run_fuzzy_filter(_, []) ->
|
||||
|
|
|
@ -22,23 +22,24 @@
|
|||
%% API
|
||||
-behaviour(minirest_api).
|
||||
|
||||
-export([ api_spec/0
|
||||
, paths/0
|
||||
, schema/1
|
||||
, fields/1
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
paths/0,
|
||||
schema/1,
|
||||
fields/1
|
||||
]).
|
||||
|
||||
-export([ topics/2
|
||||
, topic/2
|
||||
]).
|
||||
-export([
|
||||
topics/2,
|
||||
topic/2
|
||||
]).
|
||||
|
||||
-export([ query/4]).
|
||||
-export([query/4]).
|
||||
|
||||
-define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND').
|
||||
|
||||
-define(TOPICS_QUERY_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]).
|
||||
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||
|
||||
|
@ -73,19 +74,23 @@ schema("/topics/:topic") ->
|
|||
responses => #{
|
||||
200 => hoconsc:mk(hoconsc:ref(topic), #{}),
|
||||
404 =>
|
||||
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'],<<"Topic not found">>)
|
||||
emqx_dashboard_swagger:error_codes(['TOPIC_NOT_FOUND'], <<"Topic not found">>)
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(topic) ->
|
||||
[
|
||||
{topic, hoconsc:mk(binary(), #{
|
||||
desc => <<"Topic Name">>,
|
||||
required => true})},
|
||||
{node, hoconsc:mk(binary(), #{
|
||||
desc => <<"Node">>,
|
||||
required => true})}
|
||||
{topic,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Topic Name">>,
|
||||
required => true
|
||||
})},
|
||||
{node,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Node">>,
|
||||
required => true
|
||||
})}
|
||||
];
|
||||
fields(meta) ->
|
||||
emqx_dashboard_swagger:fields(page) ++
|
||||
|
@ -103,8 +108,11 @@ topic(get, #{bindings := Bindings}) ->
|
|||
%%%==============================================================================================
|
||||
%% api apply
|
||||
do_list(Params) ->
|
||||
case emqx_mgmt_api:node_query(
|
||||
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}) of
|
||||
case
|
||||
emqx_mgmt_api:node_query(
|
||||
node(), Params, emqx_route, ?TOPICS_QUERY_SCHEMA, {?MODULE, query}
|
||||
)
|
||||
of
|
||||
{error, page_limit_invalid} ->
|
||||
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
|
||||
{error, Node, {badrpc, R}} ->
|
||||
|
@ -128,16 +136,18 @@ generate_topic(Params = #{<<"topic">> := Topic}) ->
|
|||
Params#{<<"topic">> => uri_string:percent_decode(Topic)};
|
||||
generate_topic(Params = #{topic := Topic}) ->
|
||||
Params#{topic => uri_string:percent_decode(Topic)};
|
||||
generate_topic(Params) -> Params.
|
||||
generate_topic(Params) ->
|
||||
Params.
|
||||
|
||||
query(Tab, {Qs, _}, Continuation, Limit) ->
|
||||
Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]),
|
||||
emqx_mgmt_api:select_table_with_count(Tab, Ms, Continuation, Limit, fun format/1).
|
||||
|
||||
qs2ms([], Res) -> Res;
|
||||
qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
||||
qs2ms([], Res) ->
|
||||
Res;
|
||||
qs2ms([{topic, '=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) ->
|
||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]);
|
||||
qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
||||
qs2ms([{node, '=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) ->
|
||||
qs2ms(Qs, [{{route, T, N}, [], ['$_']}]).
|
||||
|
||||
format(#route{topic = Topic, dest = {_, Node}}) ->
|
||||
|
@ -147,7 +157,8 @@ format(#route{topic = Topic, dest = Node}) ->
|
|||
|
||||
topic_param(In) ->
|
||||
{
|
||||
topic, hoconsc:mk(binary(), #{
|
||||
topic,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Topic Name">>,
|
||||
in => In,
|
||||
required => (In == path),
|
||||
|
@ -155,9 +166,10 @@ topic_param(In) ->
|
|||
})
|
||||
}.
|
||||
|
||||
node_param()->
|
||||
node_param() ->
|
||||
{
|
||||
node, hoconsc:mk(binary(), #{
|
||||
node,
|
||||
hoconsc:mk(binary(), #{
|
||||
desc => <<"Node Name">>,
|
||||
in => query,
|
||||
required => false,
|
||||
|
|
|
@ -21,26 +21,29 @@
|
|||
-include_lib("typerefl/include/types.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ api_spec/0
|
||||
, fields/1
|
||||
, paths/0
|
||||
, schema/1
|
||||
, namespace/0
|
||||
]).
|
||||
-export([
|
||||
api_spec/0,
|
||||
fields/1,
|
||||
paths/0,
|
||||
schema/1,
|
||||
namespace/0
|
||||
]).
|
||||
|
||||
-export([ trace/2
|
||||
, delete_trace/2
|
||||
, update_trace/2
|
||||
, download_trace_log/2
|
||||
, stream_log_file/2
|
||||
]).
|
||||
-export([
|
||||
trace/2,
|
||||
delete_trace/2,
|
||||
update_trace/2,
|
||||
download_trace_log/2,
|
||||
stream_log_file/2
|
||||
]).
|
||||
|
||||
-export([validate_name/1]).
|
||||
|
||||
%% for rpc
|
||||
-export([ read_trace_file/3
|
||||
, get_trace_size/0
|
||||
]).
|
||||
-export([
|
||||
read_trace_file/3,
|
||||
get_trace_size/0
|
||||
]).
|
||||
|
||||
-define(TO_BIN(_B_), iolist_to_binary(_B_)).
|
||||
-define(NOT_FOUND(N), {404, #{code => 'NOT_FOUND', message => ?TO_BIN([N, " NOT FOUND"])}}).
|
||||
|
@ -53,7 +56,6 @@ api_spec() ->
|
|||
paths() ->
|
||||
["/trace", "/trace/:name/stop", "/trace/:name/download", "/trace/:name/log", "/trace/:name"].
|
||||
|
||||
|
||||
schema("/trace") ->
|
||||
#{
|
||||
'operationId' => trace,
|
||||
|
@ -68,9 +70,14 @@ schema("/trace") ->
|
|||
'requestBody' => delete([status, log_size], fields(trace)),
|
||||
responses => #{
|
||||
200 => hoconsc:ref(trace),
|
||||
400 => emqx_dashboard_swagger:error_codes(['ALREADY_EXISTS',
|
||||
'DUPLICATE_CONDITION', 'INVALID_PARAMS'],
|
||||
<<"trace name already exists">>)
|
||||
400 => emqx_dashboard_swagger:error_codes(
|
||||
[
|
||||
'ALREADY_EXISTS',
|
||||
'DUPLICATE_CONDITION',
|
||||
'INVALID_PARAMS'
|
||||
],
|
||||
<<"trace name already exists">>
|
||||
)
|
||||
}
|
||||
},
|
||||
delete => #{
|
||||
|
@ -112,12 +119,13 @@ schema("/trace/:name/download") ->
|
|||
parameters => [hoconsc:ref(name)],
|
||||
responses => #{
|
||||
200 =>
|
||||
#{description => "A trace zip file",
|
||||
content => #{
|
||||
'application/octet-stream' =>
|
||||
#{schema => #{type => "string", format => "binary"}}
|
||||
#{
|
||||
description => "A trace zip file",
|
||||
content => #{
|
||||
'application/octet-stream' =>
|
||||
#{schema => #{type => "string", format => "binary"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -134,92 +142,151 @@ schema("/trace/:name/log") ->
|
|||
],
|
||||
responses => #{
|
||||
200 =>
|
||||
[
|
||||
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
||||
| fields(bytes) ++ fields(position)
|
||||
]
|
||||
[
|
||||
{items, hoconsc:mk(binary(), #{example => "TEXT-LOG-ITEMS"})}
|
||||
| fields(bytes) ++ fields(position)
|
||||
]
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
fields(trace) ->
|
||||
[
|
||||
{name, hoconsc:mk(binary(),
|
||||
#{desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
required => true,
|
||||
example => <<"EMQX-TRACE-1">>})},
|
||||
{type, hoconsc:mk(hoconsc:enum([clientid, topic, ip_address]),
|
||||
#{desc => """Filter type""",
|
||||
required => true,
|
||||
example => <<"clientid">>})},
|
||||
{topic, hoconsc:mk(binary(),
|
||||
#{desc => """support mqtt wildcard topic.""",
|
||||
required => false,
|
||||
example => <<"/dev/#">>})},
|
||||
{clientid, hoconsc:mk(binary(),
|
||||
#{desc => """mqtt clientid.""",
|
||||
required => false,
|
||||
example => <<"dev-001">>})},
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Unique and format by [a-zA-Z0-9-_]",
|
||||
validator => fun ?MODULE:validate_name/1,
|
||||
required => true,
|
||||
example => <<"EMQX-TRACE-1">>
|
||||
}
|
||||
)},
|
||||
{type,
|
||||
hoconsc:mk(
|
||||
hoconsc:enum([clientid, topic, ip_address]),
|
||||
#{
|
||||
desc => "" "Filter type" "",
|
||||
required => true,
|
||||
example => <<"clientid">>
|
||||
}
|
||||
)},
|
||||
{topic,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "" "support mqtt wildcard topic." "",
|
||||
required => false,
|
||||
example => <<"/dev/#">>
|
||||
}
|
||||
)},
|
||||
{clientid,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "" "mqtt clientid." "",
|
||||
required => false,
|
||||
example => <<"dev-001">>
|
||||
}
|
||||
)},
|
||||
%% TODO add ip_address type in emqx_schema.erl
|
||||
{ip_address, hoconsc:mk(binary(),
|
||||
#{desc => "client ip address",
|
||||
required => false,
|
||||
example => <<"127.0.0.1">>
|
||||
})},
|
||||
{status, hoconsc:mk(hoconsc:enum([running, stopped, waiting]),
|
||||
#{desc => "trace status",
|
||||
required => false,
|
||||
example => running
|
||||
})},
|
||||
{start_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
||||
#{desc => "rfc3339 timestamp or epoch second",
|
||||
required => false,
|
||||
example => <<"2021-11-04T18:17:38+08:00">>
|
||||
})},
|
||||
{end_at, hoconsc:mk(emqx_datetime:epoch_second(),
|
||||
#{desc => "rfc3339 timestamp or epoch second",
|
||||
required => false,
|
||||
example => <<"2021-11-05T18:17:38+08:00">>
|
||||
})},
|
||||
{log_size, hoconsc:mk(hoconsc:array(map()),
|
||||
#{desc => "trace log size",
|
||||
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
||||
required => false})}
|
||||
{ip_address,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "client ip address",
|
||||
required => false,
|
||||
example => <<"127.0.0.1">>
|
||||
}
|
||||
)},
|
||||
{status,
|
||||
hoconsc:mk(
|
||||
hoconsc:enum([running, stopped, waiting]),
|
||||
#{
|
||||
desc => "trace status",
|
||||
required => false,
|
||||
example => running
|
||||
}
|
||||
)},
|
||||
{start_at,
|
||||
hoconsc:mk(
|
||||
emqx_datetime:epoch_second(),
|
||||
#{
|
||||
desc => "rfc3339 timestamp or epoch second",
|
||||
required => false,
|
||||
example => <<"2021-11-04T18:17:38+08:00">>
|
||||
}
|
||||
)},
|
||||
{end_at,
|
||||
hoconsc:mk(
|
||||
emqx_datetime:epoch_second(),
|
||||
#{
|
||||
desc => "rfc3339 timestamp or epoch second",
|
||||
required => false,
|
||||
example => <<"2021-11-05T18:17:38+08:00">>
|
||||
}
|
||||
)},
|
||||
{log_size,
|
||||
hoconsc:mk(
|
||||
hoconsc:array(map()),
|
||||
#{
|
||||
desc => "trace log size",
|
||||
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
|
||||
required => false
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(name) ->
|
||||
[{name, hoconsc:mk(binary(),
|
||||
#{
|
||||
desc => <<"[a-zA-Z0-9-_]">>,
|
||||
example => <<"EMQX-TRACE-1">>,
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
})}
|
||||
[
|
||||
{name,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => <<"[a-zA-Z0-9-_]">>,
|
||||
example => <<"EMQX-TRACE-1">>,
|
||||
in => path,
|
||||
validator => fun ?MODULE:validate_name/1
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(node) ->
|
||||
[{node, hoconsc:mk(binary(),
|
||||
#{
|
||||
desc => "Node name",
|
||||
in => query,
|
||||
required => false
|
||||
})}];
|
||||
[
|
||||
{node,
|
||||
hoconsc:mk(
|
||||
binary(),
|
||||
#{
|
||||
desc => "Node name",
|
||||
in => query,
|
||||
required => false
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(bytes) ->
|
||||
[{bytes, hoconsc:mk(integer(),
|
||||
#{
|
||||
desc => "Maximum number of bytes to store in request",
|
||||
in => query,
|
||||
required => false,
|
||||
default => 1000
|
||||
})}];
|
||||
[
|
||||
{bytes,
|
||||
hoconsc:mk(
|
||||
integer(),
|
||||
#{
|
||||
desc => "Maximum number of bytes to store in request",
|
||||
in => query,
|
||||
required => false,
|
||||
default => 1000
|
||||
}
|
||||
)}
|
||||
];
|
||||
fields(position) ->
|
||||
[{position, hoconsc:mk(integer(),
|
||||
#{
|
||||
desc => "Offset from the current trace position.",
|
||||
in => query,
|
||||
required => false,
|
||||
default => 0
|
||||
})}].
|
||||
|
||||
[
|
||||
{position,
|
||||
hoconsc:mk(
|
||||
integer(),
|
||||
#{
|
||||
desc => "Offset from the current trace position.",
|
||||
in => query,
|
||||
required => false,
|
||||
default => 0
|
||||
}
|
||||
)}
|
||||
].
|
||||
|
||||
-define(NAME_RE, "^[A-Za-z]+[A-Za-z0-9-_]*$").
|
||||
|
||||
|
@ -231,7 +298,8 @@ validate_name(Name) ->
|
|||
nomatch -> {error, "Name should be " ?NAME_RE};
|
||||
_ -> ok
|
||||
end;
|
||||
false -> {error, "Name Length must =< 256"}
|
||||
false ->
|
||||
{error, "Name Length must =< 256"}
|
||||
end.
|
||||
|
||||
delete(Keys, Fields) ->
|
||||
|
@ -239,32 +307,48 @@ delete(Keys, Fields) ->
|
|||
|
||||
trace(get, _Params) ->
|
||||
case emqx_trace:list() of
|
||||
[] -> {200, []};
|
||||
[] ->
|
||||
{200, []};
|
||||
List0 ->
|
||||
List = lists:sort(fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
||||
emqx_trace:format(List0)),
|
||||
List = lists:sort(
|
||||
fun(#{start_at := A}, #{start_at := B}) -> A > B end,
|
||||
emqx_trace:format(List0)
|
||||
),
|
||||
Nodes = mria_mnesia:running_nodes(),
|
||||
TraceSize = wrap_rpc(emqx_mgmt_trace_proto_v1:get_trace_size(Nodes)),
|
||||
AllFileSize = lists:foldl(fun(F, Acc) -> maps:merge(Acc, F) end, #{}, TraceSize),
|
||||
Now = erlang:system_time(second),
|
||||
Traces =
|
||||
lists:map(fun(Trace = #{name := Name, start_at := Start,
|
||||
end_at := End, enable := Enable, type := Type, filter := Filter}) ->
|
||||
FileName = emqx_trace:filename(Name, Start),
|
||||
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
||||
Trace0 = maps:without([enable, filter], Trace),
|
||||
Trace0#{log_size => LogSize
|
||||
, Type => iolist_to_binary(Filter)
|
||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
||||
, status => status(Enable, Start, End, Now)
|
||||
}
|
||||
end, List),
|
||||
lists:map(
|
||||
fun(
|
||||
Trace = #{
|
||||
name := Name,
|
||||
start_at := Start,
|
||||
end_at := End,
|
||||
enable := Enable,
|
||||
type := Type,
|
||||
filter := Filter
|
||||
}
|
||||
) ->
|
||||
FileName = emqx_trace:filename(Name, Start),
|
||||
LogSize = collect_file_size(Nodes, FileName, AllFileSize),
|
||||
Trace0 = maps:without([enable, filter], Trace),
|
||||
Trace0#{
|
||||
log_size => LogSize,
|
||||
Type => iolist_to_binary(Filter),
|
||||
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||
status => status(Enable, Start, End, Now)
|
||||
}
|
||||
end,
|
||||
List
|
||||
),
|
||||
{200, Traces}
|
||||
end;
|
||||
trace(post, #{body := Param}) ->
|
||||
case emqx_trace:create(Param) of
|
||||
{ok, Trace0} -> {200, format_trace(Trace0)};
|
||||
{ok, Trace0} ->
|
||||
{200, format_trace(Trace0)};
|
||||
{error, {already_existed, Name}} ->
|
||||
{400, #{
|
||||
code => 'ALREADY_EXISTS',
|
||||
|
@ -287,18 +371,27 @@ trace(delete, _Param) ->
|
|||
|
||||
format_trace(Trace0) ->
|
||||
[
|
||||
#{start_at := Start, end_at := End,
|
||||
enable := Enable, type := Type, filter := Filter} = Trace1
|
||||
#{
|
||||
start_at := Start,
|
||||
end_at := End,
|
||||
enable := Enable,
|
||||
type := Type,
|
||||
filter := Filter
|
||||
} = Trace1
|
||||
] = emqx_trace:format([Trace0]),
|
||||
Now = erlang:system_time(second),
|
||||
LogSize = lists:foldl(fun(Node, Acc) -> Acc#{Node => 0} end, #{},
|
||||
mria_mnesia:running_nodes()),
|
||||
LogSize = lists:foldl(
|
||||
fun(Node, Acc) -> Acc#{Node => 0} end,
|
||||
#{},
|
||||
mria_mnesia:running_nodes()
|
||||
),
|
||||
Trace2 = maps:without([enable, filter], Trace1),
|
||||
Trace2#{log_size => LogSize
|
||||
, Type => iolist_to_binary(Filter)
|
||||
, start_at => list_to_binary(calendar:system_time_to_rfc3339(Start))
|
||||
, end_at => list_to_binary(calendar:system_time_to_rfc3339(End))
|
||||
, status => status(Enable, Start, End, Now)
|
||||
Trace2#{
|
||||
log_size => LogSize,
|
||||
Type => iolist_to_binary(Filter),
|
||||
start_at => list_to_binary(calendar:system_time_to_rfc3339(Start)),
|
||||
end_at => list_to_binary(calendar:system_time_to_rfc3339(End)),
|
||||
status => status(Enable, Start, End, Now)
|
||||
}.
|
||||
|
||||
delete_trace(delete, #{bindings := #{name := Name}}) ->
|
||||
|
@ -334,25 +427,34 @@ download_trace_log(get, #{bindings := #{name := Name}}) ->
|
|||
<<"content-disposition">> => iolist_to_binary("attachment; filename=" ++ ZipName)
|
||||
},
|
||||
{200, Headers, {file_binary, ZipName, Binary}};
|
||||
{error, not_found} -> ?NOT_FOUND(Name)
|
||||
{error, not_found} ->
|
||||
?NOT_FOUND(Name)
|
||||
end.
|
||||
|
||||
group_trace_file(ZipDir, TraceLog, TraceFiles) ->
|
||||
lists:foldl(fun(Res, Acc) ->
|
||||
case Res of
|
||||
{ok, Node, Bin} ->
|
||||
FileName = Node ++ "-" ++ TraceLog,
|
||||
ZipName = filename:join([ZipDir, FileName]),
|
||||
case file:write_file(ZipName, Bin) of
|
||||
ok -> [FileName | Acc];
|
||||
_ -> Acc
|
||||
end;
|
||||
{error, Node, Reason} ->
|
||||
?SLOG(error, #{msg => "download_trace_log_error", node => Node,
|
||||
log => TraceLog, reason => Reason}),
|
||||
Acc
|
||||
end
|
||||
end, [], TraceFiles).
|
||||
lists:foldl(
|
||||
fun(Res, Acc) ->
|
||||
case Res of
|
||||
{ok, Node, Bin} ->
|
||||
FileName = Node ++ "-" ++ TraceLog,
|
||||
ZipName = filename:join([ZipDir, FileName]),
|
||||
case file:write_file(ZipName, Bin) of
|
||||
ok -> [FileName | Acc];
|
||||
_ -> Acc
|
||||
end;
|
||||
{error, Node, Reason} ->
|
||||
?SLOG(error, #{
|
||||
msg => "download_trace_log_error",
|
||||
node => Node,
|
||||
log => TraceLog,
|
||||
reason => Reason
|
||||
}),
|
||||
Acc
|
||||
end
|
||||
end,
|
||||
[],
|
||||
TraceFiles
|
||||
).
|
||||
|
||||
collect_trace_file(TraceLog) ->
|
||||
Nodes = mria_mnesia:running_nodes(),
|
||||
|
@ -376,18 +478,25 @@ stream_log_file(get, #{bindings := #{name := Name}, query_string := Query}) ->
|
|||
{eof, Size} ->
|
||||
Meta = #{<<"position">> => Size, <<"bytes">> => Bytes},
|
||||
{200, #{meta => Meta, items => <<"">>}};
|
||||
{error, enoent} -> %% the waiting trace should return "" not error.
|
||||
%% the waiting trace should return "" not error.
|
||||
{error, enoent} ->
|
||||
Meta = #{<<"position">> => Position, <<"bytes">> => Bytes},
|
||||
{200, #{meta => Meta, items => <<"">>}};
|
||||
{error, Reason} ->
|
||||
?SLOG(error, #{msg => "read_file_failed",
|
||||
node => Node, name => Name, reason => Reason,
|
||||
position => Position, bytes => Bytes}),
|
||||
?SLOG(error, #{
|
||||
msg => "read_file_failed",
|
||||
node => Node,
|
||||
name => Name,
|
||||
reason => Reason,
|
||||
position => Position,
|
||||
bytes => Bytes
|
||||
}),
|
||||
{400, #{code => 'READ_FILE_ERROR', message => Reason}};
|
||||
{badrpc, nodedown} ->
|
||||
{400, #{code => 'RPC_ERROR', message => "BadRpc node down"}}
|
||||
end;
|
||||
{error, not_found} -> {400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
||||
{error, not_found} ->
|
||||
{400, #{code => 'NODE_ERROR', message => <<"Node not found">>}}
|
||||
end.
|
||||
|
||||
-spec get_trace_size() -> #{{node(), file:name_all()} => non_neg_integer()}.
|
||||
|
@ -396,23 +505,31 @@ get_trace_size() ->
|
|||
Node = node(),
|
||||
case file:list_dir(TraceDir) of
|
||||
{ok, AllFiles} ->
|
||||
lists:foldl(fun(File, Acc) ->
|
||||
FullFileName = filename:join(TraceDir, File),
|
||||
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
||||
end, #{}, lists:delete("zip", AllFiles));
|
||||
_ -> #{}
|
||||
lists:foldl(
|
||||
fun(File, Acc) ->
|
||||
FullFileName = filename:join(TraceDir, File),
|
||||
Acc#{{Node, File} => filelib:file_size(FullFileName)}
|
||||
end,
|
||||
#{},
|
||||
lists:delete("zip", AllFiles)
|
||||
);
|
||||
_ ->
|
||||
#{}
|
||||
end.
|
||||
|
||||
%% this is an rpc call for stream_log_file/2
|
||||
-spec read_trace_file( binary()
|
||||
, non_neg_integer()
|
||||
, non_neg_integer()
|
||||
) -> {ok, binary()}
|
||||
| {error, _}
|
||||
| {eof, non_neg_integer()}.
|
||||
-spec read_trace_file(
|
||||
binary(),
|
||||
non_neg_integer(),
|
||||
non_neg_integer()
|
||||
) ->
|
||||
{ok, binary()}
|
||||
| {error, _}
|
||||
| {eof, non_neg_integer()}.
|
||||
read_trace_file(Name, Position, Limit) ->
|
||||
case emqx_trace:get_trace_filename(Name) of
|
||||
{error, _} = Error -> Error;
|
||||
{error, _} = Error ->
|
||||
Error;
|
||||
{ok, TraceFile} ->
|
||||
TraceDir = emqx_trace:trace_dir(),
|
||||
TracePath = filename:join([TraceDir, TraceFile]),
|
||||
|
@ -423,13 +540,16 @@ read_file(Path, Offset, Bytes) ->
|
|||
case file:open(Path, [read, raw, binary]) of
|
||||
{ok, IoDevice} ->
|
||||
try
|
||||
_ = case Offset of
|
||||
_ =
|
||||
case Offset of
|
||||
0 -> ok;
|
||||
_ -> file:position(IoDevice, {bof, Offset})
|
||||
end,
|
||||
case file:read(IoDevice, Bytes) of
|
||||
{ok, Bin} -> {ok, Bin};
|
||||
{error, Reason} -> {error, Reason};
|
||||
{ok, Bin} ->
|
||||
{ok, Bin};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
eof ->
|
||||
{ok, #file_info{size = Size}} = file:read_file_info(IoDevice),
|
||||
{eof, Size}
|
||||
|
@ -437,20 +557,27 @@ read_file(Path, Offset, Bytes) ->
|
|||
after
|
||||
file:close(IoDevice)
|
||||
end;
|
||||
{error, Reason} -> {error, Reason}
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
to_node(Node) ->
|
||||
try {ok, binary_to_existing_atom(Node)}
|
||||
catch _:_ ->
|
||||
{error, not_found}
|
||||
try
|
||||
{ok, binary_to_existing_atom(Node)}
|
||||
catch
|
||||
_:_ ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
collect_file_size(Nodes, FileName, AllFiles) ->
|
||||
lists:foldl(fun(Node, Acc) ->
|
||||
Size = maps:get({Node, FileName}, AllFiles, 0),
|
||||
Acc#{Node => Size}
|
||||
end, #{}, Nodes).
|
||||
lists:foldl(
|
||||
fun(Node, Acc) ->
|
||||
Size = maps:get({Node, FileName}, AllFiles, 0),
|
||||
Acc#{Node => Size}
|
||||
end,
|
||||
#{},
|
||||
Nodes
|
||||
).
|
||||
|
||||
status(false, _Start, _End, _Now) -> <<"stopped">>;
|
||||
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
|
||||
-define(APP, emqx_management).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
-export([
|
||||
start/2,
|
||||
stop/1
|
||||
]).
|
||||
|
||||
-include("emqx_mgmt.hrl").
|
||||
|
||||
|
|
|
@ -20,14 +20,15 @@
|
|||
-export([mnesia/1]).
|
||||
-boot_mnesia({mnesia, [boot]}).
|
||||
|
||||
-export([ create/4
|
||||
, read/1
|
||||
, update/4
|
||||
, delete/1
|
||||
, list/0
|
||||
]).
|
||||
-export([
|
||||
create/4,
|
||||
read/1,
|
||||
update/4,
|
||||
delete/1,
|
||||
list/0
|
||||
]).
|
||||
|
||||
-export([ authorize/3 ]).
|
||||
-export([authorize/3]).
|
||||
|
||||
-define(APP, emqx_app).
|
||||
|
||||
|
@ -39,7 +40,7 @@
|
|||
desc = <<>> :: binary() | '_',
|
||||
expired_at = 0 :: integer() | undefined | '_',
|
||||
created_at = 0 :: integer() | '_'
|
||||
}).
|
||||
}).
|
||||
|
||||
mnesia(boot) ->
|
||||
ok = mria:create_table(?APP, [
|
||||
|
@ -47,7 +48,8 @@ mnesia(boot) ->
|
|||
{rlog_shard, ?COMMON_SHARD},
|
||||
{storage, disc_copies},
|
||||
{record_name, ?APP},
|
||||
{attributes, record_info(fields, ?APP)}]).
|
||||
{attributes, record_info(fields, ?APP)}
|
||||
]).
|
||||
|
||||
create(Name, Enable, ExpiredAt, Desc) ->
|
||||
case mnesia:table_info(?APP, size) < 30 of
|
||||
|
@ -61,13 +63,14 @@ read(Name) ->
|
|||
[] -> mnesia:abort(not_found);
|
||||
[App] -> to_map(App)
|
||||
end
|
||||
end,
|
||||
end,
|
||||
trans(Fun).
|
||||
|
||||
update(Name, Enable, ExpiredAt, Desc) ->
|
||||
Fun = fun() ->
|
||||
case mnesia:read(?APP, Name, write) of
|
||||
[] -> mnesia:abort(not_found);
|
||||
[] ->
|
||||
mnesia:abort(not_found);
|
||||
[App0 = #?APP{enable = Enable0, desc = Desc0}] ->
|
||||
App =
|
||||
App0#?APP{
|
||||
|
@ -78,22 +81,25 @@ update(Name, Enable, ExpiredAt, Desc) ->
|
|||
ok = mnesia:write(App),
|
||||
to_map(App)
|
||||
end
|
||||
end,
|
||||
end,
|
||||
trans(Fun).
|
||||
|
||||
delete(Name) ->
|
||||
Fun = fun() ->
|
||||
case mnesia:read(?APP, Name) of
|
||||
[] -> mnesia:abort(not_found);
|
||||
[_App] -> mnesia:delete({?APP, Name}) end
|
||||
end,
|
||||
[_App] -> mnesia:delete({?APP, Name})
|
||||
end
|
||||
end,
|
||||
trans(Fun).
|
||||
|
||||
list() ->
|
||||
to_map(ets:match_object(?APP, #?APP{_ = '_'})).
|
||||
|
||||
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) -> {error, <<"not_allowed">>};
|
||||
authorize(<<"/api/v5/users", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||
{error, <<"not_allowed">>};
|
||||
authorize(<<"/api/v5/api_key", _/binary>>, _ApiKey, _ApiSecret) ->
|
||||
{error, <<"not_allowed">>};
|
||||
authorize(_Path, ApiKey, ApiSecret) ->
|
||||
Now = erlang:system_time(second),
|
||||
case find_by_api_key(ApiKey) of
|
||||
|
@ -102,28 +108,35 @@ authorize(_Path, ApiKey, ApiSecret) ->
|
|||
ok -> ok;
|
||||
error -> {error, "secret_error"}
|
||||
end;
|
||||
{ok, true, _ExpiredAt, _SecretHash} -> {error, "secret_expired"};
|
||||
{ok, false, _ExpiredAt, _SecretHash} -> {error, "secret_disable"};
|
||||
{error, Reason} -> {error, Reason}
|
||||
{ok, true, _ExpiredAt, _SecretHash} ->
|
||||
{error, "secret_expired"};
|
||||
{ok, false, _ExpiredAt, _SecretHash} ->
|
||||
{error, "secret_disable"};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
find_by_api_key(ApiKey) ->
|
||||
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
||||
Fun = fun() -> mnesia:match_object(#?APP{api_key = ApiKey, _ = '_'}) end,
|
||||
case trans(Fun) of
|
||||
{ok, [#?APP{api_secret_hash = SecretHash, enable = Enable, expired_at = ExpiredAt}]} ->
|
||||
{ok, Enable, ExpiredAt, SecretHash};
|
||||
_ -> {error, "not_found"}
|
||||
_ ->
|
||||
{error, "not_found"}
|
||||
end.
|
||||
|
||||
ensure_not_undefined(undefined, Old) -> Old;
|
||||
ensure_not_undefined(New, _Old) -> New.
|
||||
|
||||
to_map(Apps)when is_list(Apps) ->
|
||||
to_map(Apps) when is_list(Apps) ->
|
||||
Fields = record_info(fields, ?APP),
|
||||
lists:map(fun(Trace0 = #?APP{}) ->
|
||||
[_ | Values] = tuple_to_list(Trace0),
|
||||
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
||||
end, Apps);
|
||||
lists:map(
|
||||
fun(Trace0 = #?APP{}) ->
|
||||
[_ | Values] = tuple_to_list(Trace0),
|
||||
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
|
||||
end,
|
||||
Apps
|
||||
);
|
||||
to_map(App0) ->
|
||||
[App] = to_map([App0]),
|
||||
App.
|
||||
|
@ -149,16 +162,18 @@ create_app(Name, Enable, ExpiredAt, Desc) ->
|
|||
create_app(App = #?APP{api_key = ApiKey, name = Name}) ->
|
||||
trans(fun() ->
|
||||
case mnesia:read(?APP, Name) of
|
||||
[_] -> mnesia:abort(name_already_existed);
|
||||
[_] ->
|
||||
mnesia:abort(name_already_existed);
|
||||
[] ->
|
||||
case mnesia:match_object(?APP, #?APP{api_key = ApiKey, _ = '_'}, read) of
|
||||
[] ->
|
||||
ok = mnesia:write(App),
|
||||
to_map(App);
|
||||
_ -> mnesia:abort(api_key_already_existed)
|
||||
_ ->
|
||||
mnesia:abort(api_key_already_existed)
|
||||
end
|
||||
end
|
||||
end).
|
||||
end).
|
||||
|
||||
trans(Fun) ->
|
||||
case mria:transaction(?COMMON_SHARD, Fun) of
|
||||
|
|
|
@ -23,8 +23,7 @@
|
|||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
{ok, {{one_for_one, 1, 5}, []}}.
|
||||
|
||||
{ok, {{one_for_one, 1, 5}, []}}.
|
||||
|
|
|
@ -16,38 +16,40 @@
|
|||
|
||||
-module(emqx_mgmt_util).
|
||||
|
||||
-export([ strftime/1
|
||||
, datetime/1
|
||||
, kmg/1
|
||||
, ntoa/1
|
||||
, merge_maps/2
|
||||
, batch_operation/3
|
||||
]).
|
||||
-export([
|
||||
strftime/1,
|
||||
datetime/1,
|
||||
kmg/1,
|
||||
ntoa/1,
|
||||
merge_maps/2,
|
||||
batch_operation/3
|
||||
]).
|
||||
|
||||
-export([ bad_request/0
|
||||
, bad_request/1
|
||||
, properties/1
|
||||
, page_params/0
|
||||
, schema/1
|
||||
, schema/2
|
||||
, object_schema/1
|
||||
, object_schema/2
|
||||
, array_schema/1
|
||||
, array_schema/2
|
||||
, object_array_schema/1
|
||||
, object_array_schema/2
|
||||
, page_schema/1
|
||||
, page_object_schema/1
|
||||
, error_schema/1
|
||||
, error_schema/2
|
||||
, batch_schema/1
|
||||
]).
|
||||
-export([
|
||||
bad_request/0,
|
||||
bad_request/1,
|
||||
properties/1,
|
||||
page_params/0,
|
||||
schema/1,
|
||||
schema/2,
|
||||
object_schema/1,
|
||||
object_schema/2,
|
||||
array_schema/1,
|
||||
array_schema/2,
|
||||
object_array_schema/1,
|
||||
object_array_schema/2,
|
||||
page_schema/1,
|
||||
page_object_schema/1,
|
||||
error_schema/1,
|
||||
error_schema/2,
|
||||
batch_schema/1
|
||||
]).
|
||||
|
||||
-export([urldecode/1]).
|
||||
|
||||
-define(KB, 1024).
|
||||
-define(MB, (1024*1024)).
|
||||
-define(GB, (1024*1024*1024)).
|
||||
-define(MB, (1024 * 1024)).
|
||||
-define(GB, (1024 * 1024 * 1024)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Strftime
|
||||
|
@ -55,17 +57,17 @@
|
|||
|
||||
strftime({MegaSecs, Secs, _MicroSecs}) ->
|
||||
strftime(datetime(MegaSecs * 1000000 + Secs));
|
||||
|
||||
strftime(Secs) when is_integer(Secs) ->
|
||||
strftime(datetime(Secs));
|
||||
|
||||
strftime({{Y,M,D}, {H,MM,S}}) ->
|
||||
strftime({{Y, M, D}, {H, MM, S}}) ->
|
||||
lists:flatten(
|
||||
io_lib:format(
|
||||
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S])).
|
||||
"~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Y, M, D, H, MM, S]
|
||||
)
|
||||
).
|
||||
|
||||
datetime(Timestamp) when is_integer(Timestamp) ->
|
||||
Epoch = calendar:datetime_to_gregorian_seconds({{1970,1,1}, {0,0,0}}),
|
||||
Epoch = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
|
||||
Universal = calendar:gregorian_seconds_to_datetime(Timestamp + Epoch),
|
||||
calendar:universal_time_to_local_time(Universal).
|
||||
|
||||
|
@ -80,19 +82,27 @@ kmg(Byte) ->
|
|||
kmg(F, S) ->
|
||||
iolist_to_binary(io_lib:format("~.2f~ts", [F, S])).
|
||||
|
||||
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
|
||||
ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
|
||||
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
||||
ntoa(IP) ->
|
||||
inet_parse:ntoa(IP).
|
||||
|
||||
merge_maps(Default, New) ->
|
||||
maps:fold(fun(K, V, Acc) ->
|
||||
case maps:get(K, Acc, undefined) of
|
||||
OldV when is_map(OldV),
|
||||
is_map(V) -> Acc#{K => merge_maps(OldV, V)};
|
||||
_ -> Acc#{K => V}
|
||||
end
|
||||
end, Default, New).
|
||||
maps:fold(
|
||||
fun(K, V, Acc) ->
|
||||
case maps:get(K, Acc, undefined) of
|
||||
OldV when
|
||||
is_map(OldV),
|
||||
is_map(V)
|
||||
->
|
||||
Acc#{K => merge_maps(OldV, V)};
|
||||
_ ->
|
||||
Acc#{K => V}
|
||||
end
|
||||
end,
|
||||
Default,
|
||||
New
|
||||
).
|
||||
|
||||
urldecode(S) ->
|
||||
emqx_http_lib:uri_decode(S).
|
||||
|
@ -123,8 +133,13 @@ array_schema(Schema, Desc) ->
|
|||
object_array_schema(Properties) when is_map(Properties) ->
|
||||
json_content_schema(#{type => array, items => #{type => object, properties => Properties}}).
|
||||
object_array_schema(Properties, Desc) ->
|
||||
json_content_schema(#{type => array,
|
||||
items => #{type => object, properties => Properties}}, Desc).
|
||||
json_content_schema(
|
||||
#{
|
||||
type => array,
|
||||
items => #{type => object, properties => Properties}
|
||||
},
|
||||
Desc
|
||||
).
|
||||
|
||||
page_schema(Ref) when is_atom(Ref) ->
|
||||
page_schema(minirest:ref(atom_to_binary(Ref, utf8)));
|
||||
|
@ -134,9 +149,11 @@ page_schema(Schema) ->
|
|||
properties => #{
|
||||
meta => #{
|
||||
type => object,
|
||||
properties => properties([{page, integer},
|
||||
{limit, integer},
|
||||
{count, integer}])
|
||||
properties => properties([
|
||||
{page, integer},
|
||||
{limit, integer},
|
||||
{count, integer}
|
||||
])
|
||||
},
|
||||
data => #{
|
||||
type => array,
|
||||
|
@ -155,8 +172,10 @@ error_schema(Description) ->
|
|||
error_schema(Description, Enum) ->
|
||||
Schema = #{
|
||||
type => object,
|
||||
properties => properties([{code, string, <<>>, Enum},
|
||||
{message, string}])
|
||||
properties => properties([
|
||||
{code, string, <<>>, Enum},
|
||||
{message, string}
|
||||
])
|
||||
},
|
||||
json_content_schema(Schema, Description).
|
||||
|
||||
|
@ -168,20 +187,28 @@ batch_schema(DefName) when is_binary(DefName) ->
|
|||
properties => #{
|
||||
success => #{
|
||||
type => integer,
|
||||
description => <<"Success count">>},
|
||||
description => <<"Success count">>
|
||||
},
|
||||
failed => #{
|
||||
type => integer,
|
||||
description => <<"Failed count">>},
|
||||
description => <<"Failed count">>
|
||||
},
|
||||
detail => #{
|
||||
type => array,
|
||||
description => <<"Failed object & reason">>,
|
||||
items => #{
|
||||
type => object,
|
||||
properties =>
|
||||
#{
|
||||
data => minirest:ref(DefName),
|
||||
reason => #{
|
||||
type => <<"string">>}}}}}},
|
||||
#{
|
||||
data => minirest:ref(DefName),
|
||||
reason => #{
|
||||
type => <<"string">>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
json_content_schema(Schema).
|
||||
|
||||
json_content_schema(Schema) when is_map(Schema) ->
|
||||
|
@ -211,7 +238,7 @@ batch_operation(Module, Function, [Args | ArgsList], Failed) ->
|
|||
case erlang:apply(Module, Function, Args) of
|
||||
ok ->
|
||||
batch_operation(Module, Function, ArgsList, Failed);
|
||||
{error ,Reason} ->
|
||||
{error, Reason} ->
|
||||
batch_operation(Module, Function, ArgsList, [{Args, Reason} | Failed])
|
||||
end.
|
||||
|
||||
|
@ -224,36 +251,75 @@ properties([Key | Props], Acc) when is_atom(Key) ->
|
|||
properties([{Key, Type} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => Type}, Acc));
|
||||
properties([{Key, object, Props1} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => object,
|
||||
properties => properties(Props1)}, Acc));
|
||||
properties(
|
||||
Props,
|
||||
maps:put(
|
||||
Key,
|
||||
#{
|
||||
type => object,
|
||||
properties => properties(Props1)
|
||||
},
|
||||
Acc
|
||||
)
|
||||
);
|
||||
properties([{Key, {array, object}, Props1} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => array,
|
||||
items => #{type => object,
|
||||
properties => properties(Props1)
|
||||
}}, Acc));
|
||||
properties(
|
||||
Props,
|
||||
maps:put(
|
||||
Key,
|
||||
#{
|
||||
type => array,
|
||||
items => #{
|
||||
type => object,
|
||||
properties => properties(Props1)
|
||||
}
|
||||
},
|
||||
Acc
|
||||
)
|
||||
);
|
||||
properties([{Key, {array, Type}, Desc} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => array,
|
||||
items => #{type => Type},
|
||||
description => Desc}, Acc));
|
||||
properties(
|
||||
Props,
|
||||
maps:put(
|
||||
Key,
|
||||
#{
|
||||
type => array,
|
||||
items => #{type => Type},
|
||||
description => Desc
|
||||
},
|
||||
Acc
|
||||
)
|
||||
);
|
||||
properties([{Key, Type, Desc} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => Type, description => Desc}, Acc));
|
||||
properties([{Key, Type, Desc, Enum} | Props], Acc) ->
|
||||
properties(Props, maps:put(Key, #{type => Type,
|
||||
description => Desc,
|
||||
enum => Enum}, Acc)).
|
||||
properties(
|
||||
Props,
|
||||
maps:put(
|
||||
Key,
|
||||
#{
|
||||
type => Type,
|
||||
description => Desc,
|
||||
enum => Enum
|
||||
},
|
||||
Acc
|
||||
)
|
||||
).
|
||||
page_params() ->
|
||||
[#{
|
||||
name => page,
|
||||
in => query,
|
||||
description => <<"Page">>,
|
||||
schema => #{type => integer, default => 1}
|
||||
},
|
||||
#{
|
||||
name => limit,
|
||||
in => query,
|
||||
description => <<"Page size">>,
|
||||
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
||||
}].
|
||||
[
|
||||
#{
|
||||
name => page,
|
||||
in => query,
|
||||
description => <<"Page">>,
|
||||
schema => #{type => integer, default => 1}
|
||||
},
|
||||
#{
|
||||
name => limit,
|
||||
in => query,
|
||||
description => <<"Page size">>,
|
||||
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
|
||||
}
|
||||
].
|
||||
|
||||
bad_request() ->
|
||||
bad_request(<<"Bad Request">>).
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
, get_plugins/0
|
||||
, install_package/2
|
||||
, describe_package/1
|
||||
, delete_package/1
|
||||
, ensure_action/2
|
||||
-export([
|
||||
introduced_in/0,
|
||||
get_plugins/0,
|
||||
install_package/2,
|
||||
describe_package/1,
|
||||
delete_package/1,
|
||||
ensure_action/2
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/bpapi.hrl").
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
|
||||
-behaviour(emqx_bpapi).
|
||||
|
||||
-export([ introduced_in/0
|
||||
-export([
|
||||
introduced_in/0,
|
||||
|
||||
, trace_file/2
|
||||
, get_trace_size/1
|
||||
, read_trace_file/4
|
||||
]).
|
||||
trace_file/2,
|
||||
get_trace_size/1,
|
||||
read_trace_file/4
|
||||
]).
|
||||
|
||||
-include_lib("emqx/include/bpapi.hrl").
|
||||
|
||||
|
@ -31,21 +32,22 @@ introduced_in() ->
|
|||
"5.0.0".
|
||||
|
||||
-spec get_trace_size([node()]) ->
|
||||
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
||||
emqx_rpc:multicall_result(#{{node(), file:name_all()} => non_neg_integer()}).
|
||||
get_trace_size(Nodes) ->
|
||||
rpc:multicall(Nodes, emqx_mgmt_api_trace, get_trace_size, [], 30000).
|
||||
|
||||
-spec trace_file([node()], file:name_all()) ->
|
||||
emqx_rpc:multicall_result(
|
||||
{ok, Node :: list(), Binary :: binary()} |
|
||||
{error, Node :: list(), Reason :: term()}).
|
||||
emqx_rpc:multicall_result(
|
||||
{ok, Node :: list(), Binary :: binary()}
|
||||
| {error, Node :: list(), Reason :: term()}
|
||||
).
|
||||
trace_file(Nodes, File) ->
|
||||
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).
|
||||
|
||||
-spec read_trace_file(node(), binary(), non_neg_integer(), non_neg_integer()) ->
|
||||
{ok, binary()}
|
||||
| {error, _}
|
||||
| {eof, non_neg_integer()}
|
||||
| {badrpc, _}.
|
||||
{ok, binary()}
|
||||
| {error, _}
|
||||
| {eof, non_neg_integer()}
|
||||
| {badrpc, _}.
|
||||
read_trace_file(Node, Name, Position, Limit) ->
|
||||
rpc:call(Node, emqx_mgmt_api_trace, read_trace_file, [Name, Position, Limit]).
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
%%--------------------------------------------------------------------
|
||||
-module(emqx_mgmt_api_alarms_SUITE).
|
||||
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
|
@ -55,8 +54,8 @@ get_alarms(AssertCount, Activated) ->
|
|||
Headers = emqx_mgmt_api_test_util:auth_header_(),
|
||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(get, Path, Qs, Headers),
|
||||
Data = emqx_json:decode(Response, [return_maps]),
|
||||
Meta = maps:get(<<"meta">>, Data),
|
||||
Page = maps:get(<<"page">>, Meta),
|
||||
Meta = maps:get(<<"meta">>, Data),
|
||||
Page = maps:get(<<"page">>, Meta),
|
||||
Limit = maps:get(<<"limit">>, Meta),
|
||||
Count = maps:get(<<"count">>, Meta),
|
||||
?assertEqual(Page, 1),
|
||||
|
|
|
@ -22,10 +22,11 @@
|
|||
|
||||
all() -> [{group, parallel}, {group, sequence}].
|
||||
suite() -> [{timetrap, {minutes, 1}}].
|
||||
groups() -> [
|
||||
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
||||
{sequence, [], [t_create_failed]}
|
||||
].
|
||||
groups() ->
|
||||
[
|
||||
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
|
||||
{sequence, [], [t_create_failed]}
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_mgmt_api_test_util:init_suite(),
|
||||
|
@ -37,15 +38,20 @@ end_per_suite(_) ->
|
|||
t_create(_Config) ->
|
||||
Name = <<"EMQX-API-KEY-1">>,
|
||||
{ok, Create} = create_app(Name),
|
||||
?assertMatch(#{<<"api_key">> := _,
|
||||
<<"api_secret">> := _,
|
||||
<<"created_at">> := _,
|
||||
<<"desc">> := _,
|
||||
<<"enable">> := true,
|
||||
<<"expired_at">> := _,
|
||||
<<"name">> := Name}, Create),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"api_key">> := _,
|
||||
<<"api_secret">> := _,
|
||||
<<"created_at">> := _,
|
||||
<<"desc">> := _,
|
||||
<<"enable">> := true,
|
||||
<<"expired_at">> := _,
|
||||
<<"name">> := Name
|
||||
},
|
||||
Create
|
||||
),
|
||||
{ok, List} = list_app(),
|
||||
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
||||
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
|
||||
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
|
||||
{ok, App1} = read_app(Name),
|
||||
?assertEqual(Name, maps:get(<<"name">>, App1)),
|
||||
|
@ -64,9 +70,12 @@ t_create_failed(_Config) ->
|
|||
|
||||
{ok, List} = list_app(),
|
||||
CreateNum = 30 - erlang:length(List),
|
||||
Names = lists:map(fun(Seq) ->
|
||||
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
||||
end, lists:seq(1, CreateNum)),
|
||||
Names = lists:map(
|
||||
fun(Seq) ->
|
||||
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
|
||||
end,
|
||||
lists:seq(1, CreateNum)
|
||||
),
|
||||
lists:foreach(fun(N) -> {ok, _} = create_app(N) end, Names),
|
||||
?assertEqual(BadRequest, create_app(<<"EMQX-API-KEY-MAXIMUM">>)),
|
||||
|
||||
|
@ -93,7 +102,8 @@ t_update(_Config) ->
|
|||
?assertEqual(Name, maps:get(<<"name">>, Update1)),
|
||||
?assertEqual(false, maps:get(<<"enable">>, Update1)),
|
||||
?assertEqual(<<"NoteVersion1"/utf8>>, maps:get(<<"desc">>, Update1)),
|
||||
?assertEqual(calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
||||
?assertEqual(
|
||||
calendar:rfc3339_to_system_time(binary_to_list(ExpiredAt)),
|
||||
calendar:rfc3339_to_system_time(binary_to_list(maps:get(<<"expired_at">>, Update1)))
|
||||
),
|
||||
Unexpired1 = maps:without([expired_at], Change),
|
||||
|
@ -117,10 +127,14 @@ t_delete(_Config) ->
|
|||
t_authorize(_Config) ->
|
||||
Name = <<"EMQX-API-AUTHORIZE-KEY">>,
|
||||
{ok, #{<<"api_key">> := ApiKey, <<"api_secret">> := ApiSecret}} = create_app(Name),
|
||||
BasicHeader = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
||||
binary_to_list(ApiSecret)),
|
||||
SecretError = emqx_common_test_http:auth_header(binary_to_list(ApiKey),
|
||||
binary_to_list(ApiKey)),
|
||||
BasicHeader = emqx_common_test_http:auth_header(
|
||||
binary_to_list(ApiKey),
|
||||
binary_to_list(ApiSecret)
|
||||
),
|
||||
SecretError = emqx_common_test_http:auth_header(
|
||||
binary_to_list(ApiKey),
|
||||
binary_to_list(ApiKey)
|
||||
),
|
||||
KeyError = emqx_common_test_http:auth_header("not_found_key", binary_to_list(ApiSecret)),
|
||||
Unauthorized = {error, {"HTTP/1.1", 401, "Unauthorized"}},
|
||||
|
||||
|
@ -134,8 +148,10 @@ t_authorize(_Config) ->
|
|||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, ApiKeyPath, BasicHeader)),
|
||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, UserPath, BasicHeader)),
|
||||
|
||||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
||||
update_app(Name, #{enable => false})),
|
||||
?assertMatch(
|
||||
{ok, #{<<"api_key">> := _, <<"enable">> := false}},
|
||||
update_app(Name, #{enable => false})
|
||||
),
|
||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||
|
||||
Expired = #{
|
||||
|
@ -145,8 +161,10 @@ t_authorize(_Config) ->
|
|||
?assertMatch({ok, #{<<"api_key">> := _, <<"enable">> := true}}, update_app(Name, Expired)),
|
||||
?assertEqual(Unauthorized, emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader)),
|
||||
UnExpired = #{expired_at => undefined},
|
||||
?assertMatch({ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
||||
update_app(Name, UnExpired)),
|
||||
?assertMatch(
|
||||
{ok, #{<<"api_key">> := _, <<"expired_at">> := <<"undefined">>}},
|
||||
update_app(Name, UnExpired)
|
||||
),
|
||||
{ok, _Status1} = emqx_mgmt_api_test_util:request_api(get, BanPath, BasicHeader),
|
||||
ok.
|
||||
|
||||
|
@ -159,7 +177,6 @@ t_create_unexpired_app(_Config) ->
|
|||
?assertMatch(#{<<"expired_at">> := <<"undefined">>}, Create2),
|
||||
ok.
|
||||
|
||||
|
||||
list_app() ->
|
||||
Path = emqx_mgmt_api_test_util:api_path(["api_key"]),
|
||||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||
|
|
|
@ -47,13 +47,17 @@ t_create(_Config) ->
|
|||
until => Until
|
||||
},
|
||||
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
|
||||
?assertEqual(#{<<"as">> => As,
|
||||
<<"at">> => At,
|
||||
<<"by">> => By,
|
||||
<<"reason">> => Reason,
|
||||
<<"until">> => Until,
|
||||
<<"who">> => ClientId
|
||||
}, ClientIdBannedRes),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"as">> => As,
|
||||
<<"at">> => At,
|
||||
<<"by">> => By,
|
||||
<<"reason">> => Reason,
|
||||
<<"until">> => Until,
|
||||
<<"who">> => ClientId
|
||||
},
|
||||
ClientIdBannedRes
|
||||
),
|
||||
PeerHost = <<"192.168.2.13">>,
|
||||
PeerHostBanned = #{
|
||||
as => <<"peerhost">>,
|
||||
|
@ -64,15 +68,19 @@ t_create(_Config) ->
|
|||
until => Until
|
||||
},
|
||||
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
|
||||
?assertEqual(#{<<"as">> => <<"peerhost">>,
|
||||
<<"at">> => At,
|
||||
<<"by">> => By,
|
||||
<<"reason">> => Reason,
|
||||
<<"until">> => Until,
|
||||
<<"who">> => PeerHost
|
||||
}, PeerHostBannedRes),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"as">> => <<"peerhost">>,
|
||||
<<"at">> => At,
|
||||
<<"by">> => By,
|
||||
<<"reason">> => Reason,
|
||||
<<"until">> => Until,
|
||||
<<"who">> => PeerHost
|
||||
},
|
||||
PeerHostBannedRes
|
||||
),
|
||||
{ok, #{<<"data">> := List}} = list_banned(),
|
||||
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
||||
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
|
||||
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
|
||||
ok.
|
||||
|
||||
|
@ -94,8 +102,10 @@ t_create_failed(_Config) ->
|
|||
},
|
||||
BadRequest = {error, {"HTTP/1.1", 400, "Bad Request"}},
|
||||
?assertEqual(BadRequest, create_banned(BadPeerHost)),
|
||||
Expired = BadPeerHost#{until => emqx_banned:to_rfc3339(Now - 1),
|
||||
who => <<"127.0.0.1">>},
|
||||
Expired = BadPeerHost#{
|
||||
until => emqx_banned:to_rfc3339(Now - 1),
|
||||
who => <<"127.0.0.1">>
|
||||
},
|
||||
?assertEqual(BadRequest, create_banned(Expired)),
|
||||
ok.
|
||||
|
||||
|
@ -117,8 +127,10 @@ t_delete(_Config) ->
|
|||
},
|
||||
{ok, _} = create_banned(Banned),
|
||||
?assertMatch({ok, _}, delete_banned(binary_to_list(As), binary_to_list(Who))),
|
||||
?assertMatch({error,{"HTTP/1.1",404,"Not Found"}},
|
||||
delete_banned(binary_to_list(As), binary_to_list(Who))),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 404, "Not Found"}},
|
||||
delete_banned(binary_to_list(As), binary_to_list(Who))
|
||||
),
|
||||
ok.
|
||||
|
||||
list_banned() ->
|
||||
|
|
|
@ -44,20 +44,20 @@ t_clients(_) ->
|
|||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||
|
||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||
{ok, _} = emqtt:connect(C1),
|
||||
{ok, _} = emqtt:connect(C1),
|
||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||
{ok, _} = emqtt:connect(C2),
|
||||
{ok, _} = emqtt:connect(C2),
|
||||
|
||||
timer:sleep(300),
|
||||
|
||||
%% get /clients
|
||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||
{ok, Clients} = emqx_mgmt_api_test_util:request_api(get, ClientsPath),
|
||||
ClientsResponse = emqx_json:decode(Clients, [return_maps]),
|
||||
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
||||
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
||||
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
||||
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
||||
ClientsMeta = maps:get(<<"meta">>, ClientsResponse),
|
||||
ClientsPage = maps:get(<<"page">>, ClientsMeta),
|
||||
ClientsLimit = maps:get(<<"limit">>, ClientsMeta),
|
||||
ClientsCount = maps:get(<<"count">>, ClientsMeta),
|
||||
?assertEqual(ClientsPage, 1),
|
||||
?assertEqual(ClientsLimit, emqx_mgmt:max_row_limit()),
|
||||
?assertEqual(ClientsCount, 2),
|
||||
|
@ -77,28 +77,48 @@ t_clients(_) ->
|
|||
?assertEqual({error, {"HTTP/1.1", 404, "Not Found"}}, AfterKickoutResponse2),
|
||||
|
||||
%% get /clients/:clientid/authorization/cache should has no authz cache
|
||||
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path(["clients",
|
||||
binary_to_list(ClientId1), "authorization", "cache"]),
|
||||
Client1AuthzCachePath = emqx_mgmt_api_test_util:api_path([
|
||||
"clients",
|
||||
binary_to_list(ClientId1),
|
||||
"authorization",
|
||||
"cache"
|
||||
]),
|
||||
{ok, Client1AuthzCache} = emqx_mgmt_api_test_util:request_api(get, Client1AuthzCachePath),
|
||||
?assertEqual("[]", Client1AuthzCache),
|
||||
|
||||
%% post /clients/:clientid/subscribe
|
||||
SubscribeBody = #{topic => Topic, qos => Qos},
|
||||
SubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
||||
binary_to_list(ClientId1), "subscribe"]),
|
||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, SubscribePath,
|
||||
"", AuthHeader, SubscribeBody),
|
||||
SubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||
"clients",
|
||||
binary_to_list(ClientId1),
|
||||
"subscribe"
|
||||
]),
|
||||
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||
post,
|
||||
SubscribePath,
|
||||
"",
|
||||
AuthHeader,
|
||||
SubscribeBody
|
||||
),
|
||||
timer:sleep(100),
|
||||
[{AfterSubTopic, #{qos := AfterSubQos}}] = emqx_mgmt:lookup_subscriptions(ClientId1),
|
||||
?assertEqual(AfterSubTopic, Topic),
|
||||
?assertEqual(AfterSubQos, Qos),
|
||||
|
||||
%% post /clients/:clientid/unsubscribe
|
||||
UnSubscribePath = emqx_mgmt_api_test_util:api_path(["clients",
|
||||
binary_to_list(ClientId1), "unsubscribe"]),
|
||||
UnSubscribePath = emqx_mgmt_api_test_util:api_path([
|
||||
"clients",
|
||||
binary_to_list(ClientId1),
|
||||
"unsubscribe"
|
||||
]),
|
||||
UnSubscribeBody = #{topic => Topic},
|
||||
{ok, _} = emqx_mgmt_api_test_util:request_api(post, UnSubscribePath,
|
||||
"", AuthHeader, UnSubscribeBody),
|
||||
{ok, _} = emqx_mgmt_api_test_util:request_api(
|
||||
post,
|
||||
UnSubscribePath,
|
||||
"",
|
||||
AuthHeader,
|
||||
UnSubscribeBody
|
||||
),
|
||||
timer:sleep(100),
|
||||
?assertEqual([], emqx_mgmt:lookup_subscriptions(Client1)),
|
||||
|
||||
|
@ -118,44 +138,58 @@ t_query_clients_with_time(_) ->
|
|||
ClientId2 = <<"client2">>,
|
||||
|
||||
{ok, C1} = emqtt:start_link(#{username => Username1, clientid => ClientId1}),
|
||||
{ok, _} = emqtt:connect(C1),
|
||||
{ok, _} = emqtt:connect(C1),
|
||||
{ok, C2} = emqtt:start_link(#{username => Username2, clientid => ClientId2}),
|
||||
{ok, _} = emqtt:connect(C2),
|
||||
{ok, _} = emqtt:connect(C2),
|
||||
|
||||
timer:sleep(100),
|
||||
|
||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||
ClientsPath = emqx_mgmt_api_test_util:api_path(["clients"]),
|
||||
%% get /clients with time(rfc3339)
|
||||
NowTimeStampInt = erlang:system_time(millisecond),
|
||||
%% Do not uri_encode `=` to `%3D`
|
||||
Rfc3339String = emqx_http_lib:uri_encode(binary:bin_to_list(
|
||||
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt))),
|
||||
Rfc3339String = emqx_http_lib:uri_encode(
|
||||
binary:bin_to_list(
|
||||
emqx_datetime:epoch_to_rfc3339(NowTimeStampInt)
|
||||
)
|
||||
),
|
||||
TimeStampString = emqx_http_lib:uri_encode(integer_to_list(NowTimeStampInt)),
|
||||
|
||||
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
||||
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
||||
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
||||
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
||||
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
||||
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
||||
LteKeys = ["lte_created_at=", "lte_connected_at="],
|
||||
GteKeys = ["gte_created_at=", "gte_connected_at="],
|
||||
LteParamRfc3339 = [Param ++ Rfc3339String || Param <- LteKeys],
|
||||
LteParamStamp = [Param ++ TimeStampString || Param <- LteKeys],
|
||||
GteParamRfc3339 = [Param ++ Rfc3339String || Param <- GteKeys],
|
||||
GteParamStamp = [Param ++ TimeStampString || Param <- GteKeys],
|
||||
|
||||
RequestResults =
|
||||
[emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
||||
|| Param <- LteParamRfc3339 ++ LteParamStamp
|
||||
++ GteParamRfc3339 ++ GteParamStamp],
|
||||
DecodedResults = [emqx_json:decode(Response, [return_maps])
|
||||
|| {ok, Response} <- RequestResults],
|
||||
RequestResults =
|
||||
[
|
||||
emqx_mgmt_api_test_util:request_api(get, ClientsPath, Param, AuthHeader)
|
||||
|| Param <-
|
||||
LteParamRfc3339 ++ LteParamStamp ++
|
||||
GteParamRfc3339 ++ GteParamStamp
|
||||
],
|
||||
DecodedResults = [
|
||||
emqx_json:decode(Response, [return_maps])
|
||||
|| {ok, Response} <- RequestResults
|
||||
],
|
||||
{LteResponseDecodeds, GteResponseDecodeds} = lists:split(4, DecodedResults),
|
||||
%% EachData :: list()
|
||||
[?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
||||
[
|
||||
?assert(time_string_to_epoch_millisecond(CreatedAt) < NowTimeStampInt)
|
||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||
#{<<"created_at">> := CreatedAt} <- EachData],
|
||||
[?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
||||
#{<<"created_at">> := CreatedAt} <- EachData
|
||||
],
|
||||
[
|
||||
?assert(time_string_to_epoch_millisecond(ConnectedAt) < NowTimeStampInt)
|
||||
|| #{<<"data">> := EachData} <- LteResponseDecodeds,
|
||||
#{<<"connected_at">> := ConnectedAt} <- EachData],
|
||||
[?assertEqual(EachData, [])
|
||||
|| #{<<"data">> := EachData} <- GteResponseDecodeds],
|
||||
#{<<"connected_at">> := ConnectedAt} <- EachData
|
||||
],
|
||||
[
|
||||
?assertEqual(EachData, [])
|
||||
|| #{<<"data">> := EachData} <- GteResponseDecodeds
|
||||
],
|
||||
|
||||
%% testcase cleanup, kickout client1 and client2
|
||||
Client1Path = emqx_mgmt_api_test_util:api_path(["clients", binary_to_list(ClientId1)]),
|
||||
|
@ -169,7 +203,7 @@ t_keepalive(_Config) ->
|
|||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||
Path = emqx_mgmt_api_test_util:api_path(["clients", ClientId, "keepalive"]),
|
||||
Body = #{interval => 11},
|
||||
{error,{"HTTP/1.1",404,"Not Found"}} =
|
||||
{error, {"HTTP/1.1", 404, "Not Found"}} =
|
||||
emqx_mgmt_api_test_util:request_api(put, Path, <<"">>, AuthHeader, Body),
|
||||
{ok, C1} = emqtt:start_link(#{username => Username, clientid => ClientId}),
|
||||
{ok, _} = emqtt:connect(C1),
|
||||
|
@ -190,5 +224,6 @@ time_string_to_epoch(DateTime, Unit) when is_binary(DateTime) ->
|
|||
catch
|
||||
error:badarg ->
|
||||
calendar:rfc3339_to_system_time(
|
||||
binary_to_list(DateTime), [{unit, Unit}])
|
||||
binary_to_list(DateTime), [{unit, Unit}]
|
||||
)
|
||||
end.
|
||||
|
|
|
@ -32,10 +32,13 @@ end_per_suite(_) ->
|
|||
|
||||
t_get(_Config) ->
|
||||
{ok, Configs} = get_configs(),
|
||||
maps:map(fun(Name, Value) ->
|
||||
{ok, Config} = get_config(Name),
|
||||
?assertEqual(Value, Config)
|
||||
end, maps:remove(<<"license">>, Configs)),
|
||||
maps:map(
|
||||
fun(Name, Value) ->
|
||||
{ok, Config} = get_config(Name),
|
||||
?assertEqual(Value, Config)
|
||||
end,
|
||||
maps:remove(<<"license">>, Configs)
|
||||
),
|
||||
ok.
|
||||
|
||||
t_update(_Config) ->
|
||||
|
@ -50,8 +53,10 @@ t_update(_Config) ->
|
|||
|
||||
%% update failed
|
||||
ErrorSysMon = emqx_map_lib:deep_put([<<"vm">>, <<"busy_port">>], SysMon, "123"),
|
||||
?assertMatch({error, {"HTTP/1.1", 400, _}},
|
||||
update_config(<<"sysmon">>, ErrorSysMon)),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 400, _}},
|
||||
update_config(<<"sysmon">>, ErrorSysMon)
|
||||
),
|
||||
{ok, SysMon2} = get_config(<<"sysmon">>),
|
||||
?assertEqual(SysMon1, SysMon2),
|
||||
|
||||
|
@ -101,8 +106,10 @@ t_global_zone(_Config) ->
|
|||
{ok, Zones} = get_global_zone(),
|
||||
ZonesKeys = lists:map(fun({K, _}) -> K end, hocon_schema:roots(emqx_zone_schema)),
|
||||
?assertEqual(lists:usort(ZonesKeys), lists:usort(maps:keys(Zones))),
|
||||
?assertEqual(emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
||||
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)),
|
||||
?assertEqual(
|
||||
emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed]),
|
||||
emqx_map_lib:deep_get([<<"mqtt">>, <<"max_qos_allowed">>], Zones)
|
||||
),
|
||||
NewZones = emqx_map_lib:deep_put([<<"mqtt">>, <<"max_qos_allowed">>], Zones, 1),
|
||||
{ok, #{}} = update_global_zone(NewZones),
|
||||
?assertEqual(1, emqx_config:get_zone_conf(no_default, [mqtt, max_qos_allowed])),
|
||||
|
@ -133,7 +140,8 @@ get_config(Name) ->
|
|||
case emqx_mgmt_api_test_util:request_api(get, Path) of
|
||||
{ok, Res} ->
|
||||
{ok, emqx_json:decode(Res, [return_maps])};
|
||||
Error -> Error
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
get_configs() ->
|
||||
|
@ -153,8 +161,11 @@ update_config(Name, Change) ->
|
|||
|
||||
reset_config(Name, Key) ->
|
||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||
Path = binary_to_list(iolist_to_binary(
|
||||
emqx_mgmt_api_test_util:api_path(["configs_reset", Name]))),
|
||||
Path = binary_to_list(
|
||||
iolist_to_binary(
|
||||
emqx_mgmt_api_test_util:api_path(["configs_reset", Name])
|
||||
)
|
||||
),
|
||||
case emqx_mgmt_api_test_util:request_api(post, Path, Key, AuthHeader, []) of
|
||||
{ok, []} -> ok;
|
||||
Error -> Error
|
||||
|
|
|
@ -40,16 +40,17 @@ t_single_node_metrics_api(_) ->
|
|||
{ok, MetricsResponse} = request_helper("metrics"),
|
||||
[MetricsFromAPI] = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||
LocalNodeMetrics = maps:from_list(
|
||||
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]),
|
||||
emqx_mgmt:get_metrics(node()) ++ [{node, to_bin(node())}]
|
||||
),
|
||||
match_helper(LocalNodeMetrics, MetricsFromAPI).
|
||||
|
||||
match_helper(SystemMetrics, MetricsFromAPI) ->
|
||||
length_equal(SystemMetrics, MetricsFromAPI),
|
||||
Fun =
|
||||
fun (Key, {SysMetrics, APIMetrics}) ->
|
||||
Value = maps:get(Key, SysMetrics),
|
||||
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
||||
{Value, {SysMetrics, APIMetrics}}
|
||||
fun(Key, {SysMetrics, APIMetrics}) ->
|
||||
Value = maps:get(Key, SysMetrics),
|
||||
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
|
||||
{Value, {SysMetrics, APIMetrics}}
|
||||
end,
|
||||
lists:mapfoldl(Fun, {SystemMetrics, MetricsFromAPI}, maps:keys(SystemMetrics)).
|
||||
|
||||
|
|
|
@ -67,19 +67,21 @@ t_nodes_api(_) ->
|
|||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode"]),
|
||||
?assertMatch(
|
||||
{error, {_, 400, _}},
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||
).
|
||||
|
||||
t_log_path(_) ->
|
||||
NodePath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_list(node())]),
|
||||
{ok, NodeInfo} = emqx_mgmt_api_test_util:request_api(get, NodePath),
|
||||
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
|
||||
?assertEqual(
|
||||
<<"emqx-test.log">>,
|
||||
filename:basename(Path)).
|
||||
<<"emqx-test.log">>,
|
||||
filename:basename(Path)
|
||||
).
|
||||
|
||||
t_node_stats_api(_) ->
|
||||
StatsPath = emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "stats"]),
|
||||
SystemStats= emqx_mgmt:get_stats(),
|
||||
SystemStats = emqx_mgmt:get_stats(),
|
||||
{ok, StatsResponse} = emqx_mgmt_api_test_util:request_api(get, StatsPath),
|
||||
Stats = emqx_json:decode(StatsResponse, [return_maps]),
|
||||
Fun =
|
||||
|
@ -91,12 +93,13 @@ t_node_stats_api(_) ->
|
|||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "stats"]),
|
||||
?assertMatch(
|
||||
{error, {_, 400, _}},
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||
).
|
||||
|
||||
t_node_metrics_api(_) ->
|
||||
MetricsPath =
|
||||
emqx_mgmt_api_test_util:api_path(["nodes", atom_to_binary(node(), utf8), "metrics"]),
|
||||
SystemMetrics= emqx_mgmt:get_metrics(),
|
||||
SystemMetrics = emqx_mgmt:get_metrics(),
|
||||
{ok, MetricsResponse} = emqx_mgmt_api_test_util:request_api(get, MetricsPath),
|
||||
Metrics = emqx_json:decode(MetricsResponse, [return_maps]),
|
||||
Fun =
|
||||
|
@ -108,4 +111,5 @@ t_node_metrics_api(_) ->
|
|||
BadNodePath = emqx_mgmt_api_test_util:api_path(["nodes", "badnode", "metrics"]),
|
||||
?assertMatch(
|
||||
{error, {_, 400, _}},
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)).
|
||||
emqx_mgmt_api_test_util:request_api(get, BadNodePath)
|
||||
).
|
||||
|
|
|
@ -56,17 +56,35 @@ todo_t_plugins(Config) ->
|
|||
ok = emqx_plugins:delete_package(NameVsn),
|
||||
ok = install_plugin(PackagePath),
|
||||
{ok, StopRes} = describe_plugins(NameVsn),
|
||||
?assertMatch(#{<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||
]
|
||||
},
|
||||
StopRes
|
||||
),
|
||||
{ok, StopRes1} = update_plugin(NameVsn, "start"),
|
||||
?assertEqual([], StopRes1),
|
||||
{ok, StartRes} = describe_plugins(NameVsn),
|
||||
?assertMatch(#{<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}]}, StartRes),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"running">>}
|
||||
]
|
||||
},
|
||||
StartRes
|
||||
),
|
||||
{ok, []} = update_plugin(NameVsn, "stop"),
|
||||
{ok, StopRes2} = describe_plugins(NameVsn),
|
||||
?assertMatch(#{<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}]}, StopRes2),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"running_status">> := [
|
||||
#{<<"node">> := <<"test@127.0.0.1">>, <<"status">> := <<"stopped">>}
|
||||
]
|
||||
},
|
||||
StopRes2
|
||||
),
|
||||
{ok, []} = uninstall_plugin(NameVsn),
|
||||
ok.
|
||||
|
||||
|
@ -87,8 +105,16 @@ describe_plugins(Name) ->
|
|||
install_plugin(FilePath) ->
|
||||
{ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>),
|
||||
Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]),
|
||||
case emqx_mgmt_api_test_util:upload_request(Path, FilePath, "plugin",
|
||||
<<"application/gzip">>, [], Token) of
|
||||
case
|
||||
emqx_mgmt_api_test_util:upload_request(
|
||||
Path,
|
||||
FilePath,
|
||||
"plugin",
|
||||
<<"application/gzip">>,
|
||||
[],
|
||||
Token
|
||||
)
|
||||
of
|
||||
{ok, {{"HTTP/1.1", 200, "OK"}, _Headers, <<>>}} -> ok;
|
||||
Error -> Error
|
||||
end.
|
||||
|
@ -109,7 +135,6 @@ uninstall_plugin(Name) ->
|
|||
DeletePath = emqx_mgmt_api_test_util:api_path(["plugins", Name]),
|
||||
emqx_mgmt_api_test_util:request_api(delete, DeletePath).
|
||||
|
||||
|
||||
build_demo_plugin_package(Dir) ->
|
||||
#{package := Pkg} = emqx_plugins_SUITE:build_demo_plugin_package(),
|
||||
FileName = "emqx_plugin_template-" ++ ?EMQX_PLUGIN_TEMPLATE_VSN ++ ?PACKAGE_SUFFIX,
|
||||
|
|
|
@ -37,7 +37,9 @@ end_per_suite(_) ->
|
|||
emqx_mgmt_api_test_util:end_suite().
|
||||
|
||||
t_publish_api(_) ->
|
||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
||||
{ok, Client} = emqtt:start_link(#{
|
||||
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||
}),
|
||||
{ok, _} = emqtt:connect(Client),
|
||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||
|
@ -50,14 +52,16 @@ t_publish_api(_) ->
|
|||
emqtt:disconnect(Client).
|
||||
|
||||
t_publish_bulk_api(_) ->
|
||||
{ok, Client} = emqtt:start_link(#{username => <<"api_username">>, clientid => <<"api_clientid">>}),
|
||||
{ok, Client} = emqtt:start_link(#{
|
||||
username => <<"api_username">>, clientid => <<"api_clientid">>
|
||||
}),
|
||||
{ok, _} = emqtt:connect(Client),
|
||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC1),
|
||||
{ok, _, [0]} = emqtt:subscribe(Client, ?TOPIC2),
|
||||
Payload = <<"hello">>,
|
||||
Path = emqx_mgmt_api_test_util:api_path(["publish", "bulk"]),
|
||||
Auth = emqx_mgmt_api_test_util:auth_header_(),
|
||||
Body =[#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
||||
Body = [#{topic => ?TOPIC1, payload => Payload}, #{topic => ?TOPIC2, payload => Payload}],
|
||||
{ok, Response} = emqx_mgmt_api_test_util:request_api(post, Path, "", Auth, Body),
|
||||
ResponseMap = emqx_json:decode(Response, [return_maps]),
|
||||
?assertEqual(2, erlang:length(ResponseMap)),
|
||||
|
@ -68,12 +72,12 @@ t_publish_bulk_api(_) ->
|
|||
receive_assert(Topic, Qos, Payload) ->
|
||||
receive
|
||||
{publish, Message} ->
|
||||
ReceiveTopic = maps:get(topic, Message),
|
||||
ReceiveQos = maps:get(qos, Message),
|
||||
ReceivePayload = maps:get(payload, Message),
|
||||
?assertEqual(ReceiveTopic , Topic),
|
||||
?assertEqual(ReceiveQos , Qos),
|
||||
?assertEqual(ReceivePayload , Payload),
|
||||
ReceiveTopic = maps:get(topic, Message),
|
||||
ReceiveQos = maps:get(qos, Message),
|
||||
ReceivePayload = maps:get(payload, Message),
|
||||
?assertEqual(ReceiveTopic, Topic),
|
||||
?assertEqual(ReceiveQos, Qos),
|
||||
?assertEqual(ReceivePayload, Payload),
|
||||
ok
|
||||
after 5000 ->
|
||||
timeout
|
||||
|
|
|
@ -37,7 +37,7 @@ t_stats_api(_) ->
|
|||
SystemStats1 = emqx_mgmt:get_stats(),
|
||||
Fun1 =
|
||||
fun(Key) ->
|
||||
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
||||
?assertEqual(maps:get(Key, SystemStats1), maps:get(atom_to_binary(Key, utf8), Stats1))
|
||||
end,
|
||||
lists:foreach(Fun1, maps:keys(SystemStats1)),
|
||||
StatsPath = emqx_mgmt_api_test_util:api_path(["stats?aggregate=true"]),
|
||||
|
|
|
@ -58,13 +58,13 @@ t_subscription_api(_) ->
|
|||
fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) ->
|
||||
maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT)
|
||||
end,
|
||||
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
||||
[Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions),
|
||||
?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1),
|
||||
?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2),
|
||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID),
|
||||
?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID),
|
||||
|
||||
QS = uri_string:compose_query([
|
||||
QS = uri_string:compose_query([
|
||||
{"clientid", ?CLIENTID},
|
||||
{"topic", ?TOPIC2_TOPIC_ONLY},
|
||||
{"node", atom_to_list(node())},
|
||||
|
@ -83,11 +83,11 @@ t_subscription_api(_) ->
|
|||
?assertEqual(length(SubscriptionsList2), 1),
|
||||
|
||||
MatchQs = uri_string:compose_query([
|
||||
{"clientid", ?CLIENTID},
|
||||
{"node", atom_to_list(node())},
|
||||
{"qos", "0"},
|
||||
{"match_topic", "t/#"}
|
||||
]),
|
||||
{"clientid", ?CLIENTID},
|
||||
{"node", atom_to_list(node())},
|
||||
{"qos", "0"},
|
||||
{"match_topic", "t/#"}
|
||||
]),
|
||||
|
||||
{ok, MatchRes} = emqx_mgmt_api_test_util:request_api(get, Path, MatchQs, Headers),
|
||||
MatchData = emqx_json:decode(MatchRes, [return_maps]),
|
||||
|
|
|
@ -28,7 +28,6 @@ init_suite(Apps) ->
|
|||
application:load(emqx_management),
|
||||
emqx_common_test_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1).
|
||||
|
||||
|
||||
end_suite() ->
|
||||
end_suite([]).
|
||||
|
||||
|
@ -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, [])
|
||||
when (Method =:= options) orelse
|
||||
(Method =:= get) orelse
|
||||
(Method =:= put) orelse
|
||||
(Method =:= head) orelse
|
||||
(Method =:= delete) orelse
|
||||
(Method =:= trace) ->
|
||||
NewUrl = case QueryParams of
|
||||
"" -> Url;
|
||||
_ -> Url ++ "?" ++ QueryParams
|
||||
end,
|
||||
request_api(Method, Url, QueryParams, AuthOrHeaders, []) when
|
||||
(Method =:= options) orelse
|
||||
(Method =:= get) orelse
|
||||
(Method =:= put) orelse
|
||||
(Method =:= head) orelse
|
||||
(Method =:= delete) orelse
|
||||
(Method =:= trace)
|
||||
->
|
||||
NewUrl =
|
||||
case QueryParams of
|
||||
"" -> Url;
|
||||
_ -> Url ++ "?" ++ QueryParams
|
||||
end,
|
||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders)});
|
||||
request_api(Method, Url, QueryParams, AuthOrHeaders, Body)
|
||||
when (Method =:= post) orelse
|
||||
(Method =:= patch) orelse
|
||||
(Method =:= put) orelse
|
||||
(Method =:= delete) ->
|
||||
NewUrl = case QueryParams of
|
||||
"" -> Url;
|
||||
_ -> Url ++ "?" ++ QueryParams
|
||||
end,
|
||||
do_request_api(Method, {NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}).
|
||||
request_api(Method, Url, QueryParams, AuthOrHeaders, Body) when
|
||||
(Method =:= post) orelse
|
||||
(Method =:= patch) orelse
|
||||
(Method =:= put) orelse
|
||||
(Method =:= delete)
|
||||
->
|
||||
NewUrl =
|
||||
case QueryParams of
|
||||
"" -> Url;
|
||||
_ -> Url ++ "?" ++ QueryParams
|
||||
end,
|
||||
do_request_api(
|
||||
Method,
|
||||
{NewUrl, build_http_header(AuthOrHeaders), "application/json", emqx_json:encode(Body)}
|
||||
).
|
||||
|
||||
do_request_api(Method, Request)->
|
||||
do_request_api(Method, Request) ->
|
||||
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
|
||||
case httpc:request(Method, Request, [], []) of
|
||||
{error, socket_closed_remotely} ->
|
||||
{error, socket_closed_remotely};
|
||||
{ok, {{"HTTP/1.1", Code, _}, _, Return} }
|
||||
when Code >= 200 andalso Code =< 299 ->
|
||||
{ok, {{"HTTP/1.1", Code, _}, _, Return}} when
|
||||
Code >= 200 andalso Code =< 299
|
||||
->
|
||||
{ok, Return};
|
||||
{ok, {Reason, _, _} = Error} ->
|
||||
ct:pal("error: ~p~n", [Error]),
|
||||
|
@ -97,11 +104,10 @@ auth_header_() ->
|
|||
|
||||
build_http_header(X) when is_list(X) ->
|
||||
X;
|
||||
|
||||
build_http_header(X) ->
|
||||
[X].
|
||||
|
||||
api_path(Parts)->
|
||||
api_path(Parts) ->
|
||||
?SERVER ++ filename:join([?BASE_PATH | Parts]).
|
||||
|
||||
%% Usage:
|
||||
|
@ -117,20 +123,27 @@ api_path(Parts)->
|
|||
%% upload_request(<<"site.com/api/upload">>, <<"path/to/file.png">>,
|
||||
%% <<"upload">>, <<"image/png">>, RequestData, <<"some-token">>)
|
||||
-spec upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||
{ok, binary()} | {error, list()} when
|
||||
URL:: binary(),
|
||||
FilePath:: binary(),
|
||||
Name:: binary(),
|
||||
MimeType:: binary(),
|
||||
RequestData:: list(),
|
||||
AuthorizationToken:: binary().
|
||||
{ok, binary()} | {error, list()}
|
||||
when
|
||||
URL :: binary(),
|
||||
FilePath :: binary(),
|
||||
Name :: binary(),
|
||||
MimeType :: binary(),
|
||||
RequestData :: list(),
|
||||
AuthorizationToken :: binary().
|
||||
upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) ->
|
||||
Method = post,
|
||||
Filename = filename:basename(FilePath),
|
||||
{ok, Data} = file:read_file(FilePath),
|
||||
Boundary = emqx_guid:to_base62(emqx_guid:gen()),
|
||||
RequestBody = format_multipart_formdata(Data, RequestData, Name,
|
||||
[Filename], MimeType, Boundary),
|
||||
RequestBody = format_multipart_formdata(
|
||||
Data,
|
||||
RequestData,
|
||||
Name,
|
||||
[Filename],
|
||||
MimeType,
|
||||
Boundary
|
||||
),
|
||||
ContentType = "multipart/form-data; boundary=" ++ binary_to_list(Boundary),
|
||||
ContentLength = integer_to_list(length(binary_to_list(RequestBody))),
|
||||
Headers = [
|
||||
|
@ -146,34 +159,56 @@ upload_request(URL, FilePath, Name, MimeType, RequestData, AuthorizationToken) -
|
|||
httpc:request(Method, {URL, Headers, ContentType, RequestBody}, HTTPOptions, Options).
|
||||
|
||||
-spec format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||
binary() when
|
||||
Data:: binary(),
|
||||
Params:: list(),
|
||||
Name:: binary(),
|
||||
FileNames:: list(),
|
||||
MimeType:: binary(),
|
||||
Boundary:: binary().
|
||||
binary()
|
||||
when
|
||||
Data :: binary(),
|
||||
Params :: list(),
|
||||
Name :: binary(),
|
||||
FileNames :: list(),
|
||||
MimeType :: binary(),
|
||||
Boundary :: binary().
|
||||
format_multipart_formdata(Data, Params, Name, FileNames, MimeType, Boundary) ->
|
||||
StartBoundary = erlang:iolist_to_binary([<<"--">>, Boundary]),
|
||||
LineSeparator = <<"\r\n">>,
|
||||
WithParams = lists:foldl(fun({Key, Value}, Acc) ->
|
||||
erlang:iolist_to_binary([
|
||||
Acc,
|
||||
StartBoundary, LineSeparator,
|
||||
<<"Content-Disposition: form-data; name=\"">>, Key, <<"\"">>,
|
||||
LineSeparator, LineSeparator,
|
||||
Value, LineSeparator
|
||||
])
|
||||
end, <<"">>, Params),
|
||||
WithPaths = lists:foldl(fun(FileName, Acc) ->
|
||||
erlang:iolist_to_binary([
|
||||
Acc,
|
||||
StartBoundary, LineSeparator,
|
||||
<<"Content-Disposition: form-data; name=\"">>, Name, <<"\"; filename=\"">>,
|
||||
FileName, <<"\"">>, LineSeparator,
|
||||
<<"Content-Type: ">>, MimeType, LineSeparator, LineSeparator,
|
||||
Data,
|
||||
LineSeparator
|
||||
])
|
||||
end, WithParams, FileNames),
|
||||
WithParams = lists:foldl(
|
||||
fun({Key, Value}, Acc) ->
|
||||
erlang:iolist_to_binary([
|
||||
Acc,
|
||||
StartBoundary,
|
||||
LineSeparator,
|
||||
<<"Content-Disposition: form-data; name=\"">>,
|
||||
Key,
|
||||
<<"\"">>,
|
||||
LineSeparator,
|
||||
LineSeparator,
|
||||
Value,
|
||||
LineSeparator
|
||||
])
|
||||
end,
|
||||
<<"">>,
|
||||
Params
|
||||
),
|
||||
WithPaths = lists:foldl(
|
||||
fun(FileName, Acc) ->
|
||||
erlang:iolist_to_binary([
|
||||
Acc,
|
||||
StartBoundary,
|
||||
LineSeparator,
|
||||
<<"Content-Disposition: form-data; name=\"">>,
|
||||
Name,
|
||||
<<"\"; filename=\"">>,
|
||||
FileName,
|
||||
<<"\"">>,
|
||||
LineSeparator,
|
||||
<<"Content-Type: ">>,
|
||||
MimeType,
|
||||
LineSeparator,
|
||||
LineSeparator,
|
||||
Data,
|
||||
LineSeparator
|
||||
])
|
||||
end,
|
||||
WithParams,
|
||||
FileNames
|
||||
),
|
||||
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).
|
||||
|
|
|
@ -32,7 +32,9 @@ end_per_suite(_) ->
|
|||
|
||||
t_nodes_api(_) ->
|
||||
Topic = <<"test_topic">>,
|
||||
{ok, Client} = emqtt:start_link(#{username => <<"routes_username">>, clientid => <<"routes_cid">>}),
|
||||
{ok, Client} = emqtt:start_link(#{
|
||||
username => <<"routes_username">>, clientid => <<"routes_cid">>
|
||||
}),
|
||||
{ok, _} = emqtt:connect(Client),
|
||||
{ok, _, _} = emqtt:subscribe(Client, Topic),
|
||||
|
||||
|
|
|
@ -59,7 +59,9 @@ t_http_test(_Config) ->
|
|||
#{
|
||||
<<"code">> => <<"BAD_REQUEST">>,
|
||||
<<"message">> => <<"name : mandatory_required_field">>
|
||||
}, json(Body)),
|
||||
},
|
||||
json(Body)
|
||||
),
|
||||
|
||||
Name = <<"test-name">>,
|
||||
Trace = [
|
||||
|
@ -77,32 +79,47 @@ t_http_test(_Config) ->
|
|||
|
||||
%% update
|
||||
{ok, Update} = request_api(put, api_path("trace/test-name/stop"), Header, #{}),
|
||||
?assertEqual(#{<<"enable">> => false,
|
||||
<<"name">> => <<"test-name">>}, json(Update)),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"enable">> => false,
|
||||
<<"name">> => <<"test-name">>
|
||||
},
|
||||
json(Update)
|
||||
),
|
||||
|
||||
?assertMatch({error, {"HTTP/1.1", 404, _}, _},
|
||||
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 404, _}, _},
|
||||
request_api(put, api_path("trace/test-name-not-found/stop"), Header, #{})
|
||||
),
|
||||
{ok, List1} = request_api(get, api_path("trace"), Header),
|
||||
[Data1] = json(List1),
|
||||
Node = atom_to_binary(node()),
|
||||
?assertMatch(#{
|
||||
<<"status">> := <<"stopped">>,
|
||||
<<"name">> := <<"test-name">>,
|
||||
<<"log_size">> := #{Node := _},
|
||||
<<"start_at">> := _,
|
||||
<<"end_at">> := _,
|
||||
<<"type">> := <<"topic">>,
|
||||
<<"topic">> := <<"/x/y/z">>
|
||||
}, Data1),
|
||||
?assertMatch(
|
||||
#{
|
||||
<<"status">> := <<"stopped">>,
|
||||
<<"name">> := <<"test-name">>,
|
||||
<<"log_size">> := #{Node := _},
|
||||
<<"start_at">> := _,
|
||||
<<"end_at">> := _,
|
||||
<<"type">> := <<"topic">>,
|
||||
<<"topic">> := <<"/x/y/z">>
|
||||
},
|
||||
Data1
|
||||
),
|
||||
|
||||
%% delete
|
||||
{ok, Delete} = request_api(delete, api_path("trace/test-name"), Header),
|
||||
?assertEqual(<<>>, Delete),
|
||||
|
||||
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound}
|
||||
= request_api(delete, api_path("trace/test-name"), Header),
|
||||
?assertEqual(#{<<"code">> => <<"NOT_FOUND">>,
|
||||
<<"message">> => <<"test-name NOT FOUND">>}, json(DeleteNotFound)),
|
||||
{error, {"HTTP/1.1", 404, "Not Found"}, DeleteNotFound} =
|
||||
request_api(delete, api_path("trace/test-name"), Header),
|
||||
?assertEqual(
|
||||
#{
|
||||
<<"code">> => <<"NOT_FOUND">>,
|
||||
<<"message">> => <<"test-name NOT FOUND">>
|
||||
},
|
||||
json(DeleteNotFound)
|
||||
),
|
||||
|
||||
{ok, List2} = request_api(get, api_path("trace"), Header),
|
||||
?assertEqual([], json(List2)),
|
||||
|
@ -123,29 +140,43 @@ t_create_failed(_Config) ->
|
|||
Trace = [{<<"type">>, <<"topic">>}, {<<"topic">>, <<"/x/y/z">>}],
|
||||
|
||||
BadName1 = {<<"name">>, <<"test/bad">>},
|
||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [BadName1 | Trace])),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [BadName1 | Trace])
|
||||
),
|
||||
BadName2 = {<<"name">>, list_to_binary(lists:duplicate(257, "t"))},
|
||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [BadName2 | Trace])),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [BadName2 | Trace])
|
||||
),
|
||||
|
||||
%% already_exist
|
||||
GoodName = {<<"name">>, <<"test-name-0">>},
|
||||
{ok, Create} = request_api(post, api_path("trace"), Header, [GoodName | Trace]),
|
||||
?assertMatch(#{<<"name">> := <<"test-name-0">>}, json(Create)),
|
||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [GoodName | Trace])),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [GoodName | Trace])
|
||||
),
|
||||
|
||||
%% MAX Limited
|
||||
lists:map(fun(Seq) ->
|
||||
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
||||
Trace0 = [{name, Name0}, {type, topic},
|
||||
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}],
|
||||
{ok, _} = emqx_trace:create(Trace0)
|
||||
end, lists:seq(1, 30 - ets:info(emqx_trace, size))),
|
||||
lists:map(
|
||||
fun(Seq) ->
|
||||
Name0 = list_to_binary("name" ++ integer_to_list(Seq)),
|
||||
Trace0 = [
|
||||
{name, Name0},
|
||||
{type, topic},
|
||||
{topic, list_to_binary("/x/y/" ++ integer_to_list(Seq))}
|
||||
],
|
||||
{ok, _} = emqx_trace:create(Trace0)
|
||||
end,
|
||||
lists:seq(1, 30 - ets:info(emqx_trace, size))
|
||||
),
|
||||
GoodName1 = {<<"name">>, <<"test-name-1">>},
|
||||
?assertMatch({error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])),
|
||||
?assertMatch(
|
||||
{error, {"HTTP/1.1", 400, _}, _},
|
||||
request_api(post, api_path("trace"), Header, [GoodName1 | Trace])
|
||||
),
|
||||
unload(),
|
||||
emqx_trace:clear(),
|
||||
ok.
|
||||
|
@ -158,14 +189,23 @@ t_download_log(_Config) ->
|
|||
create_trace(Name, ClientId, Now),
|
||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||
{ok, _} = emqtt:connect(Client),
|
||||
[begin _ = emqtt:ping(Client) end ||_ <- lists:seq(1, 5)],
|
||||
[
|
||||
begin
|
||||
_ = emqtt:ping(Client)
|
||||
end
|
||||
|| _ <- lists:seq(1, 5)
|
||||
],
|
||||
ok = emqx_trace_handler_SUITE:filesync(Name, clientid),
|
||||
Header = auth_header_(),
|
||||
{ok, Binary} = request_api(get, api_path("trace/test_client_id/download"), Header),
|
||||
{ok, [_Comment,
|
||||
#zip_file{name = ZipName,
|
||||
info = #file_info{size = Size, type = regular, access = read_write}}]}
|
||||
= zip:table(Binary),
|
||||
{ok, [
|
||||
_Comment,
|
||||
#zip_file{
|
||||
name = ZipName,
|
||||
info = #file_info{size = Size, type = regular, access = read_write}
|
||||
}
|
||||
]} =
|
||||
zip:table(Binary),
|
||||
?assert(Size > 0),
|
||||
ZipNamePrefix = lists:flatten(io_lib:format("~s-trace_~s", [node(), Name])),
|
||||
?assertNotEqual(nomatch, re:run(ZipName, [ZipNamePrefix])),
|
||||
|
@ -176,13 +216,18 @@ create_trace(Name, ClientId, Start) ->
|
|||
?check_trace(
|
||||
#{timetrap => 900},
|
||||
begin
|
||||
{ok, _} = emqx_trace:create([{<<"name">>, Name},
|
||||
{<<"type">>, clientid}, {<<"clientid">>, ClientId}, {<<"start_at">>, Start}]),
|
||||
{ok, _} = emqx_trace:create([
|
||||
{<<"name">>, Name},
|
||||
{<<"type">>, clientid},
|
||||
{<<"clientid">>, ClientId},
|
||||
{<<"start_at">>, Start}
|
||||
]),
|
||||
?block_until(#{?snk_kind := update_trace_done})
|
||||
end,
|
||||
fun(Trace) ->
|
||||
?assertMatch([#{}], ?of_kind(update_trace_done, Trace))
|
||||
end).
|
||||
end
|
||||
).
|
||||
|
||||
t_stream_log(_Config) ->
|
||||
application:set_env(emqx, allow_anonymous, true),
|
||||
|
@ -194,7 +239,12 @@ t_stream_log(_Config) ->
|
|||
create_trace(Name, ClientId, Now - 10),
|
||||
{ok, Client} = emqtt:start_link([{clean_start, true}, {clientid, ClientId}]),
|
||||
{ok, _} = emqtt:connect(Client),
|
||||
[begin _ = emqtt:ping(Client) end || _ <- lists:seq(1, 5)],
|
||||
[
|
||||
begin
|
||||
_ = emqtt:ping(Client)
|
||||
end
|
||||
|| _ <- lists:seq(1, 5)
|
||||
],
|
||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood1">>, [{qos, 0}]),
|
||||
emqtt:publish(Client, <<"/good">>, #{}, <<"ghood2">>, [{qos, 0}]),
|
||||
ok = emqtt:disconnect(Client),
|
||||
|
@ -239,8 +289,9 @@ do_request_api(Method, Request) ->
|
|||
{error, socket_closed_remotely};
|
||||
{error, {shutdown, server_closed}} ->
|
||||
{error, server_closed};
|
||||
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}}
|
||||
when Code =:= 200 orelse Code =:= 201 orelse Code =:= 204 ->
|
||||
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when
|
||||
Code =:= 200 orelse Code =:= 201 orelse Code =:= 204
|
||||
->
|
||||
{ok, Return};
|
||||
{ok, {Reason, _Header, Body}} ->
|
||||
{error, Reason, Body}
|
||||
|
@ -250,7 +301,8 @@ api_path(Path) ->
|
|||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, Path]).
|
||||
|
||||
json(Data) ->
|
||||
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.
|
||||
{ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]),
|
||||
Jsx.
|
||||
|
||||
load() ->
|
||||
emqx_trace:start_link().
|
||||
|
|
Loading…
Reference in New Issue