Skip to content

Commit 21fe841

Browse files
authored
feat: add histogram and summary metric types (#2705)
- Add `Histogram`, `HistogramGroup`, `Summary`, `SummaryGroup` metric types - Add `registerHistogram`, `registerHistogramGroup`, `registerSummary`, `registerSummaryGroup` methods to `Metrics` interface - Add corresponding implementations to prometheus and simple metrics packages
1 parent 67587a2 commit 21fe841

File tree

15 files changed

+1452
-5
lines changed

15 files changed

+1452
-5
lines changed

packages/interface-compliance-tests/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,12 @@
138138
"p-wait-for": "^5.0.2",
139139
"protons-runtime": "^5.4.0",
140140
"sinon": "^18.0.0",
141+
"tdigest": "^0.1.2",
141142
"uint8arraylist": "^2.4.8",
142143
"uint8arrays": "^5.1.0"
143144
},
144145
"devDependencies": {
146+
"@types/tdigest": "^0.1.4",
145147
"protons": "^7.5.0"
146148
}
147149
}

packages/interface-compliance-tests/src/mocks/metrics.ts

Lines changed: 225 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions } from '@libp2p/interface'
1+
import { TDigest } from 'tdigest'
2+
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions } from '@libp2p/interface'
23

34
class DefaultMetric implements Metric {
45
public value: number = 0
@@ -68,6 +69,153 @@ class DefaultGroupMetric implements MetricGroup {
6869
}
6970
}
7071

