mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-10 17:53:36 +02:00
Get closer to prom-client types
This commit is contained in:
parent
4806106fc4
commit
05a338c487
114
src/lib/metrics-gauge.test.ts
Normal file
114
src/lib/metrics-gauge.test.ts
Normal file
@ -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();
|
||||||
|
});
|
@ -2,37 +2,40 @@ import type { Logger } from './logger';
|
|||||||
import type { IUnleashConfig } from './types';
|
import type { IUnleashConfig } from './types';
|
||||||
import { createGauge, type Gauge } from './util/metrics';
|
import { createGauge, type Gauge } from './util/metrics';
|
||||||
|
|
||||||
type RestrictedRecord<T extends readonly string[]> = Record<T[number], string>;
|
|
||||||
type Query<R> = () => Promise<R | undefined | null>;
|
type Query<R> = () => Promise<R | undefined | null>;
|
||||||
type MetricValue<R> = {
|
type MetricValue<L extends string> = {
|
||||||
count: number;
|
value: number;
|
||||||
labels: RestrictedRecord<GaugeDefinition<R>['labelNames']>;
|
labels?: Record<L, string | number>;
|
||||||
};
|
};
|
||||||
type MapResult<R> = (result: R) => MetricValue<R> | MetricValue<R>[];
|
type MapResult<R, L extends string> = (
|
||||||
|
result: R,
|
||||||
|
) => MetricValue<L> | MetricValue<L>[];
|
||||||
|
|
||||||
type GaugeDefinition<T> = {
|
type GaugeDefinition<T, L extends string> = {
|
||||||
name: string;
|
name: string;
|
||||||
help: string;
|
help: string;
|
||||||
labelNames: string[];
|
labelNames: L[];
|
||||||
query: Query<T>;
|
query: Query<T>;
|
||||||
map: MapResult<T>;
|
map: MapResult<T, L>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Task = () => Promise<void>;
|
type Task = () => Promise<void>;
|
||||||
export class DbMetricsMonitor {
|
export class DbMetricsMonitor {
|
||||||
private tasks: Set<Task> = new Set();
|
private tasks: Set<Task> = new Set();
|
||||||
private gauges: Map<string, Gauge<string>> = new Map();
|
private gauges: Map<string, Gauge<string>> = new Map();
|
||||||
private logger: Logger;
|
private log: Logger;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig) {
|
constructor({ getLogger }: Pick<IUnleashConfig, 'getLogger'>) {
|
||||||
this.logger = config.getLogger('gauge-metrics');
|
this.log = getLogger('gauge-metrics');
|
||||||
}
|
}
|
||||||
|
|
||||||
private asArray<T>(value: T | T[]): T[] {
|
private asArray<T>(value: T | T[]): T[] {
|
||||||
return Array.isArray(value) ? value : [value];
|
return Array.isArray(value) ? value : [value];
|
||||||
}
|
}
|
||||||
|
|
||||||
registerGaugeDbMetric<T>(definition: GaugeDefinition<T>): Task {
|
registerGaugeDbMetric<T, L extends string>(
|
||||||
|
definition: GaugeDefinition<T, L>,
|
||||||
|
): Task {
|
||||||
const gauge = createGauge(definition);
|
const gauge = createGauge(definition);
|
||||||
this.gauges.set(definition.name, gauge);
|
this.gauges.set(definition.name, gauge);
|
||||||
const task = async () => {
|
const task = async () => {
|
||||||
@ -42,11 +45,15 @@ export class DbMetricsMonitor {
|
|||||||
const results = this.asArray(definition.map(result));
|
const results = this.asArray(definition.map(result));
|
||||||
gauge.reset();
|
gauge.reset();
|
||||||
for (const r of results) {
|
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) {
|
} catch (e) {
|
||||||
this.logger.warn(`Failed to refresh ${definition.name}`, e);
|
this.log.warn(`Failed to refresh ${definition.name}`, e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.tasks.add(task);
|
this.tasks.add(task);
|
||||||
@ -59,10 +66,21 @@ export class DbMetricsMonitor {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async getLastValue(name: string): Promise<number | undefined> {
|
async findValue(
|
||||||
|
name: string,
|
||||||
|
labels?: Record<string, string | number>,
|
||||||
|
): Promise<number | undefined> {
|
||||||
const gauge = await this.gauges.get(name)?.gauge?.get();
|
const gauge = await this.gauges.get(name)?.gauge?.get();
|
||||||
if (gauge && gauge.values.length > 0) {
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ export default class MetricsMonitor {
|
|||||||
help: 'Number of feature flags',
|
help: 'Number of feature flags',
|
||||||
labelNames: ['version'],
|
labelNames: ['version'],
|
||||||
query: () => instanceStatsService.getToggleCount(),
|
query: () => instanceStatsService.getToggleCount(),
|
||||||
map: (count) => ({ count, labels: { version } }),
|
map: (value) => ({ value, labels: { version } }),
|
||||||
})();
|
})();
|
||||||
|
|
||||||
dbMetrics.registerGaugeDbMetric({
|
dbMetrics.registerGaugeDbMetric({
|
||||||
@ -134,7 +134,7 @@ export default class MetricsMonitor {
|
|||||||
query: () =>
|
query: () =>
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
||||||
map: (result) => ({
|
map: (result) => ({
|
||||||
count: result.count,
|
value: result.count,
|
||||||
labels: {
|
labels: {
|
||||||
environment: result.environment,
|
environment: result.environment,
|
||||||
feature: result.feature,
|
feature: result.feature,
|
||||||
@ -149,7 +149,7 @@ export default class MetricsMonitor {
|
|||||||
query: () =>
|
query: () =>
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
||||||
map: (result) => ({
|
map: (result) => ({
|
||||||
count: result.count,
|
value: result.count,
|
||||||
labels: { feature: result.feature },
|
labels: { feature: result.feature },
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -268,7 +268,7 @@ export default class MetricsMonitor {
|
|||||||
query: () => instanceStatsService.getLabeledAppCounts(),
|
query: () => instanceStatsService.getLabeledAppCounts(),
|
||||||
map: (result) =>
|
map: (result) =>
|
||||||
Object.entries(result).map(([range, count]) => ({
|
Object.entries(result).map(([range, count]) => ({
|
||||||
count,
|
value: count,
|
||||||
labels: { range },
|
labels: { range },
|
||||||
})),
|
})),
|
||||||
})();
|
})();
|
||||||
|
Loading…
Reference in New Issue
Block a user