Merge pull request #23 from overleaf/csh-metadata-fallback-node-fetch

Support file and GCE metadata as log level sources
This commit is contained in:
Christopher Hoskin 2020-08-11 11:39:56 +01:00 committed by GitHub
commit f49bc2974d
3 changed files with 194 additions and 45 deletions

View file

@ -1,4 +1,5 @@
const bunyan = require('bunyan') const bunyan = require('bunyan')
const fetch = require('node-fetch')
const fs = require('fs') const fs = require('fs')
const yn = require('yn') const yn = require('yn')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
@ -21,6 +22,7 @@ const errSerializer = function (err) {
const Logger = (module.exports = { const Logger = (module.exports = {
initialize(name) { initialize(name) {
this.logLevelSource = (process.env.LOG_LEVEL_SOURCE || 'file').toLowerCase()
this.isProduction = this.isProduction =
(process.env.NODE_ENV || '').toLowerCase() === 'production' (process.env.NODE_ENV || '').toLowerCase() === 'production'
this.defaultLevel = this.defaultLevel =
@ -41,18 +43,33 @@ const Logger = (module.exports = {
return this return this
}, },
checkLogLevel() { async checkLogLevel() {
fs.readFile('/logging/tracingEndTime', (error, end) => { try {
if (error || !end) { const end = await this.getTracingEndTime()
this.logger.level(this.defaultLevel) if (parseInt(end, 10) > Date.now()) {
return
}
if (parseInt(end) > Date.now()) {
this.logger.level('trace') this.logger.level('trace')
} else { } else {
this.logger.level(this.defaultLevel) this.logger.level(this.defaultLevel)
} }
}) } catch (err) {
this.logger.level(this.defaultLevel)
}
},
async getTracingEndTimeFile() {
return fs.promises.readFile('/logging/tracingEndTime')
},
async getTracingEndTimeMetadata() {
const options = {
headers: {
'Metadata-Flavor': 'Google'
}
}
const uri = `http://metadata.google.internal/computeMetadata/v1/project/attributes/${this.loggerName}-setLogLevelEndTime`
const res = await fetch(uri, options)
if (!res.ok) throw new Error('Metadata not okay')
return res.text()
}, },
initializeErrorReporting(sentryDsn, options) { initializeErrorReporting(sentryDsn, options) {
@ -241,6 +258,16 @@ const Logger = (module.exports = {
if (this.checkInterval) { if (this.checkInterval) {
clearInterval(this.checkInterval) clearInterval(this.checkInterval)
} }
if (this.logLevelSource === 'file') {
this.getTracingEndTime = this.getTracingEndTimeFile
} else if (this.logLevelSource === 'gce_metadata') {
this.getTracingEndTime = this.getTracingEndTimeMetadata
} else if (this.logLevelSource === 'none') {
return
} else {
console.log('Unrecognised log level source')
return
}
// check for log level override on startup // check for log level override on startup
this.checkLogLevel() this.checkLogLevel()
// re-check log level every minute // re-check log level every minute

View file

@ -7,9 +7,9 @@
"url": "http://github.com/sharelatex/logger-sharelatex.git" "url": "http://github.com/sharelatex/logger-sharelatex.git"
}, },
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"version": "2.1.1", "version": "2.2.0",
"scripts": { "scripts": {
"test": "mocha test/**/*.js", "test": "mocha --grep=$MOCHA_GREP test/**/*.js",
"format": "prettier-eslint $PWD'/**/*.js' --list-different", "format": "prettier-eslint $PWD'/**/*.js' --list-different",
"format:fix": "prettier-eslint $PWD'/**/*.js' --write", "format:fix": "prettier-eslint $PWD'/**/*.js' --write",
"lint": "eslint -f unix ." "lint": "eslint -f unix ."
@ -18,6 +18,7 @@
"@google-cloud/logging-bunyan": "^3.0.0", "@google-cloud/logging-bunyan": "^3.0.0",
"@overleaf/o-error": "^3.0.0", "@overleaf/o-error": "^3.0.0",
"bunyan": "^1.8.14", "bunyan": "^1.8.14",
"node-fetch": "^2.6.0",
"raven": "^2.6.4", "raven": "^2.6.4",
"yn": "^4.0.0" "yn": "^4.0.0"
}, },

View file

@ -29,6 +29,11 @@ describe('LoggingManager', function () {
captureException: this.captureException, captureException: this.captureException,
once: sinon.stub().yields() once: sinon.stub().yields()
} }
this.fetchResponse = {
text: sinon.stub().resolves(''),
status: 200,
ok: true
}
this.Bunyan = { this.Bunyan = {
createLogger: sinon.stub().returns(this.bunyanLogger), createLogger: sinon.stub().returns(this.bunyanLogger),
RingBuffer: bunyan.RingBuffer, RingBuffer: bunyan.RingBuffer,
@ -40,9 +45,13 @@ describe('LoggingManager', function () {
this.Raven = { this.Raven = {
Client: sinon.stub().returns(this.ravenClient) Client: sinon.stub().returns(this.ravenClient)
} }
this.Fetch = sinon.stub().resolves(this.fetchResponse)
this.Fs = { this.Fs = {
readFile: sinon.stub(),
promises: {
readFile: sinon.stub() readFile: sinon.stub()
} }
}
this.stackdriverStreamConfig = { stream: 'stackdriver' } this.stackdriverStreamConfig = { stream: 'stackdriver' }
this.stackdriverClient = { this.stackdriverClient = {
stream: sinon.stub().returns(this.stackdriverStreamConfig) stream: sinon.stub().returns(this.stackdriverStreamConfig)
@ -55,6 +64,7 @@ describe('LoggingManager', function () {
requires: { requires: {
bunyan: this.Bunyan, bunyan: this.Bunyan,
raven: this.Raven, raven: this.Raven,
'node-fetch': this.Fetch,
fs: this.Fs, fs: this.Fs,
'@google-cloud/logging-bunyan': this.GCPLogging '@google-cloud/logging-bunyan': this.GCPLogging
} }
@ -70,7 +80,9 @@ describe('LoggingManager', function () {
describe('initialize', function () { describe('initialize', function () {
beforeEach(function () { beforeEach(function () {
this.checkLogLevelStub = sinon.stub(this.LoggingManager, 'checkLogLevel') this.checkLogLevelStub = sinon
.stub(this.LoggingManager, 'checkLogLevel')
.resolves('')
this.Bunyan.createLogger.reset() this.Bunyan.createLogger.reset()
}) })
@ -108,6 +120,7 @@ describe('LoggingManager', function () {
) )
}) })
describe('logLevelSource file', function () {
it('should run checkLogLevel', function () { it('should run checkLogLevel', function () {
this.checkLogLevelStub.should.have.been.calledOnce this.checkLogLevelStub.should.have.been.calledOnce
}) })
@ -124,6 +137,7 @@ describe('LoggingManager', function () {
this.checkLogLevelStub.should.have.been.calledThrice this.checkLogLevelStub.should.have.been.calledThrice
})) }))
}) })
})
describe('when LOG_LEVEL set in env', function () { describe('when LOG_LEVEL set in env', function () {
beforeEach(function () { beforeEach(function () {
@ -287,18 +301,20 @@ describe('LoggingManager', function () {
}) })
}) })
describe('checkLogLevel', function () { describe('checkLogLevelFile', function () {
it('should request log level override from the config map', function () { it('should request log level override from the config map', async function () {
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
this.Fs.readFile.should.have.been.calledWithMatch( await this.logger.checkLogLevel()
this.Fs.promises.readFile.should.have.been.calledWithMatch(
'/logging/tracingEndTime' '/logging/tracingEndTime'
) )
}) })
describe('when read errors', function () { describe('when read errors', function () {
beforeEach(function () { beforeEach(async function () {
this.Fs.readFile.yields(new Error('error')) this.Fs.promises.readFile.throws(new Error('test read error'))
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
await this.logger.checkLogLevel()
}) })
it('should only set default level', function () { it('should only set default level', function () {
@ -309,9 +325,10 @@ describe('LoggingManager', function () {
}) })
describe('when the file is empty', function () { describe('when the file is empty', function () {
beforeEach(function () { beforeEach(async function () {
this.Fs.readFile.yields(null, '') this.Fs.promises.readFile.returns('')
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
await this.logger.checkLogLevel()
}) })
it('should only set default level', function () { it('should only set default level', function () {
@ -322,9 +339,10 @@ describe('LoggingManager', function () {
}) })
describe('when time value returned that is less than current time', function () { describe('when time value returned that is less than current time', function () {
beforeEach(function () { beforeEach(async function () {
this.Fs.readFile.yields(null, '1') this.Fs.promises.readFile.returns('1')
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
await this.logger.checkLogLevel()
}) })
it('should only set default level', function () { it('should only set default level', function () {
@ -336,10 +354,11 @@ describe('LoggingManager', function () {
describe('when time value returned that is more than current time', function () { describe('when time value returned that is more than current time', function () {
describe('when level is already set', function () { describe('when level is already set', function () {
beforeEach(function () { beforeEach(async function () {
this.bunyanLogger.level.returns(10) this.bunyanLogger.level.returns(10)
this.Fs.readFile.yields(null, (this.start + 1000).toString()) this.Fs.promises.readFile.returns((this.start + 1000).toString())
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
await this.logger.checkLogLevel()
}) })
it('should set trace level', function () { it('should set trace level', function () {
@ -350,10 +369,11 @@ describe('LoggingManager', function () {
}) })
describe('when level is not already set', function () { describe('when level is not already set', function () {
beforeEach(function () { beforeEach(async function () {
this.bunyanLogger.level.returns(20) this.bunyanLogger.level.returns(20)
this.Fs.readFile.yields(null, (this.start + 1000).toString()) this.Fs.promises.readFile.returns((this.start + 1000).toString())
this.logger.checkLogLevel() this.logger.getTracingEndTime = this.logger.getTracingEndTimeFile
await this.logger.checkLogLevel()
}) })
it('should set trace level', function () { it('should set trace level', function () {
@ -365,6 +385,107 @@ describe('LoggingManager', function () {
}) })
}) })
describe('checkLogLevelMetadata', function () {
beforeEach(function () {
this.logger = this.LoggingManager.initialize(this.loggerName)
})
describe('checkLogLevel', function () {
it('should request log level override from google meta data service', async function () {
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
await this.logger.checkLogLevel()
const options = {
headers: {
'Metadata-Flavor': 'Google'
}
}
const uri = `http://metadata.google.internal/computeMetadata/v1/project/attributes/${this.loggerName}-setLogLevelEndTime`
this.Fetch.should.have.been.calledWithMatch(uri, options)
})
describe('when request has error', function () {
beforeEach(async function () {
this.Fetch = sinon.stub().throws()
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
await this.logger.checkLogLevel()
})
it('should only set default level', function () {
this.bunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when statusCode is not 200', function () {
beforeEach(async function () {
this.fetchResponse.status = 404
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
await this.logger.checkLogLevel()
})
it('should only set default level', function () {
this.bunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when time value returned that is less than current time', function () {
beforeEach(async function () {
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
this.fetchResponse.text = sinon.stub().resolves('1')
await this.logger.checkLogLevel()
})
it('should only set default level', function () {
this.bunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'debug'
)
})
})
describe('when time value returned that is more than current time', function () {
describe('when level is already set', function () {
beforeEach(async function () {
this.bunyanLogger.level.returns(10)
this.fetchResponse.text = sinon
.stub()
.resolves((this.start + 1000).toString())
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
await this.logger.checkLogLevel()
})
it('should set trace level', function () {
this.bunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'trace'
)
})
})
describe('when level is not already set', function () {
beforeEach(async function () {
this.bunyanLogger.level.returns(20)
this.fetchResponse.text = sinon
.stub()
.resolves((this.start + 1000).toString())
this.Fetch.fetch = sinon.stub().resolves(this.fetchResponse)
this.logger.getTracingEndTime = this.logger.getTracingEndTimeMetadata
await this.logger.checkLogLevel()
})
it('should set trace level', function () {
this.bunyanLogger.level.should.have.been.calledOnce.and.calledWith(
'trace'
)
})
})
})
})
})
describe('ringbuffer', function () { describe('ringbuffer', function () {
beforeEach(function () { beforeEach(function () {
this.logBufferMock = [ this.logBufferMock = [