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_shadow_vars, warn_unused_vars,
warn_unused_import, warn_shadow_vars,
warn_obsolete_guard, warn_unused_import,
debug_info, warn_obsolete_guard,
{parse_transform}]}. debug_info,
{parse_transform}
]}.
{xref_checks, [undefined_function_calls, undefined_functions, {xref_checks, [
locals_not_used, deprecated_function_calls, undefined_function_calls,
warnings_as_errors, deprecated_functions]}. undefined_functions,
locals_not_used,
deprecated_function_calls,
warnings_as_errors,
deprecated_functions
]}.
{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_v_1_hook_provider_bhvr, emqx_exhook_pb,
emqx_exhook_v_1_hook_provider_client]}. emqx_exhook_v_1_hook_provider_bhvr,
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

@ -1,9 +1,8 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{VSN, {VSN,
[ [
{<<".*">>, []} {<<".*">>, []}
], ],
[ [
{<<".*">>, []} {<<".*">>, []}
] ]}.
}.

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,12 +39,16 @@ 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()) ->
| {stop, term()}. {ok, 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,33 +95,43 @@ 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(),
NumServers = length(Servers), NumServers = length(Servers),
ServerInfo = ServerInfo =
lists:map( lists:map(
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,
driver => grpc %% currently, only grpc driver exists.
} driver => grpc
end, }
Servers), end,
#{ num_servers => NumServers Servers
, 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
@ -50,142 +53,174 @@ paths() -> ["/exhooks", "/exhooks/:name", "/exhooks/:name/move", "/exhooks/:name
schema(("/exhooks")) -> schema(("/exhooks")) ->
#{ #{
'operationId' => exhooks, 'operationId' => exhooks,
get => #{tags => ?TAGS, get => #{
desc => <<"List all servers">>, tags => ?TAGS,
responses => #{200 => mk(array(ref(detail_server_info)), #{})} desc => <<"List all servers">>,
}, responses => #{200 => mk(array(ref(detail_server_info)), #{})}
post => #{tags => ?TAGS, },
desc => <<"Add a servers">>, post => #{
'requestBody' => server_conf_schema(), tags => ?TAGS,
responses => #{201 => mk(ref(detail_server_info), #{}), desc => <<"Add a servers">>,
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) 'requestBody' => server_conf_schema(),
} responses => #{
} 201 => mk(ref(detail_server_info), #{}),
}; 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
}
}
};
schema("/exhooks/:name") -> schema("/exhooks/:name") ->
#{'operationId' => action_with_name, #{
get => #{tags => ?TAGS, 'operationId' => action_with_name,
desc => <<"Get the detail information of server">>, get => #{
parameters => params_server_name_in_path(), tags => ?TAGS,
responses => #{200 => mk(ref(detail_server_info), #{}), desc => <<"Get the detail information of server">>,
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>) parameters => params_server_name_in_path(),
} responses => #{
}, 200 => mk(ref(detail_server_info), #{}),
put => #{tags => ?TAGS, 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
desc => <<"Update the server">>, }
parameters => params_server_name_in_path(), },
'requestBody' => server_conf_schema(), put => #{
responses => #{200 => <<>>, tags => ?TAGS,
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>), desc => <<"Update the server">>,
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) parameters => params_server_name_in_path(),
} 'requestBody' => server_conf_schema(),
}, responses => #{
delete => #{tags => ?TAGS, 200 => <<>>,
desc => <<"Delete the server">>, 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
parameters => params_server_name_in_path(), 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
responses => #{204 => <<>>, }
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) },
} delete => #{
} tags => ?TAGS,
}; desc => <<"Delete the server">>,
parameters => params_server_name_in_path(),
responses => #{
204 => <<>>,
500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
}
}
};
schema("/exhooks/:name/hooks") -> schema("/exhooks/:name/hooks") ->
#{'operationId' => server_hooks, #{
get => #{tags => ?TAGS, 'operationId' => server_hooks,
desc => <<"Get the hooks information of server">>, get => #{
parameters => params_server_name_in_path(), tags => ?TAGS,
responses => #{200 => mk(array(ref(list_hook_info)), #{}), desc => <<"Get the hooks information of server">>,
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>) parameters => params_server_name_in_path(),
} responses => #{
} 200 => mk(array(ref(list_hook_info)), #{}),
}; 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
};
schema("/exhooks/:name/move") -> schema("/exhooks/:name/move") ->
#{'operationId' => move, #{
post => #{tags => ?TAGS, 'operationId' => move,
desc => post => #{
<<"Move the server.\n", tags => ?TAGS,
"NOTE: The position should be \"front|rear|before:{name}|after:{name}\"\n">>, desc =>
parameters => params_server_name_in_path(), <<"Move the server.\n",
'requestBody' => emqx_dashboard_swagger:schema_with_examples( "NOTE: The position should be \"front|rear|before:{name}|after:{name}\"\n">>,
ref(move_req), parameters => params_server_name_in_path(),
position_example()), 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
responses => #{204 => <<"No Content">>, ref(move_req),
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>), position_example()
500 => error_codes([?BAD_RPC], <<"Bad RPC">>) ),
} responses => #{
} 204 => <<"No Content">>,
}. 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
}
}
}.
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, [
required => true, {name,
example => <<"default">>})} mk(string(), #{
in => path,
required => true,
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), },
#{ name => "default" schema_with_example(
, enable => true ref(server_config),
, url => <<"http://127.0.0.1:8081">> #{
, request_timeout => "5s" name => "default",
, failed_action => deny enable => true,
, auto_reconnect => "60s" url => <<"http://127.0.0.1:8081">>,
, pool_size => 8 request_timeout => "5s",
, ssl => SSL failed_action => deny,
}). 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,67 +236,86 @@ 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, #{
message => <<"Server not found">> code => <<"BAD_REQUEST">>,
}}; message => <<"Server not found">>
}};
{ok, {error, Reason}} -> {ok, {error, Reason}} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
message => unicode:characters_to_binary( code => <<"BAD_REQUEST">>,
io_lib:format("Error Reason:~p~n", [Reason])) message => unicode:characters_to_binary(
}}; io_lib:format("Error Reason:~p~n", [Reason])
)
}};
{ok, _} -> {ok, _} ->
{200}; {200};
{error, Error} -> {error, Error} ->
{500, #{code => <<"BAD_RPC">>, {500, #{
message => Error code => <<"BAD_RPC">>,
}} 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, #{
message => Error code => <<"BAD_RPC">>,
}} message => Error
}}
end. end.
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, #{
message => <<"Server not found">> code => <<"BAD_REQUEST">>,
}}; message => <<"Server not found">>
}};
{error, Error} -> {error, Error} ->
{500, #{code => <<"BAD_RPC">>, {500, #{
message => Error code => <<"BAD_RPC">>,
}} message => Error
}}
end; end;
{error, invalid_position} -> {error, invalid_position} ->
{400, #{code => <<"BAD_REQUEST">>, {400, #{
message => <<"Invalid Position">> code => <<"BAD_REQUEST">>,
}} message => <<"Invalid Position">>
}}
end. end.
server_hooks(get, #{bindings := #{name := Name}}) -> 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, #{
message => <<"Server not found">> code => <<"BAD_REQUEST">>,
}}; message => <<"Server not found">>
}};
_ -> _ ->
Info = get_nodes_server_hooks_info(Name), Info = get_nodes_server_hooks_info(Name),
{200, Info} {200, Info}
@ -272,9 +325,10 @@ 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, #{
message => <<"Server not found">> code => <<"BAD_REQUEST">>,
}}; message => <<"Server not found">>
}};
{value, Conf} -> {value, Conf} ->
NodeStatus = nodes_server_info(Name), NodeStatus = nodes_server_info(Name),
{200, maps:merge(Conf, NodeStatus)} {200, maps:merge(Conf, NodeStatus)}
@ -292,33 +346,34 @@ 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(
[#{node => Node, status => error} | StatusL], T,
[#{node => Node, metrics => Default} | MetricsL], [#{node => Node, status => error} | StatusL],
ServerName, [#{node => Node, metrics => Default} | MetricsL],
Default); ServerName,
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(
T, T,
[#{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, #{
node_metrics => MetricsL, metrics => Metrics,
node_status => StatusL, node_metrics => MetricsL,
hooks => emqx_exhook_mgr:hooks(ServerName) node_status => StatusL,
}. hooks => emqx_exhook_mgr:hooks(ServerName)
}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% GET /exhooks/{name} %% GET /exhooks/{name}
@ -329,39 +384,41 @@ 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(
Name, T,
Default, Name,
[#{node => Node, status => error} | StatusL], Default,
[#{node => Node, metrics => Default} | MetricsL] [#{node => Node, status => error} | StatusL],
); [#{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(
Name, T,
Default, Name,
[#{node => Node, status => Status} | StatusL], Default,
[#{node => Node, metrics => Metrics} | MetricsL] [#{node => Node, status => Status} | StatusL],
); [#{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), #{
node_status => StatusL, metrics => emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
node_metrics => MetricsL, node_status => StatusL,
hooks => emqx_exhook_mgr:hooks(Name) node_metrics => MetricsL,
}. hooks => emqx_exhook_mgr:hooks(Name)
}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% GET /exhooks/{name}/hooks %% GET /exhooks/{name}/hooks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
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)
end), end),
Default = emqx_exhook_metrics:new_metrics_info(), Default = emqx_exhook_metrics:new_metrics_info(),
get_nodes_server_hooks_info(Hooks, AllInfos, Default, []) get_nodes_server_hooks_info(Hooks, AllInfos, Default, [])
end. end.
@ -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}.
@ -391,31 +445,39 @@ fill_server_hooks_info([], _Name, _Default, MetricsL) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec call_cluster(fun(([node()]) -> emqx_rpc:erpc_multicall(A))) -> -spec call_cluster(fun(([node()]) -> emqx_rpc:erpc_multicall(A))) ->
[{node(), A | {error, _Err}}]. [{node(), A | {error, _Err}}].
call_cluster(Fun) -> call_cluster(Fun) ->
Nodes = mria_mnesia:running_nodes(), Nodes = mria_mnesia:running_nodes(),
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">>) ->
{ok, ?CMD_MOVE_FRONT}; {ok, ?CMD_MOVE_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 = #{
props => properties(Props) conninfo => conninfo(ConnInfo),
}, 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 = #{
result_code => stringfy(Rc), conninfo => conninfo(ConnInfo),
props => properties(Props)}, result_code => stringfy(Rc),
props => properties(Props)
},
cast('client.connack', Req). cast('client.connack', Req).
on_client_connected(ClientInfo, _ConnInfo) -> on_client_connected(ClientInfo, _ConnInfo) ->
@ -85,9 +94,10 @@ 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 = #{
reason => stringfy(Reason) clientinfo => clientinfo(ClientInfo),
}, reason => stringfy(Reason)
},
cast('client.disconnected', Req). cast('client.disconnected', Req).
on_client_authenticate(ClientInfo, AuthResult) -> on_client_authenticate(ClientInfo, AuthResult) ->
@ -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 = #{
result => Bool clientinfo => clientinfo(ClientInfo),
}, 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,35 +133,49 @@ 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 =
publish -> 'PUBLISH'; case PubSub of
subscribe -> 'SUBSCRIBE' publish -> 'PUBLISH';
end, subscribe -> 'SUBSCRIBE'
Req = #{clientinfo => clientinfo(ClientInfo), end,
type => Type, Req = #{
topic => Topic, clientinfo => clientinfo(ClientInfo),
result => Bool type => Type,
}, topic => Topic,
case call_fold('client.authorize', Req, result => Bool
fun merge_responsed_bool/2) of },
case
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 = #{
props => properties(Props), clientinfo => clientinfo(ClientInfo),
topic_filters => topicfilters(TopicFilters) props => properties(Props),
}, 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 = #{
props => properties(Props), clientinfo => clientinfo(ClientInfo),
topic_filters => topicfilters(TopicFilters) props => properties(Props),
}, topic_filters => topicfilters(TopicFilters)
},
cast('client.unsubscribe', Req). cast('client.unsubscribe', Req).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -153,16 +187,18 @@ 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 = #{
topic => Topic, clientinfo => clientinfo(ClientInfo),
subopts => maps:with([qos, share, rh, rap, nl], SubOpts) topic => Topic,
}, 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 = #{
topic => Topic clientinfo => clientinfo(ClientInfo),
}, topic => Topic
},
cast('session.unsubscribed', Req). cast('session.unsubscribed', Req).
on_session_resumed(ClientInfo, _SessInfo) -> on_session_resumed(ClientInfo, _SessInfo) ->
@ -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,110 +228,159 @@ 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 = #{
reason => stringfy(Reason) message => message(Message),
}, reason => stringfy(Reason)
},
cast('message.dropped', Req). cast('message.dropped', Req).
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 = #{
message => message(Message) clientinfo => clientinfo(ClientInfo),
}, message => message(Message)
},
cast('message.delivered', Req). cast('message.delivered', Req).
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 = #{
message => message(Message) clientinfo => clientinfo(ClientInfo),
}, message => message(Message)
},
cast('message.acked', Req). cast('message.acked', Req).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% 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 =
peername := {Peerhost, _}, #{
sockname := {_, SockPort}}) -> clientid := ClientId,
peername := {Peerhost, _},
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()), #{
clientid => ClientId, node => stringfy(node()),
username => maybe(Username), clientid => ClientId,
peerhost => ntoa(Peerhost), username => maybe(Username),
sockport => SockPort, peerhost => ntoa(Peerhost),
proto_name => ProtoName, sockport => SockPort,
proto_ver => stringfy(ProtoVer), proto_name => ProtoName,
keepalive => Keepalive}. proto_ver => stringfy(ProtoVer),
keepalive => Keepalive
}.
clientinfo(ClientInfo = clientinfo(
#{clientid := ClientId, username := Username, peerhost := PeerHost, ClientInfo =
sockport := SockPort, protocol := Protocol, mountpoint := Mountpoiont}) -> #{
#{node => stringfy(node()), clientid := ClientId,
clientid => ClientId, username := Username,
username => maybe(Username), peerhost := PeerHost,
password => maybe(maps:get(password, ClientInfo, undefined)), sockport := SockPort,
peerhost => ntoa(PeerHost), protocol := Protocol,
sockport => SockPort, mountpoint := Mountpoiont
protocol => stringfy(Protocol), }
mountpoint => maybe(Mountpoiont), ) ->
is_superuser => maps:get(is_superuser, ClientInfo, false), #{
anonymous => maps:get(anonymous, ClientInfo, true), node => stringfy(node()),
cn => maybe(maps:get(cn, ClientInfo, undefined)), clientid => ClientId,
dn => maybe(maps:get(dn, ClientInfo, undefined))}. username => maybe(Username),
password => maybe(maps:get(password, ClientInfo, undefined)),
peerhost => ntoa(PeerHost),
sockport => SockPort,
protocol => stringfy(Protocol),
mountpoint => maybe(Mountpoiont),
is_superuser => maps:get(is_superuser, ClientInfo, false),
anonymous => maps:get(anonymous, ClientInfo, true),
cn => maybe(maps:get(cn, 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,
id => emqx_guid:to_hexstr(Id), from = From,
qos => Qos, topic = Topic,
from => stringfy(From), payload = Payload,
topic => Topic, timestamp = Ts,
payload => Payload, headers = Headers
timestamp => Ts, }) ->
headers => headers(Headers) #{
}. node => stringfy(node()),
id => emqx_guid:to_hexstr(Id),
qos => Qos,
from => stringfy(From),
topic => Topic,
payload => Payload,
timestamp => Ts,
headers => headers(Headers)
}.
headers(Headers) -> headers(Headers) ->
Ls = [username, protocol, peerhost, allow_publish], Ls = [username, protocol, peerhost, allow_publish],
maps:fold( maps:fold(
fun fun
(_, undefined, Acc) -> (_, undefined, Acc) ->
Acc; %% Ignore undefined value %% Ignore undefined value
(K, V, Acc) -> Acc;
case lists:member(K, Ls) of (K, V, Acc) ->
true -> case lists:member(K, Ls) of
Acc#{atom_to_binary(K) => bin(K, V)}; true ->
_ -> Acc#{atom_to_binary(K) => bin(K, V)};
Acc _ ->
end Acc
end, #{}, Headers). end
end,
#{},
Headers
).
bin(K, V) when K == username; bin(K, V) when
K == protocol; K == username;
K == allow_publish -> K == protocol;
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,33 +19,43 @@
-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().
-type hookpoint() :: emqx_exhook_server:hookpoint(). -type hookpoint() :: emqx_exhook_server:hookpoint().
-type index() :: {server_name(), hookpoint()}. -type index() :: {server_name(), hookpoint()}.
-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,77 +64,113 @@
%%% 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, #{
failed => 0, succeed => 0,
rate => 0, failed => 0,
max_rate => 0 rate => 0,
}. max_rate => 0
}.
-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(
window_rate = WindowRate, #metrics{
max_rate = MaxRate} = Metrics, rate = Rate,
_) -> window_rate = WindowRate,
case calc_metric(WindowRate, Interval) of max_rate = MaxRate
Rate -> true; } = Metrics,
NewRate -> _
MaxRate2 = erlang:max(MaxRate, NewRate), ) ->
Metrics2 = Metrics#metrics{rate = NewRate, case calc_metric(WindowRate, Interval) of
window_rate = 0, Rate ->
max_rate = MaxRate2}, true;
ets:insert(?HOOKS_METRICS, Metrics2) NewRate ->
end MaxRate2 = erlang:max(MaxRate, NewRate),
end, Metrics2 = Metrics#metrics{
rate = NewRate,
window_rate = 0,
max_rate = MaxRate2
},
ets:insert(?HOOKS_METRICS, Metrics2)
end
end,
ets:foldl(Fun, true, ?HOOKS_METRICS). ets:foldl(Fun, true, ?HOOKS_METRICS).
-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(
failed = Failed, #metrics{
rate = Rate, succeed = Succeed,
max_rate = MaxRate}, failed = Failed,
Acc) -> rate = Rate,
[#{ succeed => Succeed max_rate = MaxRate
, failed => Failed },
, rate => Rate Acc
, max_rate => MaxRate ) ->
} | Acc] [
end, #{
succeed => Succeed,
failed => Failed,
rate => Rate,
max_rate => MaxRate
}
| Acc
]
end,
AllMetrics = lists:foldl(Fold, [], Hooks), AllMetrics = lists:foldl(Fold, [], Hooks),
metrics_aggregate(AllMetrics). metrics_aggregate(AllMetrics).
@ -133,21 +179,25 @@ 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(
succeed = Succeed, #metrics{
failed = Failed, index = ?INDEX(ServerName, _),
rate = Rate, succeed = Succeed,
max_rate = MaxRate failed = Failed,
}, rate = Rate,
Acc) -> max_rate = MaxRate
SvrGroup = maps:get(ServerName, Acc, []), },
Metrics = #{ succeed => Succeed Acc
, failed => Failed ) ->
, rate => Rate SvrGroup = maps:get(ServerName, Acc, []),
, max_rate => MaxRate Metrics = #{
}, succeed => Succeed,
Acc#{ServerName => [Metrics | SvrGroup]} failed => Failed,
end, rate => Rate,
max_rate => MaxRate
},
Acc#{ServerName => [Metrics | SvrGroup]}
end,
GroupBySever = lists:foldl(GroupFun, #{}, AllMetrics), GroupBySever = lists:foldl(GroupFun, #{}, AllMetrics),
@ -156,21 +206,30 @@ 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(
succeed = Succeed, #metrics{
failed = Failed, index = ?INDEX(_, HookPoint),
rate = Rate, succeed = Succeed,
max_rate = MaxRate}, failed = Failed,
Acc) -> rate = Rate,
Acc#{HookPoint => #{ succeed => Succeed max_rate = MaxRate
, failed => Failed },
, rate => Rate Acc
, max_rate => MaxRate ) ->
}} Acc#{
end, HookPoint => #{
succeed => Succeed,
failed => Failed,
rate => Rate,
max_rate => MaxRate
}
}
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,19 +252,22 @@ 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(
Index, ?HOOKS_METRICS,
[{#metrics.window_rate, 1}, {Pos, 1}], Index,
Default), [{#metrics.window_rate, 1}, {Pos, 1}],
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().
calc_metric(Val, Interval) -> calc_metric(Val, Interval) ->
@ -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#{
max_rate := MaxRate div Len rate := Rate div Len,
}. max_rate := MaxRate div Len
}.

View File

@ -26,66 +26,73 @@
-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
running := servers(), -type state() :: #{
%% Wait to reload servers running := servers(),
waiting := servers(), %% Wait to reload servers
%% Marked stopped servers waiting := servers(),
stopped := servers(), %% Marked stopped servers
%% Timer references stopped := servers(),
trefs := map(), %% Timer references
orders := orders() trefs := map(),
}. 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() ::
| rear front
| {before, binary()} | rear
| {'after', binary()}. | {before, binary()}
| {'after', binary()}.
-type orders() :: #{server_name() => integer()}. -type orders() :: #{server_name() => integer()}.
-type server_info() :: #{name := server_name(), -type server_info() :: #{
status := running | waiting | stopped, name := server_name(),
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,9 +103,10 @@
%% APIs %% APIs
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec start_link() -> ignore -spec start_link() ->
| {ok, pid()} ignore
| {error, any()}. | {ok, pid()}
| {error, any()}.
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@ -137,7 +145,7 @@ roots() ->
update_config(KeyPath, UpdateReq) -> update_config(KeyPath, UpdateReq) ->
case emqx_conf:update(KeyPath, UpdateReq, #{override_to => cluster}) of case emqx_conf:update(KeyPath, UpdateReq, #{override_to => cluster}) of
{ok, UpdateResult} -> {ok, UpdateResult} ->
#{post_config_update := #{?MODULE := Result}} = UpdateResult, #{post_config_update := #{?MODULE := Result}} = UpdateResult,
{ok, Result}; {ok, Result};
Error -> Error ->
@ -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(
running => Running, #{
stopped => Stopped, waiting => Waiting,
trefs => #{}, running => Running,
orders => Orders stopped => Stopped,
})}. trefs => #{},
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(
waiting := Waiting, list,
stopped := Stopped, _From,
orders := Orders}) -> State = #{
running := Running,
waiting := Waiting,
stopped := Stopped,
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,
stopped := Stopped #{
} = State2} = do_unload_server(ToDelete, State), orders := Orders,
stopped := Stopped
} = State2} = do_unload_server(ToDelete, State),
State3 = State2#{stopped := maps:remove(ToDelete, Stopped), State3 = State2#{
orders := maps:remove(ToDelete, Orders) stopped := maps:remove(ToDelete, Stopped),
}, 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,
waiting := Waiting, _From,
stopped := Stopped} = State) -> #{
running := Running,
waiting := Waiting,
stopped := Stopped
} = State
) ->
MakeStatus = fun(Status, Servers, Acc) -> MakeStatus = fun(Status, Servers, Acc) ->
lists:foldl(fun(Name, IAcc) -> IAcc#{Name => Status} end, lists:foldl(
Acc, fun(Name, IAcc) -> IAcc#{Name => Status} end,
maps:keys(Servers)) Acc,
end, maps:keys(Servers)
Status = lists:foldl(fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end, )
#{}, end,
[{running, Running}, {waiting, Waiting}, {stopped, Stopped}]), Status = lists:foldl(
fun({Status, Servers}, Acc) -> MakeStatus(Status, Servers, Acc) end,
#{},
[{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(
{ok, NAccIn} = do_unload_server(Name, AccIn), fun(Name, _, AccIn) ->
NAccIn {ok, NAccIn} = do_unload_server(Name, AccIn),
end, State, Running), NAccIn
end,
State,
Running
),
_ = unload_exhooks(), _ = unload_exhooks(),
ok. ok.
@ -362,12 +396,15 @@ 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, already_started}, state()} {{error, not_found}, state()}
| {ok, state()}. | {{error, already_started}, state()}
| {ok, state()}.
do_load_server(Name, State = #{orders := Orders}) -> do_load_server(Name, State = #{orders := Orders}) ->
case where_is_server(Name, State) of case where_is_server(Name, State) of
not_found -> not_found ->
@ -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,45 +438,58 @@ 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(
stopped := maps:put(Name, Options, Stopped) Name,
} State#{
)}; waiting := Waiting2,
stopped := maps:put(Name, Options, Stopped)
}
)};
{running, Running} -> {running, Running} ->
Service = server(Name), Service = server(Name),
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#{
stopped := maps:put(Name, Options, Stopped) running := Running2,
}}; 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(
stopped := Stopped, State = #{
trefs := TRefs}) -> waiting := Waiting,
stopped := Stopped,
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#{
stopped := Stopped2, waiting := Waitting2,
trefs := TRefs2}. stopped := Stopped2,
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,54 +499,49 @@ 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:remove(Name, Waiting), maps:next(Iter),
maps:put(Name, Opts, Stopped), maps:remove(Name, Waiting),
TimerRef). maps:put(Name, Opts, Stopped),
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}
end. end.
-spec do_move(binary(), position(), list(server_options())) -> -spec do_move(binary(), position(), list(server_options())) ->
not_found | list(server_options()). not_found | list(server_options()).
do_move(Name, Position, ConfL) -> do_move(Name, Position, ConfL) ->
move(ConfL, Name, Position, []). move(ConfL, Name, Position, []).
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,48 +553,49 @@ 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, #{
end, status => Status,
hooks => hooks(Name)
})
| Acc
]
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())) ->
| list(server_options()). not_found
| list(server_options()).
replace_conf(Name, ReplaceFun, ConfL) -> replace_conf(Name, ReplaceFun, ConfL) ->
replace_conf(ConfL, Name, ReplaceFun, []). replace_conf(ConfL, Name, ReplaceFun, []).
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()) ->
| {{error, term()}, state()}. {ok, 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
false -> false ->
@ -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(
maps:get(A, Orders) < maps:get(B, Orders); fun
(#{name := A}, #{name := B}) -> (A, B) when is_binary(A) ->
maps:get(A, Orders) < maps:get(B, Orders) maps:get(A, Orders) < maps:get(B, Orders);
end, (#{name := A}, #{name := B}) ->
Names). maps:get(A, Orders) < maps:get(B, Orders)
end,
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(
"When gRPC is not available, exhook tries to request the gRPC service at " duration(),
"that interval and reinitialize the list of mounted hooks." #{
})} default => "5s",
, {pool_size, desc => "The timeout to request gRPC server."
sc(integer(), }
#{ default => 8 )},
, example => 8 {failed_action, failed_action()},
, desc => "The process pool size for gRPC client." {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 "
"that interval and reinitialize the list of mounted hooks."
}
)},
{pool_size,
sc(
integer(),
#{
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 " #{
"to the gRPC server fails for any reason." default => deny,
}). desc =>
"The value that is returned when the request "
"to the gRPC server fails for any reason."
}
).
server_config() -> server_config() ->
fields(server). fields(server).

