fix(frontend): 补齐 analysis 构建依赖与共享组件搬迁

补 commit hooks/图表组件、路由与 i18n domains、logs metrics API;
StatCard/TimeRangePicker/ingress-access-log 抽到共享路径,干净 checkout 可编过。
This commit is contained in:
Codex committed 2026-10-28 22:00:30 +08:00
1 parent f993d5b420
commit 11d0fff764
22 files changed
+552 -313

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
/** ingress-nginx 默认 access log;采集器可能在前面附加 CRI 时间/stdout 包装。 */
export interface IngressAccessLog {
clientIp: string
method: string
path: string
status: string
bytesSent: string
referer: string
domain: string
domainSource: 'host' | 'referer' | ''
userAgent: string
requestLength: string
requestTime: string
upstreamName: string
upstreamAddress: string
upstreamStatus: string
upstreamResponseTime: string
}
export function stripLogEnvelope(message: string): string {
return message.replace(/^\d{4}-\d\d-\d\dT\S+\s+(?:stdout|stderr)\s+[FP]\s+/, '')
}
const accessPattern = /^(\S+)\s+\S+\s+\S+\s+\[[^\]]+\]\s+"([A-Z]+)\s+(\S+)\s+[^"]+"\s+(\d{3})\s+(\S+)\s+"([^"]*)"\s+"([^"]*)"(?:\s+(.*))?$/
const upstreamPattern = /^(\S+)\s+(\S+)\s+\[([^\]]*)\]\s+\[[^\]]*\]\s+(\S+)\s+\S+\s+(\S+)\s+(\S+)(?:\s+\S+)?$/
const asText = (value: unknown) => value === undefined || value === null ? '' : String(value)
function domainFromHost(value: string): string {
const host = value.trim().toLowerCase()
if (!host || host === '-') return ''
try {
return new URL(/^https?:\/\//.test(host) ? host : `http://${host}`).hostname
} catch {
return ''
}
}
function domainFromReferer(value: string): string {
try {
return new URL(value).hostname.toLowerCase()
} catch {
return ''
}
}
function accessDomain(host: string, path: string): Pick<IngressAccessLog, 'domain' | 'domainSource'> {
const requestHost = domainFromHost(host) || (/^https?:\/\//i.test(path) ? domainFromReferer(path) : '')
if (requestHost) return { domain: requestHost, domainSource: 'host' }
return { domain: '', domainSource: '' }
}
export function parseIngressAccessLog(message: string): IngressAccessLog | null {
const content = stripLogEnvelope(message).trim()
if (content.startsWith('{')) {
try {
const value: unknown = JSON.parse(content)
if (value && typeof value === 'object' && !Array.isArray(value)) {
const row = value as Record<string, unknown>
const method = asText(row.method ?? row.request_method)
const path = asText(row.path ?? row.uri ?? row.request_uri)
const status = asText(row.status ?? row.status_code)
const referer = asText(row.http_referer ?? row.referer)
if (method && path && /^\d{3}$/.test(status)) return {
clientIp: asText(row.remote_addr ?? row.client_ip), method, path, status,
bytesSent: asText(row.bytes_sent ?? row.body_bytes_sent),
referer,
...accessDomain(asText(row.http_host || row.host || row.server_name), path),
userAgent: asText(row.http_user_agent ?? row.user_agent),
requestLength: asText(row.request_length), requestTime: asText(row.request_time),
upstreamName: asText(row.proxy_upstream_name ?? row.upstream_name),
upstreamAddress: asText(row.upstream_addr),
upstreamStatus: asText(row.upstream_status),
upstreamResponseTime: asText(row.upstream_response_time),
}
}
} catch { /* Fall through to plain access log parsing. */ }
}
const match = content.match(accessPattern)
if (!match) return null
const upstream = (match[8] ?? '').match(upstreamPattern)
return {
clientIp: match[1], method: match[2], path: match[3], status: match[4],
bytesSent: match[5], referer: match[6], userAgent: match[7],
...accessDomain('', match[3]),
requestLength: upstream?.[1] ?? '', requestTime: upstream?.[2] ?? '',
upstreamName: upstream?.[3] ?? '', upstreamAddress: upstream?.[4] ?? '',
upstreamResponseTime: upstream?.[5] ?? '', upstreamStatus: upstream?.[6] ?? '',
}
}
export function httpStatusClass(status: string): '2xx' | '3xx' | '4xx' | '5xx' | 'other' {
const group = status[0]
return group === '2' || group === '3' || group === '4' || group === '5' ? `${group}xx` : 'other'
}
export function pathWithoutQuery(path: string): string {
return path.split('?')[0] || '/'
}