Skip to content
This repository was archived by the owner on Aug 24, 2023. It is now read-only.

Commit 92bde9b

Browse files
fix: allow multiple consumers of metrics (#6)
To support using labels as disambiguators across multiple reporters of the same metric, cache metrics globally then return the pre-exsting metric when it's registered on subsequent occasions. Co-authored-by: Marin Petrunic <marin.petrunic@gmail.com>
1 parent 123fb81 commit 92bde9b

10 files changed

+221
-32
lines changed

src/counter-group.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import type { CounterGroup, CalculateMetric } from '@libp2p/interface-metrics'
22
import { Counter as PromCounter, CollectFunction } from 'prom-client'
3+
import { normaliseString, CalculatedMetric } from './utils.js'
34
import type { PrometheusCalculatedMetricOptions } from './index.js'
4-
import { normaliseString } from './utils.js'
55

6-
export class PrometheusCounterGroup implements CounterGroup {
6+
export class PrometheusCounterGroup implements CounterGroup, CalculatedMetric<Record<string, number>> {
77
private readonly counter: PromCounter
88
private readonly label: string
9+
private readonly calculators: Array<CalculateMetric<Record<string, number>>>
910

1011
constructor (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>) {
1112
name = normaliseString(name)
1213
const help = normaliseString(opts.help ?? name)
1314
const label = this.label = normaliseString(opts.label ?? name)
1415
let collect: CollectFunction<PromCounter<any>> | undefined
16+
this.calculators = []
1517

1618
// calculated metric
1719
if (opts?.calculate != null) {
18-
const calculate: CalculateMetric<Record<string, number>> = opts.calculate
20+
this.calculators.push(opts.calculate)
21+
const self = this
1922

2023
collect = async function () {
21-
const values = await calculate()
24+
await Promise.all(self.calculators.map(async calculate => {
25+
const values = await calculate()
2226

23-
Object.entries(values).forEach(([key, value]) => {
24-
this.inc({ [label]: key }, value)
25-
})
27+
Object.entries(values).forEach(([key, value]) => {
28+
this.inc({ [label]: key }, value)
29+
})
30+
}))
2631
}
2732
}
2833

@@ -35,6 +40,10 @@ export class PrometheusCounterGroup implements CounterGroup {
3540
})
3641
}
3742

43+
addCalculator (calculator: CalculateMetric<Record<string, number>>) {
44+
this.calculators.push(calculator)
45+
}
46+
3847
increment (values: Record<string, number | unknown>): void {
3948
Object.entries(values).forEach(([key, value]) => {
4049
const inc = typeof value === 'number' ? value : 1

src/counter.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
1-
import type { Counter } from '@libp2p/interface-metrics'
1+
import type { CalculateMetric, Counter } from '@libp2p/interface-metrics'
22
import { CollectFunction, Counter as PromCounter } from 'prom-client'
33
import type { PrometheusCalculatedMetricOptions } from './index.js'
4-
import { normaliseString } from './utils.js'
4+
import { normaliseString, CalculatedMetric } from './utils.js'
55

6-
export class PrometheusCounter implements Counter {
6+
export class PrometheusCounter implements Counter, CalculatedMetric {
77
private readonly counter: PromCounter
8+
private readonly calculators: CalculateMetric[]
89

910
constructor (name: string, opts: PrometheusCalculatedMetricOptions) {
1011
name = normaliseString(name)
1112
const help = normaliseString(opts.help ?? name)
1213
const labels = opts.label != null ? [normaliseString(opts.label)] : []
1314
let collect: CollectFunction<PromCounter<any>> | undefined
15+
this.calculators = []
1416

1517
// calculated metric
1618
if (opts?.calculate != null) {
17-
const calculate = opts.calculate
19+
this.calculators.push(opts.calculate)
20+
const self = this
1821

1922
collect = async function () {
20-
const value = await calculate()
23+
const values = await Promise.all(self.calculators.map(async calculate => await calculate()))
24+
const sum = values.reduce((acc, curr) => acc + curr, 0)
2125

22-
this.inc(value)
26+
this.inc(sum)
2327
}
2428
}
2529

@@ -32,6 +36,10 @@ export class PrometheusCounter implements Counter {
3236
})
3337
}
3438

39+
addCalculator (calculator: CalculateMetric) {
40+
this.calculators.push(calculator)
41+
}
42+
3543
increment (value: number = 1): void {
3644
this.counter.inc(value)
3745
}

src/index.ts

+68-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { logger } from '@libp2p/logger'
1111

1212
const log = logger('libp2p:prometheus-metrics')
1313

14+
// prom-client metrics are global
15+
const metrics = new Map<string, any>()
16+
1417
export interface PrometheusMetricsInit {
1518
/**
1619
* Use a custom registry to register metrics.
@@ -50,6 +53,7 @@ class PrometheusMetrics implements Metrics {
5053

5154
if (init?.preserveExistingMetrics !== true) {
5255
log('Clearing existing metrics')
56+
metrics.clear()
5357
;(this.registry ?? register).clear()
5458
}
5559

@@ -140,8 +144,22 @@ class PrometheusMetrics implements Metrics {
140144
throw new Error('Metric name is required')
141145
}
142146

147+
let metric = metrics.get(name)
148+
149+
if (metrics.has(name)) {
150+
log('Reuse existing metric', name)
151+
152+
if (opts.calculate != null) {
153+
metric.addCalculator(opts.calculate)
154+
}
155+
156+
return metrics.get(name)
157+
}
158+
143159
log('Register metric', name)
144-
const metric = new PrometheusMetric(name, { registry: this.registry, ...opts })
160+
metric = new PrometheusMetric(name, { registry: this.registry, ...opts })
161+
162+
metrics.set(name, metric)
145163

146164
if (opts.calculate == null) {
147165
return metric
@@ -152,14 +170,28 @@ class PrometheusMetrics implements Metrics {
152170
registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup
153171
registerMetricGroup (name: string, opts: any = {}): any {
154172
if (name == null ?? name.trim() === '') {
155-
throw new Error('Metric name is required')
173+
throw new Error('Metric group name is required')
174+
}
175+
176+
let metricGroup = metrics.get(name)
177+
178+
if (metricGroup != null) {
179+
log('Reuse existing metric group', name)
180+
181+
if (opts.calculate != null) {
182+
metricGroup.addCalculator(opts.calculate)
183+
}
184+
185+
return metricGroup
156186
}
157187

158188
log('Register metric group', name)
159-
const group = new PrometheusMetricGroup(name, { registry: this.registry, ...opts })
189+
metricGroup = new PrometheusMetricGroup(name, { registry: this.registry, ...opts })
190+
191+
metrics.set(name, metricGroup)
160192

161193
if (opts.calculate == null) {
162-
return group
194+
return metricGroup
163195
}
164196
}
165197

@@ -170,8 +202,22 @@ class PrometheusMetrics implements Metrics {
170202
throw new Error('Counter name is required')
171203
}
172204

205+
let counter = metrics.get(name)
206+
207+
if (counter != null) {
208+
log('Reuse existing counter', name)
209+
210+
if (opts.calculate != null) {
211+
counter.addCalculator(opts.calculate)
212+
}
213+
214+
return metrics.get(name)
215+
}
216+
173217
log('Register counter', name)
174-
const counter = new PrometheusCounter(name, { registry: this.registry, ...opts })
218+
counter = new PrometheusCounter(name, { registry: this.registry, ...opts })
219+
220+
metrics.set(name, counter)
175221

176222
if (opts.calculate == null) {
177223
return counter
@@ -182,14 +228,28 @@ class PrometheusMetrics implements Metrics {
182228
registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup
183229
registerCounterGroup (name: string, opts: any = {}): any {
184230
if (name == null ?? name.trim() === '') {
185-
throw new Error('Metric name is required')
231+
throw new Error('Counter group name is required')
232+
}
233+
234+
let counterGroup = metrics.get(name)
235+
236+
if (counterGroup != null) {
237+
log('Reuse existing counter group', name)
238+
239+
if (opts.calculate != null) {
240+
counterGroup.addCalculator(opts.calculate)
241+
}
242+
243+
return counterGroup
186244
}
187245

188246
log('Register counter group', name)
189-
const group = new PrometheusCounterGroup(name, { registry: this.registry, ...opts })
247+
counterGroup = new PrometheusCounterGroup(name, { registry: this.registry, ...opts })
248+
249+
metrics.set(name, counterGroup)
190250

191251
if (opts.calculate == null) {
192-
return group
252+
return counterGroup
193253
}
194254
}
195255
}

src/metric-group.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import type { CalculateMetric, MetricGroup, StopTimer } from '@libp2p/interface-metrics'
22
import { CollectFunction, Gauge } from 'prom-client'
33
import type { PrometheusCalculatedMetricOptions } from './index.js'
4-
import { normaliseString } from './utils.js'
4+
import { normaliseString, CalculatedMetric } from './utils.js'
55

6-
export class PrometheusMetricGroup implements MetricGroup {
6+
export class PrometheusMetricGroup implements MetricGroup, CalculatedMetric<Record<string, number>> {
77
private readonly gauge: Gauge
88
private readonly label: string
9+
private readonly calculators: Array<CalculateMetric<Record<string, number>>>
910

1011
constructor (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>) {
1112
name = normaliseString(name)
1213
const help = normaliseString(opts.help ?? name)
1314
const label = this.label = normaliseString(opts.label ?? name)
1415
let collect: CollectFunction<Gauge<any>> | undefined
16+
this.calculators = []
1517

1618
// calculated metric
1719
if (opts?.calculate != null) {
18-
const calculate: CalculateMetric<Record<string, number>> = opts.calculate
20+
this.calculators.push(opts.calculate)
21+
const self = this
1922

2023
collect = async function () {
21-
const values = await calculate()
24+
await Promise.all(self.calculators.map(async calculate => {
25+
const values = await calculate()
2226

23-
Object.entries(values).forEach(([key, value]) => {
24-
this.set({ [label]: key }, value)
25-
})
27+
Object.entries(values).forEach(([key, value]) => {
28+
this.set({ [label]: key }, value)
29+
})
30+
}))
2631
}
2732
}
2833

@@ -35,6 +40,10 @@ export class PrometheusMetricGroup implements MetricGroup {
3540
})
3641
}
3742

43+
addCalculator (calculator: CalculateMetric<Record<string, number>>) {
44+
this.calculators.push(calculator)
45+
}
46+
3847
update (values: Record<string, number>): void {
3948
Object.entries(values).forEach(([key, value]) => {
4049
this.gauge.set({ [this.label]: key }, value)

src/metric.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
1-
import type { Metric, StopTimer } from '@libp2p/interface-metrics'
1+
import type { Metric, StopTimer, CalculateMetric } from '@libp2p/interface-metrics'
22
import { CollectFunction, Gauge } from 'prom-client'
33
import type { PrometheusCalculatedMetricOptions } from './index.js'
44
import { normaliseString } from './utils.js'
55

66
export class PrometheusMetric implements Metric {
77
private readonly gauge: Gauge
8+
private readonly calculators: CalculateMetric[]
89

910
constructor (name: string, opts: PrometheusCalculatedMetricOptions) {
1011
name = normaliseString(name)
1112
const help = normaliseString(opts.help ?? name)
1213
const labels = opts.label != null ? [normaliseString(opts.label)] : []
1314
let collect: CollectFunction<Gauge<any>> | undefined
15+
this.calculators = []
1416

1517
// calculated metric
1618
if (opts?.calculate != null) {
17-
const calculate = opts.calculate
19+
this.calculators.push(opts.calculate)
20+
const self = this
1821

1922
collect = async function () {
20-
const value = await calculate()
23+
const values = await Promise.all(self.calculators.map(async calculate => await calculate()))
24+
const sum = values.reduce((acc, curr) => acc + curr, 0)
2125

22-
this.set(value)
26+
this.set(sum)
2327
}
2428
}
2529

@@ -32,6 +36,10 @@ export class PrometheusMetric implements Metric {
3236
})
3337
}
3438

39+
addCalculator (calculator: CalculateMetric) {
40+
this.calculators.push(calculator)
41+
}
42+
3543
update (value: number): void {
3644
this.gauge.set(value)
3745
}

src/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import type { CalculateMetric } from '@libp2p/interface-metrics'
2+
3+
export interface CalculatedMetric <T = number> {
4+
addCalculator: (calculator: CalculateMetric<T>) => void
5+
}
16

27
export const ONE_SECOND = 1000
38
export const ONE_MINUTE = 60 * ONE_SECOND

test/counter-groups.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,31 @@ describe('counter groups', () => {
9090

9191
await expect(client.register.metrics()).to.eventually.not.include(metricKey, 'still included metric key')
9292
})
93+
94+
it('should allow use of the same counter group from multiple reporters', async () => {
95+
const metricName = randomMetricName()
96+
const metricKey1 = randomMetricName('key_')
97+
const metricKey2 = randomMetricName('key_')
98+
const metricLabel = randomMetricName('label_')
99+
const metricValue1 = 5
100+
const metricValue2 = 7
101+
const metrics = prometheusMetrics()()
102+
const metric1 = metrics.registerCounterGroup(metricName, {
103+
label: metricLabel
104+
})
105+
metric1.increment({
106+
[metricKey1]: metricValue1
107+
})
108+
const metric2 = metrics.registerCounterGroup(metricName, {
109+
label: metricLabel
110+
})
111+
metric2.increment({
112+
[metricKey2]: metricValue2
113+
})
114+
115+
const reportedMetrics = await client.register.metrics()
116+
117+
expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey1}"} ${metricValue1}`, 'did not include updated metric')
118+
expect(reportedMetrics).to.include(`${metricName}{${metricLabel}="${metricKey2}"} ${metricValue2}`, 'did not include updated metric')
119+
})
93120
})

test/counters.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,22 @@ describe('counters', () => {
6464

6565
await expect(client.register.metrics()).to.eventually.include(`${metricName} 0`, 'did not include updated metric')
6666
})
67+
68+
it('should allow use of the same counter from multiple reporters', async () => {
69+
const metricName = randomMetricName()
70+
const metricLabel = randomMetricName('label_')
71+
const metricValue1 = 5
72+
const metricValue2 = 7
73+
const metrics = prometheusMetrics()()
74+
const metric1 = metrics.registerCounter(metricName, {
75+
label: metricLabel
76+
})
77+
metric1.increment(metricValue1)
78+
const metric2 = metrics.registerCounter(metricName, {
79+
label: metricLabel
80+
})
81+
metric2.increment(metricValue2)
82+
83+
await expect(client.register.metrics()).to.eventually.include(`${metricName} ${metricValue1 + metricValue2}`)
84+
})
6785
})

0 commit comments

Comments
 (0)