// 移植自用户提供的 Fastify 版本,做了少量调整以适配 Next.js 环境 import { UAParser } from "ua-parser-js"; export interface LogAnalysisResult { uniqueIPCount: number; indexAccessCount: number; totalBytes: number; provinceCityProportions: { [key: string]: number }; statusCodeDistribution: { [key: string]: number }; topUrls: { url: string; count: number }[]; timeAccess: { time: number; count: number }[]; browserVersions: { [browser: string]: { [version: string]: number } }; deviceTypes: { [type: string]: number }; osVersions: { [os: string]: { [version: string]: number } }; recentRawLog: string[]; version: string; } export async function analyzeLogs(log: string): Promise { const uniqueIPs = new Set(); let indexAccessCount = 0; let totalBytes = 0; const ipCounts: Map = new Map(); const statusCodeCounts: Map = new Map(); const urlCounts: Map = new Map(); const timeCounts: Map = new Map(); const browserVersionCounts: Map> = new Map(); const deviceTypeCounts: Map = new Map(); const osVersionCounts: Map> = new Map(); const lines = log.split("\n"); const logRegex = /^(\S+) - - \[(.*?)\] "(\S+) (\S+) (\S+)" (\d{3}) (\d+|-) "(.*?)" "(.*?)" ".*?"$/; for (const line of lines) { const match = logRegex.exec(line); if (match) { const ip = match[1]; const datetime = match[2]; const url = match[4]; const statusCode = match[6]; const bytesStr = match[7]; const bytes = bytesStr === "-" ? 0 : parseInt(bytesStr, 10); const userAgentStr = match[9]; uniqueIPs.add(ip); if (url === "/" || url === "/index.html") indexAccessCount++; totalBytes += bytes; ipCounts.set(ip, (ipCounts.get(ip) || 0) + 1); statusCodeCounts.set(statusCode, (statusCodeCounts.get(statusCode) || 0) + 1); urlCounts.set(url, (urlCounts.get(url) || 0) + 1); const dateTimeParts = datetime.split(":"); if (dateTimeParts.length >= 2) { const datePart = dateTimeParts[0]; const hourPart = dateTimeParts[1]; const timeKey = `${datePart}:${hourPart}`; const date = new Date(parseLogDateTime(timeKey)).getTime(); if (!Number.isNaN(date)) { timeCounts.set(date, (timeCounts.get(date) || 0) + 1); } } const parser = new UAParser(userAgentStr as any); const browser = parser.getBrowser(); const device = parser.getDevice(); const os = parser.getOS(); const browserName = browser.name || "Unknown"; const browserVersion = browser.version || "Unknown"; if (!browserVersionCounts.has(browserName)) { browserVersionCounts.set(browserName, new Map()); } const browserMap = browserVersionCounts.get(browserName)!; browserMap.set(browserVersion, (browserMap.get(browserVersion) || 0) + 1); const deviceType = (device.type as string) || "Desktop"; deviceTypeCounts.set(deviceType, (deviceTypeCounts.get(deviceType) || 0) + 1); const osName = os.name || "Unknown"; const osVersion = os.version || "Unknown"; if (!osVersionCounts.has(osName)) { osVersionCounts.set(osName, new Map()); } const osMap = osVersionCounts.get(osName)!; osMap.set(osVersion, (osMap.get(osVersion) || 0) + 1); } } const provinceCityCounts: Map = new Map(); // 动态加载 geoip-lite,避免构建期读取数据文件 const geoip = await import("geoip-lite"); for (const [ip, count] of ipCounts.entries()) { const geo = geoip.lookup(ip as any); if (geo) { const country = geo.country || "Unknown"; const region = (geo as any).region || "Unknown"; // geoip-lite typings const city = geo.city || "Unknown"; const key = `${country}-${region}-${city}`; provinceCityCounts.set(key, (provinceCityCounts.get(key) || 0) + count); } else { provinceCityCounts.set("Unknown", (provinceCityCounts.get("Unknown") || 0) + count); } } const totalAccesses = [...ipCounts.values()].reduce((a, b) => a + b, 0) || 1; const provinceCityProportions: { [key: string]: number } = {}; for (const [key, count] of provinceCityCounts.entries()) { provinceCityProportions[key] = parseFloat(((count / totalAccesses) * 100).toFixed(2)); } const topUrls = Array.from(urlCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 20) .map(([url, count]) => ({ url, count })); const timeAccess = Array.from(timeCounts.entries()) .sort((a, b) => a[0] - b[0]) .map(([time, count]) => ({ time, count })); const statusCodeDistribution: { [key: string]: number } = {}; for (const [code, count] of statusCodeCounts.entries()) { statusCodeDistribution[code] = count; } const browserVersions: { [browser: string]: { [version: string]: number } } = {}; for (const [browser, versions] of browserVersionCounts.entries()) { browserVersions[browser] = {}; for (const [version, count] of versions.entries()) { browserVersions[browser][version] = count; } } const deviceTypes: { [type: string]: number } = {}; for (const [type, count] of deviceTypeCounts.entries()) { deviceTypes[type] = count; } const osVersions: { [os: string]: { [version: string]: number } } = {}; for (const [os, versions] of osVersionCounts.entries()) { osVersions[os] = {}; for (const [version, count] of versions.entries()) { osVersions[os][version] = count; } } return { uniqueIPCount: uniqueIPs.size, indexAccessCount, totalBytes, provinceCityProportions, statusCodeDistribution, topUrls, timeAccess, browserVersions, deviceTypes, osVersions, recentRawLog: lines.slice(-100), version: "1.0.3", }; } function parseLogDateTime(timeKey: string): string { const [datePart, hourPart] = timeKey.split(":"); const [day, monthStr, year] = datePart.split("/"); const month = monthStringToNumber(monthStr); return `${year}-${month}-${day}T${hourPart}:00:00+08:00`; } function monthStringToNumber(monthStr: string): string { const months: { [key: string]: string } = { Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06", Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12", }; return months[monthStr] || "01"; }