logger.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. // Copyright IBM Corp. 2014,2018. All Rights Reserved.
  2. // Node module: strong-log-transformer
  3. // This file is licensed under the Apache License 2.0.
  4. // License text available at https://opensource.org/licenses/Apache-2.0
  5. 'use strict';
  6. var stream = require('stream');
  7. var util = require('util');
  8. var fs = require('fs');
  9. var through = require('through');
  10. var duplexer = require('duplexer');
  11. var StringDecoder = require('string_decoder').StringDecoder;
  12. module.exports = Logger;
  13. Logger.DEFAULTS = {
  14. format: 'text',
  15. tag: '',
  16. mergeMultiline: false,
  17. timeStamp: false,
  18. };
  19. var formatters = {
  20. text: textFormatter,
  21. json: jsonFormatter,
  22. }
  23. function Logger(options) {
  24. var defaults = JSON.parse(JSON.stringify(Logger.DEFAULTS));
  25. options = util._extend(defaults, options || {});
  26. var catcher = deLiner();
  27. var emitter = catcher;
  28. var transforms = [
  29. objectifier(),
  30. ];
  31. if (options.tag) {
  32. transforms.push(staticTagger(options.tag));
  33. }
  34. if (options.mergeMultiline) {
  35. transforms.push(lineMerger());
  36. }
  37. // TODO
  38. // if (options.pidStamp) {
  39. // transforms.push(pidStamper(options.pid));
  40. // }
  41. // TODO
  42. // if (options.workerStamp) {
  43. // transforms.push(workerStamper(options.worker));
  44. // }
  45. transforms.push(formatters[options.format](options));
  46. // restore line endings that were removed by line splitting
  47. transforms.push(reLiner());
  48. for (var t in transforms) {
  49. emitter = emitter.pipe(transforms[t]);
  50. }
  51. return duplexer(catcher, emitter);
  52. }
  53. function deLiner() {
  54. var decoder = new StringDecoder('utf8');
  55. var last = '';
  56. return new stream.Transform({
  57. transform(chunk, _enc, callback) {
  58. last += decoder.write(chunk);
  59. var list = last.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
  60. last = list.pop();
  61. for (var i = 0; i < list.length; i++) {
  62. // swallow empty lines
  63. if (list[i]) {
  64. this.push(list[i]);
  65. }
  66. }
  67. callback();
  68. },
  69. flush(callback) {
  70. // incomplete UTF8 sequences become UTF8 replacement characters
  71. last += decoder.end();
  72. if (last) {
  73. this.push(last);
  74. }
  75. callback();
  76. },
  77. });
  78. }
  79. function reLiner() {
  80. return through(appendNewline);
  81. function appendNewline(line) {
  82. this.emit('data', line + '\n');
  83. }
  84. }
  85. function objectifier() {
  86. return through(objectify, null, {autoDestroy: false});
  87. function objectify(line) {
  88. this.emit('data', {
  89. msg: line,
  90. time: Date.now(),
  91. });
  92. }
  93. }
  94. function staticTagger(tag) {
  95. return through(tagger);
  96. function tagger(logEvent) {
  97. logEvent.tag = tag;
  98. this.emit('data', logEvent);
  99. }
  100. }
  101. function textFormatter(options) {
  102. return through(textify);
  103. function textify(logEvent) {
  104. var line = util.format('%s%s', textifyTags(logEvent.tag),
  105. logEvent.msg.toString());
  106. if (options.timeStamp) {
  107. line = util.format('%s %s', new Date(logEvent.time).toISOString(), line);
  108. }
  109. this.emit('data', line.replace(/\n/g, '\\n'));
  110. }
  111. function textifyTags(tags) {
  112. var str = '';
  113. if (typeof tags === 'string') {
  114. str = tags + ' ';
  115. } else if (typeof tags === 'object') {
  116. for (var t in tags) {
  117. str += t + ':' + tags[t] + ' ';
  118. }
  119. }
  120. return str;
  121. }
  122. }
  123. function jsonFormatter(options) {
  124. return through(jsonify);
  125. function jsonify(logEvent) {
  126. if (options.timeStamp) {
  127. logEvent.time = new Date(logEvent.time).toISOString();
  128. } else {
  129. delete logEvent.time;
  130. }
  131. logEvent.msg = logEvent.msg.toString();
  132. this.emit('data', JSON.stringify(logEvent));
  133. }
  134. }
  135. function lineMerger(host) {
  136. var previousLine = null;
  137. var flushTimer = null;
  138. var stream = through(lineMergerWrite, lineMergerEnd);
  139. var flush = _flush.bind(stream);
  140. return stream;
  141. function lineMergerWrite(line) {
  142. if (/^\s+/.test(line.msg)) {
  143. if (previousLine) {
  144. previousLine.msg += '\n' + line.msg;
  145. } else {
  146. previousLine = line;
  147. }
  148. } else {
  149. flush();
  150. previousLine = line;
  151. }
  152. // rolling timeout
  153. clearTimeout(flushTimer);
  154. flushTimer = setTimeout(flush.bind(this), 10);
  155. }
  156. function _flush() {
  157. if (previousLine) {
  158. this.emit('data', previousLine);
  159. previousLine = null;
  160. }
  161. }
  162. function lineMergerEnd() {
  163. flush.call(this);
  164. this.emit('end');
  165. }
  166. }