logging-manager.coffee 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. bunyan = require('bunyan')
  2. request = require('request')
  3. module.exports = Logger =
  4. initialize: (name) ->
  5. isProduction = process.env['NODE_ENV']?.toLowerCase() == 'production'
  6. @defaultLevel = process.env['LOG_LEVEL'] or if isProduction then "warn" else "debug"
  7. @loggerName = name
  8. @logger = bunyan.createLogger
  9. name: name
  10. serializers: bunyan.stdSerializers
  11. level: @defaultLevel
  12. if isProduction
  13. # check for log level override on startup
  14. @.checkLogLevel()
  15. # re-check log level every minute
  16. checkLogLevel = () => @.checkLogLevel()
  17. setInterval(checkLogLevel, 1000 * 60)
  18. return @
  19. checkLogLevel: () ->
  20. options =
  21. headers:
  22. "Metadata-Flavor": "Google"
  23. uri: "http://metadata.google.internal/computeMetadata/v1/project/attributes/#{@loggerName}-setLogLevelEndTime"
  24. request options, (err, response, body) =>
  25. if parseInt(body) > Date.now()
  26. @logger.level('trace')
  27. else
  28. @logger.level(@defaultLevel)
  29. initializeErrorReporting: (sentry_dsn, options) ->
  30. raven = require "raven"
  31. @raven = new raven.Client(sentry_dsn, options)
  32. @lastErrorTimeStamp = 0 # for rate limiting on sentry reporting
  33. @lastErrorCount = 0
  34. captureException: (attributes, message, level) ->
  35. # handle case of logger.error "message"
  36. if typeof attributes is 'string'
  37. attributes = {err: new Error(attributes)}
  38. # extract any error object
  39. error = attributes.err or attributes.error
  40. # avoid reporting errors twice
  41. for key, value of attributes
  42. return if value instanceof Error && value.reportedToSentry
  43. # include our log message in the error report
  44. if not error?
  45. error = {message: message} if typeof message is 'string'
  46. else if message?
  47. attributes.description = message
  48. # report the error
  49. if error?
  50. # capture attributes and use *_id objects as tags
  51. tags = {}
  52. extra = {}
  53. for key, value of attributes
  54. tags[key] = value if key.match(/_id/) and typeof value == 'string'
  55. extra[key] = value
  56. # capture req object if available
  57. req = attributes.req
  58. if req?
  59. extra.req =
  60. method: req.method
  61. url: req.originalUrl
  62. query: req.query
  63. headers: req.headers
  64. ip: req.ip
  65. # recreate error objects that have been converted to a normal object
  66. if !(error instanceof Error) and typeof error is "object"
  67. newError = new Error(error.message)
  68. for own key, value of error
  69. newError[key] = value
  70. error = newError
  71. # filter paths from the message to avoid duplicate errors in sentry
  72. # (e.g. errors from `fs` methods which have a path attribute)
  73. try
  74. error.message = error.message.replace(" '#{error.path}'", '') if error.path
  75. # send the error to sentry
  76. try
  77. @raven.captureException(error, {tags: tags, extra: extra, level: level})
  78. # put a flag on the errors to avoid reporting them multiple times
  79. for key, value of attributes
  80. value.reportedToSentry = true if value instanceof Error
  81. catch
  82. return # ignore any errors
  83. debug : () ->
  84. @logger.debug.apply(@logger, arguments)
  85. info : ()->
  86. @logger.info.apply(@logger, arguments)
  87. log : ()->
  88. @logger.info.apply(@logger, arguments)
  89. error: (attributes, message, args...)->
  90. @logger.error(attributes, message, args...)
  91. if @raven?
  92. MAX_ERRORS = 5 # maximum number of errors in 1 minute
  93. now = new Date()
  94. # have we recently reported an error?
  95. recentSentryReport = (now - @lastErrorTimeStamp) < 60 * 1000
  96. # if so, increment the error count
  97. if recentSentryReport
  98. @lastErrorCount++
  99. else
  100. @lastErrorCount = 0
  101. @lastErrorTimeStamp = now
  102. # only report 5 errors every minute to avoid overload
  103. if @lastErrorCount < MAX_ERRORS
  104. # add a note if the rate limit has been hit
  105. note = if @lastErrorCount+1 is MAX_ERRORS then "(rate limited)" else ""
  106. # report the exception
  107. @captureException(attributes, message, "error#{note}")
  108. err: () ->
  109. @error.apply(this, arguments)
  110. warn: ()->
  111. @logger.warn.apply(@logger, arguments)
  112. fatal: (attributes, message, callback = () ->) ->
  113. @logger.fatal(attributes, message)
  114. if @raven?
  115. cb = (e) -> # call the callback once after 'logged' or 'error' event
  116. callback()
  117. cb = () ->
  118. @captureException(attributes, message, "fatal")
  119. @raven.once 'logged', cb
  120. @raven.once 'error', cb
  121. else
  122. callback()
  123. Logger.initialize("default-sharelatex")