diff --git a/Makefile.am b/Makefile.am
index 41ca5cf..2144f19 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -70,3 +70,7 @@ distsign: distcheck
> $(top_srcdir)/$(distdir).tar.gz.md5
sha1sum $(top_srcdir)/$(distdir).tar.gz \
> $(top_srcdir)/$(distdir).tar.gz.sha
+
+.PHONY: t
+t: all
+ @erl -pa src/couchdb/ -eval "couch_stats_aggregator:test()" -eval "couch_stats_collector:test()"
\ No newline at end of file
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 28ab36f..06e8b36 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -33,6 +33,8 @@ external_manager={couch_external_manager, start_link, []}
db_update_notifier={couch_db_update_notifier_sup, start_link, []}
query_servers={couch_query_servers, start_link, []}
httpd={couch_httpd, start_link, []}
+stats_aggregator={couch_stats_aggregator, start, []}
+stats_collector={couch_stats_collector, start, []}
[httpd_global_handlers]
/ = {couch_httpd_misc_handlers, handle_welcome_req, <<"Welcome">>}
@@ -40,12 +42,14 @@ favicon.ico = {couch_httpd_misc_handlers, handle_favicon_req, "%localdatadir%/ww
_utils = {couch_httpd_misc_handlers, handle_utils_dir_req, "%localdatadir%/www"}
_all_dbs = {couch_httpd_misc_handlers, handle_all_dbs_req}
-_stats = {couch_httpd_misc_handlers, handle_stats_req}
+;_stats = {couch_httpd_misc_handlers, handle_stats_req}
_active_tasks = {couch_httpd_misc_handlers, handle_task_status_req}
_config = {couch_httpd_misc_handlers, handle_config_req}
_replicate = {couch_httpd_misc_handlers, handle_replicate_req}
_uuids = {couch_httpd_misc_handlers, handle_uuids_req}
_restart = {couch_httpd_misc_handlers, handle_restart_req}
+_stats = {couch_httpd_stats_handlers, handle_stats_req}
+
[httpd_db_handlers]
_view = {couch_httpd_view, handle_view_req}
diff --git a/etc/couchdb/local_dev.ini b/etc/couchdb/local_dev.ini
index 09123cf..510f5c6 100644
--- a/etc/couchdb/local_dev.ini
+++ b/etc/couchdb/local_dev.ini
@@ -12,7 +12,10 @@
;bind_address = 127.0.0.1
[log]
-level = error
+level = debug
[update_notification]
;unique notifier name=/full/path/to/exe -with "cmd line arg"
+
+[test]
+foo = bar
diff --git a/share/www/script/couch.js b/share/www/script/couch.js
index 0a2698a..7926645 100644
--- a/share/www/script/couch.js
+++ b/share/www/script/couch.js
@@ -295,6 +295,13 @@ CouchDB.request = function(method, uri, options) {
return req;
}
+CouchDB.requestStats = function(module, key, options) {
+ options = options || {};
+ var stat = CouchDB.request("GET", "/_stats/" + module + "/" + key +
+ "?" + CouchDB.params(options)).responseText;
+ return JSON.parse(stat)[module][key];
+}
+
CouchDB.uuids_cache = [];
CouchDB.newUuids = function(n) {
@@ -327,3 +334,13 @@ CouchDB.maybeThrowError = function(req) {
throw result;
}
}
+
+CouchDB.params = function(options) {
+ options = options || {};
+ var returnArray = [];
+ for(var key in options) {
+ var value = options[key];
+ returnArray.push(key + "=" + value);
+ }
+ return returnArray.join("&");
+}
\ No newline at end of file
diff --git a/share/www/script/couch_test_runner.js b/share/www/script/couch_test_runner.js
index 31bd4a4..beede46 100644
--- a/share/www/script/couch_test_runner.js
+++ b/share/www/script/couch_test_runner.js
@@ -152,13 +152,13 @@ function updateTestsFooter() {
// display the line that failed.
// Example:
// T(MyValue==1);
-function T(arg1, arg2) {
+function T(arg1, arg2, testName) {
if (!arg1) {
if (currentRow) {
if ($("td.details ol", currentRow).length == 0) {
$("
").appendTo($("td.details", currentRow));
}
- $("Assertion failed: ")
+ $("Assertion " + (testName ? "'" + testName + "'" : "") + " failed: ")
.find("code").text((arg2 != null ? arg2 : arg1).toString()).end()
.appendTo($("td.details ol", currentRow));
}
@@ -166,6 +166,10 @@ function T(arg1, arg2) {
}
}
+function TEquals(expected, actual, testName) {
+ T(equals(expected, actual), "expected '" + expected + "', got '" + actual + "'", testName);
+}
+
function equals(a,b) {
if (a === b) return true;
try {
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index a6c47a7..e1eefd9 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -21,6 +21,425 @@ if (typeof window == 'undefined' || !window) {
var tests = {
+ stats: function(debug) {
+ if (debug) debugger;
+
+ var open_databases_tests = {
+ 'should increment the number of open databases when creating a db': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ var open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+ db.createDb();
+
+ var new_open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+ TEquals(parseInt(open_databases) + 1, parseInt(new_open_databases), name);
+ },
+ 'should increment the number of open databases when opening a db': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ restartServer();
+
+ var open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+
+ db.open("123");
+
+ var new_open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+ TEquals(parseInt(open_databases) + 1, parseInt(new_open_databases), name);
+ },
+ 'should decrement the number of open databases when deleting': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ var open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+
+ db.deleteDb();
+ var new_open_databases = parseInt(CouchDB.requestStats("couch_db", "open_databases"));
+ TEquals(parseInt(open_databases) - 1, parseInt(new_open_databases), name);
+ },
+ 'should keep the same number of open databases when reaching the max_dbs_open limit': function(name) {
+ restartServer();
+ var max = 5;
+ run_on_modified_server(
+ [{section: "couchdb",
+ key: "max_dbs_open",
+ value: max.toString()}],
+
+ function () {
+ for(var i=0; i= 0, "requests >= 0", name);
+ TEquals(requests + 1, new_requests, name);
+ },
+ 'should return the average requests/s for the last minute': function(name) {
+ restartServer();
+ var requests = parseFloat(CouchDB.requestStats("httpd", "average_requests"));
+ TEquals(requests, 0.0, name);
+ },
+ 'should return the average request/s for the last 5 and 15 minutes': function(name) {
+ restartServer();
+ var requests = parseInt(CouchDB.requestStats("httpd", "average_requests"), {"timeframe":5});
+ TEquals(requests, 0, name);
+
+ var requests = parseInt(CouchDB.requestStats("httpd", "average_requests"), {"timeframe":15});
+ TEquals(requests, 0, name);
+ }
+ };
+
+ var document_read_count_tests = {
+ 'should increase read document counter when a document is read': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ db.save({"_id":"test"});
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+ db.open("test");
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+
+ TEquals(reads + 1 , new_reads, name);
+ },
+ 'should not increase read document counter when a non-document is read': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ db.save({"_id":"test"});
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+ CouchDB.request("GET", "/");
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+
+ TEquals(reads, new_reads, name);
+ },
+ 'should increase read document counter when a document\'s revisions are read': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ db.save({"_id":"test"});
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+ db.open("test", {"open_revs":"all"});
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "document_reads"));
+
+ TEquals(reads + 1 , new_reads, name);
+ }
+ };
+
+ var view_read_count_tests = {
+ 'should increase the permanent view read counter': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+ createAndRequestView(db);
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+
+ TEquals(reads + 1 , new_reads, name);
+ },
+ 'should not increase the permanent view read counter when a document is read': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ db.save({"_id":"test"});
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+ db.open("test");
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+
+ TEquals(reads, new_reads, name);
+ },
+ 'should not increase the permanent view read counter when a temporary view is read': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+ db.query(function(doc) { emit(doc._id)});
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+
+ TEquals(reads, new_reads, name);
+ },
+ 'should increase the temporary view read counter': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "temporary_view_reads"));
+ db.query(function(doc) { emit(doc._id)});
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "temporary_view_reads"));
+
+ TEquals(reads + 1, new_reads, name);
+ },
+ 'should increase the temporary view read counter when querying a permanent view': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+ createAndRequestView(db);
+ var new_reads = parseInt(CouchDB.requestStats("httpd", "view_reads"));
+
+ TEquals(reads + 1 , new_reads, name);
+ }
+ };
+
+ var http_requests_by_method_tests = {
+ 'should count GET requests': function(name) {
+ var requests = parseInt(CouchDB.requestStats("httpd", "get_requests"));
+ var new_requests = parseInt(CouchDB.requestStats("httpd", "get_requests"));
+
+ TEquals(requests + 1, new_requests, name);
+ },
+ 'should not count GET requests for POST request': function(name) {
+ var requests = parseInt(CouchDB.requestStats("httpd", "get_requests"));
+ CouchDB.request("POST", "/");
+ var new_requests = parseInt(CouchDB.requestStats("httpd", "get_requests"));
+
+ TEquals(requests + 1, new_requests, name);
+ },
+ 'should count POST requests': function(name) {
+ var requests = parseInt(CouchDB.requestStats("httpd", "post_requests"));
+ CouchDB.request("POST", "/");
+ var new_requests = parseInt(CouchDB.requestStats("httpd", "post_requests"));
+
+ TEquals(requests + 1, new_requests, name);
+ }
+ };
+
+ var document_write_count_tests = {
+ 'should increment counter for document creates': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+ db.save({"a":"1"});
+ var new_creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+
+ TEquals(creates + 1, new_creates, name);
+ },
+ 'should not increment counter for document creates when updating a doc': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {"_id":"test"};
+ db.save(doc);
+
+ var creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+ db.save(doc);
+ var new_creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+
+ TEquals(creates, new_creates, name);
+ },
+ 'should increment counter for document updates': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {"_id":"test"};
+ db.save(doc);
+
+ var updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+ db.save(doc);
+ var new_updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+
+ TEquals(updates + 1, new_updates, name);
+ },
+ 'should not increment counter for document updates when creating a document': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+ db.save({"a":"1"});
+ var new_updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+
+ TEquals(updates, new_updates, name);
+ },
+ 'should increment counter for document deletes': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {"_id":"test"};
+ db.save(doc);
+
+ var deletes = parseInt(CouchDB.requestStats("httpd", "document_deletes"));
+ db.deleteDoc(doc);
+ var new_deletes = parseInt(CouchDB.requestStats("httpd", "document_deletes"));
+
+ TEquals(deletes + 1, new_deletes, name);
+ },
+ 'should increment the copy counter': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {"_id":"test"};
+ db.save(doc);
+
+ var copies = parseInt(CouchDB.requestStats("httpd", "document_copies"));
+ CouchDB.request("COPY", "/test_suite_db/test", {
+ headers: {"Destination":"copy_of_test"}
+ });
+ var new_copies = parseInt(CouchDB.requestStats("httpd", "document_copies"));
+
+ TEquals(copies + 1, new_copies, name);
+ },
+ 'should increment the move counter': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {"_id":"test"};
+ db.save(doc);
+
+ var moves = parseInt(CouchDB.requestStats("httpd", "document_moves"));
+ CouchDB.request("MOVE", "/test_suite_db/test?rev=" + doc._rev, {
+ headers: {"Destination":"move_of_test"}
+ });
+ var new_moves = parseInt(CouchDB.requestStats("httpd", "document_moves"));
+
+ TEquals(moves + 1, new_moves, name);
+ },
+ 'should increase the bulk doc counter': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var bulks = parseInt(CouchDB.requestStats("httpd", "bulk_requests"));
+
+ var docs = makeDocs(5);
+ db.bulkSave(docs);
+
+ var new_bulks = parseInt(CouchDB.requestStats("httpd", "bulk_requests"));
+
+ TEquals(bulks + 1, new_bulks, name);
+ },
+ 'should increment counter for document creates using POST': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+ CouchDB.request("POST", "/test_suite_db", {body:'{"a":"1"}'});
+ var new_creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+
+ TEquals(creates + 1, new_creates, name);
+ },
+ 'should increment document create counter when adding attachment': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+ CouchDB.request("PUT", "/test_suite_db/bin_doc2/foo2.txt", {
+ body:"This is no base64 encoded text",
+ headers:{"Content-Type": "text/plain;charset=utf-8"}
+ });
+ var new_creates = parseInt(CouchDB.requestStats("httpd", "document_creates"));
+ TEquals(creates + 1, new_creates, name);
+ },
+ 'should increment document update counter when adding attachment to existing doc': function(name) {
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+
+ var doc = {_id:"test"};
+ db.save(doc);
+
+ var updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+ CouchDB.request("PUT", "/test_suite_db/test/foo2.txt?rev=" + doc._rev, {
+ body:"This is no base64 encoded text",
+ headers:{"Content-Type": "text/plain;charset=utf-8"}
+ });
+ var new_updates = parseInt(CouchDB.requestStats("httpd", "document_updates"));
+ TEquals(updates + 1, new_updates, name);
+ }
+
+ };
+ var response_codes_tests = {
+ 'should increment the response code counter': function(name) {
+ var db = new CouchDB("nonexistant_db");
+ db.deleteDb();
+
+ var not_founds = parseInt(CouchDB.requestStats("http_status_codes", "404"));
+ CouchDB.request("GET", "/nonexistant_db");
+ var new_not_founds = parseInt(CouchDB.requestStats("http_status_codes", "404"));
+
+ TEquals(not_founds + 1, new_not_founds, name);
+ },
+ 'should not increment respinse code counter for other response code': function(name) {
+ var not_founds = parseInt(CouchDB.requestStats("http_status_codes", "404"));
+ CouchDB.request("GET", "/");
+ var new_not_founds = parseInt(CouchDB.requestStats("http_status_codes", "404"));
+
+ TEquals(not_founds, new_not_founds, name);
+ }
+
+ };
+
+ var tests = [
+ open_databases_tests,
+ request_count_tests,
+ document_read_count_tests,
+ view_read_count_tests,
+ http_requests_by_method_tests,
+ document_write_count_tests,
+ response_codes_tests
+ ];
+
+ for(var testGroup in tests) {
+ for(var test in tests[testGroup]) {
+ tests[testGroup][test](test);
+ }
+ };
+
+ function createAndRequestView(db) {
+ var designDoc = {
+ _id:"_design/test", // turn off couch.js id escaping?
+ language: "javascript",
+ views: {
+ all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"},
+ }
+ };
+ db.save(designDoc);
+
+ db.view("test/all_docs_twice");
+ }
+
+ },
+
// Do some basic tests.
basics: function(debug) {
var result = JSON.parse(CouchDB.request("GET", "/").responseText);
@@ -2667,7 +3086,7 @@ var tests = {
}
})
})
- }
+ },
};
T(db.save(designDoc).ok);
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index 1ad5d14..769cd1c 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -58,6 +58,7 @@ source_files = \
couch_httpd_show.erl \
couch_httpd_view.erl \
couch_httpd_misc_handlers.erl \
+ couch_httpd_stats_handlers.erl \
couch_key_tree.erl \
couch_log.erl \
couch_os_process.erl \
@@ -65,6 +66,8 @@ source_files = \
couch_rep.erl \
couch_server.erl \
couch_server_sup.erl \
+ couch_stats_aggregator.erl \
+ couch_stats_collector.erl \
couch_stream.erl \
couch_task_status.erl \
couch_util.erl \
@@ -95,6 +98,7 @@ compiled_files = \
couch_httpd_show.beam \
couch_httpd_view.beam \
couch_httpd_misc_handlers.beam \
+ couch_httpd_stats_handlers.beam \
couch_key_tree.beam \
couch_log.beam \
couch_os_process.beam \
@@ -102,6 +106,8 @@ compiled_files = \
couch_rep.beam \
couch_server.beam \
couch_server_sup.beam \
+ couch_stats_aggregator.beam \
+ couch_stats_collector.beam \
couch_stream.beam \
couch_task_status.beam \
couch_util.beam \
@@ -150,7 +156,7 @@ couch.app: couch.app.tpl
# $(ERL) -noshell -run edoc_run files [\"$<\"]
%.beam: %.erl couch_db.hrl
- $(ERLC) $<
+ $(ERLC) -D$TEST $<
install-data-hook:
if test -f "$(DESTDIR)/$(couchprivlibdir)/couch_erl_driver"; then \
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index 7c37132..a9a130b 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -126,11 +126,8 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers) ->
mochiweb_headers:to_list(MochiReq:get(headers))
]),
- Method =
+ Method1 =
case MochiReq:get(method) of
- % alias HEAD to GET as mochiweb takes care of stripping the body
- 'HEAD' -> 'GET';
-
% already an atom
Meth when is_atom(Meth) -> Meth;
@@ -138,6 +135,15 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers) ->
% possible (if any module references the atom, then it's existing).
Meth -> couch_util:to_existing_atom(Meth)
end,
+
+ increment_method_stats(Method1),
+
+ % alias HEAD to GET as mochiweb takes care of stripping the body
+ Method = case Method1 of
+ 'HEAD' -> 'GET';
+ Other -> Other
+ end,
+
HttpReq = #httpd{
mochi_req = MochiReq,
method = Method,
@@ -166,8 +172,12 @@ handle_request(MochiReq, UrlHandlers, DbUrlHandlers) ->
RawUri,
Resp:get(code)
]),
+ couch_stats_collector:increment({httpd, request_count}),
{ok, Resp}.
+increment_method_stats(Method) ->
+ CounterName = list_to_atom(string:to_lower(atom_to_list(Method)) ++ "_requests"),
+ couch_stats_collector:increment({httpd, CounterName}).
special_test_authentication_handler(Req) ->
case header_value(Req, "WWW-Authenticate") of
@@ -319,6 +329,7 @@ basic_username_pw(Req) ->
start_chunked_response(#httpd{mochi_req=MochiReq}, Code, Headers) ->
+ couch_stats_collector:increment({http_status_codes, Code}),
{ok, MochiReq:respond({Code, Headers ++ server_header(), chunked})}.
send_chunk(Resp, Data) ->
@@ -326,6 +337,7 @@ send_chunk(Resp, Data) ->
{ok, Resp}.
send_response(#httpd{mochi_req=MochiReq}, Code, Headers, Body) ->
+ couch_stats_collector:increment({http_status_codes, Code}),
if Code >= 400 ->
?LOG_DEBUG("HTTPd ~p error response:~n ~s", [Code, Body]);
true -> ok
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 2cb4c40..c66c903 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -80,6 +80,7 @@ db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
DocId = couch_util:new_uuid(),
{ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=[]}, []),
+ couch_stats_collector:increment({httpd, document_creates}),
send_json(Req, 201, {[
{ok, true},
{id, DocId},
@@ -100,6 +101,7 @@ db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) ->
send_method_not_allowed(Req, "POST");
db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
+ couch_stats_collector:increment({httpd, bulk_requests}),
{JsonProps} = couch_httpd:json_body(Req),
DocsArray = proplists:get_value(<<"docs">>, JsonProps),
case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of
@@ -370,6 +372,7 @@ db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
couch_httpd:send_error(Req, 412, <<"missing_rev">>,
<<"Document rev/etag must be specified to delete">>);
RevToDelete ->
+ couch_stats_collector:increment({httpd, document_deletes}),
{ok, NewRev} = couch_db:delete_doc(Db, DocId, [RevToDelete]),
send_json(Req, 200, {[
{ok, true},
@@ -393,7 +396,8 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
[] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta
_ -> []
end,
- send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
+ couch_stats_collector:increment({httpd, document_reads}),
+ send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
end);
_ ->
{ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options),
@@ -416,6 +420,7 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
end,
"", Results),
send_chunk(Resp, "]"),
+ couch_stats_collector:increment({httpd, document_reads}),
end_json_response(Resp)
end;
@@ -460,8 +465,10 @@ db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
end,
case extract_header_rev(Req, ExplicitRev) of
missing_rev ->
+ couch_stats_collector:increment({httpd, document_creates}),
Revs = [];
Rev ->
+ couch_stats_collector:increment({httpd, document_updates}),
Revs = [Rev]
end,
{ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=Revs}, Options),
@@ -485,6 +492,7 @@ db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
% save new doc
{ok, NewTargetRev} = couch_db:update_doc(Db, Doc#doc{id=TargetDocId, revs=TargetRev}, []),
+ couch_stats_collector:increment({httpd, document_copies}),
send_json(Req, 201, [{"Etag", "\"" ++ binary_to_list(NewTargetRev) ++ "\""}], {[
{ok, true},
@@ -510,9 +518,9 @@ db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) ->
Doc#doc{id=TargetDocId, revs=TargetRev},
#doc{id=SourceDocId, revs=[SourceRev], deleted=true}
],
-
{ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
-
+ couch_stats_collector:increment({httpd, document_moves}),
+
DocResults = lists:zipwith(
fun(FDoc, NewRev) ->
{[{id, FDoc#doc.id}, {rev, NewRev}]}
@@ -602,8 +610,10 @@ db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts)
Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
missing_rev -> % make the new doc
+ couch_stats_collector:increment({httpd, document_creates}),
#doc{id=DocId};
Rev ->
+ couch_stats_collector:increment({httpd, document_updates}),
case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
{ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]};
{ok, [Error]} -> throw(Error)
diff --git a/src/couchdb/couch_httpd_stats_handlers.erl b/src/couchdb/couch_httpd_stats_handlers.erl
new file mode 100644
index 0000000..f307582
--- /dev/null
+++ b/src/couchdb/couch_httpd_stats_handlers.erl
@@ -0,0 +1,30 @@
+% 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_httpd_stats_handlers).
+-include("couch_db.hrl").
+
+-export([handle_stats_req/1]).
+-import(couch_httpd,
+ [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+ start_json_response/2,send_chunk/2,end_json_response/1,
+ start_chunked_response/3, send_error/4]).
+
+handle_stats_req(#httpd{method='GET', path_parts=PathParts}=Req) ->
+ [_Db, Module, Key] = PathParts,
+ Options = couch_httpd:qs(Req),
+
+ Count = couch_stats_aggregator:get({Module, Key}, Options),
+ Response = {[{Module, {[{Key, Count}]}}]},
+ send_json(Req, Response);
+handle_stats_req(Req) ->
+ send_method_not_allowed(Req, "GET").
diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl
index a6174a2..da43b6c 100644
--- a/src/couchdb/couch_httpd_view.erl
+++ b/src/couchdb/couch_httpd_view.erl
@@ -28,7 +28,7 @@ design_doc_view(Req, Db, Id, ViewName, Keys) ->
reduce = Reduce
} = QueryArgs = parse_view_query(Req, Keys),
DesignId = <<"_design/", Id/binary>>,
- case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of
+ Result = case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of
{ok, View} ->
output_map_view(Req, View, Db, QueryArgs, Keys);
{not_found, Reason} ->
@@ -45,7 +45,9 @@ design_doc_view(Req, Db, Id, ViewName, Keys) ->
_ ->
throw({not_found, Reason})
end
- end.
+ end,
+ couch_stats_collector:increment({httpd, view_reads}),
+ Result.
handle_view_req(#httpd{method='GET',path_parts=[_,_, Id, ViewName]}=Req, Db) ->
design_doc_view(Req, Db, Id, ViewName, nil);
@@ -60,7 +62,7 @@ handle_view_req(Req, _Db) ->
handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
QueryArgs = parse_view_query(Req),
-
+ couch_stats_collector:increment({httpd, temporary_view_reads}),
case couch_httpd:primary_header_value(Req, "content-type") of
undefined -> ok;
"application/json" -> ok;
diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl
index 69cfa36..f4f0e35 100644
--- a/src/couchdb/couch_server.erl
+++ b/src/couchdb/couch_server.erl
@@ -182,7 +182,9 @@ maybe_close_lru_db(#server{dbs_open=NumOpen, max_dbs_open=MaxOpen}=Server)
maybe_close_lru_db(#server{dbs_open=NumOpen}=Server) ->
% must free up the lru db.
case try_close_lru(now()) of
- ok -> {ok, Server#server{dbs_open=NumOpen-1}};
+ ok ->
+ couch_stats_collector:decrement({couch_db, open_databases}),
+ {ok, Server#server{dbs_open=NumOpen - 1}};
Error -> Error
end.
@@ -235,6 +237,7 @@ handle_call({open, DbName, Options}, _From, Server) ->
true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}),
true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}),
DbsOpen = Server2#server.dbs_open + 1,
+ couch_stats_collector:increment({couch_db, open_databases}),
{reply, {ok, MainPid},
Server2#server{dbs_open=DbsOpen}};
Error ->
@@ -270,6 +273,7 @@ handle_call({create, DbName, Options}, _From, Server) ->
true = ets:insert(couch_dbs_by_pid, {MainPid, DbName}),
true = ets:insert(couch_dbs_by_lru, {LruTime, DbName}),
DbsOpen = Server2#server.dbs_open + 1,
+ couch_stats_collector:increment({couch_db, open_databases}),
couch_db_update_notifier:notify({created, DbName}),
{reply, {ok, MainPid},
Server2#server{dbs_open=DbsOpen}};
@@ -299,6 +303,7 @@ handle_call({delete, DbName, _Options}, _From, Server) ->
true = ets:delete(couch_dbs_by_name, DbName),
true = ets:delete(couch_dbs_by_pid, Pid),
true = ets:delete(couch_dbs_by_lru, LruTime),
+ couch_stats_collector:decrement({couch_db, open_databases}),
Server#server{dbs_open=Server#server.dbs_open - 1}
end,
case file:delete(FullFilepath) of
@@ -328,6 +333,7 @@ handle_info({'EXIT', Pid, _Reason}, #server{dbs_open=DbsOpen}=Server) ->
true = ets:delete(couch_dbs_by_pid, Pid),
true = ets:delete(couch_dbs_by_name, DbName),
true = ets:delete(couch_dbs_by_lru, LruTime),
- {noreply, Server#server{dbs_open=DbsOpen-1}};
+ couch_stats_collector:decrement({couch_db, open_databases}),
+ {noreply, Server#server{dbs_open=DbsOpen - 1}};
handle_info(Info, _Server) ->
exit({unknown_message, Info}).
diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl
index 627c34a..57a848e 100644
--- a/src/couchdb/couch_server_sup.erl
+++ b/src/couchdb/couch_server_sup.erl
@@ -32,6 +32,7 @@ start_link(IniFiles) ->
end.
restart_core_server() ->
+ catch couch_stats_collector:stop(), % TODO: move to more appropriate place
supervisor:terminate_child(couch_primary_services, couch_server),
supervisor:restart_child(couch_primary_services, couch_server).
diff --git a/src/couchdb/couch_stats_aggregator.erl b/src/couchdb/couch_stats_aggregator.erl
new file mode 100644
index 0000000..f1a17ff
--- /dev/null
+++ b/src/couchdb/couch_stats_aggregator.erl
@@ -0,0 +1,189 @@
+% 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_aggregator).
+
+-define(TEST, true).
+-ifdef(TEST).
+ -include_lib("eunit/include/eunit.hrl").
+-endif.
+
+-behaviour(gen_server).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+
+-export([start/0, stop/0, get/1, get/2, time_passed/1]).
+
+-record(state, {}).
+
+-define(COLLECTOR, couch_stats_collector).
+
+% PUBLIC API
+
+start() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+ gen_server:call(?MODULE, stop).
+
+get(Key) ->
+ get(Key, []).
+get(Key, Options) ->
+ gen_server:call(?MODULE, {get, Key, Options}).
+
+time_passed(Time) ->
+ gen_server:call(?MODULE, {time_passed, Time}).
+
+
+% GEN_SERVER
+
+init(_) ->
+ ets:new(?MODULE, [named_table, set, protected]),
+ lists:map(fun(Time) -> init_counter(Time) end, [1, 5, 15]),
+ {ok, #state{}}.
+
+handle_call({get, {Module, Key}, Options}, _, State) ->
+ Value =
+ case a2b(Key) of
+ <<"average_",CollectorKey/binary>> ->
+ get_average(Module, CollectorKey, Options);
+ _ ->
+ ?COLLECTOR:get({b2a(Module), b2a(Key)})
+ end,
+
+ {reply, integer_to_binary(Value), State};
+
+handle_call(stop, _, State) ->
+ {stop, normal, stopped, State};
+
+% update all counters that match `Time` = int()
+handle_call({time_passed, Time}, _, State) ->
+ lists:foreach(fun(Counter) ->
+ {{Module, Key, _}, {_, PreviousCount}} = Counter,
+ CurrentCount = ?COLLECTOR:get({Module, get_collector_key(Key)}),
+ ets:insert(?MODULE, {{Module, Key, Time}, {PreviousCount, CurrentCount}})
+ end, ets:tab2list(?MODULE)),
+ {reply, ok, State}.
+
+% PRIVATE API
+
+get_average_counters() ->
+ [{httpd, <<"previous_request_count">>}].
+
+get_average(Module, Key, Options) ->
+ Time = proplists:get_value("timeframe", Options, 1) * 60, % default to 1 minute, in seconds
+ case ets:lookup(?MODULE, {Module, <<"previous_",Key/binary>>, Time}) of
+ [] -> 0;
+ [{_, {PreviousCounter, CurrentCounter}}] ->
+ round((CurrentCounter - PreviousCounter) / Time)
+ end.
+
+get_collector_key(Key) ->
+ <<"previous_", CollectorKey/binary>> = a2b(Key),
+ b2a(CollectorKey).
+
+init_counter(Time) ->
+ Seconds = Time * 60,
+ lists:map(
+ fun({Module, Key}) ->
+ start_timer(Seconds, fun() -> ?MODULE:time_passed(Seconds) end),
+ ets:insert(?MODULE, {{Module, Key, Time}, {0, 0}})
+ end, get_average_counters()).
+
+
+start_timer(Time, Fun) ->
+ spawn(fun() -> timer(Time * 1000, Fun) end).
+
+timer(Time, Fun) ->
+ receive
+ cancel -> void
+ after
+ Time -> Fun(),
+ timer(Time, Fun)
+ end.
+
+% UTILS
+
+integer_to_binary(Integer) ->
+ list_to_binary(integer_to_list(Integer)).
+
+b2a(Binary) when is_atom(Binary)->
+ Binary;
+b2a(Binary) ->
+ list_to_atom(binary_to_list(Binary)).
+
+a2b(Atom) when is_binary(Atom) ->
+ Atom;
+a2b(Atom) ->
+ list_to_binary(atom_to_list(Atom)).
+
+% Unused gen_server behaviour API functions that we need to declare.
+
+%% @doc Unused
+handle_cast(foo, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%% @doc Unused
+terminate(_Reason, _State) -> ok.
+
+%% @doc Unused
+code_change(_OldVersion, State, _Extra) -> {ok, State}.
+
+% TESTS
+
+test_helper(Fun) ->
+ catch ?MODULE:stop(),
+ ?MODULE:start(),
+ ?COLLECTOR:start(),
+
+ Fun(),
+
+ ?MODULE:stop(),
+ ?COLLECTOR:stop().
+
+should_return_value_from_collector_test() ->
+ test_helper(fun() ->
+ ?assertEqual(<<"0">>, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_handle_multiple_key_value_pairs_test() ->
+ test_helper(fun() ->
+ ?COLLECTOR:increment({couch_db, open_databases}),
+ ?assertEqual(<<"1">>, ?MODULE:get({couch_db, open_databases})),
+ ?assertEqual(<<"0">>, ?MODULE:get({couch_db, request_count}))
+ end).
+
+should_return_the_average_over_the_last_minute_test() ->
+ test_helper(fun() ->
+ lists:map(fun(_) ->
+ ?COLLECTOR:increment({httpd, request_count})
+ end, lists:seq(1, 200)),
+
+ ?MODULE:time_passed(60), % seconds
+ ?assertEqual(<<"3">>, ?MODULE:get({httpd, average_request_count}))
+ end).
+
+should_return_the_average_over_the_last_five_minutes_test() ->
+ test_helper(fun() ->
+ lists:map(fun(_) ->
+ ?COLLECTOR:increment({httpd, request_count})
+ end, lists:seq(1, 2000)),
+
+ ?MODULE:time_passed(300), % seconds
+ Result = ?MODULE:get({httpd, average_request_count}, [{"timeframe", list_to_integer("5")}]),
+ ?assertEqual(<<"7">>, Result)
+ end).
diff --git a/src/couchdb/couch_stats_collector.erl b/src/couchdb/couch_stats_collector.erl
new file mode 100644
index 0000000..13fb168
--- /dev/null
+++ b/src/couchdb/couch_stats_collector.erl
@@ -0,0 +1,155 @@
+% 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_collector).
+
+-define(TEST, true).
+-ifdef(TEST).
+ -include_lib("eunit/include/eunit.hrl").
+-endif.
+
+-behaviour(gen_server).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+
+-export([start/0, stop/0, get/1, increment/1, decrement/1]).
+
+-record(state, {}).
+
+
+% PUBLIC API
+
+start() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+ gen_server:call(?MODULE, stop).
+
+get(Key) ->
+ gen_server:call(?MODULE, {get, Key}).
+
+increment({Module, Key}) when is_integer(Key) ->
+ gen_server:call(?MODULE, {increment, {Module, list_to_atom(integer_to_list(Key))}});
+increment(Key) ->
+ gen_server:call(?MODULE, {increment, Key}).
+
+decrement(Key) ->
+ gen_server:call(?MODULE, {decrement, Key}).
+
+% GEN_SERVER
+
+init(_) ->
+ ets:new(?MODULE, [named_table, set, protected]),
+ {ok, #state{}}.
+
+handle_call({get, Key}, _, State) ->
+ Result = case ets:lookup(?MODULE, Key) of
+ [] -> 0;
+ [{_,Result1}] -> Result1
+ end,
+ {reply, Result, State};
+
+handle_call({increment, Key}, _, State) ->
+ case catch ets:update_counter(?MODULE, Key, 1) of
+ {'EXIT', {badarg, _}} -> ets:insert(?MODULE, {Key, 1});
+ _ -> ok
+ end,
+ {reply, ok, State};
+
+handle_call({decrement, Key}, _, State) ->
+ case catch ets:update_counter(?MODULE, Key, -1) of
+ {'EXIT', {badarg, _}} -> ets:insert(?MODULE, {Key, -1});
+ _ -> ok
+ end,
+ {reply, ok, State};
+
+% handle_call(reset, _, State) ->
+% ets:insert(?MODULE, {Key, 0}),
+% {reply, ok, 0};
+
+handle_call(stop, _, State) ->
+ {stop, normal, stopped, State}.
+
+
+% Unused gen_server behaviour API functions that we need to declare.
+
+%% @doc Unused
+handle_cast(foo, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%% @doc Unused
+terminate(_Reason, _State) -> ok.
+
+%% @doc Unused
+code_change(_OldVersion, State, _Extra) -> {ok, State}.
+
+% TESTS
+
+test_helper(Fun) ->
+ catch ?MODULE:stop(),
+ ?MODULE:start(),
+
+ Fun(),
+
+ ?MODULE:stop().
+
+should_return_value_from_store_test() ->
+ test_helper(fun() ->
+ ?assertEqual(0, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_increment_value_test() ->
+ test_helper(fun() ->
+ ?assert(?MODULE:increment({couch_db, open_databases}) =:= ok),
+ ?assertEqual(1, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_decrement_value_test() ->
+ test_helper(fun() ->
+ ?assert(?MODULE:decrement({couch_db, open_databases}) =:= ok),
+ ?assertEqual(-1, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_increment_and_decrement_value_test() ->
+ test_helper(fun() ->
+ ?assert(?MODULE:increment({couch_db, open_databases}) =:= ok),
+ ?assert(?MODULE:decrement({couch_db, open_databases}) =:= ok),
+ ?assertEqual(0, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_reset_counter_value_test() ->
+ test_helper(fun() ->
+ ?assert(?MODULE:increment({couch_db, open_databases}) =:= ok),
+ ?MODULE:stop(),
+ ?MODULE:start(),
+ ?assertEqual(0, ?MODULE:get({couch_db, open_databases}))
+ end).
+
+should_handle_multiple_key_value_pairs_test() ->
+ test_helper(fun() ->
+ ?MODULE:increment({couch_db, open_databases}),
+ ?assertEqual(1, ?MODULE:get({couch_db, open_databases})),
+ ?assertEqual(0, ?MODULE:get({couch_db, request_count}))
+ end).
+
+should_restart_module_should_create_new_pid_test() ->
+ test_helper(fun() ->
+ OldPid = whereis(?MODULE),
+ ?MODULE:stop(),
+ ?MODULE:start(),
+ ?assertNot(whereis(?MODULE) =:= OldPid)
+ end).