log.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. 'use strict'
  2. var Progress = require('are-we-there-yet')
  3. var Gauge = require('gauge')
  4. var EE = require('events').EventEmitter
  5. var log = exports = module.exports = new EE()
  6. var util = require('util')
  7. var setBlocking = require('set-blocking')
  8. var consoleControl = require('console-control-strings')
  9. setBlocking(true)
  10. var stream = process.stderr
  11. Object.defineProperty(log, 'stream', {
  12. set: function (newStream) {
  13. stream = newStream
  14. if (this.gauge) this.gauge.setWriteTo(stream, stream)
  15. },
  16. get: function () {
  17. return stream
  18. }
  19. })
  20. // by default, decide based on tty-ness.
  21. var colorEnabled
  22. log.useColor = function () {
  23. return colorEnabled != null ? colorEnabled : stream.isTTY
  24. }
  25. log.enableColor = function () {
  26. colorEnabled = true
  27. this.gauge.setTheme({hasColor: colorEnabled, hasUnicode: unicodeEnabled})
  28. }
  29. log.disableColor = function () {
  30. colorEnabled = false
  31. this.gauge.setTheme({hasColor: colorEnabled, hasUnicode: unicodeEnabled})
  32. }
  33. // default level
  34. log.level = 'info'
  35. log.gauge = new Gauge(stream, {
  36. enabled: false, // no progress bars unless asked
  37. theme: {hasColor: log.useColor()},
  38. template: [
  39. {type: 'progressbar', length: 20},
  40. {type: 'activityIndicator', kerning: 1, length: 1},
  41. {type: 'section', default: ''},
  42. ':',
  43. {type: 'logline', kerning: 1, default: ''}
  44. ]
  45. })
  46. log.tracker = new Progress.TrackerGroup()
  47. // we track this separately as we may need to temporarily disable the
  48. // display of the status bar for our own loggy purposes.
  49. log.progressEnabled = log.gauge.isEnabled()
  50. var unicodeEnabled
  51. log.enableUnicode = function () {
  52. unicodeEnabled = true
  53. this.gauge.setTheme({hasColor: this.useColor(), hasUnicode: unicodeEnabled})
  54. }
  55. log.disableUnicode = function () {
  56. unicodeEnabled = false
  57. this.gauge.setTheme({hasColor: this.useColor(), hasUnicode: unicodeEnabled})
  58. }
  59. log.setGaugeThemeset = function (themes) {
  60. this.gauge.setThemeset(themes)
  61. }
  62. log.setGaugeTemplate = function (template) {
  63. this.gauge.setTemplate(template)
  64. }
  65. log.enableProgress = function () {
  66. if (this.progressEnabled) return
  67. this.progressEnabled = true
  68. this.tracker.on('change', this.showProgress)
  69. if (this._pause) return
  70. this.gauge.enable()
  71. }
  72. log.disableProgress = function () {
  73. if (!this.progressEnabled) return
  74. this.progressEnabled = false
  75. this.tracker.removeListener('change', this.showProgress)
  76. this.gauge.disable()
  77. }
  78. var trackerConstructors = ['newGroup', 'newItem', 'newStream']
  79. var mixinLog = function (tracker) {
  80. // mixin the public methods from log into the tracker
  81. // (except: conflicts and one's we handle specially)
  82. Object.keys(log).forEach(function (P) {
  83. if (P[0] === '_') return
  84. if (trackerConstructors.filter(function (C) { return C === P }).length) return
  85. if (tracker[P]) return
  86. if (typeof log[P] !== 'function') return
  87. var func = log[P]
  88. tracker[P] = function () {
  89. return func.apply(log, arguments)
  90. }
  91. })
  92. // if the new tracker is a group, make sure any subtrackers get
  93. // mixed in too
  94. if (tracker instanceof Progress.TrackerGroup) {
  95. trackerConstructors.forEach(function (C) {
  96. var func = tracker[C]
  97. tracker[C] = function () { return mixinLog(func.apply(tracker, arguments)) }
  98. })
  99. }
  100. return tracker
  101. }
  102. // Add tracker constructors to the top level log object
  103. trackerConstructors.forEach(function (C) {
  104. log[C] = function () { return mixinLog(this.tracker[C].apply(this.tracker, arguments)) }
  105. })
  106. log.clearProgress = function (cb) {
  107. if (!this.progressEnabled) return cb && process.nextTick(cb)
  108. this.gauge.hide(cb)
  109. }
  110. log.showProgress = function (name, completed) {
  111. if (!this.progressEnabled) return
  112. var values = {}
  113. if (name) values.section = name
  114. var last = log.record[log.record.length - 1]
  115. if (last) {
  116. values.subsection = last.prefix
  117. var disp = log.disp[last.level] || last.level
  118. var logline = this._format(disp, log.style[last.level])
  119. if (last.prefix) logline += ' ' + this._format(last.prefix, this.prefixStyle)
  120. logline += ' ' + last.message.split(/\r?\n/)[0]
  121. values.logline = logline
  122. }
  123. values.completed = completed || this.tracker.completed()
  124. this.gauge.show(values)
  125. }.bind(log) // bind for use in tracker's on-change listener
  126. // temporarily stop emitting, but don't drop
  127. log.pause = function () {
  128. this._paused = true
  129. if (this.progressEnabled) this.gauge.disable()
  130. }
  131. log.resume = function () {
  132. if (!this._paused) return
  133. this._paused = false
  134. var b = this._buffer
  135. this._buffer = []
  136. b.forEach(function (m) {
  137. this.emitLog(m)
  138. }, this)
  139. if (this.progressEnabled) this.gauge.enable()
  140. }
  141. log._buffer = []
  142. var id = 0
  143. log.record = []
  144. log.maxRecordSize = 10000
  145. log.log = function (lvl, prefix, message) {
  146. var l = this.levels[lvl]
  147. if (l === undefined) {
  148. return this.emit('error', new Error(util.format(
  149. 'Undefined log level: %j', lvl)))
  150. }
  151. var a = new Array(arguments.length - 2)
  152. var stack = null
  153. for (var i = 2; i < arguments.length; i++) {
  154. var arg = a[i - 2] = arguments[i]
  155. // resolve stack traces to a plain string.
  156. if (typeof arg === 'object' && arg &&
  157. (arg instanceof Error) && arg.stack) {
  158. Object.defineProperty(arg, 'stack', {
  159. value: stack = arg.stack + '',
  160. enumerable: true,
  161. writable: true
  162. })
  163. }
  164. }
  165. if (stack) a.unshift(stack + '\n')
  166. message = util.format.apply(util, a)
  167. var m = { id: id++,
  168. level: lvl,
  169. prefix: String(prefix || ''),
  170. message: message,
  171. messageRaw: a }
  172. this.emit('log', m)
  173. this.emit('log.' + lvl, m)
  174. if (m.prefix) this.emit(m.prefix, m)
  175. this.record.push(m)
  176. var mrs = this.maxRecordSize
  177. var n = this.record.length - mrs
  178. if (n > mrs / 10) {
  179. var newSize = Math.floor(mrs * 0.9)
  180. this.record = this.record.slice(-1 * newSize)
  181. }
  182. this.emitLog(m)
  183. }.bind(log)
  184. log.emitLog = function (m) {
  185. if (this._paused) {
  186. this._buffer.push(m)
  187. return
  188. }
  189. if (this.progressEnabled) this.gauge.pulse(m.prefix)
  190. var l = this.levels[m.level]
  191. if (l === undefined) return
  192. if (l < this.levels[this.level]) return
  193. if (l > 0 && !isFinite(l)) return
  194. // If 'disp' is null or undefined, use the lvl as a default
  195. // Allows: '', 0 as valid disp
  196. var disp = log.disp[m.level] != null ? log.disp[m.level] : m.level
  197. this.clearProgress()
  198. m.message.split(/\r?\n/).forEach(function (line) {
  199. if (this.heading) {
  200. this.write(this.heading, this.headingStyle)
  201. this.write(' ')
  202. }
  203. this.write(disp, log.style[m.level])
  204. var p = m.prefix || ''
  205. if (p) this.write(' ')
  206. this.write(p, this.prefixStyle)
  207. this.write(' ' + line + '\n')
  208. }, this)
  209. this.showProgress()
  210. }
  211. log._format = function (msg, style) {
  212. if (!stream) return
  213. var output = ''
  214. if (this.useColor()) {
  215. style = style || {}
  216. var settings = []
  217. if (style.fg) settings.push(style.fg)
  218. if (style.bg) settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1))
  219. if (style.bold) settings.push('bold')
  220. if (style.underline) settings.push('underline')
  221. if (style.inverse) settings.push('inverse')
  222. if (settings.length) output += consoleControl.color(settings)
  223. if (style.beep) output += consoleControl.beep()
  224. }
  225. output += msg
  226. if (this.useColor()) {
  227. output += consoleControl.color('reset')
  228. }
  229. return output
  230. }
  231. log.write = function (msg, style) {
  232. if (!stream) return
  233. stream.write(this._format(msg, style))
  234. }
  235. log.addLevel = function (lvl, n, style, disp) {
  236. // If 'disp' is null or undefined, use the lvl as a default
  237. if (disp == null) disp = lvl
  238. this.levels[lvl] = n
  239. this.style[lvl] = style
  240. if (!this[lvl]) {
  241. this[lvl] = function () {
  242. var a = new Array(arguments.length + 1)
  243. a[0] = lvl
  244. for (var i = 0; i < arguments.length; i++) {
  245. a[i + 1] = arguments[i]
  246. }
  247. return this.log.apply(this, a)
  248. }.bind(this)
  249. }
  250. this.disp[lvl] = disp
  251. }
  252. log.prefixStyle = { fg: 'magenta' }
  253. log.headingStyle = { fg: 'white', bg: 'black' }
  254. log.style = {}
  255. log.levels = {}
  256. log.disp = {}
  257. log.addLevel('silly', -Infinity, { inverse: true }, 'sill')
  258. log.addLevel('verbose', 1000, { fg: 'blue', bg: 'black' }, 'verb')
  259. log.addLevel('info', 2000, { fg: 'green' })
  260. log.addLevel('timing', 2500, { fg: 'green', bg: 'black' })
  261. log.addLevel('http', 3000, { fg: 'green', bg: 'black' })
  262. log.addLevel('notice', 3500, { fg: 'blue', bg: 'black' })
  263. log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN')
  264. log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!')
  265. log.addLevel('silent', Infinity)
  266. // allow 'error' prefix
  267. log.on('error', function () {})