View File

@ -22,56 +22,59 @@
-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)
name := binary(), -type server() :: #{
%% The function options name := binary(),
options := map(), %% The function options
%% gRPC channel pid options := map(),
channel := pid(), %% gRPC channel pid
%% Registered hook names and options channel := pid(),
hookspec := #{hookpoint() => map()}, %% Registered hook names and options
%% Metrcis name prefix hookspec := #{hookpoint() => map()},
prefix := list() %% Metrcis name prefix
}. prefix := list()
}.
-type hookpoint() ::
-type hookpoint() :: 'client.connect' 'client.connect'
| 'client.connack' | 'client.connack'
| 'client.connected' | 'client.connected'
| 'client.disconnected' | 'client.disconnected'
| 'client.authenticate' | 'client.authenticate'
| 'client.authorize' | 'client.authorize'
| 'client.subscribe' | 'client.subscribe'
| 'client.unsubscribe' | 'client.unsubscribe'
| 'session.created' | 'session.created'
| 'session.subscribed' | 'session.subscribed'
| 'session.unsubscribed' | 'session.unsubscribed'
| 'session.resumed' | 'session.resumed'
| 'session.discarded' | 'session.discarded'
| 'session.takenover' | 'session.takenover'
| 'session.terminated' | 'session.terminated'
| 'message.publish' | 'message.publish'
| 'message.delivered' | 'message.delivered'
| 'message.acked' | 'message.acked'
| 'message.dropped'. | 'message.dropped'.
-export_type([server/0, hookpoint/0]). -export_type([server/0, hookpoint/0]).
@ -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
Name, emqx_exhook_sup:start_grpc_client_channel(
SvrAddr, Name,
ClientOpts) of SvrAddr,
ClientOpts
)
of
{ok, _ChannPoolPid} -> {ok, _ChannPoolPid} ->
case do_init(Name, ReqOpts) of case do_init(Name, ReqOpts) of
{ok, HookSpecs} -> {ok, HookSpecs} ->
@ -102,42 +107,53 @@ 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, #{
options => ReqOpts, name => Name,
channel => _ChannPoolPid, options => ReqOpts,
hookspec => HookSpecs, channel => _ChannPoolPid,
prefix => Prefix }}; hookspec => HookSpecs,
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)}, [
{certfile, maps:get(certfile, MapOpts, undefined)}, {cacertfile, maps:get(cacertfile, MapOpts, undefined)},
{keyfile, maps:get(keyfile, MapOpts, undefined)} {certfile, maps:get(certfile, 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 ->
error({bad_server_url, URL, Error}) error({bad_server_url, URL, 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,19 +178,24 @@ 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 ->
channel_name => ChannName, ?SLOG(error, #{
reason => Reason, msg => "failed_to_init_channel",
stacktrace => Stk}), channel_name => ChannName,
{error, Reason} reason => Reason,
stacktrace => Stk
}),
{error, Reason}
end; end;
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
@ -184,60 +205,80 @@ 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(
case maps:get(name, HookSpec, undefined) of fun(HookSpec, Acc) ->
undefined -> Acc; case maps:get(name, HookSpec, undefined) of
Name0 -> undefined ->
Name = try Acc;
binary_to_existing_atom(Name0, utf8) Name0 ->
catch T:R:_ -> {T,R} Name =
end, try
case {lists:member(Name, AvailableHooks), binary_to_existing_atom(Name0, utf8)
lists:member(Name, MessageHooks)} of catch
{false, _} -> T:R:_ -> {T, R}
error({unknown_hookpoint, Name}); end,
{true, false} -> case {lists:member(Name, AvailableHooks), lists:member(Name, MessageHooks)} of
Acc#{Name => #{}}; {false, _} ->
{true, true} -> error({unknown_hookpoint, Name});
Acc#{Name => #{ {true, false} ->
topics => maps:get(topics, HookSpec, [])}} Acc#{Name => #{}};
end {true, true} ->
end Acc#{
end, #{}, HookSpecs). Name => #{
topics => maps:get(topics, HookSpec, [])
}
}
end
end
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(
case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of fun(Hookpoint) ->
false -> case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of
?SLOG(error, #{msg => "skipped_unknown_hookpoint", hookpoint => Hookpoint}); false ->
{Hookpoint, {M, F, A}} -> ?SLOG(error, #{msg => "skipped_unknown_hookpoint", hookpoint => Hookpoint});
emqx_hooks:put(Hookpoint, {M, F, A}), {Hookpoint, {M, F, A}} ->
ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, 1}, {Hookpoint, 0}) emqx_hooks:put(Hookpoint, {M, F, A}),
end ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, 1}, {Hookpoint, 0})
end, maps:keys(HookSpecs)). end
end,
maps:keys(HookSpecs)
).
may_unload_hooks(HookSpecs) -> may_unload_hooks(HookSpecs) ->
lists:foreach(fun(Hookpoint) -> lists:foreach(
case ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, -1}, {Hookpoint, 0}) of fun(Hookpoint) ->
Cnt when Cnt =< 0 -> case ets:update_counter(?HOOKS_REF_COUNTER, Hookpoint, {2, -1}, {Hookpoint, 0}) of
case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of Cnt when Cnt =< 0 ->
{Hookpoint, {M, F, _A}} -> case lists:keyfind(Hookpoint, 1, ?ENABLED_HOOKS) of
emqx_hooks:del(Hookpoint, {M, F}); {Hookpoint, {M, F, _A}} ->
_ -> ok emqx_hooks:del(Hookpoint, {M, F});
end, _ ->
ets:delete(?HOOKS_REF_COUNTER, Hookpoint); ok
_ -> ok end,
end ets:delete(?HOOKS_REF_COUNTER, Hookpoint);
end, maps:keys(HookSpecs)). _ ->
ok
end
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,
end, params => Params
}
| Acc
]
end,
maps:fold(FoldFun, [], Hooks). maps:fold(FoldFun, [], Hooks).
-spec call(hookpoint(), map(), server()) -> ignore -spec call(hookpoint(), map(), server()) ->
| {ok, Resp :: term()} ignore
| {error, term()}. | {ok, Resp :: term()}
call(Hookpoint, Req, #{name := ChannName, options := ReqOpts, | {error, term()}.
hookspec := Hooks, prefix := Prefix}) -> call(Hookpoint, Req, #{
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 ->
#{message := #{topic := Topic}} = Req, true;
match_topic_filter(Topic, maps:get(topics, Opts, [])) _ ->
end, #{message := #{topic := Topic}} = Req,
match_topic_filter(Topic, maps:get(topics, Opts, []))
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
@ -52,9 +53,10 @@ 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").
@ -31,16 +32,16 @@ introduced_in() ->
"5.0.0". "5.0.0".
-spec all_servers_info([node()]) -> -spec all_servers_info([node()]) ->
emqx_rpc:erpc_multicall(map()). emqx_rpc:erpc_multicall(map()).
all_servers_info(Nodes) -> all_servers_info(Nodes) ->
erpc:multicall(Nodes, emqx_exhook_mgr, all_servers_info, []). erpc:multicall(Nodes, emqx_exhook_mgr, all_servers_info, []).
-spec server_info([node()], emqx_exhook_mgr:server_name()) -> -spec server_info([node()], emqx_exhook_mgr:server_name()) ->
emqx_rpc:erpc_multicall(map()). emqx_rpc:erpc_multicall(map()).
server_info(Nodes, Name) -> server_info(Nodes, Name) ->
erpc:multicall(Nodes, emqx_exhook_mgr, server_info, [Name]). erpc:multicall(Nodes, emqx_exhook_mgr, server_info, [Name]).
-spec server_hooks_metrics([node()], emqx_exhook_mgr:server_name()) -> -spec server_hooks_metrics([node()], emqx_exhook_mgr:server_name()) ->
emqx_rpc:erpc_multicall(emqx_exhook_metrics:hooks_metrics()). emqx_rpc:erpc_multicall(emqx_exhook_metrics:hooks_metrics()).
server_hooks_metrics(Nodes, Name) -> server_hooks_metrics(Nodes, Name) ->
erpc:multicall(Nodes, emqx_exhook_mgr, server_hooks_metrics, [Name]). erpc:multicall(Nodes, emqx_exhook_mgr, server_hooks_metrics, [Name]).

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 = #{
username => <<"usera">>, clientid => <<"user-id-1">>,
peerhost => {127,0,0,1}, username => <<"usera">>,
sockport => 1883, peerhost => {127, 0, 0, 1},
protocol => mqtt, sockport => 1883,
mountpoint => undefined protocol => mqtt,
}, mountpoint => undefined
?assertMatch({stop, {error, not_authorized}}, },
emqx_exhook_handler:on_client_authenticate(ClientInfo, #{auth_result => success})), ?assertMatch(
{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(
status := _, #{
hooks := _}, H). name := _,
status := _,
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 = #{
<<"url">> => <<"http://127.0.0.1:9001">>, <<"name">> => Name,
<<"enable">> => false}, <<"url">> => <<"http://127.0.0.1:9001">>,
<<"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([
{port, 1883}, {host, "localhost"},
{username, <<"gooduser">>}, {port, 1883},
{clientid, <<"exhook_gooduser">>}]), {username, <<"gooduser">>},
{clientid, <<"exhook_gooduser">>}
]),
{ok, _} = emqtt:connect(C), {ok, _} = emqtt:connect(C),
%% pub/sub %% pub/sub
@ -212,13 +230,14 @@ t_handler(_) ->
ok. ok.
t_simulated_handler(_) -> t_simulated_handler(_) ->
ClientInfo = #{clientid => <<"user-id-1">>, ClientInfo = #{
username => <<"usera">>, clientid => <<"user-id-1">>,
peerhost => {127,0,0,1}, username => <<"usera">>,
sockport => 1883, peerhost => {127, 0, 0, 1},
protocol => mqtt, sockport => 1883,
mountpoint => undefined protocol => mqtt,
}, mountpoint => undefined
},
%% resume/takeover %% resume/takeover
ok = emqx_exhook_handler:on_session_resumed(ClientInfo, undefined), ok = emqx_exhook_handler:on_session_resumed(ClientInfo, undefined),
ok = emqx_exhook_handler:on_session_discarded(ClientInfo, undefined), ok = emqx_exhook_handler:on_session_discarded(ClientInfo, undefined),
@ -232,36 +251,38 @@ 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,
} = emqx_exhook:get_basic_usage_info(), servers := Servers
} = emqx_exhook:get_basic_usage_info(),
?assertEqual(1, NumServers), ?assertEqual(1, NumServers),
?assertMatch([_], Servers), ?assertMatch([_], Servers),
[#{driver := Driver, hooks := Hooks}] = Servers, [#{driver := Driver, hooks := Hooks}] = Servers,
?assertEqual(grpc, Driver), ?assertEqual(grpc, Driver),
?assertEqual( ?assertEqual(
[ [
'client.authenticate', 'client.authenticate',
'client.authorize', 'client.authorize',
'client.connack', 'client.connack',
'client.connect', 'client.connect',
'client.connected', 'client.connected',
'client.disconnected', 'client.disconnected',
'client.subscribe', 'client.subscribe',
'client.unsubscribe', 'client.unsubscribe',
'message.acked', 'message.acked',
'message.delivered', 'message.delivered',
'message.dropped', 'message.dropped',
'message.publish', 'message.publish',
'session.created', 'session.created',
'session.discarded', 'session.discarded',
'session.resumed', 'session.resumed',
'session.subscribed', 'session.subscribed',
'session.takenover', 'session.takenover',
'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(
Name = element(2, E), fun(E) ->
Callbacks = element(3, E), Name = element(2, E),
case lists:any(fun is_exhook_callback/1, Callbacks) of Callbacks = element(3, E),
true -> {true, Name}; case lists:any(fun is_exhook_callback/1, Callbacks) of
_ -> false true -> {true, Name};
end _ -> false
end, ets:tab2list(emqx_hooks)). end
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(
metrics := _, #{
node_metrics := _, name := <<"default">>,
node_status := _, metrics := _,
hooks := _ node_metrics := _,
}, Svr). node_status := _,
hooks := _
},
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(
metrics := _, #{
node_metrics := _, name := <<"default">>,
node_status := _, metrics := _,
hooks := _ node_metrics := _,
}, Svr). node_status := _,
hooks := _
},
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#{
url => "http://127.0.0.1:9001" name => <<"test1">>,
}, url => "http://127.0.0.1:9001"
{ok, Data} = request_api(post, api_path(["exhooks"]), "", },
auth_header_(), Instance), {ok, Data} = request_api(
post,
api_path(["exhooks"]),
"",
auth_header_(),
Instance
),
Svr = decode_json(Data), Svr = decode_json(Data),
?assertMatch(#{name := <<"test1">>, ?assertMatch(
metrics := _, #{
node_metrics := _, name := <<"test1">>,
node_status := _, metrics := _,
hooks := _}, Svr), node_metrics := _,
node_status := _,
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(
auth_header_(), post,
#{position => <<"front">>}), api_path(["exhooks", "default", "move"]),
"",
auth_header_(),
#{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(
auth_header_(), post,
#{position => <<"rear">>}), api_path(["exhooks", "default", "move"]),
"",
auth_header_(),
#{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(
auth_header_(), post,
#{position => <<"before:test1">>}), api_path(["exhooks", "default", "move"]),
"",
auth_header_(),
#{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(
auth_header_(), post,
#{position => <<"after:test1">>}), api_path(["exhooks", "default", "move"]),
"",
auth_header_(),
#{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(
params := _, #{
metrics := _, name := _,
node_metrics := _ params := _,
}, Hook1). metrics := _,
node_metrics := _
},
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 =
"" -> Url; case QueryParams of
_ -> Url ++ "?" ++ QueryParams "" -> Url;
end, _ -> Url ++ "?" ++ QueryParams
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 =
"" -> Url; case QueryParams of
_ -> Url ++ "?" ++ QueryParams "" -> Url;
end, _ -> Url ++ "?" ++ QueryParams
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).
@ -62,7 +64,7 @@ start() ->
start(?NAME, ?PORT). start(?NAME, ?PORT).
start(Name, Port) -> start(Name, Port) ->
Pid = spawn(fun() -> mgr_main(Name, Port) end), Pid = spawn(fun() -> mgr_main(Name, Port) end),
register(to_atom_name(Name), Pid), register(to_atom_name(Name), Pid),
{ok, Pid}. {ok, Pid}.
@ -75,17 +77,20 @@ 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 = #{
services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr} protos => [emqx_exhook_pb],
}, services => #{'emqx.exhook.v1.HookProvider' => emqx_exhook_demo_svr}
},
Options = [], Options = [],
Svr = grpc:start_server(Name, Port, Services, Options), Svr = grpc:start_server(Name, Port, Services, Options),
mgr_loop([Svr, queue:new(), queue:new()]). mgr_loop([Svr, queue:new(), queue:new()]).
@ -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,240 +130,286 @@ 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,
#{name => <<"client.connect">>}, #{
#{name => <<"client.connack">>}, hooks => [
#{name => <<"client.connected">>}, #{name => <<"client.connect">>},
#{name => <<"client.disconnected">>}, #{name => <<"client.connack">>},
#{name => <<"client.authenticate">>}, #{name => <<"client.connected">>},
#{name => <<"client.authorize">>}, #{name => <<"client.disconnected">>},
#{name => <<"client.subscribe">>}, #{name => <<"client.authenticate">>},
#{name => <<"client.unsubscribe">>}, #{name => <<"client.authorize">>},
#{name => <<"session.created">>}, #{name => <<"client.subscribe">>},
#{name => <<"session.subscribed">>}, #{name => <<"client.unsubscribe">>},
#{name => <<"session.unsubscribed">>}, #{name => <<"session.created">>},
#{name => <<"session.resumed">>}, #{name => <<"session.subscribed">>},
#{name => <<"session.discarded">>}, #{name => <<"session.unsubscribed">>},
#{name => <<"session.takenover">>}, #{name => <<"session.resumed">>},
#{name => <<"session.terminated">>}, #{name => <<"session.discarded">>},
#{name => <<"message.publish">>}, #{name => <<"session.takenover">>},
#{name => <<"message.delivered">>}, #{name => <<"session.terminated">>},
#{name => <<"message.acked">>}, #{name => <<"message.publish">>},
#{name => <<"message.dropped">>}]}, Md}. #{name => <<"message.delivered">>},
-spec on_provider_unloaded(emqx_exhook_pb:provider_unloaded_request(), grpc:metadata()) #{name => <<"message.acked">>},
-> {ok, emqx_exhook_pb:empty_success(), grpc:metadata()} #{name => <<"message.dropped">>}
| {error, grpc_cowboy_h:error_response()}. ]
},
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()}.
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}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% 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}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% 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}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]),
%% some cases for testing %% some cases for testing
case From of case From of
<<"baduser">> -> <<"baduser">> ->
NMsg = deny(Msg#{qos => 0, NMsg = deny(Msg#{
topic => <<"">>, qos => 0,
payload => <<"">> topic => <<"">>,
}), payload => <<"">>
{ok, #{type => 'STOP_AND_RETURN', }),
value => {message, NMsg}}, Md}; {ok,
#{
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}),
%io:format("fun: ~p, req: ~0p~n", [?FUNCTION_NAME, Req]), %io:format("fun: ~p, req: ~0p~n", [?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
@ -78,11 +79,11 @@ end_per_testcase(_, Config) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
t_servers_metrics(_Cfg) -> t_servers_metrics(_Cfg) ->
Test = fun(C) -> Test = fun(C) ->
Repeat = fun() -> Repeat = fun() ->
emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0) emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0)
end, end,
repeat(Repeat, 10) repeat(Repeat, 10)
end, end,
with_connection(Test), with_connection(Test),
timer:sleep(200), timer:sleep(200),
@ -99,54 +100,58 @@ t_servers_metrics(_Cfg) ->
t_rate(_) -> t_rate(_) ->
Test = fun(C) -> Test = fun(C) ->
Repeat = fun() -> Repeat = fun() ->
emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0) emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0)
end, end,
repeat(Repeat, 5), repeat(Repeat, 5),
timer:sleep(200), timer:sleep(200),
emqx_exhook_metrics:update(timer:seconds(1)), emqx_exhook_metrics:update(timer:seconds(1)),
SM = emqx_exhook_metrics:server_metrics(<<"succed">>), SM = emqx_exhook_metrics:server_metrics(<<"succed">>),
?assertMatch(#{rate := 5, max_rate := 5}, SM), ?assertMatch(#{rate := 5, max_rate := 5}, SM),
repeat(Repeat, 6), repeat(Repeat, 6),
timer:sleep(200), timer:sleep(200),
emqx_exhook_metrics:update(timer:seconds(1)), emqx_exhook_metrics:update(timer:seconds(1)),
SM2 = emqx_exhook_metrics:server_metrics(<<"succed">>), SM2 = emqx_exhook_metrics:server_metrics(<<"succed">>),
?assertMatch(#{rate := 6, max_rate := 6}, SM2), ?assertMatch(#{rate := 6, max_rate := 6}, SM2),
repeat(Repeat, 3), repeat(Repeat, 3),
timer:sleep(200), timer:sleep(200),
emqx_exhook_metrics:update(timer:seconds(1)), emqx_exhook_metrics:update(timer:seconds(1)),
SM3 = emqx_exhook_metrics:server_metrics(<<"succed">>), SM3 = emqx_exhook_metrics:server_metrics(<<"succed">>),
?assertMatch(#{rate := 3, max_rate := 6}, SM3) ?assertMatch(#{rate := 3, max_rate := 6}, SM3)
end, end,
with_connection(Test), with_connection(Test),
ok. ok.
t_hooks_metrics(_) -> t_hooks_metrics(_) ->
Test = fun(C) -> Test = fun(C) ->
Repeat = fun() -> Repeat = fun() ->
emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0) emqtt:publish(C, <<"/exhook/metrics">>, <<>>, qos0)
end, end,
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) #{
end, 'message.publish' :=
#{failed := 0, succeed := 5}
},
HM
)
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)
end, end,
repeat(Repeat, 10) repeat(Repeat, 10)
end, end,
with_connection(Test), with_connection(Test),
timer:sleep(200), timer:sleep(200),
@ -165,50 +170,56 @@ 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(
Str = erlang:atom_to_list(Name), fun({Name, _}) ->
case lists:prefix("on_", Str) of Str = erlang:atom_to_list(Name),
true -> case lists:prefix("on_", Str) of
Action = fun(Req, #{<<"channel">> := SvrName} = Md) -> true ->
case maps:get(?SvrFun(SvrName, Name), Injects, undefined) of Action = fun(Req, #{<<"channel">> := SvrName} = Md) ->
undefined -> case maps:get(?SvrFun(SvrName, Name), Injects, undefined) of
meck:passthrough([Req, Md]); undefined ->
Injection -> meck:passthrough([Req, Md]);
Injection(Req, Md) Injection ->
end Injection(Req, Md)
end, end
end,
meck:expect(emqx_exhook_demo_svr, Name, Action); meck:expect(emqx_exhook_demo_svr, Name, Action);
_ -> _ ->
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)) => #{
fun(_Req, _Md) -> ?SvrFun(<<"failed">>, emqx_exhook_server:hk2func(?TARGET_HOOK)) =>
{error, "Error due to test"} fun(_Req, _Md) ->
end, {error, "Error due to test"}
?SvrFun(<<"failed">>, on_provider_loaded) => end,
fun(_Req, Md) -> ?SvrFun(<<"failed">>, on_provider_loaded) =>
{ok, #{hooks => [#{name => <<"message.publish">>}]}, Md} fun(_Req, Md) ->
end, {ok, #{hooks => [#{name => <<"message.publish">>}]}, Md}
?SvrFun(<<"succed">>, on_provider_loaded) => end,
fun(_Req, Md) -> ?SvrFun(<<"succed">>, on_provider_loaded) =>
{ok, #{hooks => [#{name => <<"message.publish">>}]}, Md} fun(_Req, Md) ->
end {ok, #{hooks => [#{name => <<"message.publish">>}]}, Md}
}. end
}.
with_connection(Fun) -> with_connection(Fun) ->
{ok, C} = emqtt:start_link([{host, "localhost"}, {ok, C} = emqtt:start_link([
{port, 1883}, {host, "localhost"},
{username, <<"admin">>}, {port, 1883},
{clientid, <<"exhook_tester">>}]), {username, <<"admin">>},
{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,363 +19,440 @@
-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(), conn_properties()}, {ConnInfo, ConnProps},
begin {conninfo(), conn_properties()},
ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]), begin
{'on_client_connect', Resp} = emqx_exhook_demo_svr:take(), ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]),
Expected = {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(),
#{props => properties(ConnProps), Expected =
conninfo => from_conninfo(ConnInfo) #{
props => properties(ConnProps),
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(), connack_return_code(), ack_properties()}, {ConnInfo, Rc, AckProps},
{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), #{
result_code => atom_to_binary(Rc, utf8), props => properties(AckProps),
conninfo => from_conninfo(ConnInfo) result_code => atom_to_binary(Rc, utf8),
}, conninfo => from_conninfo(ConnInfo)
},
?assertEqual(Expected, Resp), ?assertEqual(Expected, Resp),
true true
end). end
).
prop_client_authenticate() -> prop_client_authenticate() ->
?ALL({ClientInfo0, AuthResult}, ?ALL(
{clientinfo(), authresult()}, {ClientInfo0, 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 -> ok; ok;
_ -> {error, not_authorized} <<"normaluser">> ->
end ok;
end, _ ->
case AuthResult of
ok -> ok;
_ -> {error, not_authorized}
end
end,
?assertEqual(ExpectedAuthResult, OutAuthResult), ?assertEqual(ExpectedAuthResult, OutAuthResult),
{'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), #{
clientinfo => from_clientinfo(ClientInfo) result => authresult_to_bool(AuthResult),
}, 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 ),
<<"baduser">> -> deny; ExpectedOutResult =
<<"gooduser">> -> allow; case maps:get(username, ClientInfo) of
<<"normaluser">> -> allow; <<"baduser">> -> deny;
_ -> Result <<"gooduser">> -> allow;
end, <<"normaluser">> -> allow;
_ -> Result
end,
?assertEqual(ExpectedOutResult, OutResult), ?assertEqual(ExpectedOutResult, OutResult),
{'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), #{
type => pubsub_to_enum(PubSub), result => aclresult_to_bool(Result),
topic => Topic, type => pubsub_to_enum(PubSub),
clientinfo => from_clientinfo(ClientInfo) topic => Topic,
}, 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(), shutdown_reason(), conninfo()}, {ClientInfo, 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), #{
clientinfo => from_clientinfo(ClientInfo) reason => stringfy(Reason),
}, 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(), sub_properties(), topictab()}, {ClientInfo, SubProps, 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), #{
topic_filters => topicfilters(TopicTab), props => properties(SubProps),
clientinfo => from_clientinfo(ClientInfo) topic_filters => topicfilters(TopicTab),
}, 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(), unsub_properties(), topictab()}, {ClientInfo, UnSubProps, 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), #{
topic_filters => topicfilters(TopicTab), props => properties(UnSubProps),
clientinfo => from_clientinfo(ClientInfo) topic_filters => topicfilters(TopicTab),
}, 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, #{
subopts => subopts(SubOpts), topic => Topic,
clientinfo => from_clientinfo(ClientInfo) subopts => subopts(SubOpts),
}, 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, #{
clientinfo => from_clientinfo(ClientInfo) topic => Topic,
}, 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(), shutdown_reason(), sessioninfo()}, {ClientInfo, Reason, SessInfo},
{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), #{
clientinfo => from_clientinfo(ClientInfo) reason => stringfy(Reason),
}, 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 =
<<"baduser">> -> case emqx_message:from(Msg) of
MsgMap = #{headers := Headers} <<"baduser">> ->
= emqx_message:to_map(Msg), MsgMap =
emqx_message:from_map( #{headers := Headers} =
MsgMap#{qos => 0, emqx_message:to_map(Msg),
topic => <<"">>, emqx_message:from_map(
payload => <<"">>, MsgMap#{
headers => maps:put(allow_publish, false, Headers) qos => 0,
}); topic => <<"">>,
<<"gooduser">> = From -> payload => <<"">>,
MsgMap = #{headers := Headers} headers => maps:put(allow_publish, false, Headers)
= emqx_message:to_map(Msg), }
emqx_message:from_map( );
MsgMap#{topic => From, <<"gooduser">> = From ->
payload => From, MsgMap =
headers => maps:put(allow_publish, true, Headers) #{headers := Headers} =
}); emqx_message:to_map(Msg),
_ -> emqx_message:from_map(
Msg MsgMap#{
end, topic => From,
payload => From,
headers => maps:put(allow_publish, true, Headers)
}
);
_ ->
Msg
end,
?assertEqual(ExpectedOutMsg, OutMsg), ?assertEqual(ExpectedOutMsg, OutMsg),
{'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), #{
message => from_message(Msg) reason => stringfy(Reason),
}, 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), #{
message => from_message(Msg) clientinfo => from_clientinfo(ClientInfo),
}, 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), #{
message => from_message(Msg) clientinfo => from_clientinfo(ClientInfo),
}, 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,12 +504,13 @@ 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), #{
rh => maps:get(rh, SubOpts, 0), qos => maps:get(qos, SubOpts, 0),
rap => maps:get(rap, SubOpts, 0), rh => maps:get(rh, SubOpts, 0),
nl => maps:get(nl, SubOpts, 0), rap => maps:get(rap, SubOpts, 0),
share => maps:get(share, SubOpts, <<>>) nl => maps:get(nl, SubOpts, 0),
}. share => maps:get(share, SubOpts, <<>>)
}.
authresult_to_bool(AuthResult) -> authresult_to_bool(AuthResult) ->
AuthResult == ok. AuthResult == ok.
@ -434,42 +522,46 @@ pubsub_to_enum(publish) -> 'PUBLISH';
pubsub_to_enum(subscribe) -> 'SUBSCRIBE'. pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
from_conninfo(ConnInfo) -> from_conninfo(ConnInfo) ->
#{node => nodestr(), #{
clientid => maps:get(clientid, ConnInfo), node => nodestr(),
username => maybe(maps:get(username, ConnInfo, <<>>)), clientid => maps:get(clientid, ConnInfo),
peerhost => peerhost(ConnInfo), username => maybe(maps:get(username, ConnInfo, <<>>)),
sockport => sockport(ConnInfo), peerhost => peerhost(ConnInfo),
proto_name => maps:get(proto_name, ConnInfo), sockport => sockport(ConnInfo),
proto_ver => stringfy(maps:get(proto_ver, ConnInfo)), proto_name => maps:get(proto_name, ConnInfo),
keepalive => maps:get(keepalive, ConnInfo) proto_ver => stringfy(maps:get(proto_ver, ConnInfo)),
}. keepalive => maps:get(keepalive, ConnInfo)
}.
from_clientinfo(ClientInfo) -> from_clientinfo(ClientInfo) ->
#{node => nodestr(), #{
clientid => maps:get(clientid, ClientInfo), node => nodestr(),
username => maybe(maps:get(username, ClientInfo, <<>>)), clientid => maps:get(clientid, ClientInfo),
password => maybe(maps:get(password, ClientInfo, <<>>)), username => maybe(maps:get(username, ClientInfo, <<>>)),
peerhost => ntoa(maps:get(peerhost, ClientInfo)), password => maybe(maps:get(password, ClientInfo, <<>>)),
sockport => maps:get(sockport, ClientInfo), peerhost => ntoa(maps:get(peerhost, ClientInfo)),
protocol => stringfy(maps:get(protocol, ClientInfo)), sockport => maps:get(sockport, ClientInfo),
mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)), protocol => stringfy(maps:get(protocol, ClientInfo)),
is_superuser => maps:get(is_superuser, ClientInfo, false), mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)),
anonymous => maps:get(anonymous, ClientInfo, true), is_superuser => maps:get(is_superuser, ClientInfo, false),
cn => maybe(maps:get(cn, ClientInfo, <<>>)), anonymous => maps:get(anonymous, ClientInfo, true),
dn => maybe(maps:get(dn, ClientInfo, <<>>)) cn => maybe(maps:get(cn, ClientInfo, <<>>)),
dn => maybe(maps:get(dn, ClientInfo, <<>>))
}. }.
from_message(Msg) -> from_message(Msg) ->
#{node => nodestr(), #{
id => emqx_guid:to_hexstr(emqx_message:id(Msg)), node => nodestr(),
qos => emqx_message:qos(Msg), id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
from => stringfy(emqx_message:from(Msg)), qos => emqx_message:qos(Msg),
topic => emqx_message:topic(Msg), from => stringfy(emqx_message:from(Msg)),
payload => emqx_message:payload(Msg), topic => emqx_message:topic(Msg),
timestamp => emqx_message:timestamp(Msg), payload => emqx_message:payload(Msg),
headers => emqx_exhook_handler:headers( timestamp => emqx_message:timestamp(Msg),
emqx_message:get_headers(Msg)) headers => emqx_exhook_handler:headers(
}. emqx_message:get_headers(Msg)
)
}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Helper %% Helper
@ -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(
case RC of RC,
success -> ok; connack_return_code(),
_ -> {error, RC} case RC of
end). success -> ok;
_ -> {error, RC}
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() ->