Logger.js 6.3 KB

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