@libp2p/opentelemetry-metrics 0.0.0-abe9bd154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +77 -0
  2. package/dist/index.min.js +3 -0
  3. package/dist/src/counter-group.d.ts +12 -0
  4. package/dist/src/counter-group.d.ts.map +1 -0
  5. package/dist/src/counter-group.js +31 -0
  6. package/dist/src/counter-group.js.map +1 -0
  7. package/dist/src/counter.d.ts +9 -0
  8. package/dist/src/counter.d.ts.map +1 -0
  9. package/dist/src/counter.js +13 -0
  10. package/dist/src/counter.js.map +1 -0
  11. package/dist/src/histogram-group.d.ts +11 -0
  12. package/dist/src/histogram-group.d.ts.map +1 -0
  13. package/dist/src/histogram-group.js +27 -0
  14. package/dist/src/histogram-group.js.map +1 -0
  15. package/dist/src/histogram.d.ts +10 -0
  16. package/dist/src/histogram.d.ts.map +1 -0
  17. package/dist/src/histogram.js +19 -0
  18. package/dist/src/histogram.js.map +1 -0
  19. package/dist/src/index.d.ts +67 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +416 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/metric-group.d.ts +14 -0
  24. package/dist/src/metric-group.d.ts.map +1 -0
  25. package/dist/src/metric-group.js +58 -0
  26. package/dist/src/metric-group.js.map +1 -0
  27. package/dist/src/metric.d.ts +13 -0
  28. package/dist/src/metric.d.ts.map +1 -0
  29. package/dist/src/metric.js +35 -0
  30. package/dist/src/metric.js.map +1 -0
  31. package/dist/src/summary-group.d.ts +12 -0
  32. package/dist/src/summary-group.d.ts.map +1 -0
  33. package/dist/src/summary-group.js +36 -0
  34. package/dist/src/summary-group.js.map +1 -0
  35. package/dist/src/summary.d.ts +10 -0
  36. package/dist/src/summary.d.ts.map +1 -0
  37. package/dist/src/summary.js +19 -0
  38. package/dist/src/summary.js.map +1 -0
  39. package/dist/src/system-metrics.browser.d.ts +2 -0
  40. package/dist/src/system-metrics.browser.d.ts.map +1 -0
  41. package/dist/src/system-metrics.browser.js +3 -0
  42. package/dist/src/system-metrics.browser.js.map +1 -0
  43. package/dist/src/system-metrics.d.ts +6 -0
  44. package/dist/src/system-metrics.d.ts.map +1 -0
  45. package/dist/src/system-metrics.js +439 -0
  46. package/dist/src/system-metrics.js.map +1 -0
  47. package/package.json +62 -0
  48. package/src/counter-group.ts +38 -0
  49. package/src/counter.ts +18 -0
  50. package/src/histogram-group.ts +34 -0
  51. package/src/histogram.ts +26 -0
  52. package/src/index.ts +557 -0
  53. package/src/metric-group.ts +69 -0
  54. package/src/metric.ts +44 -0
  55. package/src/summary-group.ts +43 -0
  56. package/src/summary.ts +26 -0
  57. package/src/system-metrics.browser.ts +3 -0
  58. package/src/system-metrics.ts +504 -0
