You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by to...@apache.org on 2021/04/14 00:23:44 UTC

[couchdb] 01/05: Add new app couch_prometheus

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

tonysun83 pushed a commit to branch port-prometheus-3.x
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit a69766e47396fe0d5a059ff748c6573ae5774570
Author: Tony Sun <to...@gmail.com>
AuthorDate: Wed Mar 10 08:13:51 2021 -0800

    Add new app couch_prometheus
    
    This will be a new app add a _prometheus endpoint which will
    return metrics information that adheres to the format described at
    https://prometheus.io/.
    
    Initial implementation of new _prometheus endpoint. A gen_server
    waits for scraping calls while polling couch_stats:fetch and
    other system info. The return value is constructed to adhere to
    prometheus format and returned as text/plain. The format code
    was originally written by @davisp.
---
 rebar.config.script                                |   1 +
 rel/reltool.config                                 |   2 +
 src/chttpd/src/chttpd_node.erl                     |   6 +
 src/couch/src/couch.app.src                        |   3 +-
 src/couch_prometheus/src/couch_prometheus.app.src  |  20 +++
 src/couch_prometheus/src/couch_prometheus.hrl      |  13 ++
 src/couch_prometheus/src/couch_prometheus_app.erl  |  23 +++
 .../src/couch_prometheus_server.erl                | 168 +++++++++++++++++++++
 src/couch_prometheus/src/couch_prometheus_sup.erl  |  33 ++++
 src/couch_prometheus/src/couch_prometheus_util.erl | 166 ++++++++++++++++++++
 .../test/eunit/couch_prometheus_util_tests.erl     |  67 ++++++++
 11 files changed, 501 insertions(+), 1 deletion(-)

diff --git a/rebar.config.script b/rebar.config.script
index f12ef38..a878524 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -139,6 +139,7 @@ SubDirs = [
     "src/rexi",
     "src/setup",
     "src/smoosh",
+    "src/couch_prometheus",
     "rel"
 ].
 
diff --git a/rel/reltool.config b/rel/reltool.config
index 70f7bbc..c966e9d 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -61,6 +61,7 @@
         setup,
         smoosh,
         snappy,
+        couch_prometheus,
         %% extra
         recon
     ]},
@@ -121,6 +122,7 @@
     {app, setup, [{incl_cond, include}]},
     {app, smoosh, [{incl_cond, include}]},
     {app, snappy, [{incl_cond, include}]},
+    {app, couch_prometheus, [{incl_cond, include}]},
 
     %% extra
     {app, recon, [{incl_cond, include}]}
diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl
index c48dfc0..063ef4b 100644
--- a/src/chttpd/src/chttpd_node.erl
+++ b/src/chttpd/src/chttpd_node.erl
@@ -117,6 +117,12 @@ handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_stats">> | Path]}=
     chttpd:send_json(Req, EJSON1);
 handle_node_req(#httpd{path_parts=[_, _Node, <<"_stats">>]}=Req) ->
     send_method_not_allowed(Req, "GET");
+handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_prometheus">>]}=Req) ->
+    Metrics = call_node(Node, couch_prometheus_server, scrape, []),
+    Header = [{<<"Content-Type">>, <<"text/plain">>}],
+    chttpd:send_response(Req, 200, Header, Metrics);
+handle_node_req(#httpd{path_parts=[_, _Node, <<"_prometheus">>]}=Req) ->
+    send_method_not_allowed(Req, "GET");
 % GET /_node/$node/_system
 handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_system">>]}=Req) ->
     Stats = call_node(Node, chttpd_node, get_stats, []),
diff --git a/src/couch/src/couch.app.src b/src/couch/src/couch.app.src
index 6116c79..74674bb 100644
--- a/src/couch/src/couch.app.src
+++ b/src/couch/src/couch.app.src
@@ -45,7 +45,8 @@
         couch_event,
         ioq,
         couch_stats,
-        hyper
+        hyper,
+        couch_prometheus
     ]},
     {env, [
         { httpd_global_handlers, [
diff --git a/src/couch_prometheus/src/couch_prometheus.app.src b/src/couch_prometheus/src/couch_prometheus.app.src
new file mode 100644
index 0000000..3080b29
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus.app.src
@@ -0,0 +1,20 @@
+% 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.
+
+{application, couch_prometheus, [
+    {description, "Aggregated metrics info for Prometheus consumption"},
+    {vsn, git},
+    {registered, []},
+    {applications, [kernel, stdlib, folsom, couch_stats]},
+    {mod, {couch_prometheus_app, []}},
+    {env, []}
+]}.
diff --git a/src/couch_prometheus/src/couch_prometheus.hrl b/src/couch_prometheus/src/couch_prometheus.hrl
new file mode 100644
index 0000000..e82fb90
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus.hrl
@@ -0,0 +1,13 @@
+% 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.
+
+-define(REFRESH_INTERVAL, 60).
diff --git a/src/couch_prometheus/src/couch_prometheus_app.erl b/src/couch_prometheus/src/couch_prometheus_app.erl
new file mode 100644
index 0000000..232c16a
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus_app.erl
@@ -0,0 +1,23 @@
+% 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_app).
+
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) ->
+    couch_prometheus_sup:start_link().
+
+stop(_State) ->
+    ok.
diff --git a/src/couch_prometheus/src/couch_prometheus_server.erl b/src/couch_prometheus/src/couch_prometheus_server.erl
new file mode 100644
index 0000000..a0accba
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus_server.erl
@@ -0,0 +1,168 @@
+% 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_server).
+
+-behaviour(gen_server).
+
+-import(couch_prometheus_util, [
+    couch_to_prom/3,
+    to_prom/3,
+    to_prom_summary/2
+]).
+
+-export([
+    start_link/0,
+    init/1,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3,
+    terminate/2,
+
+    scrape/0
+]).
+
+-include("couch_prometheus.hrl").
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+-record(st, {
+    metrics,
+    refresh
+}).
+
+init([]) ->
+    Metrics = refresh_metrics(),
+    RT = update_refresh_timer(),
+    {ok, #st{metrics=Metrics, refresh=RT}}.
+
+scrape() ->
+    {ok, Metrics} = gen_server:call(?MODULE, scrape),
+    Metrics.
+
+
+handle_call(scrape, _from, #st{metrics = Metrics}=State) ->
+    {reply, {ok, Metrics}, State};
+handle_call(refresh, _from, #st{refresh=OldRT} = State) ->
+    timer:cancel(OldRT),
+    Metrics = refresh_metrics(),
+    RT = update_refresh_timer(),
+    {reply, ok, State#st{metrics=Metrics, refresh=RT}};
+handle_call(Msg, _From, State) ->
+    {stop, {unknown_call, Msg}, error, State}.
+
+handle_cast(Msg, State) ->
+    {stop, {unknown_cast, Msg}, State}.
+
+handle_info(refresh, State) ->
+    Metrics = refresh_metrics(),
+    {noreply, State#st{metrics=Metrics}};
+handle_info(Msg, State) ->
+    {stop, {unknown_info, Msg}, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+refresh_metrics() ->
+    CouchDB = get_couchdb_stats(),
+    System = couch_stats_httpd:to_ejson(get_system_stats()),
+    couch_prometheus_util:to_bin(lists:map(fun(Line) ->
+        io_lib:format("~s~n", [Line])
+    end, CouchDB ++ System)).
+
+get_couchdb_stats() ->
+    Stats = lists:sort(couch_stats:fetch()),
+    lists:flatmap(fun({Path, Info}) ->
+        couch_to_prom(Path, Info, Stats)
+    end, Stats).
+
+get_system_stats() ->
+    lists:flatten([
+        get_uptime_stat(),
+        get_vm_stats(),
+        get_io_stats(),
+        get_message_queue_stats(),
+        get_run_queue_stats(),
+        get_vm_stats(),
+        get_ets_stats()
+    ]).
+
+get_uptime_stat() ->
+    to_prom(uptime_seconds, counter, couch_app:uptime() div 1000).
+
+get_vm_stats() ->
+    MemLabels = lists:map(fun({Type,  Value}) ->
+        {[{memory_type, Type}], Value}
+    end, erlang:memory()),
+    {NumGCs, WordsReclaimed, _} = erlang:statistics(garbage_collection),
+    CtxSwitches = element(1, erlang:statistics(context_switches)),
+    Reds = element(1, erlang:statistics(reductions)),
+    ProcCount = erlang:system_info(process_count),
+    ProcLimit = erlang:system_info(process_limit),
+    [
+        to_prom(erlang_memory_bytes, gauge, MemLabels),
+        to_prom(erlang_gc_collections_total, counter, NumGCs),
+        to_prom(erlang_gc_words_reclaimed_total, counter, WordsReclaimed),
+        to_prom(erlang_context_switches_total, counter, CtxSwitches),
+        to_prom(erlang_reductions_total, counter, Reds),
+        to_prom(erlang_processes, gauge, ProcCount),
+        to_prom(erlang_process_limit, gauge, ProcLimit)
+    ].
+
+get_io_stats() ->
+    {{input, In}, {output, Out}} = erlang:statistics(io),
+    [
+        to_prom(erlang_io_recv_bytes_total, counter, In),
+        to_prom(erlang_io_sent_bytes_total, counter, Out)
+    ].
+
+get_message_queue_stats() ->
+    Queues = lists:map(fun(Name) ->
+        case process_info(whereis(Name), message_queue_len) of
+            {message_queue_len, N} ->
+                N;
+            _ ->
+                0
+        end
+    end, registered()),
+    [
+        to_prom(erlang_message_queues, gauge, lists:sum(Queues)),
+        to_prom(erlang_message_queue_min, gauge, lists:min(Queues)),
+        to_prom(erlang_message_queue_max, gauge, lists:max(Queues))
+    ].
+
+get_run_queue_stats() ->
+    %% Workaround for https://bugs.erlang.org/browse/ERL-1355
+    {Normal, Dirty} = case erlang:system_info(dirty_cpu_schedulers) > 0 of
+        false ->
+            {statistics(run_queue), 0};
+        true ->
+            [DCQ | SQs] = lists:reverse(statistics(run_queue_lengths)),
+            {lists:sum(SQs), DCQ}
+    end,
+    [
+        to_prom(erlang_scheduler_queues, gauge, Normal),
+        to_prom(erlang_dirty_cpu_scheduler_queues, gauge, Dirty)
+    ].
+
+get_ets_stats() ->
+    NumTabs = length(ets:all()),
+    to_prom(erlang_ets_table, gauge, NumTabs).
+
+update_refresh_timer() ->
+    RefreshTime = 1000 * config:get_integer("couch_prometheus", "interval", ?REFRESH_INTERVAL),
+    erlang:send_after(RefreshTime, self(), refresh).
diff --git a/src/couch_prometheus/src/couch_prometheus_sup.erl b/src/couch_prometheus/src/couch_prometheus_sup.erl
new file mode 100644
index 0000000..09ed45f
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus_sup.erl
@@ -0,0 +1,33 @@
+% 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_sup).
+
+-behaviour(supervisor).
+
+-export([
+    start_link/0,
+    init/1
+]).
+
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+    {ok, {
+        {one_for_one, 5, 10}, [
+            ?CHILD(couch_prometheus_server, worker)
+        ]
+    }}.
+
diff --git a/src/couch_prometheus/src/couch_prometheus_util.erl b/src/couch_prometheus/src/couch_prometheus_util.erl
new file mode 100644
index 0000000..c3b58cb
--- /dev/null
+++ b/src/couch_prometheus/src/couch_prometheus_util.erl
@@ -0,0 +1,166 @@
+% 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_util ).
+
+-export([
+    couch_to_prom/3,
+    to_bin/1,
+    to_prom/3,
+    to_prom_summary/2
+]).
+
+-include("couch_prometheus.hrl").
+
+couch_to_prom([couch_log, level, alert], Info, _All) ->
+    to_prom(couch_log_requests_total, counter, {[{level, alert}], val(Info)});
+couch_to_prom([couch_log, level, Level], Info, _All) ->
+    to_prom(couch_log_requests_total, {[{level, Level}], val(Info)});
+
+couch_to_prom([couch_replicator, checkpoints, failure], Info, _All) ->
+    to_prom(couch_replicator_checkpoints_failure_total, counter, val(Info));
+couch_to_prom([couch_replicator, checkpoints, success], Info, All) ->
+    Total = val(Info) + val([couch_replicator, checkpoints, failure], All),
+    to_prom(couch_replicator_checkpoints_total, counter, Total);
+couch_to_prom([couch_replicator, responses, failure], Info, _All) ->
+    to_prom(couch_replicator_responses_failure_total, counter, val(Info));
+couch_to_prom([couch_replicator, responses, success], Info, All) ->
+    Total = val(Info) + val([couch_replicator, responses, failure], All),
+    to_prom(couch_replicator_responses_total, counter, Total);
+couch_to_prom([couch_replicator, stream_responses, failure], Info, _All) ->
+    to_prom(couch_replicator_stream_responses_failure_total, counter, val(Info));
+couch_to_prom([couch_replicator, stream_responses, success], Info, All) ->
+    Total = val(Info) + val([couch_replicator, stream_responses, failure], All),
+    to_prom(couch_replicator_stream_responses_total, counter, Total);
+
+couch_to_prom([couchdb, auth_cache_hits], Info, All) ->
+    Total = val(Info) + val([couchdb, auth_cache_misses], All),
+    to_prom(auth_cache_requests_total, counter, Total);
+couch_to_prom([couchdb, auth_cache_misses], Info, _All) ->
+    to_prom(auth_cache_misses_total, counter, val(Info));
+couch_to_prom([couchdb, httpd_request_methods, 'COPY'], Info, _All) ->
+    to_prom(httpd_request_methods, counter, {[{method, 'COPY'}], val(Info)});
+couch_to_prom([couchdb, httpd_request_methods, Method], Info, _All) ->
+    to_prom(httpd_request_methods, {[{method, Method}], val(Info)});
+couch_to_prom([couchdb, httpd_status_codes, Code], Info, _All) ->
+    to_prom(httpd_status_codes, {[{code, Code}], val(Info)});
+
+couch_to_prom([ddoc_cache, hit], Info, All) ->
+    Total = val(Info) + val([ddoc_cache, miss], All),
+    to_prom(ddoc_cache_requests_total, counter, Total);
+couch_to_prom([ddoc_cache, miss], Info, _All) ->
+    to_prom(ddoc_cache_requests_failures_total, counter, val(Info));
+couch_to_prom([ddoc_cache, recovery], Info, _All) ->
+    to_prom(ddoc_cache_requests_recovery_total, counter, val(Info));
+
+couch_to_prom([fabric, read_repairs, failure], Info, _All) ->
+    to_prom(fabric_read_repairs_failures_total, counter, val(Info));
+couch_to_prom([fabric, read_repairs, success], Info, All) ->
+    Total = val(Info) + val([fabric, read_repairs, failure], All),
+    to_prom(fabric_read_repairs_total, counter, Total);
+
+couch_to_prom([rexi, streams, timeout, init_stream], Info, _All) ->
+    to_prom(rexi_streams_timeout_total, counter, {[{stage, init_stream}], val(Info)});
+couch_to_prom([rexi_streams, timeout, Stage], Info, _All) ->
+    to_prom(rexi_streams_timeout_total, {[{stage, Stage}], val(Info)});
+
+couch_to_prom([couchdb | Rest], Info, All) ->
+    couch_to_prom(Rest, Info, All);
+
+couch_to_prom(Path, Info, _All) ->
+    case lists:keyfind(type, 1, Info) of
+        {type, counter} ->
+            Metric = counter_metric(Path),
+            to_prom(Metric, counter, val(Info));
+        {type, gauge} ->
+            to_prom(path_to_name(Path), gauge, val(Info));
+        {type, histogram} ->
+            to_prom_summary(Path, Info)
+    end.
+
+to_prom(Metric, Type, Data) ->
+    TypeStr = to_bin(io_lib:format("# TYPE ~s ~s", [to_prom_name(Metric), Type])),
+    [TypeStr] ++ to_prom(Metric, Data).
+
+to_prom(Metric, Instances) when is_list(Instances) ->
+    lists:flatmap(fun(Inst) -> to_prom(Metric, Inst) end, Instances);
+to_prom(Metric, {Labels, Value}) ->
+    LabelParts = lists:map(fun({K, V}) ->
+        lists:flatten(io_lib:format("~s=\"~s\"", [to_bin(K), to_bin(V)]))
+    end, Labels),
+    MetricStr = case length(LabelParts) > 0 of
+        true ->
+            LabelStr = string:join(LabelParts, ", "),
+            lists:flatten(io_lib:format("~s{~s}", [to_prom_name(Metric), LabelStr]));
+        false ->
+            lists:flatten(io_lib:format("~s", [to_prom_name(Metric)]))
+    end,
+    [to_bin(io_lib:format("~s ~p", [MetricStr, Value]))];
+to_prom(Metric, Value) ->
+    [to_bin(io_lib:format("~s ~p", [to_prom_name(Metric), Value]))].
+
+to_prom_summary(Path, Info) ->
+    Metric = path_to_name(Path ++ ["seconds"]),
+    {value, Value} = lists:keyfind(value, 1, Info),
+    {arithmetic_mean, Mean} = lists:keyfind(arithmetic_mean, 1, Value),
+    {percentile, Percentiles} = lists:keyfind(percentile, 1, Value),
+    {n, Count} = lists:keyfind(n, 1, Value),
+    Quantiles = lists:map(fun({Perc, Val0}) ->
+        % Prometheus uses seconds, so we need to covert milliseconds to seconds
+        Val = Val0/1000, 
+        case Perc of
+            50 -> {[{quantile, <<"0.5">>}], Val};
+            75 -> {[{quantile, <<"0.75">>}], Val};
+            90 -> {[{quantile, <<"0.9">>}], Val};
+            95 -> {[{quantile, <<"0.95">>}], Val};
+            99 -> {[{quantile, <<"0.99">>}], Val};
+            999 -> {[{quantile, <<"0.999">>}], Val}
+        end
+    end, Percentiles),
+    SumMetric = path_to_name(Path ++ ["seconds", "sum"]),
+    SumStat = to_prom(SumMetric, Count * Mean),
+    CountMetric = path_to_name(Path ++ ["seconds", "count"]),
+    CountStat = to_prom(CountMetric, Count),
+    to_prom(Metric, summary, Quantiles) ++ [SumStat, CountStat].
+
+to_prom_name(Metric) ->
+    to_bin(io_lib:format("couchdb_~s", [Metric])).
+
+path_to_name(Path) ->
+    Parts = lists:map(fun(Part) ->
+        io_lib:format("~s", [Part])
+    end, Path),
+    string:join(Parts, "_").
+
+counter_metric(Path) ->
+    Name = path_to_name(Path),
+    case string:find(Name, <<"_total">>, trailing) == <<"_total">> of
+        true -> Name;
+        false -> to_bin(io_lib:format("~s_total", [Name]))
+    end.
+
+to_bin(Data) when is_list(Data) ->
+    iolist_to_binary(Data);
+to_bin(Data) when is_atom(Data) ->
+    atom_to_binary(Data, utf8);
+to_bin(Data) when is_integer(Data) ->
+    integer_to_binary(Data);
+to_bin(Data) when is_binary(Data) ->
+    Data.
+
+val(Data) ->
+    {value, V} = lists:keyfind(value, 1, Data),
+    V.
+
+val(Key, Stats) ->
+    {Key, Data} = lists:keyfind(Key, 1, Stats),
+    val(Data).
\ No newline at end of file
diff --git a/src/couch_prometheus/test/eunit/couch_prometheus_util_tests.erl b/src/couch_prometheus/test/eunit/couch_prometheus_util_tests.erl
new file mode 100644
index 0000000..f45a8ec
--- /dev/null
+++ b/src/couch_prometheus/test/eunit/couch_prometheus_util_tests.erl
@@ -0,0 +1,67 @@
+% 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_util_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+
+-import(couch_prometheus_util, [
+    to_prom/3,
+    to_prom_summary/2
+]).
+
+couch_prometheus_util_test_() ->
+    [
+         ?_assertEqual(<<"couchdb_ddoc_cache 10">>,
+            test_to_prom_output(ddoc_cache, counter, 10)),
+         ?_assertEqual(<<"couchdb_httpd_status_codes{code=\"200\"} 3">>,
+            test_to_prom_output(httpd_status_codes, counter, {[{code, 200}], 3})),
+         ?_assertEqual(<<"couchdb_temperature_celsius 36">>,
+            test_to_prom_output(temperature_celsius, gauge, 36)),
+         ?_assertEqual(<<"couchdb_mango_query_time_seconds{quantile=\"0.75\"} 4.5">>,
+            test_to_prom_sum_output([mango_query_time], [
+                {value,
+                    [
+                        {min,0.0},
+                        {max,0.0},
+                        {arithmetic_mean,0.0},
+                        {geometric_mean,0.0},
+                        {harmonic_mean,0.0},
+                        {median,0.0},{variance,0.0},
+                        {standard_deviation,0.0},
+                        {skewness,0.0},{kurtosis,0.0},
+                        {percentile,[
+                            {50,0.0},
+                            {75, 4500},
+                            {90,0.0},
+                            {95,0.0},
+                            {99,0.0},
+                            {999,0.0}]},
+                            {histogram,[
+                                {0,0}]},
+                            {n,0}
+                    ]
+                },
+                {type,histogram},
+                {desc, <<"length of time processing a mango query">>}
+            ]))
+    ].
+
+test_to_prom_output(Metric, Type, Val) ->
+    Out = to_prom(Metric, Type, Val),
+    lists:nth(2, Out).
+
+
+test_to_prom_sum_output(Metric, Info) ->
+    Out = to_prom_summary(Metric, Info),
+    ?debugMsg(Out),
+    lists:nth(3, Out).
\ No newline at end of file