daily-rotate-file.js 12 KB

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