C0 code coverage information

Generated on 2009-08-09 21:22:54 with etap 0.3.4.

Name Total lines Lines of code Total coverage Code coverage
couch_stats_aggregator ?? ?? ??
58% 
....1 % Licensed under the Apache License, Version 2.0 (the "License"); you may not
....2 % use this file except in compliance with the License. You may obtain a copy of
....3 % the License at
....4 %
....5 %   http://www.apache.org/licenses/LICENSE-2.0
....6 %
....7 % Unless required by applicable law or agreed to in writing, software
....8 % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
....9 % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
...10 % License for the specific language governing permissions and limitations under
...11 % the License.
...12 
...13 -module(couch_stats_aggregator).
...14 -include("couch_stats.hrl").
...15 
...16 -behaviour(gen_server).
...17 
...18 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
...19         terminate/2, code_change/3]).
...20 
...21 -export([start/0, stop/0,
...22          get/1, get/2, get_json/1, get_json/2, all/0,
...23          time_passed/0, clear_aggregates/1]).
...24 
...25 -record(state, {
...26     aggregates = [],
...27     descriptions = []
...28 }).
...29 
...30 -define(COLLECTOR, couch_stats_collector).
...31 -define(QUEUE_MAX_LENGTH, 900). % maximimum number of seconds
...32 
...33 % PUBLIC API
...34 
...35 start() ->
...36     gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
...37 
...38 stop() ->
...39     gen_server:call(?MODULE, stop).
...40 
...41 get(Key) ->
...42     gen_server:call(?MODULE, {get, Key}).
...43 get(Key, Time) ->
...44     gen_server:call(?MODULE, {get, Key, Time}).
...45 
...46 get_json(Key) ->
...47     gen_server:call(?MODULE, {get_json, Key}).
...48 get_json(Key, Time) ->
...49     gen_server:call(?MODULE, {get_json, Key, Time}).
...50 
...51 time_passed() ->
...52     gen_server:call(?MODULE, time_passed).
...53 
...54 clear_aggregates(Time) ->
...55     gen_server:call(?MODULE, {clear_aggregates, Time}).
...56 
...57 all() ->
...58     gen_server:call(?MODULE, all).
...59 
...60 % GEN_SERVER
...61 
...62 init(_) ->
...63     ets:new(?MODULE, [named_table, set, protected]),
...64     init_timers(),
...65     init_descriptions(),
...66     {ok, #state{}}.
...67 
...68 handle_call({get, Key}, _, State) ->
...69     Value = get_aggregate(Key, State),
...70     {reply, Value, State};
...71 
...72 handle_call({get, Key, Time}, _, State) ->
...73     Value = get_aggregate(Key, State, Time),
...74     {reply, Value, State};
...75 
...76 handle_call({get_json, Key}, _, State) ->
...77     Value = aggregate_to_json_term(get_aggregate(Key, State)),
...78     {reply, Value, State};
...79 
...80 handle_call({get_json, Key, Time}, _, State) ->
...81     Value = aggregate_to_json_term(get_aggregate(Key, State, Time)),
...82     {reply, Value, State};
...83 
...84 handle_call(time_passed, _, OldState) ->
...85 
...86     % the foldls below could probably be refactored into a less code-duping form
...87 
...88     % update aggregates on incremental counters
...89     NextState = lists:foldl(fun(Counter, State) ->
...90         {Key, Value} = Counter,
...91         update_aggregates_loop(Key, Value, State, incremental)
...92     end, OldState, ?COLLECTOR:all(incremental)),
...93 
...94     % update aggregates on absolute value counters
...95     NewState = lists:foldl(fun(Counter, State) ->
...96         {Key, Value} = Counter,
...97         % clear the counter, we've got the important bits in State
...98         ?COLLECTOR:clear(Key),
...99         update_aggregates_loop(Key, Value, State, absolute)
..100     end, NextState, ?COLLECTOR:all(absolute)),
..101 
..102     {reply, ok, NewState};
..103 
..104 handle_call({clear_aggregates, Time}, _, State) ->
..105     {reply, ok, do_clear_aggregates(Time, State)};
..106 
..107 handle_call(all, _ , State) ->
..108     Results = do_get_all(State),
..109     {reply, Results, State};
..110 
..111 handle_call(stop, _, State) ->
..112     {stop, normal, stopped, State}.
..113 
..114 
..115 % PRIVATE API
..116 
..117 % Stats = [{Key, TimesProplist}]
..118 % TimesProplist = [{Time, Aggrgates}]
..119 % Aggregates = #aggregates{}
..120 %
..121 % [
..122 %  {Key, [
..123 %             {TimeA, #aggregates{}},
..124 %             {TimeB, #aggregates{}},
..125 %             {TimeC, #aggregates{}},
..126 %             {TimeD, #aggregates{}}
..127 %        ]
..128 %  },
..129 %
..130 % ]
..131 
..132 %% clear the aggregats record for a specific Time = 60 | 300 | 900
..133 do_clear_aggregates(Time, #state{aggregates=Stats}) ->
..134     NewStats = lists:map(fun({Key, TimesProplist}) ->
..135         {Key, case proplists:lookup(Time, TimesProplist) of
..136             % do have stats for this key, if we don't, return Stat unmodified
..137             none ->
..138                 TimesProplist;
..139             % there are stats, let's unset the Time one
..140             {_Time, _Stat} ->
..141                 [{Time, #aggregates{}} | proplists:delete(Time, TimesProplist)]
..142         end}
..143     end, Stats),
..144     #state{aggregates=NewStats}.
..145 
..146 get_aggregate(Key, State) ->
..147     %% default Time is 0, which is when CouchDB started
..148     get_aggregate(Key, State, '0').
..149 get_aggregate(Key, #state{aggregates=StatsList}, Time) ->
..150     Description = get_description(Key),
..151     Aggregates = case proplists:lookup(Key, StatsList) of
..152         % if we don't have any data here, return an empty record
..153         none -> #aggregates{description=Description};
..154         {Key, Stats} ->
..155             case proplists:lookup(Time, Stats) of
..156                 none -> #aggregates{description=Description}; % empty record again
..157                 {Time, Stat} -> Stat#aggregates{description=Description}
..158             end
..159     end,
..160     Aggregates.
..161 
..162 get_description(Key) ->
..163     case ets:lookup(?MODULE, Key) of
..164         [] -> <<"No description yet.">>;
..165         [{_Key, Description}] -> Description
..166     end.
..167 
..168 %% updates all aggregates for Key
..169 update_aggregates_loop(Key, Values, State, CounterType) ->
..170     #state{aggregates=AllStats} = State,
..171     % if we don't have any aggregates yet, put a list of empty atoms in
..172     % so we can loop over them in update_aggregates().
..173     % [{{httpd,requests},
..174     %              [{'0',{aggregates,1,1,1,0,0,1,1}},
..175     %               {'60',{aggregates,1,1,1,0,0,1,1}},
..176     %               {'300',{aggregates,1,1,1,0,0,1,1}},
..177     %               {'900',{aggregates,1,1,1,0,0,1,1}}]}]
..178     [{_Key, StatsList}] = case proplists:lookup(Key, AllStats) of
..179         none -> [{Key, [
..180                 {'0', empty},
..181                 {'60', empty},
..182                 {'300', empty},
..183                 {'900', empty}
..184              ]}];
..185         AllStatsMatch ->
..186         [AllStatsMatch]
..187     end,
..188 
..189     % if we  get called with a single value, wrap in in a list
..190     ValuesList = case is_list(Values) of
..191         false -> [Values];
..192         _True -> Values
..193     end,
..194 
..195     % loop over all Time's
..196     NewStats = lists:map(fun({Time, Stats}) ->
..197         % loop over all values for Key
..198         lists:foldl(fun(Value, Stat) ->
..199             {Time, update_aggregates(Value, Stat, CounterType)}
..200         end, Stats, ValuesList)
..201     end, StatsList),
..202 
..203     % put the newly calculated aggregates into State and delete the previous
..204     % entry
..205     #state{
..206         aggregates=[{Key, NewStats} | proplists:delete(Key, AllStats)]
..207     }.
..208 
..209 % does the actual updating of the aggregate record
..210 update_aggregates(Value, Stat, CounterType) ->
..211     case Stat of
..212         % the first time this is called, we don't have to calculate anything
..213         % we just populate the record with Value
..214         empty -> #aggregates{
..215             min=Value,
..216             max=Value,
..217             mean=Value,
..218             variance=0,
..219             stddev=0,
..220             count=1,
..221             current=Value
..222         };
..223         % this sure could look nicer -- any ideas?
..224         StatsRecord ->
..225             #aggregates{
..226                 min=Min,
..227                 max=Max,
..228                 mean=Mean,
..229                 variance=Variance,
..230                 count=Count,
..231                 current=Current
..232             } = StatsRecord,
..233 
..234             % incremental counters need to keep track of the last update's value
..235             NewValue = case CounterType of
..236                 incremental -> Value - Current;
..237                 absolute -> Value
..238             end,
..239                 % Knuth, The Art of Computer Programming, vol. 2, p. 232.
..240                 NewCount = Count + 1,
..241                 NewMean = Mean + (NewValue - Mean) / NewCount, % NewCount is never 0.
..242                 NewVariance = Variance + (NewValue - Mean) * (NewValue - NewMean),
..243                 #aggregates{
..244                     min=lists:min([NewValue, Min]),
..245                     max=lists:max([NewValue, Max]),
..246                     mean=NewMean,
..247                     variance=NewVariance,
..248                     stddev=math:sqrt(NewVariance / NewCount),
..249                     count=NewCount,
..250                     current=Value
..251                 }
..252     end.
..253 
..254 
..255 aggregate_to_json_term(#aggregates{min=Min,max=Max,mean=Mean,stddev=Stddev,count=Count,current=Current,description=Description}) ->
..256     {[
..257         {current, Current},
..258         {count, Count},
..259         {mean, Mean},
..260         {min, Min},
..261         {max, Max},
..262         {stddev, Stddev},
..263         {description, Description}
..264     ]}.
..265 
..266 get_stats(Key, State) ->
..267     aggregate_to_json_term(get_aggregate(Key, State)).
..268 
..269 % convert ets2list() list into JSON-erlang-terms.
..270 % Thanks to Paul Davis
..271 do_get_all(#state{aggregates=Stats}=State) ->
..272     case Stats of
..273         [] -> {[]};
..274         _ ->
..275         [{LastMod, LastVals} | LastRestMods] = lists:foldl(fun({{Module, Key}, _Count}, AccIn) ->
..276               case AccIn of
..277                   [] ->
..278                       [{Module, [{Key, get_stats({Module, Key}, State)}]}];
..279                   [{Module, PrevVals} | RestMods] ->
..280                       [{Module, [{Key, get_stats({Module, Key}, State)} | PrevVals]} | RestMods];
..281                   [{OtherMod, ModVals} | RestMods] ->
..282                       [{Module, [{Key, get_stats({Module, Key}, State)}]}, {OtherMod, {lists:reverse(ModVals)}} | RestMods]
..283               end
..284           end, [], lists:sort(Stats)),
..285           {[{LastMod, {lists:sort(LastVals)}} | LastRestMods]}
..286     end.
..287 
..288 
..289 init_descriptions() ->
..290 
..291     % ets is probably overkill here, but I didn't manage to keep the
..292     % descriptions in the gen_server state. Which means there is probably
..293     % a bug in one of the handle_call() functions most likely the one that
..294     % handles the time_passed message. But don't tell anyone, the math is
..295     % correct :) -- Jan
..296 
..297 
..298     % Style guide for descriptions: Start with a lowercase letter & do not add
..299     % a trailing full-stop / period.
..300 
..301     % please keep this in alphabetical order
..302     ets:insert(?MODULE, {{couchdb, database_writes}, <<"number of times a database was changed">>}),
..303     ets:insert(?MODULE, {{couchdb, database_reads}, <<"number of times a document was read from a database">>}),
..304     ets:insert(?MODULE, {{couchdb, open_databases}, <<"number of open databases">>}),
..305     ets:insert(?MODULE, {{couchdb, open_os_files}, <<"number of file descriptors CouchDB has open">>}),
..306     ets:insert(?MODULE, {{couchdb, request_time}, <<"length of a request inside CouchDB without MochiWeb">>}),
..307 
..308     ets:insert(?MODULE, {{httpd, bulk_requests}, <<"number of bulk requests">>}),
..309     ets:insert(?MODULE, {{httpd, requests}, <<"number of HTTP requests">>}),
..310     ets:insert(?MODULE, {{httpd, temporary_view_reads}, <<"number of temporary view reads">>}),
..311     ets:insert(?MODULE, {{httpd, view_reads}, <<"number of view reads">>}),
..312     ets:insert(?MODULE, {{httpd, clients_requesting_changes}, <<"Number of clients currently requesting continuous _changes">>}),
..313 
..314     ets:insert(?MODULE, {{httpd_request_methods, 'COPY'}, <<"number of HTTP COPY requests">>}),
..315     ets:insert(?MODULE, {{httpd_request_methods, 'DELETE'}, <<"number of HTTP DELETE requests">>}),
..316     ets:insert(?MODULE, {{httpd_request_methods, 'GET'}, <<"number of HTTP GET requests">>}),
..317     ets:insert(?MODULE, {{httpd_request_methods, 'HEAD'}, <<"number of HTTP HEAD requests">>}),
..318     ets:insert(?MODULE, {{httpd_request_methods, 'MOVE'}, <<"number of HTTP MOVE requests">>}),
..319     ets:insert(?MODULE, {{httpd_request_methods, 'POST'}, <<"number of HTTP POST requests">>}),
..320     ets:insert(?MODULE, {{httpd_request_methods, 'PUT'}, <<"number of HTTP PUT requests">>}),
..321 
..322     ets:insert(?MODULE, {{httpd_status_codes, '200'}, <<"number of HTTP 200 OK responses">>}),
..323     ets:insert(?MODULE, {{httpd_status_codes, '201'}, <<"number of HTTP 201 Created responses">>}),
..324     ets:insert(?MODULE, {{httpd_status_codes, '202'}, <<"number of HTTP 202 Accepted responses">>}),
..325     ets:insert(?MODULE, {{httpd_status_codes, '301'}, <<"number of HTTP 301 Moved Permanently responses">>}),
..326     ets:insert(?MODULE, {{httpd_status_codes, '304'}, <<"number of HTTP 304 Not Modified responses">>}),
..327     ets:insert(?MODULE, {{httpd_status_codes, '400'}, <<"number of HTTP 400 Bad Request responses">>}),
..328     ets:insert(?MODULE, {{httpd_status_codes, '401'}, <<"number of HTTP 401 Unauthorized responses">>}),
..329     ets:insert(?MODULE, {{httpd_status_codes, '403'}, <<"number of HTTP 403 Forbidden responses">>}),
..330     ets:insert(?MODULE, {{httpd_status_codes, '404'}, <<"number of HTTP 404 Not Found responses">>}),
..331     ets:insert(?MODULE, {{httpd_status_codes, '405'}, <<"number of HTTP 405 Method Not Allowed responses">>}),
..332     ets:insert(?MODULE, {{httpd_status_codes, '409'}, <<"number of HTTP 409 Conflict responses">>}),
..333     ets:insert(?MODULE, {{httpd_status_codes, '412'}, <<"number of HTTP 412 Precondition Failed responses">>}),
..334     ets:insert(?MODULE, {{httpd_status_codes, '500'}, <<"number of HTTP 500 Internal Server Error responses">>}).
..335     % please keep this in alphabetical order
..336 
..337 
..338 % Timer
..339 
..340 init_timers() ->
..341 
..342     % OTP docs on timer: http://erlang.org/doc/man/timer.html
..343     %   start() -> ok
..344     %   Starts the timer server. Normally, the server does not need to be
..345     %   started explicitly. It is started dynamically if it is needed. This is
..346     %   useful during development, but in a target system the server should be
..347     %   started explicitly. Use configuration parameters for kernel for this.
..348     %
..349     % TODO: Add timer_start to kernel start options.
..350 
..351 
..352     % start timers every second, minute, five minutes and fifteen minutes
..353     % in the rare event of a timer death, couch_stats_aggregator will die,
..354     % too and restarted by the supervision tree, all stats (for the last
..355     % fifteen minutes) are gone.
..356 
..357     {ok, _} = timer:apply_interval(1000, ?MODULE, time_passed, []),
..358     {ok, _} = timer:apply_interval(60000, ?MODULE, clear_aggregates, ['60']),
..359     {ok, _} = timer:apply_interval(300000, ?MODULE, clear_aggregates, ['300']),
..360     {ok, _} = timer:apply_interval(900000, ?MODULE, clear_aggregates, ['900']).
..361 
..362 
..363 % Unused gen_server behaviour API functions that we need to declare.
..364 
..365 %% @doc Unused
..366 handle_cast(foo, State) ->
..367     {noreply, State}.
..368 
..369 handle_info(_Info, State) ->
..370     {noreply, State}.
..371 
..372 %% @doc Unused
..373 terminate(_Reason, _State) -> ok.
..374 
..375 %% @doc Unused
..376 code_change(_OldVersion, State, _Extra) -> {ok, State}.
..377 
..378 
..379 %% Tests
..380 
..381 -ifdef(TEST).
..382 % Internal API unit tests go here
..383 
..384 
..385 -endif.

Generated using etap 0.3.4.