Skip to content

Commit f5dcc91

Browse files
Merge pull request #13577 from rabbitmq/mergify/bp/v4.1.x/pr-13571
New HTTP API health check endpoints (backport #13571)
2 parents 985ac4e + 6cddad4 commit f5dcc91

4 files changed

+263
-6
lines changed

Diff for: deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ dispatcher() ->
202202
{"/health/checks/port-listener/:port", rabbit_mgmt_wm_health_check_port_listener, []},
203203
{"/health/checks/protocol-listener/:protocol", rabbit_mgmt_wm_health_check_protocol_listener, []},
204204
{"/health/checks/virtual-hosts", rabbit_mgmt_wm_health_check_virtual_hosts, []},
205+
{"/health/checks/quorum-queues-without-elected-leaders/all-vhosts/", rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders_across_all_vhosts, []},
206+
{"/health/checks/quorum-queues-without-elected-leaders/vhost/:vhost/", rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders, []},
207+
{"/health/checks/quorum-queues-without-elected-leaders/all-vhosts/pattern/:pattern", rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders_across_all_vhosts, []},
208+
{"/health/checks/quorum-queues-without-elected-leaders/vhost/:vhost/pattern/:pattern", rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders, []},
205209
{"/health/checks/node-is-quorum-critical", rabbit_mgmt_wm_health_check_node_is_quorum_critical, []},
206210
{"/reset", rabbit_mgmt_wm_reset, []},
207211
{"/reset/:node", rabbit_mgmt_wm_reset, []},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
%% An HTTP API counterpart of 'rabbitmq-diagnostics check_for_quorum_queues_without_an_elected_leader'
9+
-module(rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders).
10+
11+
-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]).
12+
-export([resource_exists/2]).
13+
-export([variances/2]).
14+
15+
-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
16+
17+
-define(DEFAULT_PATTERN, <<".*">>).
18+
19+
%%--------------------------------------------------------------------
20+
21+
init(Req, _State) ->
22+
{cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}.
23+
24+
variances(Req, Context) ->
25+
{[<<"accept-encoding">>, <<"origin">>], Req, Context}.
26+
27+
content_types_provided(ReqData, Context) ->
28+
{rabbit_mgmt_util:responder_map(to_json), ReqData, Context}.
29+
30+
resource_exists(ReqData, Context) ->
31+
Result = case {vhost(ReqData), pattern(ReqData)} of
32+
{none, _} -> false;
33+
{_, none} -> false;
34+
_ -> true
35+
end,
36+
{Result, ReqData, Context}.
37+
38+
to_json(ReqData, Context) ->
39+
case rabbit_quorum_queue:leader_health_check(pattern(ReqData), vhost(ReqData)) of
40+
[] ->
41+
rabbit_mgmt_util:reply(#{status => ok}, ReqData, Context);
42+
Qs when length(Qs) > 0 ->
43+
Msg = <<"Detected quorum queues without an elected leader">>,
44+
failure(Msg, Qs, ReqData, Context)
45+
end.
46+
47+
failure(Message, Qs, ReqData, Context) ->
48+
Body = #{status => failed,
49+
reason => Message,
50+
queues => Qs},
51+
{Response, ReqData1, Context1} = rabbit_mgmt_util:reply(Body, ReqData, Context),
52+
{stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}.
53+
54+
is_authorized(ReqData, Context) ->
55+
rabbit_mgmt_util:is_authorized(ReqData, Context).
56+
57+
%%
58+
%% Implementation
59+
%%
60+
61+
vhost(ReqData) ->
62+
rabbit_mgmt_util:id(vhost, ReqData).
63+
64+
pattern(ReqData) ->
65+
case rabbit_mgmt_util:id(pattern, ReqData) of
66+
none -> ?DEFAULT_PATTERN;
67+
Other -> Other
68+
end.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
%% An HTTP API counterpart of 'rabbitmq-diagnostics check_for_quorum_queues_without_an_elected_leader --across-all-vhosts'
9+
-module(rabbit_mgmt_wm_health_check_quorum_queues_without_elected_leaders_across_all_vhosts).
10+
11+
-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]).
12+
-export([resource_exists/2]).
13+
-export([variances/2]).
14+
15+
-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
16+
17+
-define(ACROSS_ALL_VHOSTS, across_all_vhosts).
18+
-define(DEFAULT_PATTERN, <<".*">>).
19+
20+
%%--------------------------------------------------------------------
21+
22+
init(Req, _State) ->
23+
{cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}.
24+
25+
variances(Req, Context) ->
26+
{[<<"accept-encoding">>, <<"origin">>], Req, Context}.
27+
28+
content_types_provided(ReqData, Context) ->
29+
{rabbit_mgmt_util:responder_map(to_json), ReqData, Context}.
30+
31+
resource_exists(ReqData, Context) ->
32+
{true, ReqData, Context}.
33+
34+
to_json(ReqData, Context) ->
35+
case rabbit_quorum_queue:leader_health_check(pattern(ReqData), ?ACROSS_ALL_VHOSTS) of
36+
[] ->
37+
rabbit_mgmt_util:reply(#{status => ok}, ReqData, Context);
38+
Qs when length(Qs) > 0 ->
39+
Msg = <<"Detected quorum queues without an elected leader">>,
40+
failure(Msg, Qs, ReqData, Context)
41+
end.
42+
43+
failure(Message, Qs, ReqData, Context) ->
44+
Body = #{status => failed,
45+
reason => Message,
46+
queues => Qs},
47+
{Response, ReqData1, Context1} = rabbit_mgmt_util:reply(Body, ReqData, Context),
48+
{stop, cowboy_req:reply(503, #{}, Response, ReqData1), Context1}.
49+
50+
is_authorized(ReqData, Context) ->
51+
rabbit_mgmt_util:is_authorized(ReqData, Context).
52+
53+
%%
54+
%% Implementation
55+
%%
56+
57+
pattern(ReqData) ->
58+
case rabbit_mgmt_util:id(pattern, ReqData) of
59+
none -> ?DEFAULT_PATTERN;
60+
Other -> Other
61+
end.

Diff for: deps/rabbitmq_management/test/rabbit_mgmt_http_health_checks_SUITE.erl

+130-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ groups() ->
3737
local_alarms_test,
3838
metadata_store_initialized_test,
3939
metadata_store_initialized_with_data_test,
40-
is_quorum_critical_single_node_test]}
40+
is_quorum_critical_single_node_test,
41+
quorum_queues_without_elected_leader_single_node_test,
42+
quorum_queues_without_elected_leader_across_all_virtual_hosts_single_node_test
43+
]}
4144
].
4245

