UNPKG

adonis-forge

Version:

Bundle utils for AdonisJS

279 lines (278 loc) 8.24 kB
// 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