blob: 29190e6b003a6a2fe24b8817270e418bbaf6f941 [file] [log] [blame]
% 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_stats).
-export([
fetch/0,
reload/0,
sample/1,
increment_counter/1,
increment_counter/2,
decrement_counter/1,
decrement_counter/2,
update_histogram/2,
update_gauge/2
]).
%% couch_stats_resource_tracker API
-export([
create_context/3,
maybe_track_rexi_init_p/1
]).
-type response() :: ok | {error, unknown_metric} | {error, invalid_metric}.
-type stat() :: {any(), [{atom(), any()}]}.
fetch() ->
Seconds = couch_stats_util:histogram_interval_sec(),
StartSec = now_sec() - (Seconds - 1),
% Last -1 is because the interval ends are inclusive
couch_stats_util:fetch(stats(), StartSec, Seconds).
reload() ->
couch_stats_server:reload().
-spec sample(any()) -> stat().
sample(Name) ->
Seconds = couch_stats_util:histogram_interval_sec(),
StartSec = now_sec() - (Seconds - 1),
% Last -1 is because the interval ends are inclusive
couch_stats_util:sample(Name, stats(), StartSec, Seconds).
-spec increment_counter(any()) -> response().
increment_counter(Name) ->
increment_counter(Name, 1).
-spec increment_counter(any(), pos_integer()) -> response().
increment_counter(Name, Value) ->
%% Should maybe_track_local happen before or after notify?
%% If after, only currently tracked metrics declared in the app's
%% stats_description.cfg will be trackable locally. Pros/cons.
%io:format("NOTIFY_EXISTING_METRIC: ~p || ~p || ~p~n", [Name, Op, Type]),
ok = maybe_track_local_counter(Name, Value),
case couch_stats_util:get_counter(Name, stats()) of
{ok, Ctx} -> couch_stats_counter:increment(Ctx, Value);
{error, Error} -> {error, Error}
end.
-spec decrement_counter(any()) -> response().
decrement_counter(Name) ->
decrement_counter(Name, 1).
-spec decrement_counter(any(), pos_integer()) -> response().
decrement_counter(Name, Value) ->
case couch_stats_util:get_counter(Name, stats()) of
{ok, Ctx} -> couch_stats_counter:decrement(Ctx, Value);
{error, Error} -> {error, Error}
end.
-spec update_gauge(any(), number()) -> response().
update_gauge(Name, Value) ->
case couch_stats_util:get_gauge(Name, stats()) of
{ok, Ctx} -> couch_stats_gauge:update(Ctx, Value);
{error, Error} -> {error, Error}
end.
-spec update_histogram
(any(), number()) -> response();
(any(), function()) -> any().
update_histogram(Name, Fun) when is_function(Fun, 0) ->
Begin = erlang:monotonic_time(),
Result = Fun(),
Dt = erlang:monotonic_time() - Begin,
Duration = erlang:convert_time_unit(Dt, native, millisecond),
case update_histogram(Name, Duration) of
ok ->
Result;
{error, unknown_metric} ->
throw({unknown_metric, Name});
{error, invalid_metric} ->
throw({invalid_metric, Name})
end;
update_histogram(Name, Value) when is_number(Value) ->
case couch_stats_util:get_histogram(Name, stats()) of
{ok, Ctx} -> couch_stats_histogram:update(Ctx, now_sec(), Value);
{error, Error} -> {error, Error}
end.
stats() ->
couch_stats_util:stats().
now_sec() ->
erlang:monotonic_time(second).
%% Only potentially track positive increments to counters
-spec maybe_track_local_counter(any(), any()) -> ok.
maybe_track_local_counter(Name, Val) when is_integer(Val) andalso Val > 0 ->
%%io:format("maybe_track_local[~p]: ~p~n", [Val, Name]),
couch_stats_resource_tracker:maybe_inc(Name, Val),
ok;
maybe_track_local_counter(_, _) ->
ok.
create_context(From, MFA, Nonce) ->
couch_stats_resource_tracker:create_context(From, MFA, Nonce).
maybe_track_rexi_init_p({M, F, _A}) ->
Metric = [M, F, spawned],
case couch_stats_resource_tracker:should_track(Metric) of
true -> increment_counter(Metric);
false -> ok
end.
-ifdef(TEST).
-include_lib("couch/include/couch_eunit.hrl").
couch_stats_test_() ->
{
foreach,
fun setup/0,
fun teardown/1,
[
?TDEF_FE(t_fetch_metrics),
?TDEF_FE(t_sample_metrics),
?TDEF_FE(t_reload),
?TDEF_FE(t_increment_counter),
?TDEF_FE(t_decrement_counter),
?TDEF_FE(t_update_gauge),
?TDEF_FE(t_update_histogram),
?TDEF_FE(t_update_histogram_fun),
?TDEF_FE(t_access_invalid_metrics)
]
}.
setup() ->
test_util:start_couch([couch_replicator]).
teardown(Ctx) ->
config:delete("stats", "interval", _Persist = false),
test_util:stop_couch(Ctx).
t_fetch_metrics(_) ->
Metrics = fetch(),
?assertEqual(map_size(stats()), length(Metrics)),
?assertMatch([{_, [{value, _}, {type, _}, {desc, _}]} | _], Metrics).
t_sample_metrics(_) ->
Hist = sample([fsync, time]),
?assertMatch([{_Name, _Val} | _], Hist),
Count = sample([fsync, count]),
?assert(is_integer(Count)),
?assert(Count >= 0),
?assertEqual(0, sample([couch_replicator, jobs, total])).
t_reload(_) ->
% This is tested in detail in couch_stats_server.
?assertEqual(ok, reload()).
t_increment_counter(_) ->
[increment_counter([fsync, count]) || _ <- lists:seq(1, 1000)],
?assert(sample([fsync, count]) > 1000).
t_decrement_counter(_) ->
[decrement_counter([fsync, count]) || _ <- lists:seq(1, 10000)],
?assert(sample([fsync, count]) < 10).
t_update_gauge(_) ->
application:stop(couch_replicator),
% We don't want replicator to reset the gauge back to 0
update_gauge([couch_replicator, jobs, total], 42),
?assertEqual(42, sample([couch_replicator, jobs, total])).
t_update_histogram(_) ->
[update_histogram([fsync, time], rand:uniform(1000)) || _ <- lists:seq(1, 1000)],
Hist = sample([fsync, time]),
N = proplists:get_value(n, Hist),
?assert(is_integer(N)),
?assert(N >= 1000).
t_update_histogram_fun(_) ->
Fun = fun() -> timer:sleep(rand:uniform(2)) end,
[update_histogram([fsync, time], Fun) || _ <- lists:seq(1, 100)],
Hist = sample([fsync, time]),
N = proplists:get_value(n, Hist),
?assert(is_integer(N)),
?assert(N >= 100).
t_access_invalid_metrics(_) ->
Fun = fun() -> ok end,
?assertThrow(unknown_metric, sample([invalid])),
?assertEqual({error, unknown_metric}, increment_counter([invalid], 100)),
?assertEqual({error, unknown_metric}, decrement_counter([invalid], 100)),
?assertEqual({error, unknown_metric}, update_gauge([invalid], 100)),
?assertEqual({error, unknown_metric}, update_histogram([invalid], 100)),
?assertThrow({unknown_metric, _}, update_histogram([invalid], Fun)),
% Invalid metric types
?assertEqual({error, invalid_metric}, increment_counter([fsync, time], 100)),
?assertEqual({error, invalid_metric}, decrement_counter([fsync, time], 100)),
?assertEqual({error, invalid_metric}, update_gauge([fsync, count], 100)),
?assertEqual({error, invalid_metric}, update_histogram([fsync, count], 100)),
?assertThrow({invalid_metric, _}, update_histogram([fsync, count], Fun)),
InvalidMetrics = #{[bad] => {invalid, <<"desc">>}},
?assertThrow({unknown_metric, _}, couch_stats_util:create_metrics(InvalidMetrics)).
-endif.