From 002c4c2cce60001c946d80d8aad92b3d0611826f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 15 Nov 2018 10:23:42 +0000 Subject: [PATCH] Allow custom logger overrides This change adds the ability to override ShareDB's logging behaviour. By default, ShareDB will still log to `console`. However, this default can be overridden with custom methods on both the backend and in the client. ## Supported methods The ShareDB logger only supports the following methods: - `info` - `warn` - `error` Any method that is not overridden will default to `console`. ## Backend The backend methods can be overridden when instantiating `Backend`: ```javascript var share = new Backend({ logger: { info: () => {}, // Silence info warn: () => alerts.warn(arguments), // Forward warnings error: () => alerts.critical(arguments) // Map errors to critical } }); ``` ## Client Client methods can also be overridden: ```javascript var ShareDB = require('sharedb/lib/client'); ShareDB.logger.setMethods({ info: () => {}, // etc. }); ``` --- README.md | 43 +++++++++++++++++++++++++++++++++++++ lib/agent.js | 9 ++++---- lib/backend.js | 3 +++ lib/client/connection.js | 11 +++++----- lib/client/doc.js | 3 ++- lib/client/index.js | 1 + lib/logger/index.js | 3 +++ lib/logger/logger.js | 21 ++++++++++++++++++ lib/stream-socket.js | 3 ++- test/logger.js | 46 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 lib/logger/index.js create mode 100644 lib/logger/logger.js create mode 100644 test/logger.js diff --git a/README.md b/README.md index e8c86d3d..0a12c5a1 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,28 @@ share.addProjection('users_limited', 'users', { name:true, profileUrl:true }); Note that only the [JSON0 OT type](https://github.com/ottypes/json0) is supported for projections. +### Logging + +By default, ShareDB logs to `console`. This can be overridden if you wish to silence logs, or to log to your own logging driver or alert service. + +Methods can be overridden by passing a [`console`-like object](https://developer.mozilla.org/en-US/docs/Web/API/console) to `Backend`: + +```javascript +var share = new Backend({ + logger: { + info: () => {}, // Silence info + warn: () => alerts.warn(arguments), // Forward warnings to alerting service + error: () => alerts.critical(arguments) // Remap errors to critical alerts + } +}); +``` + +ShareDB only supports the following logger methods: + + - `info` + - `warn` + - `error` + ### Shutdown `share.close(callback)` @@ -358,6 +380,27 @@ after a sequence of diffs are handled. `query.on('extra', function() {...}))` (Only fires on subscription queries) `query.extra` changed. +### Logging + +By default, ShareDB logs to `console`. This can be overridden if you wish to silence logs, or to log to your own logging driver or alert service. + +Methods can be overridden by passing a [`console`-like object](https://developer.mozilla.org/en-US/docs/Web/API/console) to `logger.setMethods` + +```javascript +var ShareDB = require('sharedb/lib/client'); +ShareDB.logger.setMethods({ + info: () => {}, // Silence info + warn: () => alerts.warn(arguments), // Forward warnings to alerting service + error: () => alerts.critical(arguments) // Remap errors to critical alerts +}); +``` + +ShareDB only supports the following logger methods: + + - `info` + - `warn` + - `error` + ## Error codes diff --git a/lib/agent.js b/lib/agent.js index 3ef55836..0bd0b496 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,6 +1,7 @@ var hat = require('hat'); var util = require('./util'); var types = require('./types'); +var logger = require('./logger'); /** * Agent deserializes the wire protocol messages received from the stream and @@ -47,7 +48,7 @@ module.exports = Agent; // Close the agent with the client. Agent.prototype.close = function(err) { if (err) { - console.warn('Agent closed due to error', this.clientId, err.stack || err); + logger.warn('Agent closed due to error', this.clientId, err.stack || err); } if (this.closed) return; // This will end the writable stream and emit 'finish' @@ -95,7 +96,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { // Log then silently ignore errors in a subscription stream, since these // may not be the client's fault, and they were not the result of a // direct request by the client - console.error('Doc subscription stream error', collection, id, data.error); + logger.error('Doc subscription stream error', collection, id, data.error); return; } if (agent._isOwnOp(collection, data)) return; @@ -137,7 +138,7 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query // Log then silently ignore errors in a subscription stream, since these // may not be the client's fault, and they were not the result of a // direct request by the client - console.error('Query subscription stream error', collection, query, err); + logger.error('Query subscription stream error', collection, query, err); }; emitter.onOp = function(op) { @@ -195,7 +196,7 @@ Agent.prototype._reply = function(request, err, message) { }; } else { if (err.stack) { - console.warn(err.stack); + logger.warn(err.stack); } request.error = { code: err.code, diff --git a/lib/backend.js b/lib/backend.js index e766202e..4c0d624d 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -2,6 +2,7 @@ var async = require('async'); var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); +var logger = require('./logger'); var MemoryDB = require('./db/memory'); var NoOpMilestoneDB = require('./milestone-db/no-op'); var MemoryPubSub = require('./pubsub/memory'); @@ -40,6 +41,8 @@ function Backend(options) { this.agentsCount = 0; this.remoteAgentsCount = 0; + logger.setMethods(options.logger); + // The below shims are for backwards compatibility. These options will be // removed in a future major version if (!options.disableDocAction) { diff --git a/lib/client/connection.js b/lib/client/connection.js index da51948b..9a2a1fef 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -5,6 +5,7 @@ var emitter = require('../emitter'); var ShareDBError = require('../error'); var types = require('../types'); var util = require('../util'); +var logger = require('../logger'); function connectionState(socket) { if (socket.readyState === 0 || socket.readyState === 1) return 'connecting'; @@ -116,11 +117,11 @@ Connection.prototype.bindToSocket = function(socket) { var data = (typeof event.data === 'string') ? JSON.parse(event.data) : event.data; } catch (err) { - console.warn('Failed to parse message', event); + logger.warn('Failed to parse message', event); return; } - if (connection.debug) console.log('RECV', JSON.stringify(data)); + if (connection.debug) logger.info('RECV', JSON.stringify(data)); var request = {data: data}; connection.emit('receive', request); @@ -252,7 +253,7 @@ Connection.prototype.handleMessage = function(message) { return; default: - console.warn('Ignoring unrecognized message', message); + logger.warn('Ignoring unrecognized message', message); } }; @@ -274,7 +275,7 @@ Connection.prototype._handleBulkMessage = function(message, method) { if (doc) doc[method](message.error); } } else { - console.error('Invalid bulk message', message); + logger.error('Invalid bulk message', message); } }; @@ -426,7 +427,7 @@ Connection.prototype.sendOp = function(doc, op) { * Sends a message down the socket */ Connection.prototype.send = function(message) { - if (this.debug) console.log('SEND', JSON.stringify(message)); + if (this.debug) logger.info('SEND', JSON.stringify(message)); this.emit('send', message); this.socket.send(JSON.stringify(message)); diff --git a/lib/client/doc.js b/lib/client/doc.js index 71f4e204..eaff1623 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1,4 +1,5 @@ var emitter = require('../emitter'); +var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); @@ -832,7 +833,7 @@ Doc.prototype._opAcknowledged = function(message) { } else if (message.v !== this.version) { // We should already be at the same version, because the server should // have sent all the ops that have happened before acknowledging our op - console.warn('Invalid version from server. Expected: ' + this.version + ' Received: ' + message.v, message); + logger.warn('Invalid version from server. Expected: ' + this.version + ' Received: ' + message.v, message); // Fetching should get us back to a working document state return this.fetch(); diff --git a/lib/client/index.js b/lib/client/index.js index 12e17f5d..78914aca 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -3,3 +3,4 @@ exports.Doc = require('./doc'); exports.Error = require('../error'); exports.Query = require('./query'); exports.types = require('../types'); +exports.logger = require('../logger'); diff --git a/lib/logger/index.js b/lib/logger/index.js new file mode 100644 index 00000000..9a80c1f1 --- /dev/null +++ b/lib/logger/index.js @@ -0,0 +1,3 @@ +var Logger = require('./logger'); +var logger = new Logger(); +module.exports = logger; diff --git a/lib/logger/logger.js b/lib/logger/logger.js new file mode 100644 index 00000000..6d193e16 --- /dev/null +++ b/lib/logger/logger.js @@ -0,0 +1,21 @@ +var SUPPORTED_METHODS = [ + 'info', + 'warn', + 'error' +]; + +function Logger() { + this.setMethods(console); +} +module.exports = Logger; + +Logger.prototype.setMethods = function (overrides) { + overrides = overrides || {}; + var logger = this; + + SUPPORTED_METHODS.forEach(function (method) { + if (typeof overrides[method] === 'function') { + logger[method] = overrides[method]; + } + }); +}; diff --git a/lib/stream-socket.js b/lib/stream-socket.js index e9c5303f..696c24f3 100644 --- a/lib/stream-socket.js +++ b/lib/stream-socket.js @@ -1,5 +1,6 @@ var Duplex = require('stream').Duplex; var inherits = require('util').inherits; +var logger = require('./logger'); var util = require('./util'); function StreamSocket() { @@ -36,7 +37,7 @@ function ServerStream(socket) { this.socket = socket; this.on('error', function(error) { - console.warn('ShareDB client message stream error', error); + logger.warn('ShareDB client message stream error', error); socket.close('stopped'); }); diff --git a/test/logger.js b/test/logger.js new file mode 100644 index 00000000..aba680eb --- /dev/null +++ b/test/logger.js @@ -0,0 +1,46 @@ +var Logger = require('../lib/logger/logger'); +var expect = require('expect.js'); +var sinon = require('sinon'); + +describe('Logger', function () { + describe('Stubbing console.warn', function () { + beforeEach(function () { + sinon.stub(console, 'warn'); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('logs to console by default', function () { + var logger = new Logger(); + logger.warn('warning'); + expect(console.warn.calledOnceWithExactly('warning')).to.be(true); + }); + + it('overrides console', function () { + var customWarn = sinon.stub(); + var logger = new Logger(); + logger.setMethods({ + warn: customWarn + }); + + logger.warn('warning'); + + expect(console.warn.notCalled).to.be(true); + expect(customWarn.calledOnceWithExactly('warning')).to.be(true); + }); + + it('only overrides if provided with a method', function () { + var badWarn = 'not a function'; + var logger = new Logger(); + logger.setMethods({ + warn: badWarn + }); + + logger.warn('warning'); + + expect(console.warn.calledOnceWithExactly('warning')).to.be(true); + }); + }); +});