-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #77 from Financial-Times/keen-health
Keen health checks
- Loading branch information
Showing
9 changed files
with
271 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
/node_modules/ | ||
|
||
/npm-debug.log | ||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
'use strict'; | ||
|
||
const logger = require('@financial-times/n-logger').default; | ||
const status = require('./status'); | ||
const Check = require('./check'); | ||
const KeenQuery = require('keen-query'); | ||
const ms = require('ms'); | ||
|
||
const logEventPrefix = 'KEEN_THRESHOLD_CHECK'; | ||
|
||
// Detects when the value of a metric climbs above/below a threshold value | ||
|
||
class KeenThresholdCheck extends Check { | ||
|
||
constructor(options){ | ||
super(options); | ||
this.threshold = options.threshold; | ||
this.direction = options.direction || 'below'; | ||
|
||
this.timeframe = options.timeframe || 'this_60_minutes'; | ||
|
||
this.keenProjectId = process.env.KEEN_PROJECT_ID; | ||
this.keenReadKey = process.env.KEEN_READ_KEY; | ||
if (!(this.keenProjectId && this.keenReadKey)) { | ||
throw new Error('You must set KEEN_PROJECT_ID and KEEN_READ_KEY environment variables'); | ||
} | ||
|
||
KeenQuery.setConfig({ | ||
KEEN_PROJECT_ID: this.keenProjectId, | ||
KEEN_READ_KEY: this.keenReadKey, | ||
KEEN_HOST: 'https://keen-proxy.ft.com/3.0' | ||
}); | ||
|
||
if (!options.query) { | ||
throw new Error(`You must pass in a query for the "${options.name}" check - e.g., "page:view->filter(context.app=article)->count()"`); | ||
} | ||
|
||
|
||
this.query = options.query; | ||
//Default to 10 minute interval for keen checks so we don't overwhelm it | ||
this.interval = options.interval || 10 * 60 * 1000; | ||
|
||
this.checkOutput = 'Keen threshold check has not yet run'; | ||
} | ||
|
||
tick() { | ||
return KeenQuery.build(this.query) | ||
.filter('user.subscriptions.isStaff!=true') | ||
.filter('user.geo.isFinancialTimesOffice!=true') | ||
.filter('device.isRobot!=true') | ||
.relTime(this.timeframe) | ||
.print() | ||
.then(result => { | ||
if(result && result.rows) { | ||
let data = Number(result.rows[0][1]); | ||
let failed = this.direction === 'above' ? | ||
data && data > this.threshold : | ||
data && data < this.threshold; | ||
this.status = failed ? status.FAILED : status.PASSED; | ||
this.checkOutput = `Got ${data} ${this.timeframe.split('_').join(' ').replace('this', 'in the last')}, expected not to be ${this.direction} the threshold of ${this.threshold} | ||
${this.query} | ||
`; | ||
} | ||
}) | ||
.catch(err => { | ||
logger.error({ event: `${logEventPrefix}_ERROR`, url: this.query }, err); | ||
this.status = status.FAILED; | ||
this.checkOutput = 'Keen threshold check failed to fetch data: ' + err.message; | ||
}); | ||
} | ||
|
||
} | ||
|
||
module.exports = KeenThresholdCheck; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
'use strict'; | ||
module.exports = { | ||
name: 'keen', | ||
descriptions : '', | ||
checks : [ | ||
{ | ||
type: 'keenThreshold', | ||
query: 'page:view->count()', | ||
name: 'Some keen value is above some threshold', | ||
severity: 2, | ||
threshold: 4, | ||
businessImpact: 'catastrophic', | ||
technicalSummary: 'god knows', | ||
panicGuide: 'Don\'t Panic' | ||
} | ||
] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
'use strict'; | ||
|
||
const expect = require('chai').expect; | ||
const fixture = require('./fixtures/config/keenThresholdFixture').checks[0]; | ||
const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); | ||
const sinon = require('sinon'); | ||
|
||
function getCheckConfig (conf) { | ||
return Object.assign({}, fixture, conf || {}); | ||
} | ||
|
||
let mockKeenQuery; | ||
let Check; | ||
|
||
|
||
// Mocks a pair of calls to keen for sample and baseline data | ||
function mockKeen (results) { | ||
|
||
mockKeenQuery = { | ||
setConfig: sinon.stub(), | ||
build: sinon.stub().returnsThis(), | ||
filter: sinon.stub().returnsThis(), | ||
relTime: sinon.stub().returnsThis(), | ||
print: sinon.stub().returns(Promise.resolve(results)) | ||
}; | ||
|
||
Check = proxyquire('../src/checks/keenThreshold.check', {'keen-query': mockKeenQuery}); | ||
} | ||
|
||
describe('Keen Threshold Check', function(){ | ||
|
||
let check; | ||
|
||
afterEach(function(){ | ||
check.stop(); | ||
}); | ||
|
||
context('Upper threshold enforced', function () { | ||
|
||
it('Should be healthy if result above upper threshold', function (done) { | ||
mockKeen({ | ||
rows: [ | ||
['something', 100] | ||
] | ||
}); | ||
check = new Check(getCheckConfig({ | ||
threshold: 11 | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
|
||
expect(mockKeenQuery.build.firstCall.args[0]).to.contain('page:view->count()'); | ||
expect(mockKeenQuery.relTime.firstCall.args[0]).to.contain('this_60_minutes'); | ||
expect(check.getStatus().ok).to.be.true; | ||
done(); | ||
}); | ||
}); | ||
|
||
|
||
it('should be unhealthy if result is below upper threshold', done => { | ||
mockKeen({ | ||
rows: [ | ||
['something', 10] | ||
] | ||
}); | ||
check = new Check(getCheckConfig({ | ||
threshold: 11 | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
expect(check.getStatus().ok).to.be.false; | ||
done(); | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
context('Lower threshold enforced', function () { | ||
|
||
it('Should be healthy if all datapoints are above lower threshold', function (done) { | ||
mockKeen({ | ||
rows: [ | ||
['something', 10] | ||
] | ||
}); | ||
check = new Check(getCheckConfig({ | ||
threshold: 5, | ||
direction: 'below' | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
expect(check.getStatus().ok).to.be.true; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('Should be healthy if any datapoints are equal to lower threshold', function (done) { | ||
mockKeen({ | ||
rows: [ | ||
['something', 10] | ||
] | ||
}); | ||
check = new Check(getCheckConfig({ | ||
threshold: 10, | ||
direction: 'below' | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
expect(check.getStatus().ok).to.be.true; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should be unhealthy if any datapoints are below lower threshold', done => { | ||
mockKeen({ | ||
rows: [ | ||
['something', 5] | ||
] | ||
}); | ||
check = new Check(getCheckConfig({ | ||
threshold: 10, | ||
direction: 'below' | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
expect(check.getStatus().ok).to.be.false; | ||
done(); | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
it('Should be possible to configure sample period', function(done){ | ||
mockKeen(); | ||
check = new Check(getCheckConfig({ | ||
timeframe: 'this_2_days' | ||
})); | ||
check.start(); | ||
setTimeout(() => { | ||
expect(mockKeenQuery.build.firstCall.args[0]).to.contain('page:view->count()'); | ||
expect(mockKeenQuery.relTime.firstCall.args[0]).to.contain('this_2_days'); | ||
done(); | ||
}); | ||
}); | ||
|
||
}); |