1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

feat: implement a store for stat_traffic_data (#6190)

## About the changes

Implements a new store for collected traffic data usage that connects to
the new table `stat_traffic_data` primary key'd on [day, trafficGroup,
status_code_series].

Day being a date
Traffic group being which endpoint is being counted for, ie /api/admin,
/api/frontend etc
Status code series grouping 2xx status responses and 304 into their
respective 200 / 300 series.

No service here, this is for pro/enterprise
This commit is contained in:
David Leek 2024-02-12 08:39:51 +01:00 committed by GitHub
parent 70a957c615
commit ccd2fee4ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 299 additions and 0 deletions

View File

@ -41,6 +41,7 @@ import { DependentFeaturesStore } from '../features/dependent-features/dependent
import LastSeenStore from '../features/metrics/last-seen/last-seen-store';
import FeatureSearchStore from '../features/feature-search/feature-search-store';
import { InactiveUsersStore } from '../users/inactive/inactive-users-store';
import { TrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store';
export const createStores = (
config: IUnleashConfig,
@ -143,6 +144,7 @@ export const createStores = (
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
featureSearchStore: new FeatureSearchStore(db, eventBus, getLogger),
inactiveUsersStore: new InactiveUsersStore(db, eventBus, getLogger),
trafficDataUsageStore: new TrafficDataUsageStore(db, getLogger),
};
};

View File

@ -0,0 +1,29 @@
import {
IStatTrafficUsageKey,
IStatTrafficUsage,
} from './traffic-data-usage-store-type';
import { ITrafficDataUsageStore } from '../../types';
export class FakeTrafficDataUsageStore implements ITrafficDataUsageStore {
get(key: IStatTrafficUsageKey): Promise<IStatTrafficUsage> {
throw new Error('Method not implemented.');
}
getAll(query?: Object | undefined): Promise<IStatTrafficUsage[]> {
throw new Error('Method not implemented.');
}
exists(key: IStatTrafficUsageKey): Promise<boolean> {
throw new Error('Method not implemented.');
}
delete(key: IStatTrafficUsageKey): Promise<void> {
throw new Error('Method not implemented.');
}
deleteAll(): Promise<void> {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void> {
throw new Error('Method not implemented.');
}
}

View File

@ -0,0 +1,19 @@
import { Store } from '../../types/stores/store';
export type IStatTrafficUsage = {
day: Date;
trafficGroup: string;
statusCodeSeries: number;
count: number | string;
};
export interface IStatTrafficUsageKey {
day: Date;
trafficGroup: string;
statusCodeSeries: number;
}
export interface ITrafficDataUsageStore
extends Store<IStatTrafficUsage, IStatTrafficUsageKey> {
upsert(trafficDataUsage: IStatTrafficUsage): Promise<void>;
}

View File

@ -0,0 +1,154 @@
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import { ITrafficDataUsageStore, IUnleashStores } from '../../types';
let stores: IUnleashStores;
let db: ITestDb;
let trafficDataUsageStore: ITrafficDataUsageStore;
beforeAll(async () => {
db = await dbInit('traffic_data_usage_serial', getLogger, {
experimental: {
flags: {},
},
});
stores = db.stores;
trafficDataUsageStore = stores.trafficDataUsageStore;
});
afterAll(async () => {
await db.destroy();
});
test('upsert stores new entries', async () => {
const data = {
day: new Date(),
trafficGroup: 'default',
statusCodeSeries: 200,
count: '1',
};
await trafficDataUsageStore.upsert(data);
const data2 = await trafficDataUsageStore.get({
day: data.day,
trafficGroup: data.trafficGroup,
statusCodeSeries: data.statusCodeSeries,
});
expect(data2).toBeDefined();
expect(data2.count).toBe('1');
});
test('upsert upserts', async () => {
const data = {
day: new Date(),
trafficGroup: 'default2',
statusCodeSeries: 200,
count: '1',
};
const dataSecondTime = {
day: new Date(),
trafficGroup: 'default2',
statusCodeSeries: 200,
count: '3',
};
await trafficDataUsageStore.upsert(data);
await trafficDataUsageStore.upsert(dataSecondTime);
const data2 = await trafficDataUsageStore.get({
day: data.day,
trafficGroup: data.trafficGroup,
statusCodeSeries: data.statusCodeSeries,
});
expect(data2).toBeDefined();
expect(data2.count).toBe('4');
});
test('getAll returns all', async () => {
trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
count: '1',
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
count: '3',
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
const results = await trafficDataUsageStore.getAll();
expect(results).toBeDefined();
expect(results.length).toBe(2);
});
test('delete deletes the specified item', async () => {
trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
count: '1',
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
count: '3',
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
await trafficDataUsageStore.delete({
day: data1.day,
trafficGroup: data1.trafficGroup,
statusCodeSeries: data1.statusCodeSeries,
});
const results = await trafficDataUsageStore.getAll();
expect(results).toBeDefined();
expect(results.length).toBe(1);
expect(results[0].trafficGroup).toBe('default4');
});
test('can query for specific items', async () => {
trafficDataUsageStore.deleteAll();
const data1 = {
day: new Date(),
trafficGroup: 'default3',
statusCodeSeries: 200,
count: '1',
};
const data2 = {
day: new Date(),
trafficGroup: 'default4',
statusCodeSeries: 200,
count: '3',
};
const data3 = {
day: new Date(),
trafficGroup: 'default5',
statusCodeSeries: 200,
count: '2',
};
await trafficDataUsageStore.upsert(data1);
await trafficDataUsageStore.upsert(data2);
await trafficDataUsageStore.upsert(data3);
const results_traffic_group = await trafficDataUsageStore.getAll({
traffic_group: data1.trafficGroup,
});
expect(results_traffic_group).toBeDefined();
expect(results_traffic_group.length).toBe(1);
expect(results_traffic_group[0].trafficGroup).toBe('default3');
const results_day = await trafficDataUsageStore.getAll({
day: data2.day,
});
expect(results_day).toBeDefined();
expect(results_day.length).toBe(3);
const results_status_code = await trafficDataUsageStore.getAll({
status_code_series: 200,
});
expect(results_status_code).toBeDefined();
expect(results_status_code.length).toBe(3);
});

View File

@ -0,0 +1,90 @@
import { Db } from '../../db/db';
import { Logger, LogProvider } from '../../logger';
import {
IStatTrafficUsage,
IStatTrafficUsageKey,
ITrafficDataUsageStore,
} from './traffic-data-usage-store-type';
const TABLE = 'stat_traffic_usage';
const COLUMNS = ['day', 'traffic_group', 'status_code_series', 'count'];
const toRow = (trafficDataUsage: IStatTrafficUsage) => {
return {
day: trafficDataUsage.day,
traffic_group: trafficDataUsage.trafficGroup,
status_code_series: trafficDataUsage.statusCodeSeries,
count: trafficDataUsage.count,
};
};
const mapRow = (row: any): IStatTrafficUsage => {
return {
day: row.day,
trafficGroup: row.traffic_group,
statusCodeSeries: row.status_code_series,
count: row.count,
};
};
export class TrafficDataUsageStore implements ITrafficDataUsageStore {
private db: Db;
private logger: Logger;
constructor(db: Db, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('traffic-data-usage-store.ts');
}
async get(key: IStatTrafficUsageKey): Promise<IStatTrafficUsage> {
const row = await this.db
.table(TABLE)
.select()
.where({
day: key.day,
traffic_group: key.trafficGroup,
status_code_series: key.statusCodeSeries,
})
.first();
return mapRow(row);
}
async getAll(query = {}): Promise<IStatTrafficUsage[]> {
const rows = await this.db.select(COLUMNS).where(query).from(TABLE);
return rows.map(mapRow);
}
async exists(key: IStatTrafficUsageKey): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE
day = ? AND
traffic_group = ? AND
status_code_series ?) AS present`,
[key.day, key.trafficGroup, key.statusCodeSeries],
);
const { present } = result.rows[0];
return present;
}
async delete(key: IStatTrafficUsageKey): Promise<void> {
await this.db(TABLE)
.where({
day: key.day,
traffic_group: key.trafficGroup,
status_code_series: key.statusCodeSeries,
})
.del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
destroy(): void {}
async upsert(trafficDataUsage: IStatTrafficUsage): Promise<void> {
const row = toRow(trafficDataUsage);
await this.db(TABLE)
.insert(row)
.onConflict(['day', 'traffic_group', 'status_code_series'])
.merge({
count: this.db.raw('stat_traffic_usage.count + EXCLUDED.count'),
});
}
}

View File

@ -38,6 +38,7 @@ import { IDependentFeaturesStore } from '../features/dependent-features/dependen
import { ILastSeenStore } from '../features/metrics/last-seen/types/last-seen-store-type';
import { IFeatureSearchStore } from '../features/feature-search/feature-search-store-type';
import { IInactiveUsersStore } from '../users/inactive/types/inactive-users-store-type';
import { ITrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store-type';
export interface IUnleashStores {
accessStore: IAccessStore;
@ -80,6 +81,7 @@ export interface IUnleashStores {
lastSeenStore: ILastSeenStore;
featureSearchStore: IFeatureSearchStore;
inactiveUsersStore: IInactiveUsersStore;
trafficDataUsageStore: ITrafficDataUsageStore;
}
export {
@ -121,4 +123,5 @@ export {
IDependentFeaturesStore,
ILastSeenStore,
IFeatureSearchStore,
ITrafficDataUsageStore,
};

View File

@ -41,6 +41,7 @@ import { FakeDependentFeaturesStore } from '../../lib/features/dependent-feature
import { FakeLastSeenStore } from '../../lib/features/metrics/last-seen/fake-last-seen-store';
import FakeFeatureSearchStore from '../../lib/features/feature-search/fake-feature-search-store';
import { FakeInactiveUsersStore } from '../../lib/users/inactive/fakes/fake-inactive-users-store';
import { FakeTrafficDataUsageStore } from '../../lib/features/traffic-data-usage/fake-traffic-data-usage-store';
const db = {
select: () => ({
@ -91,6 +92,7 @@ const createStores: () => IUnleashStores = () => {
lastSeenStore: new FakeLastSeenStore(),
featureSearchStore: new FakeFeatureSearchStore(),
inactiveUsersStore: new FakeInactiveUsersStore(),
trafficDataUsageStore: new FakeTrafficDataUsageStore(),
};
};