nginx-monitor-panel/lib/analyzeLogs.ts
2025-08-28 11:34:44 +08:00

188 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 移植自用户提供的 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";
}