log.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. arg.stack = stack = arg.stack + ''
  159. }
  160. }
  161. if (stack) a.unshift(stack + '\n')
  162. message = util.format.apply(util, a)
  163. var m = { id: id++,
  164. level: lvl,
  165. prefix: String(prefix || ''),
  166. message: message,
  167. messageRaw: a }
  168. this.emit('log', m)
  169. this.emit('log.' + lvl, m)
  170. if (m.prefix) this.emit(m.prefix, m)
  171. this.record.push(m)
  172. var mrs = this.maxRecordSize
  173. var n = this.record.length - mrs
  174. if (n > mrs / 10) {
  175. var newSize = Math.floor(mrs * 0.9)
  176. this.record = this.record.slice(-1 * newSize)
  177. }
  178. this.emitLog(m)
  179. }.bind(log)
  180. log.emitLog = function (m) {
  181. if (this._paused) {
  182. this._buffer.push(m)
  183. return
  184. }
  185. if (this.progressEnabled) this.gauge.pulse(m.prefix)
  186. var l = this.levels[m.level]
  187. if (l === undefined) return
  188. if (l < this.levels[this.level]) return
  189. if (l > 0 && !isFinite(l)) return
  190. // If 'disp' is null or undefined, use the lvl as a default
  191. // Allows: '', 0 as valid disp
  192. var disp = log.disp[m.level] != null ? log.disp[m.level] : m.level
  193. this.clearProgress()
  194. m.message.split(/\r?\n/).forEach(function (line) {
  195. if (this.heading) {
  196. this.write(this.heading, this.headingStyle)
  197. this.write(' ')
  198. }
  199. this.write(disp, log.style[m.level])
  200. var p = m.prefix || ''
  201. if (p) this.write(' ')
  202. this.write(p, this.prefixStyle)
  203. this.write(' ' + line + '\n')
  204. }, this)
  205. this.showProgress()
  206. }
  207. log._format = function (msg, style) {
  208. if (!stream) return
  209. var output = ''
  210. if (this.useColor()) {
  211. style = style || {}
  212. var settings = []
  213. if (style.fg) settings.push(style.fg)
  214. if (style.bg) settings.push('bg' + style.bg[0].toUpperCase() + style.bg.slice(1))
  215. if (style.bold) settings.push('bold')
  216. if (style.underline) settings.push('underline')
  217. if (style.inverse) settings.push('inverse')
  218. if (settings.length) output += consoleControl.color(settings)
  219. if (style.beep) output += consoleControl.beep()
  220. }
  221. output += msg
  222. if (this.useColor()) {
  223. output += consoleControl.color('reset')
  224. }
  225. return output
  226. }
  227. log.write = function (msg, style) {
  228. if (!stream) return
  229. stream.write(this._format(msg, style))
  230. }
  231. log.addLevel = function (lvl, n, style, disp) {
  232. // If 'disp' is null or undefined, use the lvl as a default
  233. if (disp == null) disp = lvl
  234. this.levels[lvl] = n
  235. this.style[lvl] = style
  236. if (!this[lvl]) {
  237. this[lvl] = function () {
  238. var a = new Array(arguments.length + 1)
  239. a[0] = lvl
  240. for (var i = 0; i < arguments.length; i++) {
  241. a[i + 1] = arguments[i]
  242. }
  243. return this.log.apply(this, a)
  244. }.bind(this)
  245. }
  246. this.disp[lvl] = disp
  247. }
  248. log.prefixStyle = { fg: 'magenta' }
  249. log.headingStyle = { fg: 'white', bg: 'black' }
  250. log.style = {}
  251. log.levels = {}
  252. log.disp = {}
  253. log.addLevel('silly', -Infinity, { inverse: true }, 'sill')
  254. log.addLevel('verbose', 1000, { fg: 'blue', bg: 'black' }, 'verb')
  255. log.addLevel('info', 2000, { fg: 'green' })
  256. log.addLevel('http', 3000, { fg: 'green', bg: 'black' })
  257. log.addLevel('warn', 4000, { fg: 'black', bg: 'yellow' }, 'WARN')
  258. log.addLevel('error', 5000, { fg: 'red', bg: 'black' }, 'ERR!')
  259. log.addLevel('silent', Infinity)
  260. // allow 'error' prefix
  261. log.on('error', function () {})