188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
// 移植自用户提供的 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<LogAnalysisResult> {
|
||
const uniqueIPs = new Set<string>();
|
||
let indexAccessCount = 0;
|
||
let totalBytes = 0;
|
||
const ipCounts: Map<string, number> = new Map();
|
||
const statusCodeCounts: Map<string, number> = new Map();
|
||
const urlCounts: Map<string, number> = new Map();
|
||
const timeCounts: Map<number, number> = new Map();
|
||
|
||
const browserVersionCounts: Map<string, Map<string, number>> = new Map();
|
||
const deviceTypeCounts: Map<string, number> = new Map();
|
||
const osVersionCounts: Map<string, Map<string, number>> = 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<string, number> = 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";
|
||
}
|