chore(exhook): reformat exhook codes

This commit is contained in:
firest 2022-04-13 17:30:37 +08:00
parent cfb6c4b0be
commit 1a4afabe9f
19 changed files with 2299 additions and 1613 deletions

View File

@ -21,27 +21,27 @@
-define(HOOKS_REF_COUNTER, emqx_exhook_ref_counter). -define(HOOKS_REF_COUNTER, emqx_exhook_ref_counter).
-define(HOOKS_METRICS, emqx_exhook_metrics). -define(HOOKS_METRICS, emqx_exhook_metrics).
-define(ENABLED_HOOKS, -define(ENABLED_HOOKS, [
[ {'client.connect', {emqx_exhook_handler, on_client_connect, []}} {'client.connect', {emqx_exhook_handler, on_client_connect, []}},
, {'client.connack', {emqx_exhook_handler, on_client_connack, []}} {'client.connack', {emqx_exhook_handler, on_client_connack, []}},
, {'client.connected', {emqx_exhook_handler, on_client_connected, []}} {'client.connected', {emqx_exhook_handler, on_client_connected, []}},
, {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}} {'client.disconnected', {emqx_exhook_handler, on_client_disconnected, []}},
, {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}} {'client.authenticate', {emqx_exhook_handler, on_client_authenticate, []}},
, {'client.authorize', {emqx_exhook_handler, on_client_authorize, []}} {'client.authorize', {emqx_exhook_handler, on_client_authorize, []}},
, {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}} {'client.subscribe', {emqx_exhook_handler, on_client_subscribe, []}},
, {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}} {'client.unsubscribe', {emqx_exhook_handler, on_client_unsubscribe, []}},
, {'session.created', {emqx_exhook_handler, on_session_created, []}} {'session.created', {emqx_exhook_handler, on_session_created, []}},
, {'session.subscribed', {emqx_exhook_handler, on_session_subscribed, []}} {'session.subscribed', {emqx_exhook_handler, on_session_subscribed, []}},
, {'session.unsubscribed',{emqx_exhook_handler, on_session_unsubscribed, []}} {'session.unsubscribed', {emqx_exhook_handler, on_session_unsubscribed, []}},
, {'session.resumed', {emqx_exhook_handler, on_session_resumed, []}} {'session.resumed', {emqx_exhook_handler, on_session_resumed, []}},
, {'session.discarded', {emqx_exhook_handler, on_session_discarded, []}} {'session.discarded', {emqx_exhook_handler, on_session_discarded, []}},
, {'session.takenover', {emqx_exhook_handler, on_session_takenover, []}} {'session.takenover', {emqx_exhook_handler, on_session_takenover, []}},
, {'session.terminated', {emqx_exhook_handler, on_session_terminated, []}} {'session.terminated', {emqx_exhook_handler, on_session_terminated, []}},
, {'message.publish', {emqx_exhook_handler, on_message_publish, []}} {'message.publish', {emqx_exhook_handler, on_message_publish, []}},
, {'message.delivered', {emqx_exhook_handler, on_message_delivered, []}} {'message.delivered', {emqx_exhook_handler, on_message_delivered, []}},
, {'message.acked', {emqx_exhook_handler, on_message_acked, []}} {'message.acked', {emqx_exhook_handler, on_message_acked, []}},
, {'message.dropped', {emqx_exhook_handler, on_message_dropped, []}} {'message.dropped', {emqx_exhook_handler, on_message_dropped, []}}
]). ]).
-endif. -endif.

View File

@ -1,42 +1,57 @@
%%-*- mode: erlang -*- %%-*- mode: erlang -*-
{plugins, {plugins, [
[rebar3_proper, rebar3_proper,
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}} {grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
]}. ]}.
{deps, {deps, [
[ {emqx, {path, "../emqx"}} {emqx, {path, "../emqx"}},
, {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.4"}}} {grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.4"}}}
]}. ]}.
{grpc, {grpc, [
[{protos, ["priv/protos"]}, {protos, ["priv/protos"]},
{gpb_opts, [{module_name_prefix, "emqx_"}, {gpb_opts, [
{module_name_suffix, "_pb"}]} {module_name_prefix, "emqx_"},
{module_name_suffix, "_pb"}
]}
]}. ]}.
{provider_hooks, {provider_hooks, [
[{pre, [{compile, {grpc, gen}}, {pre, [
{clean, {grpc, clean}}]} {compile, {grpc, gen}},
{clean, {grpc, clean}}
]}
]}. ]}.
{edoc_opts, [{preprocess, true}]}. {edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars, {erl_opts, [
warn_unused_vars,
warn_shadow_vars, warn_shadow_vars,
warn_unused_import, warn_unused_import,
warn_obsolete_guard, warn_obsolete_guard,
debug_info, debug_info,
{parse_transform}]}. {parse_transform}
]}.
{xref_checks, [undefined_function_calls, undefined_functions, {xref_checks, [
locals_not_used, deprecated_function_calls, undefined_function_calls,
warnings_as_errors, deprecated_functions]}. undefined_functions,
locals_not_used,
deprecated_function_calls,
warnings_as_errors,
deprecated_functions
]}.
{xref_ignores, [emqx_exhook_pb]}. {xref_ignores, [emqx_exhook_pb]}.
{cover_enabled, true}. {cover_enabled, true}.
{cover_opts, [verbose]}. {cover_opts, [verbose]}.
{cover_export_enabled, true}. {cover_export_enabled, true}.
{cover_excl_mods, [emqx_exhook_pb, {cover_excl_mods, [
emqx_exhook_pb,
emqx_exhook_v_1_hook_provider_bhvr, emqx_exhook_v_1_hook_provider_bhvr,
emqx_exhook_v_1_hook_provider_client]}. emqx_exhook_v_1_hook_provider_client
]}.
{project_plugins, [erlfmt]}.

View File

@ -1,13 +1,13 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_exhook, {application, emqx_exhook, [
[{description, "EMQX Extension for Hook"}, {description, "EMQX Extension for Hook"},
{vsn, "5.0.0"}, {vsn, "5.0.0"},
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{mod, {emqx_exhook_app, []}}, {mod, {emqx_exhook_app, []}},
{applications, [kernel,stdlib,grpc,emqx]}, {applications, [kernel, stdlib, grpc, emqx]},
{env,[]}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},
{maintainers, ["EMQX Team <contact@emqx.io>"]}, {maintainers, ["EMQX Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"}]} {links, [{"Homepage", "https://emqx.io/"}]}
]}. ]}.

View File

@ -5,5 +5,4 @@
], ],
[ [
{<<".*">>, []} {<<".*">>, []}
] ]}.
}.

View File

@ -19,9 +19,10 @@
-include("emqx_exhook.hrl"). -include("emqx_exhook.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-export([ cast/2 -export([
, call_fold/3 cast/2,
]). call_fold/3
]).
%% exported for `emqx_telemetry' %% exported for `emqx_telemetry'
-export([get_basic_usage_info/0]). -export([get_basic_usage_info/0]).
@ -38,11 +39,15 @@ cast(_, _, []) ->
ok; ok;
cast(Hookpoint, Req, [ServerName | More]) -> cast(Hookpoint, Req, [ServerName | More]) ->
%% XXX: Need a real asynchronous running %% XXX: Need a real asynchronous running
_ = emqx_exhook_server:call(Hookpoint, Req, _ = emqx_exhook_server:call(
emqx_exhook_mgr:server(ServerName)), Hookpoint,
Req,
emqx_exhook_mgr:server(ServerName)
),
cast(Hookpoint, Req, More). cast(Hookpoint, Req, More).
-spec call_fold(atom(), term(), function()) -> {ok, term()} -spec call_fold(atom(), term(), function()) ->
{ok, term()}
| {stop, term()}. | {stop, term()}.
call_fold(Hookpoint, Req, AccFun) -> call_fold(Hookpoint, Req, AccFun) ->
case emqx_exhook_mgr:running() of case emqx_exhook_mgr:running() of
@ -90,12 +95,18 @@ deny_action_result('message.publish', Msg) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec get_basic_usage_info() -> -spec get_basic_usage_info() ->
#{ num_servers => non_neg_integer() #{
, servers => num_servers => non_neg_integer(),
[#{ driver => Driver servers =>
, hooks => [emqx_exhook_server:hookpoint()] [
}] #{
} when Driver :: grpc. driver => Driver,
hooks => [emqx_exhook_server:hookpoint()]
}
]
}
when
Driver :: grpc.
get_basic_usage_info() -> get_basic_usage_info() ->
try try
Servers = emqx_exhook_mgr:running(), Servers = emqx_exhook_mgr:running(),
@ -105,18 +116,22 @@ get_basic_usage_info() ->
fun(ServerName) -> fun(ServerName) ->
Hooks = emqx_exhook_mgr:hooks(ServerName), Hooks = emqx_exhook_mgr:hooks(ServerName),
HookNames = lists:map(fun(#{name := Name}) -> Name end, Hooks), HookNames = lists:map(fun(#{name := Name}) -> Name end, Hooks),
#{ hooks => HookNames #{
, %% currently, only grpc driver exists. hooks => HookNames,
%% currently, only grpc driver exists.
driver => grpc driver => grpc
} }
end, end,
Servers), Servers
#{ num_servers => NumServers ),
, servers => ServerInfo #{
num_servers => NumServers,
servers => ServerInfo
} }
catch catch
_:_ -> _:_ ->
#{ num_servers => 0 #{
, servers => [] num_servers => 0,
servers => []
} }
end. end.

View File

