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);
      }
    }
  }
}