From 05a338c48797ff700d9f505993216cbdbfbfeba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 16 Oct 2024 23:12:09 +0200 Subject: [PATCH] Get closer to prom-client types --- src/lib/metrics-gauge.test.ts | 114 ++++++++++++++++++++++++++++++++++ src/lib/metrics-gauge.ts | 50 ++++++++++----- src/lib/metrics.ts | 8 +-- 3 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 src/lib/metrics-gauge.test.ts diff --git a/src/lib/metrics-gauge.test.ts b/src/lib/metrics-gauge.test.ts new file mode 100644 index 0000000000..e024563f96 --- /dev/null +++ b/src/lib/metrics-gauge.test.ts @@ -0,0 +1,114 @@ +import { register } from 'prom-client'; +import { createTestConfig } from '../test/config/test-config'; +import type { IUnleashConfig } from './types'; +import { DbMetricsMonitor } from './metrics-gauge'; + +const prometheusRegister = register; +let config: IUnleashConfig; +let dbMetrics: DbMetricsMonitor; + +beforeAll(async () => { + config = createTestConfig({ + server: { + serverMetrics: true, + }, + }); +}); + +beforeEach(async () => { + dbMetrics = new DbMetricsMonitor(config); +}); + +test('should collect registered metrics', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'my_metric', + help: 'This is the answer to life, the univers, and everything', + labelNames: [], + query: () => Promise.resolve(42), + map: (result) => ({ value: result }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch(/my_metric 42/); +}); + +test('should collect registered metrics with labels', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'life_the_universe_and_everything', + help: 'This is the answer to life, the univers, and everything', + labelNames: ['test'], + query: () => Promise.resolve(42), + map: (result) => ({ value: result, labels: { test: 'case' } }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch( + /life_the_universe_and_everything\{test="case"\} 42/, + ); +}); + +test('should collect multiple registered metrics with and without labels', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'my_first_metric', + help: 'This is the answer to life, the univers, and everything', + labelNames: [], + query: () => Promise.resolve(42), + map: (result) => ({ value: result }), + }); + + dbMetrics.registerGaugeDbMetric({ + name: 'my_other_metric', + help: 'This is Eulers number', + labelNames: ['euler'], + query: () => Promise.resolve(Math.E), + map: (result) => ({ value: result, labels: { euler: 'number' } }), + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch(/my_first_metric 42/); + expect(metrics).toMatch(/my_other_metric\{euler="number"\} 2.71828/); +}); + +test('should support different label and value pairs', async () => { + dbMetrics.registerGaugeDbMetric({ + name: 'multi_dimensional', + help: 'This metric has different values for different labels', + labelNames: ['version', 'range'], + query: () => Promise.resolve(2), + map: (result) => [ + { value: result, labels: { version: '1', range: 'linear' } }, + { + value: result * result, + labels: { version: '2', range: 'square' }, + }, + { value: result / 2, labels: { version: '3', range: 'half' } }, + ], + }); + + await dbMetrics.refreshDbMetrics(); + + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch( + /multi_dimensional\{version="1",range="linear"\} 2\nmulti_dimensional\{version="2",range="square"\} 4\nmulti_dimensional\{version="3",range="half"\} 1/, + ); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'linear' }), + ).toBe(2); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'half' }), + ).toBe(1); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'square' }), + ).toBe(4); + expect( + await dbMetrics.findValue('multi_dimensional', { range: 'x' }), + ).toBeUndefined(); + expect(await dbMetrics.findValue('multi_dimensional')).toBe(2); // first match + expect(await dbMetrics.findValue('other')).toBeUndefined(); +}); diff --git a/src/lib/metrics-gauge.ts b/src/lib/metrics-gauge.ts index e4f62d74bd..60accbeaba 100644 --- a/src/lib/metrics-gauge.ts +++ b/src/lib/metrics-gauge.ts @@ -2,37 +2,40 @@ import type { Logger } from './logger'; import type { IUnleashConfig } from './types'; import { createGauge, type Gauge } from './util/metrics'; -type RestrictedRecord = Record; type Query = () => Promise; -type MetricValue = { - count: number; - labels: RestrictedRecord['labelNames']>; +type MetricValue = { + value: number; + labels?: Record; }; -type MapResult = (result: R) => MetricValue | MetricValue[]; +type MapResult = ( + result: R, +) => MetricValue | MetricValue[]; -type GaugeDefinition = { +type GaugeDefinition = { name: string; help: string; - labelNames: string[]; + labelNames: L[]; query: Query; - map: MapResult; + map: MapResult; }; type Task = () => Promise; export class DbMetricsMonitor { private tasks: Set = new Set(); private gauges: Map> = new Map(); - private logger: Logger; + private log: Logger; - constructor(config: IUnleashConfig) { - this.logger = config.getLogger('gauge-metrics'); + constructor({ getLogger }: Pick) { + this.log = getLogger('gauge-metrics'); } private asArray(value: T | T[]): T[] { return Array.isArray(value) ? value : [value]; } - registerGaugeDbMetric(definition: GaugeDefinition): Task { + registerGaugeDbMetric( + definition: GaugeDefinition, + ): Task { const gauge = createGauge(definition); this.gauges.set(definition.name, gauge); const task = async () => { @@ -42,11 +45,15 @@ export class DbMetricsMonitor { const results = this.asArray(definition.map(result)); gauge.reset(); for (const r of results) { - gauge.labels(r.labels).set(r.count); + if (r.labels) { + gauge.labels(r.labels).set(r.value); + } else { + gauge.set(r.value); + } } } } catch (e) { - this.logger.warn(`Failed to refresh ${definition.name}`, e); + this.log.warn(`Failed to refresh ${definition.name}`, e); } }; this.tasks.add(task); @@ -59,10 +66,21 @@ export class DbMetricsMonitor { } }; - async getLastValue(name: string): Promise { + async findValue( + name: string, + labels?: Record, + ): Promise { const gauge = await this.gauges.get(name)?.gauge?.get(); if (gauge && gauge.values.length > 0) { - return gauge.values[0].value; + const values = labels + ? gauge.values.filter(({ labels: l }) => { + return Object.entries(labels).every( + ([key, value]) => l[key] === value, + ); + }) + : gauge.values; + // return first value + return values.map(({ value }) => value).shift(); } return undefined; } diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 75b7c966ed..8b0a0e390e 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -124,7 +124,7 @@ export default class MetricsMonitor { help: 'Number of feature flags', labelNames: ['version'], query: () => instanceStatsService.getToggleCount(), - map: (count) => ({ count, labels: { version } }), + map: (value) => ({ value, labels: { version } }), })(); dbMetrics.registerGaugeDbMetric({ @@ -134,7 +134,7 @@ export default class MetricsMonitor { query: () => stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), map: (result) => ({ - count: result.count, + value: result.count, labels: { environment: result.environment, feature: result.feature, @@ -149,7 +149,7 @@ export default class MetricsMonitor { query: () => stores.featureStrategiesReadModel.getMaxFeatureStrategies(), map: (result) => ({ - count: result.count, + value: result.count, labels: { feature: result.feature }, }), }); @@ -268,7 +268,7 @@ export default class MetricsMonitor { query: () => instanceStatsService.getLabeledAppCounts(), map: (result) => Object.entries(result).map(([range, count]) => ({ - count, + value: count, labels: { range }, })), })();