chore: reformat mgmt code.

This commit is contained in:
Zhongwen Deng 2022-04-19 14:02:39 +08:00
parent 5661948a86
commit aa7807baeb
40 changed files with 3082 additions and 1843 deletions

View File

@ -1,20 +1,28 @@
%% -*- mode: erlang -*-
{deps, [ {emqx, {path, "../emqx"}}
]}.
{deps, [{emqx, {path, "../emqx"}}]}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
{erl_opts, [
warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
warnings_as_errors,
debug_info,
{parse_transform}]}.
{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]}.

View File

@ -1,15 +1,17 @@
%% -*- mode: erlang -*-
{application, emqx_management,
[{description, "EMQX Management API and CLI"},
{vsn, "5.0.0"}, % strict semver, bump manually!
{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,[]}},
{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/"},
{links, [
{"Homepage", "https://emqx.io/"},
{"Github", "https://github.com/emqx/emqx-management"}
]}
]}.
]}.

View File

@ -19,9 +19,11 @@
-behaviour(hocon_schema).
-export([ namespace/0
, roots/0
, fields/1]).
-export([
namespace/0,
roots/0,
fields/1
]).
namespace() -> management.

View File

@ -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).
@ -116,7 +122,8 @@ node_info() ->
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(),
Info#{
node => node(),
otp_release => otp_rel(),
memory_total => proplists:get_value(allocated, Memory),
memory_used => proplists:get_value(used, Memory),
@ -124,7 +131,8 @@ node_info() ->
process_used => erlang:system_info(process_count),
max_fds => proplists:get_value(
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))),
max_fds, lists:usort(lists:flatten(erlang:system_info(check_io)))
),
connections => ets:info(emqx_channel, size),
node_status => 'Running',
uptime => proplists:get_value(uptime, BrokerInfo),
@ -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}) ->
{error, Err} ->
{error, Err};
L ->
lists:map(
fun({Chan, Info0, Stats}) ->
Info = Info0#{node => Node},
M:F({Chan, Info, Stats})
end,
L)
L
)
end.
kickout_client({ClientID, FormatFun}) ->
@ -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;
Expected = lists:filter(
fun
({error, _}) -> false;
(_) -> true
end, Results),
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,17 +344,23 @@ 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
@ -354,10 +384,8 @@ 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}.
@ -366,8 +394,7 @@ subscribe([], _ClientId, _TopicTables) ->
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: ???
@ -395,8 +422,7 @@ unsubscribe([], _ClientId, _Topic) ->
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]).
%%--------------------------------------------------------------------

View File

@ -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]).
@ -53,27 +55,27 @@ do_paginate(Qh, Count, Params, {Module, FormatFun}) ->
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]).
@ -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
<<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,8 +380,7 @@ 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),
@ -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},
QSchema = [
{<<"str">>, binary},
{<<"int">>, integer},
{<<"atom">>, atom},
{<<"ts">>, timestamp},
{<<"gte_range">>, integer},
{<<"lte_range">>, integer},
{<<"like_fuzzy">>, binary},
{<<"match_topic">>, binary}],
QString = [{<<"str">>, <<"abc">>},
{<<"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">>},
{<<"match_topic">>, <<"t/#">>}
],
ExpectedQs = [
{str, '=:=', <<"abc">>},
{int, '=:=', 123},
{atom, '=:=', connected},
{ts, '=:=', 156000},
{range, '>=', 1, '=<', 5}
],
FuzzyNQString = [{fuzzy, like, <<"user">>},
{topic, match, <<"t/#">>}],
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) ->

View File

@ -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,
{activated,
hoconsc:mk(boolean(), #{
in => query,
desc => ?DESC(get_alarms_qs_activated),
required => false})}
required => false
})}
],
responses => #{
200 => [
@ -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).

View File

