1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: bulk impact metrics (#10251)

This commit is contained in:
Mateusz Kwasniewski 2025-07-01 09:50:44 +02:00 committed by GitHub
parent 0a42d22c52
commit 661fd6febf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 152 additions and 5 deletions

View File

@ -196,8 +196,14 @@ export default class ClientMetricsServiceV2 {
}
async registerImpactMetrics(impactMetrics: Metric[]) {
const value = await impactMetricsSchema.validateAsync(impactMetrics);
this.impactMetricsTranslator.translateMetrics(value);
try {
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(

View File

@ -26,6 +26,20 @@ const sendImpactMetrics = async (impactMetrics: Metric[], status = 202) =>
})
.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 () => {
db = await dbInit('impact_metrics', getLogger);
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([]);
// missing help
// missing help = no error but value ignored
await sendImpactMetrics(
[
// @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
@ -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).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/);
});

View File

@ -230,7 +230,7 @@ export default class ClientMetricsController extends Controller {
res.status(204).end();
} else {
const { body, ip: clientIp } = req;
const { metrics, applications } = body;
const { metrics, applications, impactMetrics } = body;
try {
const promises: Promise<void>[] = [];
for (const app of applications) {
@ -275,6 +275,17 @@ export default class ClientMetricsController extends Controller {
);
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);
res.status(202).end();

View File

@ -2,6 +2,7 @@ import type { FromSchema } from 'json-schema-to-ts';
import { bulkRegistrationSchema } from './bulk-registration-schema.js';
import { dateSchema } from './date-schema.js';
import { clientMetricsEnvSchema } from './client-metrics-env-schema.js';
import { impactMetricsSchema } from './impact-metrics-schema.js';
export const bulkMetricsSchema = {
$id: '#/components/schemas/bulkMetricsSchema',
@ -25,12 +26,21 @@ export const bulkMetricsSchema = {
$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: {
schemas: {
bulkRegistrationSchema,
dateSchema,
clientMetricsEnvSchema,
impactMetricsSchema,
},
},
} as const;

View 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>;

View File

@ -113,6 +113,7 @@ export * from './health-overview-schema.js';
export * from './health-report-schema.js';
export * from './id-schema.js';
export * from './ids-schema.js';
export * from './impact-metrics-schema.js';
export * from './import-toggles-schema.js';
export * from './import-toggles-validate-item-schema.js';
export * from './import-toggles-validate-schema.js';