Skip to content

Commit a4907bd

Browse files
authored
feat: impact metrics public api (#744)
1 parent bb95928 commit a4907bd

File tree

5 files changed

+246
-9
lines changed

5 files changed

+246
-9
lines changed

src/impact-metrics/metric-client.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,72 @@
1-
import { Unleash } from '../unleash';
1+
import { EventEmitter } from 'stream';
2+
import { StaticContext, Unleash, UnleashEvents } from '../unleash';
3+
import { ImpactMetricRegistry } from './metric-types';
4+
5+
export class MetricsAPI extends EventEmitter {
6+
constructor(
7+
private metricRegistry: ImpactMetricRegistry,
8+
private staticContext: StaticContext,
9+
) {
10+
super();
11+
}
12+
13+
defineCounter(name: string, help: string) {
14+
if (!name || !help) {
15+
this.emit(UnleashEvents.Warn, `Counter name or help cannot be empty: ${name}, ${help}.`);
16+
return;
17+
}
18+
const labelNames = ['featureName', 'appName', 'environment'];
19+
this.metricRegistry.counter({ name, help, labelNames });
20+
}
21+
22+
defineGauge(name: string, help: string) {
23+
if (!name || !help) {
24+
this.emit(UnleashEvents.Warn, `Gauge name or help cannot be empty: ${name}, ${help}.`);
25+
return;
26+
}
27+
const labelNames = ['featureName', 'appName', 'environment'];
28+
this.metricRegistry.gauge({ name, help, labelNames });
29+
}
30+
31+
incrementCounter(name: string, value?: number, featureName?: string): void {
32+
const counter = this.metricRegistry.getCounter(name);
33+
if (!counter) {
34+
this.emit(
35+
UnleashEvents.Warn,
36+
`Counter ${name} not defined, this counter will not be incremented.`,
37+
);
38+
return;
39+
}
40+
41+
const labels = {
42+
...(featureName ? { featureName } : {}),
43+
...this.staticContext,
44+
};
45+
46+
counter.inc(value, labels);
47+
}
48+
49+
updateGauge(name: string, value: number, featureName?: string): void {
50+
const gauge = this.metricRegistry.getGauge(name);
51+
if (!gauge) {
52+
this.emit(UnleashEvents.Warn, `Gauge ${name} not defined, this gauge will not be updated.`);
53+
return;
54+
}
55+
56+
const labels = {
57+
...(featureName ? { featureName } : {}),
58+
...this.staticContext,
59+
};
60+
61+
gauge.set(value, labels);
62+
}
63+
}
264

365
export class UnleashMetricClient extends Unleash {
4-
// empty for now, we'll use this to add in the public API for metrics later
66+
public impactMetrics: MetricsAPI;
67+
68+
constructor(...args: ConstructorParameters<typeof Unleash>) {
69+
super(...args);
70+
this.impactMetrics = new MetricsAPI(this.metricRegistry, this.staticContext);
71+
}
572
}

src/impact-metrics/metric-types.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,31 @@ export interface Gauge {
117117
set(value: number, labels?: MetricLabels): void;
118118
}
119119

120-
export interface ImpactMetricRegistry {
120+
export interface ImpactMetricsDataSource {
121121
collect(): CollectedMetric[];
122122
restore(metrics: CollectedMetric[]): void;
123123
}
124124

125-
export class InMemoryMetricRegistry implements ImpactMetricRegistry {
125+
export interface ImpactMetricRegistry {
126+
getCounter(counterName: string): Counter | undefined;
127+
getGauge(gaugeName: string): Gauge | undefined;
128+
counter(opts: MetricOptions): Counter;
129+
gauge(opts: MetricOptions): Gauge;
130+
}
131+
132+
export class InMemoryMetricRegistry implements ImpactMetricsDataSource, ImpactMetricRegistry {
126133
private counters = new Map<string, Counter & CollectibleMetric>();
127134

128135
private gauges = new Map<string, Gauge & CollectibleMetric>();
129136

137+
getCounter(counterName: string): Counter | undefined {
138+
return this.counters.get(counterName);
139+
}
140+
141+
getGauge(gaugeName: string): Gauge | undefined {
142+
return this.gauges.get(gaugeName);
143+
}
144+
130145
counter(opts: MetricOptions): Counter {
131146
const key = opts.name;
132147
if (!this.counters.has(key)) {

src/metrics.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { suffixSlash, resolveUrl } from './url-utils';
77
import { UnleashEvents } from './events';
88
import { getAppliedJitter } from './helpers';
99
import { SUPPORTED_SPEC_VERSION } from './repository';
10-
import { CollectedMetric, ImpactMetricRegistry } from './impact-metrics/metric-types';
10+
import { CollectedMetric, ImpactMetricsDataSource } from './impact-metrics/metric-types';
1111

1212
export interface MetricsOptions {
1313
appName: string;
@@ -22,7 +22,7 @@ export interface MetricsOptions {
2222
customHeadersFunction?: CustomHeadersFunction;
2323
timeout?: number;
2424
httpOptions?: HttpOptions;
25-
metricRegistry?: ImpactMetricRegistry;
25+
metricRegistry?: ImpactMetricsDataSource;
2626
}
2727

2828
interface VariantBucket {
@@ -115,7 +115,7 @@ export default class Metrics extends EventEmitter {
115115

116116
private platformData: PlatformData;
117117

118-
private metricRegistry?: ImpactMetricRegistry;
118+
private metricRegistry?: ImpactMetricsDataSource;
119119

120120
constructor({
121121
appName,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { MetricsAPI } from '../../impact-metrics/metric-client';
2+
3+
import test from 'ava';
4+
5+
test('should not register a counter with empty name or help', (t) => {
6+
let counterRegistered = false;
7+
8+
const fakeRegistry = {
9+
counter: () => {
10+
counterRegistered = true;
11+
},
12+
};
13+
14+
const staticContext = { appName: 'my-app', environment: 'dev' };
15+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
16+
17+
api.defineCounter('some_name', '');
18+
t.false(counterRegistered, 'Counter should not be registered with empty help');
19+
20+
api.defineCounter('', 'some_help');
21+
t.false(counterRegistered, 'Counter should not be registered with empty name');
22+
});
23+
24+
test('should register a counter with valid name and help', (t) => {
25+
let counterRegistered = false;
26+
27+
const fakeRegistry = {
28+
counter: () => {
29+
counterRegistered = true;
30+
},
31+
};
32+
33+
const staticContext = { appName: 'my-app', environment: 'dev' };
34+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
35+
36+
api.defineCounter('valid_name', 'Valid help text');
37+
t.true(counterRegistered, 'Counter should be registered with valid name and help');
38+
});
39+
40+
test('should not register a gauge with empty name or help', (t) => {
41+
let gaugeRegistered = false;
42+
43+
const fakeRegistry = {
44+
gauge: () => {
45+
gaugeRegistered = true;
46+
},
47+
};
48+
49+
const staticContext = { appName: 'my-app', environment: 'dev' };
50+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
51+
52+
api.defineGauge('some_name', '');
53+
t.false(gaugeRegistered, 'Gauge should not be registered with empty help');
54+
55+
api.defineGauge('', 'some_help');
56+
t.false(gaugeRegistered, 'Gauge should not be registered with empty name');
57+
});
58+
59+
test('should register a gauge with valid name and help', (t) => {
60+
let gaugeRegistered = false;
61+
62+
const fakeRegistry = {
63+
gauge: () => {
64+
gaugeRegistered = true;
65+
},
66+
};
67+
68+
const staticContext = { appName: 'my-app', environment: 'dev' };
69+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
70+
71+
api.defineGauge('valid_name', 'Valid help text');
72+
t.true(gaugeRegistered, 'Gauge should be registered with valid name and help');
73+
});
74+
75+
test('should increment counter with valid parameters', (t) => {
76+
let counterIncremented = false;
77+
78+
const fakeCounter = {
79+
inc: () => {
80+
counterIncremented = true;
81+
},
82+
};
83+
84+
const fakeRegistry = {
85+
getCounter: () => fakeCounter,
86+
};
87+
88+
const staticContext = { appName: 'my-app', environment: 'dev' };
89+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
90+
91+
api.incrementCounter('valid_counter', 5, 'featureX');
92+
t.true(counterIncremented, 'Counter should be incremented with valid parameters');
93+
});
94+
95+
test('should set gauge with valid parameters', (t) => {
96+
let gaugeSet = false;
97+
98+
const fakeGauge = {
99+
set: () => {
100+
gaugeSet = true;
101+
},
102+
};
103+
104+
const fakeRegistry = {
105+
getGauge: () => fakeGauge,
106+
};
107+
108+
const staticContext = { appName: 'my-app', environment: 'dev' };
109+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
110+
111+
api.updateGauge('valid_gauge', 10, 'featureY');
112+
t.true(gaugeSet, 'Gauge should be set with valid parameters');
113+
});
114+
115+
test('defining a counter automatically sets label names', (t) => {
116+
let counterRegistered = false;
117+
118+
const fakeRegistry = {
119+
counter: (config: any) => {
120+
counterRegistered = true;
121+
t.deepEqual(
122+
config.labelNames,
123+
['featureName', 'appName', 'environment'],
124+
'Label names should be set correctly',
125+
);
126+
},
127+
};
128+
129+
const staticContext = { appName: 'my-app', environment: 'dev' };
130+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
131+
132+
api.defineCounter('test_counter', 'Test help text');
133+
t.true(counterRegistered, 'Counter should be registered');
134+
});
135+
136+
test('defining a gauge automatically sets label names', (t) => {
137+
let gaugeRegistered = false;
138+
139+
const fakeRegistry = {
140+
gauge: (config: any) => {
141+
gaugeRegistered = true;
142+
t.deepEqual(
143+
config.labelNames,
144+
['featureName', 'appName', 'environment'],
145+
'Label names should be set correctly',
146+
);
147+
},
148+
};
149+
150+
const staticContext = { appName: 'my-app', environment: 'dev' };
151+
const api = new MetricsAPI(fakeRegistry as any, staticContext);
152+
153+
api.defineGauge('test_gauge', 'Test help text');
154+
t.true(gaugeRegistered, 'Gauge should be registered');
155+
});

src/unleash.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ export class Unleash extends EventEmitter {
4545

4646
private metrics: Metrics;
4747

48-
private staticContext: StaticContext;
48+
protected staticContext: StaticContext;
4949

5050
private synchronized: boolean = false;
5151

5252
private ready: boolean = false;
5353

5454
private started: boolean = false;
5555

56-
private metricRegistry = new InMemoryMetricRegistry();
56+
protected metricRegistry = new InMemoryMetricRegistry();
5757

5858
constructor({
5959
appName,

0 commit comments

Comments
 (0)