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:
parent
0a42d22c52
commit
661fd6febf
@ -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(
|
||||
|
@ -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/);
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
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 './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';
|
||||
|
Loading…
Reference in New Issue
Block a user