@ -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-_]",
{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 => <<"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",
}
)},
{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})},
}
)},
{desc,
hoconsc:mk(
binary(),
#{example => <<"Note">>, required => false}
)},
{enable, hoconsc:mk(boolean(), #{desc => "Enable/Disable", required => false})}
];
fields(name) ->
[{name, hoconsc:mk(binary(),
[
{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.

View File

@ -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).
@ -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), #{})}
]
@ -70,7 +73,8 @@ schema("/banned") ->
200 => [{data, hoconsc:mk(hoconsc:array(hoconsc:ref(ban)), #{})}],
400 => emqx_dashboard_swagger:error_codes(
['ALREADY_EXISTS', 'BAD_REQUEST'],
?DESC(create_banned_api_response400))
?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), #{
{as,
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
desc => ?DESC(as),
in => path,
example => username})},
{who, hoconsc:mk(binary(), #{
example => username
})},
{who,
hoconsc:mk(binary(), #{
desc => ?DESC(who),
in => path,
example => <<"Badass">>})}
example => <<"Badass">>
})}
],
responses => #{
204 => <<"Delete banned success">>,
404 => emqx_dashboard_swagger:error_codes(
['NOT_FOUND'],
?DESC(delete_banned_api_response404))
?DESC(delete_banned_api_response404)
)
}
}
}.
fields(ban) ->
[
{as, hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
{as,
hoconsc:mk(hoconsc:enum(?BANNED_TYPES), #{
desc => ?DESC(as),
required => true,
example => username})},
{who, hoconsc:mk(binary(), #{
example => username
})},
{who,
hoconsc:mk(binary(), #{
desc => ?DESC(who),
required => true,
example => <<"Banned name"/utf8>>})},
{by, hoconsc:mk(binary(), #{
example => <<"Banned name"/utf8>>
})},
{by,
hoconsc:mk(binary(), #{
desc => ?DESC(by),
required => false,
example => <<"mgmt_api">>})},
{reason, hoconsc:mk(binary(), #{
example => <<"mgmt_api">>
})},
{reason,
hoconsc:mk(binary(), #{
desc => ?DESC(reason),
required => false,
example => <<"Too many requests">>})},
{at, hoconsc:mk(emqx_datetime:epoch_second(), #{
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(), #{
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">>})
}
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.

View File

@ -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(), #{
{node,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Node name">>,
example => atom_to_list(node())})},
{username, hoconsc:mk(binary(), #{
example => atom_to_list(node())
})},
{username,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"User name">>})},
{zone, hoconsc:mk(binary(), #{
desc => <<"User name">>
})},
{zone,
hoconsc:mk(binary(), #{
in => query,
required => false})},
{ip_address, hoconsc:mk(binary(), #{
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]), #{
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(), #{
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']), #{
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(), #{
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(), #{
desc => <<"Client protocol version">>
})},
{like_clientid,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Fuzzy search `clientid` as substring">>})},
{like_username, hoconsc:mk(binary(), #{
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(), #{
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(), #{
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(), #{
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(), #{
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)">>})}
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 =>
{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">>})},
" 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
[
begin
Topic = maps:get(<<"topic">>, TopicInfo),
Qos = maps:get(<<"qos">>, TopicInfo, 0),
#{topic => Topic, qos => Qos}
end || TopicInfo <- TopicInfos],
end
|| TopicInfo <- TopicInfos
],
subscribe_batch(#{clientid => ClientID, topics => Topics}).
subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
@ -441,11 +580,12 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) ->
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,15 +594,25 @@ set_keepalive(put, #{bindings := #{clientid := ClientID}, body := Body}) ->
%% api apply
list_clients(QString) ->
Result = case maps:get(<<"node">>, QString, undefined) of
Result =
case maps:get(<<"node">>, QString, undefined) of
undefined ->
emqx_mgmt_api:cluster_query(QString, ?CLIENT_QTAB,
?CLIENT_QSCHEMA, ?QUERY_FUN);
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)
emqx_mgmt_api:node_query(
Node1,
QStringWithoutNode,
?CLIENT_QTAB,
?CLIENT_QSCHEMA,
?QUERY_FUN
)
end,
case Result of
{error, 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};
@ -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,14 +798,17 @@ 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
Val =
case maps:get(Key, ClientInfo, <<>>) of
undefined -> <<>>;
V -> V
end,
@ -654,12 +818,15 @@ run_fuzzy_filter(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, like, SubStr} |
%% format funcs
format_channel_info({_, ClientInfo, ClientStats}) ->
Node = case ClientInfo of
Node =
case ClientInfo of
#{node := N} -> N;
_ -> node()
end,
StatsMap = maps:without([memory, next_pkt_id, total_heap_size],
maps:from_list(ClientStats)),
StatsMap = maps:without(
[memory, next_pkt_id, total_heap_size],
maps:from_list(ClientStats)
),
ClientInfoMap0 = maps:fold(fun take_maps_from_inner/3, #{}, ClientInfo),
{IpAddress, Port} = peername_dispart(maps:get(peername, ClientInfoMap0)),
Connected = maps:get(conn_state, ClientInfoMap0) =:= connected,
@ -669,36 +836,40 @@ format_channel_info({_, ClientInfo, ClientStats}) ->
ClientInfoMap4 = maps:put(port, Port, ClientInfoMap3),
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,19 +881,21 @@ 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,
#{
access => PubSub,
topic => Topic,
result => AuthzResult,
updated_time => Timestamp

View File

@ -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).

View File

@ -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,7 +38,8 @@
-define(ERR_MSG(MSG), list_to_binary(io_lib:format("~p", [MSG]))).
-define(OPTS, #{rawconf_with_defaults => true, override_to => cluster}).
-define(EXCLUDES, [
-define(EXCLUDES,
[
<<"exhook">>,
<<"gateway">>,
<<"plugins">>,
@ -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">>,
{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'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) ->
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}),
?SLOG(error, #{
msg => "update global zone failed",
reason => Reason,
path => Path,
value => Value
}),
Acc
end
end, #{}, Body),
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,8 +259,7 @@ config_reset(post, _Params, Req) ->
configs(get, Params, _Req) ->
Node = maps:get(node, Params, node()),
case
lists:member(Node, mria_mnesia:running_nodes())
andalso
lists:member(Node, mria_mnesia:running_nodes()) andalso
emqx_management_proto_v1:get_full_config(Node)
of
false ->
@ -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

View File

@ -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,20 +69,31 @@ 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))
]
)
}
}
}.
@ -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) ->

