diff --git a/CHANGELOG b/CHANGELOG index a4499bb..add66c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,12 @@ This document describes changes between each past release. - No changes yet. +0.19.3 (2016-02-12) +------------------- + +- Add a way to log the loop-client versions in use. (#362) + + 0.19.2 (2016-01-07) ------------------- diff --git a/config/test.json b/config/test.json index 2700bc1..e511fe9 100644 --- a/config/test.json +++ b/config/test.json @@ -71,6 +71,7 @@ "hekaMetrics": { "activated": false, "debug": false, - "level": "DEBUG" + "level": "DEBUG", + "fmt": "pretty" } } diff --git a/loop/auth.js b/loop/auth.js index a532020..74db1e8 100644 --- a/loop/auth.js +++ b/loop/auth.js @@ -76,7 +76,7 @@ module.exports = function(conf, logError, storage, statsdClient) { var hawkIdHmac = hmac(tokenId, conf.get("hawkIdSecret")); storage.setHawkSession(hawkIdHmac, authKey, function(err) { if (statsdClient && err === null) { - statsdClient.count('loop.activated-users', 1); + statsdClient.increment('loop.activated-users'); } callback(err); }); diff --git a/loop/index.js b/loop/index.js index 957149d..1e31526 100644 --- a/loop/index.js +++ b/loop/index.js @@ -24,7 +24,7 @@ var express = require('express'); var bodyParser = require('body-parser'); var raven = require('raven'); var cors = require('cors'); -var StatsdClient = require('statsd-node').client; +var StatsdClient = require('node-statsd'); var PubSub = require('./pubsub'); @@ -142,7 +142,7 @@ if (conf.get("fxaOAuth").activated !== false) { var rooms = require("./routes/rooms"); rooms(apiRouter, conf, logError, storage, filestorage, auth, validators, tokBox, - simplePush, notifications); + simplePush, notifications, statsdClient); var session = require("./routes/session"); session(apiRouter, conf, storage, auth); diff --git a/loop/routes/home.js b/loop/routes/home.js index dda8dda..0604eb0 100644 --- a/loop/routes/home.js +++ b/loop/routes/home.js @@ -81,14 +81,13 @@ module.exports = function(app, conf, logError, storage, tokBox, statsdClient) { var pushStatus = (!error && statusCodes.every(isSuccess)); returnStatus(storageStatus, tokboxError, pushStatus, verifierStatus); if (statsdClient !== undefined) { - statsdClient.count('loop.simplepush.call', 1); - var counter_push_status_counter; + var tag; if (pushStatus) { - counter_push_status_counter = 'loop.simplepush.call.heartbeat.success'; + tag = 'success'; } else { - counter_push_status_counter = 'loop.simplepush.call.heartbeat.failures'; + tag = 'failure'; } - statsdClient.count(counter_push_status_counter, 1); + statsdClient.increment('loop.simplepush.call.heartbeat', 1, [tag]); } }); }); diff --git a/loop/routes/rooms.js b/loop/routes/rooms.js index fc81791..d414f43 100644 --- a/loop/routes/rooms.js +++ b/loop/routes/rooms.js @@ -12,6 +12,7 @@ var decrypt = require('../encrypt').decrypt; var encrypt = require('../encrypt').encrypt; var errors = require('../errno.json'); var getUserAccount = require('../utils').getUserAccount; +var hekaLogger = require('../logger').hekaLogger; var sendError = require('../utils').sendError; var tokenlib = require('../tokenlib'); var time = require('../utils').time; @@ -20,7 +21,7 @@ var hmac = require('../hmac'); module.exports = function (apiRouter, conf, logError, storage, filestorage, auth, - validators, tokBox, simplePush, notifications) { + validators, tokBox, simplePush, notifications, statsdClient) { var roomsConf = conf.get("rooms"); @@ -331,14 +332,14 @@ module.exports = function (apiRouter, conf, logError, storage, filestorage, auth /** * Do an action on a room. * - * Actions are "join", "leave", "refresh". + * Actions are "join", "leave", "refresh", "status", "logDomain". **/ apiRouter.post('/rooms/:token', validators.validateRoomToken, auth.authenticateWithHawkOrToken, function(req, res) { var participantHmac = req.hawkIdHmac || req.participantTokenHmac; var roomOwnerHmac = req.roomStorageData.roomOwnerHmac; - var ROOM_ACTIONS = ["join", "refresh", "status", "leave"]; + var ROOM_ACTIONS = ["join", "refresh", "status", "leave", "logDomain"]; var action = req.body.action; var code; @@ -529,6 +530,51 @@ module.exports = function (apiRouter, conf, logError, storage, filestorage, auth }); }); }); + }, + handleLogDomain: function(req, res) { + // Log whitelisted domain count in statsd. + validators.requireParams('domains')( + req, res, function() { + var domains = req.body.domains; + var validDomains = domains.filter(function(domain) { + return domain.hasOwnProperty("domain") && domain.hasOwnProperty("count"); + }); + + if (validDomains.length !== domains.length) { + sendError(res, 400, errors.INVALID_PARAMETERS, + "Domains must be a list of objects with both a " + + "``domain`` and ``count`` property."); + return; + } + + storage.getRoomParticipant(req.token, participantHmac, + function(err, participant) { + if (res.serverError(err)) return; + if (participant === null) { + sendError(res, 400, errors.NOT_ROOM_PARTICIPANT, + "Can't update status for a room you aren't in."); + return; + } + + if (conf.get('hekaMetrics').activated === true) { + req.body.domains.forEach(function(domain) { + var line = { + domain: domain.domain, + count: domain.count + }; + hekaLogger.info('domains.counters', line); + + if (statsdClient !== undefined) { + statsdClient.increment( + "loop.room.shared_domains", + domain.count); + } + + }); + } + res.status(204).json(); + }); + }); } }; @@ -540,6 +586,8 @@ module.exports = function (apiRouter, conf, logError, storage, filestorage, auth handlers.handleUpdateStatus(req, res); } else if (action === "leave") { handlers.handleLeave(req, res); + } else if (action === "logDomain") { + handlers.handleLogDomain(req, res); } }); diff --git a/loop/simplepush.js b/loop/simplepush.js index bbbf6b9..c040081 100644 --- a/loop/simplepush.js +++ b/loop/simplepush.js @@ -26,23 +26,17 @@ SimplePush.prototype = { var self = this; urls.forEach(function(simplePushUrl) { - if (self.statsdClient !== undefined) { - self.statsdClient.count("loop.simplepush.call", 1); - self.statsdClient.count("loop.simplepush.call." + reason, 1); - } request.put({ url: simplePushUrl, form: { version: version } }, function(err) { + var status = 'success'; + if (err) { + self.logError(err); + status = 'failure'; + } if (self.statsdClient !== undefined) { - if (err) { - self.logError(err); - self.statsdClient.count("loop.simplepush.call.failures", 1); - self.statsdClient.count("loop.simplepush.call." + reason + ".failures", 1); - } else { - self.statsdClient.count("loop.simplepush.call.success", 1); - self.statsdClient.count("loop.simplepush.call." + reason + ".success", 1); - } + self.statsdClient.increment("loop.simplepush.call", 1, [reason, status]); } }); }); diff --git a/loop/tokbox.js b/loop/tokbox.js index 1153014..9a6bad0 100644 --- a/loop/tokbox.js +++ b/loop/tokbox.js @@ -72,7 +72,7 @@ TokBox.prototype = { return; } if (self.statsdClient !== undefined) { - self.statsdClient.count("loop.tokbox.createSession.count", 1); + self.statsdClient.increment("loop.tokbox.createSession.count"); self.statsdClient.timing( 'loop.tokbox.createSession', Date.now() - startTime diff --git a/package.json b/package.json index 7fb66e6..8eec4a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mozilla-loop-server", "description": "The Mozilla Loop (WebRTC App) server", - "version": "0.19.2", + "version": "0.20.0-dev", "author": "Mozilla (https://mozilla.org/)", "homepage": "https://github.com/mozilla-services/loop-server/", "bugs": "https://bugzilla.mozilla.org/enter_bug.cgi?product=Loop&component=Server", @@ -37,7 +37,7 @@ "redis": "0.12.1", "request": "2.45.0", "sodium": "1.0.13", - "statsd-node": "0.2.3", + "node-statsd": "0.1.1", "strftime": "0.8.2", "urlsafe-base64": "1.0.0", "ws": "1.0.1", diff --git a/test/functional_test.js b/test/functional_test.js index bb3b0d9..bd52ca6 100644 --- a/test/functional_test.js +++ b/test/functional_test.js @@ -393,7 +393,7 @@ function runOnPrefix(apiPrefix) { } }); - sandbox.stub(statsdClient, "count"); + sandbox.stub(statsdClient, "increment"); supertest(app) .get(apiPrefix + '/__heartbeat__') @@ -406,10 +406,8 @@ function runOnPrefix(apiPrefix) { 'push': false, 'fxaVerifier': true }); - assert.calledTwice(statsdClient.count); - assert.calledWithExactly(statsdClient.count, "loop.simplepush.call", 1); - assert.calledWithExactly(statsdClient.count, - "loop.simplepush.call.heartbeat.failures", 1); + assert.calledOnce(statsdClient.increment); + assert.calledWithExactly(statsdClient.increment, "loop.simplepush.call.heartbeat", 1, ['failure']); done(); }); }); @@ -464,7 +462,7 @@ function runOnPrefix(apiPrefix) { callback(null, {statusCode: 200}); }); - sandbox.stub(statsdClient, "count"); + sandbox.stub(statsdClient, "increment"); supertest(app) .get(apiPrefix + '/__heartbeat__') @@ -477,11 +475,8 @@ function runOnPrefix(apiPrefix) { 'push': true, 'fxaVerifier': true }); - assert.calledTwice(statsdClient.count); - assert.calledWithExactly(statsdClient.count, "loop.simplepush.call", 1); - assert.calledWithExactly(statsdClient.count, - "loop.simplepush.call.heartbeat.success", 1); - + assert.calledOnce(statsdClient.increment); + assert.calledWithExactly(statsdClient.increment, "loop.simplepush.call.heartbeat", 1, ['success']); done(); }); }); @@ -778,7 +773,7 @@ function runOnPrefix(apiPrefix) { }); it("should count new users if the session is created", function(done) { - sandbox.stub(statsdClient, "count"); + sandbox.stub(statsdClient, "increment"); supertest(app) .post(apiPrefix + '/registration') .type('json') @@ -786,25 +781,23 @@ function runOnPrefix(apiPrefix) { 'simplePushURL': pushURL }).expect(200).end(function(err) { if (err) throw err; - assert.calledOnce(statsdClient.count); + assert.calledOnce(statsdClient.increment); assert.calledWithExactly( - statsdClient.count, - "loop.activated-users", - 1 - ); + statsdClient.increment, + "loop.activated-users"); done(); }); }); it("shouldn't count a new user if the session already exists", function(done) { - sandbox.stub(statsdClient, "count"); + sandbox.stub(statsdClient, "increment"); jsonReq .send({ 'simple_push_url': pushURL }).expect(200).end(function(err) { if (err) throw err; - assert.notCalled(statsdClient.count); + assert.notCalled(statsdClient.increment); done(); }); }); diff --git a/test/fxa_oauth_tests.js b/test/fxa_oauth_tests.js index 07c12dc..76f943d 100644 --- a/test/fxa_oauth_tests.js +++ b/test/fxa_oauth_tests.js @@ -116,17 +116,16 @@ describe('/fxa-oauth', function () { }); it("should count new users if the session is created", function(done) { - sandbox.stub(statsdClient, "count"); + sandbox.stub(statsdClient, "increment"); supertest(app) .post(apiPrefix + '/fxa-oauth/params') .type('json') .send({}).expect(200).end(function(err) { if (err) throw err; - assert.calledOnce(statsdClient.count); + assert.calledOnce(statsdClient.increment); assert.calledWithExactly( - statsdClient.count, - "loop.activated-users", - 1 + statsdClient.increment, + "loop.activated-users" ); done(); }); diff --git a/test/rooms_test.js b/test/rooms_test.js index ccdc4e0..57bcc7f 100644 --- a/test/rooms_test.js +++ b/test/rooms_test.js @@ -8,6 +8,7 @@ var expect = require("chai").expect; var addHawk = require("superagent-hawk"); var supertest = addHawk(require("supertest")); var sinon = require("sinon"); +var assert = sinon.assert; var expectFormattedError = require("./support").expectFormattedError; var errors = require("../loop/errno.json"); var Token = require("express-hawkauth").Token; @@ -23,6 +24,7 @@ var auth = loop.auth; var validators = loop.validators; var apiRouter = loop.apiRouter; var conf = loop.conf; +var statsdClient = loop.statsdClient; var storage = loop.storage; var filestorage = loop.filestorage; var tokBox = loop.tokBox; @@ -1287,7 +1289,7 @@ describe("/rooms", function() { .end(function(err, res) { if (err) throw err; expectFormattedError(res, 400, errors.MISSING_PARAMETERS, - "action should be one of join, refresh, status, leave"); + "action should be one of join, refresh, status, leave, logDomain"); done(); }); }); @@ -2006,6 +2008,163 @@ describe("/rooms", function() { }); }); }); + + describe("Handle 'logDomain'", function() { + var postReq; + var roomToken; + + beforeEach(function(done) { + supertest(app) + .post('/rooms') + .type('json') + .hawk(hawkCredentials) + .send({ + roomOwner: "Alexis", + roomName: "UX discussion", + maxSize: "3", + expiresIn: "10" + }) + .expect(201) + .end(function(err, postRes) { + if (err) throw err; + roomToken = postRes.body.roomToken; + postReq = supertest(app) + .post('/rooms/' + roomToken) + .type('json'); + done(); + }); + }); + + it("should return empty body if successful.", function(done) { + joinRoom(hawkCredentials, roomToken).end(function(err) { + if (err) throw err; + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}]}) + .expect(204) + .hawk(hawkCredentials) + .end(function(err) { + if (err) throw err; + done(); + }); + }); + }); + + it("should count total domain metrics in statsd.", function(done) { + joinRoom(hawkCredentials, roomToken).end(function(err) { + if (err) throw err; + sandbox.stub(statsdClient, "increment"); + + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}, + {domain: "ebay.fr", count: 2}]}) + .expect(204) + .hawk(hawkCredentials) + .end(function(err) { + if (err) throw err; + assert.calledTwice(statsdClient.increment); + assert.calledWithExactly(statsdClient.increment, + "loop.room.shared_domains", 4); + assert.calledWithExactly(statsdClient.increment, + "loop.room.shared_domains", 2); + done(); + }); + }); + }); + + it("should reject if user had not joined the room", function(done) { + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}]}) + .expect(400) + .hawk(hawkCredentials) + .end(function(err, res) { + if (err) throw err; + expectFormattedError( + res, 400, errors.NOT_ROOM_PARTICIPANT, + "Can't update status for a room you aren't in."); + done(); + }); + }); + + it("should return a 503 in case of storage error.", function(done) { + sandbox.stub(storage, "getRoomParticipant", + function(roomTokens, participantHmac, callback) { + callback("error"); + }); + + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}]}) + .expect(503) + .hawk(hawkCredentials) + .end(function(err, res) { + if (err) throw err; + expectFormattedError(res, 503, errors.BACKEND, + "Service Unavailable"); + done(); + }); + }); + + it("should log domain count data if successful.", function(done) { + joinRoom(hawkCredentials, roomToken).end(function(err) { + if (err) throw err; + logs = []; // Reset hekaLogs. + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}, + {domain: "ebay.fr", count: 2}]}) + .expect(204) + .hawk(hawkCredentials) + .end(function(err) { + if (err) throw err; + expect(logs).to.length(3); + expect(logs[0]).to.eql({op: 'domains.counters', + domain: 'mozilla.org', + count: 4}); + expect(logs[1]).to.eql({op: 'domains.counters', + domain: 'ebay.fr', + count: 2}); + done(); + }); + }); + }); + + it("should reject if body is incomplete (domain property missing).", function(done) { + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org"}]}) + .expect(400) + .hawk(hawkCredentials) + .end(function(err, res) { + if (err) throw err; + expectFormattedError( + res, 400, errors.INVALID_PARAMETERS, + "Domains must be a list of objects with both a " + + "``domain`` and ``count`` property." + ); + done(); + }); + }); + + it("should reject if body is incomplete (count property missing).", function(done) { + postReq + .send({action: "logDomain", + domains: [{domain: "mozilla.org", count: 4}, {count: 2}]}) + .expect(400) + .hawk(hawkCredentials) + .end(function(err, res) { + if (err) throw err; + expectFormattedError( + res, 400, errors.INVALID_PARAMETERS, + "Domains must be a list of objects with both a " + + "``domain`` and ``count`` property." + ); + done(); + }); + }); + }); }); describe("Using Token", function() { @@ -2038,7 +2197,7 @@ describe("/rooms", function() { .end(function(err, res) { if (err) throw err; expectFormattedError(res, 400, errors.MISSING_PARAMETERS, - "action should be one of join, refresh, status, leave"); + "action should be one of join, refresh, status, leave, logDomain"); done(); }); }); diff --git a/test/simplepush_test.js b/test/simplepush_test.js index 9ae7ca7..8cf6aec 100644 --- a/test/simplepush_test.js +++ b/test/simplepush_test.js @@ -55,17 +55,14 @@ describe("simplePush object", function() { }); it("should notify using the statsd client if present", function() { - var statsdClient = { count: function() {} }; - var statsdSpy = sandbox.spy(statsdClient, "count"); + var statsdClient = { increment: function() {} }; + var statsdSpy = sandbox.spy(statsdClient, "increment"); var simplePush = new SimplePush(statsdClient); simplePush.notify("reason", "url1", 12345); - assert.callCount(statsdSpy, 4); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.reason", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.success", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.reason.success", 1); + assert.calledOnce(statsdSpy); + assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1, ["reason", "success"]); }); it("should notify using the statsd client for errors if present", function() { @@ -78,16 +75,13 @@ describe("simplePush object", function() { callback("error"); }); - var statsdClient = { count: function() {} }; - var statsdSpy = sandbox.spy(statsdClient, "count"); + var statsdClient = { increment: function() {} }; + var statsdSpy = sandbox.spy(statsdClient, "increment"); var simplePush = new SimplePush(statsdClient); simplePush.notify("reason", "url1", 12345); - assert.callCount(statsdSpy, 4); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.reason", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.failures", 1); - assert.calledWithExactly(statsdSpy, "loop.simplepush.call.reason.failures", 1); + assert.calledOnce(statsdSpy); + assert.calledWithExactly(statsdSpy, "loop.simplepush.call", 1, ["reason", "failure"]); }); }); diff --git a/test/tokbox_test.js b/test/tokbox_test.js index e3e3480..5172658 100644 --- a/test/tokbox_test.js +++ b/test/tokbox_test.js @@ -74,9 +74,9 @@ describe("TokBox", function() { var tokBox, openTokSpy, statsdClient, statsdTimer, statsdCount; beforeEach(function() { - statsdClient = { timing: function() {}, count: function() {} }; + statsdClient = { timing: function() {}, increment: function() {} }; statsdTimer = sandbox.spy(statsdClient, "timing"); - statsdCount = sandbox.spy(statsdClient, "count"); + statsdCount = sandbox.spy(statsdClient, "increment"); openTokSpy = sandbox.spy(loopTokbox, "OpenTok"); openTokSpy.withArgs(