In my previous post I built a logging & reporting module for Windows 8 store apps. In this post I am going to build a simple while fast and scalable logging server.
Open source node.js is used to serve as http logging server. Node.js is a JavaScript server-side solution. It's light, efficient, and using event-driven and non-blocking I/O. These features make it a good fit for logging JSON objects sent by the client.
Traditionally the logging service would just save logs to file system. However it's hard to do data analysis and poor performance to do query over big size of logged data if using plain text file logs. Alternatively we can use document type NoSQL as the data store. In our use case, each log is an independent information unit, and document stores works extremely well in such scenario. Data insert and query would be faster and more efficient than traditional SQL database. We pick MongoDB as it's easy to use in different platform, well documented and has native support to JSON data.
Node.js requires a driver or module to connect to MongoDB database. We use mongojs in our example:
npm install mongojs
The whole logging server implementation is less than 100-line of code:
var http = require('http'); var url = require('url'); var mongojs = require('mongojs'); var db = mongojs('Data'); var collection = db.collection('Logs'); var contentType = {'Content-Type': 'text/html'}; var OK = 'OK', BAD = 'BAD REQUEST'; var MAX_URL_LENGTH = 2048, MAX_POST_DATA = 10240; function createLog(request) { var logObj = {}; try { logObj.time = new Date(); logObj.method = request.method; logObj.host = request.headers.host; logObj.url = request.url; var queryStrings = url.parse(request.url, true).query; // Check if it's an empty object if (Object.keys(queryStrings).length) { logObj.query = queryStrings; for (var attrname in queryStrings) { logObj[attrname] = queryStrings[attrname]; } } } catch (ex) {} return logObj; } http.createServer(function (request, response) { // Bypass request with URL larger than 2K if (request.url.length > MAX_URL_LENGTH) { response.writeHead(414, contentType); response.end(BAD); } else if (request.method == 'GET') { // HTTP GET var logObj = createLog(request); if (logObj.query) { delete logObj.query; collection.save(logObj); response.writeHead(200, contentType); response.end(OK); } else { // Empty request response.writeHead(400, contentType); response.end(BAD); request.connection.destroy(); } } else if(request.method == 'POST') { // HTTP POST var logObj = createLog(request); var postData = ''; request.on('data', function(data) { postData += data; // Bypass request with POST data larger than 10K if(postData.length > MAX_POST_DATA) { postData = ""; response.writeHead(413, contentType); response.end(BAD); request.connection.destroy(); } }); request.on('end', function() { if (!postData && !logObj.query) { // Empty request response.writeHead(400, contentType); response.end(BAD); request.connection.destroy(); } else { if (postData) { try { postObjs = JSON.parse(postData); for (var attrname in postObjs) { logObj[attrname] = postObjs[attrname]; } } catch (ex) { logObj.postData = postData; } } if (logObj.query) { delete logObj.query; } collection.save(logObj); response.writeHead(200, contentType); response.end(OK); } }); } }).listen(process.env.PORT || 8080);