Merge pull request #12 from sharelatex/ew-use-ring-buffer

Use Ring Buffer, Decaffeinate, Update Bunyan
This commit is contained in:
Ersun Warncke
2019-03-12 09:52:11 -04:00
committed by GitHub
8 changed files with 689 additions and 376 deletions
+44
View File
@@ -0,0 +1,44 @@
{
"extends": [
"standard",
"prettier",
"prettier/standard"
],
"plugins": [
"mocha",
"chai-expect",
"chai-friendly"
],
"env": {
"mocha": true
},
"globals": {
"expect": true,
"define": true,
},
"settings": {
},
"rules": {
"max-len": ["error", {
"ignoreUrls": true,
// Ignore long describe/it test blocks, long import/require statements
"ignorePattern": "(^\\s*(it|describe)\\s*\\(['\"]|^import\\s*.*\\s*from\\s*['\"]|^.*\\s*=\\s*require\\(['\"])"
}],
// Add some mocha specific rules
"mocha/handle-done-callback": "error",
"mocha/no-exclusive-tests": "error",
"mocha/no-global-tests": "error",
"mocha/no-identical-title": "error",
"mocha/no-nested-tests": "error",
"mocha/no-pending-tests": "error",
"mocha/no-skipped-tests": "error",
// Add some chai specific rules
"chai-expect/missing-assertion": "error",
"chai-expect/terminating-properties": "error",
// Swap the no-unused-expressions rule with a more chai-friendly one
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": "error"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}
+1 -2
View File
@@ -1,2 +1 @@
require("coffee-script/register")
module.exports = require('./logging-manager');
module.exports = require('./logging-manager.js')
-129
View File
@@ -1,129 +0,0 @@
bunyan = require('bunyan')
request = require('request')
module.exports = Logger =
initialize: (name) ->
isProduction = process.env['NODE_ENV']?.toLowerCase() == 'production'
@defaultLevel = process.env['LOG_LEVEL'] or if isProduction then "warn" else "debug"
@loggerName = name
@logger = bunyan.createLogger
name: name
serializers: bunyan.stdSerializers
level: @defaultLevel
if isProduction
# check for log level override on startup
@.checkLogLevel()
# re-check log level every minute
checkLogLevel = () => @.checkLogLevel()
setInterval(checkLogLevel, 1000 * 60)
return @
checkLogLevel: () ->
options =
headers:
"Metadata-Flavor": "Google"
uri: "http://metadata.google.internal/computeMetadata/v1/project/attributes/#{@loggerName}-setLogLevelEndTime"
request options, (err, response, body) =>
if parseInt(body) > Date.now()
@logger.level('trace')
else
@logger.level(@defaultLevel)
initializeErrorReporting: (sentry_dsn, options) ->
raven = require "raven"
@raven = new raven.Client(sentry_dsn, options)
@lastErrorTimeStamp = 0 # for rate limiting on sentry reporting
@lastErrorCount = 0
captureException: (attributes, message, level) ->
# handle case of logger.error "message"
if typeof attributes is 'string'
attributes = {err: new Error(attributes)}
# extract any error object
error = attributes.err or attributes.error
# avoid reporting errors twice
for key, value of attributes
return if value instanceof Error && value.reportedToSentry
# include our log message in the error report
if not error?
error = {message: message} if typeof message is 'string'
else if message?
attributes.description = message
# report the error
if error?
# capture attributes and use *_id objects as tags
tags = {}
extra = {}
for key, value of attributes
tags[key] = value if key.match(/_id/) and typeof value == 'string'
extra[key] = value
# capture req object if available
req = attributes.req
if req?
extra.req =
method: req.method
url: req.originalUrl
query: req.query
headers: req.headers
ip: req.ip
# recreate error objects that have been converted to a normal object
if !(error instanceof Error) and typeof error is "object"
newError = new Error(error.message)
for own key, value of error
newError[key] = value
error = newError
# filter paths from the message to avoid duplicate errors in sentry
# (e.g. errors from `fs` methods which have a path attribute)
try
error.message = error.message.replace(" '#{error.path}'", '') if error.path
# send the error to sentry
try
@raven.captureException(error, {tags: tags, extra: extra, level: level})
# put a flag on the errors to avoid reporting them multiple times
for key, value of attributes
value.reportedToSentry = true if value instanceof Error
catch
return # ignore any errors
debug : () ->
@logger.debug.apply(@logger, arguments)
info : ()->
@logger.info.apply(@logger, arguments)
log : ()->
@logger.info.apply(@logger, arguments)
error: (attributes, message, args...)->
@logger.error(attributes, message, args...)
if @raven?
MAX_ERRORS = 5 # maximum number of errors in 1 minute
now = new Date()
# have we recently reported an error?
recentSentryReport = (now - @lastErrorTimeStamp) < 60 * 1000
# if so, increment the error count
if recentSentryReport
@lastErrorCount++
else
@lastErrorCount = 0
@lastErrorTimeStamp = now
# only report 5 errors every minute to avoid overload
if @lastErrorCount < MAX_ERRORS
# add a note if the rate limit has been hit
note = if @lastErrorCount+1 is MAX_ERRORS then "(rate limited)" else ""
# report the exception
@captureException(attributes, message, "error#{note}")
err: () ->
@error.apply(this, arguments)
warn: ()->
@logger.warn.apply(@logger, arguments)
fatal: (attributes, message, callback = () ->) ->
@logger.fatal(attributes, message)
if @raven?
cb = (e) -> # call the callback once after 'logged' or 'error' event
callback()
cb = () ->
@captureException(attributes, message, "fatal")
@raven.once 'logged', cb
@raven.once 'error', cb
else
callback()
Logger.initialize("default-sharelatex")
+231
View File
@@ -0,0 +1,231 @@
const bunyan = require('bunyan')
const request = require('request')
const Logger = module.exports = {
initialize(name) {
this.isProduction =
(process.env['NODE_ENV'] || '').toLowerCase() === 'production'
this.defaultLevel =
process.env['LOG_LEVEL'] || (this.isProduction ? 'warn' : 'debug')
this.loggerName = name
this.ringBufferSize = parseInt(process.env['LOG_RING_BUFFER_SIZE']) || 0
const loggerStreams = [
{
level: this.defaultLevel,
stream: process.stdout
}
]
if (this.ringBufferSize > 0) {
this.ringBuffer = new bunyan.RingBuffer({limit: this.ringBufferSize})
loggerStreams.push({
level: 'trace',
type: 'raw',
stream: this.ringBuffer
})
}
else {
this.ringBuffer = null
}
this.logger = bunyan.createLogger({
name,
serializers: bunyan.stdSerializers,
streams: loggerStreams
})
if (this.isProduction) {
// clear interval if already set
if (this.checkInterval) {
clearInterval(this.checkInterval)
}
// check for log level override on startup
this.checkLogLevel()
// re-check log level every minute
const checkLogLevel = () => this.checkLogLevel()
this.checkInterval = setInterval(checkLogLevel, 1000 * 60)
}
return this
},
checkLogLevel() {
const options = {
headers: {
'Metadata-Flavor': 'Google'
},
uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${
this.loggerName
}-setLogLevelEndTime`
}
request(options, (err, response, body) => {
if (err) {
this.logger.level(this.defaultLevel)
return
}
if (parseInt(body) > Date.now()) {
this.logger.level('trace')
} else {
this.logger.level(this.defaultLevel)
}
})
},
initializeErrorReporting(sentry_dsn, options) {
const raven = require('raven')
this.raven = new raven.Client(sentry_dsn, options)
this.lastErrorTimeStamp = 0 // for rate limiting on sentry reporting
this.lastErrorCount = 0
},
captureException(attributes, message, level) {
// handle case of logger.error "message"
let key, value
if (typeof attributes === 'string') {
attributes = { err: new Error(attributes) }
}
// extract any error object
let error = attributes.err || attributes.error
// avoid reporting errors twice
for (key in attributes) {
value = attributes[key]
if (value instanceof Error && value.reportedToSentry) {
return
}
}
// include our log message in the error report
if (error == null) {
if (typeof message === 'string') {
error = { message }
}
} else if (message != null) {
attributes.description = message
}
// report the error
if (error != null) {
// capture attributes and use *_id objects as tags
const tags = {}
const extra = {}
for (key in attributes) {
value = attributes[key]
if (key.match(/_id/) && typeof value === 'string') {
tags[key] = value
}
extra[key] = value
}
// capture req object if available
const { req } = attributes
if (req != null) {
extra.req = {
method: req.method,
url: req.originalUrl,
query: req.query,
headers: req.headers,
ip: req.ip
}
}
// recreate error objects that have been converted to a normal object
if (!(error instanceof Error) && typeof error === 'object') {
const newError = new Error(error.message)
for (key of Object.keys(error || {})) {
value = error[key]
newError[key] = value
}
error = newError
}
// filter paths from the message to avoid duplicate errors in sentry
// (e.g. errors from `fs` methods which have a path attribute)
try {
if (error.path) {
error.message = error.message.replace(` '${error.path}'`, '')
}
} catch (error1) {}
// send the error to sentry
try {
this.raven.captureException(error, { tags, extra, level })
// put a flag on the errors to avoid reporting them multiple times
return (() => {
const result = []
for (key in attributes) {
value = attributes[key]
if (value instanceof Error) {
result.push((value.reportedToSentry = true))
} else {
result.push(undefined)
}
}
return result
})()
} catch (error2) {
return
}
}
},
debug() {
return this.logger.debug.apply(this.logger, arguments)
},
info() {
return this.logger.info.apply(this.logger, arguments)
},
log() {
return this.logger.info.apply(this.logger, arguments)
},
error(attributes, message, ...args) {
if (this.ringBuffer !== null && Array.isArray(this.ringBuffer.records)) {
attributes.logBuffer = this.ringBuffer.records.filter(function (record) {
return record.level !== 50
})
}
this.logger.error(attributes, message, ...Array.from(args))
if (this.raven != null) {
const MAX_ERRORS = 5 // maximum number of errors in 1 minute
const now = new Date()
// have we recently reported an error?
const recentSentryReport = now - this.lastErrorTimeStamp < 60 * 1000
// if so, increment the error count
if (recentSentryReport) {
this.lastErrorCount++
} else {
this.lastErrorCount = 0
this.lastErrorTimeStamp = now
}
// only report 5 errors every minute to avoid overload
if (this.lastErrorCount < MAX_ERRORS) {
// add a note if the rate limit has been hit
const note =
this.lastErrorCount + 1 === MAX_ERRORS ? '(rate limited)' : ''
// report the exception
return this.captureException(attributes, message, `error${note}`)
}
}
},
err() {
return this.error.apply(this, arguments)
},
warn() {
return this.logger.warn.apply(this.logger, arguments)
},
fatal(attributes, message, callback) {
if (callback == null) {
callback = function() {}
}
this.logger.fatal(attributes, message)
if (this.raven != null) {
var cb = function(e) {
// call the callback once after 'logged' or 'error' event
callback()
return (cb = function() {})
}
this.captureException(attributes, message, 'fatal')
this.raven.once('logged', cb)
return this.raven.once('error', cb)
} else {
return callback()
}
}
}
Logger.initialize('default-sharelatex')
+23 -11
View File
@@ -6,21 +6,33 @@
"type": "git",
"url": "http://github.com/sharelatex/logger-sharelatex.git"
},
"version": "1.6.0",
"version": "1.7.0",
"scripts": {
"test": "mocha --require coffee-script/register test/**/*.coffee"
"test": "mocha test/**/*.js",
"format": "prettier-eslint '**/*.js' --list-different",
"format:fix": "prettier-eslint '**/*.js' --write",
"lint": "eslint -f unix ."
},
"dependencies": {
"bunyan": "1.5.1",
"coffee-script": "1.12.4",
"raven": "^1.1.3",
"request": "^2.88.0"
"bunyan": "1.8.12",
"raven": "1.1.3",
"request": "2.88.0"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^5.2.0",
"sandboxed-module": "0.2.0",
"sinon": "^7.2.3",
"sinon-chai": "^3.3.0"
"chai": "4.2.0",
"eslint": "^4.18.1",
"eslint-config-prettier": "^3.1.0",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-chai-expect": "^1.1.1",
"eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-mocha": "^5.2.0",
"eslint-plugin-node": "^6.0.0",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"mocha": "5.2.0",
"sandboxed-module": "2.0.3",
"sinon": "7.2.3",
"sinon-chai": "3.3.0"
}
}
@@ -1,234 +0,0 @@
SandboxedModule = require("sandboxed-module")
chai = require("chai")
path = require("path")
sinon = require("sinon")
sinonChai = require("sinon-chai")
chai.use(sinonChai)
chai.should()
modulePath = path.join __dirname, '../../../logging-manager.coffee'
describe 'LoggingManager', ->
beforeEach ->
@start = Date.now()
@clock = sinon.useFakeTimers(@start)
@captureException = sinon.stub()
@mockBunyanLogger =
debug: sinon.stub()
error: sinon.stub()
fatal: sinon.stub()
info: sinon.stub()
level: sinon.stub()
warn: sinon.stub()
@mockRavenClient =
captureException: @captureException
once: sinon.stub().yields()
@LoggingManager = SandboxedModule.require modulePath, requires:
bunyan: @Bunyan = createLogger: sinon.stub().returns(@mockBunyanLogger)
raven: @Raven = Client: sinon.stub().returns(@mockRavenClient)
request: @Request = sinon.stub()
@loggerName = "test"
@logger = @LoggingManager.initialize(@loggerName)
@logger.initializeErrorReporting("test_dsn")
afterEach ->
@clock.restore()
describe 'initialize', ->
beforeEach ->
@LoggingManager.checkLogLevel = sinon.stub()
@Bunyan.createLogger.reset()
describe "not in production", ->
beforeEach ->
@LoggingManager.initialize()
it 'should default to log level debug', ->
@Bunyan.createLogger.should.have.been.calledWithMatch level: "debug"
it 'should not run checkLogLevel', ->
@LoggingManager.checkLogLevel.should.not.have.been.called
describe "in production", ->
beforeEach ->
process.env.NODE_ENV = 'production'
@LoggingManager.initialize()
afterEach ->
delete process.env.NODE_ENV
it 'should default to log level warn', ->
@Bunyan.createLogger.should.have.been.calledWithMatch level: "warn"
it 'should run checkLogLevel', ->
@LoggingManager.checkLogLevel.should.have.been.calledOnce
describe 'after 1 minute', ->
it 'should run checkLogLevel again', ->
@clock.tick(61*1000)
@LoggingManager.checkLogLevel.should.have.been.calledTwice
describe 'after 2 minutes', ->
it 'should run checkLogLevel again', ->
@clock.tick(121*1000)
@LoggingManager.checkLogLevel.should.have.been.calledThrice
describe "when LOG_LEVEL set in env", ->
beforeEach ->
process.env.LOG_LEVEL = "trace"
@LoggingManager.initialize()
afterEach ->
delete process.env.LOG_LEVEL
it "should use custom log level", ->
@Bunyan.createLogger.should.have.been.calledWithMatch level: "trace"
describe 'bunyan logging', ->
beforeEach ->
@logArgs = [ foo: "bar", "foo", "bar" ]
it 'should log debug', ->
@logger.debug @logArgs
@mockBunyanLogger.debug.should.have.been.calledWith @logArgs
it 'should log error', ->
@logger.error @logArgs
@mockBunyanLogger.error.should.have.been.calledWith @logArgs
it 'should log fatal', ->
@logger.fatal @logArgs
@mockBunyanLogger.fatal.should.have.been.calledWith @logArgs
it 'should log info', ->
@logger.info @logArgs
@mockBunyanLogger.info.should.have.been.calledWith @logArgs
it 'should log warn', ->
@logger.warn @logArgs
@mockBunyanLogger.warn.should.have.been.calledWith @logArgs
it 'should log err', ->
@logger.err @logArgs
@mockBunyanLogger.error.should.have.been.calledWith @logArgs
it 'should log log', ->
@logger.log @logArgs
@mockBunyanLogger.info.should.have.been.calledWith @logArgs
describe 'logger.error', ->
it 'should report a single error to sentry', () ->
@logger.error {foo:'bar'}, "message"
@captureException.called.should.equal true
it 'should report the same error to sentry only once', () ->
error1 = new Error('this is the error')
@logger.error {foo: error1}, "first message"
@logger.error {bar: error1}, "second message"
@captureException.callCount.should.equal 1
it 'should report two different errors to sentry individually', () ->
error1 = new Error('this is the error')
error2 = new Error('this is the error')
@logger.error {foo: error1}, "first message"
@logger.error {bar: error2}, "second message"
@captureException.callCount.should.equal 2
it 'should remove the path from fs errors', () ->
fsError = new Error("Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'")
fsError.path = "/tmp/3279b8d0-da10-11e8-8255-efd98985942b"
@logger.error {err: fsError}, "message"
@captureException.calledWith(sinon.match.has('message', 'Error: ENOENT: no such file or directory, stat')).should.equal true
it 'for multiple errors should only report a maximum of 5 errors to sentry', () ->
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@captureException.callCount.should.equal 5
it 'for multiple errors with a minute delay should report 10 errors to sentry', () ->
# the first five errors should be reported to sentry
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
# the following errors should not be reported
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
# allow a minute to pass
@clock.tick(@start+ 61*1000)
# after a minute the next five errors should be reported to sentry
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
# the following errors should not be reported to sentry
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@logger.error {foo:'bar'}, "message"
@captureException.callCount.should.equal 10
describe 'checkLogLevel', ->
it 'should request log level override from google meta data service', ->
@LoggingManager.checkLogLevel()
options =
headers:
"Metadata-Flavor": "Google"
uri: "http://metadata.google.internal/computeMetadata/v1/project/attributes/#{@loggerName}-setLogLevelEndTime"
@Request.should.have.been.calledWithMatch options
describe 'when request has error', ->
beforeEach ->
@Request.yields "error"
@LoggingManager.checkLogLevel()
it "should only set default level", ->
@mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug')
describe 'when statusCode is not 200', ->
beforeEach ->
@Request.yields null, statusCode: 404
@LoggingManager.checkLogLevel()
it "should only set default level", ->
@mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug')
describe 'when time value returned that is less than current time', ->
beforeEach ->
@Request.yields null, statusCode: 200, '1'
@LoggingManager.checkLogLevel()
it "should only set default level", ->
@mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('debug')
describe 'when time value returned that is less than current time', ->
describe 'when level is already set', ->
beforeEach ->
@mockBunyanLogger.level.returns(10)
@Request.yields null, statusCode: 200, @start + 1000
@LoggingManager.checkLogLevel()
it "should set trace level", ->
@mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('trace')
describe 'when level is not already set', ->
beforeEach ->
@mockBunyanLogger.level.returns(20)
@Request.yields null, statusCode: 200, @start + 1000
@LoggingManager.checkLogLevel()
it "should set trace level", ->
@mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith('trace')
@@ -0,0 +1,386 @@
const SandboxedModule = require('sandboxed-module')
const bunyan = require('bunyan')
const chai = require('chai')
const path = require('path')
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
chai.use(sinonChai)
chai.should()
const modulePath = path.join(__dirname, '../../logging-manager.js')
describe('LoggingManager', function() {
beforeEach(function() {
this.start = Date.now()
this.clock = sinon.useFakeTimers(this.start)
this.captureException = sinon.stub()
this.mockBunyanLogger = {
debug: sinon.stub(),
error: sinon.stub(),
fatal: sinon.stub(),
info: sinon.stub(),
level: sinon.stub(),
warn: sinon.stub()
}
this.mockRavenClient = {
captureException: this.captureException,
once: sinon.stub().yields()
}
this.LoggingManager = SandboxedModule.require(modulePath, {
globals: { console },
requires: {
bunyan: (this.Bunyan = {
createLogger: sinon.stub().returns(this.mockBunyanLogger),
RingBuffer: bunyan.RingBuffer
}),
raven: (this.Raven = {
Client: sinon.stub().returns(this.mockRavenClient)
}),
request: (this.Request = sinon.stub())
}
})
this.loggerName = 'test'
this.logger = this.LoggingManager.initialize(this.loggerName)
this.logger.initializeErrorReporting('test_dsn')
})
afterEach(function() {
this.clock.restore()
})
describe('initialize', function() {
beforeEach(function() {
this.checkLogLevelStub = sinon.stub(this.LoggingManager, 'checkLogLevel')
this.Bunyan.createLogger.reset()
})
afterEach(function() {
this.checkLogLevelStub.restore()
})
describe('not in production', function() {
beforeEach(function() {
this.logger = this.LoggingManager.initialize(this.loggerName)
})
it('should default to log level debug', function() {
this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
'debug'
)
})
it('should not run checkLogLevel', function() {
this.checkLogLevelStub.should.not.have.been.called
})
})
describe('in production', function() {
beforeEach(function() {
process.env.NODE_ENV = 'production'
this.logger = this.LoggingManager.initialize(this.loggerName)
})
afterEach(() => delete process.env.NODE_ENV)
it('should default to log level warn', function() {
this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
'warn'
)
})
it('should run checkLogLevel', function() {
this.checkLogLevelStub.should.have.been.calledOnce
})
describe('after 1 minute', () =>
it('should run checkLogLevel again', function() {
this.clock.tick(61 * 1000)
this.checkLogLevelStub.should.have.been.calledTwice
}))
describe('after 2 minutes', () =>
it('should run checkLogLevel again', function() {
this.clock.tick(121 * 1000)
this.checkLogLevelStub.should.have.been.calledThrice
}))
})
describe('when LOG_LEVEL set in env', function() {
beforeEach(function() {
process.env.LOG_LEVEL = 'trace'
this.LoggingManager.initialize()
})
afterEach(() => delete process.env.LOG_LEVEL)
it('should use custom log level', function() {
this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
'trace'
)
})
})
})
describe('bunyan logging', function() {
beforeEach(function() {
this.logArgs = [{ foo: 'bar' }, 'foo', 'bar']
})
it('should log debug', function() {
this.logger.debug(this.logArgs)
this.mockBunyanLogger.debug.should.have.been.calledWith(this.logArgs)
})
it('should log error', function() {
this.logger.error(this.logArgs)
this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs)
})
it('should log fatal', function() {
this.logger.fatal(this.logArgs)
this.mockBunyanLogger.fatal.should.have.been.calledWith(this.logArgs)
})
it('should log info', function() {
this.logger.info(this.logArgs)
this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs)
})
it('should log warn', function() {
this.logger.warn(this.logArgs)
this.mockBunyanLogger.warn.should.have.been.calledWith(this.logArgs)
})
it('should log err', function() {
this.logger.err(this.logArgs)
this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs)
})
it('should log log', function() {
this.logger.log(this.logArgs)
this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs)
})
})
describe('logger.error', function() {
it('should report a single error to sentry', function() {
this.logger.error({ foo: 'bar' }, 'message')
this.captureException.called.should.equal(true)
})
it('should report the same error to sentry only once', function() {
const error1 = new Error('this is the error')
this.logger.error({ foo: error1 }, 'first message')
this.logger.error({ bar: error1 }, 'second message')
this.captureException.callCount.should.equal(1)
})
it('should report two different errors to sentry individually', function() {
const error1 = new Error('this is the error')
const error2 = new Error('this is the error')
this.logger.error({ foo: error1 }, 'first message')
this.logger.error({ bar: error2 }, 'second message')
this.captureException.callCount.should.equal(2)
})
it('should remove the path from fs errors', function() {
const fsError = new Error(
"Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'"
)
fsError.path = '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'
this.logger.error({ err: fsError }, 'message')
this.captureException
.calledWith(
sinon.match.has(
'message',
'Error: ENOENT: no such file or directory, stat'
)
)
.should.equal(true)
})
it('for multiple errors should only report a maximum of 5 errors to sentry', function() {
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.captureException.callCount.should.equal(5)
})
it('for multiple errors with a minute delay should report 10 errors to sentry', function() {
// the first five errors should be reported to sentry
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
// the following errors should not be reported
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
// allow a minute to pass
this.clock.tick(this.start + 61 * 1000)
// after a minute the next five errors should be reported to sentry
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
// the following errors should not be reported to sentry
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.logger.error({ foo: 'bar' }, 'message')
this.captureException.callCount.should.equal(10)
})
})
describe('checkLogLevel', function() {
it('should request log level override from google meta data service', function() {
this.logger.checkLogLevel()
const options = {
headers: {
'Metadata-Flavor': 'Google'
},
uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${
this.loggerName
}-setLogLevelEndTime`
}
this.Request.should.have.been.calledWithMatch(options)
})
describe('when request has error', function() {
beforeEach(function() {
this.Request.yields('error')
this.logger.checkLogLevel()
})
it('should only set default level', function() {
this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when statusCode is not 200', function() {
beforeEach(function() {
this.Request.yields(null, { statusCode: 404 })
this.logger.checkLogLevel()
})
it('should only set default level', function() {
this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when time value returned that is less than current time', function() {
beforeEach(function() {
this.Request.yields(null, { statusCode: 200 }, '1')
this.logger.checkLogLevel()
})
it('should only set default level', function() {
this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when time value returned that is less than current time', function() {
describe('when level is already set', function() {
beforeEach(function() {
this.mockBunyanLogger.level.returns(10)
this.Request.yields(null, { statusCode: 200 }, this.start + 1000)
this.logger.checkLogLevel()
})
it('should set trace level', function() {
this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'trace'
)
})
})
describe('when level is not already set', function() {
beforeEach(function() {
this.mockBunyanLogger.level.returns(20)
this.Request.yields(null, { statusCode: 200 }, this.start + 1000)
this.logger.checkLogLevel()
})
it('should set trace level', function() {
this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'trace'
)
})
})
})
})
describe('ringbuffer', function() {
beforeEach(function() {
this.logBufferMock = [
{
msg: 'log 1'
},
{
msg: 'log 2'
},
{
level: 50,
msg: 'error'
}
]
})
describe('when ring buffer size is positive', function() {
beforeEach(function() {
process.env['LOG_RING_BUFFER_SIZE'] = '20'
this.logger = this.LoggingManager.initialize(this.loggerName)
this.logger.ringBuffer.records = this.logBufferMock
this.logger.error({}, 'error')
})
afterEach(function() {
process.env['LOG_RING_BUFFER_SIZE'] = undefined
})
it('should include buffered logs in error log and filter out error logs in buffer', function() {
this.mockBunyanLogger.error.lastCall.args[0].logBuffer.should.deep.equal([
{
msg: 'log 1'
},
{
msg: 'log 2'
},
])
})
})
describe('when ring buffer size is zero', function() {
beforeEach(function() {
process.env['LOG_RING_BUFFER_SIZE'] = '0'
this.logger = this.LoggingManager.initialize(this.loggerName)
this.logger.error({}, 'error')
})
afterEach(function() {
process.env['LOG_RING_BUFFER_SIZE'] = undefined
})
it('should not include buffered logs in error log', function() {
chai.expect(this.mockBunyanLogger.error.lastCall.args[0].logBuffer).be
.undefined
})
})
})
})