logging-manager.js 7.0 KB

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