@serve.zone/dcrouter 10.1.2 → 10.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,9 @@ export class MetricsManager {
35
35
  queryTypes: {} as Record<string, number>,
36
36
  topDomains: new Map<string, number>(),
37
37
  lastResetDate: new Date().toDateString(),
38
- queryTimestamps: [] as number[], // Track query timestamps for rate calculation
38
+ // Per-second query count ring buffer (300 entries = 5 minutes)
39
+ queryRing: new Int32Array(300),
40
+ queryRingLastSecond: 0, // last epoch second that was written
39
41
  responseTimes: [] as number[], // Track response times in ms
40
42
  recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
41
43
  };
@@ -95,12 +97,13 @@ export class MetricsManager {
95
97
  this.dnsMetrics.cacheMisses = 0;
96
98
  this.dnsMetrics.queryTypes = {};
97
99
  this.dnsMetrics.topDomains.clear();
98
- this.dnsMetrics.queryTimestamps = [];
100
+ this.dnsMetrics.queryRing.fill(0);
101
+ this.dnsMetrics.queryRingLastSecond = 0;
99
102
  this.dnsMetrics.responseTimes = [];
100
103
  this.dnsMetrics.recentQueries = [];
101
104
  this.dnsMetrics.lastResetDate = currentDate;
102
105
  }
103
-
106
+
104
107
  if (currentDate !== this.securityMetrics.lastResetDate) {
105
108
  this.securityMetrics.blockedIPs = 0;
106
109
  this.securityMetrics.authFailures = 0;
@@ -111,15 +114,6 @@ export class MetricsManager {
111
114
  this.securityMetrics.lastResetDate = currentDate;
112
115
  }
113
116
 
114
- // Prune old query timestamps (keep last 5 minutes)
115
- const fiveMinutesAgo = Date.now() - 300000;
116
- const idx = this.dnsMetrics.queryTimestamps.findIndex(ts => ts >= fiveMinutesAgo);
117
- if (idx > 0) {
118
- this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.slice(idx);
119
- } else if (idx === -1) {
120
- this.dnsMetrics.queryTimestamps = [];
121
- }
122
-
123
117
  // Prune old time-series buckets every minute (don't wait for lazy query)
124
118
  this.pruneOldBuckets();
125
119
  }, 60000); // Check every minute
@@ -150,16 +144,16 @@ export class MetricsManager {
150
144
  const smartMetricsData = await this.smartMetrics.getMetrics();
151
145
  const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
152
146
  const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
147
+ const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
153
148
 
154
149
  return {
155
150
  uptime: process.uptime(),
156
151
  startTime: Date.now() - (process.uptime() * 1000),
157
152
  memoryUsage: {
158
- heapUsed: process.memoryUsage().heapUsed,
159
- heapTotal: process.memoryUsage().heapTotal,
160
- external: process.memoryUsage().external,
161
- rss: process.memoryUsage().rss,
162
- // Add SmartMetrics memory data
153
+ heapUsed,
154
+ heapTotal,
155
+ external,
156
+ rss,
163
157
  maxMemoryMB: this.smartMetrics.maxMemoryMB,
164
158
  actualUsageBytes: smartMetricsData.memoryUsageBytes,
165
159
  actualUsagePercentage: smartMetricsData.memoryPercentage,
@@ -228,11 +222,8 @@ export class MetricsManager {
228
222
  .slice(0, 10)
229
223
  .map(([domain, count]) => ({ domain, count }));
230
224
 
231
- // Calculate queries per second from recent timestamps
232
- const now = Date.now();
233
- const oneMinuteAgo = now - 60000;
234
- const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
235
- const queriesPerSecond = recentQueries.length / 60;
225
+ // Calculate queries per second from ring buffer (sum last 60 seconds)
226
+ const queriesPerSecond = this.getQueryRingSum(60) / 60;
236
227
 
237
228
  // Calculate average response time
238
229
  const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
@@ -436,8 +427,8 @@ export class MetricsManager {
436
427
  this.dnsMetrics.cacheMisses++;
437
428
  }
438
429
 
439
- // Track query timestamp (pruning moved to resetInterval to avoid O(n) per query)
440
- this.dnsMetrics.queryTimestamps.push(Date.now());
430
+ // Increment per-second query counter in ring buffer
431
+ this.incrementQueryRing();
441
432
 
442
433
  // Track response time if provided
443
434
  if (responseTimeMs) {
@@ -609,7 +600,7 @@ export class MetricsManager {
609
600
  requestsPerSecond,
610
601
  requestsTotal,
611
602
  };
612
- }, 200); // Use 200ms cache for more frequent updates
603
+ }, 1000); // 1s cache matches typical dashboard poll interval
613
604
  }
614
605
 
615
606
  // --- Time-series helpers ---
@@ -638,6 +629,63 @@ export class MetricsManager {
638
629
  bucket.queries++;
639
630
  }
640
631
 