4346
all_tests() -> [
@@ -165,7 +168,8 @@ local_alarms_test(Config) ->
165168

166169

167170
is_quorum_critical_single_node_test(Config) ->
168-
Check0 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK),
171+
EndpointPath = "/health/checks/node-is-quorum-critical",
172+
Check0 = http_get(Config, EndpointPath, ?OK),
169173
?assertEqual(<<"single node cluster">>, maps:get(reason, Check0)),
170174
?assertEqual(<<"ok">>, maps:get(status, Check0)),
171175

@@ -178,13 +182,14 @@ is_quorum_critical_single_node_test(Config) ->
178182
durable = true,
179183
auto_delete = false,
180184
arguments = Args})),
181-
Check1 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK),
185+
Check1 = http_get(Config, EndpointPath, ?OK),
182186
?assertEqual(<<"single node cluster">>, maps:get(reason, Check1)),
183187

184188
passed.
185189

186190
is_quorum_critical_test(Config) ->
187-
Check0 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK),
191+
EndpointPath = "/health/checks/node-is-quorum-critical",
192+
Check0 = http_get(Config, EndpointPath, ?OK),
188193
?assertEqual(false, maps:is_key(reason, Check0)),
189194
?assertEqual(<<"ok">>, maps:get(status, Check0)),
190195

@@ -198,7 +203,7 @@ is_quorum_critical_test(Config) ->
198203
durable = true,
199204
auto_delete = false,
200205
arguments = Args})),
201-
Check1 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK),
206+
Check1 = http_get(Config, EndpointPath, ?OK),
202207
?assertEqual(false, maps:is_key(reason, Check1)),
203208

204209
RaName = binary_to_atom(<<"%2F_", QName/binary>>, utf8),
@@ -207,7 +212,104 @@ is_quorum_critical_test(Config) ->
207212
ok = rabbit_ct_broker_helpers:stop_node(Config, Server2),
208213
ok = rabbit_ct_broker_helpers:stop_node(Config, Server3),
209214

210-
Body = http_get_failed(Config, "/health/checks/node-is-quorum-critical"),
215+
Body = http_get_failed(Config, EndpointPath),
216+
?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)),
217+
?assertEqual(true, maps:is_key(<<"reason">>, Body)),
218+
Queues = maps:get(<<"queues">>, Body),
219+
?assert(lists:any(
220+
fun(Item) ->
221+
QName =:= maps:get(<<"name">>, Item)
222+
end, Queues)),
223+
224+
passed.
225+
226+
quorum_queues_without_elected_leader_single_node_test(Config) ->
227+
EndpointPath = "/health/checks/quorum-queues-without-elected-leaders/all-vhosts/",
228+
Check0 = http_get(Config, EndpointPath, ?OK),
229+
?assertEqual(false, maps:is_key(reason, Check0)),
230+
?assertEqual(<<"ok">>, maps:get(status, Check0)),
231+
232+
[Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
233+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
234+
Args = [{<<"x-queue-type">>, longstr, <<"quorum">>},
235+
{<<"x-quorum-initial-group-size">>, long, 3}],
236+
QName = <<"quorum_queues_without_elected_leader">>,
237+
?assertEqual({'queue.declare_ok', QName, 0, 0},
238+
amqp_channel:call(Ch, #'queue.declare'{
239+
queue = QName,
240+
durable = true,
241+
auto_delete = false,
242+
arguments = Args
243+
})),
244+
245+
Check1 = http_get(Config, EndpointPath, ?OK),
246+
?assertEqual(false, maps:is_key(reason, Check1)),
247+
248+
RaSystem = quorum_queues,
249+
QResource = rabbit_misc:r(<<"/">>, queue, QName),
250+
{ok, Q1} = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_db_queue, get, [QResource]),
251+
252+
_ = rabbit_ct_broker_helpers:rpc(Config, 0, ra, stop_server, [RaSystem, amqqueue:get_pid(Q1)]),
253+
254+
Body = http_get_failed(Config, EndpointPath),
255+
?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)),
256+
?assertEqual(true, maps:is_key(<<"reason">>, Body)),
257+
Queues = maps:get(<<"queues">>, Body),
258+
?assert(lists:any(
259+
fun(Item) ->
260+
QName =:= maps:get(<<"name">>, Item)
261+
end, Queues)),
262+
263+
_ = rabbit_ct_broker_helpers:rpc(Config, 0, ra, restart_server, [RaSystem, amqqueue:get_pid(Q1)]),
264+
rabbit_ct_helpers:await_condition(
265+
fun() ->
266+
try
267+
Check2 = http_get(Config, EndpointPath, ?OK),
268+
false =:= maps:is_key(reason, Check2)
269+
catch _:_ ->
270+
false
271+
end
272+
end),
273+
274+
passed.
275+
276+
quorum_queues_without_elected_leader_across_all_virtual_hosts_single_node_test(Config) ->
277+
VH2 = <<"vh-2">>,
278+
rabbit_ct_broker_helpers:add_vhost(Config, VH2),
279+
280+
EndpointPath1 = "/health/checks/quorum-queues-without-elected-leaders/vhost/%2f/",
281+
EndpointPath2 = "/health/checks/quorum-queues-without-elected-leaders/vhost/vh-2/",
282+
%% ^other
283+
EndpointPath3 = "/health/checks/quorum-queues-without-elected-leaders/vhost/vh-2/pattern/%5Eother",
284+
285+
Check0 = http_get(Config, EndpointPath1, ?OK),
286+
Check0 = http_get(Config, EndpointPath2, ?OK),
287+
?assertEqual(false, maps:is_key(reason, Check0)),
288+
?assertEqual(<<"ok">>, maps:get(status, Check0)),
289+
290+
[Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
291+
Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
292+
Args = [{<<"x-queue-type">>, longstr, <<"quorum">>},
293+
{<<"x-quorum-initial-group-size">>, long, 3}],
294+
QName = <<"quorum_queues_without_elected_leader_across_all_virtual_hosts_single_node_test">>,
295+
?assertEqual({'queue.declare_ok', QName, 0, 0},
296+
amqp_channel:call(Ch, #'queue.declare'{
297+
queue = QName,
298+
durable = true,
299+
auto_delete = false,
300+
arguments = Args
301+
})),
302+
303+
Check1 = http_get(Config, EndpointPath1, ?OK),
304+
?assertEqual(false, maps:is_key(reason, Check1)),
305+
306+
RaSystem = quorum_queues,
307+
QResource = rabbit_misc:r(<<"/">>, queue, QName),
308+
{ok, Q1} = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_db_queue, get, [QResource]),
309+
310+
_ = rabbit_ct_broker_helpers:rpc(Config, 0, ra, stop_server, [RaSystem, amqqueue:get_pid(Q1)]),
311+
312+
Body = http_get_failed(Config, EndpointPath1),
211313
?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)),
212314
?assertEqual(true, maps:is_key(<<"reason">>, Body)),
213315
Queues = maps:get(<<"queues">>, Body),
@@ -216,8 +318,30 @@ is_quorum_critical_test(Config) ->
216318
QName =:= maps:get(<<"name">>, Item)
217319
end, Queues)),
218320

321+
%% virtual host vh-2 is still fine
322+
Check2 = http_get(Config, EndpointPath2, ?OK),
323+
?assertEqual(false, maps:is_key(reason, Check2)),
324+
325+
%% a different queue name pattern succeeds
326+
Check3 = http_get(Config, EndpointPath3, ?OK),
327+
?assertEqual(false, maps:is_key(reason, Check3)),
328+
329+
_ = rabbit_ct_broker_helpers:rpc(Config, 0, ra, restart_server, [RaSystem, amqqueue:get_pid(Q1)]),
330+
rabbit_ct_helpers:await_condition(
331+
fun() ->
332+
try
333+
Check4 = http_get(Config, EndpointPath1, ?OK),
334+
false =:= maps:is_key(reason, Check4)
335+
catch _:_ ->
336+
false
337+
end
338+
end),
339+
340+
rabbit_ct_broker_helpers:delete_vhost(Config, VH2),
341+
219342
passed.
220343

344+
221345
virtual_hosts_test(Config) ->
222346
VHost1 = <<"vhost1">>,
223347
VHost2 = <<"vhost2">>,

0 commit comments

Comments
 (0)