18 文件 ~35 处 UI 硬编码中文→t()/i18n.t() 调用: - agent-utils.ts timeAgo() 秒前/分钟前/小时前 - utils.ts formatDistanceToNow() 分钟前/小时前/天前 - request.ts 响应解析失败错误消息 - ActiveSessionsPanel/HistorySessionsPanel RDP 在线/历史会话标签 - MetricsExplorer 清除/已选/最近1小时 - HostMonitor 当前主机 subtitle - AIAnalysisResult 证据链 (新增 useTranslation hook) - capacity-predict.ts 容量预测摘要模板 - ApprovalResourcePage 阿里云/腾讯云/华为云 - ApprovalPermissionPage 堡垒机 - site-monitor/helpers.ts 频率/时间/证书天数格式化 - site-monitor/analytics.ts 证书剩余/检查失败/延迟波动事件 - middleware-monitor MwTargetChartsPanel/MwTrendChart/helpers.ts - snmp-monitor SnmpMetricTrendChart/SnmpDeviceChartsPanel - host-monitor/utils.ts 图表点数标签 + PortPolicyDialog 系统进程说明 新增 34 翻译键 (common.duration.secAgo/hourAgo/dayAgo + common.selected/ responseParseError/rdpActiveSessions/rdpHistorySessions/rdpSessionCount/ evidenceChain/last1Hour/currentHost + monitor.lbl 10键 + monitor.siteMonitor 8键 + approval.provider 3键 + approval.permType 1键 + monitor.portPolicyDialog.systemProcessDesc + aiops.capacity.summary) zh/en 7813→7847 对齐; tsc -b=0, npm build✓ (17.31s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
206 lines
8.0 KiB
TypeScript
206 lines
8.0 KiB
TypeScript
// 站点拨测可观测分析 — 按协议聚合指标与异常事件 // Logan.Li
|
|
import type { SiteResult, SiteTask } from '../helpers';
|
|
import { fmtMs, fmtTime, normalizeProbeType } from '../helpers';
|
|
import i18n from '@/i18n';
|
|
|
|
export interface ProbeEvent {
|
|
id: string;
|
|
at: string;
|
|
timeLabel: string;
|
|
severity: 'info' | 'warn' | 'error';
|
|
title: string;
|
|
detail?: string;
|
|
}
|
|
|
|
export interface HttpObsSummary {
|
|
expectedCode: number;
|
|
okCount: number;
|
|
err4xx: number;
|
|
err5xx: number;
|
|
otherErr: number;
|
|
lastCode?: number;
|
|
lastCodeTone: string;
|
|
events: ProbeEvent[];
|
|
chart: Array<{ time: string; code: number; tone: string; ok: boolean }>;
|
|
}
|
|
|
|
export interface SslObsSummary {
|
|
expiry?: string;
|
|
issuer?: string;
|
|
days?: number;
|
|
daysTone: string;
|
|
handshakeMs: number;
|
|
events: ProbeEvent[];
|
|
chart: Array<{ time: string; days: number }>;
|
|
}
|
|
|
|
export interface LatencyObsSummary {
|
|
avg: number;
|
|
min: number;
|
|
max: number;
|
|
p95: number;
|
|
jitter: number;
|
|
spikeCount: number;
|
|
events: ProbeEvent[];
|
|
chart: Array<{ time: string; latency: number; failed: boolean; spike: boolean }>;
|
|
}
|
|
|
|
export interface ConnectivityObsSummary {
|
|
upRate: string;
|
|
failCount: number;
|
|
lastFailAt?: string;
|
|
events: ProbeEvent[];
|
|
chart: Array<{ time: string; up: number; failed: boolean }>;
|
|
}
|
|
|
|
const tooltipOrder = (results: SiteResult[]) => [...results].reverse();
|
|
|
|
function httpCodeTone(code: number, expected: number): string {
|
|
if (code === expected || (expected === 200 && code >= 200 && code < 300)) return 'dg-text-green';
|
|
if (code >= 500) return 'dg-text-red';
|
|
if (code >= 400) return 'dg-text-yellow';
|
|
return 'dg-text-red';
|
|
}
|
|
|
|
function pushEvent(events: ProbeEvent[], r: SiteResult, title: string, severity: ProbeEvent['severity'], detail?: string) {
|
|
events.push({
|
|
id: `${r.id}-${title}`,
|
|
at: r.checked_at,
|
|
timeLabel: fmtTime(r.checked_at),
|
|
severity,
|
|
title,
|
|
detail,
|
|
});
|
|
}
|
|
|
|
export function analyzeHttpResults(results: SiteResult[], task: SiteTask): HttpObsSummary {
|
|
const expected = task.expected_status_code || 200;
|
|
const chart = tooltipOrder(results).map((r) => {
|
|
const code = r.http_status_code ?? (r.status === 'success' ? expected : 0);
|
|
const ok = r.status === 'success' && (code === expected || (expected === 200 && code >= 200 && code < 300));
|
|
return { time: fmtTime(r.checked_at).slice(-5), code, tone: code > 0 ? httpCodeTone(code, expected) : 'dg-text-red', ok };
|
|
});
|
|
let okCount = 0;
|
|
let err4xx = 0;
|
|
let err5xx = 0;
|
|
let otherErr = 0;
|
|
const events: ProbeEvent[] = [];
|
|
for (const r of results) {
|
|
const code = r.http_status_code ?? 0;
|
|
if (r.status === 'success' && (code === expected || (expected === 200 && code >= 200 && code < 300))) {
|
|
okCount++;
|
|
continue;
|
|
}
|
|
if (code >= 500) {
|
|
err5xx++;
|
|
pushEvent(events, r, `HTTP ${code}`, 'error', r.error_message || i18n.t('monitor.lbl.siteServerError'));
|
|
} else if (code >= 400) {
|
|
err4xx++;
|
|
pushEvent(events, r, `HTTP ${code}`, 'warn', r.error_message || i18n.t('monitor.lbl.siteClientError'));
|
|
} else if (r.status === 'timeout') {
|
|
otherErr++;
|
|
pushEvent(events, r, i18n.t('common.timeoutError'), 'error', r.error_message);
|
|
} else {
|
|
otherErr++;
|
|
pushEvent(events, r, code > 0 ? `HTTP ${code}` : i18n.t('monitor.lbl.siteRequestFailed'), 'error', r.error_message);
|
|
}
|
|
}
|
|
const latest = results[0];
|
|
const lastCode = latest?.http_status_code;
|
|
return {
|
|
expectedCode: expected,
|
|
okCount,
|
|
err4xx,
|
|
err5xx,
|
|
otherErr,
|
|
lastCode,
|
|
lastCodeTone: lastCode ? httpCodeTone(lastCode, expected) : 'dg-text-red',
|
|
events: events.slice(0, 8),
|
|
chart,
|
|
};
|
|
}
|
|
|
|
export function analyzeSslResults(results: SiteResult[], task: SiteTask): SslObsSummary {
|
|
const latest = results.find((r) => r.ssl_days_remaining != null) ?? results[0];
|
|
const days = latest?.ssl_days_remaining ?? task.ssl_days_remaining;
|
|
const expiry = latest?.ssl_cert_expiry?.slice(0, 10) ?? task.ssl_cert_expiry?.slice(0, 10);
|
|
const issuer = latest?.ssl_cert_issuer;
|
|
const events: ProbeEvent[] = [];
|
|
for (const r of results) {
|
|
if (r.status !== 'success') {
|
|
pushEvent(events, r, i18n.t('monitor.lbl.siteRequestTimeout'), 'error', r.error_message);
|
|
} else if (r.ssl_days_remaining != null && r.ssl_days_remaining < 30) {
|
|
pushEvent(events, r, i18n.t('monitor.siteMonitor.certRemaining', { days: r.ssl_days_remaining }), r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
|
|
}
|
|
}
|
|
const chart = tooltipOrder(results)
|
|
.filter((r) => r.ssl_days_remaining != null)
|
|
.map((r) => ({ time: fmtTime(r.checked_at).slice(-5), days: r.ssl_days_remaining as number }));
|
|
const tone = days == null ? 'text-[var(--dg-text-muted)]' : days <= 7 ? 'dg-text-red' : days <= 30 ? 'dg-text-yellow' : 'dg-text-green';
|
|
const handshakeMs = results.length
|
|
? results.reduce((s, r) => s + (r.response_time || r.total_time || 0), 0) / results.length
|
|
: 0;
|
|
return { expiry, issuer, days, daysTone: tone, handshakeMs, events: events.slice(0, 8), chart };
|
|
}
|
|
|
|
function analyzeLatencyResults(results: SiteResult[], label: string): LatencyObsSummary {
|
|
const latencies = results.map((r) => r.response_time ?? r.total_time ?? 0).filter((v) => v > 0);
|
|
const sorted = [...latencies].sort((a, b) => a - b);
|
|
const avg = latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
|
|
const min = sorted[0] ?? 0;
|
|
const max = sorted[sorted.length - 1] ?? 0;
|
|
const p95 = sorted.length ? sorted[Math.floor(sorted.length * 0.95)] ?? max : 0;
|
|
const jitter = max - min;
|
|
const spikeThreshold = Math.max(avg * 1.8, avg + 50);
|
|
const events: ProbeEvent[] = [];
|
|
let spikeCount = 0;
|
|
const chart = tooltipOrder(results).map((r) => {
|
|
const latency = r.response_time ?? r.total_time ?? 0;
|
|
const failed = r.status !== 'success';
|
|
const spike = !failed && latency > spikeThreshold;
|
|
if (failed) pushEvent(events, r, i18n.t('monitor.siteMonitor.checkFailed', { label }), 'error', r.error_message || r.remote_ip);
|
|
else if (spike) {
|
|
spikeCount++;
|
|
pushEvent(events, r, i18n.t('monitor.siteMonitor.latencyFluctuation', { label }), 'warn', fmtMs(latency));
|
|
}
|
|
return { time: fmtTime(r.checked_at).slice(-5), latency, failed, spike };
|
|
});
|
|
return { avg, min, max, p95, jitter, spikeCount, events: events.slice(0, 8), chart };
|
|
}
|
|
|
|
export function analyzePingResults(results: SiteResult[]): LatencyObsSummary {
|
|
return analyzeLatencyResults(results, 'PING');
|
|
}
|
|
|
|
export function analyzeDnsResults(results: SiteResult[]): LatencyObsSummary {
|
|
return analyzeLatencyResults(results, i18n.t('cloud.domainResolve'));
|
|
}
|
|
|
|
export function analyzeTcpResults(results: SiteResult[]): ConnectivityObsSummary {
|
|
const successCount = results.filter((r) => r.status === 'success').length;
|
|
const failCount = results.length - successCount;
|
|
const upRate = results.length > 0 ? `${(successCount / results.length * 100).toFixed(1)}%` : '--';
|
|
const events: ProbeEvent[] = [];
|
|
let lastFailAt: string | undefined;
|
|
const chart = tooltipOrder(results).map((r) => {
|
|
const failed = r.status !== 'success';
|
|
if (failed) {
|
|
lastFailAt = r.checked_at;
|
|
pushEvent(events, r, i18n.t('monitor.lbl.siteConnFailed'), 'error', r.error_message || i18n.t('aiops.llmTestFailed'));
|
|
}
|
|
return { time: fmtTime(r.checked_at).slice(-5), up: failed ? 0 : 100, failed };
|
|
});
|
|
return { upRate, failCount, lastFailAt: lastFailAt ? fmtTime(lastFailAt) : undefined, events: events.slice(0, 8), chart };
|
|
}
|
|
|
|
export function getProbeObsSummary(task: SiteTask, results: SiteResult[]) {
|
|
const type = normalizeProbeType(task.type);
|
|
switch (type) {
|
|
case 'HTTP': return { type, http: analyzeHttpResults(results, task) } as const;
|
|
case 'SSL': return { type, ssl: analyzeSslResults(results, task) } as const;
|
|
case 'PING': return { type, latency: analyzePingResults(results) } as const;
|
|
case 'DNS': return { type, latency: analyzeDnsResults(results) } as const;
|
|
case 'TCP': return { type, tcp: analyzeTcpResults(results) } as const;
|
|
default: return { type: 'HTTP' as const, http: analyzeHttpResults(results, task) };
|
|
}
|
|
} |