log.js 8.8 KB

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