feat(ops/monitoring): add in-memory log buffer, metrics time-series and ops UI integration
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '7.0.1',
|
||||
version: '7.1.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ export class OpsViewLogs extends DeesElement {
|
||||
filters: {},
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor filterLevel: string | undefined;
|
||||
|
||||
@state()
|
||||
accessor filterCategory: string | undefined;
|
||||
|
||||
@state()
|
||||
accessor filterLimit: number = 100;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.logStatePart
|
||||
@@ -174,29 +183,43 @@ export class OpsViewLogs extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Auto-fetch logs when the view mounts
|
||||
this.fetchLogs();
|
||||
}
|
||||
|
||||
private async fetchLogs() {
|
||||
const filters = this.getActiveFilters();
|
||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
||||
limit: filters.limit || 100,
|
||||
level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
||||
limit: this.filterLimit,
|
||||
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private updateFilter(type: string, value: string) {
|
||||
if (value === 'all') {
|
||||
value = undefined;
|
||||
const resolved = value === 'all' ? undefined : value;
|
||||
|
||||
switch (type) {
|
||||
case 'level':
|
||||
this.filterLevel = resolved;
|
||||
break;
|
||||
case 'category':
|
||||
this.filterCategory = resolved;
|
||||
break;
|
||||
case 'limit':
|
||||
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update filters then fetch logs
|
||||
|
||||
this.fetchLogs();
|
||||
}
|
||||
|
||||
private getActiveFilters() {
|
||||
return {
|
||||
level: this.logState.filters.level?.[0],
|
||||
category: this.logState.filters.category?.[0],
|
||||
limit: 100,
|
||||
level: this.filterLevel,
|
||||
category: this.filterCategory,
|
||||
limit: this.filterLimit,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
|
||||
error: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor logState: appstate.ILogState = {
|
||||
recentLogs: [],
|
||||
isStreaming: false,
|
||||
filters: {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.statsStatePart
|
||||
const statsSub = appstate.statsStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((statsState) => {
|
||||
this.statsState = statsState;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
this.rxSubscriptions.push(statsSub);
|
||||
|
||||
const logSub = appstate.logStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((logState) => {
|
||||
this.logState = logState;
|
||||
});
|
||||
this.rxSubscriptions.push(logSub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Ensure logs are fetched for the overview charts
|
||||
if (this.logState.recentLogs.length === 0) {
|
||||
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
|
||||
${this.renderDnsStats()}
|
||||
|
||||
<div class="chartGrid">
|
||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
||||
<dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area>
|
||||
<dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log>
|
||||
<dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log>
|
||||
<dees-chart-area
|
||||
.label=${'Email Traffic (24h)'}
|
||||
.series=${this.getEmailTrafficSeries()}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-area
|
||||
.label=${'DNS Queries (24h)'}
|
||||
.series=${this.getDnsQuerySeries()}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-log
|
||||
.label=${'Recent Events'}
|
||||
.logEntries=${this.getRecentEventEntries()}
|
||||
></dees-chart-log>
|
||||
<dees-chart-log
|
||||
.label=${'Security Alerts'}
|
||||
.logEntries=${this.getSecurityAlertEntries()}
|
||||
></dees-chart-log>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -337,4 +373,42 @@ export class OpsViewOverview extends DeesElement {
|
||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Chart data helpers ---
|
||||
|
||||
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||
return this.logState.recentLogs.map((log) => ({
|
||||
timestamp: new Date(log.timestamp).toISOString(),
|
||||
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
||||
message: log.message,
|
||||
source: log.category,
|
||||
}));
|
||||
}
|
||||
|
||||
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
|
||||
return events.map((evt: any) => ({
|
||||
timestamp: new Date(evt.timestamp).toISOString(),
|
||||
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
|
||||
message: evt.message,
|
||||
source: evt.type,
|
||||
}));
|
||||
}
|
||||
|
||||
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
||||
const ts = this.statsState.emailStats?.timeSeries;
|
||||
if (!ts) return [];
|
||||
return [
|
||||
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||
];
|
||||
}
|
||||
|
||||
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
||||
const ts = this.statsState.dnsStats?.timeSeries;
|
||||
if (!ts) return [];
|
||||
return [
|
||||
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -249,7 +249,14 @@ export class OpsViewSecurity extends DeesElement {
|
||||
private renderOverview(metrics: any) {
|
||||
const threatLevel = this.calculateThreatLevel(metrics);
|
||||
const threatScore = this.getThreatScore(metrics);
|
||||
|
||||
|
||||
// Derive active sessions from recent successful auth events (last hour)
|
||||
const allEvents: any[] = metrics.recentEvents || [];
|
||||
const oneHourAgo = Date.now() - 3600000;
|
||||
const recentAuthSuccesses = allEvents.filter(
|
||||
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
|
||||
).length;
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'threatLevel',
|
||||
@@ -271,7 +278,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
{
|
||||
id: 'blockedThreats',
|
||||
title: 'Blocked Threats',
|
||||
value: metrics.blockedIPs.length + metrics.spamDetected,
|
||||
value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
|
||||
type: 'number',
|
||||
icon: 'lucide:ShieldCheck',
|
||||
color: '#ef4444',
|
||||
@@ -280,11 +287,11 @@ export class OpsViewSecurity extends DeesElement {
|
||||
{
|
||||
id: 'activeSessions',
|
||||
title: 'Active Sessions',
|
||||
value: 0,
|
||||
value: recentAuthSuccesses,
|
||||
type: 'number',
|
||||
icon: 'lucide:Users',
|
||||
color: '#22c55e',
|
||||
description: 'Current authenticated sessions',
|
||||
description: 'Authenticated in last hour',
|
||||
},
|
||||
{
|
||||
id: 'authFailures',
|
||||
@@ -349,6 +356,11 @@ export class OpsViewSecurity extends DeesElement {
|
||||
}
|
||||
|
||||
private renderAuthentication(metrics: any) {
|
||||
// Derive auth events from recentEvents
|
||||
const allEvents: any[] = metrics.recentEvents || [];
|
||||
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
|
||||
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'authFailures',
|
||||
@@ -362,7 +374,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
{
|
||||
id: 'successfulLogins',
|
||||
title: 'Successful Logins',
|
||||
value: 0,
|
||||
value: successfulLogins,
|
||||
type: 'number',
|
||||
icon: 'lucide:Lock',
|
||||
color: '#22c55e',
|
||||
@@ -370,6 +382,15 @@ export class OpsViewSecurity extends DeesElement {
|
||||
},
|
||||
];
|
||||
|
||||
// Map auth events to login history table data
|
||||
const loginHistory = authEvents.map((evt: any) => ({
|
||||
timestamp: evt.timestamp,
|
||||
username: evt.details?.username || 'unknown',
|
||||
ipAddress: evt.ipAddress || 'unknown',
|
||||
success: evt.success ?? false,
|
||||
reason: evt.success ? '' : evt.message || 'Authentication failed',
|
||||
}));
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
<dees-table
|
||||
.heading1=${'Login History'}
|
||||
.heading2=${'Recent authentication attempts'}
|
||||
.data=${[]}
|
||||
.data=${loginHistory}
|
||||
.displayFunction=${(item) => ({
|
||||
'Time': new Date(item.timestamp).toLocaleString(),
|
||||
'Username': item.username,
|
||||
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
|
||||
private getThreatScore(metrics: any): number {
|
||||
// Simple scoring algorithm
|
||||
let score = 100;
|
||||
score -= metrics.blockedIPs.length * 2;
|
||||
score -= metrics.authenticationFailures * 1;
|
||||
score -= metrics.spamDetected * 0.5;
|
||||
score -= metrics.malwareDetected * 3;
|
||||
score -= metrics.phishingDetected * 3;
|
||||
score -= metrics.suspiciousActivities * 2;
|
||||
const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
|
||||
score -= blockedCount * 2;
|
||||
score -= (metrics.authenticationFailures || 0) * 1;
|
||||
score -= (metrics.spamDetected || 0) * 0.5;
|
||||
score -= (metrics.malwareDetected || 0) * 3;
|
||||
score -= (metrics.phishingDetected || 0) * 3;
|
||||
score -= (metrics.suspiciousActivities || 0) * 2;
|
||||
return Math.max(0, Math.min(100, Math.round(score)));
|
||||
}
|
||||
|
||||
private getSecurityEvents(metrics: any): any[] {
|
||||
// Mock data - in real implementation, this would come from the server
|
||||
return [
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 5,
|
||||
event: 'Multiple failed login attempts',
|
||||
severity: 'warning',
|
||||
details: 'IP: 192.168.1.100',
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 15,
|
||||
event: 'SPF check failed',
|
||||
severity: 'medium',
|
||||
details: 'Domain: example.com',
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 30,
|
||||
event: 'IP blocked due to spam',
|
||||
severity: 'high',
|
||||
details: 'IP: 10.0.0.1',
|
||||
},
|
||||
];
|
||||
const events: any[] = metrics.recentEvents || [];
|
||||
return events.map((evt: any) => ({
|
||||
timestamp: evt.timestamp,
|
||||
event: evt.message,
|
||||
severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
|
||||
details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
|
||||
}));
|
||||
}
|
||||
|
||||
private async clearBlockedIPs() {
|
||||
console.log('Clear blocked IPs');
|
||||
// SmartProxy manages IP blocking — not yet exposed via API
|
||||
alert('Clearing blocked IPs is not yet supported from the UI.');
|
||||
}
|
||||
|
||||
private async unblockIP(ip: string) {
|
||||
console.log('Unblock IP:', ip);
|
||||
// SmartProxy manages IP blocking — not yet exposed via API
|
||||
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
||||
}
|
||||
|
||||
private async saveEmailSecuritySettings() {
|
||||
console.log('Save email security settings');
|
||||
// Config is read-only from the UI for now
|
||||
alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user