Skip to content

Commit 01328a0

Browse files
authored
fix: clear references to metrics on stop (#3154)
Otherwise the node can leak memory.
1 parent b32bc84 commit 01328a0

File tree

7 files changed

+510
-171
lines changed

7 files changed

+510
-171
lines changed

packages/metrics-opentelemetry/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"it-stream-types": "^2.0.2"
4848
},
4949
"devDependencies": {
50+
"@libp2p/logger": "^5.1.18",
5051
"aegir": "^47.0.14"
5152
},
5253
"browser": {

packages/metrics-opentelemetry/src/index.ts

Lines changed: 135 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ import { OpenTelemetryMetric } from './metric.js'
4848
import { OpenTelemetrySummaryGroup } from './summary-group.js'
4949
import { OpenTelemetrySummary } from './summary.js'
5050
import { collectSystemMetrics } from './system-metrics.js'
51-
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, Metrics, CalculatedMetricOptions, MetricOptions, Counter, CounterGroup, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions, NodeInfo, TraceFunctionOptions, TraceGeneratorFunctionOptions, TraceAttributes } from '@libp2p/interface'
52-
import type { Span, Attributes } from '@opentelemetry/api'
51+
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, Metrics, CalculatedMetricOptions, MetricOptions, Counter, CounterGroup, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions, NodeInfo, TraceFunctionOptions, TraceGeneratorFunctionOptions, TraceAttributes, ComponentLogger, Logger } from '@libp2p/interface'
52+
import type { Span, Attributes, Meter, Observable } from '@opentelemetry/api'
5353
import type { Duplex } from 'it-stream-types'
5454

5555
// see https://betterstack.com/community/guides/observability/opentelemetry-metrics-nodejs/#prerequisites
5656

5757
export interface OpenTelemetryComponents {
5858
nodeInfo: NodeInfo
59+
logger: ComponentLogger
5960
}
6061

