fix(metrics): address PR #11 code review feedback
fix(metrics): address PR #11 code review feedback
This commit is contained in:
Generated
+38
@@ -18,6 +18,7 @@
|
||||
"music-metadata": "^11.7.0",
|
||||
"p-queue": "^9.0.0",
|
||||
"pino": "^9.6",
|
||||
"prom-client": "^15.1.3",
|
||||
"protobufjs": "^7.2.4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
@@ -3115,6 +3116,15 @@
|
||||
"@octokit/openapi-types": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
@@ -4726,6 +4736,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
|
||||
@@ -11821,6 +11837,19 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "15.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz",
|
||||
"integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/promise.allsettled": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz",
|
||||
@@ -13651,6 +13680,15 @@
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"music-metadata": "^11.7.0",
|
||||
"p-queue": "^9.0.0",
|
||||
"pino": "^9.6",
|
||||
"prom-client": "^15.1.3",
|
||||
"protobufjs": "^7.2.4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
|
||||
@@ -82,8 +82,13 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||
// Circuit breaker configuration
|
||||
enableCircuitBreaker: true,
|
||||
// Listener limits (memory leak prevention)
|
||||
// 8 base WS events + 10 dynamic listeners + 2 buffer slots
|
||||
// WebSocket: 8 core events (open, close, error, message, ping, pong, upgrade, unexpected-response)
|
||||
// + 10 dynamic listeners (reconnect handlers, custom events)
|
||||
// + 2 buffer slots for temporary listeners = 20 total
|
||||
maxWebSocketListeners: 20,
|
||||
// SocketClient: 20 core events (connection, messaging, presence, groups, calls, etc.)
|
||||
// + 20 dynamic listeners (user handlers, plugins)
|
||||
// + 10 buffer slots for high-load scenarios = 50 total
|
||||
maxSocketClientListeners: 50
|
||||
}
|
||||
|
||||
@@ -165,8 +170,9 @@ export const DEFAULT_CACHE_MAX_KEYS = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry configuration with exponential backoff
|
||||
* Delays in milliseconds: 1s, 2s, 5s, 10s, 20s
|
||||
* Retry configuration with custom progressive backoff
|
||||
* Fixed delay steps in milliseconds: 1s → 2s → 5s → 10s → 20s
|
||||
* Use with 'stepped' backoff strategy in retry-utils.ts
|
||||
*/
|
||||
export const RETRY_BACKOFF_DELAYS = [1000, 2000, 5000, 10000, 20000]
|
||||
|
||||
|
||||
+2
-1
@@ -170,13 +170,14 @@ export type SocketConfig = {
|
||||
|
||||
/**
|
||||
* Max WebSocket event listeners (default: 20)
|
||||
* Calculated as: 8 base WS events + 10 dynamic listeners + 2 buffer slots
|
||||
* Calculation: 8 core WS events + 10 dynamic listeners + 2 buffer slots
|
||||
* WARNING: Setting to 0 disables limit and allows potential memory leaks!
|
||||
*/
|
||||
maxWebSocketListeners?: number
|
||||
|
||||
/**
|
||||
* Max SocketClient event listeners (default: 50)
|
||||
* Calculation: 20 core events + 20 dynamic listeners + 10 buffer slots
|
||||
* WARNING: Setting to 0 disables limit and allows potential memory leaks!
|
||||
*/
|
||||
maxSocketClientListeners?: number
|
||||
|
||||
+256
-201
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Prometheus Metrics Exposition - Enterprise Grade
|
||||
*
|
||||
* Uses prom-client for full Prometheus compatibility including:
|
||||
* - Native OpenMetrics format export
|
||||
* - Default Node.js metrics collection
|
||||
* - Proper histogram buckets
|
||||
* - Registry management
|
||||
*
|
||||
* Provides:
|
||||
* - Counters for event counting
|
||||
* - Gauges for instantaneous values
|
||||
@@ -27,6 +33,10 @@
|
||||
|
||||
import { createServer, IncomingMessage, ServerResponse, Server } from 'http'
|
||||
import * as os from 'os'
|
||||
import * as promClient from 'prom-client'
|
||||
|
||||
// Create a custom registry to avoid conflicts with global registry
|
||||
const customRegistry = new promClient.Registry()
|
||||
|
||||
// ============================================
|
||||
// Configuration
|
||||
@@ -157,24 +167,32 @@ export interface SummaryValue {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Counter Class
|
||||
// Counter Class (prom-client backed)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Counter class - monotonically increasing metric
|
||||
* Now backed by prom-client for full Prometheus compatibility
|
||||
*
|
||||
* Counters only go up (or reset to zero). They are used for
|
||||
* counting events like requests, errors, tasks completed, etc.
|
||||
*/
|
||||
export class Counter implements BaseMetric {
|
||||
readonly type = 'counter' as const
|
||||
private values: Map<string, MetricValue> = new Map()
|
||||
private promCounter: promClient.Counter<string>
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public help: string,
|
||||
public labelNames: string[] = []
|
||||
) {}
|
||||
) {
|
||||
this.promCounter = new promClient.Counter({
|
||||
name,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [customRegistry], // Use custom registry, not global
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the counter
|
||||
@@ -194,46 +212,48 @@ export class Counter implements BaseMetric {
|
||||
throw new Error('Counter cannot be decremented')
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
const existing = this.values.get(key)
|
||||
|
||||
if (existing) {
|
||||
existing.value += incValue
|
||||
existing.timestamp = Date.now()
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promCounter.labels(labels).inc(incValue)
|
||||
} else {
|
||||
this.values.set(key, {
|
||||
labels,
|
||||
value: incValue,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
this.promCounter.inc(incValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current value
|
||||
* Get current value (async, from prom-client)
|
||||
*/
|
||||
get(labels: Labels = {}): number {
|
||||
async get(labels: Labels = {}): Promise<number> {
|
||||
const metric = await this.promCounter.get()
|
||||
const key = this.labelsToKey(labels)
|
||||
return this.values.get(key)?.value ?? 0
|
||||
const value = metric.values.find(v => this.labelsToKey(v.labels as Labels) === key)
|
||||
return value?.value ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the counter
|
||||
* If labels provided, removes only that label combination
|
||||
* Otherwise resets all values
|
||||
*/
|
||||
reset(labels?: Labels): void {
|
||||
if (labels) {
|
||||
const key = this.labelsToKey(labels)
|
||||
this.values.delete(key)
|
||||
if (labels && Object.keys(labels).length > 0) {
|
||||
// prom-client doesn't support removing specific label combinations
|
||||
// Use remove() to remove the specific label set
|
||||
this.promCounter.remove(labels)
|
||||
} else {
|
||||
this.values.clear()
|
||||
this.promCounter.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values
|
||||
* Get all values (async, from prom-client)
|
||||
*/
|
||||
getValues(): MetricValue[] {
|
||||
return Array.from(this.values.values())
|
||||
async getValues(): Promise<MetricValue[]> {
|
||||
const metric = await this.promCounter.get()
|
||||
return metric.values.map(v => ({
|
||||
labels: v.labels as Labels,
|
||||
value: v.value,
|
||||
}))
|
||||
}
|
||||
|
||||
private labelsToKey(labels: Labels): string {
|
||||
@@ -251,24 +271,32 @@ export class Counter implements BaseMetric {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Gauge Class
|
||||
// Gauge Class (prom-client backed)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Gauge class - value that can increase and decrease
|
||||
* Now backed by prom-client for full Prometheus compatibility
|
||||
*
|
||||
* Gauges represent a snapshot of a value at a point in time.
|
||||
* Examples: temperature, current memory usage, queue size.
|
||||
*/
|
||||
export class Gauge implements BaseMetric {
|
||||
readonly type = 'gauge' as const
|
||||
private values: Map<string, MetricValue> = new Map()
|
||||
private promGauge: promClient.Gauge<string>
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public help: string,
|
||||
public labelNames: string[] = []
|
||||
) {}
|
||||
) {
|
||||
this.promGauge = new promClient.Gauge({
|
||||
name,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [customRegistry], // Use custom registry, not global
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value
|
||||
@@ -284,12 +312,12 @@ export class Gauge implements BaseMetric {
|
||||
setValue = value ?? 0
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
this.values.set(key, {
|
||||
labels,
|
||||
value: setValue,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promGauge.labels(labels).set(setValue)
|
||||
} else {
|
||||
this.promGauge.set(setValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,11 +334,12 @@ export class Gauge implements BaseMetric {
|
||||
incValue = value ?? 1
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
const existing = this.values.get(key)
|
||||
const currentValue = existing?.value ?? 0
|
||||
|
||||
this.set(labels, currentValue + incValue)
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promGauge.labels(labels).inc(incValue)
|
||||
} else {
|
||||
this.promGauge.inc(incValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,11 +356,12 @@ export class Gauge implements BaseMetric {
|
||||
decValue = value ?? 1
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
const existing = this.values.get(key)
|
||||
const currentValue = existing?.value ?? 0
|
||||
|
||||
this.set(labels, currentValue - decValue)
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promGauge.labels(labels).dec(decValue)
|
||||
} else {
|
||||
this.promGauge.dec(decValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,30 +372,37 @@ export class Gauge implements BaseMetric {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current value
|
||||
* Get current value (async, from prom-client)
|
||||
*/
|
||||
get(labels: Labels = {}): number {
|
||||
async get(labels: Labels = {}): Promise<number> {
|
||||
const metric = await this.promGauge.get()
|
||||
const key = this.labelsToKey(labels)
|
||||
return this.values.get(key)?.value ?? 0
|
||||
const value = metric.values.find(v => this.labelsToKey(v.labels as Labels) === key)
|
||||
return value?.value ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the gauge
|
||||
* If labels provided, removes only that label combination
|
||||
* Otherwise resets all values
|
||||
*/
|
||||
reset(labels?: Labels): void {
|
||||
if (labels) {
|
||||
const key = this.labelsToKey(labels)
|
||||
this.values.delete(key)
|
||||
if (labels && Object.keys(labels).length > 0) {
|
||||
this.promGauge.remove(labels)
|
||||
} else {
|
||||
this.values.clear()
|
||||
this.promGauge.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values
|
||||
* Get all values (async, from prom-client)
|
||||
*/
|
||||
getValues(): MetricValue[] {
|
||||
return Array.from(this.values.values())
|
||||
async getValues(): Promise<MetricValue[]> {
|
||||
const metric = await this.promGauge.get()
|
||||
return metric.values.map(v => ({
|
||||
labels: v.labels as Labels,
|
||||
value: v.value,
|
||||
}))
|
||||
}
|
||||
|
||||
private labelsToKey(labels: Labels): string {
|
||||
@@ -401,18 +438,19 @@ export class Gauge implements BaseMetric {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Histogram Class
|
||||
// Histogram Class (prom-client backed)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Histogram class - distribution of values in buckets
|
||||
* Now backed by prom-client for full Prometheus compatibility
|
||||
*
|
||||
* Histograms sample observations and count them in configurable buckets.
|
||||
* They also provide sum and count of observations.
|
||||
*/
|
||||
export class Histogram implements BaseMetric {
|
||||
readonly type = 'histogram' as const
|
||||
private values: Map<string, HistogramValue> = new Map()
|
||||
private promHistogram: promClient.Histogram<string>
|
||||
private buckets: number[]
|
||||
|
||||
constructor(
|
||||
@@ -422,6 +460,13 @@ export class Histogram implements BaseMetric {
|
||||
buckets: number[] = DEFAULT_BUCKETS
|
||||
) {
|
||||
this.buckets = [...buckets].sort((a, b) => a - b)
|
||||
this.promHistogram = new promClient.Histogram({
|
||||
name,
|
||||
help,
|
||||
labelNames,
|
||||
buckets: this.buckets,
|
||||
registers: [customRegistry], // Use custom registry, not global
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -438,28 +483,12 @@ export class Histogram implements BaseMetric {
|
||||
observeValue = value ?? 0
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
let histValue = this.values.get(key)
|
||||
|
||||
if (!histValue) {
|
||||
histValue = {
|
||||
labels,
|
||||
buckets: new Map(this.buckets.map((b) => [b, 0])),
|
||||
sum: 0,
|
||||
count: 0,
|
||||
}
|
||||
this.values.set(key, histValue)
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promHistogram.labels(labels).observe(observeValue)
|
||||
} else {
|
||||
this.promHistogram.observe(observeValue)
|
||||
}
|
||||
|
||||
// Increment appropriate buckets (cumulative)
|
||||
for (const bucket of this.buckets) {
|
||||
if (observeValue <= bucket) {
|
||||
histValue.buckets.set(bucket, (histValue.buckets.get(bucket) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
histValue.sum += observeValue
|
||||
histValue.count++
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,30 +516,85 @@ export class Histogram implements BaseMetric {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get histogram values
|
||||
* Get histogram values (async, from prom-client)
|
||||
*/
|
||||
get(labels: Labels = {}): HistogramValue | undefined {
|
||||
async get(labels: Labels = {}): Promise<HistogramValue | undefined> {
|
||||
const metric = await this.promHistogram.get()
|
||||
const key = this.labelsToKey(labels)
|
||||
return this.values.get(key)
|
||||
|
||||
// Find values matching labels
|
||||
const matchingValues = metric.values.filter(v => {
|
||||
const vLabels = { ...v.labels } as Labels
|
||||
delete (vLabels as Record<string, unknown>)['le'] // Remove bucket label
|
||||
return this.labelsToKey(vLabels) === key
|
||||
})
|
||||
|
||||
if (matchingValues.length === 0) return undefined
|
||||
|
||||
const buckets = new Map<number, number>()
|
||||
let sum = 0
|
||||
let count = 0
|
||||
|
||||
for (const v of matchingValues) {
|
||||
if (v.metricName?.endsWith('_bucket')) {
|
||||
const le = parseFloat((v.labels as Record<string, string>)['le'] ?? '0')
|
||||
buckets.set(le, v.value)
|
||||
} else if (v.metricName?.endsWith('_sum')) {
|
||||
sum = v.value
|
||||
} else if (v.metricName?.endsWith('_count')) {
|
||||
count = v.value
|
||||
}
|
||||
}
|
||||
|
||||
return { labels, buckets, sum, count }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the histogram
|
||||
* If labels provided, removes only that label combination
|
||||
* Otherwise resets all values
|
||||
*/
|
||||
reset(labels?: Labels): void {
|
||||
if (labels) {
|
||||
const key = this.labelsToKey(labels)
|
||||
this.values.delete(key)
|
||||
if (labels && Object.keys(labels).length > 0) {
|
||||
this.promHistogram.remove(labels)
|
||||
} else {
|
||||
this.values.clear()
|
||||
this.promHistogram.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values
|
||||
* Get all values (async, from prom-client)
|
||||
*/
|
||||
getValues(): HistogramValue[] {
|
||||
return Array.from(this.values.values())
|
||||
async getValues(): Promise<HistogramValue[]> {
|
||||
const metric = await this.promHistogram.get()
|
||||
const grouped = new Map<string, HistogramValue>()
|
||||
|
||||
for (const v of metric.values) {
|
||||
const vLabels = { ...v.labels } as Labels
|
||||
delete (vLabels as Record<string, unknown>)['le']
|
||||
const key = this.labelsToKey(vLabels)
|
||||
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
labels: vLabels,
|
||||
buckets: new Map(),
|
||||
sum: 0,
|
||||
count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const histValue = grouped.get(key)!
|
||||
if (v.metricName?.endsWith('_bucket')) {
|
||||
const le = parseFloat((v.labels as Record<string, string>)['le'] ?? '0')
|
||||
histValue.buckets.set(le, v.value)
|
||||
} else if (v.metricName?.endsWith('_sum')) {
|
||||
histValue.sum = v.value
|
||||
} else if (v.metricName?.endsWith('_count')) {
|
||||
histValue.count = v.value
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(grouped.values())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,26 +628,32 @@ export class Histogram implements BaseMetric {
|
||||
|
||||
/**
|
||||
* Summary class - value percentiles (quantiles)
|
||||
* Now backed by prom-client for full Prometheus compatibility
|
||||
*
|
||||
* Summaries calculate quantiles over a sliding time window.
|
||||
* Useful for tracking latency distributions.
|
||||
*/
|
||||
export class Summary implements BaseMetric {
|
||||
readonly type = 'summary' as const
|
||||
private values: Map<string, SummaryValue> = new Map()
|
||||
private promSummary: promClient.Summary<string>
|
||||
private percentiles: number[]
|
||||
private maxAge: number // ms
|
||||
private maxSize: number
|
||||
|
||||
constructor(
|
||||
public name: string,
|
||||
public help: string,
|
||||
public labelNames: string[] = [],
|
||||
options: { percentiles?: number[]; maxAge?: number; maxSize?: number } = {}
|
||||
options: { percentiles?: number[]; maxAgeSeconds?: number; ageBuckets?: number } = {}
|
||||
) {
|
||||
this.percentiles = options.percentiles ?? DEFAULT_PERCENTILES
|
||||
this.maxAge = options.maxAge ?? 600000 // 10 min
|
||||
this.maxSize = options.maxSize ?? 1000
|
||||
this.promSummary = new promClient.Summary({
|
||||
name,
|
||||
help,
|
||||
labelNames,
|
||||
percentiles: this.percentiles,
|
||||
maxAgeSeconds: options.maxAgeSeconds ?? 600, // 10 min default
|
||||
ageBuckets: options.ageBuckets ?? 5,
|
||||
registers: [customRegistry], // Use custom registry, not global
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,30 +670,11 @@ export class Summary implements BaseMetric {
|
||||
observeValue = value ?? 0
|
||||
}
|
||||
|
||||
const key = this.labelsToKey(labels)
|
||||
let summaryValue = this.values.get(key)
|
||||
|
||||
if (!summaryValue) {
|
||||
summaryValue = {
|
||||
labels,
|
||||
values: [],
|
||||
sum: 0,
|
||||
count: 0,
|
||||
}
|
||||
this.values.set(key, summaryValue)
|
||||
}
|
||||
|
||||
summaryValue.values.push(observeValue)
|
||||
summaryValue.sum += observeValue
|
||||
summaryValue.count++
|
||||
|
||||
// Limit size to prevent memory leaks
|
||||
if (summaryValue.values.length > this.maxSize) {
|
||||
const removed = summaryValue.values.shift()
|
||||
if (removed !== undefined) {
|
||||
summaryValue.sum -= removed
|
||||
summaryValue.count--
|
||||
}
|
||||
// Use prom-client as single source of truth
|
||||
if (Object.keys(labels).length > 0) {
|
||||
this.promSummary.labels(labels).observe(observeValue)
|
||||
} else {
|
||||
this.promSummary.observe(observeValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,46 +691,78 @@ export class Summary implements BaseMetric {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentile
|
||||
* Get summary values (async, from prom-client)
|
||||
*/
|
||||
getPercentile(labels: Labels, percentile: number): number | undefined {
|
||||
async get(labels: Labels = {}): Promise<SummaryValue | undefined> {
|
||||
const metric = await this.promSummary.get()
|
||||
const key = this.labelsToKey(labels)
|
||||
const summaryValue = this.values.get(key)
|
||||
|
||||
if (!summaryValue || summaryValue.values.length === 0) {
|
||||
return undefined
|
||||
// Find values matching labels
|
||||
const matchingValues = metric.values.filter(v => {
|
||||
const vLabels = { ...v.labels } as Labels
|
||||
delete (vLabels as Record<string, unknown>)['quantile']
|
||||
return this.labelsToKey(vLabels) === key
|
||||
})
|
||||
|
||||
if (matchingValues.length === 0) return undefined
|
||||
|
||||
let sum = 0
|
||||
let count = 0
|
||||
|
||||
for (const v of matchingValues) {
|
||||
if (v.metricName?.endsWith('_sum')) {
|
||||
sum = v.value
|
||||
} else if (v.metricName?.endsWith('_count')) {
|
||||
count = v.value
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...summaryValue.values].sort((a, b) => a - b)
|
||||
const index = Math.ceil(percentile * sorted.length) - 1
|
||||
return sorted[Math.max(0, index)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary values
|
||||
*/
|
||||
get(labels: Labels = {}): SummaryValue | undefined {
|
||||
const key = this.labelsToKey(labels)
|
||||
return this.values.get(key)
|
||||
return { labels, values: [], sum, count }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the summary
|
||||
* If labels provided, removes only that label combination
|
||||
* Otherwise resets all values
|
||||
*/
|
||||
reset(labels?: Labels): void {
|
||||
if (labels) {
|
||||
const key = this.labelsToKey(labels)
|
||||
this.values.delete(key)
|
||||
if (labels && Object.keys(labels).length > 0) {
|
||||
this.promSummary.remove(labels)
|
||||
} else {
|
||||
this.values.clear()
|
||||
this.promSummary.reset()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values
|
||||
* Get all values (async, from prom-client)
|
||||
*/
|
||||
getValues(): SummaryValue[] {
|
||||
return Array.from(this.values.values())
|
||||
async getValues(): Promise<SummaryValue[]> {
|
||||
const metric = await this.promSummary.get()
|
||||
const grouped = new Map<string, SummaryValue>()
|
||||
|
||||
for (const v of metric.values) {
|
||||
const vLabels = { ...v.labels } as Labels
|
||||
delete (vLabels as Record<string, unknown>)['quantile']
|
||||
const key = this.labelsToKey(vLabels)
|
||||
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, {
|
||||
labels: vLabels,
|
||||
values: [],
|
||||
sum: 0,
|
||||
count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const summaryValue = grouped.get(key)!
|
||||
if (v.metricName?.endsWith('_sum')) {
|
||||
summaryValue.sum = v.value
|
||||
} else if (v.metricName?.endsWith('_count')) {
|
||||
summaryValue.count = v.value
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(grouped.values())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -765,75 +868,19 @@ export class MetricsRegistry {
|
||||
|
||||
/**
|
||||
* Return metrics in Prometheus exposition format
|
||||
* Uses custom registry with configured prefix and defaultLabels
|
||||
*/
|
||||
async getMetricsOutput(): Promise<string> {
|
||||
const lines: string[] = []
|
||||
|
||||
for (const [name, metric] of this.metricsMap) {
|
||||
lines.push(`# HELP ${name} ${metric.help}`)
|
||||
lines.push(`# TYPE ${name} ${metric.type}`)
|
||||
|
||||
if (metric instanceof Counter || metric instanceof Gauge) {
|
||||
for (const value of metric.getValues()) {
|
||||
const labelsStr = this.formatLabels({ ...this.defaultLabels, ...value.labels })
|
||||
lines.push(`${name}${labelsStr} ${value.value}`)
|
||||
}
|
||||
} else if (metric instanceof Histogram) {
|
||||
for (const value of metric.getValues()) {
|
||||
const labelsStr = this.formatLabels({ ...this.defaultLabels, ...value.labels })
|
||||
const buckets = metric.getBuckets()
|
||||
|
||||
for (const bucket of buckets) {
|
||||
const bucketLabels = this.formatLabels({
|
||||
...this.defaultLabels,
|
||||
...value.labels,
|
||||
le: String(bucket),
|
||||
})
|
||||
lines.push(`${name}_bucket${bucketLabels} ${value.buckets.get(bucket) ?? 0}`)
|
||||
}
|
||||
|
||||
// +Inf bucket (total count)
|
||||
const infLabels = this.formatLabels({
|
||||
...this.defaultLabels,
|
||||
...value.labels,
|
||||
le: '+Inf',
|
||||
})
|
||||
lines.push(`${name}_bucket${infLabels} ${value.count}`)
|
||||
lines.push(`${name}_sum${labelsStr} ${value.sum}`)
|
||||
lines.push(`${name}_count${labelsStr} ${value.count}`)
|
||||
}
|
||||
} else if (metric instanceof Summary) {
|
||||
for (const value of metric.getValues()) {
|
||||
const labelsStr = this.formatLabels({ ...this.defaultLabels, ...value.labels })
|
||||
|
||||
for (const percentile of metric.getPercentiles()) {
|
||||
const quantileLabels = this.formatLabels({
|
||||
...this.defaultLabels,
|
||||
...value.labels,
|
||||
quantile: String(percentile),
|
||||
})
|
||||
const percentileValue = metric.getPercentile(value.labels, percentile)
|
||||
if (percentileValue !== undefined) {
|
||||
lines.push(`${name}${quantileLabels} ${percentileValue}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`${name}_sum${labelsStr} ${value.sum}`)
|
||||
lines.push(`${name}_count${labelsStr} ${value.count}`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
// Use customRegistry (not global) for proper isolation
|
||||
// Prefix and defaultLabels are applied when metrics are created
|
||||
return await customRegistry.metrics()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return content type for Prometheus
|
||||
* Return content type for Prometheus (using prom-client)
|
||||
*/
|
||||
contentType(): string {
|
||||
return 'text/plain; version=0.0.4; charset=utf-8'
|
||||
return customRegistry.contentType
|
||||
}
|
||||
|
||||
private formatLabels(labels: Labels): string {
|
||||
@@ -1071,6 +1118,14 @@ export class MetricsServer {
|
||||
|
||||
// Cache the promise so concurrent calls get the same one
|
||||
this.startPromise = new Promise<void>((resolve, reject) => {
|
||||
// Enable prom-client's default Node.js metrics collection
|
||||
if (this.config.collectDefaultMetrics) {
|
||||
promClient.collectDefaultMetrics({
|
||||
prefix: this.config.prefix ? `${this.config.prefix}_` : '',
|
||||
labels: this.config.defaultLabels,
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize system collector if enabled (only once)
|
||||
if (this.config.includeSystem && !this.systemCollector) {
|
||||
this.systemCollector = new SystemMetricsCollector(this.registry)
|
||||
|
||||
@@ -16,12 +16,24 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { metrics } from './prometheus-metrics.js'
|
||||
import type { CircuitBreaker } from './circuit-breaker.js'
|
||||
import { RETRY_BACKOFF_DELAYS, RETRY_JITTER_FACTOR } from '../Defaults/index.js'
|
||||
|
||||
/**
|
||||
* Retry configuration with custom progressive backoff
|
||||
* Fixed delay steps in milliseconds: 1s → 2s → 5s → 10s → 20s
|
||||
* Defined locally to avoid circular dependency with Defaults
|
||||
*/
|
||||
const RETRY_BACKOFF_DELAYS = [1000, 2000, 5000, 10000, 20000]
|
||||
|
||||
/**
|
||||
* Jitter factor for retry delays (0.15 = ±15% randomization)
|
||||
* Helps prevent thundering herd problem
|
||||
*/
|
||||
const RETRY_JITTER_FACTOR = 0.15
|
||||
|
||||
/**
|
||||
* Backoff strategies
|
||||
*/
|
||||
export type BackoffStrategy = 'exponential' | 'linear' | 'constant' | 'fibonacci'
|
||||
export type BackoffStrategy = 'exponential' | 'linear' | 'constant' | 'fibonacci' | 'stepped'
|
||||
|
||||
/**
|
||||
* Retry configuration options
|
||||
@@ -140,6 +152,14 @@ export function calculateDelay(
|
||||
break
|
||||
}
|
||||
|
||||
case 'stepped': {
|
||||
// Uses pre-defined delay array directly (ignores baseDelay/multiplier)
|
||||
// Falls back to last delay if attempt exceeds array length
|
||||
const index = Math.min(attempt - 1, RETRY_BACKOFF_DELAYS.length - 1)
|
||||
delay = RETRY_BACKOFF_DELAYS[index] ?? RETRY_BACKOFF_DELAYS[0] ?? baseDelay
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
delay = baseDelay
|
||||
}
|
||||
@@ -634,21 +654,22 @@ export const retryConfigs = {
|
||||
},
|
||||
|
||||
/**
|
||||
* RSocket-style retry (uses RETRY_BACKOFF_DELAYS from Defaults)
|
||||
* Delays: 1s, 2s, 5s, 10s, 20s with jitter
|
||||
* RSocket-style retry with stepped delays
|
||||
* Uses fixed delay array: 1s, 2s, 5s, 10s, 20s (with ±15% jitter)
|
||||
* Unlike exponential, this uses exact delays from RETRY_BACKOFF_DELAYS
|
||||
*/
|
||||
rsocket: {
|
||||
maxAttempts: RETRY_BACKOFF_DELAYS.length,
|
||||
baseDelay: RETRY_BACKOFF_DELAYS[0],
|
||||
maxDelay: RETRY_BACKOFF_DELAYS[RETRY_BACKOFF_DELAYS.length - 1],
|
||||
backoffStrategy: 'exponential' as const,
|
||||
backoffStrategy: 'stepped' as const,
|
||||
jitter: RETRY_JITTER_FACTOR,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Get retry delay with jitter applied
|
||||
* Uses RETRY_BACKOFF_DELAYS and RETRY_JITTER_FACTOR from Defaults
|
||||
* Uses RETRY_BACKOFF_DELAYS and RETRY_JITTER_FACTOR defined locally
|
||||
*
|
||||
* @param attempt - Current attempt number (1-based)
|
||||
* @returns Delay in ms with jitter applied
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
calculateDelay,
|
||||
retryPredicates,
|
||||
retryConfigs,
|
||||
getRetryDelayWithJitter,
|
||||
getAllRetryDelaysWithJitter,
|
||||
type RetryContext,
|
||||
} from '../../Utils/retry-utils.js'
|
||||
|
||||
@@ -114,7 +116,7 @@ describe('retry', () => {
|
||||
|
||||
describe('callbacks', () => {
|
||||
it('should call onRetry callback', async () => {
|
||||
const onRetry = jest.fn()
|
||||
const onRetry = jest.fn<(error: Error, attempt: number, delay: number) => void>()
|
||||
let attempts = 0
|
||||
|
||||
await retry(
|
||||
@@ -388,13 +390,13 @@ describe('RetryManager', () => {
|
||||
describe('retryPredicates', () => {
|
||||
describe('always', () => {
|
||||
it('should always return true', () => {
|
||||
expect(retryPredicates.always(new Error(), 1)).toBe(true)
|
||||
expect(retryPredicates.always()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('never', () => {
|
||||
it('should always return false', () => {
|
||||
expect(retryPredicates.never(new Error(), 1)).toBe(false)
|
||||
expect(retryPredicates.never()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -463,4 +465,153 @@ describe('retryConfigs', () => {
|
||||
it('should have network config with predicate', () => {
|
||||
expect(retryConfigs.network.shouldRetry).toBe(retryPredicates.onNetworkError)
|
||||
})
|
||||
|
||||
it('should have rsocket config with stepped strategy', () => {
|
||||
expect(retryConfigs.rsocket.backoffStrategy).toBe('stepped')
|
||||
expect(retryConfigs.rsocket.maxAttempts).toBe(5)
|
||||
expect(retryConfigs.rsocket.baseDelay).toBe(1000)
|
||||
expect(retryConfigs.rsocket.maxDelay).toBe(20000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateDelay - stepped strategy', () => {
|
||||
it('should use exact delays from RETRY_BACKOFF_DELAYS array', () => {
|
||||
// Expected delays: [1000, 2000, 5000, 10000, 20000]
|
||||
const delay1 = calculateDelay(1, 100, 50000, 'stepped', 2, 0)
|
||||
const delay2 = calculateDelay(2, 100, 50000, 'stepped', 2, 0)
|
||||
const delay3 = calculateDelay(3, 100, 50000, 'stepped', 2, 0)
|
||||
const delay4 = calculateDelay(4, 100, 50000, 'stepped', 2, 0)
|
||||
const delay5 = calculateDelay(5, 100, 50000, 'stepped', 2, 0)
|
||||
|
||||
expect(delay1).toBe(1000)
|
||||
expect(delay2).toBe(2000)
|
||||
expect(delay3).toBe(5000)
|
||||
expect(delay4).toBe(10000)
|
||||
expect(delay5).toBe(20000)
|
||||
})
|
||||
|
||||
it('should use last delay for attempts beyond array length', () => {
|
||||
const delay6 = calculateDelay(6, 100, 50000, 'stepped', 2, 0)
|
||||
const delay10 = calculateDelay(10, 100, 50000, 'stepped', 2, 0)
|
||||
|
||||
expect(delay6).toBe(20000)
|
||||
expect(delay10).toBe(20000)
|
||||
})
|
||||
|
||||
it('should ignore baseDelay and multiplier parameters', () => {
|
||||
// baseDelay=9999 and multiplier=99 should be ignored
|
||||
const delay = calculateDelay(1, 9999, 50000, 'stepped', 99, 0)
|
||||
expect(delay).toBe(1000) // First step from RETRY_BACKOFF_DELAYS
|
||||
})
|
||||
|
||||
it('should still apply jitter to stepped delays', () => {
|
||||
const delays = new Set<number>()
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
delays.add(calculateDelay(1, 100, 50000, 'stepped', 2, 0.15))
|
||||
}
|
||||
|
||||
// With 15% jitter on 1000ms, delays should vary between ~850ms and ~1150ms
|
||||
expect(delays.size).toBeGreaterThan(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRetryDelayWithJitter', () => {
|
||||
it('should return delay for each attempt with jitter applied', () => {
|
||||
// Test multiple times to verify jitter creates variation
|
||||
const delays: number[] = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
delays.push(getRetryDelayWithJitter(1))
|
||||
}
|
||||
|
||||
// All delays should be within ±15% of 1000ms (850-1150ms)
|
||||
delays.forEach(delay => {
|
||||
expect(delay).toBeGreaterThanOrEqual(850)
|
||||
expect(delay).toBeLessThanOrEqual(1150)
|
||||
})
|
||||
|
||||
// With jitter, we should have some variation
|
||||
const uniqueDelays = new Set(delays)
|
||||
expect(uniqueDelays.size).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('should return correct base delays for each attempt', () => {
|
||||
// Test without considering jitter - just verify rough ranges
|
||||
// Attempt 1: 1000ms ±15% = 850-1150ms
|
||||
// Attempt 2: 2000ms ±15% = 1700-2300ms
|
||||
// Attempt 3: 5000ms ±15% = 4250-5750ms
|
||||
// Attempt 4: 10000ms ±15% = 8500-11500ms
|
||||
// Attempt 5: 20000ms ±15% = 17000-23000ms
|
||||
|
||||
const d1 = getRetryDelayWithJitter(1)
|
||||
const d2 = getRetryDelayWithJitter(2)
|
||||
const d3 = getRetryDelayWithJitter(3)
|
||||
const d4 = getRetryDelayWithJitter(4)
|
||||
const d5 = getRetryDelayWithJitter(5)
|
||||
|
||||
expect(d1).toBeGreaterThanOrEqual(850)
|
||||
expect(d1).toBeLessThanOrEqual(1150)
|
||||
|
||||
expect(d2).toBeGreaterThanOrEqual(1700)
|
||||
expect(d2).toBeLessThanOrEqual(2300)
|
||||
|
||||
expect(d3).toBeGreaterThanOrEqual(4250)
|
||||
expect(d3).toBeLessThanOrEqual(5750)
|
||||
|
||||
expect(d4).toBeGreaterThanOrEqual(8500)
|
||||
expect(d4).toBeLessThanOrEqual(11500)
|
||||
|
||||
expect(d5).toBeGreaterThanOrEqual(17000)
|
||||
expect(d5).toBeLessThanOrEqual(23000)
|
||||
})
|
||||
|
||||
it('should handle attempt 0 by using first delay', () => {
|
||||
const delay = getRetryDelayWithJitter(0)
|
||||
expect(delay).toBeGreaterThanOrEqual(850)
|
||||
expect(delay).toBeLessThanOrEqual(1150)
|
||||
})
|
||||
|
||||
it('should handle negative attempts by using first delay', () => {
|
||||
const delay = getRetryDelayWithJitter(-5)
|
||||
expect(delay).toBeGreaterThanOrEqual(850)
|
||||
expect(delay).toBeLessThanOrEqual(1150)
|
||||
})
|
||||
|
||||
it('should use last delay for attempts beyond array length', () => {
|
||||
const delay6 = getRetryDelayWithJitter(6)
|
||||
const delay100 = getRetryDelayWithJitter(100)
|
||||
|
||||
// Both should use 20000ms ±15% = 17000-23000ms
|
||||
expect(delay6).toBeGreaterThanOrEqual(17000)
|
||||
expect(delay6).toBeLessThanOrEqual(23000)
|
||||
|
||||
expect(delay100).toBeGreaterThanOrEqual(17000)
|
||||
expect(delay100).toBeLessThanOrEqual(23000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllRetryDelaysWithJitter', () => {
|
||||
it('should return array with 5 delays', () => {
|
||||
const delays = getAllRetryDelaysWithJitter()
|
||||
expect(delays).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('should return delays in increasing order (approximately)', () => {
|
||||
const delays = getAllRetryDelaysWithJitter()
|
||||
|
||||
// Each delay should be roughly larger than the previous
|
||||
// (with jitter, there's small chance of overlap at boundaries)
|
||||
expect(delays[0]!).toBeLessThan(delays[2]!) // 1000 < 5000
|
||||
expect(delays[1]!).toBeLessThan(delays[3]!) // 2000 < 10000
|
||||
expect(delays[2]!).toBeLessThan(delays[4]!) // 5000 < 20000
|
||||
})
|
||||
|
||||
it('should return different values on each call due to jitter', () => {
|
||||
const delays1 = getAllRetryDelaysWithJitter()
|
||||
const delays2 = getAllRetryDelaysWithJitter()
|
||||
|
||||
// At least one delay should differ between calls
|
||||
const allSame = delays1.every((d, i) => d === delays2[i])
|
||||
expect(allSame).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -443,6 +443,11 @@
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz"
|
||||
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz"
|
||||
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
|
||||
|
||||
"@img/sharp-linux-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz"
|
||||
@@ -450,6 +455,13 @@
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linux-x64" "1.2.4"
|
||||
|
||||
"@img/sharp-linuxmusl-x64@0.34.5":
|
||||
version "0.34.5"
|
||||
resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz"
|
||||
integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
|
||||
optionalDependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
||||
@@ -1157,6 +1169,11 @@
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^18.0.0"
|
||||
|
||||
"@opentelemetry/api@^1.4.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz"
|
||||
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
|
||||
|
||||
"@pinojs/redact@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz"
|
||||
@@ -1600,6 +1617,11 @@
|
||||
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz"
|
||||
integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==
|
||||
|
||||
"@unrs/resolver-binding-linux-x64-musl@1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz"
|
||||
integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==
|
||||
|
||||
"@whiskeysockets/eslint-config@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@whiskeysockets/eslint-config/-/eslint-config-1.0.0.tgz"
|
||||
@@ -1886,6 +1908,11 @@ big-integer@^1.6.44:
|
||||
resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz"
|
||||
integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==
|
||||
|
||||
bintrees@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz"
|
||||
integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==
|
||||
|
||||
bl@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz"
|
||||
@@ -5669,6 +5696,14 @@ process@^0.11.10:
|
||||
resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
prom-client@^15.1.3:
|
||||
version "15.1.3"
|
||||
resolved "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz"
|
||||
integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==
|
||||
dependencies:
|
||||
"@opentelemetry/api" "^1.4.0"
|
||||
tdigest "^0.1.1"
|
||||
|
||||
promise.allsettled@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz"
|
||||
@@ -6572,6 +6607,13 @@ synckit@^0.11.12, synckit@^0.11.8:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tdigest@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz"
|
||||
integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==
|
||||
dependencies:
|
||||
bintrees "1.0.2"
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user