@ -33,10 +33,13 @@
-define(BAD_REQUEST, 'BAD_REQUEST'). -define(BAD_REQUEST, 'BAD_REQUEST').
-define(BAD_RPC, 'BAD_RPC'). -define(BAD_RPC, 'BAD_RPC').
-dialyzer([{nowarn_function, [ fill_cluster_server_info/5 -dialyzer([
, nodes_server_info/5 {nowarn_function, [
, fill_server_hooks_info/4 fill_cluster_server_info/5,
]}]). nodes_server_info/5,
fill_server_hooks_info/4
]}
]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% schema %% schema
@ -51,68 +54,82 @@ paths() -> ["/exhooks", "/exhooks/:name", "/exhooks/:name/move", "/exhooks/:name
schema(("/exhooks")) -> schema(("/exhooks")) ->
#{ #{
'operationId' => exhooks, 'operationId' => exhooks,
get => #{tags => ?TAGS, get => #{
tags => ?TAGS,
desc => <<"List all servers">>, desc => <<"List all servers">>,
responses => #{200 => mk(array(ref(detail_server_info)), #{})} responses => #{200 => mk(array(ref(detail_server_info)), #{})}
}, },
post => #{tags => ?TAGS, post => #{
tags => ?TAGS,
desc => <<"Add a servers">>, desc => <<"Add a servers">>,
'requestBody' => server_conf_schema(), 'requestBody' => server_conf_schema(),
responses => #{201 => mk(ref(detail_server_info), #{}), responses => #{
201 => mk(ref(detail_server_info), #{}),
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
} }
} }
}; };
schema("/exhooks/:name") -> schema("/exhooks/:name") ->
#{'operationId' => action_with_name, #{
get => #{tags => ?TAGS, 'operationId' => action_with_name,
get => #{
tags => ?TAGS,
desc => <<"Get the detail information of server">>, desc => <<"Get the detail information of server">>,
parameters => params_server_name_in_path(), parameters => params_server_name_in_path(),
responses => #{200 => mk(ref(detail_server_info), #{}), responses => #{
200 => mk(ref(detail_server_info), #{}),
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>) 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
} }
}, },
put => #{tags => ?TAGS, put => #{
tags => ?TAGS,
desc => <<"Update the server">>, desc => <<"Update the server">>,
parameters => params_server_name_in_path(), parameters => params_server_name_in_path(),
'requestBody' => server_conf_schema(), 'requestBody' => server_conf_schema(),
responses => #{200 => <<>>, responses => #{
200 => <<>>,
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>), 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
} }
}, },
delete => #{tags => ?TAGS, delete => #{
tags => ?TAGS,
desc => <<"Delete the server">>, desc => <<"Delete the server">>,
parameters => params_server_name_in_path(), parameters => params_server_name_in_path(),
responses => #{204 => <<>>, responses => #{
204 => <<>>,
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
} }
} }
}; };
schema("/exhooks/:name/hooks") -> schema("/exhooks/:name/hooks") ->
#{'operationId' => server_hooks, #{
get => #{tags => ?TAGS, 'operationId' => server_hooks,
get => #{
tags => ?TAGS,
desc => <<"Get the hooks information of server">>, desc => <<"Get the hooks information of server">>,
parameters => params_server_name_in_path(), parameters => params_server_name_in_path(),
responses => #{200 => mk(array(ref(list_hook_info)), #{}), responses => #{
200 => mk(array(ref(list_hook_info)), #{}),
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>) 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
} }
} }
}; };
schema("/exhooks/:name/move") -> schema("/exhooks/:name/move") ->
#{'operationId' => move, #{
post => #{tags => ?TAGS, 'operationId' => move,
post => #{
tags => ?TAGS,
desc => desc =>
<<"Move the server.\n", <<"Move the server.\n",
"NOTE: The position should be \"front|rear|before:{name}|after:{name}\"\n">>, "NOTE: The position should be \"front|rear|before:{name}|after:{name}\"\n">>,
parameters => params_server_name_in_path(), parameters => params_server_name_in_path(),
'requestBody' => emqx_dashboard_swagger:schema_with_examples( 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
ref(move_req), ref(move_req),
position_example()), position_example()
responses => #{204 => <<"No Content">>, ),
responses => #{
204 => <<"No Content">>,
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>), 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
} }
@ -120,72 +137,90 @@ schema("/exhooks/:name/move") ->
}. }.
fields(move_req) -> fields(move_req) ->
[{position, mk(string(), #{ desc => <<"The target position to be moved.">> [
, example => <<"front">>})}]; {position,
mk(string(), #{
desc => <<"The target position to be moved.">>,
example => <<"front">>
})}
];
fields(detail_server_info) -> fields(detail_server_info) ->
[ {metrics, mk(ref(metrics), #{})} [
, {node_metrics, mk(array(ref(node_metrics)), #{})} {metrics, mk(ref(metrics), #{})},
, {node_status, mk(array(ref(node_status)), #{})} {node_metrics, mk(array(ref(node_metrics)), #{})},
, {hooks, mk(array(ref(hook_info)), #{})} {node_status, mk(array(ref(node_status)), #{})},
{hooks, mk(array(ref(hook_info)), #{})}
] ++ emqx_exhook_schema:server_config(); ] ++ emqx_exhook_schema:server_config();
fields(list_hook_info) -> fields(list_hook_info) ->
[ {name, mk(binary(), #{desc => <<"The hook's name">>})} [
, {params, mk(map(name, binary()), {name, mk(binary(), #{desc => <<"The hook's name">>})},
#{desc => <<"The parameters used when the hook is registered">>})} {params,
, {metrics, mk(ref(metrics), #{})} mk(
, {node_metrics, mk(array(ref(node_metrics)), #{})} map(name, binary()),
#{desc => <<"The parameters used when the hook is registered">>}
)},
{metrics, mk(ref(metrics), #{})},
{node_metrics, mk(array(ref(node_metrics)), #{})}
]; ];
fields(node_metrics) -> fields(node_metrics) ->
[ {node, mk(string(), #{})} [
, {metrics, mk(ref(metrics), #{})} {node, mk(string(), #{})},
{metrics, mk(ref(metrics), #{})}
]; ];
fields(node_status) -> fields(node_status) ->
[ {node, mk(string(), #{})} [
, {status, mk(enum([running, waiting, stopped, error]), #{})} {node, mk(string(), #{})},
{status, mk(enum([running, waiting, stopped, error]), #{})}
]; ];
fields(hook_info) -> fields(hook_info) ->
[ {name, mk(binary(), #{desc => <<"The hook's name">>})} [
, {params, mk(map(name, binary()), {name, mk(binary(), #{desc => <<"The hook's name">>})},
#{desc => <<"The parameters used when the hook is registered">>})} {params,
mk(
map(name, binary()),
#{desc => <<"The parameters used when the hook is registered">>}
)}
]; ];
fields(metrics) -> fields(metrics) ->
[ {succeed, mk(integer(), #{})} [
, {failed, mk(integer(), #{})} {succeed, mk(integer(), #{})},
, {rate, mk(integer(), #{})} {failed, mk(integer(), #{})},
, {max_rate, mk(integer(), #{})} {rate, mk(integer(), #{})},
{max_rate, mk(integer(), #{})}
]; ];
fields(server_config) -> fields(server_config) ->
emqx_exhook_schema:server_config(). emqx_exhook_schema:server_config().
params_server_name_in_path() -> params_server_name_in_path() ->
[{name, mk(string(), #{in => path, [
{name,
mk(string(), #{
in => path,
required => true, required => true,
example => <<"default">>})} example => <<"default">>
})}
]. ].
server_conf_schema() -> server_conf_schema() ->
SSL = #{ enable => false SSL = #{
, cacertfile => emqx:cert_file(<<"cacert.pem">>) enable => false,
, certfile => emqx:cert_file(<<"cert.pem">>) cacertfile => emqx:cert_file(<<"cacert.pem">>),
, keyfile => emqx:cert_file(<<"key.pem">>) certfile => emqx:cert_file(<<"cert.pem">>),
keyfile => emqx:cert_file(<<"key.pem">>)
}, },
schema_with_example(ref(server_config), schema_with_example(
#{ name => "default" ref(server_config),
, enable => true #{
, url => <<"http://127.0.0.1:8081">> name => "default",
, request_timeout => "5s" enable => true,
, failed_action => deny url => <<"http://127.0.0.1:8081">>,
, auto_reconnect => "60s" request_timeout => "5s",
, pool_size => 8 failed_action => deny,
, ssl => SSL auto_reconnect => "60s",
}). pool_size => 8,
ssl => SSL
}
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% API %% API
@ -194,7 +229,6 @@ exhooks(get, _) ->
Confs = emqx:get_config([exhook, servers]), Confs = emqx:get_config([exhook, servers]),
Infos = nodes_all_server_info(Confs), Infos = nodes_all_server_info(Confs),
{200, Infos}; {200, Infos};
exhooks(post, #{body := Body}) -> exhooks(post, #{body := Body}) ->
{ok, _} = emqx_exhook_mgr:update_config([exhook, servers], {add, Body}), {ok, _} = emqx_exhook_mgr:update_config([exhook, servers], {add, Body}),
#{<<"name">> := Name} = Body, #{<<"name">> := Name} = Body,
@ -202,34 +236,45 @@ exhooks(post, #{body := Body}) ->
action_with_name(get, #{bindings := #{name := Name}}) -> action_with_name(get, #{bindings := #{name := Name}}) ->
get_nodes_server_info(Name); get_nodes_server_info(Name);
action_with_name(put, #{bindings := #{name := Name}, body := Body}) -> action_with_name(put, #{bindings := #{name := Name}, body := Body}) ->
case emqx_exhook_mgr:update_config([exhook, servers], case
{update, Name, Body}) of emqx_exhook_mgr:update_config(
[exhook, servers],
{update, Name, Body}
)
of
{ok, not_found} -> {ok, not_found} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => <<"Server not found">> message => <<"Server not found">>
}}; }};
{ok, {error, Reason}} -> {ok, {error, Reason}} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => unicode:characters_to_binary( message => unicode:characters_to_binary(
io_lib:format("Error Reason:~p~n", [Reason])) io_lib:format("Error Reason:~p~n", [Reason])
)
}}; }};
{ok, _} -> {ok, _} ->
{200}; {200};
{error, Error} -> {error, Error} ->
{500, #{code => <<"BAD_RPC">>, {500, #{
code => <<"BAD_RPC">>,
message => Error message => Error
}} }}
end; end;
action_with_name(delete, #{bindings := #{name := Name}}) -> action_with_name(delete, #{bindings := #{name := Name}}) ->
case emqx_exhook_mgr:update_config([exhook, servers], case
{delete, Name}) of emqx_exhook_mgr:update_config(
[exhook, servers],
{delete, Name}
)
of
{ok, _} -> {ok, _} ->
{200}; {200};
{error, Error} -> {error, Error} ->
{500, #{code => <<"BAD_RPC">>, {500, #{
code => <<"BAD_RPC">>,
message => Error message => Error
}} }}
end. end.
@ -237,21 +282,28 @@ action_with_name(delete, #{bindings := #{name := Name}}) ->
move(post, #{bindings := #{name := Name}, body := #{<<"position">> := RawPosition}}) -> move(post, #{bindings := #{name := Name}, body := #{<<"position">> := RawPosition}}) ->
case parse_position(RawPosition) of case parse_position(RawPosition) of
{ok, Position} -> {ok, Position} ->
case emqx_exhook_mgr:update_config([exhook, servers], case
{move, Name, Position}) of emqx_exhook_mgr:update_config(
[exhook, servers],
{move, Name, Position}
)
of
{ok, ok} -> {ok, ok} ->
{204}; {204};
{ok, not_found} -> {ok, not_found} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => <<"Server not found">> message => <<"Server not found">>
}}; }};
{error, Error} -> {error, Error} ->
{500, #{code => <<"BAD_RPC">>, {500, #{
code => <<"BAD_RPC">>,
message => Error message => Error
}} }}
end; end;
{error, invalid_position} -> {error, invalid_position} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => <<"Invalid Position">> message => <<"Invalid Position">>
}} }}
end. end.
@ -260,7 +312,8 @@ server_hooks(get, #{bindings := #{name := Name}}) ->
Confs = emqx:get_config([exhook, servers]), Confs = emqx:get_config([exhook, servers]),
case lists:search(fun(#{name := CfgName}) -> CfgName =:= Name end, Confs) of case lists:search(fun(#{name := CfgName}) -> CfgName =:= Name end, Confs) of
false -> false ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => <<"Server not found">> message => <<"Server not found">>
}}; }};
_ -> _ ->
@ -272,7 +325,8 @@ get_nodes_server_info(Name) ->
Confs = emqx:get_config([exhook, servers]), Confs = emqx:get_config([exhook, servers]),
case lists:search(fun(#{name := CfgName}) -> CfgName =:= Name end, Confs) of case lists:search(fun(#{name := CfgName}) -> CfgName =:= Name end, Confs) of
false -> false ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
code => <<"BAD_REQUEST">>,
message => <<"Server not found">> message => <<"Server not found">>
}}; }};
{value, Conf} -> {value, Conf} ->
@ -292,17 +346,17 @@ node_all_server_info([#{name := ServerName} = Conf | T], AllInfos, Default, Acc)
Info = fill_cluster_server_info(AllInfos, [], [], ServerName, Default), Info = fill_cluster_server_info(AllInfos, [], [], ServerName, Default),
AllInfo = maps:merge(Conf, Info), AllInfo = maps:merge(Conf, Info),
node_all_server_info(T, AllInfos, Default, [AllInfo | Acc]); node_all_server_info(T, AllInfos, Default, [AllInfo | Acc]);
node_all_server_info([], _, _, Acc) -> node_all_server_info([], _, _, Acc) ->
lists:reverse(Acc). lists:reverse(Acc).
fill_cluster_server_info([{Node, {error, _}} | T], StatusL, MetricsL, ServerName, Default) -> fill_cluster_server_info([{Node, {error, _}} | T], StatusL, MetricsL, ServerName, Default) ->
fill_cluster_server_info(T, fill_cluster_server_info(
T,
[#{node => Node, status => error} | StatusL], [#{node => Node, status => error} | StatusL],
[#{node => Node, metrics => Default} | MetricsL], [#{node => Node, metrics => Default} | MetricsL],
ServerName, ServerName,
Default); Default
);
fill_cluster_server_info([{Node, Result} | T], StatusL, MetricsL, ServerName, Default) -> fill_cluster_server_info([{Node, Result} | T], StatusL, MetricsL, ServerName, Default) ->
#{status := Status, metrics := Metrics} = Result, #{status := Status, metrics := Metrics} = Result,
fill_cluster_server_info( fill_cluster_server_info(
@ -310,11 +364,12 @@ fill_cluster_server_info([{Node, Result} | T], StatusL, MetricsL, ServerName, De
[#{node => Node, status => maps:get(ServerName, Status, error)} | StatusL], [#{node => Node, status => maps:get(ServerName, Status, error)} | StatusL],
[#{node => Node, metrics => maps:get(ServerName, Metrics, Default)} | MetricsL], [#{node => Node, metrics => maps:get(ServerName, Metrics, Default)} | MetricsL],
ServerName, ServerName,
Default); Default
);
fill_cluster_server_info([], StatusL, MetricsL, ServerName, _) -> fill_cluster_server_info([], StatusL, MetricsL, ServerName, _) ->
Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL), Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
#{metrics => Metrics, #{
metrics => Metrics,
node_metrics => MetricsL, node_metrics => MetricsL,
node_status => StatusL, node_status => StatusL,
hooks => emqx_exhook_mgr:hooks(ServerName) hooks => emqx_exhook_mgr:hooks(ServerName)
@ -329,24 +384,25 @@ nodes_server_info(Name) ->
nodes_server_info(InfoL, Name, Default, [], []). nodes_server_info(InfoL, Name, Default, [], []).
nodes_server_info([{Node, {error, _}} | T], Name, Default, StatusL, MetricsL) -> nodes_server_info([{Node, {error, _}} | T], Name, Default, StatusL, MetricsL) ->
nodes_server_info(T, nodes_server_info(
T,
Name, Name,
Default, Default,
[#{node => Node, status => error} | StatusL], [#{node => Node, status => error} | StatusL],
[#{node => Node, metrics => Default} | MetricsL] [#{node => Node, metrics => Default} | MetricsL]
); );
nodes_server_info([{Node, Result} | T], Name, Default, StatusL, MetricsL) -> nodes_server_info([{Node, Result} | T], Name, Default, StatusL, MetricsL) ->
#{status := Status, metrics := Metrics} = Result, #{status := Status, metrics := Metrics} = Result,
nodes_server_info(T, nodes_server_info(
T,
Name, Name,
Default, Default,
[#{node => Node, status => Status} | StatusL], [#{node => Node, status => Status} | StatusL],
[#{node => Node, metrics => Metrics} | MetricsL] [#{node => Node, metrics => Metrics} | MetricsL]
); );
nodes_server_info([], Name, _, StatusL, MetricsL) -> nodes_server_info([], Name, _, StatusL, MetricsL) ->
#{metrics => emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL), #{
metrics => emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
node_status => StatusL, node_status => StatusL,
node_metrics => MetricsL, node_metrics => MetricsL,
hooks => emqx_exhook_mgr:hooks(Name) hooks => emqx_exhook_mgr:hooks(Name)
@ -357,7 +413,8 @@ nodes_server_info([], Name, _, StatusL, MetricsL) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
get_nodes_server_hooks_info(Name) -> get_nodes_server_hooks_info(Name) ->
case emqx_exhook_mgr:hooks(Name) of case emqx_exhook_mgr:hooks(Name) of
[] -> []; [] ->
[];
Hooks -> Hooks ->
AllInfos = call_cluster(fun(Nodes) -> AllInfos = call_cluster(fun(Nodes) ->
emqx_exhook_proto_v1:server_hooks_metrics(Nodes, Name) emqx_exhook_proto_v1:server_hooks_metrics(Nodes, Name)
@ -370,18 +427,15 @@ get_nodes_server_hooks_info([#{name := Name} = Spec | T], AllInfos, Default, Acc
Info = fill_server_hooks_info(AllInfos, Name, Default, []), Info = fill_server_hooks_info(AllInfos, Name, Default, []),
AllInfo = maps:merge(Spec, Info), AllInfo = maps:merge(Spec, Info),
get_nodes_server_hooks_info(T, AllInfos, Default, [AllInfo | Acc]); get_nodes_server_hooks_info(T, AllInfos, Default, [AllInfo | Acc]);
get_nodes_server_hooks_info([], _, _, Acc) -> get_nodes_server_hooks_info([], _, _, Acc) ->
Acc. Acc.
fill_server_hooks_info([{_, {error, _}} | T], Name, Default, MetricsL) -> fill_server_hooks_info([{_, {error, _}} | T], Name, Default, MetricsL) ->
fill_server_hooks_info(T, Name, Default, MetricsL); fill_server_hooks_info(T, Name, Default, MetricsL);
fill_server_hooks_info([{Node, MetricsMap} | T], Name, Default, MetricsL) -> fill_server_hooks_info([{Node, MetricsMap} | T], Name, Default, MetricsL) ->
Metrics = maps:get(Name, MetricsMap, Default), Metrics = maps:get(Name, MetricsMap, Default),
NodeMetrics = #{node => Node, metrics => Metrics}, NodeMetrics = #{node => Node, metrics => Metrics},
fill_server_hooks_info(T, Name, Default, [NodeMetrics | MetricsL]); fill_server_hooks_info(T, Name, Default, [NodeMetrics | MetricsL]);
fill_server_hooks_info([], _Name, _Default, MetricsL) -> fill_server_hooks_info([], _Name, _Default, MetricsL) ->
Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL), Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
#{metrics => Metrics, node_metrics => MetricsL}. #{metrics => Metrics, node_metrics => MetricsL}.
@ -397,24 +451,32 @@ call_cluster(Fun) ->
Ret = Fun(Nodes), Ret = Fun(Nodes),
lists:zip(Nodes, lists:map(fun emqx_rpc:unwrap_erpc/1, Ret)). lists:zip(Nodes, lists:map(fun emqx_rpc:unwrap_erpc/1, Ret)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal Funcs %% Internal Funcs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
position_example() -> position_example() ->
#{ front => #{
#{ summary => <<"absolute position 'front'">> front =>
, value => #{<<"position">> => <<"front">>}} #{
, rear => summary => <<"absolute position 'front'">>,
#{ summary => <<"absolute position 'rear'">> value => #{<<"position">> => <<"front">>}
, value => #{<<"position">> => <<"rear">>}} },
, related_before => rear =>
#{ summary => <<"relative position 'before'">> #{
, value => #{<<"position">> => <<"before:default">>}} summary => <<"absolute position 'rear'">>,
, related_after => value => #{<<"position">> => <<"rear">>}
#{ summary => <<"relative position 'after'">> },
, value => #{<<"position">> => <<"after:default">>}} related_before =>
#{
summary => <<"relative position 'before'">>,
value => #{<<"position">> => <<"before:default">>}
},
related_after =>
#{
summary => <<"relative position 'after'">>,
value => #{<<"position">> => <<"after:default">>}
}
}. }.
parse_position(<<"front">>) -> parse_position(<<"front">>) ->

View File

@ -20,10 +20,11 @@
-include("emqx_exhook.hrl"). -include("emqx_exhook.hrl").
-export([ start/2 -export([
, stop/1 start/2,
, prep_stop/1 stop/1,
]). prep_stop/1
]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Application callbacks %% Application callbacks

View File

@ -20,47 +20,53 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-export([
-export([ on_client_connect/2 on_client_connect/2,
, on_client_connack/3 on_client_connack/3,
, on_client_connected/2 on_client_connected/2,
, on_client_disconnected/3 on_client_disconnected/3,
, on_client_authenticate/2 on_client_authenticate/2,
, on_client_authorize/4 on_client_authorize/4,
, on_client_subscribe/3 on_client_subscribe/3,
, on_client_unsubscribe/3 on_client_unsubscribe/3
]). ]).
%% Session Lifecircle Hooks %% Session Lifecircle Hooks
-export([ on_session_created/2 -export([
, on_session_subscribed/3 on_session_created/2,
, on_session_unsubscribed/3 on_session_subscribed/3,
, on_session_resumed/2 on_session_unsubscribed/3,
, on_session_discarded/2 on_session_resumed/2,
, on_session_takenover/2 on_session_discarded/2,
, on_session_terminated/3 on_session_takenover/2,
]). on_session_terminated/3
]).
-export([ on_message_publish/1 -export([
, on_message_dropped/3 on_message_publish/1,
, on_message_delivered/2 on_message_dropped/3,
, on_message_acked/2 on_message_delivered/2,
]). on_message_acked/2
]).
%% Utils %% Utils
-export([ message/1 -export([
, headers/1 message/1,
, stringfy/1 headers/1,
, merge_responsed_bool/2 stringfy/1,
, merge_responsed_message/2 merge_responsed_bool/2,
, assign_to_message/2 merge_responsed_message/2,
, clientinfo/1 assign_to_message/2,
]). clientinfo/1
]).
-import(emqx_exhook, -import(
[ cast/2 emqx_exhook,
, call_fold/3 [
]). cast/2,
call_fold/3
]
).
-elvis([{elvis_style, god_modules, disable}]). -elvis([{elvis_style, god_modules, disable}]).
@ -69,15 +75,18 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
on_client_connect(ConnInfo, Props) -> on_client_connect(ConnInfo, Props) ->
Req = #{conninfo => conninfo(ConnInfo), Req = #{
conninfo => conninfo(ConnInfo),
props => properties(Props) props => properties(Props)
}, },
cast('client.connect', Req). cast('client.connect', Req).
on_client_connack(ConnInfo, Rc, Props) -> on_client_connack(ConnInfo, Rc, Props) ->
Req = #{conninfo => conninfo(ConnInfo), Req = #{
conninfo => conninfo(ConnInfo),
result_code => stringfy(Rc), result_code => stringfy(Rc),
props => properties(Props)}, props => properties(Props)
},
cast('client.connack', Req). cast('client.connack', Req).
on_client_connected(ClientInfo, _ConnInfo) -> on_client_connected(ClientInfo, _ConnInfo) ->
@ -85,7 +94,8 @@ on_client_connected(ClientInfo, _ConnInfo) ->
cast('client.connected', Req). cast('client.connected', Req).
on_client_disconnected(ClientInfo, Reason, _ConnInfo) -> on_client_disconnected(ClientInfo, Reason, _ConnInfo) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
reason => stringfy(Reason) reason => stringfy(Reason)
}, },
cast('client.disconnected', Req). cast('client.disconnected', Req).
@ -98,14 +108,24 @@ on_client_authenticate(ClientInfo, AuthResult) ->
%% detailed info too. %% detailed info too.
%% %%
Bool = AuthResult == ok, Bool = AuthResult == ok,
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
result => Bool result => Bool
}, },
case call_fold('client.authenticate', Req, case
fun merge_responsed_bool/2) of call_fold(
'client.authenticate',
Req,
fun merge_responsed_bool/2
)
of
{StopOrOk, #{result := Result0}} when is_boolean(Result0) -> {StopOrOk, #{result := Result0}} when is_boolean(Result0) ->
Result = case Result0 of true -> ok; _ -> {error, not_authorized} end, Result =
case Result0 of
true -> ok;
_ -> {error, not_authorized}
end,
{StopOrOk, Result}; {StopOrOk, Result};
_ -> _ ->
{ok, AuthResult} {ok, AuthResult}
@ -113,32 +133,46 @@ on_client_authenticate(ClientInfo, AuthResult) ->
on_client_authorize(ClientInfo, PubSub, Topic, Result) -> on_client_authorize(ClientInfo, PubSub, Topic, Result) ->
Bool = Result == allow, Bool = Result == allow,
Type = case PubSub of Type =
case PubSub of
publish -> 'PUBLISH'; publish -> 'PUBLISH';
subscribe -> 'SUBSCRIBE' subscribe -> 'SUBSCRIBE'
end, end,
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
type => Type, type => Type,
topic => Topic, topic => Topic,
result => Bool result => Bool
}, },
case call_fold('client.authorize', Req, case
fun merge_responsed_bool/2) of call_fold(
'client.authorize',
Req,
fun merge_responsed_bool/2
)
of
{StopOrOk, #{result := Result0}} when is_boolean(Result0) -> {StopOrOk, #{result := Result0}} when is_boolean(Result0) ->
NResult = case Result0 of true -> allow; _ -> deny end, NResult =
case Result0 of
true -> allow;
_ -> deny
end,
{StopOrOk, NResult}; {StopOrOk, NResult};
_ -> {ok, Result} _ ->
{ok, Result}
end. end.
on_client_subscribe(ClientInfo, Props, TopicFilters) -> on_client_subscribe(ClientInfo, Props, TopicFilters) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
props => properties(Props), props => properties(Props),
topic_filters => topicfilters(TopicFilters) topic_filters => topicfilters(TopicFilters)
}, },
cast('client.subscribe', Req). cast('client.subscribe', Req).
on_client_unsubscribe(ClientInfo, Props, TopicFilters) -> on_client_unsubscribe(ClientInfo, Props, TopicFilters) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
props => properties(Props), props => properties(Props),
topic_filters => topicfilters(TopicFilters) topic_filters => topicfilters(TopicFilters)
}, },
@ -153,14 +187,16 @@ on_session_created(ClientInfo, _SessInfo) ->
cast('session.created', Req). cast('session.created', Req).
on_session_subscribed(ClientInfo, Topic, SubOpts) -> on_session_subscribed(ClientInfo, Topic, SubOpts) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
topic => Topic, topic => Topic,
subopts => maps:with([qos, share, rh, rap, nl], SubOpts) subopts => maps:with([qos, share, rh, rap, nl], SubOpts)
}, },
cast('session.subscribed', Req). cast('session.subscribed', Req).
on_session_unsubscribed(ClientInfo, Topic, _SubOpts) -> on_session_unsubscribed(ClientInfo, Topic, _SubOpts) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
topic => Topic topic => Topic
}, },
cast('session.unsubscribed', Req). cast('session.unsubscribed', Req).
@ -178,8 +214,10 @@ on_session_takenover(ClientInfo, _SessInfo) ->
cast('session.takenover', Req). cast('session.takenover', Req).
on_session_terminated(ClientInfo, Reason, _SessInfo) -> on_session_terminated(ClientInfo, Reason, _SessInfo) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
reason => stringfy(Reason)}, clientinfo => clientinfo(ClientInfo),
reason => stringfy(Reason)
},
cast('session.terminated', Req). cast('session.terminated', Req).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -190,17 +228,24 @@ on_message_publish(#message{topic = <<"$SYS/", _/binary>>}) ->
ok; ok;
on_message_publish(Message) -> on_message_publish(Message) ->
Req = #{message => message(Message)}, Req = #{message => message(Message)},
case call_fold('message.publish', Req, case
fun emqx_exhook_handler:merge_responsed_message/2) of call_fold(
'message.publish',
Req,
fun emqx_exhook_handler:merge_responsed_message/2
)
of
{StopOrOk, #{message := NMessage}} -> {StopOrOk, #{message := NMessage}} ->
{StopOrOk, assign_to_message(NMessage, Message)}; {StopOrOk, assign_to_message(NMessage, Message)};
_ -> {ok, Message} _ ->
{ok, Message}
end. end.
on_message_dropped(#message{topic = <<"$SYS/", _/binary>>}, _By, _Reason) -> on_message_dropped(#message{topic = <<"$SYS/", _/binary>>}, _By, _Reason) ->
ok; ok;
on_message_dropped(Message, _By, Reason) -> on_message_dropped(Message, _By, Reason) ->
Req = #{message => message(Message), Req = #{
message => message(Message),
reason => stringfy(Reason) reason => stringfy(Reason)
}, },
cast('message.dropped', Req). cast('message.dropped', Req).
@ -208,7 +253,8 @@ on_message_dropped(Message, _By, Reason) ->
on_message_delivered(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) -> on_message_delivered(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) ->
ok; ok;
on_message_delivered(ClientInfo, Message) -> on_message_delivered(ClientInfo, Message) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
message => message(Message) message => message(Message)
}, },
cast('message.delivered', Req). cast('message.delivered', Req).
@ -216,7 +262,8 @@ on_message_delivered(ClientInfo, Message) ->
on_message_acked(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) -> on_message_acked(_ClientInfo, #message{topic = <<"$SYS/", _/binary>>}) ->
ok; ok;
on_message_acked(ClientInfo, Message) -> on_message_acked(ClientInfo, Message) ->
Req = #{clientinfo => clientinfo(ClientInfo), Req = #{
clientinfo => clientinfo(ClientInfo),
message => message(Message) message => message(Message)
}, },
cast('message.acked', Req). cast('message.acked', Req).
@ -224,34 +271,59 @@ on_message_acked(ClientInfo, Message) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Types %% Types
properties(undefined) -> []; properties(undefined) ->
[];
properties(M) when is_map(M) -> properties(M) when is_map(M) ->
maps:fold(fun(K, V, Acc) -> maps:fold(
[#{name => stringfy(K), fun(K, V, Acc) ->
value => stringfy(V)} | Acc] [
end, [], M). #{
name => stringfy(K),
value => stringfy(V)
}
| Acc
]
end,
[],
M
).
conninfo(ConnInfo = conninfo(
#{clientid := ClientId, ConnInfo =
#{
clientid := ClientId,
peername := {Peerhost, _}, peername := {Peerhost, _},
sockname := {_, SockPort}}) -> sockname := {_, SockPort}
}
) ->
Username = maps:get(username, ConnInfo, undefined), Username = maps:get(username, ConnInfo, undefined),
ProtoName = maps:get(proto_name, ConnInfo, undefined), ProtoName = maps:get(proto_name, ConnInfo, undefined),
ProtoVer = maps:get(proto_ver, ConnInfo, undefined), ProtoVer = maps:get(proto_ver, ConnInfo, undefined),
Keepalive = maps:get(keepalive, ConnInfo, 0), Keepalive = maps:get(keepalive, ConnInfo, 0),
#{node => stringfy(node()), #{
node => stringfy(node()),
clientid => ClientId, clientid => ClientId,
username => maybe(Username), username => maybe(Username),
peerhost => ntoa(Peerhost), peerhost => ntoa(Peerhost),
sockport => SockPort, sockport => SockPort,
proto_name => ProtoName, proto_name => ProtoName,
proto_ver => stringfy(ProtoVer), proto_ver => stringfy(ProtoVer),
keepalive => Keepalive}. keepalive => Keepalive
}.
clientinfo(ClientInfo = clientinfo(
#{clientid := ClientId, username := Username, peerhost := PeerHost, ClientInfo =
sockport := SockPort, protocol := Protocol, mountpoint := Mountpoiont}) -> #{
#{node => stringfy(node()), clientid := ClientId,
username := Username,
peerhost := PeerHost,
sockport := SockPort,
protocol := Protocol,
mountpoint := Mountpoiont
}
) ->
#{
node => stringfy(node()),
clientid => ClientId, clientid => ClientId,
username => maybe(Username), username => maybe(Username),
password => maybe(maps:get(password, ClientInfo, undefined)), password => maybe(maps:get(password, ClientInfo, undefined)),
@ -262,11 +334,20 @@ clientinfo(ClientInfo =
is_superuser => maps:get(is_superuser, ClientInfo, false), is_superuser => maps:get(is_superuser, ClientInfo, false),
anonymous => maps:get(anonymous, ClientInfo, true), anonymous => maps:get(anonymous, ClientInfo, true),
cn => maybe(maps:get(cn, ClientInfo, undefined)), cn => maybe(maps:get(cn, ClientInfo, undefined)),
dn => maybe(maps:get(dn, ClientInfo, undefined))}. dn => maybe(maps:get(dn, ClientInfo, undefined))
}.
message(#message{id = Id, qos = Qos, from = From, topic = Topic, message(#message{
payload = Payload, timestamp = Ts, headers = Headers}) -> id = Id,
#{node => stringfy(node()), qos = Qos,
from = From,
topic = Topic,
payload = Payload,
timestamp = Ts,
headers = Headers
}) ->
#{
node => stringfy(node()),
id => emqx_guid:to_hexstr(Id), id => emqx_guid:to_hexstr(Id),
qos => Qos, qos => Qos,
from => stringfy(From), from => stringfy(From),
@ -281,7 +362,8 @@ headers(Headers) ->
maps:fold( maps:fold(
fun fun
(_, undefined, Acc) -> (_, undefined, Acc) ->
Acc; %% Ignore undefined value %% Ignore undefined value
Acc;
(K, V, Acc) -> (K, V, Acc) ->
case lists:member(K, Ls) of case lists:member(K, Ls) of
true -> true ->
@ -289,11 +371,16 @@ headers(Headers) ->
_ -> _ ->
Acc Acc
end end
end, #{}, Headers). end,
#{},
Headers
).
bin(K, V) when K == username; bin(K, V) when
K == username;
K == protocol; K == protocol;
K == allow_publish -> K == allow_publish
->
bin(V); bin(V);
bin(peerhost, V) -> bin(peerhost, V) ->
bin(inet:ntoa(V)). bin(inet:ntoa(V)).
@ -302,8 +389,14 @@ bin(V) when is_binary(V) -> V;
bin(V) when is_atom(V) -> atom_to_binary(V); bin(V) when is_atom(V) -> atom_to_binary(V);
bin(V) when is_list(V) -> iolist_to_binary(V). bin(V) when is_list(V) -> iolist_to_binary(V).
assign_to_message(InMessage = #{qos := Qos, topic := Topic, assign_to_message(
payload := Payload}, Message) -> InMessage = #{
qos := Qos,
topic := Topic,
payload := Payload
},
Message
) ->
NMsg = Message#message{qos = Qos, topic = Topic, payload = Payload}, NMsg = Message#message{qos = Qos, topic = Topic, payload = Payload},
enrich_header(maps:get(headers, InMessage, #{}), NMsg). enrich_header(maps:get(headers, InMessage, #{}), NMsg).
@ -320,7 +413,7 @@ enrich_header(Headers, Message) ->
topicfilters(Tfs) when is_list(Tfs) -> topicfilters(Tfs) when is_list(Tfs) ->
[#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs]. [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256})); list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
ntoa(IP) -> ntoa(IP) ->
list_to_binary(inet_parse:ntoa(IP)). list_to_binary(inet_parse:ntoa(IP)).
@ -344,8 +437,9 @@ stringfy(Term) ->
%% see exhook.proto %% see exhook.proto
merge_responsed_bool(_Req, #{type := 'IGNORE'}) -> merge_responsed_bool(_Req, #{type := 'IGNORE'}) ->
ignore; ignore;
merge_responsed_bool(Req, #{type := Type, value := {bool_result, NewBool}}) merge_responsed_bool(Req, #{type := Type, value := {bool_result, NewBool}}) when
when is_boolean(NewBool) -> is_boolean(NewBool)
->
{ret(Type), Req#{result => NewBool}}; {ret(Type), Req#{result => NewBool}};
merge_responsed_bool(_Req, Resp) -> merge_responsed_bool(_Req, Resp) ->
?SLOG(warning, #{msg => "unknown_responsed_value", resp => Resp}), ?SLOG(warning, #{msg => "unknown_responsed_value", resp => Resp}),

View File

@ -19,20 +19,29 @@
-include("emqx_exhook.hrl"). -include("emqx_exhook.hrl").
%% API %% API
-export([ init/0, succeed/2, failed/2 -export([
, update/1, new_metrics_info/0, servers_metrics/0 init/0,
, on_server_deleted/1, server_metrics/1, hooks_metrics/1 succeed/2,
, metrics_aggregate/1, metrics_aggregate_by_key/2 failed/2,
, metrics_aggregate_by/2 update/1,
]). new_metrics_info/0,
servers_metrics/0,
on_server_deleted/1,
server_metrics/1,
hooks_metrics/1,
metrics_aggregate/1,
metrics_aggregate_by_key/2,
metrics_aggregate_by/2
]).
-record(metrics, { index :: index() -record(metrics, {
, succeed = 0 :: non_neg_integer() index :: index(),
, failed = 0 :: non_neg_integer() succeed = 0 :: non_neg_integer(),
, rate = 0 :: non_neg_integer() failed = 0 :: non_neg_integer(),
, max_rate = 0 :: non_neg_integer() rate = 0 :: non_neg_integer(),
, window_rate :: integer() max_rate = 0 :: non_neg_integer(),
}). window_rate :: integer()
}).
-type metrics() :: #metrics{}. -type metrics() :: #metrics{}.
-type server_name() :: emqx_exhook_mgr:server_name(). -type server_name() :: emqx_exhook_mgr:server_name().
@ -41,11 +50,12 @@
-type hooks_metrics() :: #{hookpoint() => metrics_info()}. -type hooks_metrics() :: #{hookpoint() => metrics_info()}.
-type servers_metrics() :: #{server_name() => metrics_info()}. -type servers_metrics() :: #{server_name() => metrics_info()}.
-type metrics_info() :: #{ succeed := non_neg_integer() -type metrics_info() :: #{
, failed := non_neg_integer() succeed := non_neg_integer(),
, rate := number() failed := non_neg_integer(),
, max_rate := number() rate := number(),
}. max_rate := number()
}.
-define(INDEX(ServerName, HookPoint), {ServerName, HookPoint}). -define(INDEX(ServerName, HookPoint), {ServerName, HookPoint}).
-export_type([metrics_info/0, servers_metrics/0, hooks_metrics/0]). -export_type([metrics_info/0, servers_metrics/0, hooks_metrics/0]).
@ -54,16 +64,23 @@
%%% API %%% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
init() -> init() ->
_ = ets:new(?HOOKS_METRICS, _ = ets:new(
[ set, named_table, public ?HOOKS_METRICS,
, {keypos, #metrics.index}, {write_concurrency, true} [
, {read_concurrency, true} set,
]), named_table,
public,
{keypos, #metrics.index},
{write_concurrency, true},
{read_concurrency, true}
]
),
ok. ok.
-spec new_metric_info() -> metrics_info(). -spec new_metric_info() -> metrics_info().
new_metric_info() -> new_metric_info() ->
#{succeed => 0, #{
succeed => 0,
failed => 0, failed => 0,
rate => 0, rate => 0,
max_rate => 0 max_rate => 0
@ -71,33 +88,50 @@ new_metric_info() ->
-spec succeed(server_name(), hookpoint()) -> ok. -spec succeed(server_name(), hookpoint()) -> ok.
succeed(Server, Hook) -> succeed(Server, Hook) ->
inc(Server, Hook, #metrics.succeed, inc(
#metrics{ index = {Server, Hook} Server,
, window_rate = 0 Hook,
, succeed = 0 #metrics.succeed,
}). #metrics{
index = {Server, Hook},
window_rate = 0,
succeed = 0
}
).
-spec failed(server_name(), hookpoint()) -> ok. -spec failed(server_name(), hookpoint()) -> ok.
failed(Server, Hook) -> failed(Server, Hook) ->
inc(Server, Hook, #metrics.failed, inc(
#metrics{ index = {Server, Hook} Server,
, window_rate = 0 Hook,
, failed = 0 #metrics.failed,
}). #metrics{
index = {Server, Hook},
window_rate = 0,
failed = 0
}
).
-spec update(pos_integer()) -> true. -spec update(pos_integer()) -> true.
update(Interval) -> update(Interval) ->
Fun = fun(#metrics{rate = Rate, Fun = fun(
#metrics{
rate = Rate,
window_rate = WindowRate, window_rate = WindowRate,
max_rate = MaxRate} = Metrics, max_rate = MaxRate
_) -> } = Metrics,
_
) ->
case calc_metric(WindowRate, Interval) of case calc_metric(WindowRate, Interval) of
Rate -> true; Rate ->
true;
NewRate -> NewRate ->
MaxRate2 = erlang:max(MaxRate, NewRate), MaxRate2 = erlang:max(MaxRate, NewRate),
Metrics2 = Metrics#metrics{rate = NewRate, Metrics2 = Metrics#metrics{
rate = NewRate,
window_rate = 0, window_rate = 0,
max_rate = MaxRate2}, max_rate = MaxRate2
},
ets:insert(?HOOKS_METRICS, Metrics2) ets:insert(?HOOKS_METRICS, Metrics2)
end end
end, end,
@ -106,24 +140,36 @@ update(Interval) ->
-spec on_server_deleted(server_name()) -> true. -spec on_server_deleted(server_name()) -> true.
on_server_deleted(Name) -> on_server_deleted(Name) ->
ets:match_delete(?HOOKS_METRICS, ets:match_delete(
{metrics, {Name, '_'}, '_', '_', '_', '_', '_'}). ?HOOKS_METRICS,
{metrics, {Name, '_'}, '_', '_', '_', '_', '_'}
).
-spec server_metrics(server_name()) -> metrics_info(). -spec server_metrics(server_name()) -> metrics_info().
server_metrics(SvrName) -> server_metrics(SvrName) ->
Hooks = ets:match_object(?HOOKS_METRICS, Hooks = ets:match_object(
{metrics, {SvrName, '_'}, '_', '_', '_', '_', '_'}), ?HOOKS_METRICS,
{metrics, {SvrName, '_'}, '_', '_', '_', '_', '_'}
),
Fold = fun(#metrics{succeed = Succeed, Fold = fun(
#metrics{
succeed = Succeed,
failed = Failed, failed = Failed,
rate = Rate, rate = Rate,
max_rate = MaxRate}, max_rate = MaxRate
Acc) -> },
[#{ succeed => Succeed Acc
, failed => Failed ) ->
, rate => Rate [
, max_rate => MaxRate #{
} | Acc] succeed => Succeed,
failed => Failed,
rate => Rate,
max_rate => MaxRate
}
| Acc
]
end, end,
AllMetrics = lists:foldl(Fold, [], Hooks), AllMetrics = lists:foldl(Fold, [], Hooks),
@ -133,18 +179,22 @@ server_metrics(SvrName) ->
servers_metrics() -> servers_metrics() ->
AllMetrics = ets:tab2list(?HOOKS_METRICS), AllMetrics = ets:tab2list(?HOOKS_METRICS),
GroupFun = fun(#metrics{index = ?INDEX(ServerName, _), GroupFun = fun(
#metrics{
index = ?INDEX(ServerName, _),
succeed = Succeed, succeed = Succeed,
failed = Failed, failed = Failed,
rate = Rate, rate = Rate,
max_rate = MaxRate max_rate = MaxRate
}, },
Acc) -> Acc
) ->
SvrGroup = maps:get(ServerName, Acc, []), SvrGroup = maps:get(ServerName, Acc, []),
Metrics = #{ succeed => Succeed Metrics = #{
, failed => Failed succeed => Succeed,
, rate => Rate failed => Failed,
, max_rate => MaxRate rate => Rate,
max_rate => MaxRate
}, },
Acc#{ServerName => [Metrics | SvrGroup]} Acc#{ServerName => [Metrics | SvrGroup]}
end, end,
@ -156,20 +206,29 @@ servers_metrics() ->
-spec hooks_metrics(server_name()) -> hooks_metrics(). -spec hooks_metrics(server_name()) -> hooks_metrics().
hooks_metrics(SvrName) -> hooks_metrics(SvrName) ->
Hooks = ets:match_object(?HOOKS_METRICS, Hooks = ets:match_object(
{metrics, {SvrName, '_'}, '_', '_', '_', '_', '_'}), ?HOOKS_METRICS,
{metrics, {SvrName, '_'}, '_', '_', '_', '_', '_'}
),
Fold = fun(#metrics{index = ?INDEX(_, HookPoint), Fold = fun(
#metrics{
index = ?INDEX(_, HookPoint),
succeed = Succeed, succeed = Succeed,
failed = Failed, failed = Failed,
rate = Rate, rate = Rate,
max_rate = MaxRate}, max_rate = MaxRate
Acc) -> },
Acc#{HookPoint => #{ succeed => Succeed Acc
, failed => Failed ) ->
, rate => Rate Acc#{
, max_rate => MaxRate HookPoint => #{
}} succeed => Succeed,
failed => Failed,
rate => Rate,
max_rate => MaxRate
}
}
end, end,
lists:foldl(Fold, #{}, Hooks). lists:foldl(Fold, #{}, Hooks).
@ -178,12 +237,14 @@ hooks_metrics(SvrName) ->
metrics_aggregate(MetricsL) -> metrics_aggregate(MetricsL) ->
metrics_aggregate_by(fun(X) -> X end, MetricsL). metrics_aggregate_by(fun(X) -> X end, MetricsL).
-spec metrics_aggregate_by_key(Key, list(HasMetrics)) -> metrics_info() -spec metrics_aggregate_by_key(Key, list(HasMetrics)) -> metrics_info() when
when Key :: any(), Key :: any(),
HasMetrics :: #{Key => metrics_info()}. HasMetrics :: #{Key => metrics_info()}.
metrics_aggregate_by_key(Key, MetricsL) -> metrics_aggregate_by_key(Key, MetricsL) ->
metrics_aggregate_by(fun(X) -> maps:get(Key, X, new_metrics_info()) end, metrics_aggregate_by(
MetricsL). fun(X) -> maps:get(Key, X, new_metrics_info()) end,
MetricsL
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%%% Internal functions %%% Internal functions
@ -191,18 +252,21 @@ metrics_aggregate_by_key(Key, MetricsL) ->
-spec inc(server_name(), hookpoint(), pos_integer(), metrics()) -> ok. -spec inc(server_name(), hookpoint(), pos_integer(), metrics()) -> ok.
inc(Server, Hook, Pos, Default) -> inc(Server, Hook, Pos, Default) ->
Index = {Server, Hook}, Index = {Server, Hook},
_ = ets:update_counter(?HOOKS_METRICS, _ = ets:update_counter(
?HOOKS_METRICS,
Index, Index,
[{#metrics.window_rate, 1}, {Pos, 1}], [{#metrics.window_rate, 1}, {Pos, 1}],
Default), Default
),
ok. ok.
-spec new_metrics_info() -> metrics_info(). -spec new_metrics_info() -> metrics_info().
new_metrics_info() -> new_metrics_info() ->
#{ succeed => 0 #{
, failed => 0 succeed => 0,
, rate => 0 failed => 0,
, max_rate => 0 rate => 0,
max_rate => 0
}. }.
-spec calc_metric(non_neg_integer(), non_neg_integer()) -> non_neg_integer(). -spec calc_metric(non_neg_integer(), non_neg_integer()) -> non_neg_integer().
@ -211,26 +275,31 @@ calc_metric(Val, Interval) ->
erlang:ceil(Val * 1000 / Interval). erlang:ceil(Val * 1000 / Interval).
-spec metrics_add(metrics_info(), metrics_info()) -> metrics_info(). -spec metrics_add(metrics_info(), metrics_info()) -> metrics_info().
metrics_add(#{succeed := S1, failed := F1, rate := R1, max_rate := M1} metrics_add(
, #{succeed := S2, failed := F2, rate := R2, max_rate := M2} = Acc) -> #{succeed := S1, failed := F1, rate := R1, max_rate := M1},
Acc#{ succeed := S1 + S2 #{succeed := S2, failed := F2, rate := R2, max_rate := M2} = Acc
, failed := F1 + F2 ) ->
, rate := R1 + R2 Acc#{
, max_rate := M1 + M2 succeed := S1 + S2,
failed := F1 + F2,
rate := R1 + R2,
max_rate := M1 + M2
}. }.
-spec metrics_aggregate_by(fun((X) -> metrics_info()), list(X)) -> metrics_info() -spec metrics_aggregate_by(fun((X) -> metrics_info()), list(X)) -> metrics_info() when
when X :: any(). X :: any().
metrics_aggregate_by(_, []) -> metrics_aggregate_by(_, []) ->
new_metric_info(); new_metric_info();
metrics_aggregate_by(Fun, MetricsL) -> metrics_aggregate_by(Fun, MetricsL) ->
Fold = fun(E, Acc) -> metrics_add(Fun(E), Acc) end, Fold = fun(E, Acc) -> metrics_add(Fun(E), Acc) end,
#{rate := Rate, #{
max_rate := MaxRate} = Result = lists:foldl(Fold, new_metric_info(), MetricsL), rate := Rate,
max_rate := MaxRate
} = Result = lists:foldl(Fold, new_metric_info(), MetricsL),
Len = erlang:length(MetricsL), Len = erlang:length(MetricsL),
Result#{rate := Rate div Len, Result#{
rate := Rate div Len,
max_rate := MaxRate div Len max_rate := MaxRate div Len
}. }.

