adonis-forge
Version:
Bundle utils for AdonisJS
279 lines (278 loc) • 8.24 kB
JavaScript
// src/utils/pino_log_reader.ts
import * as fs from "node:fs";
import * as readline from "node:readline";
var PinoLogReader = class {
levelMap = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60
};
/**
* 读取 Pino 日志文件
* @param filePath - 日志文件路径
* @param options - 筛选选项
* @returns Promise<LogReadResult> 筛选后的日志结果
*/
async readLogs(filePath, options = {}) {
const { maxLines = 1e3, keyword, logLevel, minLevel, reverse = false, fields = null } = options;
if (reverse) {
return this.readLogsReverse(filePath, options);
}
const results = [];
let processedCount = 0;
const targetLevel = this.getTargetLevel(logLevel, minLevel);
try {
const fileStream = fs.createReadStream(filePath, { encoding: "utf8" });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Number.POSITIVE_INFINITY
});
for await (const line of rl) {
if (results.length >= maxLines) {
break;
}
const logEntry = this.parseLine(line);
if (!logEntry) {
continue;
}
processedCount++;
if (this.shouldIncludeLog(logEntry, {
keyword,
targetLevel: targetLevel ?? void 0,
fields
})) {
results.push(logEntry);
}
}
rl.close();
fileStream.close();
} catch (error) {
throw new Error(`\u8BFB\u53D6\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25: ${error.message}`);
}
return {
logs: results,
totalProcessed: processedCount,
totalMatched: results.length
};
}
/**
* 流式读取日志(适合大文件)
* @param filePath - 日志文件路径
* @param options - 筛选选项
* @param onData - 每找到一条匹配日志时的回调
* @param onEnd - 读取完成回调
*/
readLogsStream(filePath, options = {}, onData, onEnd) {
const { maxLines = 1e3, keyword, logLevel, minLevel, fields = null } = options;
let matchedCount = 0;
let processedCount = 0;
const targetLevel = this.getTargetLevel(logLevel, minLevel);
const fileStream = fs.createReadStream(filePath, { encoding: "utf8" });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Number.POSITIVE_INFINITY
});
rl.on("line", (line) => {
if (matchedCount >= maxLines) {
rl.close();
return;
}
const logEntry = this.parseLine(line);
if (!logEntry) {
return;
}
processedCount++;
if (this.shouldIncludeLog(logEntry, { keyword, targetLevel: targetLevel ?? void 0, fields })) {
matchedCount++;
onData(logEntry, matchedCount, processedCount);
}
});
rl.on("close", () => {
if (onEnd) {
onEnd({ totalMatched: matchedCount, totalProcessed: processedCount });
}
});
rl.on("error", (error) => {
throw new Error(`\u6D41\u5F0F\u8BFB\u53D6\u5931\u8D25: ${error.message}`);
});
}
/**
* 逆序读取日志(从文件末尾开始)
*/
async readLogsReverse(filePath, options) {
const { maxLines = 1e3, keyword, logLevel, minLevel, fields } = options;
const results = [];
const targetLevel = this.getTargetLevel(logLevel, minLevel);
try {
const stats = await fs.promises.stat(filePath);
const fileSize = stats.size;
const chunkSize = 64 * 1024;
let buffer = "";
let position = fileSize;
let processedCount = 0;
while (position > 0 && results.length < maxLines) {
const readSize = Math.min(chunkSize, position);
position -= readSize;
const chunk = Buffer.alloc(readSize);
const fd = await fs.promises.open(filePath, "r");
await fd.read(chunk, 0, readSize, position);
await fd.close();
buffer = chunk.toString("utf8") + buffer;
const lines = buffer.split("\n");
if (position > 0) {
buffer = lines.shift() || "";
} else {
buffer = "";
}
for (let i = lines.length - 1; i >= 0; i--) {
if (results.length >= maxLines) break;
const line = lines[i].trim();
if (!line) continue;
const logEntry = this.parseLine(line);
if (!logEntry) continue;
processedCount++;
if (this.shouldIncludeLog(logEntry, {
keyword,
targetLevel: targetLevel ?? void 0,
fields
})) {
results.unshift(logEntry);
}
}
}
return {
logs: results,
totalProcessed: processedCount,
totalMatched: results.length
};
} catch (error) {
throw new Error(`\u9006\u5E8F\u8BFB\u53D6\u5931\u8D25: ${error.message}`);
}
}
/**
* 解析单行日志
*/
parseLine(line) {
const trimmedLine = line.trim();
if (!trimmedLine) return null;
try {
return JSON.parse(trimmedLine);
} catch (error) {
return {
level: 30,
// 默认为 info 级别
time: (/* @__PURE__ */ new Date()).toISOString(),
msg: trimmedLine,
_isPlainText: true
};
}
}
/**
* 判断是否应该包含此日志条目
*/
shouldIncludeLog(logEntry, options) {
const { keyword, targetLevel, fields } = options;
if (targetLevel && logEntry.level < targetLevel) {
return false;
}
if (keyword) {
const searchText = this.getSearchText(logEntry, fields);
const keywordLower = keyword.toLowerCase();
if (!searchText.toLowerCase().includes(keywordLower)) {
return false;
}
}
return true;
}
/**
* 获取用于搜索的文本内容
*/
getSearchText(logEntry, fields) {
if (fields && Array.isArray(fields)) {
return fields.map((field) => this.getNestedValue(logEntry, field)).filter((value) => value !== void 0).map((value) => String(value)).join(" ");
} else {
return JSON.stringify(logEntry);
}
}
/**
* 获取嵌套对象的值
*/
getNestedValue(obj, path) {
return path.split(".").reduce((current, key) => {
return current && current[key] !== void 0 ? current[key] : void 0;
}, obj);
}
/**
* 获取目标日志级别
*/
getTargetLevel(logLevel, minLevel) {
if (minLevel !== void 0) {
return minLevel;
}
if (logLevel && this.levelMap[logLevel]) {
return this.levelMap[logLevel];
}
return null;
}
/**
* 统计日志信息
*/
async getLogStats(filePath) {
const stats = {
totalLines: 0,
validLogLines: 0,
levelCounts: {},
timeRange: { start: null, end: null },
fileSize: 0
};
try {
const fileStats = await fs.promises.stat(filePath);
stats.fileSize = fileStats.size;
const fileStream = fs.createReadStream(filePath, { encoding: "utf8" });
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Number.POSITIVE_INFINITY
});
for await (const line of rl) {
stats.totalLines++;
const logEntry = this.parseLine(line);
if (!logEntry) continue;
stats.validLogLines++;
const levelName = this.getLevelName(logEntry.level);
stats.levelCounts[levelName] = (stats.levelCounts[levelName] || 0) + 1;
if (logEntry.time) {
const logTime = new Date(logEntry.time);
if (!stats.timeRange.start || logTime < stats.timeRange.start) {
stats.timeRange.start = logTime;
}
if (!stats.timeRange.end || logTime > stats.timeRange.end) {
stats.timeRange.end = logTime;
}
}
}
rl.close();
fileStream.close();
} catch (error) {
throw new Error(`\u83B7\u53D6\u7EDF\u8BA1\u4FE1\u606F\u5931\u8D25: ${error.message}`);
}
return stats;
}
/**
* 根据级别数值获取级别名称
*/
getLevelName(levelValue) {
for (const [name, value] of Object.entries(this.levelMap)) {
if (value === levelValue) {
return name;
}
}
return "unknown";
}
};
export {
PinoLogReader
};
//# sourceMappingURL=chunk-GLRYP47D.js.map