6162
export interface OpenTelemetryMetricsInit {
@@ -92,14 +93,20 @@ export interface OpenTelemetryMetricsInit {
9293
class OpenTelemetryMetrics implements Metrics {
9394
private transferStats: Map<string, number>
9495
private readonly tracer: ReturnType<typeof trace.getTracer>
95-
private readonly meterName: string
96+
private readonly meter: Meter
97+
private readonly log: Logger
98+
private metrics: Map<string, OpenTelemetryMetric | OpenTelemetryMetricGroup | OpenTelemetryCounter | OpenTelemetryCounterGroup | OpenTelemetryHistogram | OpenTelemetryHistogramGroup | OpenTelemetrySummary | OpenTelemetrySummaryGroup>
99+
private observables: Map<string, Observable>
96100

97101
constructor (components: OpenTelemetryComponents, init?: OpenTelemetryMetricsInit) {
102+
this.log = components.logger.forComponent('libp2p:open-telemetry-metrics')
98103
this.tracer = trace.getTracer(init?.appName ?? components.nodeInfo.name, init?.appVersion ?? components.nodeInfo.version)
104+
this.metrics = new Map()
105+
this.observables = new Map()
99106

100107
// holds global and per-protocol sent/received stats
101108
this.transferStats = new Map()
102-
this.meterName = init?.meterName ?? components.nodeInfo.name
109+
this.meter = metrics.getMeterProvider().getMeter(init?.meterName ?? components.nodeInfo.name)
103110

104111
this.registerCounterGroup('libp2p_data_transfer_bytes_total', {
105112
label: 'protocol',
@@ -126,6 +133,16 @@ class OpenTelemetryMetrics implements Metrics {
126133
'@libp2p/metrics'
127134
]
128135

136+
start (): void {
137+
138+
}
139+
140+
stop (): void {
141+
this.transferStats.clear()
142+
this.metrics.clear()
143+
this.observables.clear()
144+
}
145+
129146
/**
130147
* Increment the transfer stat for the passed key, making sure
131148
* it exists first
@@ -177,23 +194,32 @@ class OpenTelemetryMetrics implements Metrics {
177194
throw new InvalidParametersError('Metric name is required')
178195
}
179196

180-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
181-
182197
if (isCalculatedMetricOptions<CalculatedMetricOptions>(opts)) {
183-
const calculate = opts.calculate
184-
const counter = meter.createObservableGauge(name, {
198+
const gauge = this.observables.get(name) ?? this.meter.createObservableGauge(name, {
185199
description: opts?.help ?? name
186200
})
187-
counter.addCallback(async (result) => {
201+
202+
const calculate = opts.calculate
203+
gauge.addCallback(async (result) => {
188204
result.observe(await calculate())
189205
})
190206

207+
this.observables.set(name, gauge)
208+
191209
return
192210
}
193211

194-
return new OpenTelemetryMetric(meter.createGauge(name, {
195-
description: opts?.help ?? name
196-
}))
212+
let metric = this.metrics.get(name)
213+
214+
if (metric == null) {
215+
metric = new OpenTelemetryMetric(this.meter.createGauge(name, {
216+
description: opts?.help ?? name
217+
}))
218+
219+
this.metrics.set(name, metric)
220+
}
221+
222+
return metric
197223
}
198224

199225
registerMetricGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
@@ -203,14 +229,14 @@ class OpenTelemetryMetrics implements Metrics {
203229
throw new InvalidParametersError('Metric name is required')
204230
}
205231

206-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
207232
const label = opts?.label ?? name
208233

209234
if (isCalculatedMetricOptions<CalculatedMetricOptions<Record<string, number>>>(opts)) {
210-
const calculate = opts.calculate
211-
const gauge = meter.createObservableGauge(name, {
235+
const gauge = this.observables.get(name) ?? this.meter.createObservableGauge(name, {
212236
description: opts?.help ?? name
213237
})
238+
239+
const calculate = opts.calculate
214240
gauge.addCallback(async (observable) => {
215241
const observed = await calculate()
216242

@@ -221,12 +247,22 @@ class OpenTelemetryMetrics implements Metrics {
221247
}
222248
})
223249

250+
this.observables.set(name, gauge)
251+
224252
return
225253
}
226254

227-
return new OpenTelemetryMetricGroup(label, meter.createGauge(name, {
228-
description: opts?.help ?? name
229-
}))
255+
let metric = this.metrics.get(name)
256+
257+
if (metric == null) {
258+
metric = new OpenTelemetryMetricGroup(label, this.meter.createGauge(name, {
259+
description: opts?.help ?? name
260+
}))
261+
262+
this.metrics.set(name, metric)
263+
}
264+
265+
return metric
230266
}
231267

232268
registerCounter (name: string, opts: CalculatedMetricOptions): void
@@ -236,23 +272,32 @@ class OpenTelemetryMetrics implements Metrics {
236272
throw new InvalidParametersError('Metric name is required')
237273
}
238274

239-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
240-
241275
if (isCalculatedMetricOptions<CalculatedMetricOptions>(opts)) {
242-
const calculate = opts.calculate
243-
const counter = meter.createObservableCounter(name, {
276+
const counter = this.observables.get(name) ?? this.meter.createObservableCounter(name, {
244277
description: opts?.help ?? name
245278
})
279+
280+
const calculate = opts.calculate
246281
counter.addCallback(async (result) => {
247282
result.observe(await calculate())
248283
})
249284

285+
this.observables.set(name, counter)
286+
250287
return
251288
}
252289

253-
return new OpenTelemetryCounter(meter.createCounter(name, {
254-
description: opts?.help ?? name
255-
}))
290+
let metric = this.metrics.get(name)
291+
292+
if (metric == null) {
293+
metric = new OpenTelemetryCounter(this.meter.createCounter(name, {
294+
description: opts?.help ?? name
295+
}))
296+
297+
this.metrics.set(name, metric)
298+
}
299+
300+
return metric
256301
}
257302

258303
registerCounterGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
@@ -262,15 +307,15 @@ class OpenTelemetryMetrics implements Metrics {
262307
throw new InvalidParametersError('Metric name is required')
263308
}
264309

265-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
266310
const label = opts?.label ?? name
267311

268312
if (isCalculatedMetricOptions<CalculatedMetricOptions<Record<string, number>>>(opts)) {
269-
const values: Record<string, number> = {}
270-
const calculate = opts.calculate
271-
const counter = meter.createObservableGauge(name, {
313+
const counter = this.observables.get(name) ?? this.meter.createObservableCounter(name, {
272314
description: opts?.help ?? name
273315
})
316+
317+
const values: Record<string, number> = {}
318+
const calculate = opts.calculate
274319
counter.addCallback(async (observable) => {
275320
const observed = await calculate()
276321

@@ -290,9 +335,17 @@ class OpenTelemetryMetrics implements Metrics {
290335
return
291336
}
292337

293-
return new OpenTelemetryCounterGroup(label, meter.createCounter(name, {
294-
description: opts?.help ?? name
295-
}))
338+
let metric = this.metrics.get(name)
339+
340+
if (metric == null) {
341+
metric = new OpenTelemetryCounterGroup(label, this.meter.createCounter(name, {
342+
description: opts?.help ?? name
343+
}))
344+
345+
this.metrics.set(name, metric)
346+
}
347+
348+
return metric
296349
}
297350

298351
registerHistogram (name: string, opts: CalculatedHistogramOptions): void
@@ -302,18 +355,24 @@ class OpenTelemetryMetrics implements Metrics {
302355
throw new InvalidParametersError('Metric name is required')
303356
}
304357

305-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
306-
307358
if (isCalculatedMetricOptions<CalculatedHistogramOptions>(opts)) {
308359
return
309360
}
310361

311-
return new OpenTelemetryHistogram(meter.createHistogram(name, {
312-
advice: {
313-
explicitBucketBoundaries: opts.buckets
314-
},
315-
description: opts?.help ?? name
316-
}))
362+
let metric = this.metrics.get(name)
363+
364+
if (metric == null) {
365+
metric = new OpenTelemetryHistogram(this.meter.createHistogram(name, {
366+
advice: {
367+
explicitBucketBoundaries: opts.buckets
368+
},
369+
description: opts?.help ?? name
370+
}))
371+
372+
this.metrics.set(name, metric)
373+
}
374+
375+
return metric
317376
}
318377

319378
registerHistogramGroup (name: string, opts: CalculatedHistogramOptions<Record<string, number>>): void
@@ -323,19 +382,26 @@ class OpenTelemetryMetrics implements Metrics {
323382
throw new InvalidParametersError('Metric name is required')
324383
}
325384

326-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
327385
const label = opts?.label ?? name
328386

329387
if (isCalculatedMetricOptions<CalculatedHistogramOptions<Record<string, number>>>(opts)) {
330388
return
331389
}
332390

333-
return new OpenTelemetryHistogramGroup(label, meter.createHistogram(name, {
334-
advice: {
335-
explicitBucketBoundaries: opts.buckets
336-
},
337-
description: opts?.help ?? name
338-
}))
391+
let metric = this.metrics.get(name)
392+
393+
if (metric == null) {
394+
metric = new OpenTelemetryHistogramGroup(label, this.meter.createHistogram(name, {
395+
advice: {
396+
explicitBucketBoundaries: opts.buckets
397+
},
398+
description: opts?.help ?? name
399+
}))
400+
401+
this.metrics.set(name, metric)
402+
}
403+
404+
return metric
339405
}
340406

341407
registerSummary (name: string, opts: CalculatedSummaryOptions): void
@@ -345,15 +411,21 @@ class OpenTelemetryMetrics implements Metrics {
345411
throw new InvalidParametersError('Metric name is required')
346412
}
347413

348-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
349-
350414
if (isCalculatedMetricOptions<CalculatedHistogramOptions>(opts)) {
351415
return
352416
}
353417

354-
return new OpenTelemetrySummary(meter.createGauge(name, {
355-
description: opts?.help ?? name
356-
}))
418+
let metric = this.metrics.get(name)
419+
420+
if (metric == null) {
421+
metric = new OpenTelemetrySummary(this.meter.createGauge(name, {
422+
description: opts?.help ?? name
423+
}))
424+
425+
this.metrics.set(name, metric)
426+
}
427+
428+
return metric
357429
}
358430

359431
registerSummaryGroup (name: string, opts: CalculatedSummaryOptions<Record<string, number>>): void
@@ -363,16 +435,23 @@ class OpenTelemetryMetrics implements Metrics {
363435
throw new InvalidParametersError('Metric name is required')
364436
}
365437

366-
const meter = metrics.getMeterProvider().getMeter(this.meterName)
367438
const label = opts?.label ?? name
368439

369440
if (isCalculatedMetricOptions<CalculatedSummaryOptions>(opts)) {
370441
return
371442
}
372443

373-
return new OpenTelemetrySummaryGroup(label, meter.createGauge(name, {
374-
description: opts?.help ?? name
375-
}))
444+
let metric = this.metrics.get(name)
445+
446+
if (metric == null) {
447+
metric = new OpenTelemetrySummaryGroup(label, this.meter.createGauge(name, {
448+
description: opts?.help ?? name
449+
}))
450+
451+
this.metrics.set(name, metric)
452+
}
453+
454+
return metric
376455
}
377456

378457
createTrace (): any {

0 commit comments

Comments
 (0)