@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.
- package/dist_serve/bundle.js +3354 -3354
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +12 -7
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +9 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +70 -25
- package/dist_ts/security/classes.securitylogger.js +23 -31
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +11 -6
- package/ts/monitoring/classes.metricsmanager.ts +73 -25
- package/ts/security/classes.securitylogger.ts +26 -37
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
159
|
-
heapTotal
|
|
160
|
-
external
|
|
161
|
-
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
|
|
232
|
-
const
|
|
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
|
-
//
|
|
440
|
-
this.
|
|
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
|
-
},
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
}
|