feat(ops/monitoring): add in-memory log buffer, metrics time-series and ops UI integration

This commit is contained in:
2026-02-19 17:23:43 +00:00
parent dc6ce341bd
commit eacddc7ce1
14 changed files with 482 additions and 128 deletions

View File

@@ -1,6 +1,7 @@
import * as plugins from '../plugins.js';
import { DcRouter } from '../classes.dcrouter.js';
import { MetricsCache } from './classes.metricscache.js';
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
export class MetricsManager {
private logger: plugins.smartlog.Smartlog;
@@ -37,6 +38,10 @@ export class MetricsManager {
responseTimes: [] as number[], // Track response times in ms
};
// Per-minute time-series buckets for charts
private emailMinuteBuckets = new Map<number, { sent: number; received: number; failed: number }>();
private dnsMinuteBuckets = new Map<number, { queries: number }>();
// Track security-specific metrics
private securityMetrics = {
blockedIPs: 0,
@@ -227,20 +232,45 @@ export class MetricsManager {
});
}
/**
* Sync security metrics from the SecurityLogger singleton (last 24h).
* Called before returning security stats so counters reflect real events.
*/
private syncFromSecurityLogger(): void {
try {
const securityLogger = SecurityLogger.getInstance();
const summary = securityLogger.getEventsSummary(86400000); // last 24h
this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
this.securityMetrics.authFailures =
summary.byType[SecurityEventType.AUTHENTICATION] || 0;
this.securityMetrics.blockedIPs =
(summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
(summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
} catch {
// SecurityLogger may not be initialized yet — ignore
}
}
// Get security metrics
public async getSecurityStats() {
return this.metricsCache.get('securityStats', () => {
// Sync counters from the real SecurityLogger events
this.syncFromSecurityLogger();
// Get recent incidents (last 20)
const recentIncidents = this.securityMetrics.incidents.slice(-20);
return {
blockedIPs: this.securityMetrics.blockedIPs,
authFailures: this.securityMetrics.authFailures,
spamDetected: this.securityMetrics.spamDetected,
malwareDetected: this.securityMetrics.malwareDetected,
phishingDetected: this.securityMetrics.phishingDetected,
totalThreatsBlocked: this.securityMetrics.spamDetected +
this.securityMetrics.malwareDetected +
totalThreatsBlocked: this.securityMetrics.spamDetected +
this.securityMetrics.malwareDetected +
this.securityMetrics.phishingDetected,
recentIncidents,
};
@@ -275,6 +305,7 @@ export class MetricsManager {
// Email event tracking methods
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
this.emailMetrics.sentToday++;
this.incrementEmailBucket('sent');
if (recipient) {
const count = this.emailMetrics.recipients.get(recipient) || 0;
@@ -311,6 +342,7 @@ export class MetricsManager {
public trackEmailReceived(sender?: string): void {
this.emailMetrics.receivedToday++;
this.incrementEmailBucket('received');
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
@@ -326,6 +358,7 @@ export class MetricsManager {
public trackEmailFailed(recipient?: string, reason?: string): void {
this.emailMetrics.failedToday++;
this.incrementEmailBucket('failed');
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
@@ -361,6 +394,7 @@ export class MetricsManager {
// DNS event tracking methods
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
this.dnsMetrics.totalQueries++;
this.incrementDnsBucket();
if (cacheHit) {
this.dnsMetrics.cacheHits++;
@@ -547,4 +581,90 @@ export class MetricsManager {
};
}, 200); // Use 200ms cache for more frequent updates
}
// --- Time-series helpers ---
private static minuteKey(ts: number = Date.now()): number {
return Math.floor(ts / 60000) * 60000;
}
private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
const key = MetricsManager.minuteKey();
let bucket = this.emailMinuteBuckets.get(key);
if (!bucket) {
bucket = { sent: 0, received: 0, failed: 0 };
this.emailMinuteBuckets.set(key, bucket);
}
bucket[field]++;
}
private incrementDnsBucket(): void {
const key = MetricsManager.minuteKey();
let bucket = this.dnsMinuteBuckets.get(key);
if (!bucket) {
bucket = { queries: 0 };
this.dnsMinuteBuckets.set(key, bucket);
}
bucket.queries++;
}
private pruneOldBuckets(): void {
const cutoff = Date.now() - 86400000; // 24h
for (const key of this.emailMinuteBuckets.keys()) {
if (key < cutoff) this.emailMinuteBuckets.delete(key);
}
for (const key of this.dnsMinuteBuckets.keys()) {
if (key < cutoff) this.dnsMinuteBuckets.delete(key);
}
}
/**
* Get email time-series data for the last N hours, aggregated per minute.
*/
public getEmailTimeSeries(hours: number = 24): {
sent: Array<{ timestamp: number; value: number }>;
received: Array<{ timestamp: number; value: number }>;
failed: Array<{ timestamp: number; value: number }>;
} {
this.pruneOldBuckets();
const cutoff = Date.now() - hours * 3600000;
const sent: Array<{ timestamp: number; value: number }> = [];
const received: Array<{ timestamp: number; value: number }> = [];
const failed: Array<{ timestamp: number; value: number }> = [];
const sortedKeys = Array.from(this.emailMinuteBuckets.keys())
.filter((k) => k >= cutoff)
.sort((a, b) => a - b);
for (const key of sortedKeys) {
const bucket = this.emailMinuteBuckets.get(key)!;
sent.push({ timestamp: key, value: bucket.sent });
received.push({ timestamp: key, value: bucket.received });
failed.push({ timestamp: key, value: bucket.failed });
}
return { sent, received, failed };
}
/**
* Get DNS time-series data for the last N hours, aggregated per minute.
*/
public getDnsTimeSeries(hours: number = 24): {
queries: Array<{ timestamp: number; value: number }>;
} {
this.pruneOldBuckets();
const cutoff = Date.now() - hours * 3600000;
const queries: Array<{ timestamp: number; value: number }> = [];
const sortedKeys = Array.from(this.dnsMinuteBuckets.keys())
.filter((k) => k >= cutoff)
.sort((a, b) => a - b);
for (const key of sortedKeys) {
const bucket = this.dnsMinuteBuckets.get(key)!;
queries.push({ timestamp: key, value: bucket.queries });
}
return { queries };
}
}