diff --git a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts index d2e2da83f2..d089f00604 100644 --- a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts +++ b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts @@ -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( diff --git a/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts b/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts index a1eb68ffb3..b0c4eb569b 100644 --- a/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts +++ b/src/lib/features/metrics/impact/impact-metrics.e2e.test.ts @@ -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/); +}); diff --git a/src/lib/features/metrics/instance/metrics.ts b/src/lib/features/metrics/instance/metrics.ts index 8eabc96a82..2cc73d502a 100644 --- a/src/lib/features/metrics/instance/metrics.ts +++ b/src/lib/features/metrics/instance/metrics.ts @@ -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[] = []; 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(); diff --git a/src/lib/openapi/spec/bulk-metrics-schema.ts b/src/lib/openapi/spec/bulk-metrics-schema.ts index 998007550a..b17b160221 100644 --- a/src/lib/openapi/spec/bulk-metrics-schema.ts +++ b/src/lib/openapi/spec/bulk-metrics-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/impact-metrics-schema.ts b/src/lib/openapi/spec/impact-metrics-schema.ts new file mode 100644 index 0000000000..b4d8c3df4d --- /dev/null +++ b/src/lib/openapi/spec/impact-metrics-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 04771b2f0e..6e44c2ba4b 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -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';