View File

@ -26,39 +26,44 @@
-export([start_link/0]). -export([start_link/0]).
%% Mgmt API %% Mgmt API
-export([ list/0 -export([
, lookup/1 list/0,
, enable/1 lookup/1,
, disable/1 enable/1,
, server_info/1 disable/1,
, all_servers_info/0 server_info/1,
, server_hooks_metrics/1 all_servers_info/0,
]). server_hooks_metrics/1
]).
%% Helper funcs %% Helper funcs
-export([ running/0 -export([
, server/1 running/0,
, hooks/1 server/1,
, init_ref_counter_table/0 hooks/1,
]). init_ref_counter_table/0
]).
-export([ update_config/2 -export([
, pre_config_update/3 update_config/2,
, post_config_update/5 pre_config_update/3,
]). post_config_update/5
]).
%% gen_server callbacks %% gen_server callbacks
-export([ init/1 -export([
, handle_call/3 init/1,
, handle_cast/2 handle_call/3,
, handle_info/2 handle_cast/2,
, terminate/2 handle_info/2,
, code_change/3 terminate/2,
]). code_change/3
]).
-export([roots/0]). -export([roots/0]).
-type state() :: #{%% Running servers %% Running servers
-type state() :: #{
running := servers(), running := servers(),
%% Wait to reload servers %% Wait to reload servers
waiting := servers(), waiting := servers(),
@ -67,25 +72,27 @@
%% Timer references %% Timer references
trefs := map(), trefs := map(),
orders := orders() orders := orders()
}. }.
-type server_name() :: binary(). -type server_name() :: binary().
-type servers() :: #{server_name() => server()}. -type servers() :: #{server_name() => server()}.
-type server() :: server_options(). -type server() :: server_options().
-type server_options() :: map(). -type server_options() :: map().
-type position() :: front -type position() ::
front
| rear | rear
| {before, binary()} | {before, binary()}
| {'after', binary()}. | {'after', binary()}.
-type orders() :: #{server_name() => integer()}. -type orders() :: #{server_name() => integer()}.
-type server_info() :: #{name := server_name(), -type server_info() :: #{
name := server_name(),
status := running | waiting | stopped, status := running | waiting | stopped,
atom() => term() atom() => term()
}. }.
-define(DEFAULT_TIMEOUT, 60000). -define(DEFAULT_TIMEOUT, 60000).
-define(REFRESH_INTERVAL, timer:seconds(5)). -define(REFRESH_INTERVAL, timer:seconds(5)).
@ -96,7 +103,8 @@
%% APIs %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec start_link() -> ignore -spec start_link() ->
ignore
| {ok, pid()} | {ok, pid()}
| {error, any()}. | {error, any()}.
start_link() -> start_link() ->
@ -146,26 +154,30 @@ update_config(KeyPath, UpdateReq) ->
pre_config_update(_, {add, Conf}, OldConf) -> pre_config_update(_, {add, Conf}, OldConf) ->
{ok, OldConf ++ [Conf]}; {ok, OldConf ++ [Conf]};
pre_config_update(_, {update, Name, Conf}, OldConf) -> pre_config_update(_, {update, Name, Conf}, OldConf) ->
case replace_conf(Name, fun(_) -> Conf end, OldConf) of case replace_conf(Name, fun(_) -> Conf end, OldConf) of
not_found -> {error, not_found}; not_found -> {error, not_found};
NewConf -> {ok, NewConf} NewConf -> {ok, NewConf}
end; end;
pre_config_update(_, {delete, ToDelete}, OldConf) -> pre_config_update(_, {delete, ToDelete}, OldConf) ->
{ok, lists:dropwhile(fun(#{<<"name">> := Name}) -> Name =:= ToDelete end, {ok,
OldConf)}; lists:dropwhile(
fun(#{<<"name">> := Name}) -> Name =:= ToDelete end,
OldConf
)};
pre_config_update(_, {move, Name, Position}, OldConf) -> pre_config_update(_, {move, Name, Position}, OldConf) ->
case do_move(Name, Position, OldConf) of case do_move(Name, Position, OldConf) of
not_found -> {error, not_found}; not_found -> {error, not_found};
NewConf -> {ok, NewConf} NewConf -> {ok, NewConf}
end; end;
pre_config_update(_, {enable, Name, Enable}, OldConf) -> pre_config_update(_, {enable, Name, Enable}, OldConf) ->
case replace_conf(Name, case
fun(Conf) -> Conf#{<<"enable">> => Enable} end, OldConf) of replace_conf(
Name,
fun(Conf) -> Conf#{<<"enable">> => Enable} end,
OldConf
)
of
not_found -> {error, not_found}; not_found -> {error, not_found};
NewConf -> {ok, NewConf} NewConf -> {ok, NewConf}
end. end.
@ -186,13 +198,16 @@ init([]) ->
{Waiting, Running, Stopped} = load_all_servers(ServerL), {Waiting, Running, Stopped} = load_all_servers(ServerL),
Orders = reorder(ServerL), Orders = reorder(ServerL),
refresh_tick(), refresh_tick(),
{ok, ensure_reload_timer( {ok,
#{waiting => Waiting, ensure_reload_timer(
#{
waiting => Waiting,
running => Running, running => Running,
stopped => Stopped, stopped => Stopped,
trefs => #{}, trefs => #{},
orders => Orders orders => Orders
})}. }
)}.
-spec load_all_servers(list(server_options())) -> {servers(), servers(), servers()}. -spec load_all_servers(list(server_options())) -> {servers(), servers(), servers()}.
load_all_servers(ServerL) -> load_all_servers(ServerL) ->
@ -208,15 +223,19 @@ load_all_servers([#{name := Name} = Options | More], Waiting, Running, Stopped)
disable -> disable ->
load_all_servers(More, Waiting, Running, Stopped#{Name => Options}) load_all_servers(More, Waiting, Running, Stopped#{Name => Options})
end; end;
load_all_servers([], Waiting, Running, Stopped) -> load_all_servers([], Waiting, Running, Stopped) ->
{Waiting, Running, Stopped}. {Waiting, Running, Stopped}.
handle_call(list, _From, State = #{running := Running, handle_call(
list,
_From,
State = #{
running := Running,
waiting := Waiting, waiting := Waiting,
stopped := Stopped, stopped := Stopped,
orders := Orders}) -> orders := Orders
}
) ->
R = get_servers_info(running, Running), R = get_servers_info(running, Running),
W = get_servers_info(waiting, Waiting), W = get_servers_info(waiting, Waiting),
S = get_servers_info(stopped, Stopped), S = get_servers_info(stopped, Stopped),
@ -225,29 +244,33 @@ handle_call(list, _From, State = #{running := Running,
OrderServers = sort_name_by_order(Servers, Orders), OrderServers = sort_name_by_order(Servers, Orders),
{reply, OrderServers, State}; {reply, OrderServers, State};
handle_call(
handle_call({update_config, {move, _Name, _Position}, NewConfL}, {update_config, {move, _Name, _Position}, NewConfL},
_From, _From,
State) -> State
) ->
Orders = reorder(NewConfL), Orders = reorder(NewConfL),
{reply, ok, State#{orders := Orders}}; {reply, ok, State#{orders := Orders}};
handle_call({update_config, {delete, ToDelete}, _}, _From, State) -> handle_call({update_config, {delete, ToDelete}, _}, _From, State) ->
{ok, #{orders := Orders, {ok,
#{
orders := Orders,
stopped := Stopped stopped := Stopped
} = State2} = do_unload_server(ToDelete, State), } = State2} = do_unload_server(ToDelete, State),
State3 = State2#{stopped := maps:remove(ToDelete, Stopped), State3 = State2#{
stopped := maps:remove(ToDelete, Stopped),
orders := maps:remove(ToDelete, Orders) orders := maps:remove(ToDelete, Orders)
}, },
emqx_exhook_metrics:on_server_deleted(ToDelete), emqx_exhook_metrics:on_server_deleted(ToDelete),
{reply, ok, State3}; {reply, ok, State3};
handle_call(
handle_call({update_config, {add, RawConf}, NewConfL}, {update_config, {add, RawConf}, NewConfL},
_From, _From,
#{running := Running, waiting := Waitting, stopped := Stopped} = State) -> #{running := Running, waiting := Waitting, stopped := Stopped} = State
) ->
{_, #{name := Name} = Conf} = emqx_config:check_config(?MODULE, RawConf), {_, #{name := Name} = Conf} = emqx_config:check_config(?MODULE, RawConf),
case emqx_exhook_server:load(Name, Conf) of case emqx_exhook_server:load(Name, Conf) of
@ -262,7 +285,6 @@ handle_call({update_config, {add, RawConf}, NewConfL},
end, end,
Orders = reorder(NewConfL), Orders = reorder(NewConfL),
{reply, ok, State2#{orders := Orders}}; {reply, ok, State2#{orders := Orders}};
handle_call({lookup, Name}, _From, State) -> handle_call({lookup, Name}, _From, State) ->
case where_is_server(Name, State) of case where_is_server(Name, State) of
not_found -> not_found ->
@ -271,51 +293,57 @@ handle_call({lookup, Name}, _From, State) ->
Result = maps:merge(Conf, #{status => Where}) Result = maps:merge(Conf, #{status => Where})
end, end,
{reply, Result, State}; {reply, Result, State};
handle_call({update_config, {update, Name, _Conf}, NewConfL}, _From, State) -> handle_call({update_config, {update, Name, _Conf}, NewConfL}, _From, State) ->
{Result, State2} = restart_server(Name, NewConfL, State), {Result, State2} = restart_server(Name, NewConfL, State),
{reply, Result, State2}; {reply, Result, State2};
handle_call({update_config, {enable, Name, _Enable}, NewConfL}, _From, State) -> handle_call({update_config, {enable, Name, _Enable}, NewConfL}, _From, State) ->
{Result, State2} = restart_server(Name, NewConfL, State), {Result, State2} = restart_server(Name, NewConfL, State),
{reply, Result, State2}; {reply, Result, State2};
handle_call({server_info, Name}, _From, State) -> handle_call({server_info, Name}, _From, State) ->
case where_is_server(Name, State) of case where_is_server(Name, State) of
not_found -> not_found ->
Result = not_found; Result = not_found;
{Status, _} -> {Status, _} ->
HooksMetrics = emqx_exhook_metrics:server_metrics(Name), HooksMetrics = emqx_exhook_metrics:server_metrics(Name),
Result = #{ status => Status Result = #{
, metrics => HooksMetrics status => Status,
metrics => HooksMetrics
} }
end, end,
{reply, Result, State}; {reply, Result, State};
handle_call(
handle_call(all_servers_info, _From, #{running := Running, all_servers_info,
_From,
#{
running := Running,
waiting := Waiting, waiting := Waiting,
stopped := Stopped} = State) -> stopped := Stopped
} = State
) ->
MakeStatus = fun(Status, Servers, Acc) -> MakeStatus = fun(Status, Servers, Acc) ->
lists:foldl(fun(Name, IAcc) -> IAcc#{Name => Status} end, lists:foldl(
fun(Name, IAcc) -> IAcc#{Name => Status} end,
Acc, Acc,
maps:keys(Servers)) maps:keys(Servers)
)
end, end,
Status = lists:foldl(fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end, Status = lists:foldl(
fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end,
#{}, #{},
[{running, Running}, {waiting, Waiting}, {stopped, Stopped}]), [{running, Running}, {waiting, Waiting}, {stopped, Stopped}]
),
Metrics = emqx_exhook_metrics:servers_metrics(), Metrics = emqx_exhook_metrics:servers_metrics(),
Result = #{ status => Status Result = #{
, metrics => Metrics status => Status,
metrics => Metrics
}, },
{reply, Result, State}; {reply, Result, State};
handle_call({server_hooks_metrics, Name}, _From, State) -> handle_call({server_hooks_metrics, Name}, _From, State) ->
Result = emqx_exhook_metrics:hooks_metrics(Name), Result = emqx_exhook_metrics:hooks_metrics(Name),
{reply, Result, State}; {reply, Result, State};
handle_call(_Request, _From, State) -> handle_call(_Request, _From, State) ->
Reply = ok, Reply = ok,
{reply, Reply, State}. {reply, Reply, State}.
@ -331,26 +359,32 @@ handle_info({timeout, _Ref, {reload, Name}}, State) ->
{error, not_found} -> {error, not_found} ->
{noreply, NState}; {noreply, NState};
{error, Reason} -> {error, Reason} ->
?SLOG(warning, ?SLOG(
#{msg => "failed_to_reload_exhook_callback_server", warning,
#{
msg => "failed_to_reload_exhook_callback_server",
reason => Reason, reason => Reason,
name => Name}), name => Name
}
),
{noreply, ensure_reload_timer(NState)} {noreply, ensure_reload_timer(NState)}
end; end;
handle_info(refresh_tick, State) -> handle_info(refresh_tick, State) ->
refresh_tick(), refresh_tick(),
emqx_exhook_metrics:update(?REFRESH_INTERVAL), emqx_exhook_metrics:update(?REFRESH_INTERVAL),
{noreply, State}; {noreply, State};
handle_info(_Info, State) -> handle_info(_Info, State) ->
{noreply, State}. {noreply, State}.
terminate(_Reason, State = #{running := Running}) -> terminate(_Reason, State = #{running := Running}) ->
_ = maps:fold(fun(Name, _, AccIn) -> _ = maps:fold(
fun(Name, _, AccIn) ->
{ok, NAccIn} = do_unload_server(Name, AccIn), {ok, NAccIn} = do_unload_server(Name, AccIn),
NAccIn NAccIn
end, State, Running), end,
State,
Running
),
_ = unload_exhooks(), _ = unload_exhooks(),
ok. ok.
@ -362,10 +396,13 @@ code_change(_OldVsn, State, _Extra) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
unload_exhooks() -> unload_exhooks() ->
[emqx:unhook(Name, {M, F}) || [
{Name, {M, F, _A}} <- ?ENABLED_HOOKS]. emqx:unhook(Name, {M, F})
|| {Name, {M, F, _A}} <- ?ENABLED_HOOKS
].
-spec do_load_server(server_name(), state()) -> {{error, not_found}, state()} -spec do_load_server(server_name(), state()) ->
{{error, not_found}, state()}
| {{error, already_started}, state()} | {{error, already_started}, state()}
| {ok, state()}. | {ok, state()}.
do_load_server(Name, State = #{orders := Orders}) -> do_load_server(Name, State = #{orders := Orders}) ->
@ -378,14 +415,18 @@ do_load_server(Name, State = #{orders := Orders}) ->
State2 = clean_reload_timer(Name, State), State2 = clean_reload_timer(Name, State),
{Options, Map2} = maps:take(Name, Map), {Options, Map2} = maps:take(Name, Map),
State3 = State2#{Where := Map2}, State3 = State2#{Where := Map2},
#{running := Running, #{
stopped := Stopped} = State3, running := Running,
stopped := Stopped
} = State3,
case emqx_exhook_server:load(Name, Options) of case emqx_exhook_server:load(Name, Options) of
{ok, ServerState} -> {ok, ServerState} ->
save(Name, ServerState), save(Name, ServerState),
update_order(Orders), update_order(Orders),
?SLOG(info, #{msg => "load_exhook_callback_server_ok", ?SLOG(info, #{
name => Name}), msg => "load_exhook_callback_server_ok",
name => Name
}),
{ok, State3#{running := maps:put(Name, Options, Running)}}; {ok, State3#{running := maps:put(Name, Options, Running)}};
{error, Reason} -> {error, Reason} ->
{{error, Reason}, State}; {{error, Reason}, State};
@ -397,11 +438,15 @@ do_load_server(Name, State = #{orders := Orders}) ->
-spec do_unload_server(server_name(), state()) -> {ok, state()}. -spec do_unload_server(server_name(), state()) -> {ok, state()}.
do_unload_server(Name, #{stopped := Stopped} = State) -> do_unload_server(Name, #{stopped := Stopped} = State) ->
case where_is_server(Name, State) of case where_is_server(Name, State) of
{stopped, _} -> {ok, State}; {stopped, _} ->
{ok, State};
{waiting, Waiting} -> {waiting, Waiting} ->
{Options, Waiting2} = maps:take(Name, Waiting), {Options, Waiting2} = maps:take(Name, Waiting),
{ok, clean_reload_timer(Name, {ok,
State#{waiting := Waiting2, clean_reload_timer(
Name,
State#{
waiting := Waiting2,
stopped := maps:put(Name, Options, Stopped) stopped := maps:put(Name, Options, Stopped)
} }
)}; )};
@ -410,32 +455,41 @@ do_unload_server(Name, #{stopped := Stopped} = State) ->
ok = unsave(Name), ok = unsave(Name),
ok = emqx_exhook_server:unload(Service), ok = emqx_exhook_server:unload(Service),
{Options, Running2} = maps:take(Name, Running), {Options, Running2} = maps:take(Name, Running),
{ok, State#{running := Running2, {ok, State#{
running := Running2,
stopped := maps:put(Name, Options, Stopped) stopped := maps:put(Name, Options, Stopped)
}}; }};
not_found -> {ok, State} not_found ->
{ok, State}
end. end.
-spec ensure_reload_timer(state()) -> state(). -spec ensure_reload_timer(state()) -> state().
ensure_reload_timer(State = #{waiting := Waiting, ensure_reload_timer(
State = #{
waiting := Waiting,
stopped := Stopped, stopped := Stopped,
trefs := TRefs}) -> trefs := TRefs
}
) ->
Iter = maps:iterator(Waiting), Iter = maps:iterator(Waiting),
{Waitting2, Stopped2, TRefs2} = {Waitting2, Stopped2, TRefs2} =
ensure_reload_timer(maps:next(Iter), Waiting, Stopped, TRefs), ensure_reload_timer(maps:next(Iter), Waiting, Stopped, TRefs),
State#{waiting := Waitting2, State#{
waiting := Waitting2,
stopped := Stopped2, stopped := Stopped2,
trefs := TRefs2}. trefs := TRefs2
}.
ensure_reload_timer(none, Waiting, Stopped, TimerRef) -> ensure_reload_timer(none, Waiting, Stopped, TimerRef) ->
{Waiting, Stopped, TimerRef}; {Waiting, Stopped, TimerRef};
ensure_reload_timer(
ensure_reload_timer({Name, #{auto_reconnect := Intv}, Iter}, {Name, #{auto_reconnect := Intv}, Iter},
Waiting, Waiting,
Stopped, Stopped,
TimerRef) when is_integer(Intv) -> TimerRef
) when is_integer(Intv) ->
Next = maps:next(Iter), Next = maps:next(Iter),
case maps:is_key(Name, TimerRef) of case maps:is_key(Name, TimerRef) of
true -> true ->
@ -445,17 +499,19 @@ ensure_reload_timer({Name, #{auto_reconnect := Intv}, Iter},
TimerRef2 = maps:put(Name, Ref, TimerRef), TimerRef2 = maps:put(Name, Ref, TimerRef),
ensure_reload_timer(Next, Waiting, Stopped, TimerRef2) ensure_reload_timer(Next, Waiting, Stopped, TimerRef2)
end; end;
ensure_reload_timer({Name, Opts, Iter}, Waiting, Stopped, TimerRef) -> ensure_reload_timer({Name, Opts, Iter}, Waiting, Stopped, TimerRef) ->
ensure_reload_timer(maps:next(Iter), ensure_reload_timer(
maps:next(Iter),
maps:remove(Name, Waiting), maps:remove(Name, Waiting),
maps:put(Name, Opts, Stopped), maps:put(Name, Opts, Stopped),
TimerRef). TimerRef
).
-spec clean_reload_timer(server_name(), state()) -> state(). -spec clean_reload_timer(server_name(), state()) -> state().
clean_reload_timer(Name, State = #{trefs := TRefs}) -> clean_reload_timer(Name, State = #{trefs := TRefs}) ->
case maps:take(Name, TRefs) of case maps:take(Name, TRefs) of
error -> State; error ->
State;
{TRef, NTRefs} -> {TRef, NTRefs} ->
_ = erlang:cancel_timer(TRef), _ = erlang:cancel_timer(TRef),
State#{trefs := NTRefs} State#{trefs := NTRefs}
@ -468,31 +524,24 @@ do_move(Name, Position, ConfL) ->
move([#{<<"name">> := Name} = Server | T], Name, Position, HeadL) -> move([#{<<"name">> := Name} = Server | T], Name, Position, HeadL) ->
move_to(Position, Server, lists:reverse(HeadL) ++ T); move_to(Position, Server, lists:reverse(HeadL) ++ T);
move([Server | T], Name, Position, HeadL) -> move([Server | T], Name, Position, HeadL) ->
move(T, Name, Position, [Server | HeadL]); move(T, Name, Position, [Server | HeadL]);
move([], _Name, _Position, _HeadL) -> move([], _Name, _Position, _HeadL) ->
not_found. not_found.
move_to(?CMD_MOVE_FRONT, Server, ServerL) -> move_to(?CMD_MOVE_FRONT, Server, ServerL) ->
[Server | ServerL]; [Server | ServerL];
move_to(?CMD_MOVE_REAR, Server, ServerL) -> move_to(?CMD_MOVE_REAR, Server, ServerL) ->
ServerL ++ [Server]; ServerL ++ [Server];
move_to(Position, Server, ServerL) -> move_to(Position, Server, ServerL) ->
move_to(ServerL, Position, Server, []). move_to(ServerL, Position, Server, []).
move_to([#{<<"name">> := Name} | _] = T, ?CMD_MOVE_BEFORE(Name), Server, HeadL) -> move_to([#{<<"name">> := Name} | _] = T, ?CMD_MOVE_BEFORE(Name), Server, HeadL) ->
lists:reverse(HeadL) ++ [Server | T]; lists:reverse(HeadL) ++ [Server | T];
move_to([#{<<"name">> := Name} = H | T], ?CMD_MOVE_AFTER(Name), Server, HeadL) -> move_to([#{<<"name">> := Name} = H | T], ?CMD_MOVE_AFTER(Name), Server, HeadL) ->
lists:reverse(HeadL) ++ [H, Server | T]; lists:reverse(HeadL) ++ [H, Server | T];
move_to([H | T], Position, Server, HeadL) -> move_to([H | T], Position, Server, HeadL) ->
move_to(T, Position, Server, [H | HeadL]); move_to(T, Position, Server, [H | HeadL]);
move_to([], _Position, _Server, _HeadL) -> move_to([], _Position, _Server, _HeadL) ->
not_found. not_found.
@ -504,32 +553,34 @@ reorder(ServerL) ->
reorder([#{name := Name} | T], Order, Orders) -> reorder([#{name := Name} | T], Order, Orders) ->
reorder(T, Order + 1, Orders#{Name => Order}); reorder(T, Order + 1, Orders#{Name => Order});
reorder([], _Order, Orders) -> reorder([], _Order, Orders) ->
Orders. Orders.
get_servers_info(Status, Map) -> get_servers_info(Status, Map) ->
Fold = fun(Name, Conf, Acc) -> Fold = fun(Name, Conf, Acc) ->
[maps:merge(Conf, #{status => Status, [
hooks => hooks(Name)}) | Acc] maps:merge(Conf, #{
status => Status,
hooks => hooks(Name)
})
| Acc
]
end, end,
maps:fold(Fold, [], Map). maps:fold(Fold, [], Map).
where_is_server(Name, #{running := Running}) when is_map_key(Name, Running) -> where_is_server(Name, #{running := Running}) when is_map_key(Name, Running) ->
{running, Running}; {running, Running};
where_is_server(Name, #{waiting := Waiting}) when is_map_key(Name, Waiting) -> where_is_server(Name, #{waiting := Waiting}) when is_map_key(Name, Waiting) ->
{waiting, Waiting}; {waiting, Waiting};
where_is_server(Name, #{stopped := Stopped}) when is_map_key(Name, Stopped) -> where_is_server(Name, #{stopped := Stopped}) when is_map_key(Name, Stopped) ->
{stopped, Stopped}; {stopped, Stopped};
where_is_server(_, _) -> where_is_server(_, _) ->
not_found. not_found.
-type replace_fun() :: fun((server_options()) -> server_options()). -type replace_fun() :: fun((server_options()) -> server_options()).
-spec replace_conf(binary(), replace_fun(), list(server_options())) -> not_found -spec replace_conf(binary(), replace_fun(), list(server_options())) ->
not_found
| list(server_options()). | list(server_options()).
replace_conf(Name, ReplaceFun, ConfL) -> replace_conf(Name, ReplaceFun, ConfL) ->
replace_conf(ConfL, Name, ReplaceFun, []). replace_conf(ConfL, Name, ReplaceFun, []).
@ -537,14 +588,13 @@ replace_conf(Name, ReplaceFun, ConfL) ->
replace_conf([#{<<"name">> := Name} = H | T], Name, ReplaceFun, HeadL) -> replace_conf([#{<<"name">> := Name} = H | T], Name, ReplaceFun, HeadL) ->
New = ReplaceFun(H), New = ReplaceFun(H),
lists:reverse(HeadL) ++ [New | T]; lists:reverse(HeadL) ++ [New | T];
replace_conf([H | T], Name, ReplaceFun, HeadL) -> replace_conf([H | T], Name, ReplaceFun, HeadL) ->
replace_conf(T, Name, ReplaceFun, [H | HeadL]); replace_conf(T, Name, ReplaceFun, [H | HeadL]);
replace_conf([], _, _, _) -> replace_conf([], _, _, _) ->
not_found. not_found.
-spec restart_server(binary(), list(server_options()), state()) -> {ok, state()} -spec restart_server(binary(), list(server_options()), state()) ->
{ok, state()}
| {{error, term()}, state()}. | {{error, term()}, state()}.
restart_server(Name, ConfL, State) -> restart_server(Name, ConfL, State) ->
case lists:search(fun(#{name := CName}) -> CName =:= Name end, ConfL) of case lists:search(fun(#{name := CName}) -> CName =:= Name end, ConfL) of
@ -567,12 +617,15 @@ restart_server(Name, ConfL, State) ->
end. end.
sort_name_by_order(Names, Orders) -> sort_name_by_order(Names, Orders) ->
lists:sort(fun(A, B) when is_binary(A) -> lists:sort(
fun
(A, B) when is_binary(A) ->
maps:get(A, Orders) < maps:get(B, Orders); maps:get(A, Orders) < maps:get(B, Orders);
(#{name := A}, #{name := B}) -> (#{name := A}, #{name := B}) ->
maps:get(A, Orders) < maps:get(B, Orders) maps:get(A, Orders) < maps:get(B, Orders)
end, end,
Names). Names
).
refresh_tick() -> refresh_tick() ->
erlang:send_after(?REFRESH_INTERVAL, self(), ?FUNCTION_NAME). erlang:send_after(?REFRESH_INTERVAL, self(), ?FUNCTION_NAME).

View File

@ -39,46 +39,67 @@ namespace() -> exhook.
roots() -> [exhook]. roots() -> [exhook].
fields(exhook) -> fields(exhook) ->
[{servers, [
sc(hoconsc:array(ref(server)), {servers,
#{ default => [] sc(
, desc => "List of exhook servers." hoconsc:array(ref(server)),
})} #{
default => [],
desc => "List of exhook servers."
}
)}
]; ];
fields(server) -> fields(server) ->
[ {name, sc(binary(), [
#{ desc => "Name of the exhook server." {name,
})} sc(
, {enable, sc(boolean(), binary(),
#{ default => true #{desc => "Name of the exhook server."}
, desc => "Enable the exhook server." )},
})} {enable,
, {url, sc(binary(), sc(
#{ desc => "URL of the gRPC server." boolean(),
})} #{
, {request_timeout, sc(duration(), default => true,
#{ default => "5s" desc => "Enable the exhook server."
, desc => "The timeout to request gRPC server." }
})} )},
, {failed_action, failed_action()} {url,
, {ssl, sc(
sc(ref(ssl_conf), #{})} binary(),
, {auto_reconnect, #{desc => "URL of the gRPC server."}
sc(hoconsc:union([false, duration()]), )},
#{ default => "60s" {request_timeout,
, desc => "Whether to automatically reconnect (initialize) the gRPC server.<br/>" sc(
duration(),
#{
default => "5s",
desc => "The timeout to request gRPC server."
}
)},
{failed_action, failed_action()},
{ssl, sc(ref(ssl_conf), #{})},
{auto_reconnect,
sc(
hoconsc:union([false, duration()]),
#{
default => "60s",
desc =>
"Whether to automatically reconnect (initialize) the gRPC server.<br/>"
"When gRPC is not available, exhook tries to request the gRPC service at " "When gRPC is not available, exhook tries to request the gRPC service at "
"that interval and reinitialize the list of mounted hooks." "that interval and reinitialize the list of mounted hooks."
})} }
, {pool_size, )},
sc(integer(), {pool_size,
#{ default => 8 sc(
, example => 8 integer(),
, desc => "The process pool size for gRPC client." #{
})} default => 8,
example => 8,
desc => "The process pool size for gRPC client."
}
)}
]; ];
fields(ssl_conf) -> fields(ssl_conf) ->
Schema = emqx_schema:client_ssl_opts_schema(#{}), Schema = emqx_schema:client_ssl_opts_schema(#{}),
lists:keydelete(user_lookup_fun, 1, Schema). lists:keydelete(user_lookup_fun, 1, Schema).
@ -99,11 +120,15 @@ ref(Field) ->
hoconsc:ref(?MODULE, Field). hoconsc:ref(?MODULE, Field).
failed_action() -> failed_action() ->
sc(hoconsc:enum([deny, ignore]), sc(
#{ default => deny hoconsc:enum([deny, ignore]),
, desc => "The value that is returned when the request " #{
default => deny,
desc =>
"The value that is returned when the request "
"to the gRPC server fails for any reason." "to the gRPC server fails for any reason."
}). }
).
server_config() -> server_config() ->
fields(server). fields(server).

View File

@ -22,25 +22,28 @@
-define(PB_CLIENT_MOD, emqx_exhook_v_1_hook_provider_client). -define(PB_CLIENT_MOD, emqx_exhook_v_1_hook_provider_client).
%% Load/Unload %% Load/Unload
-export([ load/2 -export([
, unload/1 load/2,
]). unload/1
]).
%% APIs %% APIs
-export([call/3]). -export([call/3]).
%% Infos %% Infos
-export([ name/1 -export([
, hooks/1 name/1,
, format/1 hooks/1,
, failed_action/1 format/1,
]). failed_action/1
]).
-ifdef(TEST). -ifdef(TEST).
-export([hk2func/1]). -export([hk2func/1]).
-endif. -endif.
-type server() :: #{%% Server name (equal to grpc client channel name) %% Server name (equal to grpc client channel name)
-type server() :: #{
name := binary(), name := binary(),
%% The function options %% The function options
options := map(), options := map(),
@ -50,10 +53,10 @@
hookspec := #{hookpoint() => map()}, hookspec := #{hookpoint() => map()},
%% Metrcis name prefix %% Metrcis name prefix
prefix := list() prefix := list()
}. }.
-type hookpoint() ::
-type hookpoint() :: 'client.connect' 'client.connect'
| 'client.connack' | 'client.connack'
| 'client.connected' | 'client.connected'
| 'client.disconnected' | 'client.disconnected'
@ -86,14 +89,16 @@
-spec load(binary(), map()) -> {ok, server()} | {error, term()} | disable. -spec load(binary(), map()) -> {ok, server()} | {error, term()} | disable.
load(_Name, #{enable := false}) -> load(_Name, #{enable := false}) ->
disable; disable;
load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts) -> load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts) ->
ReqOpts = #{timeout => Timeout, failed_action => FailedAction}, ReqOpts = #{timeout => Timeout, failed_action => FailedAction},
{SvrAddr, ClientOpts} = channel_opts(Opts), {SvrAddr, ClientOpts} = channel_opts(Opts),
case emqx_exhook_sup:start_grpc_client_channel( case
emqx_exhook_sup:start_grpc_client_channel(
Name, Name,
SvrAddr, SvrAddr,
ClientOpts) of ClientOpts
)
of
{ok, _ChannPoolPid} -> {ok, _ChannPoolPid} ->
case do_init(Name, ReqOpts) of case do_init(Name, ReqOpts) of
{ok, HookSpecs} -> {ok, HookSpecs} ->
@ -102,41 +107,52 @@ load(Name, #{request_timeout := Timeout, failed_action := FailedAction} = Opts)
ensure_metrics(Prefix, HookSpecs), ensure_metrics(Prefix, HookSpecs),
%% Ensure hooks %% Ensure hooks
ensure_hooks(HookSpecs), ensure_hooks(HookSpecs),
{ok, #{name => Name, {ok, #{
name => Name,
options => ReqOpts, options => ReqOpts,
channel => _ChannPoolPid, channel => _ChannPoolPid,
hookspec => HookSpecs, hookspec => HookSpecs,
prefix => Prefix }}; prefix => Prefix
}};
{error, _} = E -> {error, _} = E ->
emqx_exhook_sup:stop_grpc_client_channel(Name), emqx_exhook_sup:stop_grpc_client_channel(Name),
E E
end; end;
{error, _} = E -> E {error, _} = E ->
E
end. end.
%% @private %% @private
channel_opts(Opts = #{url := URL}) -> channel_opts(Opts = #{url := URL}) ->
ClientOpts = maps:merge(#{pool_size => erlang:system_info(schedulers)}, ClientOpts = maps:merge(
Opts), #{pool_size => erlang:system_info(schedulers)},
Opts
),
case uri_string:parse(URL) of case uri_string:parse(URL) of
#{scheme := <<"http">>, host := Host, port := Port} -> #{scheme := <<"http">>, host := Host, port := Port} ->
{format_http_uri("http", Host, Port), ClientOpts}; {format_http_uri("http", Host, Port), ClientOpts};
#{scheme := <<"https">>, host := Host, port := Port} -> #{scheme := <<"https">>, host := Host, port := Port} ->
SslOpts = SslOpts =
case maps:get(ssl, Opts, undefined) of case maps:get(ssl, Opts, undefined) of
undefined -> []; undefined ->
#{enable := false} -> []; [];
#{enable := false} ->
[];
MapOpts -> MapOpts ->
filter( filter(
[{cacertfile, maps:get(cacertfile, MapOpts, undefined)}, [
{cacertfile, maps:get(cacertfile, MapOpts, undefined)},
{certfile, maps:get(certfile, MapOpts, undefined)}, {certfile, maps:get(certfile, MapOpts, undefined)},
{keyfile, maps:get(keyfile, MapOpts, undefined)} {keyfile, maps:get(keyfile, MapOpts, undefined)}
]) ]
)
end, end,
NClientOpts = ClientOpts#{ NClientOpts = ClientOpts#{
gun_opts => gun_opts =>
#{transport => ssl, #{
transport_opts => SslOpts} transport => ssl,
transport_opts => SslOpts
}
}, },
{format_http_uri("https", Host, Port), NClientOpts}; {format_http_uri("https", Host, Port), NClientOpts};
Error -> Error ->
@ -147,7 +163,7 @@ format_http_uri(Scheme, Host, Port) ->
lists:flatten(io_lib:format("~ts://~ts:~w", [Scheme, Host, Port])). lists:flatten(io_lib:format("~ts://~ts:~w", [Scheme, Host, Port])).
filter(Ls) -> filter(Ls) ->
[ E || E <- Ls, E /= undefined]. [E || E <- Ls, E /= undefined].
-spec unload(server()) -> ok. -spec unload(server()) -> ok.
unload(#{name := Name, options := ReqOpts, hookspec := HookSpecs}) -> unload(#{name := Name, options := ReqOpts, hookspec := HookSpecs}) ->
@ -162,18 +178,23 @@ do_deinit(Name, ReqOpts) ->
do_init(ChannName, ReqOpts) -> do_init(ChannName, ReqOpts) ->
%% BrokerInfo defined at: exhook.protos %% BrokerInfo defined at: exhook.protos
BrokerInfo = maps:with([version, sysdescr, uptime, datetime], BrokerInfo = maps:with(
maps:from_list(emqx_sys:info())), [version, sysdescr, uptime, datetime],
maps:from_list(emqx_sys:info())
),
Req = #{broker => BrokerInfo}, Req = #{broker => BrokerInfo},
case do_call(ChannName, undefined, 'on_provider_loaded', Req, ReqOpts) of case do_call(ChannName, undefined, 'on_provider_loaded', Req, ReqOpts) of
{ok, InitialResp} -> {ok, InitialResp} ->
try try
{ok, resolve_hookspec(maps:get(hooks, InitialResp, []))} {ok, resolve_hookspec(maps:get(hooks, InitialResp, []))}
catch _:Reason:Stk -> catch
?SLOG(error, #{msg => "failed_to_init_channel", _:Reason:Stk ->
?SLOG(error, #{
msg => "failed_to_init_channel",
channel_name => ChannName, channel_name => ChannName,
reason => Reason, reason => Reason,
stacktrace => Stk}), stacktrace => Stk
}),
{error, Reason} {error, Reason}
end; end;
{error, Reason} -> {error, Reason} ->
@ -184,34 +205,46 @@ do_init(ChannName, ReqOpts) ->
resolve_hookspec(HookSpecs) when is_list(HookSpecs) -> resolve_hookspec(HookSpecs) when is_list(HookSpecs) ->
MessageHooks = message_hooks(), MessageHooks = message_hooks(),
AvailableHooks = available_hooks(), AvailableHooks = available_hooks(),
lists:foldr(fun(HookSpec, Acc) -> lists:foldr(
fun(HookSpec, Acc) ->
case maps:get(name, HookSpec, undefined) of case maps:get(name, HookSpec, undefined) of
undefined -> Acc; undefined ->
Acc;
Name0 -> Name0 ->
Name = try Name =
try
binary_to_existing_atom(Name0, utf8) binary_to_existing_atom(Name0, utf8)
catch T:R:_ -> {T,R} catch
T:R:_ -> {T, R}
end, end,
case {lists:member(Name, AvailableHooks), case {lists:member(Name, AvailableHooks), lists:member(Name, MessageHooks)} of
lists:member(Name, MessageHooks)} of
{false, _} -> {false, _} ->
error({unknown_hookpoint, Name}); error({unknown_hookpoint, Name});
{true, false} -> {true, false} ->
Acc#{Name => #{}}; Acc#{Name => #{}};
{true, true} -> {true, true} ->
Acc#{Name => #{ Acc#{
topics => maps:get(topics, HookSpec, [])}} Name => #{
topics => maps:get(topics, HookSpec, [])
}
}
end end
end end
end, #{}, HookSpecs). end,
#{},
HookSpecs
).
ensure_metrics(Prefix, HookSpecs) -> ensure_metrics(Prefix, HookSpecs) ->
Keys = [list_to_atom(Prefix ++ atom_to_list(Hookpoint)) Keys = [
|| Hookpoint <- maps:keys(HookSpecs)], list_to_atom(Prefix ++ atom_to_list(Hookpoint))
|| Hookpoint <- maps:keys(HookSpecs)
],
lists:foreach(fun emqx_metrics:ensure/1, Keys). lists:foreach(fun emqx_metrics:ensure/1, Keys).
ensure_hooks(HookSpecs) -> ensure_hooks(HookSpecs) ->
lists:foreach(fun(Hookpoint) -> lists:foreach(
fun(Hookpoint) ->
case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of
false -> false ->
?SLOG(error, #{msg => "skipped_unknown_hookpoint", hookpoint => Hookpoint}); ?SLOG(error, #{msg => "skipped_unknown_hookpoint", hookpoint => Hookpoint});
@ -219,25 +252,33 @@ ensure_hooks(HookSpecs) ->
emqx_hooks:put(Hookpoint, {M, F, A}), emqx_hooks:put(Hookpoint, {M, F, A}),
ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, 1}, {Hookpoint, 0}) ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, 1}, {Hookpoint, 0})
end end
end, maps:keys(HookSpecs)). end,
maps:keys(HookSpecs)
).
may_unload_hooks(HookSpecs) -> may_unload_hooks(HookSpecs) ->
lists:foreach(fun(Hookpoint) -> lists:foreach(
fun(Hookpoint) ->
case ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, -1}, {Hookpoint, 0}) of case ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, -1}, {Hookpoint, 0}) of
Cnt when Cnt =< 0 -> Cnt when Cnt =< 0 ->
case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of
{Hookpoint, {M, F, _A}} -> {Hookpoint, {M, F, _A}} ->
emqx_hooks:del(Hookpoint, {M, F}); emqx_hooks:del(Hookpoint, {M, F});
_ -> ok _ ->
ok
end, end,
ets:delete(?HOOKS_REF_COUNTER, Hookpoint); ets:delete(?HOOKS_REF_COUNTER, Hookpoint);
_ -> ok _ ->
ok
end end
end, maps:keys(HookSpecs)). end,
maps:keys(HookSpecs)
).
format(#{name := Name, hookspec := Hooks}) -> format(#{name := Name, hookspec := Hooks}) ->
lists:flatten( lists:flatten(
io_lib:format("name=~ts, hooks=~0p, active=true", [Name, Hooks])). io_lib:format("name=~ts, hooks=~0p, active=true", [Name, Hooks])
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% APIs %% APIs
@ -248,28 +289,41 @@ name(#{name := Name}) ->
hooks(#{hookspec := Hooks}) -> hooks(#{hookspec := Hooks}) ->
FoldFun = fun(Hook, Params, Acc) -> FoldFun = fun(Hook, Params, Acc) ->
[#{ name => Hook [
, params => Params #{
} | Acc] name => Hook,
params => Params
}
| Acc
]
end, end,
maps:fold(FoldFun, [], Hooks). maps:fold(FoldFun, [], Hooks).
-spec call(hookpoint(), map(), server()) -> ignore -spec call(hookpoint(), map(), server()) ->
ignore
| {ok, Resp :: term()} | {ok, Resp :: term()}
| {error, term()}. | {error, term()}.
call(Hookpoint, Req, #{name := ChannName, options := ReqOpts, call(Hookpoint, Req, #{
hookspec := Hooks, prefix := Prefix}) -> name := ChannName,
options := ReqOpts,
hookspec := Hooks,
prefix := Prefix
}) ->
case maps:get(Hookpoint, Hooks, undefined) of case maps:get(Hookpoint, Hooks, undefined) of
undefined -> ignore; undefined ->
ignore;
Opts -> Opts ->
NeedCall = case lists:member(Hookpoint, message_hooks()) of NeedCall =
false -> true; case lists:member(Hookpoint, message_hooks()) of
false ->
true;
_ -> _ ->
#{message := #{topic := Topic}} = Req, #{message := #{topic := Topic}} = Req,
match_topic_filter(Topic, maps:get(topics, Opts, [])) match_topic_filter(Topic, maps:get(topics, Opts, []))
end, end,
case NeedCall of case NeedCall of
false -> ignore; false ->
ignore;
_ -> _ ->
inc_metrics(Prefix, Hookpoint), inc_metrics(Prefix, Hookpoint),
GrpcFun = hk2func(Hookpoint), GrpcFun = hk2func(Hookpoint),
@ -293,42 +347,67 @@ match_topic_filter(TopicName, TopicFilter) ->
-ifdef(TEST). -ifdef(TEST).
-define(CALL_PB_CLIENT(ChanneName, Fun, Req, Options), -define(CALL_PB_CLIENT(ChanneName, Fun, Req, Options),
apply(?PB_CLIENT_MOD, Fun, [Req, #{<<"channel">> => ChannName}, Options])). apply(?PB_CLIENT_MOD, Fun, [Req, #{<<"channel">> => ChannName}, Options])
).
-else. -else.
-define(CALL_PB_CLIENT(ChanneName, Fun, Req, Options), -define(CALL_PB_CLIENT(ChanneName, Fun, Req, Options),
apply(?PB_CLIENT_MOD, Fun, [Req, Options])). apply(?PB_CLIENT_MOD, Fun, [Req, Options])
).
-endif. -endif.
-spec do_call(binary(), atom(), atom(), map(), map()) -> {ok, map()} | {error, term()}. -spec do_call(binary(), atom(), atom(), map(), map()) -> {ok, map()} | {error, term()}.
do_call(ChannName, Hookpoint, Fun, Req, ReqOpts) -> do_call(ChannName, Hookpoint, Fun, Req, ReqOpts) ->
Options = ReqOpts#{channel => ChannName}, Options = ReqOpts#{channel => ChannName},
?SLOG(debug, #{msg => "do_call", module => ?PB_CLIENT_MOD, function => Fun, ?SLOG(debug, #{
req => Req, options => Options}), msg => "do_call",
module => ?PB_CLIENT_MOD,
function => Fun,
req => Req,
options => Options
}),
case catch ?CALL_PB_CLIENT(ChanneName, Fun, Req, Options) of case catch ?CALL_PB_CLIENT(ChanneName, Fun, Req, Options) of
{ok, Resp, Metadata} -> {ok, Resp, Metadata} ->
?SLOG(debug, #{msg => "do_call_ok", resp => Resp, metadata => Metadata}), ?SLOG(debug, #{msg => "do_call_ok", resp => Resp, metadata => Metadata}),
update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:succeed/2), update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:succeed/2),
{ok, Resp}; {ok, Resp};
{error, {Code, Msg}, _Metadata} -> {error, {Code, Msg}, _Metadata} ->
?SLOG(error, #{msg => "exhook_call_error", module => ?PB_CLIENT_MOD, function => Fun, ?SLOG(error, #{
req => Req, options => Options, code => Code, packet => Msg}), msg => "exhook_call_error",
module => ?PB_CLIENT_MOD,
function => Fun,
req => Req,
options => Options,
code => Code,
packet => Msg
}),
update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2), update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2),
{error, {Code, Msg}}; {error, {Code, Msg}};
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{msg => "exhook_call_error", module => ?PB_CLIENT_MOD, function => Fun, ?SLOG(error, #{
req => Req, options => Options, reason => Reason}), msg => "exhook_call_error",
module => ?PB_CLIENT_MOD,
function => Fun,
req => Req,
options => Options,
reason => Reason
}),
update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2), update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2),
{error, Reason}; {error, Reason};
{'EXIT', {Reason, Stk}} -> {'EXIT', {Reason, Stk}} ->
?SLOG(error, #{msg => "exhook_call_exception", module => ?PB_CLIENT_MOD, function => Fun, ?SLOG(error, #{
req => Req, options => Options, stacktrace => Stk}), msg => "exhook_call_exception",
module => ?PB_CLIENT_MOD,
function => Fun,
req => Req,
options => Options,
stacktrace => Stk
}),
update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2), update_metrics(Hookpoint, ChannName, fun emqx_exhook_metrics:failed/2),
{error, Reason} {error, Reason}
end. end.
update_metrics(undefined, _ChannName, _Fun) -> update_metrics(undefined, _ChannName, _Fun) ->
ok; ok;
update_metrics(Hookpoint, ChannName, Fun) -> update_metrics(Hookpoint, ChannName, Fun) ->
Fun(ChannName, Hookpoint). Fun(ChannName, Hookpoint).
@ -356,20 +435,36 @@ hk2func('session.discarded') -> 'on_session_discarded';
hk2func('session.takenover') -> 'on_session_takenover'; hk2func('session.takenover') -> 'on_session_takenover';
hk2func('session.terminated') -> 'on_session_terminated'; hk2func('session.terminated') -> 'on_session_terminated';
hk2func('message.publish') -> 'on_message_publish'; hk2func('message.publish') -> 'on_message_publish';
hk2func('message.delivered') ->'on_message_delivered'; hk2func('message.delivered') -> 'on_message_delivered';
hk2func('message.acked') -> 'on_message_acked'; hk2func('message.acked') -> 'on_message_acked';
hk2func('message.dropped') ->'on_message_dropped'. hk2func('message.dropped') -> 'on_message_dropped'.
-compile({inline, [message_hooks/0]}). -compile({inline, [message_hooks/0]}).
message_hooks() -> message_hooks() ->
['message.publish', 'message.delivered', [
'message.acked', 'message.dropped']. 'message.publish',
'message.delivered',
'message.acked',
'message.dropped'
].
-compile({inline, [available_hooks/0]}). -compile({inline, [available_hooks/0]}).
available_hooks() -> available_hooks() ->
['client.connect', 'client.connack', 'client.connected', [
'client.disconnected', 'client.authenticate', 'client.authorize', 'client.connect',
'client.subscribe', 'client.unsubscribe', 'client.connack',
'session.created', 'session.subscribed', 'session.unsubscribed', 'client.connected',
'session.resumed', 'session.discarded', 'session.takenover', 'client.disconnected',
'session.terminated' | message_hooks()]. 'client.authenticate',
'client.authorize',
'client.subscribe',
'client.unsubscribe',
'session.created',
'session.subscribed',
'session.unsubscribed',
'session.resumed',
'session.discarded',
'session.takenover',
'session.terminated'
| message_hooks()
].

