mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
feat: bulk impact metrics (#10251)
This commit is contained in:
parent
0a42d22c52
commit
661fd6febf
@ -196,8 +196,14 @@ export default class ClientMetricsServiceV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async registerImpactMetrics(impactMetrics: Metric[]) {
|
async registerImpactMetrics(impactMetrics: Metric[]) {
|
||||||
const value = await impactMetricsSchema.validateAsync(impactMetrics);
|
try {
|
||||||
this.impactMetricsTranslator.translateMetrics(value);
|
const value =
|
||||||
|
await impactMetricsSchema.validateAsync(impactMetrics);
|
||||||
|
this.impactMetricsTranslator.translateMetrics(value);
|
||||||
|
} catch (e) {
|
||||||
|
// impact metrics should not affect other metrics on failure
|
||||||
|
this.logger.warn(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerClientMetrics(
|
async registerClientMetrics(
|
||||||
|
@ -26,6 +26,20 @@ const sendImpactMetrics = async (impactMetrics: Metric[], status = 202) =>
|
|||||||
})
|
})
|
||||||
.expect(status);
|
.expect(status);
|
||||||
|
|
||||||
|
const sendBulkMetricsWithImpact = async (
|
||||||
|
impactMetrics: Metric[],
|
||||||
|
status = 202,
|
||||||
|
) => {
|
||||||
|
return app.request
|
||||||
|
.post('/api/client/metrics/bulk')
|
||||||
|
.send({
|
||||||
|
applications: [],
|
||||||
|
metrics: [],
|
||||||
|
impactMetrics,
|
||||||
|
})
|
||||||
|
.expect(status);
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('impact_metrics', getLogger);
|
db = await dbInit('impact_metrics', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(db.stores, {
|
||||||
@ -72,7 +86,7 @@ test('should store impact metrics in memory and be able to retrieve them', async
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await sendImpactMetrics([]);
|
await sendImpactMetrics([]);
|
||||||
// missing help
|
// missing help = no error but value ignored
|
||||||
await sendImpactMetrics(
|
await sendImpactMetrics(
|
||||||
[
|
[
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@ -87,7 +101,7 @@ test('should store impact metrics in memory and be able to retrieve them', async
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
400,
|
202,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await app.request
|
const response = await app.request
|
||||||
@ -101,3 +115,48 @@ test('should store impact metrics in memory and be able to retrieve them', async
|
|||||||
expect(metricsText).toContain('# TYPE labeled_counter counter');
|
expect(metricsText).toContain('# TYPE labeled_counter counter');
|
||||||
expect(metricsText).toMatch(/labeled_counter{foo="bar"} 15/);
|
expect(metricsText).toMatch(/labeled_counter{foo="bar"} 15/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should store impact metrics sent via bulk metrics endpoint', async () => {
|
||||||
|
await sendBulkMetricsWithImpact([
|
||||||
|
{
|
||||||
|
name: 'bulk_counter',
|
||||||
|
help: 'bulk counter with labels',
|
||||||
|
type: 'counter',
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { source: 'bulk' },
|
||||||
|
value: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sendBulkMetricsWithImpact([
|
||||||
|
{
|
||||||
|
name: 'bulk_counter',
|
||||||
|
help: 'bulk counter with labels',
|
||||||
|
type: 'counter',
|
||||||
|
samples: [
|
||||||
|
{
|
||||||
|
labels: { source: 'bulk' },
|
||||||
|
value: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sendBulkMetricsWithImpact([]);
|
||||||
|
|
||||||
|
const response = await app.request
|
||||||
|
.get('/internal-backstage/impact/metrics')
|
||||||
|
.expect('Content-Type', /text/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const metricsText = response.text;
|
||||||
|
|
||||||
|
expect(metricsText).toContain(
|
||||||
|
'# HELP bulk_counter bulk counter with labels',
|
||||||
|
);
|
||||||
|
expect(metricsText).toContain('# TYPE bulk_counter counter');
|
||||||
|
expect(metricsText).toMatch(/bulk_counter{source="bulk"} 15/);
|
||||||
|
});
|
||||||
|
@ -230,7 +230,7 @@ export default class ClientMetricsController extends Controller {
|
|||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} else {
|
} else {
|
||||||
const { body, ip: clientIp } = req;
|
const { body, ip: clientIp } = req;
|
||||||
const { metrics, applications } = body;
|
const { metrics, applications, impactMetrics } = body;
|
||||||
try {
|
try {
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
for (const app of applications) {
|
for (const app of applications) {
|
||||||
@ -275,6 +275,17 @@ export default class ClientMetricsController extends Controller {
|
|||||||
);
|
);
|
||||||
this.config.eventBus.emit(CLIENT_METRICS, data);
|
this.config.eventBus.emit(CLIENT_METRICS, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.flagResolver.isEnabled('impactMetrics') &&
|
||||||
|
impactMetrics &&
|
||||||
|
impactMetrics.length > 0
|
||||||
|
) {
|
||||||
|
promises.push(
|
||||||
|
this.metricsV2.registerImpactMetrics(impactMetrics),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
res.status(202).end();
|
res.status(202).end();
|
||||||
|
@ -2,6 +2,7 @@ import type { FromSchema } from 'json-schema-to-ts';
|
|||||||
import { bulkRegistrationSchema } from './bulk-registration-schema.js';
|
import { bulkRegistrationSchema } from './bulk-registration-schema.js';
|
||||||
import { dateSchema } from './date-schema.js';
|
import { dateSchema } from './date-schema.js';
|
||||||
import { clientMetricsEnvSchema } from './client-metrics-env-schema.js';
|
import { clientMetricsEnvSchema } from './client-metrics-env-schema.js';
|
||||||
|
import { impactMetricsSchema } from './impact-metrics-schema.js';
|
||||||
|
|
||||||
export const bulkMetricsSchema = {
|
export const bulkMetricsSchema = {
|
||||||
$id: '#/components/schemas/bulkMetricsSchema',
|
$id: '#/components/schemas/bulkMetricsSchema',
|
||||||
@ -25,12 +26,21 @@ export const bulkMetricsSchema = {
|
|||||||
$ref: '#/components/schemas/clientMetricsEnvSchema',
|
$ref: '#/components/schemas/clientMetricsEnvSchema',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
impactMetrics: {
|
||||||
|
description:
|
||||||
|
'a list of custom impact metrics registered by downstream providers. (Typically Unleash Edge)',
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/impactMetricsSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
bulkRegistrationSchema,
|
bulkRegistrationSchema,
|
||||||
dateSchema,
|
dateSchema,
|
||||||
clientMetricsEnvSchema,
|
clientMetricsEnvSchema,
|
||||||
|
impactMetricsSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
60
src/lib/openapi/spec/impact-metrics-schema.ts
Normal file
60
src/lib/openapi/spec/impact-metrics-schema.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const impactMetricsSchema = {
|
||||||
|
$id: '#/components/schemas/impactMetricsSchema',
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'help', 'type', 'samples'],
|
||||||
|
description: 'Used for reporting impact metrics from SDKs',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the impact metric',
|
||||||
|
example: 'my-counter',
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
description:
|
||||||
|
'Human-readable description of what the metric measures',
|
||||||
|
type: 'string',
|
||||||
|
example: 'Counts the number of operations',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
description: 'Type of the metric',
|
||||||
|
type: 'string',
|
||||||
|
enum: ['counter', 'gauge'],
|
||||||
|
example: 'counter',
|
||||||
|
},
|
||||||
|
samples: {
|
||||||
|
description: 'Samples of the metric',
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['value'],
|
||||||
|
description:
|
||||||
|
'A sample of a metric with a value and optional labels',
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The value of the metric sample',
|
||||||
|
example: 10,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
description: 'Optional labels for the metric sample',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
example: {
|
||||||
|
application: 'my-app',
|
||||||
|
environment: 'production',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ImpactMetricsSchema = FromSchema<typeof impactMetricsSchema>;
|
@ -113,6 +113,7 @@ export * from './health-overview-schema.js';
|
|||||||
export * from './health-report-schema.js';
|
export * from './health-report-schema.js';
|
||||||
export * from './id-schema.js';
|
export * from './id-schema.js';
|
||||||
export * from './ids-schema.js';
|
export * from './ids-schema.js';
|
||||||
|
export * from './impact-metrics-schema.js';
|
||||||
export * from './import-toggles-schema.js';
|
export * from './import-toggles-schema.js';
|
||||||
export * from './import-toggles-validate-item-schema.js';
|
export * from './import-toggles-validate-item-schema.js';
|
||||||
export * from './import-toggles-validate-schema.js';
|
export * from './import-toggles-validate-schema.js';
|
||||||
|
Loading…
Reference in New Issue
Block a user