72+
class DefaultHistogram implements Histogram {
73+
public bucketValues = new Map<number, number>()
74+
public countValue: number = 0
75+
public sumValue: number = 0
76+
77+
constructor (opts: HistogramOptions) {
78+
const buckets = [
79+
...(opts.buckets ?? [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]),
80+
Infinity
81+
]
82+
for (const bucket of buckets) {
83+
this.bucketValues.set(bucket, 0)
84+
}
85+
}
86+
87+
observe (value: number): void {
88+
this.countValue++
89+
this.sumValue += value
90+
91+
for (const [bucket, count] of this.bucketValues.entries()) {
92+
if (value <= bucket) {
93+
this.bucketValues.set(bucket, count + 1)
94+
}
95+
}
96+
}
97+
98+
reset (): void {
99+
this.countValue = 0
100+
this.sumValue = 0
101+
for (const bucket of this.bucketValues.keys()) {
102+
this.bucketValues.set(bucket, 0)
103+
}
104+
}
105+
106+
timer (): StopTimer {
107+
const start = Date.now()
108+
109+
return () => {
110+
this.observe(Date.now() - start)
111+
}
112+
}
113+
}
114+
115+
class DefaultHistogramGroup implements HistogramGroup {
116+
public histograms: Record<string, DefaultHistogram> = {}
117+
118+
constructor (opts: HistogramOptions) {
119+
this.histograms = {}
120+
}
121+
122+
observe (values: Partial<Record<string, number>>): void {
123+
for (const [key, value] of Object.entries(values) as Array<[string, number]>) {
124+
if (this.histograms[key] === undefined) {
125+
this.histograms[key] = new DefaultHistogram({})
126+
}
127+
128+
this.histograms[key].observe(value)
129+
}
130+
}
131+
132+
reset (): void {
133+
for (const histogram of Object.values(this.histograms)) {
134+
histogram.reset()
135+
}
136+
}
137+
138+
timer (key: string): StopTimer {
139+
const start = Date.now()
140+
141+
return () => {
142+
this.observe({ [key]: Date.now() - start })
143+
}
144+
}
145+
}
146+
147+
class DefaultSummary implements Summary {
148+
public sumValue: number = 0
149+
public countValue: number = 0
150+
public percentiles: number[]
151+
public tdigest = new TDigest(0.01)
152+
private readonly compressCount: number
153+
154+
constructor (opts: SummaryOptions) {
155+
this.percentiles = opts.percentiles ?? [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999]
156+
this.compressCount = opts.compressCount ?? 1000
157+
}
158+
159+
observe (value: number): void {
160+
this.sumValue += value
161+
this.countValue++
162+
163+
this.tdigest.push(value)
164+
if (this.tdigest.size() > this.compressCount) {
165+
this.tdigest.compress()
166+
}
167+
}
168+
169+
reset (): void {
170+
this.sumValue = 0
171+
this.countValue = 0
172+
173+
this.tdigest.reset()
174+
}
175+
176+
timer (): StopTimer {
177+
const start = Date.now()
178+
179+
return () => {
180+
this.observe(Date.now() - start)
181+
}
182+
}
183+
}
184+
185+
class DefaultSummaryGroup implements SummaryGroup {
186+
public summaries: Record<string, DefaultSummary> = {}
187+
private readonly opts: SummaryOptions
188+
189+
constructor (opts: SummaryOptions) {
190+
this.summaries = {}
191+
this.opts = opts
192+
}
193+
194+
observe (values: Record<string, number>): void {
195+
for (const [key, value] of Object.entries(values)) {
196+
if (this.summaries[key] === undefined) {
197+
this.summaries[key] = new DefaultSummary(this.opts)
198+
}
199+
200+
this.summaries[key].observe(value)
201+
}
202+
}
203+
204+
reset (): void {
205+
for (const summary of Object.values(this.summaries)) {
206+
summary.reset()
207+
}
208+
}
209+
210+
timer (key: string): StopTimer {
211+
const start = Date.now()
212+
213+
return () => {
214+
this.observe({ [key]: Date.now() - start })
215+
}
216+
}
217+
}
218+
71219
class MockMetrics implements Metrics {
72220
public metrics = new Map<string, any>()
73221

@@ -154,6 +302,82 @@ class MockMetrics implements Metrics {
154302

155303
return metric
156304
}
305+
306+
registerHistogram (name: string, opts: CalculatedHistogramOptions): void
307+
registerHistogram (name: string, opts?: HistogramOptions): Histogram
308+
registerHistogram (name: string, opts: any = {}): any {
309+
if (name == null || name.trim() === '') {
310+
throw new Error('Metric name is required')
311+
}
312+
313+
if (opts?.calculate != null) {
314+
// calculated metric
315+
this.metrics.set(name, opts.calculate)
316+
return
317+
}
318+
319+
const metric = new DefaultHistogram(opts)
320+
this.metrics.set(name, metric)
321+
322+
return metric
323+
}
324+
325+
registerHistogramGroup (name: string, opts: CalculatedHistogramOptions<Record<string, number>>): void
326+
registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup
327+
registerHistogramGroup (name: string, opts: any = {}): any {
328+
if (name == null || name.trim() === '') {
329+
throw new Error('Metric name is required')
330+
}
331+
332+
if (opts?.calculate != null) {
333+
// calculated metric
334+
this.metrics.set(name, opts.calculate)
335+
return
336+
}
337+
338+
const metric = new DefaultHistogramGroup(opts)
339+
this.metrics.set(name, metric)
340+
341+
return metric
342+
}
343+
344+
registerSummary (name: string, opts: CalculatedSummaryOptions): void
345+
registerSummary (name: string, opts?: SummaryOptions): Summary
346+
registerSummary (name: string, opts: any = {}): any {
347+
if (name == null || name.trim() === '') {
348+
throw new Error('Metric name is required')
349+
}
350+
351+
if (opts?.calculate != null) {
352+
// calculated metric
353+
this.metrics.set(name, opts.calculate)
354+
return
355+
}
356+
357+
const metric = new DefaultSummary(opts)
358+
this.metrics.set(name, metric)
359+
360+
return metric
361+
}
362+
363+
registerSummaryGroup (name: string, opts: CalculatedSummaryOptions<Record<string, number>>): void
364+
registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup
365+
registerSummaryGroup (name: string, opts: any = {}): any {
366+
if (name == null || name.trim() === '') {
367+
throw new Error('Metric name is required')
368+
}
369+
370+
if (opts?.calculate != null) {
371+
// calculated metric
372+
this.metrics.set(name, opts.calculate)
373+
return
374+
}
375+
376+
const metric = new DefaultSummaryGroup(opts)
377+
this.metrics.set(name, metric)
378+
379+
return metric
380+
}
157381
}
158382

159383
export function mockMetrics (): () => Metrics {

0 commit comments

Comments
 (0)