View File

@ -18,21 +18,22 @@
-behaviour(supervisor). -behaviour(supervisor).
-export([ start_link/0 -export([
, init/1 start_link/0,
]). init/1
]).
-export([ start_grpc_client_channel/3 -export([
, stop_grpc_client_channel/1 start_grpc_client_channel/3,
]). stop_grpc_client_channel/1
]).
-define(CHILD(Mod, Type, Args), -define(CHILD(Mod, Type, Args), #{
#{ id => Mod id => Mod,
, start => {Mod, start_link, Args} start => {Mod, start_link, Args},
, type => Type type => Type,
, shutdown => 15000 shutdown => 15000
} }).
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Supervisor APIs & Callbacks %% Supervisor APIs & Callbacks
@ -54,7 +55,8 @@ init([]) ->
-spec start_grpc_client_channel( -spec start_grpc_client_channel(
binary(), binary(),
uri_string:uri_string(), uri_string:uri_string(),
grpc_client_sup:options()) -> {ok, pid()} | {error, term()}. grpc_client_sup:options()
) -> {ok, pid()} | {error, term()}.
start_grpc_client_channel(Name, SvrAddr, Options) -> start_grpc_client_channel(Name, SvrAddr, Options) ->
grpc_client_sup:create_channel_pool(Name, SvrAddr, Options). grpc_client_sup:create_channel_pool(Name, SvrAddr, Options).

