1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

fix: add feature metrics summary endpoint

This commit is contained in:
Ivar Conradi Østhus 2021-10-07 12:32:47 +02:00
parent 5f40560b23
commit 9a3404fc01
No known key found for this signature in database
GPG Key ID: 31AC596886B0BD09
7 changed files with 145 additions and 117 deletions

View File

@ -19,7 +19,8 @@ interface ClientMetricsEnvTable {
const TABLE = 'client_metrics_env';
function roundDownToHour(date) {
// Unsure if this would be better be done by the service?
export function roundDownToHour(date: Date): Date {
let p = 60 * 60 * 1000; // milliseconds in an hour
return new Date(Math.floor(date.getTime() / p) * p);
}
@ -72,7 +73,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
}
deleteAll(): Promise<void> {
throw new Error('Method not implemented.');
return this.db(TABLE).del();
}
destroy(): void {

View File

@ -21,10 +21,11 @@ class ClientMetricsController extends Controller {
this.metrics = clientMetricsServiceV2;
this.get('/features/:name', this.getFeatureToggleMetrics);
this.get('/features/:name/raw', this.getRawToggleMetrics);
this.get('/features/:name', this.getToggleMetricsSummary);
}
async getFeatureToggleMetrics(req: Request, res: Response): Promise<void> {
async getRawToggleMetrics(req: Request, res: Response): Promise<void> {
const { name } = req.params;
const data = await this.metrics.getClientMetricsForToggle(name);
res.json({
@ -33,5 +34,15 @@ class ClientMetricsController extends Controller {
data,
});
}
async getToggleMetricsSummary(req: Request, res: Response): Promise<void> {
const { name } = req.params;
const data = await this.metrics.getFeatureToggleMetricsSummary(name);
res.json({
version: 1,
maturity: 'experimental',
...data,
});
}
}
export default ClientMetricsController;

View File

@ -2,13 +2,12 @@ import { Logger } from '../../logger';
import { IUnleashConfig } from '../../server-impl';
import { IUnleashStores } from '../../types';
import { IClientApp } from '../../types/model';
import { GroupedClientMetrics } from '../../types/models/metrics';
import { ToggleMetricsSummary } from '../../types/models/metrics';
import {
IClientMetricsEnv,
IClientMetricsStoreV2,
} from '../../types/stores/client-metrics-store-v2';
import { clientMetricsSchema } from './client-metrics-schema';
import { groupMetricsOnEnv } from './util';
const FIVE_MINUTES = 5 * 60 * 1000;
@ -57,14 +56,45 @@ export default class ClientMetricsServiceV2 {
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
}
async getClientMetricsForToggle(
toggleName: string,
): Promise<GroupedClientMetrics[]> {
// Overview over usage last "hour" bucket and all applications using the toggle
async getFeatureToggleMetricsSummary(
featureName: string,
): Promise<ToggleMetricsSummary> {
const metrics =
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
toggleName,
featureName,
1,
);
const seenApplications =
await this.clientMetricsStoreV2.getSeenAppsForFeatureToggle(
featureName,
);
return groupMetricsOnEnv(metrics);
const groupedMetrics = metrics.reduce((prev, curr) => {
if (prev[curr.environment]) {
prev[curr.environment].yes += curr.yes;
prev[curr.environment].no += curr.no;
} else {
prev[curr.environment] = {
environment: curr.environment,
timestamp: curr.timestamp,
yes: curr.yes,
no: curr.no,
};
}
return prev;
}, {});
return {
featureName,
lastHourUsage: Object.values(groupedMetrics),
seenApplications,
};
}
async getClientMetricsForToggle(
toggleName: string,
): Promise<IClientMetricsEnv[]> {
return this.clientMetricsStoreV2.getMetricsForFeatureToggle(toggleName);
}
}

View File

@ -10,48 +10,3 @@ test('should return list of 24 horus', () => {
expect(hours[2]).toStrictEqual(new Date(2021, 10, 10, 13, 0, 0));
expect(hours[23]).toStrictEqual(new Date(2021, 10, 9, 16, 0, 0));
});
test('should group metrics together', () => {
const date = roundDownToHour(new Date());
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 3,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'test',
timestamp: date,
yes: 1,
no: 3,
},
];
const grouped = groupMetricsOnEnv(metrics);
expect(grouped[0]).toStrictEqual({
timestamp: date,
environment: 'default',
yes_count: 5,
no_count: 4,
});
expect(grouped[1]).toStrictEqual({
timestamp: date,
environment: 'test',
yes_count: 1,
no_count: 3,
});
});

View File

