1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +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:
Moritz Johner 2019-08-04 11:10:51 +02:00 committed by Ivar Conradi Østhus
parent a7cd7f76c5
commit d0f57a68b2
13 changed files with 187 additions and 21 deletions

View File

@ -1,6 +1,9 @@
/* eslint camelcase:off */ /* eslint camelcase:off */
'use strict'; 'use strict';
const metricsHelper = require('./metrics-helper');
const { DB_TIME } = require('../events');
const COLUMNS = [ const COLUMNS = [
'app_name', 'app_name',
'created_at', 'created_at',
@ -35,15 +38,22 @@ const remapRow = (input, old = {}) => ({
}); });
class ClientApplicationsDb { class ClientApplicationsDb {
constructor(db) { constructor(db, eventBus) {
this.db = db; this.db = db;
this.eventBus = eventBus;
} }
updateRow(details, prev) { updateRow(details, prev) {
details.updatedAt = 'now()'; details.updatedAt = 'now()';
return this.db(TABLE) return this.db(TABLE)
.where('app_name', details.appName) .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) { insertNewRow(details) {
@ -63,7 +73,13 @@ class ClientApplicationsDb {
} else { } else {
return this.insertNewRow(data); return this.insertNewRow(data);
} }
}); })
.then(
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
store: 'applications',
action: 'upsert',
})
);
} }
getAll() { getAll() {
@ -71,7 +87,13 @@ class ClientApplicationsDb {
.select(COLUMNS) .select(COLUMNS)
.from(TABLE) .from(TABLE)
.orderBy('app_name', 'asc') .orderBy('app_name', 'asc')
.map(mapRow); .map(mapRow)
.then(
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
store: 'applications',
action: 'getAll',
})
);
} }
getApplication(appName) { getApplication(appName) {

View File

@ -1,6 +1,9 @@
/* eslint camelcase: "off" */ /* eslint camelcase: "off" */
'use strict'; 'use strict';
const metricsHelper = require('./metrics-helper');
const { DB_TIME } = require('../events');
const COLUMNS = [ const COLUMNS = [
'app_name', 'app_name',
'instance_id', 'instance_id',
@ -23,8 +26,9 @@ const mapRow = row => ({
}); });
class ClientInstanceStore { class ClientInstanceStore {
constructor(db, getLogger) { constructor(db, eventBus, getLogger) {
this.db = db; this.db = db;
this.eventBus = eventBus;
this.logger = getLogger('client-instance-store.js'); this.logger = getLogger('client-instance-store.js');
const clearer = () => this._removeInstancesOlderThanTwoDays(); const clearer = () => this._removeInstancesOlderThanTwoDays();
setTimeout(clearer, 10).unref(); setTimeout(clearer, 10).unref();
@ -72,7 +76,13 @@ class ClientInstanceStore {
} else { } else {
return this.insertNewRow(details); return this.insertNewRow(details);
} }
}); })
.then(
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
store: 'instance',
action: 'insert',
})
);
} }
getAll() { getAll() {
@ -80,7 +90,13 @@ class ClientInstanceStore {
.select(COLUMNS) .select(COLUMNS)
.from(TABLE) .from(TABLE)
.orderBy('last_seen', 'desc') .orderBy('last_seen', 'desc')
.map(mapRow); .map(mapRow)
.then(
metricsHelper.wrapTimer(this.eventBus, DB_TIME, {
store: 'instance',
action: 'getAll',
})
);
} }
getByAppName(appName) { getByAppName(appName) {

View File

@ -1,14 +1,17 @@
'use strict'; 'use strict';
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const metricsHelper = require('./metrics-helper');
const { DB_TIME } = require('../events');
const TEN_SECONDS = 10 * 1000; const TEN_SECONDS = 10 * 1000;
class ClientMetricsStore extends EventEmitter { class ClientMetricsStore extends EventEmitter {
constructor(metricsDb, getLogger, pollInterval = TEN_SECONDS) { constructor(metricsDb, eventBus, getLogger, pollInterval = TEN_SECONDS) {
super(); super();
this.logger = getLogger('client-metrics-store.js'); this.logger = getLogger('client-metrics-store.js');
this.metricsDb = metricsDb; this.metricsDb = metricsDb;
this.eventBus = eventBus;
this.highestIdSeen = 0; this.highestIdSeen = 0;
this._init(pollInterval); this._init(pollInterval);
@ -45,7 +48,12 @@ class ClientMetricsStore extends EventEmitter {
// Insert new client metrics // Insert new client metrics
insert(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() { destroy() {

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const test = require('ava'); const test = require('ava');
const { EventEmitter } = require('events');
const ClientMetricStore = require('./client-metrics-store'); const ClientMetricStore = require('./client-metrics-store');
const lolex = require('lolex'); const lolex = require('lolex');
const getLogger = require('../../test/fixtures/no-logger'); const getLogger = require('../../test/fixtures/no-logger');
@ -24,8 +25,8 @@ function getMockDb() {
test.cb('should call database on startup', t => { test.cb('should call database on startup', t => {
const mock = getMockDb(); const mock = getMockDb();
const ee = new EventEmitter();
const store = new ClientMetricStore(mock, getLogger); const store = new ClientMetricStore(mock, ee, getLogger);
t.plan(2); t.plan(2);
@ -42,8 +43,8 @@ test.cb('should start poller even if inital database fetch fails', t => {
const clock = lolex.install(); const clock = lolex.install();
const mock = getMockDb(); const mock = getMockDb();
mock.getMetricsLastHour = () => Promise.reject('oops'); mock.getMetricsLastHour = () => Promise.reject('oops');
const ee = new EventEmitter();
const store = new ClientMetricStore(mock, getLogger, 100); const store = new ClientMetricStore(mock, ee, getLogger, 100);
const metrics = []; const metrics = [];
store.on('metrics', m => metrics.push(m)); store.on('metrics', m => metrics.push(m));
@ -64,7 +65,8 @@ test.cb('should poll for updates', t => {
const clock = lolex.install(); const clock = lolex.install();
const mock = getMockDb(); const mock = getMockDb();
const store = new ClientMetricStore(mock, getLogger, 100); const ee = new EventEmitter();
const store = new ClientMetricStore(mock, ee, getLogger, 100);
const metrics = []; const metrics = [];
store.on('metrics', m => metrics.push(m)); store.on('metrics', m => metrics.push(m));

View File

@ -9,7 +9,7 @@ const ClientMetricsDb = require('./client-metrics-db');
const ClientMetricsStore = require('./client-metrics-store'); const ClientMetricsStore = require('./client-metrics-store');
const ClientApplicationsStore = require('./client-applications-store'); const ClientApplicationsStore = require('./client-applications-store');
module.exports.createStores = config => { module.exports.createStores = (config, eventBus) => {
const getLogger = config.getLogger; const getLogger = config.getLogger;
const db = createDb(config); const db = createDb(config);
const eventStore = new EventStore(db, getLogger); const eventStore = new EventStore(db, getLogger);
@ -20,8 +20,16 @@ module.exports.createStores = config => {
eventStore, eventStore,
featureToggleStore: new FeatureToggleStore(db, eventStore, getLogger), featureToggleStore: new FeatureToggleStore(db, eventStore, getLogger),
strategyStore: new StrategyStore(db, eventStore, getLogger), strategyStore: new StrategyStore(db, eventStore, getLogger),
clientApplicationsStore: new ClientApplicationsStore(db, getLogger), clientApplicationsStore: new ClientApplicationsStore(
clientInstanceStore: new ClientInstanceStore(db, getLogger), db,
clientMetricsStore: new ClientMetricsStore(clientMetricsDb, getLogger), eventBus,
getLogger
),
clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger),
clientMetricsStore: new ClientMetricsStore(
clientMetricsDb,
eventBus,
getLogger
),
}; };
}; };

23
lib/db/metrics-helper.js Normal file
View 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,
};

View File

@ -2,4 +2,5 @@
module.exports = { module.exports = {
REQUEST_TIME: 'request_time', REQUEST_TIME: 'request_time',
DB_TIME: 'db_time',
}; };

View File

@ -30,6 +30,12 @@ exports.startMonitoring = (
labelNames: ['path', 'method', 'status'], labelNames: ['path', 'method', 'status'],
percentiles: [0.1, 0.5, 0.9, 0.99], 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({ const featureToggleUpdateTotal = new client.Counter({
name: 'feature_toggle_update_total', name: 'feature_toggle_update_total',
help: 'Number of times a toggle has been updated', help: 'Number of times a toggle has been updated',
@ -45,6 +51,10 @@ exports.startMonitoring = (
requestDuration.labels(path, method, statusCode).observe(time); 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 }) => { eventStore.on(FEATURE_CREATED, ({ data }) => {
featureToggleUpdateTotal.labels(data.name).inc(); featureToggleUpdateTotal.labels(data.name).inc();
}); });

View File

@ -5,7 +5,7 @@ const { EventEmitter } = require('events');
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
const eventStore = new EventEmitter(); const eventStore = new EventEmitter();
const clientMetricsStore = 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 { FEATURE_UPDATED } = require('./event-type');
const { startMonitoring } = require('./metrics'); const { startMonitoring } = require('./metrics');
const { register: prometheusRegister } = require('prom-client'); 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/ /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/
);
});

View File

@ -16,8 +16,8 @@ const { addEventHook } = require('./event-hook');
async function createApp(options) { async function createApp(options) {
// Database dependencies (stateful) // Database dependencies (stateful)
const logger = options.getLogger('server-impl.js'); const logger = options.getLogger('server-impl.js');
const stores = createStores(options);
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
const stores = createStores(options, eventBus);
const config = Object.assign( const config = Object.assign(
{ {

28
lib/timer.js Normal file
View 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
View 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();
});

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const { EventEmitter } = require('events');
const migrator = require('../../../migrator'); const migrator = require('../../../migrator');
const { createStores } = require('../../../lib/db'); const { createStores } = require('../../../lib/db');
const { createDb } = require('../../../lib/db/db-pool'); const { createDb } = require('../../../lib/db/db-pool');
@ -59,11 +60,12 @@ module.exports = async function init(databaseSchema = 'test', getLogger) {
}; };
const db = createDb(options); const db = createDb(options);
const eventBus = new EventEmitter();
await db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`); await db.raw(`CREATE SCHEMA IF NOT EXISTS ${options.databaseSchema}`);
await migrator(options); await migrator(options);
await db.destroy(); await db.destroy();
const stores = await createStores(options); const stores = await createStores(options, eventBus);
await resetDatabase(stores); await resetDatabase(stores);
await setupDatabase(stores); await setupDatabase(stores);