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 | ?? | ?? | ?? |
|
....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.