emqx_exhook_api.erl 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2021-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_exhook_api).
  17. -behaviour(minirest_api).
  18. -include("emqx_exhook.hrl").
  19. -include_lib("typerefl/include/types.hrl").
  20. -include_lib("emqx/include/logger.hrl").
  21. -include_lib("hocon/include/hoconsc.hrl").
  22. -export([
  23. api_spec/0,
  24. paths/0,
  25. schema/1,
  26. fields/1,
  27. namespace/0
  28. ]).
  29. -export([
  30. exhooks/2,
  31. action_with_name/2,
  32. move/2,
  33. server_hooks/2
  34. ]).
  35. -import(hoconsc, [mk/1, mk/2, ref/1, enum/1, array/1, map/2]).
  36. -import(emqx_dashboard_swagger, [schema_with_example/2, error_codes/2]).
  37. -define(TAGS, [<<"ExHook">>]).
  38. -define(NOT_FOURD, 'NOT_FOUND').
  39. -define(BAD_REQUEST, 'BAD_REQUEST').
  40. -define(BAD_RPC, 'BAD_RPC').
  41. -define(ERR_BADARGS(REASON), begin
  42. R0 = err_msg(REASON),
  43. <<"Bad Arguments: ", R0/binary>>
  44. end).
  45. -dialyzer([
  46. {nowarn_function, [
  47. fill_cluster_server_info/5,
  48. nodes_server_info/5,
  49. fill_server_hooks_info/4
  50. ]}
  51. ]).
  52. %%--------------------------------------------------------------------
  53. %% schema
  54. %%--------------------------------------------------------------------
  55. namespace() -> "exhook".
  56. api_spec() ->
  57. emqx_dashboard_swagger:spec(?MODULE).
  58. paths() -> ["/exhooks", "/exhooks/:name", "/exhooks/:name/move", "/exhooks/:name/hooks"].
  59. schema(("/exhooks")) ->
  60. #{
  61. 'operationId' => exhooks,
  62. get => #{
  63. tags => ?TAGS,
  64. desc => ?DESC(list_all_servers),
  65. responses => #{200 => mk(array(ref(detail_server_info)))}
  66. },
  67. post => #{
  68. tags => ?TAGS,
  69. desc => ?DESC(add_server),
  70. 'requestBody' => server_conf_schema(),
  71. responses => #{
  72. 200 => mk(ref(detail_server_info)),
  73. 400 => error_codes([?BAD_REQUEST], <<"Already exists">>),
  74. 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
  75. }
  76. }
  77. };
  78. schema("/exhooks/:name") ->
  79. #{
  80. 'operationId' => action_with_name,
  81. get => #{
  82. tags => ?TAGS,
  83. desc => ?DESC(get_detail),
  84. parameters => params_server_name_in_path(),
  85. responses => #{
  86. 200 => mk(ref(detail_server_info)),
  87. 404 => error_codes([?NOT_FOURD], <<"Server not found">>)
  88. }
  89. },
  90. put => #{
  91. tags => ?TAGS,
  92. desc => ?DESC(update_server),
  93. parameters => params_server_name_in_path(),
  94. 'requestBody' => server_conf_schema(),
  95. responses => #{
  96. 200 => mk(ref(detail_server_info)),
  97. 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
  98. 404 => error_codes([?NOT_FOURD], <<"Server not found">>),
  99. 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
  100. }
  101. },
  102. delete => #{
  103. tags => ?TAGS,
  104. desc => ?DESC(delete_server),
  105. parameters => params_server_name_in_path(),
  106. responses => #{
  107. 204 => <<>>,
  108. 404 => error_codes([?NOT_FOURD], <<"Server not found">>),
  109. 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
  110. }
  111. }
  112. };
  113. schema("/exhooks/:name/hooks") ->
  114. #{
  115. 'operationId' => server_hooks,
  116. get => #{
  117. tags => ?TAGS,
  118. desc => ?DESC(get_hooks),
  119. parameters => params_server_name_in_path(),
  120. responses => #{
  121. 200 => mk(array(ref(list_hook_info))),
  122. 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
  123. }
  124. }
  125. };
  126. schema("/exhooks/:name/move") ->
  127. #{
  128. 'operationId' => move,
  129. post => #{
  130. tags => ?TAGS,
  131. desc => ?DESC(move_api),
  132. parameters => params_server_name_in_path(),
  133. 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
  134. ref(move_req),
  135. position_example()
  136. ),
  137. responses => #{
  138. 204 => <<"No Content">>,
  139. 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
  140. 500 => error_codes([?BAD_RPC], <<"Bad RPC">>)
  141. }
  142. }
  143. }.
  144. fields(move_req) ->
  145. [
  146. {position,
  147. mk(string(), #{
  148. required => true,
  149. desc => ?DESC(move_position),
  150. example => <<"front">>
  151. })}
  152. ];
  153. fields(detail_server_info) ->
  154. [
  155. {metrics, mk(ref(metrics), #{desc => ?DESC(server_metrics)})},
  156. {node_metrics, mk(array(ref(node_metrics)), #{desc => ?DESC(node_metrics)})},
  157. {node_status, mk(array(ref(node_status)), #{desc => ?DESC(node_status)})},
  158. {hooks, mk(array(ref(hook_info)))}
  159. ] ++ emqx_exhook_schema:server_config();
  160. fields(list_hook_info) ->
  161. [
  162. {name, mk(binary(), #{desc => ?DESC(hook_name)})},
  163. {params, mk(map(name, binary()), #{desc => ?DESC(hook_params)})},
  164. {metrics, mk(ref(metrics), #{desc => ?DESC(hook_metrics)})},
  165. {node_metrics, mk(array(ref(node_metrics)), #{desc => ?DESC(node_hook_metrics)})}
  166. ];
  167. fields(node_metrics) ->
  168. [
  169. {node, mk(string(), #{desc => ?DESC(node)})},
  170. {metrics, mk(ref(metrics), #{desc => ?DESC(metrics)})}
  171. ];
  172. fields(node_status) ->
  173. [
  174. {node, mk(string(), #{desc => ?DESC(node)})},
  175. {status,
  176. mk(enum([connected, connecting, disconnected, disabled, error]), #{
  177. desc => ?DESC(status)
  178. })}
  179. ];
  180. fields(hook_info) ->
  181. [
  182. {name, mk(binary(), #{desc => ?DESC(hook_name)})},
  183. {params, mk(map(name, binary()), #{desc => ?DESC(hook_params)})}
  184. ];
  185. fields(metrics) ->
  186. [
  187. {succeed, mk(integer(), #{desc => ?DESC(metric_succeed)})},
  188. {failed, mk(integer(), #{desc => ?DESC(metric_failed)})},
  189. {rate, mk(integer(), #{desc => ?DESC(metric_rate)})},
  190. {max_rate, mk(integer(), #{desc => ?DESC(metric_max_rate)})}
  191. ];
  192. fields(server_config) ->
  193. emqx_exhook_schema:server_config().
  194. params_server_name_in_path() ->
  195. [
  196. {name,
  197. mk(string(), #{
  198. desc => ?DESC(server_name),
  199. in => path,
  200. required => true,
  201. example => <<"default">>
  202. })}
  203. ].
  204. server_conf_schema() ->
  205. SSL = #{
  206. enable => false,
  207. cacertfile => <<"/etc/emqx/certs/cacert.pem">>,
  208. certfile => <<"/etc/emqx/certs/cert.pem">>,
  209. keyfile => <<"/etc/emqx/certs/key.pem">>
  210. },
  211. schema_with_example(
  212. ref(server_config),
  213. #{
  214. name => "default",
  215. enable => true,
  216. url => <<"http://127.0.0.1:8081">>,
  217. request_timeout => <<"5s">>,
  218. failed_action => deny,
  219. auto_reconnect => <<"60s">>,
  220. pool_size => 8,
  221. ssl => SSL
  222. }
  223. ).
  224. %%--------------------------------------------------------------------
  225. %% API
  226. %%--------------------------------------------------------------------
  227. exhooks(get, _) ->
  228. Confs = get_raw_config(),
  229. Infos = nodes_all_server_info(Confs),
  230. {200, Infos};
  231. exhooks(post, #{body := #{<<"name">> := Name} = Body}) ->
  232. case emqx_exhook_mgr:update_config([exhook, servers], {add, Body}) of
  233. {ok, _} ->
  234. get_nodes_server_info(Name);
  235. {error, already_exists} ->
  236. {400, #{
  237. code => <<"BAD_REQUEST">>,
  238. message => <<"Already exists">>
  239. }};
  240. {error, Reason} ->
  241. {400, #{
  242. code => <<"BAD_REQUEST">>,
  243. message => ?ERR_BADARGS(Reason)
  244. }}
  245. end.
  246. action_with_name(get, #{bindings := #{name := Name}}) ->
  247. get_nodes_server_info(Name);
  248. action_with_name(put, #{bindings := #{name := Name}, body := Body}) ->
  249. case
  250. emqx_exhook_mgr:update_config(
  251. [exhook, servers],
  252. {update, Name, Body}
  253. )
  254. of
  255. {ok, _} ->
  256. get_nodes_server_info(Name);
  257. {error, not_found} ->
  258. {404, #{
  259. code => <<"NOT_FOUND">>,
  260. message => <<"Server not found">>
  261. }};
  262. {error, Reason} ->
  263. {400, #{
  264. code => <<"BAD_REQUEST">>,
  265. message => ?ERR_BADARGS(Reason)
  266. }}
  267. end;
  268. action_with_name(delete, #{bindings := #{name := Name}}) ->
  269. case
  270. emqx_exhook_mgr:update_config(
  271. [exhook, servers],
  272. {delete, Name}
  273. )
  274. of
  275. {ok, _} ->
  276. {204};
  277. {error, not_found} ->
  278. {404, #{
  279. code => <<"BAD_REQUEST">>,
  280. message => <<"Server not found">>
  281. }};
  282. {error, Reason} ->
  283. {400, #{
  284. code => <<"BAD_REQUEST">>,
  285. message => ?ERR_BADARGS(Reason)
  286. }}
  287. end.
  288. move(post, #{bindings := #{name := Name}, body := #{<<"position">> := RawPosition}}) ->
  289. case parse_position(RawPosition) of
  290. {ok, Position} ->
  291. case
  292. emqx_exhook_mgr:update_config(
  293. [exhook, servers],
  294. {move, Name, Position}
  295. )
  296. of
  297. {ok, ok} ->
  298. {204};
  299. {error, not_found} ->
  300. {404, #{
  301. code => <<"BAD_REQUEST">>,
  302. message => <<"Server not found">>
  303. }};
  304. {error, Error} ->
  305. {500, #{
  306. code => <<"BAD_RPC">>,
  307. message => Error
  308. }}
  309. end;
  310. {error, invalid_position} ->
  311. {400, #{
  312. code => <<"BAD_REQUEST">>,
  313. message => <<"Invalid Position">>
  314. }}
  315. end.
  316. server_hooks(get, #{bindings := #{name := Name}}) ->
  317. Confs = get_raw_config(),
  318. case lists:search(fun(#{<<"name">> := CfgName}) -> CfgName =:= Name end, Confs) of
  319. false ->
  320. {400, #{
  321. code => <<"BAD_REQUEST">>,
  322. message => <<"Server not found">>
  323. }};
  324. _ ->
  325. Info = get_nodes_server_hooks_info(Name),
  326. {200, Info}
  327. end.
  328. get_nodes_server_info(Name) ->
  329. Confs = get_raw_config(),
  330. case lists:search(fun(#{<<"name">> := CfgName}) -> CfgName =:= Name end, Confs) of
  331. false ->
  332. {404, #{
  333. code => <<"BAD_REQUEST">>,
  334. message => <<"Server not found">>
  335. }};
  336. {value, Conf} ->
  337. NodeStatus = nodes_server_info(Name),
  338. {200, maps:merge(Conf, NodeStatus)}
  339. end.
  340. %%--------------------------------------------------------------------
  341. %% GET /exhooks
  342. %%--------------------------------------------------------------------
  343. nodes_all_server_info(ConfL) ->
  344. AllInfos = call_cluster(fun(Nodes) -> emqx_exhook_proto_v1:all_servers_info(Nodes) end),
  345. Default = emqx_exhook_metrics:new_metrics_info(),
  346. node_all_server_info(ConfL, AllInfos, Default, []).
  347. node_all_server_info([#{<<"name">> := ServerName} = Conf | T], AllInfos, Default, Acc) ->
  348. Info = fill_cluster_server_info(AllInfos, [], [], ServerName, Default),
  349. AllInfo = maps:merge(Conf, Info),
  350. node_all_server_info(T, AllInfos, Default, [AllInfo | Acc]);
  351. node_all_server_info([], _, _, Acc) ->
  352. lists:reverse(Acc).
  353. fill_cluster_server_info([{Node, {error, _}} | T], StatusL, MetricsL, ServerName, Default) ->
  354. fill_cluster_server_info(
  355. T,
  356. [#{node => Node, status => error} | StatusL],
  357. [#{node => Node, metrics => Default} | MetricsL],
  358. ServerName,
  359. Default
  360. );
  361. fill_cluster_server_info([{Node, Result} | T], StatusL, MetricsL, ServerName, Default) ->
  362. #{status := Status, metrics := Metrics} = Result,
  363. fill_cluster_server_info(
  364. T,
  365. [#{node => Node, status => maps:get(ServerName, Status, error)} | StatusL],
  366. [#{node => Node, metrics => maps:get(ServerName, Metrics, Default)} | MetricsL],
  367. ServerName,
  368. Default
  369. );
  370. fill_cluster_server_info([], StatusL, MetricsL, ServerName, _) ->
  371. Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
  372. #{
  373. metrics => Metrics,
  374. node_metrics => MetricsL,
  375. node_status => StatusL,
  376. hooks => emqx_exhook_mgr:hooks(ServerName)
  377. }.
  378. %%--------------------------------------------------------------------
  379. %% GET /exhooks/{name}
  380. %%--------------------------------------------------------------------
  381. nodes_server_info(Name) ->
  382. InfoL = call_cluster(fun(Nodes) -> emqx_exhook_proto_v1:server_info(Nodes, Name) end),
  383. Default = emqx_exhook_metrics:new_metrics_info(),
  384. nodes_server_info(InfoL, Name, Default, [], []).
  385. nodes_server_info([{Node, {error, _}} | T], Name, Default, StatusL, MetricsL) ->
  386. nodes_server_info(
  387. T,
  388. Name,
  389. Default,
  390. [#{node => Node, status => error} | StatusL],
  391. [#{node => Node, metrics => Default} | MetricsL]
  392. );
  393. nodes_server_info([{Node, Result} | T], Name, Default, StatusL, MetricsL) ->
  394. #{status := Status, metrics := Metrics} = Result,
  395. nodes_server_info(
  396. T,
  397. Name,
  398. Default,
  399. [#{node => Node, status => Status} | StatusL],
  400. [#{node => Node, metrics => Metrics} | MetricsL]
  401. );
  402. nodes_server_info([], Name, _, StatusL, MetricsL) ->
  403. #{
  404. metrics => emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
  405. node_status => StatusL,
  406. node_metrics => MetricsL,
  407. hooks => emqx_exhook_mgr:hooks(Name)
  408. }.
  409. %%--------------------------------------------------------------------
  410. %% GET /exhooks/{name}/hooks
  411. %%--------------------------------------------------------------------
  412. get_nodes_server_hooks_info(Name) ->
  413. case emqx_exhook_mgr:hooks(Name) of
  414. [] ->
  415. [];
  416. Hooks ->
  417. AllInfos = call_cluster(fun(Nodes) ->
  418. emqx_exhook_proto_v1:server_hooks_metrics(Nodes, Name)
  419. end),
  420. Default = emqx_exhook_metrics:new_metrics_info(),
  421. get_nodes_server_hooks_info(Hooks, AllInfos, Default, [])
  422. end.
  423. get_nodes_server_hooks_info([#{name := Name} = Spec | T], AllInfos, Default, Acc) ->
  424. Info = fill_server_hooks_info(AllInfos, Name, Default, []),
  425. AllInfo = maps:merge(Spec, Info),
  426. get_nodes_server_hooks_info(T, AllInfos, Default, [AllInfo | Acc]);
  427. get_nodes_server_hooks_info([], _, _, Acc) ->
  428. Acc.
  429. fill_server_hooks_info([{_, {error, _}} | T], Name, Default, MetricsL) ->
  430. fill_server_hooks_info(T, Name, Default, MetricsL);
  431. fill_server_hooks_info([{Node, MetricsMap} | T], Name, Default, MetricsL) ->
  432. Metrics = maps:get(Name, MetricsMap, Default),
  433. NodeMetrics = #{node => Node, metrics => Metrics},
  434. fill_server_hooks_info(T, Name, Default, [NodeMetrics | MetricsL]);
  435. fill_server_hooks_info([], _Name, _Default, MetricsL) ->
  436. Metrics = emqx_exhook_metrics:metrics_aggregate_by_key(metrics, MetricsL),
  437. #{metrics => Metrics, node_metrics => MetricsL}.
  438. %%--------------------------------------------------------------------
  439. %% cluster call
  440. %%--------------------------------------------------------------------
  441. -spec call_cluster(fun(([node()]) -> emqx_rpc:erpc_multicall(A))) ->
  442. [{node(), A | {error, _Err}}].
  443. call_cluster(Fun) ->
  444. Nodes = mria:running_nodes(),
  445. Ret = Fun(Nodes),
  446. lists:zip(Nodes, lists:map(fun emqx_rpc:unwrap_erpc/1, Ret)).
  447. %%--------------------------------------------------------------------
  448. %% Internal Funcs
  449. %%--------------------------------------------------------------------
  450. err_msg(Msg) -> emqx_utils:readable_error_msg(Msg).
  451. get_raw_config() ->
  452. RawConfig = emqx:get_raw_config([exhook, servers], []),
  453. Schema = #{roots => emqx_exhook_schema:fields(exhook), fields => #{}},
  454. Conf = #{<<"servers">> => RawConfig},
  455. #{<<"servers">> := Servers} = hocon_tconf:make_serializable(Schema, Conf, #{}),
  456. Servers.
  457. position_example() ->
  458. #{
  459. front =>
  460. #{
  461. summary => <<"absolute position 'front'">>,
  462. value => #{<<"position">> => <<"front">>}
  463. },
  464. rear =>
  465. #{
  466. summary => <<"absolute position 'rear'">>,
  467. value => #{<<"position">> => <<"rear">>}
  468. },
  469. related_before =>
  470. #{
  471. summary => <<"relative position 'before'">>,
  472. value => #{<<"position">> => <<"before:default">>}
  473. },
  474. related_after =>
  475. #{
  476. summary => <<"relative position 'after'">>,
  477. value => #{<<"position">> => <<"after:default">>}
  478. }
  479. }.
  480. parse_position(<<"front">>) ->
  481. {ok, ?CMD_MOVE_FRONT};
  482. parse_position(<<"rear">>) ->
  483. {ok, ?CMD_MOVE_REAR};
  484. parse_position(<<"before:">>) ->
  485. {error, invalid_position};
  486. parse_position(<<"after:">>) ->
  487. {error, invalid_position};
  488. parse_position(<<"before:", Related/binary>>) ->
  489. {ok, ?CMD_MOVE_BEFORE(Related)};
  490. parse_position(<<"after:", Related/binary>>) ->
  491. {ok, ?CMD_MOVE_AFTER(Related)};
  492. parse_position(_) ->
  493. {error, invalid_position}.