Skip to content

Commit bb95928

Browse files
authored
feat: push impact metrics (#742)
1 parent a7565ef commit bb95928

File tree

8 files changed

+245
-125
lines changed

8 files changed

+245
-125
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"ignoreUrls": true
2121
}
2222
],
23+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
2324
"class-methods-use-this": "off"
2425
},
2526
"settings": {

src/impact-metrics/metric-client.ts

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,5 @@
11
import { Unleash } from '../unleash';
22

3-
function forwardMethods<T extends object>(target: T, source: T, methodNames: (keyof T)[]) {
4-
for (const name of methodNames) {
5-
// @ts-ignore
6-
target[name] = (...args: any[]) => source[name](...args);
7-
}
8-
}
9-
10-
export class UnleashMetricClient {
11-
private backingInstance: Unleash;
12-
13-
constructor(unleash: Unleash) {
14-
this.backingInstance = unleash;
15-
forwardMethods(this, this.backingInstance as any, [
16-
'isEnabled',
17-
'getVariant',
18-
'forceGetVariant',
19-
'getFeatureToggleDefinition',
20-
'getFeatureToggleDefinitions',
21-
'count',
22-
'countVariant',
23-
'flushMetrics',
24-
'destroyWithFlush',
25-
'on',
26-
'start',
27-
'destroy',
28-
'isSynchronized',
29-
]);
30-
}
31-
32-
isEnabled!: Unleash['isEnabled'];
33-
34-
getVariant!: Unleash['getVariant'];
35-
36-
forceGetVariant!: Unleash['forceGetVariant'];
37-
38-
getFeatureToggleDefinition!: Unleash['getFeatureToggleDefinition'];
39-
40-
getFeatureToggleDefinitions!: Unleash['getFeatureToggleDefinitions'];
41-
42-
count!: Unleash['count'];
43-
44-
countVariant!: Unleash['countVariant'];
45-
46-
flushMetrics!: Unleash['flushMetrics'];
47-
48-
destroyWithFlush!: Unleash['destroyWithFlush'];
49-
50-
on!: Unleash['on'];
51-
52-
start!: Unleash['start'];
53-
54-
destroy!: Unleash['destroy'];
55-
56-
isSynchronized!: Unleash['isSynchronized'];
3+
export class UnleashMetricClient extends Unleash {
4+
// empty for now, we'll use this to add in the public API for metrics later
575
}

src/impact-metrics/metric-types.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class CounterImpl implements Counter {
5353
value,
5454
}));
5555

56+
this.values.clear();
57+
5658
return {
5759
name: this.opts.name,
5860
help: this.opts.help,
@@ -92,6 +94,8 @@ class GaugeImpl implements Gauge {
9294
value,
9395
}));
9496

