mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +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:
parent
70a957c615
commit
ccd2fee4ee
@ -41,6 +41,7 @@ import { DependentFeaturesStore } from '../features/dependent-features/dependent
|
|||||||
import LastSeenStore from '../features/metrics/last-seen/last-seen-store';
|
import LastSeenStore from '../features/metrics/last-seen/last-seen-store';
|
||||||
import FeatureSearchStore from '../features/feature-search/feature-search-store';
|
import FeatureSearchStore from '../features/feature-search/feature-search-store';
|
||||||
import { InactiveUsersStore } from '../users/inactive/inactive-users-store';
|
import { InactiveUsersStore } from '../users/inactive/inactive-users-store';
|
||||||
|
import { TrafficDataUsageStore } from '../features/traffic-data-usage/traffic-data-usage-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -143,6 +144,7 @@ export const createStores = (
|
|||||||
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
|
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
|
||||||
featureSearchStore: new FeatureSearchStore(db, eventBus, getLogger),
|
featureSearchStore: new FeatureSearchStore(db, eventBus, getLogger),
|
||||||
inactiveUsersStore: new InactiveUsersStore(db, eventBus, getLogger),
|
inactiveUsersStore: new InactiveUsersStore(db, eventBus, getLogger),
|
||||||
|
trafficDataUsageStore: new TrafficDataUsageStore(db, getLogger),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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.');
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
||||||
|
}
|
@ -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);
|
||||||
|
});
|
@ -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'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ import { IDependentFeaturesStore } from '../features/dependent-features/dependen
|
|||||||
import { ILastSeenStore } from '../features/metrics/last-seen/types/last-seen-store-type';
|
import { ILastSeenStore } from '../features/metrics/last-seen/types/last-seen-store-type';
|
||||||
import { IFeatureSearchStore } from '../features/feature-search/feature-search-store-type';
|
import { IFeatureSearchStore } from '../features/feature-search/feature-search-store-type';
|
||||||
import { IInactiveUsersStore } from '../users/inactive/types/inactive-users-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 {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -80,6 +81,7 @@ export interface IUnleashStores {
|
|||||||
lastSeenStore: ILastSeenStore;
|
lastSeenStore: ILastSeenStore;
|
||||||
featureSearchStore: IFeatureSearchStore;
|
featureSearchStore: IFeatureSearchStore;
|
||||||
inactiveUsersStore: IInactiveUsersStore;
|
inactiveUsersStore: IInactiveUsersStore;
|
||||||
|
trafficDataUsageStore: ITrafficDataUsageStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -121,4 +123,5 @@ export {
|
|||||||
IDependentFeaturesStore,
|
IDependentFeaturesStore,
|
||||||
ILastSeenStore,
|
ILastSeenStore,
|
||||||
IFeatureSearchStore,
|
IFeatureSearchStore,
|
||||||
|
ITrafficDataUsageStore,
|
||||||
};
|
};
|
||||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -41,6 +41,7 @@ import { FakeDependentFeaturesStore } from '../../lib/features/dependent-feature
|
|||||||
import { FakeLastSeenStore } from '../../lib/features/metrics/last-seen/fake-last-seen-store';
|
import { FakeLastSeenStore } from '../../lib/features/metrics/last-seen/fake-last-seen-store';
|
||||||
import FakeFeatureSearchStore from '../../lib/features/feature-search/fake-feature-search-store';
|
import FakeFeatureSearchStore from '../../lib/features/feature-search/fake-feature-search-store';
|
||||||
import { FakeInactiveUsersStore } from '../../lib/users/inactive/fakes/fake-inactive-users-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 = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -91,6 +92,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
lastSeenStore: new FakeLastSeenStore(),
|
lastSeenStore: new FakeLastSeenStore(),
|
||||||
featureSearchStore: new FakeFeatureSearchStore(),
|
featureSearchStore: new FakeFeatureSearchStore(),
|
||||||
inactiveUsersStore: new FakeInactiveUsersStore(),
|
inactiveUsersStore: new FakeInactiveUsersStore(),
|
||||||
|
trafficDataUsageStore: new FakeTrafficDataUsageStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user