632
+ /**
633
+ * Increment the per-second query counter in the ring buffer.
634
+ * Zeros any stale slots between the last write and the current second.
635
+ */
636
+ private incrementQueryRing(): void {
637
+ const currentSecond = Math.floor(Date.now() / 1000);
638
+ const ring = this.dnsMetrics.queryRing;
639
+ const last = this.dnsMetrics.queryRingLastSecond;
640
+
641
+ if (last === 0) {
642
+ // First call — zero and anchor
643
+ ring.fill(0);
644
+ this.dnsMetrics.queryRingLastSecond = currentSecond;
645
+ ring[currentSecond % ring.length] = 1;
646
+ return;
647
+ }
648
+
649
+ const gap = currentSecond - last;
650
+ if (gap >= ring.length) {
651
+ // Entire ring is stale — clear all
652
+ ring.fill(0);
653
+ } else if (gap > 0) {
654
+ // Zero slots from (last+1) to currentSecond (inclusive)
655
+ for (let s = last + 1; s <= currentSecond; s++) {
656
+ ring[s % ring.length] = 0;
657
+ }
658
+ }
659
+
660
+ this.dnsMetrics.queryRingLastSecond = currentSecond;
661
+ ring[currentSecond % ring.length]++;
662
+ }
663
+
664
+ /**
665
+ * Sum query counts from the ring buffer for the last N seconds.
666
+ */
667
+ private getQueryRingSum(seconds: number): number {
668
+ const currentSecond = Math.floor(Date.now() / 1000);
669
+ const ring = this.dnsMetrics.queryRing;
670
+ const last = this.dnsMetrics.queryRingLastSecond;
671
+
672
+ if (last === 0) return 0;
673
+
674
+ // First, zero stale slots so reads are accurate even without writes
675
+ const gap = currentSecond - last;
676
+ if (gap >= ring.length) return 0; // all data is stale
677
+
678
+ let sum = 0;
679
+ const limit = Math.min(seconds, ring.length);
680
+ for (let i = 0; i < limit; i++) {
681
+ const sec = currentSecond - i;
682
+ if (sec < last - (ring.length - 1)) break; // slot is from older cycle
683
+ if (sec > last) continue; // no writes yet for this second
684
+ sum += ring[sec % ring.length];
685
+ }
686
+ return sum;
687
+ }
688
+
641
689
  private pruneOldBuckets(): void {
642
690
  const cutoff = Date.now() - 86400000; // 24h
643
691
  for (const key of this.emailMinuteBuckets.keys()) {
@@ -162,8 +162,9 @@ export class SecurityLogger {
162
162
  }
163
163
  }
164
164
 
165
- // Return most recent events up to limit
165
+ // Return most recent events up to limit (slice first to avoid mutating source)
166
166
  return filteredEvents
167
+ .slice()
167
168
  .sort((a, b) => b.timestamp - a.timestamp)
168
169
  .slice(0, limit);
169
170
  }
@@ -249,58 +250,46 @@ export class SecurityLogger {
249
250
  topIPs: Array<{ ip: string; count: number }>;
250
251
  topDomains: Array<{ domain: string; count: number }>;
251
252
  } {
252
- // Filter by time window if provided
253
- let events = this.securityEvents;
254
- if (timeWindow) {
255
- const cutoff = Date.now() - timeWindow;
256
- events = events.filter(e => e.timestamp >= cutoff);
253
+ const cutoff = timeWindow ? Date.now() - timeWindow : 0;
254
+
255
+ // Initialize counters
256
+ const byLevel = {} as Record<SecurityLogLevel, number>;
257
+ for (const level of Object.values(SecurityLogLevel)) {
258
+ byLevel[level] = 0;
259
+ }
260
+ const byType = {} as Record<SecurityEventType, number>;
261
+ for (const type of Object.values(SecurityEventType)) {
262
+ byType[type] = 0;
257
263
  }
258
-
259
- // Count by level
260
- const byLevel = Object.values(SecurityLogLevel).reduce((acc, level) => {
261
- acc[level] = events.filter(e => e.level === level).length;
262
- return acc;
263
- }, {} as Record<SecurityLogLevel, number>);
264
-
265
- // Count by type
266
- const byType = Object.values(SecurityEventType).reduce((acc, type) => {
267
- acc[type] = events.filter(e => e.type === type).length;
268
- return acc;
269
- }, {} as Record<SecurityEventType, number>);
270
-
271
- // Count by IP
272
264
  const ipCounts = new Map<string, number>();
273
- events.forEach(e => {
265
+ const domainCounts = new Map<string, number>();
266
+
267
+ // Single pass over all events
268
+ let total = 0;
269
+ for (const e of this.securityEvents) {
270
+ if (cutoff && e.timestamp < cutoff) continue;
271
+ total++;
272
+ byLevel[e.level]++;
273
+ byType[e.type]++;
274
274
  if (e.ipAddress) {
275
275
  ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1);
276
276
  }
277
- });
278
-
279
- // Count by domain
280
- const domainCounts = new Map<string, number>();
281
- events.forEach(e => {
282
277
  if (e.domain) {
283
278
  domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
284
279
  }
285
- });
286
-
280
+ }
281
+
287
282
  // Sort and limit top entries
288
283
  const topIPs = Array.from(ipCounts.entries())
289
284
  .map(([ip, count]) => ({ ip, count }))
290
285
  .sort((a, b) => b.count - a.count)
291
286
  .slice(0, 10);
292
-
287
+
293
288
  const topDomains = Array.from(domainCounts.entries())
294
289
  .map(([domain, count]) => ({ domain, count }))
295
290
  .sort((a, b) => b.count - a.count)
296
291
  .slice(0, 10);
297
-
298
- return {
299
- total: events.length,
300
- byLevel,
301
- byType,
302
- topIPs,
303
- topDomains
304
- };
292
+
293
+ return { total, byLevel, byType, topIPs, topDomains };
305
294
  }
306
295
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '10.1.2',
6
+ version: '10.1.5',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }