Add new HTTP endpoint `/_node/_local/_smoosh/status`. (#4766)

Introduce a new HTTP endpoint `/_node/_local/_smoosh/status`
to get status information from the CouchDB auto-compaction daemon.
Previously, this was only possible by starting a `remsh` session
and manually calling the `smoosh:status/1` function.

The internal data structures of `smoosh:status/1` are migrated
into Erlang maps to send them directly as json to the client.

To add more status information to smoosh in the future, the available
information will be stored under the json key `channels`.

Example:

{
  "channels": {
    "ratio_dbs": { ... },
    "slack_dbs": { ... },
    ...
}
diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl
index 46850fc..165b85a 100644
--- a/src/chttpd/src/chttpd_node.erl
+++ b/src/chttpd/src/chttpd_node.erl
@@ -39,6 +39,12 @@
     send_json(Req, 200, {[{name, node()}]});
 handle_node_req(#httpd{path_parts = [A, <<"_local">> | Rest]} = Req) ->
     handle_node_req(Req#httpd{path_parts = [A, node()] ++ Rest});
+% GET /_node/$node/_smoosh/status
+handle_node_req(#httpd{method = 'GET', path_parts = [_, _Node, <<"_smoosh">>, <<"status">>]} = Req) ->
+    {ok, Status} = smoosh:status(),
+    send_json(Req, 200, Status);
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_smoosh">>, <<"status">>]} = Req) ->
+    send_method_not_allowed(Req, "GET");
 % GET /_node/$node/_versions
 handle_node_req(#httpd{method = 'GET', path_parts = [_, _Node, <<"_versions">>]} = Req) ->
     IcuVer = couch_ejson_compare:get_icu_version(),
diff --git a/src/docs/src/api/server/common.rst b/src/docs/src/api/server/common.rst
index 9e645f6..783b812 100644
--- a/src/docs/src/api/server/common.rst
+++ b/src/docs/src/api/server/common.rst
@@ -1902,6 +1902,116 @@
         Accept: text/plain
         Host: localhost:17986
 
+.. _api/server/smoosh/status:
+
+=====================================
+``/_node/{node-name}/_smoosh/status``
+=====================================
+
+.. versionadded:: 3.4
+
+.. http:get:: /_node/{node-name}/_smoosh/status
+    :synopsis: Returns metrics of the CouchDB's auto-compaction daemon
+
+    This prints the state of each channel, how many jobs they are
+    currently running and how many jobs are enqueued (as well as the
+    lowest and highest priority of those enqueued items). The idea is to
+    provide, at a glance, sufficient insight into ``smoosh`` that an operator
+    can assess whether ``smoosh`` is adequately targeting the reclaimable
+    space in the cluster.
+
+    In general, a healthy status output will have
+    items in the ``ratio_dbs`` and ``ratio_views`` channels. Owing to the default
+    settings, the ``slack_dbs`` and ``slack_views`` will almost certainly have
+    items in them. Historically, we've not found that the slack channels,
+    on their own, are particularly adept at keeping things well compacted.
+
+    :code 200: Request completed successfully
+    :code 401: CouchDB Server Administrator privileges required
+
+    **Request**:
+
+    .. code-block:: http
+
+        GET /_node/_local/_smoosh/status HTTP/1.1
+        Host: 127.0.0.1:5984
+        Accept: */*
+
+    **Response**:
+
+    .. code-block:: http
+
+        HTTP/1.1 200 OK
+        Content-Type: application/json
+
+        {
+            "channels": {
+                "slack_dbs": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                },
+                "ratio_dbs": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 56,
+                        "min": 1.125,
+                        "max": 11.0625
+                    },
+                    "active": 0
+                },
+                "ratio_views": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                },
+                "upgrade_dbs": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                },
+                "slack_views": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                },
+                "upgrade_views": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                },
+                "index_cleanup": {
+                    "starting": 0,
+                    "waiting": {
+                        "size": 0,
+                        "min": 0,
+                        "max": 0
+                    },
+                    "active": 0
+                }
+            }
+        }
+
 .. _api/server/system:
 
 ==============================
diff --git a/src/smoosh/src/smoosh_channel.erl b/src/smoosh/src/smoosh_channel.erl
index 92fd341..3cfbcde 100644
--- a/src/smoosh/src/smoosh_channel.erl
+++ b/src/smoosh/src/smoosh_channel.erl
@@ -77,10 +77,10 @@
 get_status(StatusTab) when is_reference(StatusTab) ->
     try ets:lookup(StatusTab, status) of
         [{status, Status}] -> Status;
-        [] -> []
+        [] -> #{}
     catch
         error:badarg ->
-            []
+            #{}
     end.
 
 close(ServerRef) ->
@@ -235,11 +235,11 @@
 %
 set_status(#state{} = State) ->
     #state{active = Active, starting = Starting, waiting = Waiting} = State,
-    Status = [
-        {active, map_size(Active)},
-        {starting, map_size(Starting)},
-        {waiting, smoosh_priority_queue:info(Waiting)}
-    ],
+    Status = #{
+        active => map_size(Active),
+        starting => map_size(Starting),
+        waiting => smoosh_priority_queue:info(Waiting)
+    },
     true = ets:insert(State#state.stab, {status, Status}),
     State.
 
diff --git a/src/smoosh/src/smoosh_persist.erl b/src/smoosh/src/smoosh_persist.erl
index 2feab7e..c1519f6 100644
--- a/src/smoosh/src/smoosh_persist.erl
+++ b/src/smoosh/src/smoosh_persist.erl
@@ -225,7 +225,7 @@
 
     Q2 = unpersist(Name),
     ?assertEqual(Name, smoosh_priority_queue:name(Q2)),
-    ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q2)).
+    ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q2)).
 
 t_persist_unpersist_enabled(_) ->
     Name = "chan2",
@@ -241,7 +241,7 @@
     Q2 = unpersist(Name),
     ?assertEqual(Name, smoosh_priority_queue:name(Q2)),
     Info2 = smoosh_priority_queue:info(Q2),
-    ?assertEqual([{size, 3}, {min, 1.0}, {max, infinity}], Info2),
+    ?assertEqual(#{max => infinity, min => 1.0, size => 3}, Info2),
     ?assertEqual(Keys, drain_q(Q2)),
 
     % Try to persist the already unpersisted queue
@@ -249,7 +249,7 @@
     Q3 = unpersist(Name),
     ?assertEqual(Name, smoosh_priority_queue:name(Q3)),
     Info3 = smoosh_priority_queue:info(Q2),
-    ?assertEqual([{size, 3}, {min, 1.0}, {max, infinity}], Info3),
+    ?assertEqual(#{max => infinity, min => 1.0, size => 3}, Info3),
     ?assertEqual(Keys, drain_q(Q3)).
 
 t_persist_unpersist_errors(_) ->
@@ -267,7 +267,7 @@
 
     Q2 = unpersist(Name),
     ?assertEqual(Name, smoosh_priority_queue:name(Q2)),
-    ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q2)),
+    ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q2)),
 
     Dir = state_dir(),
     ok = file:make_dir(Dir),
@@ -278,7 +278,7 @@
 
     Q3 = unpersist(Name),
     ?assertEqual(Name, smoosh_priority_queue:name(Q3)),
-    ?assertEqual([{size, 0}], smoosh_priority_queue:info(Q3)),
+    ?assertEqual(#{max => 0, min => 0, size => 0}, smoosh_priority_queue:info(Q3)),
 
     ok = file:del_dir_r(Dir).
 
diff --git a/src/smoosh/src/smoosh_priority_queue.erl b/src/smoosh/src/smoosh_priority_queue.erl
index b2ef439..2f2fba6 100644
--- a/src/smoosh/src/smoosh_priority_queue.erl
+++ b/src/smoosh/src/smoosh_priority_queue.erl
@@ -64,17 +64,14 @@
     gb_trees:size(Tree).
 
 info(#priority_queue{tree = Tree} = Q) ->
-    [
-        {size, qsize(Q)}
-        | case gb_trees:is_empty(Tree) of
-            true ->
-                [];
-            false ->
-                {{Min, _}, _} = gb_trees:smallest(Tree),
-                {{Max, _}, _} = gb_trees:largest(Tree),
-                [{min, Min}, {max, Max}]
-        end
-    ].
+    case gb_trees:is_empty(Tree) of
+        true ->
+            #{size => qsize(Q), min => 0, max => 0};
+        false ->
+            {{Min, _}, _} = gb_trees:smallest(Tree),
+            {{Max, _}, _} = gb_trees:largest(Tree),
+            #{size => qsize(Q), min => Min, max => Max}
+    end.
 
 insert(Key, Priority, Capacity, #priority_queue{tree = Tree, map = Map} = Q) ->
     TreeKey = {Priority, make_ref()},
@@ -122,7 +119,7 @@
     Q = new("foo"),
     ?assertMatch(#priority_queue{}, Q),
     ?assertEqual("foo", name(Q)),
-    ?assertEqual([{size, 0}], info(Q)).
+    ?assertEqual(0, maps:get(size, info(Q))).
 
 empty_test() ->
     Q = new("foo"),
@@ -136,7 +133,7 @@
     Q0 = new("foo"),
     Q = in(?K1, ?P1, 1, Q0),
     ?assertMatch(#priority_queue{}, Q),
-    ?assertEqual([{size, 1}, {min, 1}, {max, 1}], info(Q)),
+    ?assertEqual(#{max => 1, min => 1, size => 1}, info(Q)),
     ?assertEqual(Q, truncate(1, Q)),
     ?assertMatch({?K1, #priority_queue{}}, out(Q)),
     {?K1, Q2} = out(Q),
@@ -144,7 +141,7 @@
     ?assertEqual(#{?K1 => ?P1}, to_map(Q)),
     Q3 = from_map("foo", 1, to_map(Q)),
     ?assertEqual("foo", name(Q3)),
-    ?assertEqual([{size, 1}, {min, ?P1}, {max, ?P1}], info(Q3)),
+    ?assertEqual(#{max => ?P1, min => ?P1, size => 1}, info(Q3)),
     ?assertEqual(to_map(Q), to_map(Q3)),
     ?assertEqual(Q0, flush(Q)).
 
@@ -153,7 +150,7 @@
     Q1 = in(?K1, ?P1, 10, Q0),
     Q2 = in(?K2, ?P2, 10, Q1),
     Q3 = in(?K3, ?P3, 10, Q2),
-    ?assertEqual([{size, 3}, {min, ?P1}, {max, ?P3}], info(Q3)),
+    ?assertEqual(#{max => ?P3, min => ?P1, size => 3}, info(Q3)),
     ?assertEqual([?K3, ?K2, ?K1], drain(Q3)).
 
 update_element_same_priority_test() ->
@@ -166,7 +163,7 @@
     Q1 = in(?K1, ?P1, 10, Q0),
     Q2 = in(?K2, ?P2, 10, Q1),
     Q3 = in(?K1, ?P3, 10, Q2),
-    ?assertEqual([{size, 2}, {min, ?P2}, {max, ?P3}], info(Q3)),
+    ?assertEqual(#{max => ?P3, min => ?P2, size => 2}, info(Q3)),
     ?assertEqual([?K1, ?K2], drain(Q3)).
 
 capacity_test() ->
@@ -189,7 +186,7 @@
         lists:seq(1, N)
     ),
     Q = from_map("foo", N, maps:from_list(KVs)),
-    ?assertMatch([{size, N} | _], info(Q)),
+    ?assertMatch(N, maps:get(size, info(Q))),
     {_, Priorities} = lists:unzip(drain(Q)),
     ?assertEqual(lists:reverse(lists:sort(Priorities)), Priorities).
 
diff --git a/src/smoosh/src/smoosh_server.erl b/src/smoosh/src/smoosh_server.erl
index 10368a5..3b0b868 100644
--- a/src/smoosh/src/smoosh_server.erl
+++ b/src/smoosh/src/smoosh_server.erl
@@ -96,12 +96,14 @@
     gen_server:call(?MODULE, flush, infinity).
 
 status() ->
-    try ets:foldl(fun get_channel_status/2, [], ?MODULE) of
-        Res -> {ok, Res}
-    catch
-        error:badarg ->
-            {ok, []}
-    end.
+    ChannelsStatus =
+        try ets:foldl(fun get_channel_status/2, #{}, ?MODULE) of
+            Res -> Res
+        catch
+            error:badarg ->
+                #{}
+        end,
+    {ok, #{channels => ChannelsStatus}}.
 
 enqueue(Object0) ->
     Object = smoosh_utils:validate_arg(Object0),
@@ -286,7 +288,7 @@
 
 get_channel_status(#channel{name = Name, stab = Tab}, Acc) ->
     Status = smoosh_channel:get_status(Tab),
-    [{Name, Status} | Acc];
+    Acc#{list_to_atom(Name) => Status};
 get_channel_status(_, Acc) ->
     Acc.
 
diff --git a/src/smoosh/test/smoosh_tests.erl b/src/smoosh/test/smoosh_tests.erl
index 622cabc..6861db5 100644
--- a/src/smoosh/test/smoosh_tests.erl
+++ b/src/smoosh/test/smoosh_tests.erl
@@ -82,21 +82,22 @@
     config:delete("smoosh", "cleanup_index_files", false).
 
 t_default_channels(_) ->
+    ChannelStatus = maps:get(channels, status()),
     ?assertMatch(
         [
-            {"index_cleanup", _},
-            {"ratio_dbs", _},
-            {"ratio_views", _},
-            {"slack_dbs", _},
-            {"slack_views", _},
-            {"upgrade_dbs", _},
-            {"upgrade_views", _}
+            index_cleanup,
+            ratio_dbs,
+            ratio_views,
+            slack_dbs,
+            slack_views,
+            upgrade_dbs,
+            upgrade_views
         ],
-        status()
+        lists:sort(maps:keys(ChannelStatus))
     ),
     % If app hasn't started status won't crash
     application:stop(smoosh),
-    ?assertEqual([], status()).
+    ?assertEqual(#{channels => #{}}, status()).
 
 t_channels_recreated_on_crash(_) ->
     RatioDbsPid = get_channel_pid("ratio_dbs"),
@@ -104,7 +105,8 @@
     exit(RatioDbsPid, kill),
     meck:wait(1, smoosh_channel, start_link, 1, 3000),
     wait_for_channels(7),
-    ?assertMatch([_, {"ratio_dbs", _} | _], status()),
+    ChannelStatus = maps:get(channels, status()),
+    ?assertMatch(true, maps:is_key(ratio_dbs, ChannelStatus)),
     ?assertNotEqual(RatioDbsPid, get_channel_pid("ratio_dbs")).
 
 t_can_create_and_delete_channels(_) ->
@@ -402,17 +404,17 @@
 
 status() ->
     {ok, Props} = smoosh:status(),
-    lists:keysort(1, Props).
+    Props.
 
 status(Channel) ->
-    case lists:keyfind(Channel, 1, status()) of
-        {_, Val} ->
-            Val,
-            Active = proplists:get_value(active, Val),
-            Starting = proplists:get_value(starting, Val),
-            WaitingInfo = proplists:get_value(waiting, Val),
-            Waiting = proplists:get_value(size, WaitingInfo),
-            {Active, Starting, Waiting};
+    ChannelStatus = maps:get(channels, status()),
+    ChannelAtom = list_to_atom(Channel),
+    case maps:is_key(ChannelAtom, ChannelStatus) of
+        true ->
+            #{active := Active, starting := Starting, waiting := Waiting} = maps:get(
+                ChannelAtom, ChannelStatus
+            ),
+            {Active, Starting, maps:get(size, Waiting)};
         false ->
             false
     end.
@@ -443,7 +445,8 @@
 
 wait_for_channels(N) when is_integer(N), N >= 0 ->
     WaitFun = fun() ->
-        case length(status()) of
+        ChannelStatus = maps:get(channels, status()),
+        case length(maps:keys(ChannelStatus)) of
             N -> ok;
             _ -> wait
         end