97+
this.values.clear();
98+
9599
return {
96100
name: this.opts.name,
97101
help: this.opts.help,
@@ -113,7 +117,12 @@ export interface Gauge {
113117
set(value: number, labels?: MetricLabels): void;
114118
}
115119

116-
export class ImpactMetricRegistry {
120+
export interface ImpactMetricRegistry {
121+
collect(): CollectedMetric[];
122+
restore(metrics: CollectedMetric[]): void;
123+
}
124+
125+
export class InMemoryMetricRegistry implements ImpactMetricRegistry {
117126
private counters = new Map<string, Counter & CollectibleMetric>();
118127

119128
private gauges = new Map<string, Gauge & CollectibleMetric>();
@@ -137,7 +146,32 @@ export class ImpactMetricRegistry {
137146
collect(): CollectedMetric[] {
138147
const allCounters = [...this.counters.values()].map((c) => c.collect());
139148
const allGauges = [...this.gauges.values()].map((g) => g.collect());
140-
return [...allCounters, ...allGauges];
149+
const allMetrics = [...allCounters, ...allGauges];
150+
151+
const nonEmpty = allMetrics.filter((metric) => metric.samples.length > 0);
152+
return nonEmpty.length > 0 ? nonEmpty : [];
153+
}
154+
155+
restore(metrics: CollectedMetric[]): void {
156+
for (const metric of metrics) {
157+
switch (metric.type) {
158+
case 'counter': {
159+
const counter = this.counter({ name: metric.name, help: metric.help });
160+
for (const sample of metric.samples) {
161+
counter.inc(sample.value, sample.labels);
162+
}
163+
break;
164+
}
165+
166+
case 'gauge': {
167+
const gauge = this.gauge({ name: metric.name, help: metric.help });
168+
for (const sample of metric.samples) {
169+
gauge.set(sample.value, sample.labels);
170+
}
171+
break;
172+
}
173+
}
174+
}
141175
}
142176
}
143177

src/metric-client.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

src/metrics.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +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';
1011

1112
export interface MetricsOptions {
1213
appName: string;
@@ -21,6 +22,7 @@ export interface MetricsOptions {
2122
customHeadersFunction?: CustomHeadersFunction;
2223
timeout?: number;
2324
httpOptions?: HttpOptions;
25+
metricRegistry?: ImpactMetricRegistry;
2426
}
2527

2628
interface VariantBucket {
@@ -66,6 +68,7 @@ interface BaseMetricsData {
6668

6769
interface MetricsData extends BaseMetricsData {
6870
bucket: Bucket;
71+
impactMetrics?: CollectedMetric[];
6972
}
7073

7174
interface RegistrationData extends BaseMetricsData {
@@ -112,6 +115,8 @@ export default class Metrics extends EventEmitter {
112115

113116
private platformData: PlatformData;
114117

118+
private metricRegistry?: ImpactMetricRegistry;
119+
115120
constructor({
116121
appName,
117122
instanceId,
@@ -125,6 +130,7 @@ export default class Metrics extends EventEmitter {
125130
customHeadersFunction,
126131
timeout,
127132
httpOptions,
133+
metricRegistry,
128134
}: MetricsOptions) {
129135
super();
130136
this.disabled = disableMetrics;
@@ -143,6 +149,7 @@ export default class Metrics extends EventEmitter {
143149
this.bucket = this.createBucket();
144150
this.httpOptions = httpOptions;
145151
this.platformData = this.getPlatformData();
152+
this.metricRegistry = metricRegistry;
146153
}
147154

148155
private getAppliedJitter(): number {
@@ -241,13 +248,16 @@ export default class Metrics extends EventEmitter {
241248
if (this.disabled) {
242249
return;
243250
}
244-
if (this.bucketIsEmpty()) {
251+
const impactMetrics = this.metricRegistry?.collect() || [];
252+
253+
if (this.bucketIsEmpty() && impactMetrics.length === 0) {
245254
this.resetBucket();
246255
this.startTimer();
256+
this.metricRegistry?.restore(impactMetrics);
247257
return;
248258
}
249259
const url = resolveUrl(suffixSlash(this.url), './client/metrics');
250-
const payload = this.createMetricsData();
260+
const payload = this.createMetricsData(impactMetrics);
251261

252262
const headers = this.customHeadersFunction ? await this.customHeadersFunction() : this.headers;
253263

@@ -277,12 +287,14 @@ export default class Metrics extends EventEmitter {
277287
this.backoff(url, res.status);
278288
}
279289
this.restoreBucket(payload.bucket);
290+
this.metricRegistry?.restore(impactMetrics);
280291
} else {
281292
this.emit(UnleashEvents.Sent, payload);
282293
this.reduceBackoff();
283294
}
284295
} catch (err) {
285296
this.restoreBucket(payload.bucket);
297+
this.metricRegistry?.restore(impactMetrics);
286298
this.emit(UnleashEvents.Warn, err);
287299
this.startTimer();
288300
}
@@ -356,10 +368,11 @@ export default class Metrics extends EventEmitter {
356368
this.bucket = this.createBucket();
357369
}
358370

359-
createMetricsData(): MetricsData {
371+
createMetricsData(impactMetrics: CollectedMetric[]): MetricsData {
360372
const bucket = { ...this.bucket, stop: new Date() };
361373
this.resetBucket();
362-
return {
374+
375+
const base: MetricsData = {
363376
appName: this.appName,
364377
instanceId: this.instanceId,
365378
connectionId: this.connectionId,
@@ -369,6 +382,12 @@ export default class Metrics extends EventEmitter {
369382
yggdrasilVersion: null,
370383
specVersion: SUPPORTED_SPEC_VERSION,
371384
};
385+
386+
if (impactMetrics.length > 0) {
387+
base.impactMetrics = impactMetrics;
388+
}
389+
390+
return base;
372391
}
373392

374393
private restoreBucket(bucket: Bucket): void {

0 commit comments

Comments
 (0)