This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/main by this push:
     new c0cb1737d feat(prometheus): add Clouseau, DB count, IOQ and Smoosh 
metrics
c0cb1737d is described below

commit c0cb1737dca40a4d070e00b0378c87785f06e5e8
Author: Sam Smith <[email protected]>
AuthorDate: Mon Jul 13 14:02:10 2026 +0100

    feat(prometheus): add Clouseau, DB count, IOQ and Smoosh metrics
    
    Expose new Prometheus metrics for monitoring:
    - Clouseau search connectivity status
    - Total database count in cluster
    - IOQ active channels and request counts
    - Smoosh compaction job statistics (active/starting/waiting)
    
    Improves observability of core CouchDB subsystems.
---
 src/couch_prometheus/src/couch_prometheus.erl      | 130 ++++++++++++++-
 .../test/eunit/couch_prometheus_e2e_tests.erl      |  12 +-
 .../test/eunit/couch_prometheus_tests.erl          | 185 +++++++++++++++++++++
 src/ioq/src/ioq.erl                                |   5 +-
 test/fixtures/allowed-xref.txt                     |   2 -
 5 files changed, 328 insertions(+), 6 deletions(-)

diff --git a/src/couch_prometheus/src/couch_prometheus.erl 
b/src/couch_prometheus/src/couch_prometheus.erl
index 7f3dc494d..bba7f8b57 100644
--- a/src/couch_prometheus/src/couch_prometheus.erl
+++ b/src/couch_prometheus/src/couch_prometheus.erl
@@ -26,12 +26,20 @@
 
 -ifdef(TEST).
 -export([
-    get_internal_replication_jobs_stat/0
+    get_internal_replication_jobs_stat/0,
+    get_clouseau_status/0,
+    get_database_count/0,
+    get_ioq_stats/0,
+    get_smoosh_stats/0
 ]).
 -endif.
 
 -define(PROMETHEUS_VERSION, "2.0").
 
+%% Connection status constants
+-define(CLOUSEAU_CONNECTED, 1).
+-define(CLOUSEAU_DISCONNECTED, 0).
+
 scrape() ->
     CouchDB = get_couchdb_stats(),
     System = couch_stats_httpd:to_ejson(get_system_stats()),
@@ -69,7 +77,11 @@ get_system_stats() ->
         get_membership_stat(),
         get_membership_nodes(),
         get_distribution_stats(),
-        get_bt_engine_cache_stats()
+        get_bt_engine_cache_stats(),
+        get_clouseau_status(),
+        get_database_count(),
+        get_ioq_stats(),
+        get_smoosh_stats()
     ]).
 
 get_uptime_stat() ->
