Logger.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /*
  2. Журналирование с буферизацией вывода
  3. */
  4. const Promise = require('bluebird');
  5. const fs = Promise.promisifyAll(require('fs'));
  6. global.LM_OK = 0;
  7. global.LM_INFO = 1;
  8. global.LM_WARN = 2;
  9. global.LM_ERR = 3;
  10. global.LM_FATAL = 4;
  11. global.LM_TOTAL = 5;
  12. const LOG_CACHE_BUFFER_SIZE = 8192;
  13. const LOG_BUFFER_FLUSH_INTERVAL = 200;
  14. const LOG_ROTATE_FILE_LENGTH = 1000000;
  15. const LOG_ROTATE_FILE_DEPTH = 9;
  16. const LOG_ROTATE_FILE_CHECK_INTERVAL = 60000;
  17. let msgTypeToStr = {
  18. [LM_OK]: ' OK',
  19. [LM_INFO]: ' INFO',
  20. [LM_WARN]: ' WARN',
  21. [LM_ERR]: 'ERROR',
  22. [LM_FATAL]: 'FATAL ERROR',
  23. [LM_TOTAL]: 'TOTAL'
  24. };
  25. class BaseLog {
  26. constructor(params) {
  27. this.params = params;
  28. this.exclude = new Set(params.exclude);
  29. this.outputBufferLength = 0;
  30. this.outputBuffer = [];
  31. this.flushing = false;
  32. }
  33. async flush() {
  34. if (this.flushing || !this.outputBufferLength)
  35. return;
  36. this.flushing = true;
  37. this.data = this.outputBuffer;
  38. this.outputBufferLength = 0;
  39. this.outputBuffer = [];
  40. await this.flushImpl(this.data)
  41. .catch(e => { console.log(e); process.exit(1); } );
  42. this.flushing = false;
  43. }
  44. log(msgType, message) {
  45. if (this.closed) { console.log(`Logger fatal error: log was closed (message to log: ${message}})`); process.exit(1); }
  46. if (!this.exclude.has(msgType)) {
  47. this.outputBuffer.push(message);
  48. this.outputBufferLength += message.length;
  49. if (this.outputBufferLength >= LOG_CACHE_BUFFER_SIZE && !this.flushing) {
  50. this.flush();
  51. }
  52. if (!this.iid) {
  53. this.iid = setInterval(() => {
  54. if (!this.flushing) {
  55. clearInterval(this.iid);
  56. this.iid = 0;
  57. this.flush();
  58. }
  59. }, LOG_BUFFER_FLUSH_INTERVAL);
  60. }
  61. }
  62. }
  63. close() {
  64. if (this.closed)
  65. return;
  66. if (this.iid)
  67. clearInterval(this.iid);
  68. try {
  69. if (this.flushing)
  70. this.flushImplSync(this.data);
  71. this.flushImplSync(this.outputBuffer);
  72. } catch(e) {
  73. console.log(e);
  74. process.exit(1);
  75. }
  76. this.outputBufferLength = 0;
  77. this.outputBuffer = [];
  78. this.closed = true;
  79. }
  80. }
  81. class FileLog extends BaseLog {
  82. constructor(params) {
  83. super(params);
  84. this.fileName = params.fileName;
  85. this.fd = fs.openSync(this.fileName, 'a');
  86. this.rcid = 0;
  87. }
  88. close() {
  89. if (this.closed)
  90. return;
  91. super.close();
  92. if (this.fd)
  93. fs.closeSync(this.fd);
  94. if (this.rcid)
  95. clearTimeout(this.rcid);
  96. }
  97. async rotateFile(fileName, i) {
  98. let fn = fileName;
  99. if (i > 0)
  100. fn += `.${i}`;
  101. let tn = fileName + '.' + (i + 1);
  102. let exists = await fs.accessAsync(tn).then(() => true).catch(() => false);
  103. if (exists) {
  104. if (i >= LOG_ROTATE_FILE_DEPTH - 1) {
  105. await fs.unlinkAsync(tn);
  106. } else {
  107. await this.rotateFile(fileName, i + 1);
  108. }
  109. }
  110. await fs.renameAsync(fn, tn);
  111. }
  112. async doFileRotationIfNeeded() {
  113. this.rcid = 0;
  114. let stat = await fs.fstatAsync(this.fd);
  115. if (stat.size > LOG_ROTATE_FILE_LENGTH) {
  116. await fs.closeAsync(this.fd);
  117. await this.rotateFile(this.fileName, 0);
  118. this.fd = await fs.openAsync(this.fileName, "a");
  119. }
  120. }
  121. async flushImpl(data) {
  122. if (this.closed)
  123. return;
  124. if (!this.rcid) {
  125. await this.doFileRotationIfNeeded();
  126. this.rcid = setTimeout(() => {
  127. this.rcid = 0;
  128. }, LOG_ROTATE_FILE_CHECK_INTERVAL);
  129. };
  130. await fs.writeAsync(this.fd, Buffer.from(data.join('')));
  131. }
  132. flushImplSync(data) {
  133. fs.writeSync(this.fd, Buffer.from(data.join('')));
  134. }
  135. }
  136. class ConsoleLog extends BaseLog {
  137. async flushImpl(data) {
  138. process.stdout.write(data.join(''));
  139. }
  140. flushImplSync(data) {
  141. process.stdout.write(data.join(''));
  142. }
  143. }
  144. //------------------------------------------------------------------
  145. const factory = {
  146. ConsoleLog,
  147. FileLog,
  148. };
  149. class Logger {
  150. constructor(params = null, cleanupCallback = null) {
  151. this.handlers = [];
  152. if (params) {
  153. params.forEach((logParams) => {
  154. let className = logParams.log;
  155. let loggerClass = factory[className];
  156. this.handlers.push(new loggerClass(logParams));
  157. });
  158. } else {
  159. this.handlers.push(new DummyLog);
  160. }
  161. cleanupCallback = cleanupCallback || (() => {});
  162. this.cleanup(cleanupCallback);
  163. }
  164. prepareMessage(msgType, message) {
  165. return (new Date().toISOString()) + ` ${msgTypeToStr[msgType]}: ${message}\n`;
  166. }
  167. log(msgType, message) {
  168. if (message == null) {
  169. message = msgType;
  170. msgType = LM_INFO;
  171. }
  172. const mes = this.prepareMessage(msgType, message);
  173. for (let i = 0; i < this.handlers.length; i++)
  174. this.handlers[i].log(msgType, mes);
  175. }
  176. close() {
  177. for (let i = 0; i < this.handlers.length; i++)
  178. this.handlers[i].close();
  179. }
  180. cleanup(callback) {
  181. // attach user callback to the process event emitter
  182. // if no callback, it will still exit gracefully on Ctrl-C
  183. callback = callback || (() => {});
  184. process.on('cleanup', callback);
  185. // do app specific cleaning before exiting
  186. process.on('exit', () => {
  187. this.close();
  188. process.emit('cleanup');
  189. });
  190. // catch ctrl+c event and exit normally
  191. process.on('SIGINT', () => {
  192. this.log(LM_WARN, 'Ctrl-C pressed, exiting...');
  193. process.exit(2);
  194. });
  195. process.on('SIGTERM', () => {
  196. this.log(LM_WARN, 'Kill signal, exiting...');
  197. process.exit(2);
  198. });
  199. //catch uncaught exceptions, trace, then exit normally
  200. process.on('uncaughtException', e => {
  201. try {
  202. this.log(LM_FATAL, e.stack);
  203. } catch (e) {
  204. console.log(e.stack);
  205. }
  206. process.exit(99);
  207. });
  208. }
  209. }
  210. module.exports = Logger;