View File

@ -18,12 +18,13 @@
-behaviour(emqx_bpapi). -behaviour(emqx_bpapi).
-export([ introduced_in/0 -export([
introduced_in/0,
, all_servers_info/1 all_servers_info/1,
, server_info/2 server_info/2,
, server_hooks_metrics/2 server_hooks_metrics/2
]). ]).
-include_lib("emqx/include/bpapi.hrl"). -include_lib("emqx/include/bpapi.hrl").

View File

@ -23,26 +23,27 @@
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). -define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
-define(CONF_DEFAULT, <<" -define(CONF_DEFAULT, <<
exhook { "\n"
servers = [ "exhook {\n"
{ name = default, " servers = [\n"
url = \"http://127.0.0.1:9000\" " { name = default,\n"
}, " url = \"http://127.0.0.1:9000\"\n"
{ name = enable, " },\n"
enable = false, " { name = enable,\n"
url = \"http://127.0.0.1:9000\" " enable = false,\n"
}, " url = \"http://127.0.0.1:9000\"\n"
{ name = error, " },\n"
url = \"http://127.0.0.1:9001\" " { name = error,\n"
}, " url = \"http://127.0.0.1:9001\"\n"
{ name = not_reconnect, " },\n"
auto_reconnect = false, " { name = not_reconnect,\n"
url = \"http://127.0.0.1:9001\" " auto_reconnect = false,\n"
} " url = \"http://127.0.0.1:9001\"\n"
] " }\n"
} " ]\n"
">>). "}\n"
>>).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Setups %% Setups
@ -79,7 +80,8 @@ init_per_testcase(_, Config) ->
end_per_testcase(_, Config) -> end_per_testcase(_, Config) ->
case erlang:whereis(node()) of case erlang:whereis(node()) of
undefined -> ok; undefined ->
ok;
P -> P ->
erlang:unlink(P), erlang:unlink(P),
erlang:exit(P, kill) erlang:exit(P, kill)
@ -95,22 +97,29 @@ load_cfg(Cfg) ->
t_access_failed_if_no_server_running(_) -> t_access_failed_if_no_server_running(_) ->
emqx_exhook_mgr:disable(<<"default">>), emqx_exhook_mgr:disable(<<"default">>),
ClientInfo = #{clientid => <<"user-id-1">>, ClientInfo = #{
clientid => <<"user-id-1">>,
username => <<"usera">>, username => <<"usera">>,
peerhost => {127,0,0,1}, peerhost => {127, 0, 0, 1},
sockport => 1883, sockport => 1883,
protocol => mqtt, protocol => mqtt,
mountpoint => undefined mountpoint => undefined
}, },
?assertMatch({stop, {error, not_authorized}}, ?assertMatch(
emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})), {stop, {error, not_authorized}},
emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})
),
?assertMatch({stop, deny}, ?assertMatch(
emqx_exhook_handler:on_client_authorize(ClientInfo, publish, <<"t/1">>, allow)), {stop, deny},
emqx_exhook_handler:on_client_authorize(ClientInfo, publish, <<"t/1">>, allow)
),
Message = emqx_message:make(<<"t/1">>, <<"abc">>), Message = emqx_message:make(<<"t/1">>, <<"abc">>),
?assertMatch({stop, Message}, ?assertMatch(
emqx_exhook_handler:on_message_publish(Message)), {stop, Message},
emqx_exhook_handler:on_message_publish(Message)
),
emqx_exhook_mgr:enable(<<"default">>). emqx_exhook_mgr:enable(<<"default">>).
t_lookup(_) -> t_lookup(_) ->
@ -120,9 +129,14 @@ t_lookup(_) ->
t_list(_) -> t_list(_) ->
[H | _] = emqx_exhook_mgr:list(), [H | _] = emqx_exhook_mgr:list(),
?assertMatch(#{name := _, ?assertMatch(
#{
name := _,
status := _, status := _,
hooks := _}, H). hooks := _
},
H
).
t_unexpected(_) -> t_unexpected(_) ->
ok = gen_server:cast(emqx_exhook_mgr, unexpected), ok = gen_server:cast(emqx_exhook_mgr, unexpected),
@ -149,9 +163,11 @@ t_error_update_conf(_) ->
ErrorAnd = #{<<"name">> => Name, <<"url">> => <<"http://127.0.0.1:9001">>}, ErrorAnd = #{<<"name">> => Name, <<"url">> => <<"http://127.0.0.1:9001">>},
{ok, _} = emqx_exhook_mgr:update_config(Path, {add, ErrorAnd}), {ok, _} = emqx_exhook_mgr:update_config(Path, {add, ErrorAnd}),
DisableAnd = #{<<"name">> => Name, DisableAnd = #{
<<"name">> => Name,
<<"url">> => <<"http://127.0.0.1:9001">>, <<"url">> => <<"http://127.0.0.1:9001">>,
<<"enable">> => false}, <<"enable">> => false
},
{ok, _} = emqx_exhook_mgr:update_config(Path, {add, DisableAnd}), {ok, _} = emqx_exhook_mgr:update_config(Path, {add, DisableAnd}),
{ok, _} = emqx_exhook_mgr:update_config(Path, {delete, <<"error">>}), {ok, _} = emqx_exhook_mgr:update_config(Path, {delete, <<"error">>}),
@ -179,10 +195,12 @@ t_metrics(_) ->
t_handler(_) -> t_handler(_) ->
%% connect %% connect
{ok, C} = emqtt:start_link([{host, "localhost"}, {ok, C} = emqtt:start_link([
{host, "localhost"},
{port, 1883}, {port, 1883},
{username, <<"gooduser">>}, {username, <<"gooduser">>},
{clientid, <<"exhook_gooduser">>}]), {clientid, <<"exhook_gooduser">>}
]),
{ok, _} = emqtt:connect(C), {ok, _} = emqtt:connect(C),
%% pub/sub %% pub/sub
@ -212,9 +230,10 @@ t_handler(_) ->
ok. ok.
t_simulated_handler(_) -> t_simulated_handler(_) ->
ClientInfo = #{clientid => <<"user-id-1">>, ClientInfo = #{
clientid => <<"user-id-1">>,
username => <<"usera">>, username => <<"usera">>,
peerhost => {127,0,0,1}, peerhost => {127, 0, 0, 1},
sockport => 1883, sockport => 1883,
protocol => mqtt, protocol => mqtt,
mountpoint => undefined mountpoint => undefined
@ -232,8 +251,9 @@ t_misc_test(_) ->
ok. ok.
t_get_basic_usage_info(_Config) -> t_get_basic_usage_info(_Config) ->
#{ num_servers := NumServers #{
, servers := Servers num_servers := NumServers,
servers := Servers
} = emqx_exhook:get_basic_usage_info(), } = emqx_exhook:get_basic_usage_info(),
?assertEqual(1, NumServers), ?assertEqual(1, NumServers),
?assertMatch([_], Servers), ?assertMatch([_], Servers),
@ -261,7 +281,8 @@ t_get_basic_usage_info(_Config) ->
'session.terminated', 'session.terminated',
'session.unsubscribed' 'session.unsubscribed'
], ],
lists:sort(Hooks)). lists:sort(Hooks)
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Utils %% Utils
@ -276,14 +297,17 @@ unmeck_print() ->
meck:unload(emqx_ctl). meck:unload(emqx_ctl).
loaded_exhook_hookpoints() -> loaded_exhook_hookpoints() ->
lists:filtermap(fun(E) -> lists:filtermap(
fun(E) ->
Name = element(2, E), Name = element(2, E),
Callbacks = element(3, E), Callbacks = element(3, E),
case lists:any(fun is_exhook_callback/1, Callbacks) of case lists:any(fun is_exhook_callback/1, Callbacks) of
true -> {true, Name}; true -> {true, Name};
_ -> false _ -> false
end end
end, ets:tab2list(emqx_hooks)). end,
ets:tab2list(emqx_hooks)
).
is_exhook_callback(Cb) -> is_exhook_callback(Cb) ->
Action = element(2, Cb), Action = element(2, Cb),