@@ -0,0 +1,26 @@
1
+ import type { Histogram, StopTimer } from '@libp2p/interface'
2
+ import type { Histogram as OTelHistogram } from '@opentelemetry/api'
3
+
4
+ export class OpenTelemetryHistogram implements Histogram {
5
+ private readonly histogram: OTelHistogram
6
+
7
+ constructor (histogram: OTelHistogram) {
8
+ this.histogram = histogram
9
+ }
10
+
11
+ observe (value: number): void {
12
+ this.histogram.record(value)
13
+ }
14
+
15
+ reset (): void {
16
+ this.histogram.record(0)
17
+ }
18
+
19
+ timer (): StopTimer {
20
+ const start = Date.now()
21
+
22
+ return () => {
23
+ this.observe(Date.now() - start)
24
+ }
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,557 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Uses [OpenTelemetry](https://opentelemetry.io/) to store metrics and method
5
+ * traces in libp2p.
6
+ *
7
+ * @example Node.js
8
+ *
9
+ * Use with [OpenTelemetry Desktop Viewer](https://github.com/CtrlSpice/otel-desktop-viewer):
10
+ *
11
+ * ```ts
12
+ * import { createLibp2p } from 'libp2p'
13
+ * import { openTelemetryMetrics } from '@libp2p/opentelemetry-metrics'
14
+ * import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
15
+ * import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
16
+ * import { NodeSDK } from '@opentelemetry/sdk-node'
17
+ *
18
+ * const sdk = new NodeSDK({
19
+ * traceExporter: new OTLPTraceExporter({
20
+ * url: 'http://127.0.0.1:4318/v1/traces'
21
+ * }),
22
+ * metricReader: new PrometheusExporter({
23
+ * port: 9464
24
+ * }),
25
+ * serviceName: 'my-app'
26
+ * })
27
+ * sdk.start()
28
+ *
29
+ * const node = await createLibp2p({
30
+ * // ... other options
31
+ * metrics: openTelemetryMetrics()
32
+ * })
33
+ * ```
34
+ */
35
+
36
+ import { InvalidParametersError, serviceCapabilities } from '@libp2p/interface'
37
+ import { trace, metrics, context, SpanStatusCode } from '@opentelemetry/api'
38
+ import each from 'it-foreach'
39
+ import { OpenTelemetryCounterGroup } from './counter-group.js'
40
+ import { OpenTelemetryCounter } from './counter.js'
41
+ import { OpenTelemetryHistogramGroup } from './histogram-group.js'
42
+ import { OpenTelemetryHistogram } from './histogram.js'
43
+ import { OpenTelemetryMetricGroup } from './metric-group.js'
44
+ import { OpenTelemetryMetric } from './metric.js'
45
+ import { OpenTelemetrySummaryGroup } from './summary-group.js'
46
+ import { OpenTelemetrySummary } from './summary.js'
47
+ import { collectSystemMetrics } from './system-metrics.js'
48
+ 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'
49
+ import type { Span, Attributes } from '@opentelemetry/api'
50
+ import type { Duplex } from 'it-stream-types'
51
+
52
+ // see https://betterstack.com/community/guides/observability/opentelemetry-metrics-nodejs/#prerequisites
53
+
54
+ export interface OpenTelemetryComponents {
55
+ nodeInfo: NodeInfo
56
+ }
57
+
58
+ export interface OpenTelemetryMetricsInit {
59
+ /**
60
+ * The app name used to create the tracer
61
+ *
62
+ * @default 'js-libp2p'
63
+ */
64
+ appName?: string
65
+
66
+ /**
67
+ * The app version used to create the tracer.
68
+ *
69
+ * The version number of the running version of libp2p is used as the default.
70
+ */
71
+ appVersion?: string
72
+
73
+ /**
74
+ * On Node.js platforms the current filesystem usage is reported as the metric
75
+ * `nodejs_fs_usage_bytes` using the `statfs` function from `node:fs` - the
76
+ * default location to stat is the current working directory, configured this
77
+ * location here
78
+ */
79
+ statfsLocation?: string
80
+
81
+ /**
82
+ * The meter name used for creating metrics
83
+ *
84
+ * @default 'js-libp2p'
85
+ */
86
+ meterName?: string
87
+ }
88
+
89
+ class OpenTelemetryMetrics implements Metrics {
90
+ private transferStats: Map<string, number>
91
+ private readonly tracer: ReturnType<typeof trace.getTracer>
92
+ private readonly meterName: string
93
+
94
+ constructor (components: OpenTelemetryComponents, init?: OpenTelemetryMetricsInit) {
95
+ this.tracer = trace.getTracer(init?.appName ?? components.nodeInfo.name, init?.appVersion ?? components.nodeInfo.version)
96
+
97
+ // holds global and per-protocol sent/received stats
98
+ this.transferStats = new Map()
99
+ this.meterName = init?.meterName ?? components.nodeInfo.name
100
+
101
+ this.registerCounterGroup('libp2p_data_transfer_bytes_total', {
102
+ label: 'protocol',
103
+ calculate: () => {
104
+ const output: Record<string, number> = {}
105
+
106
+ for (const [key, value] of this.transferStats.entries()) {
107
+ output[key] = value
108
+ }
109
+
110
+ // reset counts for next time
111
+ this.transferStats = new Map()
112
+
113
+ return output
114
+ }
115
+ })
116
+
117
+ collectSystemMetrics(this, init)
118
+ }
119
+
120
+ readonly [Symbol.toStringTag] = '@libp2p/metrics-opentelemetry'
121
+
122
+ readonly [serviceCapabilities]: string[] = [
123
+ '@libp2p/metrics'
124
+ ]
125
+
126
+ /**
127
+ * Increment the transfer stat for the passed key, making sure
128
+ * it exists first
129
+ */
130
+ _incrementValue (key: string, value: number): void {
131
+ const existing = this.transferStats.get(key) ?? 0
132
+
133
+ this.transferStats.set(key, existing + value)
134
+ }
135
+
136
+ /**
137
+ * Override the sink/source of the stream to count the bytes
138
+ * in and out
139
+ */
140
+ _track (stream: Duplex<AsyncGenerator<any>>, name: string): void {
141
+ const self = this
142
+
143
+ const sink = stream.sink
144
+ stream.sink = async function trackedSink (source) {
145
+ await sink(each(source, buf => {
146
+ self._incrementValue(`${name} sent`, buf.byteLength)
147
+ }))
148
+ }
149
+
150
+ const source = stream.source
151
+ stream.source = each(source, buf => {
152
+ self._incrementValue(`${name} received`, buf.byteLength)
153
+ })
154
+ }
155
+
156
+ trackMultiaddrConnection (maConn: MultiaddrConnection): void {
157
+ this._track(maConn, 'global')
158
+ }
159
+
160
+ trackProtocolStream (stream: Stream, connection: Connection): void {
161
+ if (stream.protocol == null) {
162
+ // protocol not negotiated yet, should not happen as the upgrader
163
+ // calls this handler after protocol negotiation
164
+ return
165
+ }
166
+
167
+ this._track(stream, stream.protocol)
168
+ }
169
+
170
+ registerMetric (name: string, opts: CalculatedMetricOptions): void
171
+ registerMetric (name: string, opts?: MetricOptions): Metric
172
+ registerMetric (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
173
+ if (name == null || name.trim() === '') {
174
+ throw new InvalidParametersError('Metric name is required')
175
+ }
176
+
177
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
178
+
179
+ if (isCalculatedMetricOptions<CalculatedMetricOptions>(opts)) {
180
+ const calculate = opts.calculate
181
+ const counter = meter.createObservableGauge(name, {
182
+ description: opts?.help ?? name
183
+ })
184
+ counter.addCallback(async (result) => {
185
+ result.observe(await calculate())
186
+ })
187
+
188
+ return
189
+ }
190
+
191
+ return new OpenTelemetryMetric(meter.createGauge(name, {
192
+ description: opts?.help ?? name
193
+ }))
194
+ }
195
+
196
+ registerMetricGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
197
+ registerMetricGroup (name: string, opts?: MetricOptions): MetricGroup
198
+ registerMetricGroup (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
199
+ if (name == null || name.trim() === '') {
200
+ throw new InvalidParametersError('Metric name is required')
201
+ }
202
+
203
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
204
+ const label = opts?.label ?? name
205
+
206
+ if (isCalculatedMetricOptions<CalculatedMetricOptions<Record<string, number>>>(opts)) {
207
+ const calculate = opts.calculate
208
+ const gauge = meter.createObservableGauge(name, {
209
+ description: opts?.help ?? name
210
+ })
211
+ gauge.addCallback(async (observable) => {
212
+ const observed = await calculate()
213
+
214
+ for (const [key, value] of Object.entries(observed)) {
215
+ observable.observe(value, {
216
+ [label]: key
217
+ })
218
+ }
219
+ })
220
+
221
+ return
222
+ }
223
+
224
+ return new OpenTelemetryMetricGroup(label, meter.createGauge(name, {
225
+ description: opts?.help ?? name
226
+ }))
227
+ }
228
+
229
+ registerCounter (name: string, opts: CalculatedMetricOptions): void
230
+ registerCounter (name: string, opts?: MetricOptions): Counter
231
+ registerCounter (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
232
+ if (name == null || name.trim() === '') {
233
+ throw new InvalidParametersError('Metric name is required')
234
+ }
235
+
236
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
237
+
238
+ if (isCalculatedMetricOptions<CalculatedMetricOptions>(opts)) {
239
+ const calculate = opts.calculate
240
+ const counter = meter.createObservableCounter(name, {
241
+ description: opts?.help ?? name
242
+ })
243
+ counter.addCallback(async (result) => {
244
+ result.observe(await calculate())
245
+ })
246
+
247
+ return
248
+ }
249
+
250
+ return new OpenTelemetryCounter(meter.createCounter(name, {
251
+ description: opts?.help ?? name
252
+ }))
253
+ }
254
+
255
+ registerCounterGroup (name: string, opts: CalculatedMetricOptions<Record<string, number>>): void
256
+ registerCounterGroup (name: string, opts?: MetricOptions): CounterGroup
257
+ registerCounterGroup (name: string, opts: CalculatedMetricOptions | MetricOptions = {}): any {
258
+ if (name == null || name.trim() === '') {
259
+ throw new InvalidParametersError('Metric name is required')
260
+ }
261
+
262
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
263
+ const label = opts?.label ?? name
264
+
265
+ if (isCalculatedMetricOptions<CalculatedMetricOptions<Record<string, number>>>(opts)) {
266
+ const values: Record<string, number> = {}
267
+ const calculate = opts.calculate
268
+ const counter = meter.createObservableGauge(name, {
269
+ description: opts?.help ?? name
270
+ })
271
+ counter.addCallback(async (observable) => {
272
+ const observed = await calculate()
273
+
274
+ for (const [key, value] of Object.entries(observed)) {
275
+ if (values[key] == null) {
276
+ values[key] = 0
277
+ }
278
+
279
+ values[key] += value
280
+
281
+ observable.observe(values[key], {
282
+ [label]: key
283
+ })
284
+ }
285
+ })
286
+
287
+ return
288
+ }
289
+
290
+ return new OpenTelemetryCounterGroup(label, meter.createCounter(name, {
291
+ description: opts?.help ?? name
292
+ }))
293
+ }
294
+
295
+ registerHistogram (name: string, opts: CalculatedHistogramOptions): void
296
+ registerHistogram (name: string, opts?: HistogramOptions): Histogram
297
+ registerHistogram (name: string, opts: CalculatedHistogramOptions | HistogramOptions = {}): any {
298
+ if (name == null || name.trim() === '') {
299
+ throw new InvalidParametersError('Metric name is required')
300
+ }
301
+
302
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
303
+
304
+ if (isCalculatedMetricOptions<CalculatedHistogramOptions>(opts)) {
305
+ return
306
+ }
307
+
308
+ return new OpenTelemetryHistogram(meter.createHistogram(name, {
309
+ advice: {
310
+ explicitBucketBoundaries: opts.buckets
311
+ },
312
+ description: opts?.help ?? name
313
+ }))
314
+ }
315
+
316
+ registerHistogramGroup (name: string, opts: CalculatedHistogramOptions<Record<string, number>>): void
317
+ registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup
318
+ registerHistogramGroup (name: string, opts: CalculatedHistogramOptions | HistogramOptions = {}): any {
319
+ if (name == null || name.trim() === '') {
320
+ throw new InvalidParametersError('Metric name is required')
321
+ }
322
+
323
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
324
+ const label = opts?.label ?? name
325
+
326
+ if (isCalculatedMetricOptions<CalculatedHistogramOptions<Record<string, number>>>(opts)) {
327
+ return
328
+ }
329
+
330
+ return new OpenTelemetryHistogramGroup(label, meter.createHistogram(name, {
331
+ advice: {
332
+ explicitBucketBoundaries: opts.buckets
333
+ },
334
+ description: opts?.help ?? name
335
+ }))
336
+ }
337
+
338
+ registerSummary (name: string, opts: CalculatedSummaryOptions): void
339
+ registerSummary (name: string, opts?: SummaryOptions): Summary
340
+ registerSummary (name: string, opts: CalculatedSummaryOptions | SummaryOptions = {}): any {
341
+ if (name == null || name.trim() === '') {
342
+ throw new InvalidParametersError('Metric name is required')
343
+ }
344
+
345
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
346
+
347
+ if (isCalculatedMetricOptions<CalculatedHistogramOptions>(opts)) {
348
+ return
349
+ }
350
+
351
+ return new OpenTelemetrySummary(meter.createGauge(name, {
352
+ description: opts?.help ?? name
353
+ }))
354
+ }
355
+
356
+ registerSummaryGroup (name: string, opts: CalculatedSummaryOptions<Record<string, number>>): void
357
+ registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup
358
+ registerSummaryGroup (name: string, opts: CalculatedSummaryOptions | SummaryOptions = {}): any {
359
+ if (name == null || name.trim() === '') {
360
+ throw new InvalidParametersError('Metric name is required')
361
+ }
362
+
363
+ const meter = metrics.getMeterProvider().getMeter(this.meterName)
364
+ const label = opts?.label ?? name
365
+
366
+ if (isCalculatedMetricOptions<CalculatedSummaryOptions>(opts)) {
367
+ return
368
+ }
369
+
370
+ return new OpenTelemetrySummaryGroup(label, meter.createGauge(name, {
371
+ description: opts?.help ?? name
372
+ }))
373
+ }
374
+
375
+ createTrace (): any {
376
+ return context.active()
377
+ }
378
+
379
+ traceFunction <F extends (...args: any[]) => any> (name: string, fn: F, options?: TraceFunctionOptions<Parameters<F>, ReturnType<F>>): F {
380
+ // @ts-expect-error returned function could be different to T
381
+ return (...args: Parameters<F>): any => {
382
+ const optionsIndex = options?.optionsIndex ?? 0
383
+ // make sure we have an options object
384
+ const opts = {
385
+ ...(args[optionsIndex] ?? {})
386
+ }
387
+ args[optionsIndex] = opts
388
+
389
+ // skip tracing if no context is passed
390
+ if (opts.trace == null) {
391
+ return fn.apply(null, args)
392
+ }
393
+
394
+ const attributes = {}
395
+
396
+ // extract the parent context from the options object
397
+ const parentContext = opts.trace
398
+ const span = this.tracer.startSpan(name, {
399
+ attributes: options?.getAttributesFromArgs?.(args, attributes)
400
+ }, parentContext)
401
+
402
+ const childContext = trace.setSpan(parentContext, span)
403
+ opts.trace = childContext
404
+ let result: any
405
+
406
+ try {
407
+ result = context.with(childContext, fn, undefined, ...args)
408
+ } catch (err: any) {
409
+ span.recordException(err)
410
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
411
+ span.end()
412
+ throw err
413
+ }
414
+
415
+ if (isPromise(result)) {
416
+ return wrapPromise(result, span, attributes, options)
417
+ }
418
+
419
+ if (isGenerator(result)) {
420
+ return wrapGenerator(result, span, attributes, options)
421
+ }
422
+
423
+ if (isAsyncGenerator(result)) {
424
+ return wrapAsyncGenerator(result, span, attributes, options)
425
+ }
426
+
427
+ setAttributes(span, options?.getAttributesFromReturnValue?.(result, attributes))
428
+
429
+ span.setStatus({ code: SpanStatusCode.OK })
430
+ span.end()
431
+
432
+ return result
433
+ }
434
+ }
435
+ }
436
+
437
+ export function openTelemetryMetrics (init: OpenTelemetryMetricsInit = {}): (components: OpenTelemetryComponents) => Metrics {
438
+ return (components: OpenTelemetryComponents) => new OpenTelemetryMetrics(components, init)
439
+ }
440
+
441
+ function isPromise <T = any> (obj?: any): obj is Promise<T> {
442
+ return typeof obj?.then === 'function'
443
+ }
444
+
445
+ async function wrapPromise (promise: Promise<any>, span: Span, attributes: TraceAttributes, options?: TraceFunctionOptions<any, any>): Promise<any> {
446
+ return promise
447
+ .then(res => {
448
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res, attributes))
449
+ span.setStatus({ code: SpanStatusCode.OK })
450
+ return res
451
+ })
452
+ .catch(err => {
453
+ span.recordException(err)
454
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
455
+ })
456
+ .finally(() => {
457
+ span.end()
458
+ })
459
+ }
460
+
461
+ function isGenerator (obj?: any): obj is Generator {
462
+ return obj?.[Symbol.iterator] != null
463
+ }
464
+
465
+ function wrapGenerator (gen: Generator, span: Span, attributes: TraceAttributes, options?: TraceGeneratorFunctionOptions<any, any, any>): Generator {
466
+ const iter = gen[Symbol.iterator]()
467
+ let index = 0
468
+
469
+ const wrapped: Generator = {
470
+ next: () => {
471
+ try {
472
+ const res = iter.next()
473
+
474
+ if (res.done === true) {
475
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res.value, attributes))
476
+ span.setStatus({ code: SpanStatusCode.OK })
477
+ span.end()
478
+ } else {
479
+ setAttributes(span, options?.getAttributesFromYieldedValue?.(res.value, attributes, ++index))
480
+ }
481
+
482
+ return res
483
+ } catch (err: any) {
484
+ span.recordException(err)
485
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
486
+ span.end()
487
+
488
+ throw err
489
+ }
490
+ },
491
+ return: (value) => {
492
+ return iter.return(value)
493
+ },
494
+ throw: (err) => {
495
+ return iter.throw(err)
496
+ },
497
+ [Symbol.iterator]: () => {
498
+ return wrapped
499
+ }
500
+ }
501
+
502
+ return wrapped
503
+ }
504
+
505
+ function isAsyncGenerator (obj?: any): obj is AsyncGenerator {
506
+ return obj?.[Symbol.asyncIterator] != null
507
+ }
508
+
509
+ function wrapAsyncGenerator (gen: AsyncGenerator, span: Span, attributes: TraceAttributes, options?: TraceGeneratorFunctionOptions<any, any, any>): AsyncGenerator {
510
+ const iter = gen[Symbol.asyncIterator]()
511
+ let index = 0
512
+
513
+ const wrapped: AsyncGenerator = {
514
+ next: async () => {
515
+ try {
516
+ const res = await iter.next()
517
+
518
+ if (res.done === true) {
519
+ setAttributes(span, options?.getAttributesFromReturnValue?.(res.value, attributes))
520
+ span.setStatus({ code: SpanStatusCode.OK })
521
+ span.end()
522
+ } else {
523
+ setAttributes(span, options?.getAttributesFromYieldedValue?.(res.value, attributes, ++index))
524
+ }
525
+
526
+ return res
527
+ } catch (err: any) {
528
+ span.recordException(err)
529
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.toString() })
530
+ span.end()
531
+
532
+ throw err
533
+ }
534
+ },
535
+ return: async (value) => {
536
+ return iter.return(value)
537
+ },
538
+ throw: async (err) => {
539
+ return iter.throw(err)
540
+ },
541
+ [Symbol.asyncIterator]: () => {
542
+ return wrapped
543
+ }
544
+ }
545
+
546
+ return wrapped
547
+ }
548
+
549
+ function isCalculatedMetricOptions <T> (opts?: any): opts is T {
550
+ return opts?.calculate != null
551
+ }
552
+
553
+ function setAttributes (span: Span, attributes?: Attributes): void {
554
+ if (attributes != null) {
555
+ span.setAttributes(attributes)
556
+ }
557
+ }
@@ -0,0 +1,69 @@
1
+ import type { MetricGroup, StopTimer } from '@libp2p/interface'
2
+ import type { Gauge } from '@opentelemetry/api'
3
+
4
+ export class OpenTelemetryMetricGroup implements MetricGroup {
5
+ private readonly label: string
6
+ private readonly gauge: Gauge
7
+ private readonly lastValues: Record<string, number>
8
+
9
+ constructor (label: string, gauge: Gauge) {
10
+ this.label = label
11
+ this.gauge = gauge
12
+ this.lastValues = {}
13
+ }
14
+
15
+ update (values: Record<string, number>): void {
16
+ Object.entries(values).forEach(([key, value]) => {
17
+ this.lastValues[key] = value
18
+ this.gauge.record(value, {
19
+ [this.label]: key
20
+ })
21
+ })
22
+ }
23
+
24
+ increment (values: Record<string, number | true>): void {
25
+ Object.entries(values).forEach(([key, value]) => {
26
+ if (this.lastValues[key] == null) {
27
+ this.lastValues[key] = 0
28
+ }
29
+
30
+ this.lastValues[key] += value === true ? 1 : value
31
+ this.gauge.record(this.lastValues[key], {
32
+ [this.label]: key
33
+ })
34
+ })
35
+ }
36
+
37
+ decrement (values: Record<string, number | true>): void {
38
+ Object.entries(values).forEach(([key, value]) => {
39
+ if (this.lastValues[key] == null) {
40
+ this.lastValues[key] = 0
41
+ }
42
+
43
+ this.lastValues[key] -= value === true ? 1 : value
44
+ this.gauge.record(this.lastValues[key], {
45
+ [this.label]: key
46
+ })
47
+ })
48
+ }
49
+
50
+ reset (): void {
51
+ Object.keys(this.lastValues).forEach(key => {
52
+ this.lastValues[key] = 0
53
+ this.gauge.record(0, {
54
+ [this.label]: key
55
+ })
56
+ })
57
+ }
58
+
59
+ timer (key: string): StopTimer {
60
+ const start = Date.now()
61
+
62
+ return () => {
63
+ this.lastValues[key] = Date.now() - start
64
+ this.gauge.record(this.lastValues[key], {
65
+ [this.label]: key
66
+ })
67
+ }
68
+ }
69
+ }
package/src/metric.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { Metric, StopTimer } from '@libp2p/interface'
2
+ import type { Gauge } from '@opentelemetry/api'
3
+
4
+ export class OpenTelemetryMetric implements Metric {
5
+ private readonly gauge: Gauge
6
+ private lastValue: number
7
+
8
+ constructor (gauge: Gauge) {
9
+ this.gauge = gauge
10
+ this.lastValue = 0
11
+ this.update(0)
12
+ }
13
+
14
+ update (value: number): void {
15
+ this.lastValue = value
16
+ this.gauge.record(value, {
17
+ attrName: 'attrValue'
18
+ })
19
+ }
20
+
21
+ increment (value: number = 1): void {
22
+ this.lastValue += value
23
+ this.gauge.record(this.lastValue)
24
+ }
25
+
26
+ decrement (value: number = 1): void {
27
+ this.lastValue -= value
28
+ this.gauge.record(this.lastValue)
29
+ }
30
+
31
+ reset (): void {
32
+ this.gauge.record(0)
33
+ this.lastValue = 0
34
+ }
35
+
36
+ timer (): StopTimer {
37
+ const start = Date.now()
38
+
39
+ return () => {
40
+ this.lastValue = Date.now() - start
41
+ this.gauge.record(this.lastValue)
42
+ }
43
+ }
44
+ }