@ -1,48 +1,2 @@
import { GroupedClientMetrics } from '../../types/models/metrics';
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
//duplicate from client-metrics-store-v2.ts
export function roundDownToHour(date: Date): Date {
let p = 60 * 60 * 1000; // milliseconds in an hour
return new Date(Math.floor(date.getTime() / p) * p);
}
export function generateLastNHours(n: number, start: Date): Date[] {
const nHours: Date[] = [];
nHours.push(roundDownToHour(start));
for (let i = 1; i < n; i++) {
const prev = nHours[i - 1];
const next = new Date(prev);
next.setHours(prev.getHours() - 1);
nHours.push(next);
}
return nHours;
}
export function groupMetricsOnEnv(
metrics: IClientMetricsEnv[],
): GroupedClientMetrics[] {
const hours = generateLastNHours(24, new Date());
const environments = metrics.map((m) => m.environment);
const grouped = {};
hours.forEach((time) => {
environments.forEach((environment) => {
grouped[`${time}:${environment}`] = {
timestamp: time,
environment,
yes_count: 0,
no_count: 0,
};
});
});
metrics.forEach((m) => {
grouped[`${m.timestamp}:${m.environment}`].yes_count += m.yes;
grouped[`${m.timestamp}:${m.environment}`].no_count += m.no;
});
return Object.values(grouped);
}

View File

@ -1,6 +1,12 @@
export interface GroupedClientMetrics {
environment: string;
timestamp: Date;
yes_count: number;
no_count: number;
yes: number;
no: number;
}
export interface ToggleMetricsSummary {
featureName: string;
lastHourUsage: GroupedClientMetrics[];
seenApplications: string[];
}

View File

@ -1,7 +1,6 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { roundDownToHour } from '../../../../lib/services/client-metrics/util';
import { IClientMetricsEnv } from '../../../../lib/types/stores/client-metrics-store-v2';
let app;
@ -22,10 +21,11 @@ afterAll(async () => {
afterEach(async () => {
await db.reset();
await db.stores.clientMetricsStoreV2.deleteAll();
});
test('should return grouped metrics', async () => {
const date = roundDownToHour(new Date());
test('should return raw metrics, aggregated on key', async () => {
const date = new Date();
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
@ -72,24 +72,95 @@ test('should return grouped metrics', async () => {
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
const { body: demo } = await app.request
.get('/api/admin/client-metrics/features/demo')
.get('/api/admin/client-metrics/features/demo/raw')
.expect('Content-Type', /json/)
.expect(200);
const { body: t2 } = await app.request
.get('/api/admin/client-metrics/features/t2')
.get('/api/admin/client-metrics/features/t2/raw')
.expect('Content-Type', /json/)
.expect(200);
expect(demo.data).toHaveLength(48);
expect(demo.data).toHaveLength(2);
expect(demo.data[0].environment).toBe('default');
expect(demo.data[0].yes_count).toBe(5);
expect(demo.data[0].no_count).toBe(4);
expect(demo.data[0].yes).toBe(5);
expect(demo.data[0].no).toBe(4);
expect(demo.data[1].environment).toBe('test');
expect(demo.data[1].yes_count).toBe(1);
expect(demo.data[1].no_count).toBe(3);
expect(demo.data[1].yes).toBe(1);
expect(demo.data[1].no).toBe(3);
expect(t2.data).toHaveLength(24);
expect(t2.data).toHaveLength(1);
expect(t2.data[0].environment).toBe('default');
expect(t2.data[0].yes_count).toBe(7);
expect(t2.data[0].no_count).toBe(104);
expect(t2.data[0].yes).toBe(7);
expect(t2.data[0].no).toBe(104);
});
test('should toggle summary', async () => {
const date = new Date();
const metrics: IClientMetricsEnv[] = [
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 2,
},
{
featureName: 't2',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 5,
no: 5,
},
{
featureName: 't2',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 2,
no: 99,
},
{
featureName: 'demo',
appName: 'web',
environment: 'default',
timestamp: date,
yes: 3,
no: 2,
},
{
featureName: 'demo',
appName: 'web',
environment: 'test',
timestamp: date,
yes: 1,
no: 3,
},
{
featureName: 'demo',
appName: 'backend-api',
environment: 'test',
timestamp: date,
yes: 1,
no: 3,
},
];
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
const { body: demo } = await app.request
.get('/api/admin/client-metrics/features/demo')
.expect('Content-Type', /json/)
.expect(200);
expect(demo.featureName).toBe('demo');
expect(demo.lastHourUsage).toHaveLength(2);
expect(demo.lastHourUsage[0].environment).toBe('default');
expect(demo.lastHourUsage[0].yes).toBe(5);
expect(demo.lastHourUsage[0].no).toBe(4);
expect(demo.lastHourUsage[1].environment).toBe('test');
expect(demo.lastHourUsage[1].yes).toBe(2);
expect(demo.lastHourUsage[1].no).toBe(6);
expect(demo.seenApplications).toStrictEqual(['backend-api', 'web']);
});