@@ -384,3 +396,117 @@ get_bt_engine_cache_stats() ->
         to_prom(couchdb_bt_engine_cache_memory, gauge, "memory used by the 
btree cache", Mem),
         to_prom(couchdb_bt_engine_cache_size, gauge, "number of entries in the 
btree cache", Size)
     ].
+
+get_clouseau_status() ->
+    Value =
+        case clouseau_rpc:connected() of
+            true -> ?CLOUSEAU_CONNECTED;
+            false -> ?CLOUSEAU_DISCONNECTED
+        end,
+    to_prom(clouseau_connected, gauge, "clouseau connectivity status", Value).
+
+get_database_count() ->
+    ShardsDb = config:get("mem3", "shards_db", "_dbs"),
+    case fabric:get_db_info(ShardsDb) of
+        {ok, Info} ->
+            DbCount = couch_util:get_value(doc_count, Info),
+            to_prom(database_count, gauge, "total database count", DbCount);
+        {error, Reason} ->
+            couch_log:warning("~p failed to get database count: ~p", [?MODULE, 
Reason]),
+            []
+    end.
+
+get_ioq_stats() ->
+    IOQData = ioq:get_disk_queues(),
+    Interactive = couch_util:get_value(interactive, IOQData),
+    Background = couch_util:get_value(background, IOQData),
+    case {Interactive, Background} of
+        {I, B} when is_integer(I), is_integer(B) ->
+            [
+                to_prom(ioq_interactive_requests, gauge, "IOQ interactive 
queue requests", I),
+                to_prom(ioq_background_requests, gauge, "IOQ background queue 
requests", B),
+                to_prom(ioq_total_requests, gauge, "IOQ total active 
requests", I + B)
+            ];
+        _ ->
+            Compaction = couch_util:get_value(compaction, IOQData, 0),
+            Low = couch_util:get_value(low, IOQData, 0),
+            Replication = couch_util:get_value(replication, IOQData, 0),
+            Channels =
+                case couch_util:get_value(channels, IOQData, []) of
+                    {List} when is_list(List) -> List;
+                    _ -> []
+                end,
+            ChannelRequests = count_channel_requests(Channels),
+            [
+                to_prom(ioq_active_channels, gauge, "IOQ active channels", 
length(Channels)),
+                to_prom(
+                    ioq_compaction_requests, gauge, "IOQ compaction queue 
requests", Compaction
+                ),
+                to_prom(ioq_low_requests, gauge, "IOQ low priority queue 
requests", Low),
+                to_prom(
+                    ioq_replication_requests, gauge, "IOQ replication queue 
requests", Replication
+                ),
+                to_prom(ioq_channel_requests, gauge, "IOQ channel requests", 
ChannelRequests),
+                to_prom(
+                    ioq_total_requests,
+                    gauge,
+                    "IOQ total active requests",
+                    ChannelRequests + Compaction + Low + Replication
+                )
+            ]
+    end.
+
+count_channel_requests(Channels) ->
+    lists:foldl(
+        fun
+            ({_User, Vals}, Acc) when is_list(Vals) ->
+                Acc + lists:sum(Vals);
+            (_, Acc) ->
+                Acc
+        end,
+        0,
+        Channels
+    ).
+
+get_smoosh_stats() ->
+    case smoosh:status() of
+        {ok, #{channels := Channels}} ->
+            ChannelsList = maps:to_list(Channels),
+            ChannelCount = length(ChannelsList),
+            InitAcc = #{active => 0, starting => 0, waiting => 0},
+            GlobalTotals = lists:foldl(
+                fun({_Channel, StatsMap}, Acc) ->
+                    Active = maps:get(active, StatsMap, 0),
+                    Starting = maps:get(starting, StatsMap, 0),
+                    WaitingMap = maps:get(waiting, StatsMap, #{}),
+                    WaitingSize = maps:get(size, WaitingMap, 0),
+                    #{
+                        active => Active + maps:get(active, Acc, 0),
+                        starting => Starting + maps:get(starting, Acc, 0),
+                        waiting => WaitingSize + maps:get(waiting, Acc, 0)
+                    }
+                end,
+                InitAcc,
+                ChannelsList
+            ),
+            TotalActive = maps:get(active, GlobalTotals, 0),
+            TotalStarting = maps:get(starting, GlobalTotals, 0),
+            TotalWaiting = maps:get(waiting, GlobalTotals, 0),
+            [
+                to_prom(smoosh_channel_count, gauge, "total active smoosh 
channels", ChannelCount),
+                to_prom(
+                    smoosh_active_jobs, gauge, "global total active compaction 
jobs", TotalActive
+                ),
+                to_prom(
+                    smoosh_starting_jobs,
+                    gauge,
+                    "global total starting compaction jobs",
+                    TotalStarting
+                ),
+                to_prom(
+                    smoosh_waiting_jobs, gauge, "global total waiting 
compaction jobs", TotalWaiting
+                )
+            ];
+        _ ->
+            []
+    end.
diff --git a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl 
b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
index dd8da5f79..913b80834 100644
--- a/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
+++ b/src/couch_prometheus/test/eunit/couch_prometheus_e2e_tests.erl
@@ -42,7 +42,8 @@ e2e_test_() ->
                     ?TDEF_FE(t_metric_updated),
                     ?TDEF_FE(t_no_duplicate_metrics),
                     ?TDEF_FE(t_starts_with_couchdb),
-                    ?TDEF_FE(t_survives_mem3_sync_termination)
+                    ?TDEF_FE(t_survives_mem3_sync_termination),
+                    ?TDEF_FE(t_system_stats_metrics_present)
                 ]
             }
         }
@@ -186,6 +187,15 @@ t_survives_mem3_sync_termination(_) ->
         couch_prometheus:get_internal_replication_jobs_stat()
     ).
 
+t_system_stats_metrics_present(Port) ->
+    Url = node_local_url(Port),
+    Stats = get_stats(Url),
+    % Verify system stats metrics exist in the scrape output
+    ?assertNotEqual(not_found, metric_value(Stats, 
"couchdb_clouseau_connected")),
+    ?assertNotEqual(not_found, metric_value(Stats, "couchdb_database_count")),
+    ?assertNotEqual(not_found, metric_value(Stats, 
"couchdb_ioq_total_requests")),
+    ?assertNotEqual(not_found, metric_value(Stats, 
"couchdb_smoosh_channel_count")).
+
 node_local_url(Port) ->
     Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
     lists:concat(["http://";, Addr, ":", Port, "/_node/_local/_prometheus"]).
diff --git a/src/couch_prometheus/test/eunit/couch_prometheus_tests.erl 
b/src/couch_prometheus/test/eunit/couch_prometheus_tests.erl
new file mode 100644
index 000000000..7f79cb5a5
--- /dev/null
+++ b/src/couch_prometheus/test/eunit/couch_prometheus_tests.erl
@@ -0,0 +1,185 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_prometheus_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+unit_test_() ->
+    {
+        "Unit tests for system stats metrics",
+        {
+            setup,
+            fun setup_unit/0,
+            fun teardown_unit/1,
+            with([
+                ?TDEF(t_clouseau_connected),
+                ?TDEF(t_clouseau_disconnected),
+                ?TDEF(t_database_count_success),
+                ?TDEF(t_database_count_error),
+                ?TDEF(t_ioq_stats_basic),
+                ?TDEF(t_ioq_stats_with_channels),
+                ?TDEF(t_ioq_stats_couchdb_ioq_format),
+                ?TDEF(t_ioq_stats_couchdb_ioq_format_empty),
+                ?TDEF(t_smoosh_stats_success),
+                ?TDEF(t_smoosh_stats_error)
+            ])
+        }
+    }.
+
+setup_unit() ->
+    Mods = [clouseau_rpc, fabric, ioq, smoosh, config],
+    lists:foreach(fun(M) -> meck:new(M, [passthrough]) end, Mods),
+    Mods.
+
+teardown_unit(Mods) ->
+    lists:foreach(fun meck:unload/1, Mods).
+
+t_clouseau_connected(_) ->
+    meck:expect(clouseau_rpc, connected, [], meck:val(true)),
+    Result = couch_prometheus:get_clouseau_status(),
+    ?assertMatch([_, _, <<"couchdb_clouseau_connected 1">>], Result).
+
+t_clouseau_disconnected(_) ->
+    meck:expect(clouseau_rpc, connected, [], meck:val(false)),
+    Result = couch_prometheus:get_clouseau_status(),
+    ?assertMatch([_, _, <<"couchdb_clouseau_connected 0">>], Result).
+
+t_database_count_success(_) ->
+    meck:expect(config, get, 3, meck:val("_dbs")),
+    meck:expect(fabric, get_db_info, 1, meck:val({ok, [{doc_count, 42}]})),
+    Result = couch_prometheus:get_database_count(),
+    ?assertMatch([_, _, <<"couchdb_database_count 42">>], Result).
+
+t_database_count_error(_) ->
+    meck:expect(config, get, 3, meck:val("_dbs")),
+    meck:expect(fabric, get_db_info, 1, meck:val({error, not_found})),
+    Result = couch_prometheus:get_database_count(),
+    ?assertEqual([], Result).
+
+t_ioq_stats_basic(_) ->
+    meck:expect(
+        ioq,
+        get_disk_queues,
+        [],
+        meck:val([
+            {compaction, 1},
+            {low, 2},
+            {replication, 3}
+        ])
+    ),
+    Result = couch_prometheus:get_ioq_stats(),
+    ?assert(is_list(Result)),
+    ?assertEqual(6, length(Result)),
+    % Verify total requests = 1 + 2 + 3 = 6
+    [_, _, TotalLine] = lists:last(Result),
+    ?assertMatch(<<"couchdb_ioq_total_requests 6">>, TotalLine).
+
+t_ioq_stats_with_channels(_) ->
+    meck:expect(
+        ioq,
+        get_disk_queues,
+        [],
+        meck:val([
+            {compaction, 1},
+            {low, 2},
+            {replication, 3},
+            {channels, {[{<<"user1">>, [1, 2, 3]}]}}
+        ])
+    ),
+    Result = couch_prometheus:get_ioq_stats(),
+    ?assert(is_list(Result)),
+    ?assertEqual(6, length(Result)),
+    % Verify channel requests = 1 + 2 + 3 = 6
+    [_, _, ChannelLine] = lists:nth(5, Result),
+    ?assertMatch(<<"couchdb_ioq_channel_requests 6">>, ChannelLine),
+    % Verify total requests = 1 + 2 + 3 + 6 = 12
+    [_, _, TotalLine] = lists:last(Result),
+    ?assertMatch(<<"couchdb_ioq_total_requests 12">>, TotalLine).
+
+t_ioq_stats_couchdb_ioq_format(_) ->
+    meck:expect(
+        ioq,
+        get_disk_queues,
+        [],
+        meck:val([
+            {interactive, 10},
+            {background, 5}
+        ])
+    ),
+    Result = couch_prometheus:get_ioq_stats(),
+    ?assert(is_list(Result)),
+    ?assertEqual(3, length(Result)),
+    % Verify interactive requests
+    [_, _, InteractiveLine] = lists:nth(1, Result),
+    ?assertMatch(<<"couchdb_ioq_interactive_requests 10">>, InteractiveLine),
+    % Verify background requests
+    [_, _, BackgroundLine] = lists:nth(2, Result),
+    ?assertMatch(<<"couchdb_ioq_background_requests 5">>, BackgroundLine),
+    % Verify total requests = 10 + 5 = 15
+    [_, _, TotalLine] = lists:nth(3, Result),
+    ?assertMatch(<<"couchdb_ioq_total_requests 15">>, TotalLine).
+
+t_ioq_stats_couchdb_ioq_format_empty(_) ->
+    meck:expect(
+        ioq,
+        get_disk_queues,
+        [],
+        meck:val([
+            {interactive, 0},
+            {background, 0}
+        ])
+    ),
+    Result = couch_prometheus:get_ioq_stats(),
+    ?assert(is_list(Result)),
+    ?assertEqual(3, length(Result)),
+    % Verify all metrics are 0
+    [_, _, InteractiveLine] = lists:nth(1, Result),
+    ?assertMatch(<<"couchdb_ioq_interactive_requests 0">>, InteractiveLine),
+    [_, _, BackgroundLine] = lists:nth(2, Result),
+    ?assertMatch(<<"couchdb_ioq_background_requests 0">>, BackgroundLine),
+    [_, _, TotalLine] = lists:nth(3, Result),
+    ?assertMatch(<<"couchdb_ioq_total_requests 0">>, TotalLine).
+
+t_smoosh_stats_success(_) ->
+    meck:expect(
+        smoosh,
+        status,
+        [],
+        meck:val(
+            {ok, #{
+                channels => #{
+                    <<"ratio_dbs">> => #{
+                        active => 2,
+                        starting => 1,
+                        waiting => #{size => 5}
+                    }
+                }
+            }}
+        )
+    ),
+    Result = couch_prometheus:get_smoosh_stats(),
+    ?assert(is_list(Result)),
+    ?assertEqual(4, length(Result)),
+    [_, _, ChannelLine] = lists:nth(1, Result),
+    ?assertMatch(<<"couchdb_smoosh_channel_count 1">>, ChannelLine),
+    [_, _, ActiveLine] = lists:nth(2, Result),
+    ?assertMatch(<<"couchdb_smoosh_active_jobs 2">>, ActiveLine),
+    [_, _, StartingLine] = lists:nth(3, Result),
+    ?assertMatch(<<"couchdb_smoosh_starting_jobs 1">>, StartingLine),
+    [_, _, WaitingLine] = lists:nth(4, Result),
+    ?assertMatch(<<"couchdb_smoosh_waiting_jobs 5">>, WaitingLine).
+
+t_smoosh_stats_error(_) ->
+    meck:expect(smoosh, status, [], meck:val({error, down})),
+    Result = couch_prometheus:get_smoosh_stats(),
+    ?assertEqual([], Result).
diff --git a/src/ioq/src/ioq.erl b/src/ioq/src/ioq.erl
index 031626cfe..c458b99ed 100644
--- a/src/ioq/src/ioq.erl
+++ b/src/ioq/src/ioq.erl
@@ -15,7 +15,7 @@
 -behaviour(config_listener).
 
 -export([start_link/0, call/3, call_search/3]).
--export([get_queue_lengths/0]).
+-export([get_disk_queues/0, get_queue_lengths/0]).
 -export([get_io_priority/0, set_io_priority/1, maybe_set_io_priority/1]).
 -export([bypass/2]).
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
@@ -70,6 +70,9 @@ call(Fd, Msg, Metadata) ->
 get_queue_lengths() ->
     gen_server:call(?MODULE, get_queue_lengths).
 
+get_disk_queues() ->
+    maps:to_list(get_queue_lengths()).
+
 bypass(Msg, Metadata) ->
     Priority = io_class(Msg, Metadata),
     case Priority of
diff --git a/test/fixtures/allowed-xref.txt b/test/fixtures/allowed-xref.txt
index c630fc109..e69de29bb 100644
--- a/test/fixtures/allowed-xref.txt
+++ b/test/fixtures/allowed-xref.txt
@@ -1,2 +0,0 @@
-src/ioq.erl: Warning: ioq:get_disk_queues/0 is undefined function (Xref)
-src/weatherreport_check_ioq.erl:{95,1}: Warning: 
weatherreport_check_ioq:check_legacy_int/1 calls undefined function 
ioq:get_disk_queues/0 (Xref)

Reply via email to