diff --git a/public/templates/admin/logger.tpl b/public/templates/admin/logger.tpl
new file mode 100644
index 0000000000..83f569010b
--- /dev/null
+++ b/public/templates/admin/logger.tpl
@@ -0,0 +1,49 @@
+
Logger
+
+
+
Logger Settings
+
+
+
+ By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.
+
+
+
+ Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.
+
+
+
+
+
+
+
+
+
diff --git a/public/templates/config.json b/public/templates/config.json
index f97cd5ce26..636ef7f0b3 100644
--- a/public/templates/config.json
+++ b/public/templates/config.json
@@ -11,6 +11,7 @@
"^admin/settings.*": "admin/settings",
"admin/twitter.*": "admin/twitter",
"admin/facebook.*": "admin/facebook",
+ "admin/logger.*": "admin/logger",
"admin/gplus.*": "admin/gplus",
"admin/motd/?$": "admin/motd",
"admin/groups/?$": "admin/groups",
@@ -41,4 +42,4 @@
"force_refresh": {
"logout": true
}
-}
\ No newline at end of file
+}
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000000..06133ae1ca
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,204 @@
+/*
+ * Logger module: ability to dynamically turn on/off logging for http requests & socket.io events
+ */
+
+var fs = require('fs'),
+ express = require('express'),
+ winston = require('winston'),
+ util = require('util'),
+ socketio = require('socket.io'),
+ meta = require('./meta.js');
+
+var opts = {
+ /*
+ * state used by Logger
+ */
+ express : {
+ app : {},
+ set : 0,
+ ofn : null,
+ },
+ streams : {
+ log : { f : process.stdout },
+ }
+};
+
+
+/* -- Logger -- */
+
+(function(Logger) {
+
+
+ Logger.init = function(app) {
+ opts.express.app = app;
+ /* Open log file stream & initialize express logging if meta.config.logger* variables are set */
+ Logger.setup();
+ }
+
+ Logger.setup = function() {
+ Logger.setup_one('loggerPath', meta.config.loggerPath);
+ }
+
+ Logger.setup_one = function(key,value) {
+ /*
+ * 1. Open the logger stream: stdout or file
+ * 2. Re-initialize the express logger hijack
+ */
+ switch(key) {
+ case 'loggerPath': {
+ Logger.setup_one_log(value);
+ Logger.express_open();
+ }
+ default: return;
+ }
+ }
+
+ Logger.setup_one_log = function(value) {
+ /*
+ * If logging is currently enabled, create a stream.
+ * Otherwise, close the current stream
+ */
+ if(meta.config.loggerStatus > 0 || meta.config.loggerIOStatus) {
+ var stream = Logger.open(value);
+ if(stream) opts.streams.log.f = stream;
+ else opts.streams.log.f = process.stdout;
+ }
+ else {
+ Logger.close(opts.streams.log);
+ }
+ }
+
+ Logger.open = function(value) {
+ /* Open the streams to log to: either a path or stdout */
+ var stream;
+ if(value)
+ stream = fs.createWriteStream(value, {flags: 'a'});
+ else
+ stream = process.stdout;
+ return stream;
+ }
+
+ Logger.close = function(stream) {
+ if(stream.f != process.stdout && stream.f != null) stream.end();
+ stream.f = null;
+ }
+
+ Logger.monitorConfig = function(socket, data) {
+ /*
+ * This monitor's when a user clicks "save" in the Logger section of the admin panel
+ */
+ Logger.setup_one(data.key,data.value);
+ Logger.io_close(socket);
+ Logger.io(socket);
+ }
+
+ Logger.express_open = function() {
+ if(opts.express.set != 1) {
+ opts.express.set = 1;
+ opts.express.app.use(Logger.expressLogger);
+ }
+ /*
+ * Always initialize "ofn" (original function) with the original logger function
+ */
+ opts.express.ofn = express.logger({stream : opts.streams.log.f});
+ }
+
+ Logger.expressLogger = function(req,res,next) {
+ /*
+ * The new express.logger
+ *
+ * This hijack allows us to turn logger on/off dynamically within express
+ */
+ if(meta.config.loggerStatus > 0) {
+ return opts.express.ofn(req,res,next);
+ }
+ else {
+ return next()
+ }
+ }
+
+ Logger.prepare_io_string = function(_type, _uid, _args) {
+ /*
+ * This prepares the output string for intercepted socket.io events
+ *
+ * The format is: io:
+ */
+ try {
+ return 'io: '+_uid+' '+_type+' '+util.inspect(Array.prototype.slice.call(_args))+'\n';
+ } catch(err) {
+ winston.info("Logger.prepare_io_string: Failed",err)
+ return "error";
+ }
+ }
+
+ Logger.io_close = function(socket) {
+ /*
+ * Restore all hijacked sockets to their original emit/on functions
+ */
+ var clients = socket.io.sockets.clients();
+ for(var v in clients) {
+ var client = clients[v];
+
+ if(client.oEmit != client.emit)
+ client.emit = client.oEmit;
+
+ if(client.$oEmit != client.$emit)
+ client.$emit = client.$oEmit;
+ }
+ }
+
+ Logger.io = function(socket) {
+ /*
+ * Go through all of the currently established sockets & hook their .emit/.on
+ */
+ if(socket == undefined && socket.io.sockets == undefined) {
+ return;
+ }
+
+ var clients = socket.io.sockets.clients();
+ for(var v in clients) {
+ var client = clients[v];
+ Logger.io_one(client,client.state.user.uid);
+ }
+ }
+
+ Logger.io_one = function(socket,uid) {
+ /*
+ * This function replaces a socket's .emit/.on functions in order to intercept events
+ */
+ if(socket != undefined && meta.config.loggerIOStatus > 0) {
+
+ (function() {
+ // courtesy of: http://stackoverflow.com/a/9674248
+ var user = uid
+ if(!user) user = "?"
+ socket.oEmit = socket.emit;
+ var emit = socket.emit;
+ socket.emit = function() {
+ if(opts.streams.log.f != null) {
+ opts.streams.log.f.write(Logger.prepare_io_string("emit",uid,arguments));
+ }
+ try {
+ emit.apply(socket, arguments);
+ } catch(err) {
+ winston.info("Logger.io_one: emit.apply: Failed", err);
+ }
+ };
+ socket.$oEmit = socket.$emit;
+ var $emit = socket.$emit;
+ socket.$emit = function() {
+
+ if(opts.streams.log.f != null) {
+ opts.streams.log.f.write(Logger.prepare_io_string("on",uid,arguments));
+ }
+ try {
+ $emit.apply(socket, arguments);
+ } catch(err) {
+ winston.info("Logger.io_one: $emit.apply: Failed", err);
+ }
+ };
+ })();
+ }
+ }
+
+}(exports));
diff --git a/src/routes/admin.js b/src/routes/admin.js
index b6db9cf925..a26a1be15c 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -30,7 +30,7 @@ var user = require('./../user.js'),
(function () {
var routes = [
'categories/active', 'categories/disabled', 'users', 'topics', 'settings', 'themes',
- 'twitter', 'facebook', 'gplus', 'redis', 'motd', 'groups',
+ 'twitter', 'facebook', 'gplus', 'redis', 'motd', 'groups','logger',
'users/latest', 'users/sort-posts', 'users/sort-reputation',
'users/search', 'plugins'
];
@@ -224,6 +224,10 @@ var user = require('./../user.js'),
res.json(200, {});
});
+ app.get('/logger', function(req, res) {
+ res.json(200, {});
+ });
+
app.get('/themes', function (req, res) {
res.json(200, {});
});
@@ -257,4 +261,4 @@ var user = require('./../user.js'),
};
-}(exports));
\ No newline at end of file
+}(exports));
diff --git a/src/webserver.js b/src/webserver.js
index 0759ae3e37..74d4f4ce8a 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -23,7 +23,8 @@ var express = require('express'),
plugins = require('./plugins'),
nconf = require('nconf'),
winston = require('winston'),
- validator = require('validator');
+ validator = require('validator'),
+ logger = require('./logger.js');
(function (app) {
var templates = null,
@@ -81,6 +82,9 @@ var express = require('express'),
function(next) {
// Pre-router middlewares
app.use(express.compress());
+
+ logger.init(app);
+
app.use(express.favicon(path.join(__dirname, '../', 'public', 'favicon.ico')));
app.use(require('less-middleware')({
src: path.join(__dirname, '../', 'public'),
@@ -546,4 +550,4 @@ var express = require('express'),
}(WebServer));
-global.server = server;
\ No newline at end of file
+global.server = server;
diff --git a/src/websockets.js b/src/websockets.js
index 633c5b8f86..8fae538380 100644
--- a/src/websockets.js
+++ b/src/websockets.js
@@ -16,6 +16,9 @@ var cookie = require('cookie'),
async = require('async'),
RedisStoreLib = require('connect-redis')(express),
RDB = require('./redis'),
+ util = require('util'),
+ logger = require('./logger.js'),
+ fs = require('fs')
RedisStore = new RedisStoreLib({
client: RDB,
ttl: 60 * 60 * 24 * 14
@@ -54,6 +57,7 @@ module.exports.init = function(io) {
var hs = socket.handshake,
sessionID, uid;
+
// Validate the session, if present
socketCookieParser(hs, {}, function(err) {
sessionID = socket.handshake.signedCookies["express.sid"];
@@ -64,6 +68,16 @@ module.exports.init = function(io) {
userSockets[uid] = userSockets[uid] || [];
userSockets[uid].push(socket);
+ /* Need to save some state for the logger & maybe some other modules later on */
+ socket.state = {
+ user : {
+ uid : uid
+ }
+ };
+
+ /* If meta.config.loggerIOStatus > 0, logger.io_one will hook into this socket */
+ logger.io_one(socket,uid);
+
if (uid) {
RDB.zadd('users:online', Date.now(), uid, function(err, data) {
@@ -645,6 +659,8 @@ module.exports.init = function(io) {
if (!err) socket.emit('api:config.set', {
status: 'ok'
});
+ /* Another hook, for my (adarqui's) logger module */
+ logger.monitorConfig(this, data);
});
});