logging-manager.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const bunyan = require('bunyan')
  2. const request = require('request')
  3. const Logger = module.exports = {
  4. initialize(name) {
  5. this.isProduction =
  6. (process.env['NODE_ENV'] || '').toLowerCase() === 'production'
  7. this.defaultLevel =
  8. process.env['LOG_LEVEL'] || (this.isProduction ? 'warn' : 'debug')
  9. this.loggerName = name
  10. this.ringBufferSize = parseInt(process.env['LOG_RING_BUFFER_SIZE']) || 0
  11. const loggerStreams = [
  12. {
  13. level: this.defaultLevel,
  14. stream: process.stdout
  15. }
  16. ]
  17. if (this.ringBufferSize > 0) {
  18. this.ringBuffer = new bunyan.RingBuffer({limit: this.ringBufferSize})
  19. loggerStreams.push({
  20. level: 'trace',
  21. type: 'raw',
  22. stream: this.ringBuffer
  23. })
  24. }
  25. else {
  26. this.ringBuffer = null
  27. }
  28. this.logger = bunyan.createLogger({
  29. name,
  30. serializers: bunyan.stdSerializers,
  31. streams: loggerStreams
  32. })
  33. if (this.isProduction) {
  34. // clear interval if already set
  35. if (this.checkInterval) {
  36. clearInterval(this.checkInterval)
  37. }
  38. // check for log level override on startup
  39. this.checkLogLevel()
  40. // re-check log level every minute
  41. const checkLogLevel = () => this.checkLogLevel()
  42. this.checkInterval = setInterval(checkLogLevel, 1000 * 60)
  43. }
  44. return this
  45. },
  46. checkLogLevel() {
  47. const options = {
  48. headers: {
  49. 'Metadata-Flavor': 'Google'
  50. },
  51. uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${
  52. this.loggerName
  53. }-setLogLevelEndTime`
  54. }
  55. request(options, (err, response, body) => {
  56. if (err) {
  57. this.logger.level(this.defaultLevel)
  58. return
  59. }
  60. if (parseInt(body) > Date.now()) {
  61. this.logger.level('trace')
  62. } else {
  63. this.logger.level(this.defaultLevel)
  64. }
  65. })
  66. },
  67. initializeErrorReporting(sentry_dsn, options) {
  68. const raven = require('raven')
  69. this.raven = new raven.Client(sentry_dsn, options)
  70. this.lastErrorTimeStamp = 0 // for rate limiting on sentry reporting
  71. this.lastErrorCount = 0
  72. },
  73. captureException(attributes, message, level) {
  74. // handle case of logger.error "message"
  75. let key, value
  76. if (typeof attributes === 'string') {
  77. attributes = { err: new Error(attributes) }
  78. }
  79. // extract any error object
  80. let error = attributes.err || attributes.error
  81. // avoid reporting errors twice
  82. for (key in attributes) {
  83. value = attributes[key]
  84. if (value instanceof Error && value.reportedToSentry) {
  85. return
  86. }
  87. }
  88. // include our log message in the error report
  89. if (error == null) {
  90. if (typeof message === 'string') {
  91. error = { message }
  92. }
  93. } else if (message != null) {
  94. attributes.description = message
  95. }
  96. // report the error
  97. if (error != null) {
  98. // capture attributes and use *_id objects as tags
  99. const tags = {}
  100. const extra = {}
  101. for (key in attributes) {
  102. value = attributes[key]
  103. if (key.match(/_id/) && typeof value === 'string') {
  104. tags[key] = value
  105. }
  106. extra[key] = value
  107. }
  108. // capture req object if available
  109. const { req } = attributes
  110. if (req != null) {
  111. extra.req = {
  112. method: req.method,
  113. url: req.originalUrl,
  114. query: req.query,
  115. headers: req.headers,
  116. ip: req.ip
  117. }
  118. }
  119. // recreate error objects that have been converted to a normal object
  120. if (!(error instanceof Error) && typeof error === 'object') {
  121. const newError = new Error(error.message)
  122. for (key of Object.keys(error || {})) {
  123. value = error[key]
  124. newError[key] = value
  125. }
  126. error = newError
  127. }
  128. // filter paths from the message to avoid duplicate errors in sentry
  129. // (e.g. errors from `fs` methods which have a path attribute)
  130. try {
  131. if (error.path) {
  132. error.message = error.message.replace(` '${error.path}'`, '')
  133. }
  134. } catch (error1) {}
  135. // send the error to sentry
  136. try {
  137. this.raven.captureException(error, { tags, extra, level })
  138. // put a flag on the errors to avoid reporting them multiple times
  139. return (() => {
  140. const result = []
  141. for (key in attributes) {
  142. value = attributes[key]
  143. if (value instanceof Error) {
  144. result.push((value.reportedToSentry = true))
  145. } else {
  146. result.push(undefined)
  147. }
  148. }
  149. return result
  150. })()
  151. } catch (error2) {
  152. return
  153. }
  154. }
  155. },
  156. debug() {
  157. return this.logger.debug.apply(this.logger, arguments)
  158. },
  159. info() {
  160. return this.logger.info.apply(this.logger, arguments)
  161. },
  162. log() {
  163. return this.logger.info.apply(this.logger, arguments)
  164. },
  165. error(attributes, message, ...args) {
  166. if (this.ringBuffer !== null && Array.isArray(this.ringBuffer.records)) {
  167. attributes.logBuffer = this.ringBuffer.records.filter(function (record) {
  168. return record.level !== 50
  169. })
  170. }
  171. this.logger.error(attributes, message, ...Array.from(args))
  172. if (this.raven != null) {
  173. const MAX_ERRORS = 5 // maximum number of errors in 1 minute
  174. const now = new Date()
  175. // have we recently reported an error?
  176. const recentSentryReport = now - this.lastErrorTimeStamp < 60 * 1000
  177. // if so, increment the error count
  178. if (recentSentryReport) {
  179. this.lastErrorCount++
  180. } else {
  181. this.lastErrorCount = 0
  182. this.lastErrorTimeStamp = now
  183. }
  184. // only report 5 errors every minute to avoid overload
  185. if (this.lastErrorCount < MAX_ERRORS) {
  186. // add a note if the rate limit has been hit
  187. const note =
  188. this.lastErrorCount + 1 === MAX_ERRORS ? '(rate limited)' : ''
  189. // report the exception
  190. return this.captureException(attributes, message, `error${note}`)
  191. }
  192. }
  193. },
  194. err() {
  195. return this.error.apply(this, arguments)
  196. },
  197. warn() {
  198. return this.logger.warn.apply(this.logger, arguments)
  199. },
  200. fatal(attributes, message, callback) {
  201. if (callback == null) {
  202. callback = function() {}
  203. }
  204. this.logger.fatal(attributes, message)
  205. if (this.raven != null) {
  206. var cb = function(e) {
  207. // call the callback once after 'logged' or 'error' event
  208. callback()
  209. return (cb = function() {})
  210. }
  211. this.captureException(attributes, message, 'fatal')
  212. this.raven.once('logged', cb)
  213. return this.raven.once('error', cb)
  214. } else {
  215. return callback()
  216. }
  217. }
  218. }
  219. Logger.initialize('default-sharelatex')