mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
chore: add bulk endpoint for metrics and app registration for edge (#3079)
## About the changes Implementation of bulk metrics and registration endpoint. This will be used by edge nodes to send all collected information. Types around metrics were improved and `IClientApp.bucket` with type `any` is no longer needed --------- Co-authored-by: sighphyre <liquidwicked64@gmail.com>
This commit is contained in:
parent
232ad28661
commit
7a242ecf2a
@ -23,6 +23,7 @@ import { IUnleashStores } from './types/stores';
|
||||
import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns';
|
||||
import Timer = NodeJS.Timer;
|
||||
import { InstanceStatsService } from './services/instance-stats-service';
|
||||
import { ValidatedClientMetrics } from './services/client-metrics/schema';
|
||||
|
||||
export default class MetricsMonitor {
|
||||
timer?: Timer;
|
||||
@ -268,16 +269,13 @@ export default class MetricsMonitor {
|
||||
featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc();
|
||||
});
|
||||
|
||||
eventBus.on(CLIENT_METRICS, (m) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
eventBus.on(CLIENT_METRICS, (m: ValidatedClientMetrics) => {
|
||||
for (const entry of Object.entries(m.bucket.toggles)) {
|
||||
featureToggleUsageTotal
|
||||
.labels(entry[0], 'true', m.appName)
|
||||
// @ts-expect-error
|
||||
.inc(entry[1].yes);
|
||||
featureToggleUsageTotal
|
||||
.labels(entry[0], 'false', m.appName)
|
||||
// @ts-expect-error
|
||||
.inc(entry[1].no);
|
||||
}
|
||||
});
|
||||
|
@ -135,6 +135,8 @@ import { openApiTags } from './util';
|
||||
import { URL } from 'url';
|
||||
import apiVersion from '../util/version';
|
||||
import { maintenanceSchema } from './spec/maintenance-schema';
|
||||
import { bulkRegistrationSchema } from './spec/bulk-registration-schema';
|
||||
import { bulkMetricsSchema } from './spec/bulk-metrics-schema';
|
||||
|
||||
// All schemas in `openapi/spec` should be listed here.
|
||||
export const schemas = {
|
||||
@ -147,6 +149,8 @@ export const schemas = {
|
||||
apiTokensSchema,
|
||||
applicationSchema,
|
||||
applicationsSchema,
|
||||
bulkRegistrationSchema,
|
||||
bulkMetricsSchema,
|
||||
changePasswordSchema,
|
||||
clientApplicationSchema,
|
||||
clientFeatureSchema,
|
||||
|
32
src/lib/openapi/spec/bulk-metrics-schema.ts
Normal file
32
src/lib/openapi/spec/bulk-metrics-schema.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { bulkRegistrationSchema } from './bulk-registration-schema';
|
||||
import { clientMetricsSchema } from './client-metrics-schema';
|
||||
import { dateSchema } from './date-schema';
|
||||
|
||||
export const bulkMetricsSchema = {
|
||||
$id: '#/components/schemas/bulkMetricsSchema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
applications: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/bulkRegistrationSchema',
|
||||
},
|
||||
},
|
||||
metrics: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/clientMetricsSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
bulkRegistrationSchema,
|
||||
dateSchema,
|
||||
clientMetricsSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type BulkMetricsSchema = FromSchema<typeof bulkMetricsSchema>;
|
44
src/lib/openapi/spec/bulk-registration-schema.ts
Normal file
44
src/lib/openapi/spec/bulk-registration-schema.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const bulkRegistrationSchema = {
|
||||
$id: '#/components/schemas/bulkRegistrationSchema',
|
||||
type: 'object',
|
||||
required: ['appName', 'instanceId'],
|
||||
properties: {
|
||||
connectVia: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
appName: {
|
||||
type: 'string',
|
||||
},
|
||||
environment: {
|
||||
type: 'string',
|
||||
},
|
||||
instanceId: {
|
||||
type: 'string',
|
||||
},
|
||||
interval: {
|
||||
type: 'number',
|
||||
},
|
||||
started: {
|
||||
type: 'number',
|
||||
},
|
||||
strategies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
sdkVersion: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type BulkRegistrationSchema = FromSchema<typeof bulkRegistrationSchema>;
|
@ -27,6 +27,21 @@ export const clientMetricsSchema = {
|
||||
},
|
||||
toggles: {
|
||||
type: 'object',
|
||||
example: {
|
||||
myCoolToggle: {
|
||||
yes: 25,
|
||||
no: 42,
|
||||
variants: {
|
||||
blue: 6,
|
||||
green: 15,
|
||||
red: 46,
|
||||
},
|
||||
},
|
||||
myOtherToggle: {
|
||||
yes: 0,
|
||||
no: 100,
|
||||
},
|
||||
},
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -4,14 +4,18 @@ import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import { Logger } from '../../logger';
|
||||
import { NONE } from '../../types/permissions';
|
||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||
import { RequestBody } from '../unleash-types';
|
||||
import { IAuthRequest, RequestBody } from '../unleash-types';
|
||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||
import {
|
||||
validateEdgeTokensSchema,
|
||||
ValidateEdgeTokensSchema,
|
||||
} from '../../openapi/spec/validate-edge-tokens-schema';
|
||||
import ClientInstanceService from '../../services/client-metrics/instance-service';
|
||||
import EdgeService from '../../services/edge-service';
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||
import { BulkMetricsSchema } from '../../openapi/spec/bulk-metrics-schema';
|
||||
import ClientMetricsServiceV2 from '../../services/client-metrics/metrics-service-v2';
|
||||
|
||||
export default class EdgeController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
@ -20,17 +24,31 @@ export default class EdgeController extends Controller {
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
private metricsV2: ClientMetricsServiceV2;
|
||||
|
||||
private clientInstanceService: ClientInstanceService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
edgeService,
|
||||
openApiService,
|
||||
}: Pick<IUnleashServices, 'edgeService' | 'openApiService'>,
|
||||
clientMetricsServiceV2,
|
||||
clientInstanceService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
| 'edgeService'
|
||||
| 'openApiService'
|
||||
| 'clientMetricsServiceV2'
|
||||
| 'clientInstanceService'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('edge-api/index.ts');
|
||||
this.edgeService = edgeService;
|
||||
this.openApiService = openApiService;
|
||||
this.metricsV2 = clientMetricsServiceV2;
|
||||
this.clientInstanceService = clientInstanceService;
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
@ -50,6 +68,23 @@ export default class EdgeController extends Controller {
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/metrics',
|
||||
handler: this.bulkMetrics,
|
||||
permission: NONE, // should have a permission but not bound to any environment
|
||||
middleware: [
|
||||
this.openApiService.validPath({
|
||||
tags: ['Edge'],
|
||||
operationId: 'bulkMetrics',
|
||||
requestBody: createRequestSchema('bulkMetricsSchema'),
|
||||
responses: {
|
||||
202: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getValidTokens(
|
||||
@ -66,4 +101,32 @@ export default class EdgeController extends Controller {
|
||||
tokens,
|
||||
);
|
||||
}
|
||||
|
||||
async bulkMetrics(
|
||||
req: IAuthRequest<void, void, BulkMetricsSchema>,
|
||||
res: Response<void>,
|
||||
): Promise<void> {
|
||||
const { body, ip: clientIp } = req;
|
||||
const { metrics, applications } = body;
|
||||
|
||||
try {
|
||||
let promises: Promise<void>[] = [];
|
||||
for (const app of applications) {
|
||||
promises.push(
|
||||
this.clientInstanceService.registerClient(app, clientIp),
|
||||
);
|
||||
}
|
||||
if (metrics) {
|
||||
for (const metric of metrics) {
|
||||
promises.push(
|
||||
this.metricsV2.registerClientMetrics(metric, clientIp),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
res.status(202).end();
|
||||
} catch (e) {
|
||||
res.status(400).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Logger } from '../../logger';
|
||||
import { IUnleashConfig } from '../../server-impl';
|
||||
import { IUnleashStores } from '../../types';
|
||||
import { IClientApp } from '../../types/model';
|
||||
import { ToggleMetricsSummary } from '../../types/models/metrics';
|
||||
import {
|
||||
IClientMetricsEnv,
|
||||
@ -13,7 +12,6 @@ import {
|
||||
hoursToMilliseconds,
|
||||
secondsToMilliseconds,
|
||||
} from 'date-fns';
|
||||
import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store';
|
||||
import { CLIENT_METRICS } from '../../types/events';
|
||||
import ApiUser from '../../types/api-user';
|
||||
import { ALL } from '../../types/models/api-token';
|
||||
@ -21,7 +19,7 @@ import User from '../../types/user';
|
||||
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
||||
import { LastSeenService } from './last-seen-service';
|
||||
import { generateHourBuckets } from '../../util/time-utils';
|
||||
import { IFlagResolver } from '../../types/experimental';
|
||||
import { ClientMetricsSchema } from 'lib/openapi';
|
||||
|
||||
export default class ClientMetricsServiceV2 {
|
||||
private config: IUnleashConfig;
|
||||
@ -32,28 +30,19 @@ export default class ClientMetricsServiceV2 {
|
||||
|
||||
private clientMetricsStoreV2: IClientMetricsStoreV2;
|
||||
|
||||
private featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
private lastSeenService: LastSeenService;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
{
|
||||
featureToggleStore,
|
||||
clientMetricsStoreV2,
|
||||
}: Pick<IUnleashStores, 'featureToggleStore' | 'clientMetricsStoreV2'>,
|
||||
{ clientMetricsStoreV2 }: Pick<IUnleashStores, 'clientMetricsStoreV2'>,
|
||||
config: IUnleashConfig,
|
||||
lastSeenService: LastSeenService,
|
||||
bulkInterval = secondsToMilliseconds(5),
|
||||
) {
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.clientMetricsStoreV2 = clientMetricsStoreV2;
|
||||
this.lastSeenService = lastSeenService;
|
||||
this.config = config;
|
||||
this.flagResolver = config.flagResolver;
|
||||
this.logger = config.getLogger(
|
||||
'/services/client-metrics/client-metrics-service-v2.ts',
|
||||
);
|
||||
@ -72,7 +61,7 @@ export default class ClientMetricsServiceV2 {
|
||||
}
|
||||
|
||||
async registerClientMetrics(
|
||||
data: IClientApp,
|
||||
data: ClientMetricsSchema,
|
||||
clientIp: string,
|
||||
): Promise<void> {
|
||||
const value = await clientMetricsSchema.validateAsync(data);
|
||||
@ -100,7 +89,6 @@ export default class ClientMetricsServiceV2 {
|
||||
...clientMetrics,
|
||||
]);
|
||||
this.lastSeenService.updateLastSeen(clientMetrics);
|
||||
|
||||
this.config.eventBus.emit(CLIENT_METRICS, value);
|
||||
}
|
||||
|
||||
@ -196,7 +184,10 @@ export default class ClientMetricsServiceV2 {
|
||||
return result.sort((a, b) => compareAsc(a.timestamp, b.timestamp));
|
||||
}
|
||||
|
||||
resolveMetricsEnvironment(user: User | ApiUser, data: IClientApp): string {
|
||||
resolveMetricsEnvironment(
|
||||
user: User | ApiUser,
|
||||
data: { environment?: string },
|
||||
): string {
|
||||
if (user instanceof ApiUser) {
|
||||
if (user.environment !== ALL) {
|
||||
return user.environment;
|
||||
|
@ -12,6 +12,17 @@ test('clientRegisterSchema should allow empty ("") instanceId', () => {
|
||||
expect(value.instanceId).toBe('default');
|
||||
});
|
||||
|
||||
test('clientRegisterSchema should allow string dates', () => {
|
||||
const date = new Date();
|
||||
const { value } = clientRegisterSchema.validate({
|
||||
appName: 'test',
|
||||
strategies: ['default'],
|
||||
started: date.toISOString(),
|
||||
interval: 100,
|
||||
});
|
||||
expect(value.started).toStrictEqual(date);
|
||||
});
|
||||
|
||||
test('clientRegisterSchema should allow undefined instanceId', () => {
|
||||
const { value } = clientRegisterSchema.validate({
|
||||
appName: 'test',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import joi from 'joi';
|
||||
import { IMetricsBucket } from 'lib/types';
|
||||
|
||||
const countSchema = joi
|
||||
.object()
|
||||
@ -9,8 +10,16 @@ const countSchema = joi
|
||||
variants: joi.object().pattern(joi.string(), joi.number().min(0)),
|
||||
});
|
||||
|
||||
// validated type from client-metrics-schema.ts with default values
|
||||
export type ValidatedClientMetrics = {
|
||||
environment?: string;
|
||||
appName: string;
|
||||
instanceId: string;
|
||||
bucket: IMetricsBucket;
|
||||
};
|
||||
|
||||
export const clientMetricsSchema = joi
|
||||
.object()
|
||||
.object<ValidatedClientMetrics>()
|
||||
.options({ stripUnknown: true })
|
||||
.keys({
|
||||
environment: joi.string().optional(),
|
||||
|
@ -307,7 +307,6 @@ export interface IClientApp {
|
||||
seenToggles?: string[];
|
||||
metricsCount?: number;
|
||||
strategies?: string[] | Record<string, string>[];
|
||||
bucket?: any;
|
||||
count?: number;
|
||||
started?: string | number | Date;
|
||||
interval?: number;
|
||||
@ -342,7 +341,7 @@ export interface IMetricCounts {
|
||||
export interface IMetricsBucket {
|
||||
start: Date;
|
||||
stop: Date;
|
||||
toggles: IMetricCounts;
|
||||
toggles: { [key: string]: IMetricCounts };
|
||||
}
|
||||
|
||||
export interface IImportFile extends ImportCommon {
|
||||
|
@ -328,6 +328,62 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"bulkMetricsSchema": {
|
||||
"properties": {
|
||||
"applications": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/bulkRegistrationSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"metrics": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/clientMetricsSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"bulkRegistrationSchema": {
|
||||
"properties": {
|
||||
"appName": {
|
||||
"type": "string",
|
||||
},
|
||||
"connectVia": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
},
|
||||
"instanceId": {
|
||||
"type": "string",
|
||||
},
|
||||
"interval": {
|
||||
"type": "number",
|
||||
},
|
||||
"sdkVersion": {
|
||||
"type": "string",
|
||||
},
|
||||
"started": {
|
||||
"type": "number",
|
||||
},
|
||||
"strategies": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"appName",
|
||||
"instanceId",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"changePasswordSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@ -534,6 +590,21 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"example": {
|
||||
"myCoolToggle": {
|
||||
"no": 42,
|
||||
"variants": {
|
||||
"blue": 6,
|
||||
"green": 15,
|
||||
"red": 46,
|
||||
},
|
||||
"yes": 25,
|
||||
},
|
||||
"myOtherToggle": {
|
||||
"no": 100,
|
||||
"yes": 0,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
@ -8326,6 +8397,30 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/edge/metrics": {
|
||||
"post": {
|
||||
"operationId": "bulkMetrics",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/bulkMetricsSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "bulkMetricsSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Edge",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/edge/validate": {
|
||||
"post": {
|
||||
"operationId": "getValidTokens",
|
||||
|
Loading…
Reference in New Issue
Block a user