mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: add db query latency metrics (#473)
* feat: add db metrics Signed-off-by: Moritz Johner <beller.moritz@googlemail.com> * fix: use base unit Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
This commit is contained in:
parent
a7cd7f76c5
commit
d0f57a68b2
@ -1,6 +1,9 @@
|
||||
/* eslint camelcase:off */
|
||||
'use strict';
|
||||
|
||||
const metricsHelper = require('./metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
|
||||
const COLUMNS = [
|
||||
'app_name',
|
||||
'created_at',
|
||||
@ -35,15 +38,22 @@ const remapRow = (input, old = {}) => ({
|
||||
});
|
||||
|
||||
class ClientApplicationsDb {
|
||||
constructor(db) {
|
||||
constructor(db, eventBus) {
|
||||
this.db = db;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
updateRow(details, prev) {
|
||||
details.updatedAt = 'now()';
|
||||
return this.db(TABLE)
|
||||
.where('app_name', details.appName)
|
||||
.update(remapRow(details, prev));
|
||||
.update(remapRow(details, prev))
|
||||
.then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'applications',
|
||||
action: 'updateRow',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
insertNewRow(details) {
|
||||
@ -63,7 +73,13 @@ class ClientApplicationsDb {
|
||||
} else {
|
||||
return this.insertNewRow(data);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'applications',
|
||||
action: 'upsert',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
@ -71,7 +87,13 @@ class ClientApplicationsDb {
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.orderBy('app_name', 'asc')
|
||||
.map(mapRow);
|
||||
.map(mapRow)
|
||||
.then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'applications',
|
||||
action: 'getAll',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getApplication(appName) {
|
||||
|
@ -1,6 +1,9 @@
|
||||
/* eslint camelcase: "off" */
|
||||
'use strict';
|
||||
|
||||
const metricsHelper = require('./metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
|
||||
const COLUMNS = [
|
||||
'app_name',
|
||||
'instance_id',
|
||||
@ -23,8 +26,9 @@ const mapRow = row => ({
|
||||
});
|
||||
|
||||
class ClientInstanceStore {
|
||||
constructor(db, getLogger) {
|
||||
constructor(db, eventBus, getLogger) {
|
||||
this.db = db;
|
||||
this.eventBus = eventBus;
|
||||
this.logger = getLogger('client-instance-store.js');
|
||||
const clearer = () => this._removeInstancesOlderThanTwoDays();
|
||||
setTimeout(clearer, 10).unref();
|
||||
@ -72,7 +76,13 @@ class ClientInstanceStore {
|
||||
} else {
|
||||
return this.insertNewRow(details);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'instance',
|
||||
action: 'insert',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAll() {
|
||||
@ -80,7 +90,13 @@ class ClientInstanceStore {
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.orderBy('last_seen', 'desc')
|
||||
.map(mapRow);
|
||||
.map(mapRow)
|
||||
.then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'instance',
|
||||
action: 'getAll',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getByAppName(appName) {
|
||||
|
@ -1,14 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const metricsHelper = require('./metrics-helper');
|
||||
const { DB_TIME } = require('../events');
|
||||
|
||||
const TEN_SECONDS = 10 * 1000;
|
||||
|
||||
class ClientMetricsStore extends EventEmitter {
|
||||
constructor(metricsDb, getLogger, pollInterval = TEN_SECONDS) {
|
||||
constructor(metricsDb, eventBus, getLogger, pollInterval = TEN_SECONDS) {
|
||||
super();
|
||||
this.logger = getLogger('client-metrics-store.js');
|
||||
this.metricsDb = metricsDb;
|
||||
this.eventBus = eventBus;
|
||||
this.highestIdSeen = 0;
|
||||
|
||||
this._init(pollInterval);
|
||||
@ -45,7 +48,12 @@ class ClientMetricsStore extends EventEmitter {
|
||||
|
||||
// Insert new client metrics
|
||||
insert(metrics) {
|
||||
return this.metricsDb.insert(metrics);
|
||||
return this.metricsDb.insert(metrics).then(
|
||||
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
|
||||
store: 'metrics',
|
||||
action: 'insert',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const { EventEmitter } = require('events');
|
||||
const ClientMetricStore = require('./client-metrics-store');
|
||||
const lolex = require('lolex');
|
||||
const getLogger = require('../../test/fixtures/no-logger');
|
||||
@ -24,8 +25,8 @@ function getMockDb() {
|
||||
|
||||
test.cb('should call database on startup', t => {
|
||||
const mock = getMockDb();
|
||||
|
||||
const store = new ClientMetricStore(mock, getLogger);
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger);
|
||||
|
||||
t.plan(2);
|
||||
|
||||
@ -42,8 +43,8 @@ test.cb('should start poller even if inital database fetch fails', t => {
|
||||
const clock = lolex.install();
|
||||
const mock = getMockDb();
|
||||
mock.getMetricsLastHour = () => Promise.reject('oops');
|
||||
|
||||
const store = new ClientMetricStore(mock, getLogger, 100);
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
||||
|
||||
const metrics = [];
|
||||
store.on('metrics', m => metrics.push(m));
|
||||
@ -64,7 +65,8 @@ test.cb('should poll for updates', t => {
|
||||
const clock = lolex.install();
|
||||
|
||||
const mock = getMockDb();
|
||||
const store = new ClientMetricStore(mock, getLogger, 100);
|
||||
const ee = new EventEmitter();
|
||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
||||
|
||||
const metrics = [];
|
||||
store.on('metrics', m => metrics.push(m));
|
||||
|
@ -9,7 +9,7 @@ const ClientMetricsDb = require('./client-metrics-db');
|
||||
const ClientMetricsStore = require('./client-metrics-store');
|
||||
const ClientApplicationsStore = require('./client-applications-store');
|
||||
|
||||
module.exports.createStores = config => {
|
||||
module.exports.createStores = (config, eventBus) => {
|
||||
const getLogger = config.getLogger;
|
||||
const db = createDb(config);
|
||||
const eventStore = new EventStore(db, getLogger);
|
||||
@ -20,8 +20,16 @@ module.exports.createStores = config => {
|
||||
eventStore,
|
||||
featureToggleStore: new FeatureToggleStore(db, eventStore, getLogger),
|
||||
strategyStore: new StrategyStore(db, eventStore, getLogger),
|
||||
clientApplicationsStore: new ClientApplicationsStore(db, getLogger),
|
||||
clientInstanceStore: new ClientInstanceStore(db, getLogger),
|
||||
clientMetricsStore: new ClientMetricsStore(clientMetricsDb, getLogger),
|
||||
clientApplicationsStore: new ClientApplicationsStore(
|
||||
db,
|
||||
eventBus,
|
||||
getLogger
|
||||
),
|
||||
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
|
||||
clientMetricsStore: new ClientMetricsStore(
|
||||
clientMetricsDb,
|
||||
eventBus,
|
||||
getLogger
|
||||
),
|
||||
};
|
||||
};
|
||||
|
23
lib/db/metrics-helper.js
Normal file
23
lib/db/metrics-helper.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const timer = require('../timer');
|
||||
|
||||
// wrapTimer keeps track of the timing of a async operation and emits
|
||||
// a event on the given eventBus once the operation is complete
|
||||
//
|
||||
// the returned function is designed to be used as a .then(<func>) argument.
|
||||
// It transparently passes the data to the following .then(<func>)
|
||||
//
|
||||
// usage: promise.then(wrapTimer(bus, type, { payload: 'ok' }))
|
||||
const wrapTimer = (eventBus, event, args = {}) => {
|
||||
const t = timer.new();
|
||||
return data => {
|
||||
args.time = t();
|
||||
eventBus.emit(event, args);
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
wrapTimer,
|
||||
};
|
@ -2,4 +2,5 @@
|
||||
|
||||
module.exports = {
|
||||
REQUEST_TIME: 'request_time',
|
||||
DB_TIME: 'db_time',
|
||||
};
|
||||
|
@ -30,6 +30,12 @@ exports.startMonitoring = (
|
||||
labelNames: ['path', 'method', 'status'],
|
||||
percentiles: [0.1, 0.5, 0.9, 0.99],
|
||||
});
|
||||
const dbDuration = new client.Summary({
|
||||
name: 'db_query_duration_seconds',
|
||||
help: 'DB query duration time',
|
||||
labelNames: ['store', 'action'],
|
||||
percentiles: [0.1, 0.5, 0.9, 0.99],
|
||||
});
|
||||
const featureToggleUpdateTotal = new client.Counter({
|
||||
name: 'feature_toggle_update_total',
|
||||
help: 'Number of times a toggle has been updated',
|
||||
@ -45,6 +51,10 @@ exports.startMonitoring = (
|
||||
requestDuration.labels(path, method, statusCode).observe(time);
|
||||
});
|
||||
|
||||
eventBus.on(events.DB_TIME, ({ store, action, time }) => {
|
||||
dbDuration.labels(store, action).observe(time);
|
||||
});
|
||||
|
||||
eventStore.on(FEATURE_CREATED, ({ data }) => {
|
||||
featureToggleUpdateTotal.labels(data.name).inc();
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ const { EventEmitter } = require('events');
|
||||
const eventBus = new EventEmitter();
|
||||
const eventStore = new EventEmitter();
|
||||
const clientMetricsStore = new EventEmitter();
|
||||
const { REQUEST_TIME } = require('./events');
|
||||
const { REQUEST_TIME, DB_TIME } = require('./events');
|
||||
const { FEATURE_UPDATED } = require('./event-type');
|
||||
const { startMonitoring } = require('./metrics');
|
||||
const { register: prometheusRegister } = require('prom-client');
|
||||
@ -56,3 +56,17 @@ test('should collect metrics for client metric reports', t => {
|
||||
/feature_toggle_usage_total{toggle="TestToggle",active="true"} 10\nfeature_toggle_usage_total{toggle="TestToggle",active="false"} 5/
|
||||
);
|
||||
});
|
||||
|
||||
test('should collect metrics for db query timings', t => {
|
||||
eventBus.emit(DB_TIME, {
|
||||
store: 'foo',
|
||||
action: 'bar',
|
||||
time: 0.1337,
|
||||
});
|
||||
|
||||
const metrics = prometheusRegister.metrics();
|
||||
t.regex(
|
||||
metrics,
|
||||
/db_query_duration_seconds{quantile="0\.99",store="foo",action="bar"} 0.1337/
|
||||
);
|
||||
});
|
||||
|
@ -16,8 +16,8 @@ const { addEventHook } = require('./event-hook');
|
||||
async function createApp(options) {
|
||||
// Database dependencies (stateful)
|
||||
const logger = options.getLogger('server-impl.js');
|
||||
const stores = createStores(options);
|
||||
const eventBus = new EventEmitter();
|
||||
const stores = createStores(options, eventBus);
|
||||
|
||||
const config = Object.assign(
|
||||
{
|
||||
|
28
lib/timer.js
Normal file
28
lib/timer.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const NS_TO_S = 1e9;
|
||||
|
||||
// seconds takes a tuple of [seconds, nanoseconds]
|
||||
// and returns the time in seconds
|
||||
const seconds = diff => diff[0] + diff[1] / NS_TO_S;
|
||||
|
||||
module.exports = {
|
||||
// new returns a timer function. Call it to measure the time since the call to new().
|
||||
// the timer function returns the duration in seconds
|
||||
//
|
||||
// usage:
|
||||
//
|
||||
// t = timer.new()
|
||||
// setTimeout(() => {
|
||||
// diff = t()
|
||||
// console.log(diff) // 0.500003192s
|
||||
// }, 500)
|
||||
//
|
||||
new: () => {
|
||||
const now = process.hrtime();
|
||||
// the timer function returns the time in seconds
|
||||
// since new() was called
|
||||
return () => seconds(process.hrtime(now));
|
||||
},
|
||||
seconds,
|
||||
};
|
32
lib/timer.test.js
Normal file
32
lib/timer.test.js
Normal file
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('ava');
|
||||
const timer = require('./timer');
|
||||
|
||||
function timeout(fn, ms) {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(() => {
|
||||
fn();
|
||||
resolve();
|
||||
}, ms)
|
||||
);
|
||||
}
|
||||
|
||||
test('should calculate the correct time in seconds', t => {
|
||||
t.is(timer.seconds([1, 0]), 1);
|
||||
t.is(timer.seconds([0, 1e6]), 0.001);
|
||||
t.is(timer.seconds([1, 1e6]), 1.001);
|
||||
t.is(timer.seconds([1, 552]), 1.000000552);
|
||||
});
|
||||
|
||||
test('timer should track the time', async t => {
|
||||
const tt = timer.new();
|
||||
let diff;
|
||||
await timeout(() => {
|
||||
diff = tt();
|
||||
}, 20);
|
||||
if (diff > 0.019 && diff < 0.05) {
|
||||
return t.pass();
|
||||
}
|
||||
t.fail();
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const migrator = require('../../../migrator');
|
||||
const { createStores } = require('../../../lib/db');
|
||||
const { createDb } = require('../../../lib/db/db-pool');
|
||||
@ -59,11 +60,12 @@ module.exports = async function init(databaseSchema = 'test', getLogger) {
|
||||
};
|
||||
|
||||
const db = createDb(options);
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
await db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`);
|
||||
await migrator(options);
|
||||
await db.destroy();
|
||||
const stores = await createStores(options);
|
||||
const stores = await createStores(options, eventBus);
|
||||
await resetDatabase(stores);
|
||||
await setupDatabase(stores);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user