View File

@ -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,54 +51,76 @@ 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()
}
}
}.
@ -105,67 +129,105 @@ schema("/nodes/:node/stats") ->
%% 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
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}.
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(),

View File

@ -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,7 +68,8 @@ schema("/plugins") ->
#{
'operationId' => list_plugins,
get => #{
description => "List all install plugins.<br>"
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 => #{
@ -77,7 +81,8 @@ schema("/plugins/install") ->
#{
'operationId' => upload_install,
post => #{
description => "Install a plugin(plugin-vsn.tar.gz)."
description =>
"Install a plugin(plugin-vsn.tar.gz)."
"Follow [emqx-plugin-template](https://github.com/emqx/emqx-plugin-template) "
"to develop plugin.",
'requestBody' => #{
@ -86,11 +91,16 @@ schema("/plugins/install") ->
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>"
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(),
{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"})
},
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(), #{
{git_commit_or_build_date,
hoconsc:mk(string(), #{
example => "2021-12-25",
desc => "Last git commit date by `git log -1 --pretty=format:'%cd' "
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",
{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.",
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">>]})
},
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.",
{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 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})}
required => false
})}
];
fields(name) ->
[{name, hoconsc:mk(binary(),
[
{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()]),
[
{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.
""",
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>"
{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}) ->
lists:foldl(
fun({_Node, PluginList}, {Acc, StatusAcc}) ->
pack_plugin_in_order(PluginList, Acc, StatusAcc)
end, {[], StatusMap}, List),
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) ->
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),
end,
Acc,
Plugins
),
aggregate_status(List, NewAcc).
% running_status: running loaded, stopped

View File

@ -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(), #{
{topic,
hoconsc:mk(binary(), #{
desc => <<"Topic Name">>,
required => true,
example => <<"api/example/topic">>})},
{qos, hoconsc:mk(emqx_schema:qos(), #{
example => <<"api/example/topic">>
})},
{qos,
hoconsc:mk(emqx_schema:qos(), #{
desc => <<"MQTT QoS">>,
required => false,
default => 0})},
{from, hoconsc:mk(binary(), #{
default => 0
})},
{from,
hoconsc:mk(binary(), #{
desc => <<"From client ID">>,
required => false,
example => <<"api_example_client">>})},
{payload, hoconsc:mk(binary(), #{
example => <<"api_example_client">>
})},
{payload,
hoconsc:mk(binary(), #{
desc => <<"MQTT Payload">>,
required => true,
example => <<"hello emqx api">>})},
{retain, hoconsc:mk(boolean(), #{
example => <<"hello emqx api">>
})},
{retain,
hoconsc:mk(boolean(), #{
desc => <<"MQTT Retain Message">>,
required => false,
default => 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}) ->
@ -110,9 +123,11 @@ message(Map) ->
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,

View File

@ -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.

View File

@ -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,17 +32,25 @@ 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">>
}
}
}
}

View File

@ -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(), #{
node,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Node name">>,
example => atom_to_list(node())})
example => atom_to_list(node())
})
},
{
clientid, hoconsc:mk(binary(), #{
clientid,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Client ID">>})
desc => <<"Client ID">>
})
},
{
qos, hoconsc:mk(emqx_schema:qos(), #{
qos,
hoconsc:mk(emqx_schema:qos(), #{
in => query,
required => false,
desc => <<"QoS">>})
desc => <<"QoS">>
})
},
{
topic, hoconsc:mk(binary(), #{
topic,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Topic, url encoding">>})
desc => <<"Topic, url encoding">>
})
},
{
match_topic, hoconsc:mk(binary(), #{
match_topic,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Match topic string, url encoding">>})
desc => <<"Match topic string, url encoding">>
})
},
{
share_group, hoconsc:mk(binary(), #{
share_group,
hoconsc:mk(binary(), #{
in => query,
required => false,
desc => <<"Shared subscription group name">>})
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(_, []) ->

View File

@ -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(), #{
{topic,
hoconsc:mk(binary(), #{
desc => <<"Topic Name">>,
required => true})},
{node, hoconsc:mk(binary(), #{
required => true
})},
{node,
hoconsc:mk(binary(), #{
desc => <<"Node">>,
required => true})}
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,

View File

@ -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,7 +119,8 @@ schema("/trace/:name/download") ->
parameters => [hoconsc:ref(name)],
responses => #{
200 =>
#{description => "A trace zip file",
#{
description => "A trace zip file",
content => #{
'application/octet-stream' =>
#{schema => #{type => "string", format => "binary"}}
@ -144,82 +152,141 @@ schema("/trace/:name/log") ->
fields(trace) ->
[
{name, hoconsc:mk(binary(),
#{desc => "Unique and format by [a-zA-Z0-9-_]",
{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""",
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.""",
example => <<"clientid">>
}
)},
{topic,
hoconsc:mk(
binary(),
#{
desc => "" "support mqtt wildcard topic." "",
required => false,
example => <<"/dev/#">>})},
{clientid, hoconsc:mk(binary(),
#{desc => """mqtt clientid.""",
example => <<"/dev/#">>
}
)},
{clientid,
hoconsc:mk(
binary(),
#{
desc => "" "mqtt clientid." "",
required => false,
example => <<"dev-001">>})},
example => <<"dev-001">>
}
)},
%% TODO add ip_address type in emqx_schema.erl
{ip_address, hoconsc:mk(binary(),
#{desc => "client ip address",
{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",
}
)},
{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",
}
)},
{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",
}
)},
{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",
}
)},
{log_size,
hoconsc:mk(
hoconsc:array(map()),
#{
desc => "trace log size",
example => [#{<<"node">> => <<"emqx@127.0.0.1">>, <<"size">> => 1024}],
required => false})}
required => false
}
)}
];
fields(name) ->
[{name, hoconsc:mk(binary(),
[
{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(),
[
{node,
hoconsc:mk(
binary(),
#{
desc => "Node name",
in => query,
required => false
})}];
}
)}
];
fields(bytes) ->
[{bytes, hoconsc:mk(integer(),
[
{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(),
[
{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}) ->
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)
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),
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,11 +427,13 @@ 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) ->
lists:foldl(
fun(Res, Acc) ->
case Res of
{ok, Node, Bin} ->
FileName = Node ++ "-" ++ TraceLog,
@ -348,11 +443,18 @@ group_trace_file(ZipDir, TraceLog, TraceFiles) ->
_ -> Acc
end;
{error, Node, Reason} ->
?SLOG(error, #{msg => "download_trace_log_error", node => Node,
log => TraceLog, reason => Reason}),
?SLOG(error, #{
msg => "download_trace_log_error",
node => Node,
log => TraceLog,
reason => Reason
}),
Acc
end
end, [], TraceFiles).
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) ->
lists:foldl(
fun(File, Acc) ->
FullFileName = filename:join(TraceDir, File),
Acc#{{Node, File} => filelib:file_size(FullFileName)}
end, #{}, lists:delete("zip", AllFiles));
_ -> #{}
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()}
-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 _:_ ->
try
{ok, binary_to_existing_atom(Node)}
catch
_:_ ->
{error, not_found}
end.
collect_file_size(Nodes, FileName, AllFiles) ->
lists:foldl(fun(Node, Acc) ->
lists:foldl(
fun(Node, Acc) ->
Size = maps:get({Node, FileName}, AllFiles, 0),
Acc#{Node => Size}
end, #{}, Nodes).
end,
#{},
Nodes
).
status(false, _Start, _End, _Now) -> <<"stopped">>;
status(true, Start, _End, Now) when Now < Start -> <<"waiting">>;

View File

@ -20,9 +20,10 @@
-define(APP, emqx_management).
-export([ start/2
, stop/1
]).
-export([
start/2,
stop/1
]).
-include("emqx_mgmt.hrl").

View File

@ -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
@ -67,7 +69,8 @@ read(Name) ->
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{
@ -85,15 +88,18 @@ delete(Name) ->
Fun = fun() ->
case mnesia:read(?APP, Name) of
[] -> mnesia:abort(not_found);
[_App] -> mnesia:delete({?APP, Name}) 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,9 +108,12 @@ 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) ->
@ -112,18 +121,22 @@ find_by_api_key(ApiKey) ->
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{}) ->
lists:map(
fun(Trace0 = #?APP{}) ->
[_ | Values] = tuple_to_list(Trace0),
maps:remove(api_secret_hash, maps:from_list(lists:zip(Fields, Values)))
end, Apps);
end,
Apps
);
to_map(App0) ->
[App] = to_map([App0]),
App.
@ -149,13 +162,15 @@ 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).

View File

@ -27,4 +27,3 @@ start_link() ->
init([]) ->
{ok, {{one_for_one, 1, 5}, []}}.

View File

@ -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) ->
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}
OldV when
is_map(OldV),
is_map(V)
->
Acc#{K => merge_maps(OldV, V)};
_ ->
Acc#{K => V}
end
end, Default, New).
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},
properties => properties([
{page, integer},
{limit, integer},
{count, 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,10 +187,12 @@ 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">>,
@ -181,7 +202,13 @@ batch_schema(DefName) when is_binary(DefName) ->
#{
data => minirest:ref(DefName),
reason => #{
type => <<"string">>}}}}}},
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,25 +251,63 @@ 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([{Key, {array, object}, Props1} | Props], Acc) ->
properties(Props, maps:put(Key, #{type => array,
items => #{type => object,
properties(
Props,
maps:put(
Key,
#{
type => object,
properties => properties(Props1)
}}, Acc));
},
Acc
)
);
properties([{Key, {array, object}, Props1} | Props], 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,
properties(
Props,
maps:put(
Key,
#{
type => array,
items => #{type => Type},
description => Desc}, Acc));
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,
properties(
Props,
maps:put(
Key,
#{
type => Type,
description => Desc,
enum => Enum}, Acc)).
enum => Enum
},
Acc
)
).
page_params() ->
[#{
[
#{
name => page,
in => query,
description => <<"Page">>,
@ -253,7 +318,8 @@ page_params() ->
in => query,
description => <<"Page size">>,
schema => #{type => integer, default => emqx_mgmt:max_row_limit()}
}].
}
].
bad_request() ->
bad_request(<<"Bad Request">>).

View File

@ -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").

View File

@ -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").
@ -37,8 +38,9 @@ get_trace_size(Nodes) ->
-spec trace_file([node()], file:name_all()) ->
emqx_rpc:multicall_result(
{ok, Node :: list(), Binary :: binary()} |
{error, Node :: list(), Reason :: term()}).
{ok, Node :: list(), Binary :: binary()}
| {error, Node :: list(), Reason :: term()}
).
trace_file(Nodes, File) ->
rpc:multicall(Nodes, emqx_trace, trace_file, [File], 60000).

View File

@ -15,7 +15,6 @@
%%--------------------------------------------------------------------
-module(emqx_mgmt_api_alarms_SUITE).
-compile(export_all).
-compile(nowarn_export_all).

View File

@ -22,7 +22,8 @@
all() -> [{group, parallel}, {group, sequence}].
suite() -> [{timetrap, {minutes, 1}}].
groups() -> [
groups() ->
[
{parallel, [parallel], [t_create, t_update, t_delete, t_authorize, t_create_unexpired_app]},
{sequence, [], [t_create_failed]}
].
@ -37,13 +38,18 @@ end_per_suite(_) ->
t_create(_Config) ->
Name = <<"EMQX-API-KEY-1">>,
{ok, Create} = create_app(Name),
?assertMatch(#{<<"api_key">> := _,
?assertMatch(
#{
<<"api_key">> := _,
<<"api_secret">> := _,
<<"created_at">> := _,
<<"desc">> := _,
<<"enable">> := true,
<<"expired_at">> := _,
<<"name">> := Name}, Create),
<<"name">> := Name
},
Create
),
{ok, List} = list_app(),
[App] = lists:filter(fun(#{<<"name">> := NameA}) -> NameA =:= Name end, List),
?assertEqual(false, maps:is_key(<<"api_secret">>, App)),
@ -64,9 +70,12 @@ t_create_failed(_Config) ->
{ok, List} = list_app(),
CreateNum = 30 - erlang:length(List),
Names = lists:map(fun(Seq) ->
Names = lists:map(
fun(Seq) ->
<<"EMQX-API-FAILED-KEY-", (integer_to_binary(Seq))/binary>>
end, lists:seq(1, CreateNum)),
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

View File

@ -47,13 +47,17 @@ t_create(_Config) ->
until => Until
},
{ok, ClientIdBannedRes} = create_banned(ClientIdBanned),
?assertEqual(#{<<"as">> => As,
?assertEqual(
#{
<<"as">> => As,
<<"at">> => At,
<<"by">> => By,
<<"reason">> => Reason,
<<"until">> => Until,
<<"who">> => ClientId
}, ClientIdBannedRes),
},
ClientIdBannedRes
),
PeerHost = <<"192.168.2.13">>,
PeerHostBanned = #{
as => <<"peerhost">>,
@ -64,13 +68,17 @@ t_create(_Config) ->
until => Until
},
{ok, PeerHostBannedRes} = create_banned(PeerHostBanned),
?assertEqual(#{<<"as">> => <<"peerhost">>,
?assertEqual(
#{
<<"as">> => <<"peerhost">>,
<<"at">> => At,
<<"by">> => By,
<<"reason">> => Reason,
<<"until">> => Until,
<<"who">> => PeerHost
}, PeerHostBannedRes),
},
PeerHostBannedRes
),
{ok, #{<<"data">> := List}} = list_banned(),
Bans = lists:sort(lists:map(fun(#{<<"who">> := W, <<"as">> := A}) -> {A, W} end, List)),
?assertEqual([{<<"clientid">>, ClientId}, {<<"peerhost">>, PeerHost}], Bans),
@ -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() ->

View File

@ -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)),
@ -129,8 +149,11 @@ t_query_clients_with_time(_) ->
%% 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="],
@ -141,21 +164,32 @@ t_query_clients_with_time(_) ->
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],
[
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.

View File

@ -32,10 +32,13 @@ end_per_suite(_) ->
t_get(_Config) ->
{ok, Configs} = get_configs(),
maps:map(fun(Name, Value) ->
maps:map(
fun(Name, Value) ->
{ok, Config} = get_config(Name),
?assertEqual(Value, Config)
end, maps:remove(<<"license">>, Configs)),
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

View File

@ -40,13 +40,14 @@ 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}) ->
fun(Key, {SysMetrics, APIMetrics}) ->
Value = maps:get(Key, SysMetrics),
?assertEqual(Value, maps:get(to_bin(Key), APIMetrics)),
{Value, {SysMetrics, APIMetrics}}

View File

@ -67,7 +67,8 @@ 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())]),
@ -75,11 +76,12 @@ t_log_path(_) ->
#{<<"log_path">> := Path} = emqx_json:decode(NodeInfo, [return_maps]),
?assertEqual(
<<"emqx-test.log">>,
filename:basename(Path)).
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)
).

View File

@ -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,

View File

@ -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)),
@ -71,9 +75,9 @@ receive_assert(Topic, Qos, 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),
?assertEqual(ReceiveTopic, Topic),
?assertEqual(ReceiveQos, Qos),
?assertEqual(ReceivePayload, Payload),
ok
after 5000 ->
timeout

View File

@ -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
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
(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
request_api(Method, Url, QueryParams, AuthOrHeaders, Body) when
(Method =:= post) orelse
(Method =:= patch) orelse
(Method =:= put) orelse
(Method =:= delete) ->
NewUrl = case QueryParams of
(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,
{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) ->
WithParams = lists:foldl(
fun({Key, Value}, Acc) ->
erlang:iolist_to_binary([
Acc,
StartBoundary, LineSeparator,
<<"Content-Disposition: form-data; name=\"">>, Key, <<"\"">>,
LineSeparator, LineSeparator,
Value, LineSeparator
StartBoundary,
LineSeparator,
<<"Content-Disposition: form-data; name=\"">>,
Key,
<<"\"">>,
LineSeparator,
LineSeparator,
Value,
LineSeparator
])
end, <<"">>, Params),
WithPaths = lists:foldl(fun(FileName, Acc) ->
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,
StartBoundary,
LineSeparator,
<<"Content-Disposition: form-data; name=\"">>,
Name,
<<"\"; filename=\"">>,
FileName,
<<"\"">>,
LineSeparator,
<<"Content-Type: ">>,
MimeType,
LineSeparator,
LineSeparator,
Data,
LineSeparator
])
end, WithParams, FileNames),
end,
WithParams,
FileNames
),
erlang:iolist_to_binary([WithPaths, StartBoundary, <<"--">>, LineSeparator]).

View File

@ -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),

View File

@ -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,15 +79,23 @@ 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(#{
?assertMatch(
#{
<<"status">> := <<"stopped">>,
<<"name">> := <<"test-name">>,
<<"log_size">> := #{Node := _},
@ -93,16 +103,23 @@ t_http_test(_Config) ->
<<"end_at">> := _,
<<"type">> := <<"topic">>,
<<"topic">> := <<"/x/y/z">>
}, Data1),
},
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) ->
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))}],
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))),
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().