-
Notifications
You must be signed in to change notification settings - Fork 439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add histogram and summary metric types #2705
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions } from '@libp2p/interface' | ||
import { TDigest } from 'tdigest' | ||
import type { MultiaddrConnection, Stream, Connection, Metric, MetricGroup, StopTimer, Metrics, CalculatedMetricOptions, MetricOptions, Histogram, HistogramOptions, HistogramGroup, Summary, SummaryOptions, SummaryGroup, CalculatedHistogramOptions, CalculatedSummaryOptions } from '@libp2p/interface' | ||
|
||
class DefaultMetric implements Metric { | ||
public value: number = 0 | ||
|
@@ -68,6 +69,153 @@ class DefaultGroupMetric implements MetricGroup { | |
} | ||
} | ||
|
||
class DefaultHistogram implements Histogram { | ||
public bucketValues = new Map<number, number>() | ||
public countValue: number = 0 | ||
public sumValue: number = 0 | ||
|
||
constructor (opts: HistogramOptions) { | ||
const buckets = [ | ||
...(opts.buckets ?? [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]), | ||
Infinity | ||
] | ||
for (const bucket of buckets) { | ||
this.bucketValues.set(bucket, 0) | ||
} | ||
} | ||
|
||
observe (value: number): void { | ||
this.countValue++ | ||
this.sumValue += value | ||
|
||
for (const [bucket, count] of this.bucketValues.entries()) { | ||
if (value <= bucket) { | ||
this.bucketValues.set(bucket, count + 1) | ||
} | ||
} | ||
} | ||
|
||
reset (): void { | ||
this.countValue = 0 | ||
this.sumValue = 0 | ||
for (const bucket of this.bucketValues.keys()) { | ||
this.bucketValues.set(bucket, 0) | ||
} | ||
} | ||
|
||
timer (): StopTimer { | ||
const start = Date.now() | ||
|
||
return () => { | ||
this.observe(Date.now() - start) | ||
} | ||
} | ||
} | ||
|
||
class DefaultHistogramGroup implements HistogramGroup { | ||
public histograms: Record<string, DefaultHistogram> = {} | ||
|
||
constructor (opts: HistogramOptions) { | ||
this.histograms = {} | ||
} | ||
|
||
observe (values: Partial<Record<string, number>>): void { | ||
for (const [key, value] of Object.entries(values) as Array<[string, number]>) { | ||
if (this.histograms[key] === undefined) { | ||
this.histograms[key] = new DefaultHistogram({}) | ||
} | ||
|
||
this.histograms[key].observe(value) | ||
} | ||
} | ||
|
||
reset (): void { | ||
for (const histogram of Object.values(this.histograms)) { | ||
histogram.reset() | ||
} | ||
} | ||
|
||
timer (key: string): StopTimer { | ||
const start = Date.now() | ||
|
||
return () => { | ||
this.observe({ [key]: Date.now() - start }) | ||
} | ||
} | ||
} | ||
|
||
class DefaultSummary implements Summary { | ||
public sumValue: number = 0 | ||
public countValue: number = 0 | ||
public percentiles: number[] | ||
public tdigest = new TDigest(0.01) | ||
private readonly compressCount: number | ||
|
||
constructor (opts: SummaryOptions) { | ||
this.percentiles = opts.percentiles ?? [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999] | ||
this.compressCount = opts.compressCount ?? 1000 | ||
} | ||
|
||
observe (value: number): void { | ||
this.sumValue += value | ||
this.countValue++ | ||
|
||
this.tdigest.push(value) | ||
if (this.tdigest.size() > this.compressCount) { | ||
this.tdigest.compress() | ||
} | ||
} | ||
|
||
reset (): void { | ||
this.sumValue = 0 | ||
this.countValue = 0 | ||
|
||
this.tdigest.reset() | ||
} | ||
|
||
timer (): StopTimer { | ||
const start = Date.now() | ||
|
||
return () => { | ||
this.observe(Date.now() - start) | ||
} | ||
} | ||
} | ||
|
||
class DefaultSummaryGroup implements SummaryGroup { | ||
public summaries: Record<string, DefaultSummary> = {} | ||
private readonly opts: SummaryOptions | ||
|
||
constructor (opts: SummaryOptions) { | ||
this.summaries = {} | ||
this.opts = opts | ||
} | ||
|
||
observe (values: Record<string, number>): void { | ||
for (const [key, value] of Object.entries(values)) { | ||
if (this.summaries[key] === undefined) { | ||
this.summaries[key] = new DefaultSummary(this.opts) | ||
} | ||
|
||
this.summaries[key].observe(value) | ||
} | ||
} | ||
|
||
reset (): void { | ||
for (const summary of Object.values(this.summaries)) { | ||
summary.reset() | ||
} | ||
} | ||
|
||
timer (key: string): StopTimer { | ||
const start = Date.now() | ||
|
||
return () => { | ||
this.observe({ [key]: Date.now() - start }) | ||
} | ||
} | ||
} | ||
|
||
class MockMetrics implements Metrics { | ||
public metrics = new Map<string, any>() | ||
|
||
|
@@ -154,6 +302,82 @@ class MockMetrics implements Metrics { | |
|
||
return metric | ||
} | ||
|
||
registerHistogram (name: string, opts: CalculatedHistogramOptions): void | ||
registerHistogram (name: string, opts?: HistogramOptions): Histogram | ||
registerHistogram (name: string, opts: any = {}): any { | ||
if (name == null || name.trim() === '') { | ||
throw new Error('Metric name is required') | ||
} | ||
|
||
if (opts?.calculate != null) { | ||
// calculated metric | ||
this.metrics.set(name, opts.calculate) | ||
return | ||
} | ||
|
||
const metric = new DefaultHistogram(opts) | ||
this.metrics.set(name, metric) | ||
|
||
return metric | ||
} | ||
|
||
registerHistogramGroup (name: string, opts: CalculatedHistogramOptions<Record<string, number>>): void | ||
registerHistogramGroup (name: string, opts?: HistogramOptions): HistogramGroup | ||
registerHistogramGroup (name: string, opts: any = {}): any { | ||
if (name == null || name.trim() === '') { | ||
throw new Error('Metric name is required') | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
|
||
if (opts?.calculate != null) { | ||
// calculated metric | ||
this.metrics.set(name, opts.calculate) | ||
return | ||
} | ||
|
||
const metric = new DefaultHistogramGroup(opts) | ||
this.metrics.set(name, metric) | ||
|
||
return metric | ||
} | ||
|
||
registerSummary (name: string, opts: CalculatedSummaryOptions): void | ||
registerSummary (name: string, opts?: SummaryOptions): Summary | ||
registerSummary (name: string, opts: any = {}): any { | ||
if (name == null || name.trim() === '') { | ||
throw new Error('Metric name is required') | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
|
||
if (opts?.calculate != null) { | ||
// calculated metric | ||
this.metrics.set(name, opts.calculate) | ||
return | ||
} | ||
|
||
const metric = new DefaultSummary(opts) | ||
this.metrics.set(name, metric) | ||
|
||
return metric | ||
} | ||
|
||
registerSummaryGroup (name: string, opts: CalculatedSummaryOptions<Record<string, number>>): void | ||
registerSummaryGroup (name: string, opts?: SummaryOptions): SummaryGroup | ||
registerSummaryGroup (name: string, opts: any = {}): any { | ||
if (name == null || name.trim() === '') { | ||
throw new Error('Metric name is required') | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
|
||
if (opts?.calculate != null) { | ||
// calculated metric | ||
this.metrics.set(name, opts.calculate) | ||
return | ||
} | ||
|
||
const metric = new DefaultSummaryGroup(opts) | ||
this.metrics.set(name, metric) | ||
|
||
return metric | ||
} | ||
} | ||
|
||
export function mockMetrics (): () => Metrics { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use
InvalidParametersError
for invalid parameters