Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Avatar for KuCalc : devops.
Download
50675 views
1
/*
2
* file.js: Transport for outputting to a local log file
3
*
4
* (C) 2010 Charlie Robbins
5
* MIT LICENCE
6
*
7
*/
8
9
var events = require('events'),
10
fs = require('fs'),
11
path = require('path'),
12
util = require('util'),
13
colors = require('colors'),
14
common = require('../common'),
15
Transport = require('./transport').Transport,
16
Stream = require('stream').Stream;
17
18
//
19
// ### function File (options)
20
// #### @options {Object} Options for this instance.
21
// Constructor function for the File transport object responsible
22
// for persisting log messages and metadata to one or more files.
23
//
24
var File = exports.File = function (options) {
25
Transport.call(this, options);
26
27
//
28
// Helper function which throws an `Error` in the event
29
// that any of the rest of the arguments is present in `options`.
30
//
31
function throwIf (target /*, illegal... */) {
32
Array.prototype.slice.call(arguments, 1).forEach(function (name) {
33
if (options[name]) {
34
throw new Error('Cannot set ' + name + ' and ' + target + 'together');
35
}
36
});
37
}
38
39
if (options.filename || options.dirname) {
40
throwIf('filename or dirname', 'stream');
41
this._basename = this.filename = path.basename(options.filename) || 'winston.log';
42
this.dirname = options.dirname || path.dirname(options.filename);
43
this.options = options.options || { flags: 'a' };
44
}
45
else if (options.stream) {
46
throwIf('stream', 'filename', 'maxsize');
47
this._stream = options.stream;
48
49
//
50
// We need to listen for drain events when
51
// write() returns false. This can make node
52
// mad at times.
53
//
54
this._stream.setMaxListeners(Infinity);
55
}
56
else {
57
throw new Error('Cannot log to file without filename or stream.');
58
}
59
60
this.json = options.json !== false;
61
this.colorize = options.colorize || false;
62
this.maxsize = options.maxsize || null;
63
this.maxFiles = options.maxFiles || null;
64
this.prettyPrint = options.prettyPrint || false;
65
this.timestamp = options.timestamp != null ? options.timestamp : true;
66
67
if (this.json) {
68
this.stringify = options.stringify;
69
}
70
71
//
72
// Internal state variables representing the number
73
// of files this instance has created and the current
74
// size (in bytes) of the current logfile.
75
//
76
this._size = 0;
77
this._created = 0;
78
this._buffer = [];
79
this._draining = false;
80
};
81
82
//
83
// Inherit from `winston.Transport`.
84
//
85
util.inherits(File, Transport);
86
87
//
88
// Expose the name of this Transport on the prototype
89
//
90
File.prototype.name = 'file';
91
92
//
93
// ### function log (level, msg, [meta], callback)
94
// #### @level {string} Level at which to log the message.
95
// #### @msg {string} Message to log
96
// #### @meta {Object} **Optional** Additional metadata to attach
97
// #### @callback {function} Continuation to respond to when complete.
98
// Core logging method exposed to Winston. Metadata is optional.
99
//
100
File.prototype.log = function (level, msg, meta, callback) {
101
if (this.silent) {
102
return callback(null, true);
103
}
104
105
var self = this;
106
107
var output = common.log({
108
level: level,
109
message: msg,
110
meta: meta,
111
json: this.json,
112
colorize: this.colorize,
113
prettyPrint: this.prettyPrint,
114
timestamp: this.timestamp,
115
stringify: this.stringify
116
}) + '\n';
117
118
this._size += output.length;
119
120
if (!this.filename) {
121
//
122
// If there is no `filename` on this instance then it was configured
123
// with a raw `WriteableStream` instance and we should not perform any
124
// size restrictions.
125
//
126
this._write(output, callback);
127
this._lazyDrain();
128
}
129
else {
130
this.open(function (err) {
131
if (err) {
132
//
133
// If there was an error enqueue the message
134
//
135
return self._buffer.push([output, callback]);
136
}
137
138
self._write(output, callback);
139
self._lazyDrain();
140
});
141
}
142
};
143
144
//
145
// ### function _write (data, cb)
146
// #### @data {String|Buffer} Data to write to the instance's stream.
147
// #### @cb {function} Continuation to respond to when complete.
148
// Write to the stream, ensure execution of a callback on completion.
149
//
150
File.prototype._write = function(data, callback) {
151
// If this is a file write stream, we could use the builtin
152
// callback functionality, however, the stream is not guaranteed
153
// to be an fs.WriteStream.
154
var ret = this._stream.write(data);
155
if (!callback) return;
156
if (ret === false) {
157
return this._stream.once('drain', function() {
158
callback(null, true);
159
});
160
}
161
callback(null, true);
162
};
163
164
//
165
// ### function query (options, callback)
166
// #### @options {Object} Loggly-like query options for this instance.
167
// #### @callback {function} Continuation to respond to when complete.
168
// Query the transport. Options object is optional.
169
//
170
File.prototype.query = function (options, callback) {
171
if (typeof options === 'function') {
172
callback = options;
173
options = {};
174
}
175
176
var file = path.join(this.dirname, this.filename),
177
options = this.normalizeQuery(options),
178
buff = '',
179
results = [],
180
row = 0;
181
182
var stream = fs.createReadStream(file, {
183
encoding: 'utf8'
184
});
185
186
stream.on('error', function (err) {
187
if (stream.readable) {
188
stream.destroy();
189
}
190
if (!callback) return;
191
return err.code !== 'ENOENT'
192
? callback(err)
193
: callback(null, results);
194
});
195
196
stream.on('data', function (data) {
197
var data = (buff + data).split(/\n+/),
198
l = data.length - 1,
199
i = 0;
200
201
for (; i < l; i++) {
202
if (!options.start || row >= options.start) {
203
add(data[i]);
204
}
205
row++;
206
}
207
208
buff = data[l];
209
});
210
211
stream.on('close', function () {
212
if (buff) add(buff, true);
213
if (options.order === 'desc') {
214
results = results.reverse();
215
}
216
if (callback) callback(null, results);
217
});
218
219
function add(buff, attempt) {
220
try {
221
var log = JSON.parse(buff);
222
if (check(log)) push(log);
223
} catch (e) {
224
if (!attempt) {
225
stream.emit('error', e);
226
}
227
}
228
}
229
230
function push(log) {
231
if (options.rows && results.length >= options.rows) {
232
if (stream.readable) {
233
stream.destroy();
234
}
235
return;
236
}
237
238
if (options.fields) {
239
var obj = {};
240
options.fields.forEach(function (key) {
241
obj[key] = log[key];
242
});
243
log = obj;
244
}
245
246
results.push(log);
247
}
248
249
function check(log) {
250
if (!log) return;
251
252
if (typeof log !== 'object') return;
253
254
var time = new Date(log.timestamp);
255
if ((options.from && time < options.from)
256
|| (options.until && time > options.until)) {
257
return;
258
}
259
260
return true;
261
}
262
};
263
264
//
265
// ### function _tail (options, callback)
266
// #### @options {Object} Options for tail.
267
// #### @callback {function} Callback to execute on every line.
268
// `tail -f` a file. Options must include file.
269
//
270
File.prototype._tail = function tail(options, callback) {
271
var stream = fs.createReadStream(options.file, { encoding: 'utf8' }),
272
buff = '',
273
destroy,
274
row = 0;
275
276
destroy = stream.destroy.bind(stream);
277
stream.destroy = function () {};
278
279
if (options.start === -1) {
280
delete options.start;
281
}
282
283
if (options.start == null) {
284
stream.once('end', bind);
285
} else {
286
bind();
287
}
288
289
function bind() {
290
stream.on('data', function (data) {
291
var data = (buff + data).split(/\n+/),
292
l = data.length - 1,
293
i = 0;
294
295
for (; i < l; i++) {
296
if (options.start == null || row > options.start) {
297
stream.emit('line', data[i]);
298
}
299
row++;
300
}
301
302
buff = data[l];
303
});
304
305
stream.on('line', function (data) {
306
if (callback) callback(data);
307
});
308
309
stream.on('error', function (err) {
310
destroy();
311
});
312
313
stream.on('end', function () {
314
if (buff) {
315
stream.emit('line', buff);
316
buff = '';
317
}
318
319
resume();
320
});
321
322
resume();
323
}
324
325
function resume() {
326
setTimeout(function () {
327
stream.resume();
328
}, 1000);
329
}
330
331
return destroy;
332
};
333
334
//
335
// ### function stream (options)
336
// #### @options {Object} Stream options for this instance.
337
// Returns a log stream for this transport. Options object is optional.
338
//
339
File.prototype.stream = function (options) {
340
var file = path.join(this.dirname, this.filename),
341
options = options || {},
342
stream = new Stream;
343
344
var tail = {
345
file: file,
346
start: options.start
347
};
348
349
stream.destroy = this._tail(tail, function (line) {
350
try {
351
stream.emit('data', line);
352
line = JSON.parse(line);
353
stream.emit('log', line);
354
} catch (e) {
355
stream.emit('error', e);
356
}
357
});
358
359
return stream;
360
};
361
362
//
363
// ### function open (callback)
364
// #### @callback {function} Continuation to respond to when complete
365
// Checks to see if a new file needs to be created based on the `maxsize`
366
// (if any) and the current size of the file used.
367
//
368
File.prototype.open = function (callback) {
369
if (this.opening) {
370
//
371
// If we are already attempting to open the next
372
// available file then respond with a value indicating
373
// that the message should be buffered.
374
//
375
return callback(true);
376
}
377
else if (!this._stream || (this.maxsize && this._size >= this.maxsize)) {
378
//
379
// If we dont have a stream or have exceeded our size, then create
380
// the next stream and respond with a value indicating that
381
// the message should be buffered.
382
//
383
callback(true);
384
return this._createStream();
385
}
386
387
//
388
// Otherwise we have a valid (and ready) stream.
389
//
390
callback();
391
};
392
393
//
394
// ### function close ()
395
// Closes the stream associated with this instance.
396
//
397
File.prototype.close = function () {
398
var self = this;
399
400
if (this._stream) {
401
this._stream.end();
402
this._stream.destroySoon();
403
404
this._stream.once('drain', function () {
405
self.emit('flush');
406
self.emit('closed');
407
});
408
}
409
};
410
411
//
412
// ### function flush ()
413
// Flushes any buffered messages to the current `stream`
414
// used by this instance.
415
//
416
File.prototype.flush = function () {
417
var self = this;
418
419
//
420
// Iterate over the `_buffer` of enqueued messaged
421
// and then write them to the newly created stream.
422
//
423
this._buffer.forEach(function (item) {
424
var str = item[0],
425
callback = item[1];
426
427
process.nextTick(function () {
428
self._write(str, callback);
429
self._size += str.length;
430
});
431
});
432
433
//
434
// Quickly truncate the `_buffer` once the write operations
435
// have been started
436
//
437
self._buffer.length = 0;
438
439
//
440
// When the stream has drained we have flushed
441
// our buffer.
442
//
443
self._stream.once('drain', function () {
444
self.emit('flush');
445
self.emit('logged');
446
});
447
};
448
449
//
450
// ### @private function _createStream ()
451
// Attempts to open the next appropriate file for this instance
452
// based on the common state (such as `maxsize` and `_basename`).
453
//
454
File.prototype._createStream = function () {
455
var self = this;
456
this.opening = true;
457
458
(function checkFile (target) {
459
var fullname = path.join(self.dirname, target);
460
461
//
462
// Creates the `WriteStream` and then flushes any
463
// buffered messages.
464
//
465
function createAndFlush (size) {
466
if (self._stream) {
467
self._stream.end();
468
self._stream.destroySoon();
469
}
470
471
self._size = size;
472
self.filename = target;
473
self._stream = fs.createWriteStream(fullname, self.options);
474
475
//
476
// We need to listen for drain events when
477
// write() returns false. This can make node
478
// mad at times.
479
//
480
self._stream.setMaxListeners(Infinity);
481
482
//
483
// When the current stream has finished flushing
484
// then we can be sure we have finished opening
485
// and thus can emit the `open` event.
486
//
487
self.once('flush', function () {
488
self.opening = false;
489
self.emit('open', fullname);
490
});
491
492
//
493
// Remark: It is possible that in the time it has taken to find the
494
// next logfile to be written more data than `maxsize` has been buffered,
495
// but for sensible limits (10s - 100s of MB) this seems unlikely in less
496
// than one second.
497
//
498
self.flush();
499
}
500
501
fs.stat(fullname, function (err, stats) {
502
if (err) {
503
if (err.code !== 'ENOENT') {
504
return self.emit('error', err);
505
}
506
507
return createAndFlush(0);
508
}
509
510
if (!stats || (self.maxsize && stats.size >= self.maxsize)) {
511
//
512
// If `stats.size` is greater than the `maxsize` for
513
// this instance then try again
514
//
515
return checkFile(self._getFile(true));
516
}
517
518
createAndFlush(stats.size);
519
});
520
})(this._getFile());
521
};
522
523
//
524
// ### @private function _getFile ()
525
// Gets the next filename to use for this instance
526
// in the case that log filesizes are being capped.
527
//
528
File.prototype._getFile = function (inc) {
529
var self = this,
530
ext = path.extname(this._basename),
531
basename = path.basename(this._basename, ext),
532
remaining;
533
534
if (inc) {
535
//
536
// Increment the number of files created or
537
// checked by this instance.
538
//
539
// Check for maxFiles option and delete file
540
if (this.maxFiles && (this._created >= (this.maxFiles - 1))) {
541
remaining = this._created - (this.maxFiles - 1);
542
if (remaining === 0) {
543
fs.unlinkSync(path.join(this.dirname, basename + ext));
544
}
545
else {
546
fs.unlinkSync(path.join(this.dirname, basename + remaining + ext));
547
}
548
}
549
550
this._created += 1;
551
}
552
553
return this._created
554
? basename + this._created + ext
555
: basename + ext;
556
};
557
558
//
559
// ### @private function _lazyDrain ()
560
// Lazily attempts to emit the `logged` event when `this.stream` has
561
// drained. This is really just a simple mutex that only works because
562
// Node.js is single-threaded.
563
//
564
File.prototype._lazyDrain = function () {
565
var self = this;
566
567
if (!this._draining && this._stream) {
568
this._draining = true;
569
570
this._stream.once('drain', function () {
571
this._draining = false;
572
self.emit('logged');
573
});
574
}
575
};
576
577