Permalink
Browse files

First commit with content. Should be mostly workable, but possibly un…

…stable.
  • Loading branch information...
1 parent 7c02443 commit 423d9b2733e3615590c0b7d93c167741e2a160fa @TooTallNate committed May 12, 2010
Showing with 443 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +22 −0 LICENSE
  3. +47 −0 example/loggingWebServer.js
  4. +372 −0 lib/elf-logger.js
View
2 .gitignore
@@ -0,0 +1,2 @@
+logs
+._*
View
22 LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2010 Nathan Rajlich (nathan@tootallnate.net)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
View
47 example/loggingWebServer.js
@@ -0,0 +1,47 @@
+var http = require("http"),
+ elf = require("../lib/elf-logger"),
+ responseString = "Hello World!\nNow go look in the './log' folder to see logging output.";
+
+var httpServer = http.createServer(function (req, res) {
+ res.writeHead(200, {
+ "Content-Type": "text/plain;charset=utf-8",
+ "Content-Length": responseString.length,
+ });
+ res.write(responseString, "utf8");
+ res.end();
+});
+httpServer.listen(8080);
+
+
+// You may pass an 'options' argument with a 'stream'
+// property. It's value should be a WritableStream to
+// write logging output to. If a 'stream' prop is
+// present, only the 'stream' and 'fields' are valid.
+//var stdoutLog = elf.createLogger(httpServer, {
+ // This logger will print all logging output to 'stdout'
+// stream: process.stdout,
+ // Log only a few fields
+// fields: ['date','time','c-ip','cs-method','cs-uri','sc-status']
+//});
+
+
+// Omitting the second 'options' argument creates
+// a logger instance with elf.defaultOptions
+var filesystemLog = elf.createLogger(httpServer, {
+ template: function(fields) {
+ var name = "";
+
+ if (fields['cs(host)']) {
+ var index = fields['cs(host)'].indexOf(":");
+ name += index >= 0 ? fields['cs(host)'].substring(0, index) : fields['cs(host)'];
+ } else {
+ name += "no-host";
+ }
+ name += "/";
+
+ name += fields['date'] + ".log";
+ return name;
+ },
+ dir: 'logs',
+ fields: ['date','time','c-ip','cs-method','cs-uri','sc-status']
+});
View
372 lib/elf-logger.js
@@ -0,0 +1,372 @@
+var fs = require("fs"), url = require("url"), path = require("path"), sys = require("sys");
+
+// The default options that are used if not specified in your
+// custom options argument.
+exports.defaultOptions = {
+ dir: "./log",
+ template: "{date}.log",
+ fields: [ 'date', 'time', 'c-ip', 's-ip', 's-port', 'cs-method', 'cs-uri',
+ 'cs-uri-stem', 'cs-uri-query', 'sc-status', 'cs(User-Agent)',
+ 'cs(Referer)']
+};
+
+
+
+// 'createLogger' is the function to call to have elf-logger start
+// monitoring requests and responses on a Node HTTP server instance
+exports.createLogger = function(httpServer, options) {
+ return new ElfLogger(httpServer, options || exports.defaultOptions);
+};
+
+
+
+// We need to modify the standard 'http' library a little bit, to hook into
+// logging events that otherwise aren't exposed.
+(function(http) {
+ var oCreateServer = http.createServer,
+ oOutgoingMessageEnd = http.OutgoingMessage.prototype.end,
+ oOutgoingMessageSendHeaderLines = http.OutgoingMessage.prototype.sendHeaderLines,
+ oServerResponseWriteHead = http.ServerResponse.prototype.writeHead;
+
+ http.createServer = function(requestListener) {
+ var s = oCreateServer.apply(this, arguments);
+ s._emit = s.emit;
+ s.emit = function(type, req, res) {
+ if (type === 'request' && s._elfs && s._elfs.length > 0) {
+ var e = new ElfEntry(s);
+ for (var i in req.headers) {
+ e.fields['cs('+i+')'] = req.headers[i];
+ }
+ e.fields['c-ip'] = req.connection.remoteAddress;
+ e.fields['cs-method'] = req.method;
+ e.fields['cs-uri'] = req.url;
+ var parsed = url.parse(req.url);
+ if (parsed.pathname)
+ e.fields['cs-uri-stem'] = parsed.pathname;
+ if (parsed.query)
+ e.fields['cs-uri-query'] = parsed.query;
+ req.elfEntry = res.elfEntry = e;
+ }
+ return s._emit.apply(this, arguments);
+ }
+ return s;
+ }
+
+ http.OutgoingMessage.prototype.end = function() {
+ var rtn = oOutgoingMessageEnd.apply(this, arguments);
+ if (this.elfEntry) {
+ var now = new Date();
+ this.elfEntry.fields['date'] = formatDate(now);
+ this.elfEntry.fields['time'] = formatTime(now);
+ // Write the entry to interested log files
+ this.elfEntry.log();
+ }
+ return rtn;
+ }
+
+ http.OutgoingMessage.prototype.sendHeaderLines = function(firstLine, headers) {
+ var rtn = oOutgoingMessageSendHeaderLines.apply(this, arguments);
+ if (this.elfEntry) {
+ for (var i in headers) {
+ // Log all headers as lower case, since HTTP headers are case-insensitive
+ this.elfEntry.fields[('sc('+i+')').toLowerCase()] = headers[i];
+ }
+ }
+ return rtn;
+ }
+
+ http.ServerResponse.prototype.writeHead = function(statusCode) {
+ var rtn = oServerResponseWriteHead.apply(this, arguments);
+ if (this.elfEntry) {
+ this.elfEntry.fields['sc-status'] = Number(statusCode);
+ }
+ return rtn;
+ }
+})(require("http"));
+
+
+
+
+
+
+
+
+
+
+// An ElfLogger instance represents a single logging configuration
+// for an HttpServer. Instances are created through the `createLogger`
+// function. Any number of ElfLoggers can be created for a single
+// HttpServer instance.
+function ElfLogger(httpServer, options) {
+ var self = this, i=options.fields.length;
+ extend(self, options);
+ while (i--) {
+ self.fields[i] = String(self.fields[i]).toLowerCase();
+ }
+
+ this.start = function() {
+ if (!httpServer._elfs) httpServer._elfs = [];
+ httpServer._elfs.push(self);
+ }
+ this.stop = function() {
+ var index = httpServer._elfs.indexOf(self);
+ if (index>=0)
+ arrayRemove(httpServer._elfs, index);
+ }
+
+ this.start();
+}
+
+// Any "fresh" WritableStream needs to have the header written
+// out to. This only happens once per Stream.
+extend(ElfLogger.prototype, {
+ writeHeader: function(stream) {
+ var now = new Date(),
+ header = "#Software: Node.js/"+process.version+"\n"+
+ "#Version: 1.0\n"+
+ "#Date: "+formatDate(now)+" "+formatTime(now)+"\n"+
+ "#Fields: ";
+ for (var i=0; i<this.fields.length; i++) {
+ header += this.fields[i] + (i < this.fields.length-1 ? ' ' : '\n');
+ }
+ stream.write(header, 'utf8');
+ stream._elfHeaderWritten = true;
+ },
+ getEntryFilename: function(entry) {
+ var filename = typeof this.template === 'function' ?
+ this.template(entry.fields) :
+ evalTemplate(this.template, entry.fields);
+ return this.dir ? path.join(this.dir, filename) : filename;
+ }
+});
+
+
+
+
+
+
+
+
+
+
+
+// An ElfEntry represents a single entry in a log file (or multiple
+// log files). An ElfEntry instance is attached to HttpRequest and
+// HttpResponse pairs during a 'request' event.
+function ElfEntry(server) {
+ this.server = server;
+ this.fields = {};
+}
+
+extend(ElfEntry.prototype, {
+ log: function() {
+ this.server._elfs.forEach(function(logger) {
+ if (logger.stream) { // log directly to stream
+ this.writeToStream(logger, logger.stream);
+
+ } else { // go through the standard filename templating
+ var logPath = logger.getEntryFilename(this);
+ //sys.puts(logPath);
+ writeToLog(logPath, this, logger);
+ }
+ }, this);
+ },
+ writeToStream: function(logger, stream) {
+ if (!stream._elfHeaderWritten) {
+ logger.writeHeader(stream);
+ }
+ for (var i=0; i<logger.fields.length; i++) {
+ var val = this.fields[logger.fields[i]];
+ stream.write(val ? String(val) : '-', 'utf8');
+ stream.write(i < logger.fields.length-1 ? ' ' : '\n', 'utf8');
+ }
+ }
+});
+
+
+
+
+
+
+
+
+
+
+
+
+
+function ElfLog(filepath, logger) {
+ var self = this;
+ this.logger = logger;
+ this.ready = false;
+ this.queuedEntries = [];
+ this.logPath = filepath;
+
+ var dirExists = true,
+ logName = path.basename(filepath),
+ dirs = getDirs(filepath);
+
+ //sys.puts(JSON.stringify(dirs));
+ //sys.puts(logName);
+
+ if (dirs.length > 0) {
+ var dCount = 0, dirExists = true;
+
+ var checkDir = function() {
+ var subDirs = dirs.slice(0, dCount+1);
+ subDirs = subDirs.length === 1 && subDirs[0] === '' ?
+ '/' : subDirs.join('/');
+ if (dirExists) {
+ //sys.puts('stat: ' + subDirs);
+ fs.stat(subDirs, function(err, stat) {
+ if (stat) {
+ dCount++;
+ } else {
+ // 'err' exists
+ dirExists = false;
+ }
+
+ if (dCount < dirs.length)
+ checkDir();
+ else
+ self.createWriteStream();
+ });
+ } else {
+ sys.puts('mkdir: ' + subDirs);
+ fs.mkdir(subDirs, 0777, function(err) {
+ dCount++;
+ if (dCount < dirs.length)
+ checkDir();
+ else
+ self.createWriteStream();
+ });
+ }
+ }
+ checkDir();
+
+ } else {
+ this.createWriteStream();
+ }
+}
+
+extend(ElfLog.prototype, {
+ createWriteStream: function() {
+ this.stream = fs.createWriteStream(this.logPath, {
+ 'flags': 'w',
+ 'encoding': 'utf8',
+ 'mode': 0666
+ });
+ this.ready = true;
+ this.flushQueue();
+ },
+ flushQueue: function() {
+ this.queuedEntries.forEach(function(entry) {
+ this.writeEntry(entry);
+ }, this);
+ },
+ queueEntry: function(entry) {
+ if (this.ready) {
+ this.writeEntry(entry);
+ } else {
+ this.queuedEntries.push(entry);
+ }
+ },
+ writeEntry: function(entry) {
+ entry.writeToStream(this.logger, this.stream);
+ }
+});
+
+var elfLogs = {};
+
+function writeToLog(filepath, entry, logger) {
+ var elfLog;
+ if (elfLogs[filepath]) {
+ elfLog = elfLogs[filepath];
+ } else {
+ elfLog = elfLogs[filepath] = new ElfLog(filepath, logger);
+ }
+ elfLog.queueEntry(entry);
+}
+
+
+
+
+
+
+
+
+
+
+
+// Array Remove - By John Resig (MIT Licensed)
+function arrayRemove(array, from, to) {
+ var rest = array.slice((to || from) + 1 || array.length);
+ array.length = from < 0 ? array.length + from : from;
+ return array.push.apply(array, rest);
+}
+
+// Copies the properties from 'source' onto 'destination'
+function extend(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+}
+
+function formatDate(date) {
+ var year = date.getUTCFullYear(),
+ month = date.getUTCMonth()+1,
+ day = date.getUTCDate();
+ return year + "-" + pad(month) + "-" + pad(day);
+}
+
+function formatTime(date) {
+ var hours = date.getUTCHours(),
+ mins = date.getUTCMinutes(),
+ secs = date.getUTCSeconds(),
+ tenths = Math.floor(date.getUTCMilliseconds()/100);
+ return pad(hours) + ":" + pad(mins) + ":" + pad(secs) + "." + tenths;
+}
+
+function pad(num) {
+ return num < 10 ? '0' + num : num;
+}
+
+function evalTemplate(template, user) {
+ return template.replace(/{[^{}]+}/g, function(key){
+ return user[key.replace(/[{}]+/g, "").toLowerCase()] || "-";
+ });
+}
+
+function getDirs(logPath) {
+ var dirs = [], p = String(logPath), r;
+ // TODO: a prettier way to write this loop
+ do {
+ r = getRootDir(p);
+ if (r) {
+ p = p.substring(r.length+1);
+ dirs.push(r);
+ }
+ } while (r != null);
+ return logPath.indexOf('/') === 0 ? shiftLeadingSlash(dirs) : dirs;
+}
+
+function getRootDir(dirPath) {
+ var root = null, p = path.join(dirPath, "..");
+ while (p.length > 0) {
+ root = p;
+ p = path.join(p, "..");
+ }
+ return root;
+}
+
+function shiftLeadingSlash(array) {
+ var rtn = [''], i=0, cur;
+ for (var i=0; i<array.length; i++) {
+ cur = array[i];
+ if (i===0) {
+ cur = cur.substring(1);
+ }
+ rtn.push(cur);
+ }
+ return rtn;
+}

0 comments on commit 423d9b2

Please sign in to comment.