mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
chore: add edge instances to instance stats (#10839)
https://linear.app/unleash/issue/2-3979/add-edge-instances-to-self-reported-instance-stats Adds edge instances to instance stats.
This commit is contained in:
parent
09fcbe3d49
commit
8ba35507cd
@ -964,6 +964,7 @@ export * from './instanceAdminStatsSchemaActiveUsers.js';
|
||||
export * from './instanceAdminStatsSchemaApiTokens.js';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItem.js';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItemRange.js';
|
||||
export * from './instanceAdminStatsSchemaEdgeInstances.js';
|
||||
export * from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||
export * from './instanceAdminStatsSchemaProductionChanges.js';
|
||||
export * from './instanceInsightsSchema.js';
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import type { InstanceAdminStatsSchemaActiveUsers } from './instanceAdminStatsSchemaActiveUsers.js';
|
||||
import type { InstanceAdminStatsSchemaApiTokens } from './instanceAdminStatsSchemaApiTokens.js';
|
||||
import type { InstanceAdminStatsSchemaClientAppsItem } from './instanceAdminStatsSchemaClientAppsItem.js';
|
||||
import type { InstanceAdminStatsSchemaEdgeInstances } from './instanceAdminStatsSchemaEdgeInstances.js';
|
||||
import type { InstanceAdminStatsSchemaPreviousDayMetricsBucketsCount } from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||
import type { InstanceAdminStatsSchemaProductionChanges } from './instanceAdminStatsSchemaProductionChanges.js';
|
||||
|
||||
@ -24,6 +25,8 @@ export interface InstanceAdminStatsSchema {
|
||||
* @minimum 0
|
||||
*/
|
||||
contextFields?: number;
|
||||
/** The rounded up average number of edge instances in the last month and month before last */
|
||||
edgeInstances?: InstanceAdminStatsSchemaEdgeInstances;
|
||||
/**
|
||||
* The number of environments defined in this instance
|
||||
* @minimum 0
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The rounded up average number of edge instances in the last month and month before last
|
||||
*/
|
||||
export type InstanceAdminStatsSchemaEdgeInstances = {
|
||||
/**
|
||||
* The rounded up average number of edge instances in the last month
|
||||
* @minimum 0
|
||||
*/
|
||||
lastMonth?: number;
|
||||
/**
|
||||
* The rounded up average number of edge instances in the month before last
|
||||
* @minimum 0
|
||||
*/
|
||||
monthBeforeLast?: number;
|
||||
};
|
||||
@ -50,6 +50,10 @@ import {
|
||||
} from './getLicensedUsers.js';
|
||||
import { ReleasePlanStore } from '../release-plans/release-plan-store.js';
|
||||
import { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js';
|
||||
import {
|
||||
createFakeGetEdgeInstances,
|
||||
createGetEdgeInstances,
|
||||
} from './getEdgeInstances.js';
|
||||
|
||||
export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
||||
const { eventBus, getLogger, flagResolver } = config;
|
||||
@ -134,6 +138,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
||||
const getActiveUsers = createGetActiveUsers(db);
|
||||
const getProductionChanges = createGetProductionChanges(db);
|
||||
const getLicencedUsers = createGetLicensedUsers(db);
|
||||
const getEdgeInstances = createGetEdgeInstances(db);
|
||||
const versionService = new VersionService(versionServiceStores, config);
|
||||
|
||||
const instanceStatsService = new InstanceStatsService(
|
||||
@ -143,6 +148,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
||||
getActiveUsers,
|
||||
getProductionChanges,
|
||||
getLicencedUsers,
|
||||
getEdgeInstances,
|
||||
);
|
||||
|
||||
return instanceStatsService;
|
||||
@ -199,6 +205,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
|
||||
const getActiveUsers = createFakeGetActiveUsers();
|
||||
const getLicensedUsers = createFakeGetLicensedUsers();
|
||||
const getProductionChanges = createFakeGetProductionChanges();
|
||||
const getEdgeInstances = createFakeGetEdgeInstances();
|
||||
const versionService = new VersionService(versionServiceStores, config);
|
||||
|
||||
const instanceStatsService = new InstanceStatsService(
|
||||
@ -208,6 +215,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
|
||||
getActiveUsers,
|
||||
getProductionChanges,
|
||||
getLicensedUsers,
|
||||
getEdgeInstances,
|
||||
);
|
||||
|
||||
return instanceStatsService;
|
||||
|
||||
132
src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts
Normal file
132
src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import {
|
||||
createGetEdgeInstances,
|
||||
type GetEdgeInstances,
|
||||
} from './getEdgeInstances.js';
|
||||
import dbInit, {
|
||||
type ITestDb,
|
||||
} from '../../../test/e2e/helpers/database-init.js';
|
||||
import getLogger from '../../../test/fixtures/no-logger.js';
|
||||
|
||||
let db: ITestDb;
|
||||
let getEdgeInstances: GetEdgeInstances;
|
||||
|
||||
const TABLE = 'edge_node_presence';
|
||||
|
||||
const firstDayOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
const addMonths = (d: Date, n: number) =>
|
||||
new Date(d.getFullYear(), d.getMonth() + n, 1);
|
||||
|
||||
const monthWindows = () => {
|
||||
const now = new Date();
|
||||
const thisMonthStart = firstDayOfMonth(now);
|
||||
const lastMonthStart = addMonths(thisMonthStart, -1);
|
||||
const monthBeforeLastStart = addMonths(thisMonthStart, -2);
|
||||
const lastMonthEnd = thisMonthStart;
|
||||
const monthBeforeLastEnd = lastMonthStart;
|
||||
return {
|
||||
monthBeforeLastStart,
|
||||
monthBeforeLastEnd,
|
||||
lastMonthStart,
|
||||
lastMonthEnd,
|
||||
thisMonthStart,
|
||||
};
|
||||
};
|
||||
|
||||
const atMidMonth = (start: Date) =>
|
||||
new Date(start.getFullYear(), start.getMonth(), 15);
|
||||
const atLateMonth = (start: Date) =>
|
||||
new Date(start.getFullYear(), start.getMonth(), 25);
|
||||
|
||||
const rowsForBucket = (count: number, when: Date) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
bucket_ts: when,
|
||||
node_ephem_id: `node-${when.getTime()}-${i}`,
|
||||
}));
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('edge_instances_e2e', getLogger);
|
||||
getEdgeInstances = createGetEdgeInstances(db.rawDatabase);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.rawDatabase(TABLE).delete();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('returns 0 for both months when no data', async () => {
|
||||
await expect(getEdgeInstances()).resolves.toEqual({
|
||||
lastMonth: 0,
|
||||
monthBeforeLast: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('counts only last full month', async () => {
|
||||
const { lastMonthStart } = monthWindows();
|
||||
const mid = atMidMonth(lastMonthStart);
|
||||
const late = atLateMonth(lastMonthStart);
|
||||
await db
|
||||
.rawDatabase(TABLE)
|
||||
.insert([...rowsForBucket(3, mid), ...rowsForBucket(7, late)]);
|
||||
await expect(getEdgeInstances()).resolves.toEqual({
|
||||
lastMonth: 5,
|
||||
monthBeforeLast: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('counts only month before last', async () => {
|
||||
const { monthBeforeLastStart } = monthWindows();
|
||||
const mid = atMidMonth(monthBeforeLastStart);
|
||||
const late = atLateMonth(monthBeforeLastStart);
|
||||
await db
|
||||
.rawDatabase(TABLE)
|
||||
.insert([...rowsForBucket(2, mid), ...rowsForBucket(5, late)]);
|
||||
await expect(getEdgeInstances()).resolves.toEqual({
|
||||
lastMonth: 0,
|
||||
monthBeforeLast: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('separates months correctly when both have data', async () => {
|
||||
const { monthBeforeLastStart, lastMonthStart } = monthWindows();
|
||||
const pMid = atMidMonth(monthBeforeLastStart);
|
||||
const pLate = atLateMonth(monthBeforeLastStart);
|
||||
const lMid = atMidMonth(lastMonthStart);
|
||||
const lLate = atLateMonth(lastMonthStart);
|
||||
|
||||
await db
|
||||
.rawDatabase(TABLE)
|
||||
.insert([
|
||||
...rowsForBucket(4, pMid),
|
||||
...rowsForBucket(6, pLate),
|
||||
...rowsForBucket(3, lMid),
|
||||
...rowsForBucket(7, lLate),
|
||||
]);
|
||||
|
||||
await expect(getEdgeInstances()).resolves.toEqual({
|
||||
lastMonth: 5,
|
||||
monthBeforeLast: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('ignores current month data', async () => {
|
||||
const { thisMonthStart, lastMonthStart } = monthWindows();
|
||||
const lMid = atMidMonth(lastMonthStart);
|
||||
const lLate = atLateMonth(lastMonthStart);
|
||||
const tMid = atMidMonth(thisMonthStart);
|
||||
|
||||
await db
|
||||
.rawDatabase(TABLE)
|
||||
.insert([
|
||||
...rowsForBucket(10, tMid),
|
||||
...rowsForBucket(2, lMid),
|
||||
...rowsForBucket(4, lLate),
|
||||
]);
|
||||
|
||||
await expect(getEdgeInstances()).resolves.toEqual({
|
||||
lastMonth: 3,
|
||||
monthBeforeLast: 0,
|
||||
});
|
||||
});
|
||||
69
src/lib/features/instance-stats/getEdgeInstances.ts
Normal file
69
src/lib/features/instance-stats/getEdgeInstances.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { Db } from '../../types/index.js';
|
||||
|
||||
const TABLE = 'edge_node_presence';
|
||||
|
||||
export type GetEdgeInstances = () => Promise<{
|
||||
lastMonth: number;
|
||||
monthBeforeLast: number;
|
||||
}>;
|
||||
|
||||
export const createGetEdgeInstances =
|
||||
(db: Db): GetEdgeInstances =>
|
||||
async () => {
|
||||
const result = await db
|
||||
.with('buckets', (qb) =>
|
||||
qb
|
||||
.from(TABLE)
|
||||
.whereRaw(
|
||||
"bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months'",
|
||||
)
|
||||
.groupBy('bucket_ts')
|
||||
.select(
|
||||
db.raw('bucket_ts'),
|
||||
db.raw('COUNT(*)::int AS active_nodes'),
|
||||
),
|
||||
)
|
||||
.from('buckets')
|
||||
.select({
|
||||
lastMonth: db.raw(`
|
||||
COALESCE(
|
||||
CEIL(
|
||||
AVG(active_nodes)
|
||||
FILTER (
|
||||
WHERE bucket_ts >= date_trunc('month', NOW()) - INTERVAL '1 month'
|
||||
AND bucket_ts < date_trunc('month', NOW())
|
||||
)
|
||||
)::int,
|
||||
0
|
||||
)
|
||||
`),
|
||||
monthBeforeLast: db.raw(`
|
||||
COALESCE(
|
||||
CEIL(
|
||||
AVG(active_nodes)
|
||||
FILTER (
|
||||
WHERE bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months'
|
||||
AND bucket_ts < date_trunc('month', NOW()) - INTERVAL '1 month'
|
||||
)
|
||||
)::int,
|
||||
0
|
||||
)
|
||||
`),
|
||||
})
|
||||
.first();
|
||||
|
||||
return {
|
||||
lastMonth: Number(result?.lastMonth ?? 0),
|
||||
monthBeforeLast: Number(result?.monthBeforeLast ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
export const createFakeGetEdgeInstances =
|
||||
(
|
||||
edgeInstances: Awaited<ReturnType<GetEdgeInstances>> = {
|
||||
lastMonth: 0,
|
||||
monthBeforeLast: 0,
|
||||
},
|
||||
): GetEdgeInstances =>
|
||||
() =>
|
||||
Promise.resolve(edgeInstances);
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
import { createFakeGetLicensedUsers } from './getLicensedUsers.js';
|
||||
import { vi } from 'vitest';
|
||||
import { DEFAULT_ENV } from '../../server-impl.js';
|
||||
import { createFakeGetEdgeInstances } from './getEdgeInstances.js';
|
||||
|
||||
let instanceStatsService: InstanceStatsService;
|
||||
let versionService: VersionService;
|
||||
@ -39,6 +40,7 @@ beforeEach(() => {
|
||||
createFakeGetActiveUsers(),
|
||||
createFakeGetProductionChanges(),
|
||||
createFakeGetLicensedUsers(),
|
||||
createFakeGetEdgeInstances(),
|
||||
);
|
||||
|
||||
const { collectAggDbMetrics } = registerPrometheusMetrics(
|
||||
|
||||
@ -33,6 +33,7 @@ import type { GetLicensedUsers } from './getLicensedUsers.js';
|
||||
import type { IFeatureUsageInfo } from '../../services/version-service.js';
|
||||
import type { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js';
|
||||
import type { ReleasePlanStore } from '../release-plans/release-plan-store.js';
|
||||
import type { GetEdgeInstances } from './getEdgeInstances.js';
|
||||
|
||||
export type TimeRange = 'allTime' | '30d' | '7d';
|
||||
|
||||
@ -74,6 +75,7 @@ export interface InstanceStats {
|
||||
maxConstraintValues: number;
|
||||
releaseTemplates?: number;
|
||||
releasePlans?: number;
|
||||
edgeInstances?: Awaited<ReturnType<GetEdgeInstances>>;
|
||||
}
|
||||
|
||||
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
||||
@ -124,6 +126,8 @@ export class InstanceStatsService {
|
||||
|
||||
getProductionChanges: GetProductionChanges;
|
||||
|
||||
getEdgeInstances: GetEdgeInstances;
|
||||
|
||||
private featureStrategiesReadModel: IFeatureStrategiesReadModel;
|
||||
|
||||
private featureStrategiesStore: IFeatureStrategiesStore;
|
||||
@ -185,6 +189,7 @@ export class InstanceStatsService {
|
||||
getActiveUsers: GetActiveUsers,
|
||||
getProductionChanges: GetProductionChanges,
|
||||
getLicencedUsers: GetLicensedUsers,
|
||||
getEdgeInstances: GetEdgeInstances,
|
||||
) {
|
||||
this.strategyStore = strategyStore;
|
||||
this.userStore = userStore;
|
||||
@ -209,6 +214,8 @@ export class InstanceStatsService {
|
||||
'getProductionChanges',
|
||||
getProductionChanges.bind(this),
|
||||
);
|
||||
this.getEdgeInstances = () =>
|
||||
this.memorize('getEdgeInstances', getEdgeInstances.bind(this));
|
||||
this.apiTokenStore = apiTokenStore;
|
||||
this.clientMetricsStore = clientMetricsStoreV2;
|
||||
this.flagResolver = flagResolver;
|
||||
@ -356,6 +363,7 @@ export class InstanceStatsService {
|
||||
maxConstraints,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
] = await Promise.all([
|
||||
this.getToggleCount(),
|
||||
this.getArchivedToggleCount(),
|
||||
@ -402,6 +410,7 @@ export class InstanceStatsService {
|
||||
),
|
||||
this.getReleaseTemplates(),
|
||||
this.getReleasePlans(),
|
||||
this.getEdgeInstances(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@ -442,6 +451,7 @@ export class InstanceStatsService {
|
||||
maxConstraints: maxConstraints?.count ?? 0,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
};
|
||||
}
|
||||
|
||||
@ -471,6 +481,7 @@ export class InstanceStatsService {
|
||||
hostedBy,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
] = await Promise.all([
|
||||
this.getToggleCount(),
|
||||
this.getRegisteredUsers(),
|
||||
@ -496,6 +507,7 @@ export class InstanceStatsService {
|
||||
this.getHostedBy(),
|
||||
this.getReleaseTemplates(),
|
||||
this.getReleasePlans(),
|
||||
this.getEdgeInstances(),
|
||||
]);
|
||||
const versionInfo = await this.versionService.getVersionInfo();
|
||||
|
||||
@ -533,6 +545,8 @@ export class InstanceStatsService {
|
||||
hostedBy,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstancesLastMonth: edgeInstances.lastMonth,
|
||||
edgeInstancesMonthBeforeLast: edgeInstances.monthBeforeLast,
|
||||
};
|
||||
return featureInfo;
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import dbInit, { type ITestDb } from '../test/e2e/helpers/database-init.js';
|
||||
import { FeatureLifecycleStore } from './features/feature-lifecycle/feature-lifecycle-store.js';
|
||||
import { FeatureLifecycleReadModel } from './features/feature-lifecycle/feature-lifecycle-read-model.js';
|
||||
import { createFakeGetLicensedUsers } from './features/instance-stats/getLicensedUsers.js';
|
||||
import { createFakeGetEdgeInstances } from './features/instance-stats/getEdgeInstances.js';
|
||||
|
||||
const monitor = createMetricsMonitor();
|
||||
const eventBus = new EventEmitter();
|
||||
@ -79,6 +80,7 @@ beforeAll(async () => {
|
||||
createFakeGetActiveUsers(),
|
||||
createFakeGetProductionChanges(),
|
||||
createFakeGetLicensedUsers(),
|
||||
createFakeGetEdgeInstances(),
|
||||
);
|
||||
|
||||
schedulerService = new SchedulerService(
|
||||
|
||||
@ -284,6 +284,27 @@ export const instanceAdminStatsSchema = {
|
||||
example: 1,
|
||||
description: 'The number of release plans in this instance',
|
||||
},
|
||||
edgeInstances: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the last month and month before last',
|
||||
properties: {
|
||||
lastMonth: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the last month',
|
||||
example: 10,
|
||||
minimum: 0,
|
||||
},
|
||||
monthBeforeLast: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the month before last',
|
||||
example: 12,
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
sum: {
|
||||
type: 'string',
|
||||
description:
|
||||
|
||||
@ -133,6 +133,10 @@ class InstanceAdminController extends Controller {
|
||||
maxConstraintValues: 123,
|
||||
releaseTemplates: 3,
|
||||
releasePlans: 5,
|
||||
edgeInstances: {
|
||||
lastMonth: 10,
|
||||
monthBeforeLast: 15,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,8 @@ const fakeTelemetryData = {
|
||||
hostedBy: 'self-hosted',
|
||||
releaseTemplates: 2,
|
||||
releasePlans: 4,
|
||||
edgeInstancesLastMonth: 0,
|
||||
edgeInstancesMonthBeforeLast: 0,
|
||||
};
|
||||
|
||||
test('yields current versions', async () => {
|
||||
|
||||
@ -54,6 +54,8 @@ export interface IFeatureUsageInfo {
|
||||
hostedBy: string;
|
||||
releaseTemplates: number;
|
||||
releasePlans: number;
|
||||
edgeInstancesLastMonth?: number;
|
||||
edgeInstancesMonthBeforeLast?: number;
|
||||
}
|
||||
|
||||
export default class VersionService {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user