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

Commit 5da2897

Browse files
authored
feat: register metrics with custom registry (#4)
Some applications (*cough cough lodestar*) use a custom registry to register metrics. This allows for more graceful capture of metrics from multiple services within a single javascript runtime. This PR adds the feature to pass in a custom registry to register all libp2p metrics with.
1 parent ba33924 commit 5da2897

File tree

6 files changed

+63
-19
lines changed

6 files changed

+63
-19
lines changed

src/counter-group.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import type { CounterGroup, CalculatedMetricOptions, CalculateMetric } from '@libp2p/interface-metrics'
1+
import type { CounterGroup, CalculateMetric } from '@libp2p/interface-metrics'
22
import { Counter as PromCounter, CollectFunction } from 'prom-client'
3+
import type { PrometheusCalculatedMetricOptions } from './index.js'
34
import { normaliseString } from './utils.js'
45

56
export class PrometheusCounterGroup implements CounterGroup {
67
private readonly counter: PromCounter
78
private readonly label: string
89

9-
constructor (name: string, opts: CalculatedMetricOptions<Record<string, number>>) {
10+
constructor (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>) {
1011
name = normaliseString(name)
1112
const help = normaliseString(opts.help ?? name)
1213
const label = this.label = normaliseString(opts.label ?? name)
@@ -29,6 +30,7 @@ export class PrometheusCounterGroup implements CounterGroup {
2930
name,
3031
help,
3132
labelNames: [this.label],
33+
registers: opts.registry !== undefined ? [opts.registry] : undefined,
3234
collect
3335
})
3436
}

src/counter.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { Counter, CalculatedMetricOptions } from '@libp2p/interface-metrics'
1+
import type { Counter } from '@libp2p/interface-metrics'
22
import { CollectFunction, Counter as PromCounter } from 'prom-client'
3+
import type { PrometheusCalculatedMetricOptions } from './index.js'
34
import { normaliseString } from './utils.js'
45

56
export class PrometheusCounter implements Counter {
67
private readonly counter: PromCounter
78

8-
constructor (name: string, opts: CalculatedMetricOptions) {
9+
constructor (name: string, opts: PrometheusCalculatedMetricOptions) {
910
name = normaliseString(name)
1011
const help = normaliseString(opts.help ?? name)
1112
const labels = opts.label != null ? [normaliseString(opts.label)] : []
@@ -26,6 +27,7 @@ export class PrometheusCounter implements Counter {
2627
name,
2728
help,
2829
labelNames: labels,
30+
registers: opts.registry !== undefined ? [opts.registry] : undefined,
2931
collect
3032
})
3133
}

src/index.ts

+24-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CalculatedMetricOptions, Counter, CounterGroup, Metric, MetricGroup, MetricOptions, Metrics } from '@libp2p/interface-metrics'
2-
import { collectDefaultMetrics, DefaultMetricsCollectorConfiguration, register } from 'prom-client'
2+
import { collectDefaultMetrics, DefaultMetricsCollectorConfiguration, register, Registry } from 'prom-client'
33
import type { MultiaddrConnection, Stream, Connection } from '@libp2p/interface-connection'
44
import type { Duplex } from 'it-stream-types'
55
import each from 'it-foreach'
@@ -12,6 +12,12 @@ import { logger } from '@libp2p/logger'
1212
const log = logger('libp2p:prometheus-metrics')
1313

1414
export interface PrometheusMetricsInit {
15+
/**
16+
* Use a custom registry to register metrics.
17+
* By default, the global registry is used to register metrics.
18+
*/
19+
registry?: Registry
20+
1521
/**
1622
* By default we collect default metrics - CPU, memory etc, to not do
1723
* this, pass true here
@@ -31,18 +37,25 @@ export interface PrometheusMetricsInit {
3137
preserveExistingMetrics?: boolean
3238
}
3339

40+
export interface PrometheusCalculatedMetricOptions<T=number> extends CalculatedMetricOptions<T> {
41+
registry?: Registry
42+
}
43+
3444
class PrometheusMetrics implements Metrics {
3545
private transferStats: Map<string, number>
46+
private readonly registry?: Registry
3647

3748
constructor (init?: Partial<PrometheusMetricsInit>) {
49+
this.registry = init?.registry
50+
3851
if (init?.preserveExistingMetrics !== true) {
3952
log('Clearing existing metrics')
40-
register.clear()
53+
;(this.registry ?? register).clear()
4154
}
4255

4356
if (init?.preserveExistingMetrics !== false) {
4457
log('Collecting default metrics')
45-
collectDefaultMetrics(init?.defaultMetrics)
58+
collectDefaultMetrics({ ...init?.defaultMetrics, register: this.registry ?? init?.defaultMetrics?.register })
4659
}
4760

4861
// holds global and per-protocol sent/received stats
@@ -120,60 +133,60 @@ class PrometheusMetrics implements Metrics {
120133
this._track(stream, stream.stat.protocol)
121134
}
122135

123-
registerMetric (name: string, opts: CalculatedMetricOptions): void
136+
registerMetric (name: string, opts: PrometheusCalculatedMetricOptions): void
124137
registerMetric (name: string, opts?: MetricOptions): Metric
125138
registerMetric (name: string, opts: any = {}): any {
126139
if (name == null ?? name.trim() === '') {
127140
throw new Error('Metric name is required')
128141
}
129142

130143
log('Register metric', name)
131-
const metric = new PrometheusMetric(name, opts ?? {})
144+
const metric = new PrometheusMetric(name, { registry: this.registry, ...opts })
132145

133146
if (opts.calculate == null) {
134147
return metric
135148
}
136149
}
137150

138-
registerMetricGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
151+
registerMetricGroup (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>): void
139152
registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup
140153
registerMetricGroup (name: string, opts: any = {}): any {
141154
if (name == null ?? name.trim() === '') {
142155
throw new Error('Metric name is required')
143156
}
144157

145158
log('Register metric group', name)
146-
const group = new PrometheusMetricGroup(name, opts ?? {})
159+
const group = new PrometheusMetricGroup(name, { registry: this.registry, ...opts })
147160

148161
if (opts.calculate == null) {
149162
return group
150163
}
151164
}
152165

153-
registerCounter (name: string, opts: CalculatedMetricOptions): void
166+
registerCounter (name: string, opts: PrometheusCalculatedMetricOptions): void
154167
registerCounter (name: string, opts?: MetricOptions): Counter
155168
registerCounter (name: string, opts: any = {}): any {
156169
if (name == null ?? name.trim() === '') {
157170
throw new Error('Counter name is required')
158171
}
159172

160173
log('Register counter', name)
161-
const counter = new PrometheusCounter(name, opts)
174+
const counter = new PrometheusCounter(name, { registry: this.registry, ...opts })
162175

163176
if (opts.calculate == null) {
164177
return counter
165178
}
166179
}
167180

168-
registerCounterGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
181+
registerCounterGroup (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>): void
169182
registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup
170183
registerCounterGroup (name: string, opts: any = {}): any {
171184
if (name == null ?? name.trim() === '') {
172185
throw new Error('Metric name is required')
173186
}
174187

175188
log('Register counter group', name)
176-
const group = new PrometheusCounterGroup(name, opts)
189+
const group = new PrometheusCounterGroup(name, { registry: this.registry, ...opts })
177190

178191
if (opts.calculate == null) {
179192
return group

src/metric-group.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import type { CalculatedMetricOptions, CalculateMetric, MetricGroup, StopTimer } from '@libp2p/interface-metrics'
1+
import type { CalculateMetric, MetricGroup, StopTimer } from '@libp2p/interface-metrics'
22
import { CollectFunction, Gauge } from 'prom-client'
3+
import type { PrometheusCalculatedMetricOptions } from './index.js'
34
import { normaliseString } from './utils.js'
45

56
export class PrometheusMetricGroup implements MetricGroup {
67
private readonly gauge: Gauge
78
private readonly label: string
89

9-
constructor (name: string, opts: CalculatedMetricOptions<Record<string, number>>) {
10+
constructor (name: string, opts: PrometheusCalculatedMetricOptions<Record<string, number>>) {
1011
name = normaliseString(name)
1112
const help = normaliseString(opts.help ?? name)
1213
const label = this.label = normaliseString(opts.label ?? name)
@@ -29,6 +30,7 @@ export class PrometheusMetricGroup implements MetricGroup {
2930
name,
3031
help,
3132
labelNames: [this.label],
33+
registers: opts.registry !== undefined ? [opts.registry] : undefined,
3234
collect
3335
})
3436
}

src/metric.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { Metric, CalculatedMetricOptions, StopTimer } from '@libp2p/interface-metrics'
1+
import type { Metric, StopTimer } from '@libp2p/interface-metrics'
22
import { CollectFunction, Gauge } from 'prom-client'
3+
import type { PrometheusCalculatedMetricOptions } from './index.js'
34
import { normaliseString } from './utils.js'
45

56
export class PrometheusMetric implements Metric {
67
private readonly gauge: Gauge
78

8-
constructor (name: string, opts: CalculatedMetricOptions) {
9+
constructor (name: string, opts: PrometheusCalculatedMetricOptions) {
910
name = normaliseString(name)
1011
const help = normaliseString(opts.help ?? name)
1112
const labels = opts.label != null ? [normaliseString(opts.label)] : []
@@ -26,6 +27,7 @@ export class PrometheusMetric implements Metric {
2627
name,
2728
help,
2829
labelNames: labels,
30+
registers: opts.registry !== undefined ? [opts.registry] : undefined,
2931
collect
3032
})
3133
}

test/custom-registry.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect } from 'aegir/chai'
2+
import { prometheusMetrics } from '../src/index.js'
3+
import client, { Registry } from 'prom-client'
4+
import { randomMetricName } from './fixtures/random-metric-name.js'
5+
6+
describe('custom registry', () => {
7+
it('should set a metric in the custom registry and not in the global registry', async () => {
8+
const metricName = randomMetricName()
9+
const metricValue = 5
10+
const registry = new Registry()
11+
const metrics = prometheusMetrics({ registry })()
12+
const metric = metrics.registerMetric(metricName)
13+
metric.update(metricValue)
14+
15+
const customRegistryReport = await registry.metrics()
16+
expect(customRegistryReport).to.include(`# TYPE ${metricName} gauge`, 'did not include metric type')
17+
expect(customRegistryReport).to.include(`${metricName} ${metricValue}`, 'did not include updated metric')
18+
19+
const globalRegistryReport = await client.register.metrics()
20+
expect(globalRegistryReport).to.not.include(`# TYPE ${metricName} gauge`, 'erroneously includes metric type')
21+
expect(globalRegistryReport).to.not.include(`${metricName} ${metricValue}`, 'erroneously includes updated metric')
22+
})
23+
})

0 commit comments

Comments
 (0)