View File

@ -26,19 +26,29 @@
-define(BASE_PATH, "api"). -define(BASE_PATH, "api").
-define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard). -define(CLUSTER_RPC_SHARD, emqx_cluster_rpc_shard).
-define(CONF_DEFAULT, <<" -define(CONF_DEFAULT, <<
exhook { "\n"
servers = "exhook {\n"
[ { name = default, " servers =\n"
url = \"http://127.0.0.1:9000\" " [ { name = default,\n"
} " url = \"http://127.0.0.1:9000\"\n"
] " }\n"
} " ]\n"
">>). "}\n"
>>).
all() -> all() ->
[ t_list, t_get, t_add, t_move_front, t_move_rear [
, t_move_before, t_move_after, t_delete, t_hooks, t_update t_list,
t_get,
t_add,
t_move_front,
t_move_rear,
t_move_before,
t_move_after,
t_delete,
t_hooks,
t_update
]. ].
init_per_suite(Config) -> init_per_suite(Config) ->
@ -71,7 +81,6 @@ init_per_testcase(t_add, Config) ->
_ = emqx_exhook_demo_svr:start(<<"test1">>, 9001), _ = emqx_exhook_demo_svr:start(<<"test1">>, 9001),
timer:sleep(200), timer:sleep(200),
Config; Config;
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(), {ok, _} = emqx_cluster_rpc:start_link(),
timer:sleep(200), timer:sleep(200),
@ -79,7 +88,8 @@ init_per_testcase(_, Config) ->
end_per_testcase(_, Config) -> end_per_testcase(_, Config) ->
case erlang:whereis(node()) of case erlang:whereis(node()) of
undefined -> ok; undefined ->
ok;
P -> P ->
erlang:unlink(P), erlang:unlink(P),
erlang:exit(P, kill) erlang:exit(P, kill)
@ -87,108 +97,168 @@ end_per_testcase(_, Config) ->
Config. Config.
t_list(_) -> t_list(_) ->
{ok, Data} = request_api(get, api_path(["exhooks"]), "", {ok, Data} = request_api(
auth_header_()), get,
api_path(["exhooks"]),
"",
auth_header_()
),
List = decode_json(Data), List = decode_json(Data),
?assertEqual(1, length(List)), ?assertEqual(1, length(List)),
[Svr] = List, [Svr] = List,
?assertMatch(#{name := <<"default">>, ?assertMatch(
#{
name := <<"default">>,
metrics := _, metrics := _,
node_metrics := _, node_metrics := _,
node_status := _, node_status := _,
hooks := _ hooks := _
}, Svr). },
Svr
).
t_get(_) -> t_get(_) ->
{ok, Data} = request_api(get, api_path(["exhooks", "default"]), "", {ok, Data} = request_api(
auth_header_()), get,
api_path(["exhooks", "default"]),
"",
auth_header_()
),
Svr = decode_json(Data), Svr = decode_json(Data),
?assertMatch(#{name := <<"default">>, ?assertMatch(
#{
name := <<"default">>,
metrics := _, metrics := _,
node_metrics := _, node_metrics := _,
node_status := _, node_status := _,
hooks := _ hooks := _
}, Svr). },
Svr
).
t_add(Cfg) -> t_add(Cfg) ->
Template = proplists:get_value(template, Cfg), Template = proplists:get_value(template, Cfg),
Instance = Template#{name => <<"test1">>, Instance = Template#{
name => <<"test1">>,
url => "http://127.0.0.1:9001" url => "http://127.0.0.1:9001"
}, },
{ok, Data} = request_api(post, api_path(["exhooks"]), "", {ok, Data} = request_api(
auth_header_(), Instance), post,
api_path(["exhooks"]),
"",
auth_header_(),
Instance
),
Svr = decode_json(Data), Svr = decode_json(Data),
?assertMatch(#{name := <<"test1">>, ?assertMatch(
#{
name := <<"test1">>,
metrics := _, metrics := _,
node_metrics := _, node_metrics := _,
node_status := _, node_status := _,
hooks := _}, Svr), hooks := _
},
Svr
),
?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()). ?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()).
t_move_front(_) -> t_move_front(_) ->
Result = request_api(post, api_path(["exhooks", "default", "move"]), "", Result = request_api(
post,
api_path(["exhooks", "default", "move"]),
"",
auth_header_(), auth_header_(),
#{position => <<"front">>}), #{position => <<"front">>}
),
?assertMatch({ok, <<>>}, Result), ?assertMatch({ok, <<>>}, Result),
?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()). ?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()).
t_move_rear(_) -> t_move_rear(_) ->
Result = request_api(post, api_path(["exhooks", "default", "move"]), "", Result = request_api(
post,
api_path(["exhooks", "default", "move"]),
"",
auth_header_(), auth_header_(),
#{position => <<"rear">>}), #{position => <<"rear">>}
),
?assertMatch({ok, <<>>}, Result), ?assertMatch({ok, <<>>}, Result),
?assertMatch([<<"test1">>, <<"default">>], emqx_exhook_mgr:running()). ?assertMatch([<<"test1">>, <<"default">>], emqx_exhook_mgr:running()).
t_move_before(_) -> t_move_before(_) ->
Result = request_api(post, api_path(["exhooks", "default", "move"]), "", Result = request_api(
post,
api_path(["exhooks", "default", "move"]),
"",
auth_header_(), auth_header_(),
#{position => <<"before:test1">>}), #{position => <<"before:test1">>}
),
?assertMatch({ok, <<>>}, Result), ?assertMatch({ok, <<>>}, Result),
?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()). ?assertMatch([<<"default">>, <<"test1">>], emqx_exhook_mgr:running()).
t_move_after(_) -> t_move_after(_) ->
Result = request_api(post, api_path(["exhooks", "default", "move"]), "", Result = request_api(
post,
api_path(["exhooks", "default", "move"]),
"",
auth_header_(), auth_header_(),
#{position => <<"after:test1">>}), #{position => <<"after:test1">>}
),
?assertMatch({ok, <<>>}, Result), ?assertMatch({ok, <<>>}, Result),
?assertMatch([<<"test1">>, <<"default">>], emqx_exhook_mgr:running()). ?assertMatch([<<"test1">>, <<"default">>], emqx_exhook_mgr:running()).
t_delete(_) -> t_delete(_) ->
Result = request_api(delete, api_path(["exhooks", "test1"]), "", Result = request_api(
auth_header_()), delete,
api_path(["exhooks", "test1"]),
"",
auth_header_()
),
?assertMatch({ok, <<>>}, Result), ?assertMatch({ok, <<>>}, Result),
?assertMatch([<<"default">>], emqx_exhook_mgr:running()). ?assertMatch([<<"default">>], emqx_exhook_mgr:running()).
t_hooks(_Cfg) -> t_hooks(_Cfg) ->
{ok, Data} = request_api(get, api_path(["exhooks", "default", "hooks"]), "", {ok, Data} = request_api(
auth_header_()), get,
api_path(["exhooks", "default", "hooks"]),
"",
auth_header_()
),
[Hook1 | _] = decode_json(Data), [Hook1 | _] = decode_json(Data),
?assertMatch(#{name := _, ?assertMatch(
#{
name := _,
params := _, params := _,
metrics := _, metrics := _,
node_metrics := _ node_metrics := _
}, Hook1). },
Hook1
).
t_update(Cfg) -> t_update(Cfg) ->
Template = proplists:get_value(template, Cfg), Template = proplists:get_value(template, Cfg),
Instance = Template#{enable => false}, Instance = Template#{enable => false},
{ok, <<>>} = request_api(put, api_path(["exhooks", "default"]), "", {ok, <<>>} = request_api(
auth_header_(), Instance), put,
api_path(["exhooks", "default"]),
"",
auth_header_(),
Instance
),
?assertMatch([], emqx_exhook_mgr:running()). ?assertMatch([], emqx_exhook_mgr:running()).
@ -203,24 +273,27 @@ request_api(Method, Url, QueryParams, Auth) ->
request_api(Method, Url, QueryParams, Auth, []). request_api(Method, Url, QueryParams, Auth, []).
request_api(Method, Url, QueryParams, Auth, []) -> request_api(Method, Url, QueryParams, Auth, []) ->
NewUrl = case QueryParams of NewUrl =
case QueryParams of
"" -> Url; "" -> Url;
_ -> Url ++ "?" ++ QueryParams _ -> Url ++ "?" ++ QueryParams
end, end,
do_request_api(Method, {NewUrl, [Auth]}); do_request_api(Method, {NewUrl, [Auth]});
request_api(Method, Url, QueryParams, Auth, Body) -> request_api(Method, Url, QueryParams, Auth, Body) ->
NewUrl = case QueryParams of NewUrl =
case QueryParams of
"" -> Url; "" -> Url;
_ -> Url ++ "?" ++ QueryParams _ -> Url ++ "?" ++ QueryParams
end, end,
do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}). do_request_api(Method, {NewUrl, [Auth], "application/json", emqx_json:encode(Body)}).
do_request_api(Method, Request)-> do_request_api(Method, Request) ->
case httpc:request(Method, Request, [], [{body_format, binary}]) of case httpc:request(Method, Request, [], [{body_format, binary}]) of
{error, socket_closed_remotely} -> {error, socket_closed_remotely} ->
{error, socket_closed_remotely}; {error, socket_closed_remotely};
{ok, {{"HTTP/1.1", Code, _}, _, Return} } {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
when Code =:= 200 orelse Code =:= 204 orelse Code =:= 201 -> Code =:= 200 orelse Code =:= 204 orelse Code =:= 201
->
{ok, Return}; {ok, Return};
{ok, {Reason, _, _}} -> {ok, {Reason, _, _}} ->
{error, Reason} {error, Reason}
@ -232,8 +305,8 @@ auth_header_() ->
auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)). auth_header_(binary_to_list(AppId), binary_to_list(AppSecret)).
auth_header_(User, Pass) -> auth_header_(User, Pass) ->
Encoded = base64:encode_to_string(lists:append([User,":",Pass])), Encoded = base64:encode_to_string(lists:append([User, ":", Pass])),
{"Authorization","Basic " ++ Encoded}. {"Authorization", "Basic " ++ Encoded}.
api_path(Parts)-> api_path(Parts) ->
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts). ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION] ++ Parts).

