File size: 3,317 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Rate limiter to prevent excessive telemetry recording
* Ensures we don't send metrics more frequently than specified limits
*/
export class RateLimiter {
private lastRecordTimes: Map<string, number> = new Map();
private readonly minIntervalMs: number;
private static readonly HIGH_PRIORITY_DIVISOR = 2;
constructor(minIntervalMs: number = 60000) {
if (minIntervalMs < 0) {
throw new Error('minIntervalMs must be non-negative.');
}
this.minIntervalMs = minIntervalMs;
}
/**
* Check if we should record a metric based on rate limiting
* @param metricKey - Unique key for the metric type/context
* @param isHighPriority - If true, uses shorter interval for critical events
* @returns true if metric should be recorded
*/
shouldRecord(metricKey: string, isHighPriority: boolean = false): boolean {
const now = Date.now();
const lastRecordTime = this.lastRecordTimes.get(metricKey) || 0;
// Use shorter interval for high priority events (e.g., memory leaks)
const interval = isHighPriority
? Math.round(this.minIntervalMs / RateLimiter.HIGH_PRIORITY_DIVISOR)
: this.minIntervalMs;
if (now - lastRecordTime >= interval) {
this.lastRecordTimes.set(metricKey, now);
return true;
}
return false;
}
/**
* Force record a metric (bypasses rate limiting)
* Use sparingly for critical events
*/
forceRecord(metricKey: string): void {
this.lastRecordTimes.set(metricKey, Date.now());
}
/**
* Get time until next allowed recording for a metric
*/
getTimeUntilNextAllowed(
metricKey: string,
isHighPriority: boolean = false,
): number {
const now = Date.now();
const lastRecordTime = this.lastRecordTimes.get(metricKey) || 0;
const interval = isHighPriority
? Math.round(this.minIntervalMs / RateLimiter.HIGH_PRIORITY_DIVISOR)
: this.minIntervalMs;
const nextAllowedTime = lastRecordTime + interval;
return Math.max(0, nextAllowedTime - now);
}
/**
* Get statistics about rate limiting
*/
getStats(): {
totalMetrics: number;
oldestRecord: number;
newestRecord: number;
averageInterval: number;
} {
const recordTimes = Array.from(this.lastRecordTimes.values());
if (recordTimes.length === 0) {
return {
totalMetrics: 0,
oldestRecord: 0,
newestRecord: 0,
averageInterval: 0,
};
}
const oldest = Math.min(...recordTimes);
const newest = Math.max(...recordTimes);
const totalSpan = newest - oldest;
const averageInterval =
recordTimes.length > 1 ? totalSpan / (recordTimes.length - 1) : 0;
return {
totalMetrics: recordTimes.length,
oldestRecord: oldest,
newestRecord: newest,
averageInterval,
};
}
/**
* Clear all rate limiting state
*/
reset(): void {
this.lastRecordTimes.clear();
}
/**
* Remove old entries to prevent memory leaks
*/
cleanup(maxAgeMs: number = 3600000): void {
const cutoffTime = Date.now() - maxAgeMs;
for (const [key, time] of this.lastRecordTimes.entries()) {
if (time < cutoffTime) {
this.lastRecordTimes.delete(key);
}
}
}
}
|