Jump To …

file.js

/*
 * file.js: Transport for outputting to a local log file
 *
 * (C) 2010 Charlie Robbins
 * MIT LICENCE
 *
 */

var events = require('events'),
    fs = require('fs'),
    path = require('path'),
    util = require('util'),
    colors = require('colors'),
    common = require('../common'),
    Transport = require('./transport').Transport;
    

function File (options)

@options {Object} Options for this instance.

Constructor function for the File transport object responsible for persisting log messages and metadata to one or more files.

var File = exports.File = function (options) {
  Transport.call(this, options);
  

Helper function which throws an Error in the event that any of the rest of the arguments is present in options.

  function throwIf (target /*, illegal... */) {
    Array.prototype.slice.call(arguments, 1).forEach(function (name) {
      if (options[name]) {
        throw new Error('Cannot set ' + name + ' and ' + target + 'together');
      }
    });
  }
  
  if (options.filename || options.dirname) {
    throwIf('filename or dirname', 'stream');
    this._basename = this.filename = path.basename(options.filename) || 'winston.log';
    this.dirname   = options.dirname || path.dirname(options.filename);
    this.options   = options.options || { flags: 'a' };    
  }
  else if (options.stream) {
    throwIf('stream', 'filename', 'maxsize');
    this.stream = options.stream;
  }
  else {
    throw new Error('Cannot log to file without filename or stream.');
  }
    
  this.json      = options.json !== false;
  this.colorize  = options.colorize  || false;
  this.maxsize   = options.maxsize   || null;
  this.timestamp = typeof options.timestamp !== 'undefined' ? options.timestamp : false;

Internal state variables representing the number of files this instance has created and the current size (in bytes) of the current logfile.

  this._size    = 0;
  this._created = 0;
  this._buffer  = [];
};

Inherit from winston.Transport.

util.inherits(File, Transport);

Expose the name of this Transport on the prototype

File.prototype.name = 'file';

function log (level, msg, [meta], callback)

@level {string} Level at which to log the message.

@msg {string} Message to log

@meta {Object} Optional Additional metadata to attach

@callback {function} Continuation to respond to when complete.

Core logging method exposed to Winston. Metadata is optional.

File.prototype.log = function (level, msg, meta, callback) {
  if (this.silent) {
    return callback(null, true);
  }

  var self = this, output = common.log({
    level:     level,
    message:   msg,
    meta:      meta,
    json:      this.json,
    colorize:  this.colorize,
    timestamp: this.timestamp
  }) + '\n';
  
  this._size += output.length;
  
  function onDrain () {
    self.emit('logged');
  }
  
  if (!this.filename) {

If there is no filename on this instance then it was configured with a raw WriteableStream instance and we should not perform any size restrictions.

    this.stream.write(output);
    this.stream.once('drain', onDrain);
  }
  else {
    this.open(function (err) {
      if (err) {

If there was an error enqueue the message

        return self._buffer.push(output);
      }
      
      self.stream.write(output);
      self.stream.once('drain', onDrain);
    });
  }

  callback(null, true);
};

function open (callback)

@callback {function} Continuation to respond to when complete

Checks to see if a new file needs to be created based on the maxsize (if any) and the current size of the file used.

File.prototype.open = function (callback) {
  if (this.opening) {

If we are already attempting to open the next available file then respond with a value indicating that the message should be buffered.

    return callback(true);
  }
  else if (!this.stream || (this.maxsize && this._size >= this.maxsize)) {

If we dont have a stream or have exceeded our size, then create the next stream and respond with a value indicating that the message should be buffered.

    callback(true);
    return this._createStream();
  }
  

Otherwise we have a valid (and ready) stream.

  callback();
};

function close ()

Closes the stream associated with this instance.

File.prototype.close = function() {
  var self = this;

  if (this.stream) {
    this.stream.end();
    this.stream.destroySoon();
    
    this.stream.once('drain', function () {
      self.emit('flush');
      self.emit('closed');
    });
  }
};

function flush ()

Flushes any buffered messages to the current stream used by this instance.

File.prototype.flush = function () {
  var self = this;

Iterate over the _buffer of enqueued messaged and then write them to the newly created stream.

  this._buffer.forEach(function (str) {
    process.nextTick(function () {
      self.stream.write(str);
      self._size += str.length;
    });
  });
  

Quickly truncate the _buffer once the write operations have been started

  self._buffer.length = 0;
  

When the stream has drained we have flushed our buffer.

  self.stream.once('drain', function () {
    self.emit('flush');
    self.emit('logged');
  });
};

@private function _createStream ()

Attempts to open the next appropriate file for this instance based on the common state (such as maxsize and _basename).

File.prototype._createStream = function () {
  var self = this;
  this.opening = true;
    
  (function checkFile (target) {
    var fullname = path.join(self.dirname, target);
    

Creates the WriteStream and then flushes any buffered messages.

    function createAndFlush (size) {
      if (self.stream) {
        self.stream.end();
        self.stream.destroySoon();
      }
      
      self._size = size;
      self.filename = target;
      self.stream = fs.createWriteStream(fullname, self.options);
      

When the current stream has finished flushing then we can be sure we have finished opening and thus can emit the open event.

      self.once('flush', function () {
        self.opening = false;
        self.emit('open', fullname);
      });

Remark: It is possible that in the time it has taken to find the next logfile to be written more data than maxsize has been buffered, but for sensible limits (10s - 100s of MB) this seems unlikely in less than one second.

      self.flush();
    }

    fs.stat(fullname, function (err, stats) {
      if (err) {
        if (err.code !== 'ENOENT') {
          return self.emit('error', err);
        }
        
        return createAndFlush(0);
      }
      
      if (!stats || (self.maxsize && stats.size >= self.maxsize)) {

If stats.size is greater than the maxsize for this instance then try again

        return checkFile(self._getFile(true));
      }
      
      createAndFlush(stats.size);
    });
  })(this._getFile());  
};

@private function _getFile ()

Gets the next filename to use for this instance in the case that log filesizes are being capped.

File.prototype._getFile = function (inc) {
  var self = this,
      ext = path.extname(this._basename),
      basename = path.basename(this._basename, ext);
  
  if (inc) {

Increment the number of files created or checked by this instance.

    this._created += 1;
  }
  
  return this._created 
    ? basename + this._created + ext
    : basename + ext;
};