View File

@ -19,37 +19,39 @@
-behaviour(emqx_exhook_v_1_hook_provider_bhvr). -behaviour(emqx_exhook_v_1_hook_provider_bhvr).
%% %%
-export([ start/0 -export([
, start/2 start/0,
, stop/0 start/2,
, stop/1 stop/0,
, take/0 stop/1,
, in/1 take/0,
]). in/1
]).
%% gRPC server HookProvider callbacks %% gRPC server HookProvider callbacks
-export([ on_provider_loaded/2 -export([
, on_provider_unloaded/2 on_provider_loaded/2,
, on_client_connect/2 on_provider_unloaded/2,
, on_client_connack/2 on_client_connect/2,
, on_client_connected/2 on_client_connack/2,
, on_client_disconnected/2 on_client_connected/2,
, on_client_authenticate/2 on_client_disconnected/2,
, on_client_authorize/2 on_client_authenticate/2,
, on_client_subscribe/2 on_client_authorize/2,
, on_client_unsubscribe/2 on_client_subscribe/2,
, on_session_created/2 on_client_unsubscribe/2,
, on_session_subscribed/2 on_session_created/2,
, on_session_unsubscribed/2 on_session_subscribed/2,
, on_session_resumed/2 on_session_unsubscribed/2,
, on_session_discarded/2 on_session_resumed/2,
, on_session_takenover/2 on_session_discarded/2,
, on_session_terminated/2 on_session_takenover/2,
, on_message_publish/2 on_session_terminated/2,
, on_message_delivered/2 on_message_publish/2,
, on_message_dropped/2 on_message_delivered/2,
, on_message_acked/2 on_message_dropped/2,
]). on_message_acked/2
]).
-define(PORT, 9000). -define(PORT, 9000).
-define(NAME, ?MODULE). -define(NAME, ?MODULE).
@ -75,15 +77,18 @@ stop(Name) ->
take() -> take() ->
to_atom_name(?NAME) ! {take, self()}, to_atom_name(?NAME) ! {take, self()},
receive {value, V} -> V receive
after 5000 -> error(timeout) end. {value, V} -> V
after 5000 -> error(timeout)
end.
in({FunName, Req}) -> in({FunName, Req}) ->
to_atom_name(?NAME) ! {in, FunName, Req}. to_atom_name(?NAME) ! {in, FunName, Req}.
mgr_main(Name, Port) -> mgr_main(Name, Port) ->
application:ensure_all_started(grpc), application:ensure_all_started(grpc),
Services = #{protos => [emqx_exhook_pb], Services = #{
protos => [emqx_exhook_pb],
services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr} services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr}
}, },
Options = [], Options = [],
@ -103,9 +108,12 @@ mgr_loop([Svr, Q, Takes]) ->
end. end.
reply(Q1, Q2) -> reply(Q1, Q2) ->
case queue:len(Q1) =:= 0 orelse case
queue:len(Q2) =:= 0 of queue:len(Q1) =:= 0 orelse
true -> {Q1, Q2}; queue:len(Q2) =:= 0
of
true ->
{Q1, Q2};
_ -> _ ->
{{value, {Name, V}}, NQ1} = queue:out(Q1), {{value, {Name, V}}, NQ1} = queue:out(Q1),
{{value, From}, NQ2} = queue:out(Q2), {{value, From}, NQ2} = queue:out(Q2),
@ -115,7 +123,6 @@ reply(Q1, Q2) ->
to_atom_name(Name) when is_atom(Name) -> to_atom_name(Name) when is_atom(Name) ->
Name; Name;
to_atom_name(Name) -> to_atom_name(Name) ->
erlang:binary_to_atom(Name). erlang:binary_to_atom(Name).
@ -123,14 +130,16 @@ to_atom_name(Name) ->
%% callbacks %% callbacks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata()) -spec on_provider_loaded(emqx_exhook_pb:provider_loaded_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()} {ok, emqx_exhook_pb:loaded_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_provider_loaded(Req, Md) -> on_provider_loaded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{hooks => [ {ok,
#{
hooks => [
#{name => <<"client.connect">>}, #{name => <<"client.connect">>},
#{name => <<"client.connack">>}, #{name => <<"client.connack">>},
#{name => <<"client.connected">>}, #{name => <<"client.connected">>},
@ -149,49 +158,52 @@ on_provider_loaded(Req, Md) ->
#{name => <<"message.publish">>}, #{name => <<"message.publish">>},
#{name => <<"message.delivered">>}, #{name => <<"message.delivered">>},
#{name => <<"message.acked">>}, #{name => <<"message.acked">>},
#{name => <<"message.dropped">>}]}, Md}. #{name => <<"message.dropped">>}
-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) ]
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} },
Md}.
-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) ->
{ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_provider_unloaded(Req, Md) -> on_provider_unloaded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata()) -spec on_client_connect(emqx_exhook_pb:client_connect_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_connect(Req, Md) -> on_client_connect(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata()) -spec on_client_connack(emqx_exhook_pb:client_connack_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_connack(Req, Md) -> on_client_connack(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata()) -spec on_client_connected(emqx_exhook_pb:client_connected_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_connected(Req, Md) -> on_client_connected(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata()) -spec on_client_disconnected(emqx_exhook_pb:client_disconnected_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_disconnected(Req, Md) -> on_client_disconnected(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata()) -spec on_client_authenticate(emqx_exhook_pb:client_authenticate_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) -> on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
@ -199,20 +211,32 @@ on_client_authenticate(#{clientinfo := #{username := Username}} = Req, Md) ->
%% some cases for testing %% some cases for testing
case Username of case Username of
<<"baduser">> -> <<"baduser">> ->
{ok, #{type => 'STOP_AND_RETURN', {ok,
value => {bool_result, false}}, Md}; #{
type => 'STOP_AND_RETURN',
value => {bool_result, false}
},
Md};
<<"gooduser">> -> <<"gooduser">> ->
{ok, #{type => 'STOP_AND_RETURN', {ok,
value => {bool_result, true}}, Md}; #{
type => 'STOP_AND_RETURN',
value => {bool_result, true}
},
Md};
<<"normaluser">> -> <<"normaluser">> ->
{ok, #{type => 'CONTINUE', {ok,
value => {bool_result, true}}, Md}; #{
type => 'CONTINUE',
value => {bool_result, true}
},
Md};
_ -> _ ->
{ok, #{type => 'IGNORE'}, Md} {ok, #{type => 'IGNORE'}, Md}
end. end.
-spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) -spec on_client_authorize(emqx_exhook_pb:client_authorize_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) -> on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
@ -220,92 +244,104 @@ on_client_authorize(#{clientinfo := #{username := Username}} = Req, Md) ->
%% some cases for testing %% some cases for testing
case Username of case Username of
<<"baduser">> -> <<"baduser">> ->
{ok, #{type => 'STOP_AND_RETURN', {ok,
value => {bool_result, false}}, Md}; #{
type => 'STOP_AND_RETURN',
value => {bool_result, false}
},
Md};
<<"gooduser">> -> <<"gooduser">> ->
{ok, #{type => 'STOP_AND_RETURN', {ok,
value => {bool_result, true}}, Md}; #{
type => 'STOP_AND_RETURN',
value => {bool_result, true}
},
Md};
<<"normaluser">> -> <<"normaluser">> ->
{ok, #{type => 'CONTINUE', {ok,
value => {bool_result, true}}, Md}; #{
type => 'CONTINUE',
value => {bool_result, true}
},
Md};
_ -> _ ->
{ok, #{type => 'IGNORE'}, Md} {ok, #{type => 'IGNORE'}, Md}
end. end.
-spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata()) -spec on_client_subscribe(emqx_exhook_pb:client_subscribe_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_subscribe(Req, Md) -> on_client_subscribe(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata()) -spec on_client_unsubscribe(emqx_exhook_pb:client_unsubscribe_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_client_unsubscribe(Req, Md) -> on_client_unsubscribe(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata()) -spec on_session_created(emqx_exhook_pb:session_created_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_created(Req, Md) -> on_session_created(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata()) -spec on_session_subscribed(emqx_exhook_pb:session_subscribed_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_subscribed(Req, Md) -> on_session_subscribed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata()) -spec on_session_unsubscribed(emqx_exhook_pb:session_unsubscribed_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_unsubscribed(Req, Md) -> on_session_unsubscribed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata()) -spec on_session_resumed(emqx_exhook_pb:session_resumed_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_resumed(Req, Md) -> on_session_resumed(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata()) -spec on_session_discarded(emqx_exhook_pb:session_discarded_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_discarded(Req, Md) -> on_session_discarded(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_takenover(emqx_exhook_pb:session_takenover_request(), grpc:metadata()) -spec on_session_takenover(emqx_exhook_pb:session_takenover_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_takenover(Req, Md) -> on_session_takenover(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata()) -spec on_session_terminated(emqx_exhook_pb:session_terminated_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_session_terminated(Req, Md) -> on_session_terminated(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata()) -spec on_message_publish(emqx_exhook_pb:message_publish_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:valued_response(), grpc:metadata()} {ok, emqx_exhook_pb:valued_response(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_message_publish(#{message := #{from := From} = Msg} = Req, Md) -> on_message_publish(#{message := #{from := From} = Msg} = Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
@ -313,49 +349,66 @@ on_message_publish(#{message := #{from := From} = Msg} = Req, Md) ->
%% some cases for testing %% some cases for testing
case From of case From of
<<"baduser">> -> <<"baduser">> ->
NMsg = deny(Msg#{qos => 0, NMsg = deny(Msg#{
qos => 0,
topic => <<"">>, topic => <<"">>,
payload => <<"">> payload => <<"">>
}), }),
{ok, #{type => 'STOP_AND_RETURN', {ok,
value => {message, NMsg}}, Md}; #{
type => 'STOP_AND_RETURN',
value => {message, NMsg}
},
Md};
<<"gooduser">> -> <<"gooduser">> ->
NMsg = allow(Msg#{topic => From, NMsg = allow(Msg#{
payload => From}), topic => From,
{ok, #{type => 'STOP_AND_RETURN', payload => From
value => {message, NMsg}}, Md}; }),
{ok,
#{
type => 'STOP_AND_RETURN',
value => {message, NMsg}
},
Md};
_ -> _ ->
{ok, #{type => 'IGNORE'}, Md} {ok, #{type => 'IGNORE'}, Md}
end. end.
deny(Msg) -> deny(Msg) ->
NHeader = maps:put(<<"allow_publish">>, <<"false">>, NHeader = maps:put(
maps:get(headers, Msg, #{})), <<"allow_publish">>,
<<"false">>,
maps:get(headers, Msg, #{})
),
maps:put(headers, NHeader, Msg). maps:put(headers, NHeader, Msg).
allow(Msg) -> allow(Msg) ->
NHeader = maps:put(<<"allow_publish">>, <<"true">>, NHeader = maps:put(
maps:get(headers, Msg, #{})), <<"allow_publish">>,
<<"true">>,
maps:get(headers, Msg, #{})
),
maps:put(headers, NHeader, Msg). maps:put(headers, NHeader, Msg).
-spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata()) -spec on_message_delivered(emqx_exhook_pb:message_delivered_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_message_delivered(Req, Md) -> on_message_delivered(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata()) -spec on_message_dropped(emqx_exhook_pb:message_dropped_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_message_dropped(Req, Md) -> on_message_dropped(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
{ok, #{}, Md}. {ok, #{}, Md}.
-spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata()) -spec on_message_acked(emqx_exhook_pb:message_acked_request(), grpc:metadata()) ->
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} {ok, emqx_exhook_pb:empty_success(), grpc:metadata()}
| {error, grpc_cowboy_h:error_response()}. | {error, grpc_cowboy_h:error_response()}.
on_message_acked(Req, Md) -> on_message_acked(Req, Md) ->
?MODULE:in({?FUNCTION_NAME, Req}), ?MODULE:in({?FUNCTION_NAME, Req}),

View File

@ -26,19 +26,20 @@
-define(TARGET_HOOK, 'message.publish'). -define(TARGET_HOOK, 'message.publish').
-define(CONF, <<" -define(CONF, <<
exhook { "\n"
servers = [ "exhook {\n"
{ name = succed, " servers = [\n"
url = \"http://127.0.0.1:9000\" " { name = succed,\n"
}, " url = \"http://127.0.0.1:9000\"\n"
{ name = failed, " },\n"
failed_action = ignore, " { name = failed,\n"
url = \"http://127.0.0.1:9001\" " failed_action = ignore,\n"
}, " url = \"http://127.0.0.1:9001\"\n"
] " },\n"
} " ]\n"
">>). "}\n"
>>).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Setups %% Setups
@ -133,14 +134,18 @@ t_hooks_metrics(_) ->
repeat(Repeat, 5), repeat(Repeat, 5),
timer:sleep(200), timer:sleep(200),
HM = emqx_exhook_metrics:hooks_metrics(<<"succed">>), HM = emqx_exhook_metrics:hooks_metrics(<<"succed">>),
?assertMatch(#{'message.publish' := ?assertMatch(
#{failed := 0, succeed := 5}}, HM) #{
'message.publish' :=
#{failed := 0, succeed := 5}
},
HM
)
end, end,
with_connection(Test), with_connection(Test),
ok. ok.
t_on_server_deleted(_) -> t_on_server_deleted(_) ->
Test = fun(C) -> Test = fun(C) ->
Repeat = fun() -> Repeat = fun() ->
emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0) emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0)
@ -165,7 +170,8 @@ clear_metrics() ->
ets:delete_all_objects(?HOOKS_METRICS). ets:delete_all_objects(?HOOKS_METRICS).
init_injections(Injects) -> init_injections(Injects) ->
lists:map(fun({Name, _}) -> lists:map(
fun({Name, _}) ->
Str = erlang:atom_to_list(Name), Str = erlang:atom_to_list(Name),
case lists:prefix("on_", Str) of case lists:prefix("on_", Str) of
true -> true ->
@ -183,10 +189,12 @@ init_injections(Injects) ->
false false
end end
end, end,
emqx_exhook_demo_svr:module_info(exports)). emqx_exhook_demo_svr:module_info(exports)
).
hook_injects() -> hook_injects() ->
#{?SvrFun(<<"failed">>, emqx_exhook_server:hk2func(?TARGET_HOOK)) => #{
?SvrFun(<<"failed">>, emqx_exhook_server:hk2func(?TARGET_HOOK)) =>
fun(_Req, _Md) -> fun(_Req, _Md) ->
{error, "Error due to test"} {error, "Error due to test"}
end, end,
@ -201,14 +209,17 @@ hook_injects() ->
}. }.
with_connection(Fun) -> with_connection(Fun) ->
{ok, C} = emqtt:start_link([{host, "localhost"}, {ok, C} = emqtt:start_link([
{host, "localhost"},
{port, 1883}, {port, 1883},
{username, <<"admin">>}, {username, <<"admin">>},
{clientid, <<"exhook_tester">>}]), {clientid, <<"exhook_tester">>}
]),
{ok, _} = emqtt:connect(C), {ok, _} = emqtt:connect(C),
try try
Fun(C) Fun(C)
catch Type:Error:Trace -> catch
Type:Error:Trace ->
emqtt:stop(C), emqtt:stop(C),
erlang:raise(Type, Error, Trace) erlang:raise(Type, Error, Trace)
end. end.

View File

@ -19,78 +19,97 @@
-include_lib("proper/include/proper.hrl"). -include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-import(emqx_proper_types, -import(
[ conninfo/0 emqx_proper_types,
, clientinfo/0 [
, sessioninfo/0 conninfo/0,
, message/0 clientinfo/0,
, connack_return_code/0 sessioninfo/0,
, topictab/0 message/0,
, topic/0 connack_return_code/0,
, subopts/0 topictab/0,
]). topic/0,
subopts/0
-define(CONF_DEFAULT, <<"
exhook {
servers =
[ { name = default,
url = \"http://127.0.0.1:9000\"
}
] ]
} ).
">>).
-define(CONF_DEFAULT, <<
"\n"
"exhook {\n"
" servers =\n"
" [ { name = default,\n"
" url = \"http://127.0.0.1:9000\"\n"
" }\n"
" ]\n"
"}\n"
>>).
-define(ALL(Vars, Types, Exprs), -define(ALL(Vars, Types, Exprs),
?SETUP(fun() -> ?SETUP(
fun() ->
State = do_setup(), State = do_setup(),
fun() -> do_teardown(State) end fun() -> do_teardown(State) end
end, ?FORALL(Vars, Types, Exprs))). end,
?FORALL(Vars, Types, Exprs)
)
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Properties %% Properties
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
prop_client_connect() -> prop_client_connect() ->
?ALL({ConnInfo, ConnProps}, ?ALL(
{ConnInfo, ConnProps},
{conninfo(), conn_properties()}, {conninfo(), conn_properties()},
begin begin
ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]), ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]),
{'on_client_connect', Resp} = emqx_exhook_demo_svr:take(), {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{props => properties(ConnProps), #{
props => properties(ConnProps),
conninfo => from_conninfo(ConnInfo) conninfo => from_conninfo(ConnInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_connack() -> prop_client_connack() ->
?ALL({ConnInfo, Rc, AckProps}, ?ALL(
{ConnInfo, Rc, AckProps},
{conninfo(), connack_return_code(), ack_properties()}, {conninfo(), connack_return_code(), ack_properties()},
begin begin
ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]), ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]),
{'on_client_connack', Resp} = emqx_exhook_demo_svr:take(), {'on_client_connack', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{props => properties(AckProps), #{
props => properties(AckProps),
result_code => atom_to_binary(Rc, utf8), result_code => atom_to_binary(Rc, utf8),
conninfo => from_conninfo(ConnInfo) conninfo => from_conninfo(ConnInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_authenticate() -> prop_client_authenticate() ->
?ALL({ClientInfo0, AuthResult}, ?ALL(
{ClientInfo0, AuthResult},
{clientinfo(), authresult()}, {clientinfo(), authresult()},
begin begin
ClientInfo = inject_magic_into(username, ClientInfo0), ClientInfo = inject_magic_into(username, ClientInfo0),
OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult), OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult),
ExpectedAuthResult = case maps:get(username, ClientInfo) of ExpectedAuthResult =
<<"baduser">> -> {error, not_authorized}; case maps:get(username, ClientInfo) of
<<"gooduser">> -> ok; <<"baduser">> ->
<<"normaluser">> -> ok; {error, not_authorized};
_ -> case AuthResult of <<"gooduser">> ->
ok;
<<"normaluser">> ->
ok;
_ ->
case AuthResult of
ok -> ok; ok -> ok;
_ -> {error, not_authorized} _ -> {error, not_authorized}
end end
@ -99,24 +118,28 @@ prop_client_authenticate() ->
{'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(), {'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{result => authresult_to_bool(AuthResult), #{
result => authresult_to_bool(AuthResult),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_authorize() -> prop_client_authorize() ->
?ALL({ClientInfo0, PubSub, Topic, Result}, ?ALL(
{clientinfo(), oneof([publish, subscribe]), {ClientInfo0, PubSub, Topic, Result},
topic(), oneof([allow, deny])}, {clientinfo(), oneof([publish, subscribe]), topic(), oneof([allow, deny])},
begin begin
ClientInfo = inject_magic_into(username, ClientInfo0), ClientInfo = inject_magic_into(username, ClientInfo0),
OutResult = emqx_hooks:run_fold( OutResult = emqx_hooks:run_fold(
'client.authorize', 'client.authorize',
[ClientInfo, PubSub, Topic], [ClientInfo, PubSub, Topic],
Result), Result
ExpectedOutResult = case maps:get(username, ClientInfo) of ),
ExpectedOutResult =
case maps:get(username, ClientInfo) of
<<"baduser">> -> deny; <<"baduser">> -> deny;
<<"gooduser">> -> allow; <<"gooduser">> -> allow;
<<"normaluser">> -> allow; <<"normaluser">> -> allow;
@ -126,192 +149,231 @@ prop_client_authorize() ->
{'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(), {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{result => aclresult_to_bool(Result), #{
result => aclresult_to_bool(Result),
type => pubsub_to_enum(PubSub), type => pubsub_to_enum(PubSub),
topic => Topic, topic => Topic,
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_connected() -> prop_client_connected() ->
?ALL({ClientInfo, ConnInfo}, ?ALL(
{ClientInfo, ConnInfo},
{clientinfo(), conninfo()}, {clientinfo(), conninfo()},
begin begin
ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]), ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]),
{'on_client_connected', Resp} = emqx_exhook_demo_svr:take(), {'on_client_connected', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo) #{clientinfo => from_clientinfo(ClientInfo)},
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_disconnected() -> prop_client_disconnected() ->
?ALL({ClientInfo, Reason, ConnInfo}, ?ALL(
{ClientInfo, Reason, ConnInfo},
{clientinfo(), shutdown_reason(), conninfo()}, {clientinfo(), shutdown_reason(), conninfo()},
begin begin
ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]), ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]),
{'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(), {'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{reason => stringfy(Reason), #{
reason => stringfy(Reason),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_subscribe() -> prop_client_subscribe() ->
?ALL({ClientInfo, SubProps, TopicTab}, ?ALL(
{ClientInfo, SubProps, TopicTab},
{clientinfo(), sub_properties(), topictab()}, {clientinfo(), sub_properties(), topictab()},
begin begin
ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]), ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]),
{'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(), {'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{props => properties(SubProps), #{
props => properties(SubProps),
topic_filters => topicfilters(TopicTab), topic_filters => topicfilters(TopicTab),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_unsubscribe() -> prop_client_unsubscribe() ->
?ALL({ClientInfo, UnSubProps, TopicTab}, ?ALL(
{ClientInfo, UnSubProps, TopicTab},
{clientinfo(), unsub_properties(), topictab()}, {clientinfo(), unsub_properties(), topictab()},
begin begin
ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]), ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]),
{'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(), {'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{props => properties(UnSubProps), #{
props => properties(UnSubProps),
topic_filters => topicfilters(TopicTab), topic_filters => topicfilters(TopicTab),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_created() -> prop_session_created() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, ?ALL(
{ClientInfo, SessInfo},
{clientinfo(), sessioninfo()},
begin begin
ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]), ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]),
{'on_session_created', Resp} = emqx_exhook_demo_svr:take(), {'on_session_created', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo) #{clientinfo => from_clientinfo(ClientInfo)},
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_subscribed() -> prop_session_subscribed() ->
?ALL({ClientInfo, Topic, SubOpts}, ?ALL(
{ClientInfo, Topic, SubOpts},
{clientinfo(), topic(), subopts()}, {clientinfo(), topic(), subopts()},
begin begin
ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]), ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]),
{'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(), {'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{topic => Topic, #{
topic => Topic,
subopts => subopts(SubOpts), subopts => subopts(SubOpts),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_unsubscribed() -> prop_session_unsubscribed() ->
?ALL({ClientInfo, Topic, SubOpts}, ?ALL(
{ClientInfo, Topic, SubOpts},
{clientinfo(), topic(), subopts()}, {clientinfo(), topic(), subopts()},
begin begin
ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]), ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]),
{'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(), {'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{topic => Topic, #{
topic => Topic,
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_resumed() -> prop_session_resumed() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, ?ALL(
{ClientInfo, SessInfo},
{clientinfo(), sessioninfo()},
begin begin
ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]), ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]),
{'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(), {'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo) #{clientinfo => from_clientinfo(ClientInfo)},
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_discared() -> prop_session_discared() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, ?ALL(
{ClientInfo, SessInfo},
{clientinfo(), sessioninfo()},
begin begin
ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]), ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]),
{'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(), {'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo) #{clientinfo => from_clientinfo(ClientInfo)},
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_takenover() -> prop_session_takenover() ->
?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()}, ?ALL(
{ClientInfo, SessInfo},
{clientinfo(), sessioninfo()},
begin begin
ok = emqx_hooks:run('session.takenover', [ClientInfo, SessInfo]), ok = emqx_hooks:run('session.takenover', [ClientInfo, SessInfo]),
{'on_session_takenover', Resp} = emqx_exhook_demo_svr:take(), {'on_session_takenover', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo) #{clientinfo => from_clientinfo(ClientInfo)},
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_session_terminated() -> prop_session_terminated() ->
?ALL({ClientInfo, Reason, SessInfo}, ?ALL(
{ClientInfo, Reason, SessInfo},
{clientinfo(), shutdown_reason(), sessioninfo()}, {clientinfo(), shutdown_reason(), sessioninfo()},
begin begin
ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]), ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]),
{'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(), {'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{reason => stringfy(Reason), #{
reason => stringfy(Reason),
clientinfo => from_clientinfo(ClientInfo) clientinfo => from_clientinfo(ClientInfo)
}, },
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_message_publish() -> prop_message_publish() ->
?ALL(Msg0, message(), ?ALL(
Msg0,
message(),
begin begin
Msg = emqx_message:from_map( Msg = emqx_message:from_map(
inject_magic_into(from, emqx_message:to_map(Msg0))), inject_magic_into(from, emqx_message:to_map(Msg0))
OutMsg= emqx_hooks:run_fold('message.publish', [], Msg), ),
OutMsg = emqx_hooks:run_fold('message.publish', [], Msg),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> true ->
?assertEqual(Msg, OutMsg), ?assertEqual(Msg, OutMsg),
skip; skip;
_ -> _ ->
ExpectedOutMsg = case emqx_message:from(Msg) of ExpectedOutMsg =
case emqx_message:from(Msg) of
<<"baduser">> -> <<"baduser">> ->
MsgMap = #{headers := Headers} MsgMap =
= emqx_message:to_map(Msg), #{headers := Headers} =
emqx_message:to_map(Msg),
emqx_message:from_map( emqx_message:from_map(
MsgMap#{qos => 0, MsgMap#{
qos => 0,
topic => <<"">>, topic => <<"">>,
payload => <<"">>, payload => <<"">>,
headers => maps:put(allow_publish, false, Headers) headers => maps:put(allow_publish, false, Headers)
}); }
);
<<"gooduser">> = From -> <<"gooduser">> = From ->
MsgMap = #{headers := Headers} MsgMap =
= emqx_message:to_map(Msg), #{headers := Headers} =
emqx_message:to_map(Msg),
emqx_message:from_map( emqx_message:from_map(
MsgMap#{topic => From, MsgMap#{
topic => From,
payload => From, payload => From,
headers => maps:put(allow_publish, true, Headers) headers => maps:put(allow_publish, true, Headers)
}); }
);
_ -> _ ->
Msg Msg
end, end,
@ -319,63 +381,78 @@ prop_message_publish() ->
{'on_message_publish', Resp} = emqx_exhook_demo_svr:take(), {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{message => from_message(Msg) #{message => from_message(Msg)},
},
?assertEqual(Expected, Resp) ?assertEqual(Expected, Resp)
end, end,
true true
end). end
).
prop_message_dropped() -> prop_message_dropped() ->
?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()}, ?ALL(
{Msg, By, Reason},
{message(), hardcoded, shutdown_reason()},
begin begin
ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]), ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip; true ->
skip;
_ -> _ ->
{'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(), {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{reason => stringfy(Reason), #{
reason => stringfy(Reason),
message => from_message(Msg) message => from_message(Msg)
}, },
?assertEqual(Expected, Resp) ?assertEqual(Expected, Resp)
end, end,
true true
end). end
).
prop_message_delivered() -> prop_message_delivered() ->
?ALL({ClientInfo, Msg}, {clientinfo(), message()}, ?ALL(
{ClientInfo, Msg},
{clientinfo(), message()},
begin begin
ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]), ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip; true ->
skip;
_ -> _ ->
{'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(), {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo), #{
clientinfo => from_clientinfo(ClientInfo),
message => from_message(Msg) message => from_message(Msg)
}, },
?assertEqual(Expected, Resp) ?assertEqual(Expected, Resp)
end, end,
true true
end). end
).
prop_message_acked() -> prop_message_acked() ->
?ALL({ClientInfo, Msg}, {clientinfo(), message()}, ?ALL(
{ClientInfo, Msg},
{clientinfo(), message()},
begin begin
ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]), ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
true -> skip; true ->
skip;
_ -> _ ->
{'on_message_acked', Resp} = emqx_exhook_demo_svr:take(), {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(),
Expected = Expected =
#{clientinfo => from_clientinfo(ClientInfo), #{
clientinfo => from_clientinfo(ClientInfo),
message => from_message(Msg) message => from_message(Msg)
}, },
?assertEqual(Expected, Resp) ?assertEqual(Expected, Resp)
end, end,
true true
end). end
).
nodestr() -> nodestr() ->
stringfy(node()). stringfy(node()).
@ -388,7 +465,7 @@ sockport(#{sockname := {_, Port}}) ->
%% copied from emqx_exhook %% copied from emqx_exhook
ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256})); list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
ntoa(IP) -> ntoa(IP) ->
list_to_binary(inet_parse:ntoa(IP)). list_to_binary(inet_parse:ntoa(IP)).
@ -396,12 +473,22 @@ ntoa(IP) ->
maybe(undefined) -> <<>>; maybe(undefined) -> <<>>;
maybe(B) -> B. maybe(B) -> B.
properties(undefined) -> []; properties(undefined) ->
[];
properties(M) when is_map(M) -> properties(M) when is_map(M) ->
maps:fold(fun(K, V, Acc) -> maps:fold(
[#{name => stringfy(K), fun(K, V, Acc) ->
value => stringfy(V)} | Acc] [
end, [], M). #{
name => stringfy(K),
value => stringfy(V)
}
| Acc
]
end,
[],
M
).
topicfilters(Tfs) when is_list(Tfs) -> topicfilters(Tfs) when is_list(Tfs) ->
[#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs]. [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
@ -417,7 +504,8 @@ stringfy(Term) ->
unicode:characters_to_binary((io_lib:format("~0p", [Term]))). unicode:characters_to_binary((io_lib:format("~0p", [Term]))).
subopts(SubOpts) -> subopts(SubOpts) ->
#{qos => maps:get(qos, SubOpts, 0), #{
qos => maps:get(qos, SubOpts, 0),
rh => maps:get(rh, SubOpts, 0), rh => maps:get(rh, SubOpts, 0),
rap => maps:get(rap, SubOpts, 0), rap => maps:get(rap, SubOpts, 0),
nl => maps:get(nl, SubOpts, 0), nl => maps:get(nl, SubOpts, 0),
@ -434,7 +522,8 @@ pubsub_to_enum(publish) -> 'PUBLISH';
pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
from_conninfo(ConnInfo) -> from_conninfo(ConnInfo) ->
#{node => nodestr(), #{
node => nodestr(),
clientid => maps:get(clientid, ConnInfo), clientid => maps:get(clientid, ConnInfo),
username => maybe(maps:get(username, ConnInfo, <<>>)), username => maybe(maps:get(username, ConnInfo, <<>>)),
peerhost => peerhost(ConnInfo), peerhost => peerhost(ConnInfo),
@ -445,7 +534,8 @@ from_conninfo(ConnInfo) ->
}. }.
from_clientinfo(ClientInfo) -> from_clientinfo(ClientInfo) ->
#{node => nodestr(), #{
node => nodestr(),
clientid => maps:get(clientid, ClientInfo), clientid => maps:get(clientid, ClientInfo),
username => maybe(maps:get(username, ClientInfo, <<>>)), username => maybe(maps:get(username, ClientInfo, <<>>)),
password => maybe(maps:get(password, ClientInfo, <<>>)), password => maybe(maps:get(password, ClientInfo, <<>>)),
@ -460,7 +550,8 @@ from_clientinfo(ClientInfo) ->
}. }.
from_message(Msg) -> from_message(Msg) ->
#{node => nodestr(), #{
node => nodestr(),
id => emqx_guid:to_hexstr(emqx_message:id(Msg)), id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
qos => emqx_message:qos(Msg), qos => emqx_message:qos(Msg),
from => stringfy(emqx_message:from(Msg)), from => stringfy(emqx_message:from(Msg)),
@ -468,7 +559,8 @@ from_message(Msg) ->
payload => emqx_message:payload(Msg), payload => emqx_message:payload(Msg),
timestamp => emqx_message:timestamp(Msg), timestamp => emqx_message:timestamp(Msg),
headers => emqx_exhook_handler:headers( headers => emqx_exhook_handler:headers(
emqx_message:get_headers(Msg)) emqx_message:get_headers(Msg)
)
}. }.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -513,17 +605,19 @@ shutdown_reason() ->
oneof([utf8(), {shutdown, emqx_proper_types:limited_atom()}]). oneof([utf8(), {shutdown, emqx_proper_types:limited_atom()}]).
authresult() -> authresult() ->
?LET(RC, connack_return_code(), ?LET(
RC,
connack_return_code(),
case RC of case RC of
success -> ok; success -> ok;
_ -> {error, RC} _ -> {error, RC}
end). end
).
inject_magic_into(Key, Object) -> inject_magic_into(Key, Object) ->
case castspell() of case castspell() of
muggles -> Object; muggles -> Object;
Spell -> Spell -> Object#{Key => Spell}
Object#{Key => Spell}
end. end.
castspell() -> castspell() ->