daily-rotate-file.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict';
  2. var fs = require('fs');
  3. var os = require('os');
  4. var path = require('path');
  5. var util = require('util');
  6. var zlib = require('zlib');
  7. var hash = require('object-hash');
  8. var MESSAGE = require('triple-beam').MESSAGE;
  9. var PassThrough = require('stream').PassThrough;
  10. var Transport = require('winston-transport');
  11. var loggerDefaults = {
  12. json: false,
  13. colorize: false,
  14. eol: os.EOL,
  15. logstash: null,
  16. prettyPrint: false,
  17. label: null,
  18. stringify: false,
  19. depth: null,
  20. showLevel: true,
  21. timestamp: function () {
  22. return new Date().toISOString();
  23. }
  24. };
  25. var DailyRotateFile = function (options) {
  26. options = options || {};
  27. Transport.call(this, options);
  28. function throwIf(target /* , illegal... */) {
  29. Array.prototype.slice.call(arguments, 1).forEach(function (name) {
  30. if (options[name]) {
  31. throw new Error('Cannot set ' + name + ' and ' + target + ' together');
  32. }
  33. });
  34. }
  35. function getMaxSize(size) {
  36. if (size && typeof size === 'string') {
  37. var _s = size.toLowerCase().match(/^((?:0\.)?\d+)([k|m|g])$/);
  38. if (_s) {
  39. return size;
  40. }
  41. } else if (size && Number.isInteger(size)) {
  42. var sizeK = Math.round(size / 1024);
  43. return sizeK === 0 ? '1k' : sizeK + 'k';
  44. }
  45. return null;
  46. }
  47. function isValidFileName(filename) {
  48. // eslint-disable-next-line no-control-regex
  49. return !/["<>|:*?\\/\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(filename);
  50. }
  51. function isValidDirName(dirname) {
  52. // eslint-disable-next-line no-control-regex
  53. return !/["<>|\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]/g.test(dirname);
  54. }
  55. this.options = Object.assign({}, loggerDefaults, options);
  56. if (options.stream) {
  57. throwIf('stream', 'filename', 'maxsize');
  58. this.logStream = new PassThrough();
  59. this.logStream.pipe(options.stream);
  60. } else {
  61. this.filename = options.filename ? path.basename(options.filename) : 'winston.log';
  62. this.dirname = options.dirname || path.dirname(options.filename);
  63. if (!isValidFileName(this.filename) || !isValidDirName(this.dirname)) {
  64. throw new Error('Your path or filename contain an invalid character.');
  65. }
  66. var self = this;
  67. this.logStream = require('file-stream-rotator').getStream({
  68. filename: path.join(this.dirname, this.filename),
  69. frequency: options.frequency ? options.frequency : 'custom',
  70. date_format: options.datePattern ? options.datePattern : 'YYYY-MM-DD',
  71. verbose: false,
  72. size: getMaxSize(options.maxSize),
  73. max_logs: options.maxFiles,
  74. end_stream: true,
  75. audit_file: options.auditFile ? options.auditFile : path.join(self.dirname, '.' + hash(options) + '-audit.json'),
  76. file_options: options.options ? options.options : {flags: 'a'},
  77. utc: options.utc ? options.utc : false,
  78. extension: options.extension ? options.extension : '',
  79. create_symlink: options.createSymlink ? options.createSymlink : false,
  80. symlink_name: options.symlinkName ? options.symlinkName : 'current.log',
  81. watch_log: options.watchLog ? options.watchLog : false,
  82. audit_hash_type: options.auditHashType ? options.auditHashType : 'sha256'
  83. });
  84. this.logStream.on('new', function (newFile) {
  85. self.emit('new', newFile);
  86. });
  87. this.logStream.on('rotate', function (oldFile, newFile) {
  88. self.emit('rotate', oldFile, newFile);
  89. });
  90. this.logStream.on('logRemoved', function (params) {
  91. if (options.zippedArchive) {
  92. var gzName = params.name + '.gz';
  93. if (fs.existsSync(gzName)) {
  94. try {
  95. fs.unlinkSync(gzName);
  96. }
  97. catch (_err) {
  98. // file is there but we got an error when trying to delete,
  99. // so permissions problem or concurrency issue and another
  100. // process already deleted it we could detect the concurrency
  101. // issue by checking err.type === ENOENT or EACCESS for
  102. // permissions ... but then?
  103. }
  104. self.emit('logRemoved', gzName);
  105. return;
  106. }
  107. }
  108. self.emit('logRemoved', params.name);
  109. });
  110. if (options.zippedArchive) {
  111. this.logStream.on('rotate', function (oldFile) {
  112. var oldFileExist = fs.existsSync(oldFile);
  113. var gzExist = fs.existsSync(oldFile + '.gz');
  114. if (!oldFileExist || gzExist) {
  115. return;
  116. }
  117. var gzip = zlib.createGzip();
  118. var inp = fs.createReadStream(oldFile);
  119. var out = fs.createWriteStream(oldFile + '.gz');
  120. inp.pipe(gzip).pipe(out).on('finish', function () {
  121. if (fs.existsSync(oldFile)) {
  122. fs.unlinkSync(oldFile);
  123. }
  124. self.emit('archive', oldFile + '.gz');
  125. });
  126. });
  127. }
  128. if (options.watchLog) {
  129. this.logStream.on('addWatcher', (newFile) => {
  130. self.emit('addWatcher', newFile);
  131. })
  132. }
  133. }
  134. };
  135. module.exports = DailyRotateFile;
  136. util.inherits(DailyRotateFile, Transport);
  137. DailyRotateFile.prototype.name = 'dailyRotateFile';
  138. var noop = function () {};
  139. DailyRotateFile.prototype.log = function (info, callback) {
  140. callback = callback || noop;
  141. this.logStream.write(info[MESSAGE] + this.options.eol);
  142. this.emit('logged', info);
  143. callback(null, true);
  144. };
  145. DailyRotateFile.prototype.close = function () {
  146. var self = this;
  147. if (this.logStream) {
  148. this.logStream.end(function () {
  149. self.emit('finish');
  150. });
  151. }
  152. };
  153. DailyRotateFile.prototype.query = function (options, callback) {
  154. if (typeof options === 'function') {
  155. callback = options;
  156. options = {};
  157. }
  158. if (!this.options.json) {
  159. throw new Error('query() may not be used without the json option being set to true');
  160. }
  161. if (!this.filename) {
  162. throw new Error('query() may not be used when initializing with a stream');
  163. }
  164. var self = this;
  165. var results = [];
  166. options = options || {};
  167. // limit
  168. options.rows = options.rows || options.limit || 10;
  169. // starting row offset
  170. options.start = options.start || 0;
  171. // now
  172. options.until = options.until || new Date;
  173. if (typeof options.until !== 'object') {
  174. options.until = new Date(options.until);
  175. }
  176. // now - 24
  177. options.from = options.from || (options.until - (24 * 60 * 60 * 1000));
  178. if (typeof options.from !== 'object') {
  179. options.from = new Date(options.from);
  180. }
  181. // 'asc' or 'desc'
  182. options.order = options.order || 'desc';
  183. var logFiles = (function () {
  184. var fileRegex = new RegExp(self.filename.replace('%DATE%', '.*'), 'i');
  185. return fs.readdirSync(self.dirname).filter(function (file) {
  186. return path.basename(file).match(fileRegex);
  187. });
  188. })();
  189. if (logFiles.length === 0 && callback) {
  190. callback(null, results);
  191. }
  192. (function processLogFile(file) {
  193. if (!file) {
  194. return;
  195. }
  196. var logFile = path.join(self.dirname, file);
  197. var buff = '';
  198. var stream;
  199. if (file.endsWith('.gz')) {
  200. stream = new PassThrough();
  201. fs.createReadStream(logFile).pipe(zlib.createGunzip()).pipe(stream);
  202. } else {
  203. stream = fs.createReadStream(logFile, {
  204. encoding: 'utf8'
  205. });
  206. }
  207. stream.on('error', function (err) {
  208. if (stream.readable) {
  209. stream.destroy();
  210. }
  211. if (!callback) {
  212. return;
  213. }
  214. return err.code === 'ENOENT' ? callback(null, results) : callback(err);
  215. });
  216. stream.on('data', function (data) {
  217. data = (buff + data).split(/\n+/);
  218. var l = data.length - 1;
  219. for (var i = 0; i < l; i++) {
  220. add(data[i]);
  221. }
  222. buff = data[l];
  223. });
  224. stream.on('end', function () {
  225. if (buff) {
  226. add(buff, true);
  227. }
  228. if (logFiles.length) {
  229. processLogFile(logFiles.shift());
  230. } else if (callback) {
  231. results.sort(function (a, b) {
  232. var d1 = new Date(a.timestamp).getTime();
  233. var d2 = new Date(b.timestamp).getTime();
  234. return d1 > d2 ? 1 : d1 < d2 ? -1 : 0;
  235. });
  236. if (options.order === 'desc') {
  237. results = results.reverse();
  238. }
  239. var start = options.start || 0;
  240. var limit = options.limit || results.length;
  241. results = results.slice(start, start + limit);
  242. if (options.fields) {
  243. results = results.map(function (log) {
  244. var obj = {};
  245. options.fields.forEach(function (key) {
  246. obj[key] = log[key];
  247. });
  248. return obj;
  249. });
  250. }
  251. callback(null, results);
  252. }
  253. });
  254. function add(buff, attempt) {
  255. try {
  256. var log = JSON.parse(buff);
  257. if (!log || typeof log !== 'object') {
  258. return;
  259. }
  260. var time = new Date(log.timestamp);
  261. if ((options.from && time < options.from) ||
  262. (options.until && time > options.until) ||
  263. (options.level && options.level !== log.level)) {
  264. return;
  265. }
  266. results.push(log);
  267. } catch (e) {
  268. if (!attempt) {
  269. stream.emit('error', e);
  270. }
  271. }
  272. }
  273. })(logFiles.shift());
  274. };