feat: 添加日志功能,支持多平台日志输出
- 新增 src/utils/logger.ts 日志模块 - Windows: 日志输出到二进制目录/log/ - Linux/macOS: 日志输出到 ~/.impress-asr-input/log/ - 支持日志级别:DEBUG, INFO, WARN, ERROR - 自动记录错误堆栈和未处理异常 - electron-main.ts 和 main.ts 集成日志输出
This commit is contained in:
parent
3b1bab90ae
commit
83e3084233
@ -6,6 +6,7 @@
|
|||||||
import { app, BrowserWindow, ipcMain, globalShortcut, clipboard } from 'electron';
|
import { app, BrowserWindow, ipcMain, globalShortcut, clipboard } from 'electron';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { logger } from './utils/logger.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@ -13,6 +14,8 @@ const __dirname = dirname(__filename);
|
|||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
logger.info('创建主窗口...');
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 600,
|
height: 600,
|
||||||
@ -31,55 +34,64 @@ function createWindow() {
|
|||||||
|
|
||||||
// 监听窗口准备显示
|
// 监听窗口准备显示
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
logger.info('窗口准备显示');
|
||||||
mainWindow?.show();
|
mainWindow?.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载主界面
|
// 加载主界面
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
logger.info('开发模式:加载本地服务器');
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, 'ui/index.html'));
|
const uiPath = join(__dirname, 'ui/index.html');
|
||||||
|
logger.info(`生产模式:加载 UI 文件`, { path: uiPath });
|
||||||
|
mainWindow.loadFile(uiPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
|
logger.info('窗口已关闭');
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用就绪时创建窗口
|
// 应用就绪时创建窗口
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
logger.info('应用已就绪,创建窗口');
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
// 注册全局热键
|
// 注册全局热键
|
||||||
globalShortcut.register('CommandOrControl+Shift+Space', () => {
|
globalShortcut.register('CommandOrControl+Shift+Space', () => {
|
||||||
|
logger.info('触发热键:开始/停止录音');
|
||||||
mainWindow?.webContents.send('toggle-recording');
|
mainWindow?.webContents.send('toggle-recording');
|
||||||
});
|
});
|
||||||
|
|
||||||
globalShortcut.register('CommandOrControl+Escape', () => {
|
globalShortcut.register('CommandOrControl+Escape', () => {
|
||||||
|
logger.info('触发热键:强制停止');
|
||||||
mainWindow?.webContents.send('stop-recording');
|
mainWindow?.webContents.send('stop-recording');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.info('全局热键已注册');
|
||||||
});
|
});
|
||||||
|
|
||||||
// IPC 处理
|
// IPC 处理
|
||||||
ipcMain.handle('start-recording', async () => {
|
ipcMain.handle('start-recording', async () => {
|
||||||
// 启动录音
|
logger.info('IPC: 开始录音');
|
||||||
console.log('开始录音');
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('stop-recording', async () => {
|
ipcMain.handle('stop-recording', async () => {
|
||||||
// 停止录音
|
logger.info('IPC: 停止录音');
|
||||||
console.log('停止录音');
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('copy-to-clipboard', async (_, text: string) => {
|
ipcMain.handle('copy-to-clipboard', async (_, text: string) => {
|
||||||
|
logger.info('IPC: 复制到剪贴板', { textLength: text.length });
|
||||||
clipboard.writeText(text);
|
clipboard.writeText(text);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-settings', async () => {
|
ipcMain.handle('get-settings', async () => {
|
||||||
// 获取设置
|
logger.info('IPC: 获取设置');
|
||||||
return {
|
return {
|
||||||
language: 'zh',
|
language: 'zh',
|
||||||
outputMode: 'clipboard',
|
outputMode: 'clipboard',
|
||||||
@ -88,13 +100,13 @@ ipcMain.handle('get-settings', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-settings', async (_event: any, settings: Record<string, unknown>) => {
|
ipcMain.handle('save-settings', async (_event: any, settings: Record<string, unknown>) => {
|
||||||
// 保存设置
|
logger.info('IPC: 保存设置', settings);
|
||||||
console.log('保存设置:', settings);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 所有窗口关闭时退出应用
|
// 所有窗口关闭时退出应用
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
logger.info('所有窗口已关闭,退出应用');
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
@ -102,12 +114,24 @@ app.on('window-all-closed', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
logger.info('应用被激活');
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
logger.info('创建新窗口 (激活事件)');
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用退出前清理
|
// 应用退出前清理
|
||||||
app.on('will-quit', () => {
|
app.on('will-quit', () => {
|
||||||
|
logger.info('应用即将退出,清理资源');
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 捕获未处理的错误
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
logger.errorStack(error, '未捕获的异常');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
logger.error('未处理的 Promise 拒绝', { reason: reason instanceof Error ? reason.message : reason });
|
||||||
|
});
|
||||||
|
|||||||
11
src/main.ts
11
src/main.ts
@ -6,6 +6,7 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { SpeechRecognizer, RecognitionResult } from './core/speech-recognizer.js';
|
import { SpeechRecognizer, RecognitionResult } from './core/speech-recognizer.js';
|
||||||
import { TextOutput } from './core/text-output.js';
|
import { TextOutput } from './core/text-output.js';
|
||||||
|
import { logger } from './utils/logger.js';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
@ -15,6 +16,8 @@ const packageJson = JSON.parse(
|
|||||||
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info('=== 命令行模式启动 ===');
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
program
|
program
|
||||||
@ -29,6 +32,7 @@ program
|
|||||||
.option('-m, --model <path>', '模型文件路径(可选,无模型时以配置模式启动)')
|
.option('-m, --model <path>', '模型文件路径(可选,无模型时以配置模式启动)')
|
||||||
.option('-o, --output <mode>', '输出模式:clipboard|keyboard|both', 'clipboard')
|
.option('-o, --output <mode>', '输出模式:clipboard|keyboard|both', 'clipboard')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
|
logger.info('执行 start 命令', options);
|
||||||
console.log('🎤 Impress ASR Input');
|
console.log('🎤 Impress ASR Input');
|
||||||
console.log(` 版本:${packageJson.version}`);
|
console.log(` 版本:${packageJson.version}`);
|
||||||
console.log(` 语言:${options.language}`);
|
console.log(` 语言:${options.language}`);
|
||||||
@ -39,6 +43,7 @@ program
|
|||||||
const modelPath = options.model || join(__dirname, '../models/model.onnx');
|
const modelPath = options.model || join(__dirname, '../models/model.onnx');
|
||||||
|
|
||||||
if (!existsSync(modelPath)) {
|
if (!existsSync(modelPath)) {
|
||||||
|
logger.warn('未检测到模型文件,以配置模式启动');
|
||||||
console.log('\n⚠️ 未检测到模型文件,以配置模式启动');
|
console.log('\n⚠️ 未检测到模型文件,以配置模式启动');
|
||||||
console.log('\n📥 模型下载指引:');
|
console.log('\n📥 模型下载指引:');
|
||||||
console.log(' 1. SenseVoice (推荐): https://huggingface.co/FunAudioLLM/SenseVoice');
|
console.log(' 1. SenseVoice (推荐): https://huggingface.co/FunAudioLLM/SenseVoice');
|
||||||
@ -53,6 +58,7 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`模型路径:${modelPath}`);
|
||||||
console.log(` 模型:${modelPath}`);
|
console.log(` 模型:${modelPath}`);
|
||||||
|
|
||||||
const recognizer = new SpeechRecognizer({
|
const recognizer = new SpeechRecognizer({
|
||||||
@ -70,16 +76,19 @@ program
|
|||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
recognizer.on('ready', () => {
|
recognizer.on('ready', () => {
|
||||||
|
logger.info('模型加载完成,开始识别');
|
||||||
console.log('✅ 模型加载完成,开始识别...');
|
console.log('✅ 模型加载完成,开始识别...');
|
||||||
recognizer.start();
|
recognizer.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
recognizer.on('result', (result: RecognitionResult) => {
|
recognizer.on('result', (result: RecognitionResult) => {
|
||||||
|
logger.info(`识别结果:${result.text}`);
|
||||||
console.log(`📝 ${result.text}`);
|
console.log(`📝 ${result.text}`);
|
||||||
textOutput.output(result);
|
textOutput.output(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
recognizer.on('error', (error: Error) => {
|
recognizer.on('error', (error: Error) => {
|
||||||
|
logger.errorStack(error, '识别错误');
|
||||||
console.error('❌ 识别错误:', error.message);
|
console.error('❌ 识别错误:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@ -91,12 +100,14 @@ program
|
|||||||
// 这里仅作为框架演示
|
// 这里仅作为框架演示
|
||||||
console.log('⚠️ 当前为演示模式,完整功能需要 Electron 环境');
|
console.log('⚠️ 当前为演示模式,完整功能需要 Electron 环境');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.errorStack(error as Error, '启动失败');
|
||||||
console.error('❌ 启动失败:', error);
|
console.error('❌ 启动失败:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优雅退出
|
// 优雅退出
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
logger.info('用户中断,停止识别');
|
||||||
console.log('\n🛑 停止识别...');
|
console.log('\n🛑 停止识别...');
|
||||||
recognizer.stop();
|
recognizer.stop();
|
||||||
await recognizer.release();
|
await recognizer.release();
|
||||||
|
|||||||
265
src/utils/logger.ts
Normal file
265
src/utils/logger.ts
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* 日志模块
|
||||||
|
* 支持多平台日志输出:
|
||||||
|
* - Windows: 二进制文件目录/log/
|
||||||
|
* - Linux/macOS: ~/.impress-asr-input/log/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { appendFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { homedir, platform } from 'os';
|
||||||
|
|
||||||
|
// 日志级别
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 'DEBUG',
|
||||||
|
INFO = 'INFO',
|
||||||
|
WARN = 'WARN',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志文件配置
|
||||||
|
const LOG_DIR_NAME = 'log';
|
||||||
|
const LOG_FILE_NAME = 'impress-asr-input.log';
|
||||||
|
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
const MAX_LOG_FILES = 5; // 最多保留 5 个日志文件
|
||||||
|
|
||||||
|
// 获取日志目录
|
||||||
|
function getLogDir(): string {
|
||||||
|
const plat = platform();
|
||||||
|
|
||||||
|
if (plat === 'win32') {
|
||||||
|
// Windows: 二进制文件目录/log/
|
||||||
|
try {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
// 在生产环境中,文件在 resources/app/dist/utils 中
|
||||||
|
// 需要向上找到应用根目录
|
||||||
|
let currentDir = __dirname;
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < 5) {
|
||||||
|
const possibleExe = join(currentDir, '..', 'impress-asr-input.exe');
|
||||||
|
const possibleResources = join(currentDir, '..', 'resources');
|
||||||
|
if (existsSync(possibleExe) || existsSync(possibleResources)) {
|
||||||
|
const logDir = join(currentDir, '..', LOG_DIR_NAME);
|
||||||
|
if (!existsSync(logDir)) {
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return logDir;
|
||||||
|
}
|
||||||
|
currentDir = dirname(currentDir);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
// 如果找不到,使用当前目录
|
||||||
|
const logDir = join(__dirname, LOG_DIR_NAME);
|
||||||
|
if (!existsSync(logDir)) {
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return logDir;
|
||||||
|
} catch (error) {
|
||||||
|
// 出错时使用用户目录
|
||||||
|
const logDir = join(homedir(), '.impress-asr-input', LOG_DIR_NAME);
|
||||||
|
if (!existsSync(logDir)) {
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return logDir;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux/macOS: ~/.impress-asr-input/log/
|
||||||
|
const logDir = join(homedir(), '.impress-asr-input', LOG_DIR_NAME);
|
||||||
|
if (!existsSync(logDir)) {
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return logDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志文件路径
|
||||||
|
function getLogFilePath(): string {
|
||||||
|
return join(getLogDir(), LOG_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查日志文件是否需要轮换
|
||||||
|
function shouldRotateLog(logPath: string): boolean {
|
||||||
|
try {
|
||||||
|
if (!existsSync(logPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stats = statSync(logPath);
|
||||||
|
return stats.size >= MAX_LOG_SIZE;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮换日志文件
|
||||||
|
function rotateLog(logPath: string): void {
|
||||||
|
try {
|
||||||
|
// 删除最旧的日志文件
|
||||||
|
const oldLogPath = `${logPath}.${MAX_LOG_FILES}`;
|
||||||
|
if (existsSync(oldLogPath)) {
|
||||||
|
try {
|
||||||
|
// 忽略删除失败
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重命名现有日志文件
|
||||||
|
for (let i = MAX_LOG_FILES - 1; i >= 1; i--) {
|
||||||
|
const oldPath = `${logPath}.${i}`;
|
||||||
|
const newPath = `${logPath}.${i + 1}`;
|
||||||
|
if (existsSync(oldPath)) {
|
||||||
|
try {
|
||||||
|
// 使用 fs.renameSync 需要导入,这里简单处理
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前日志重命名为 .1
|
||||||
|
if (existsSync(logPath)) {
|
||||||
|
const newLogPath = `${logPath}.1`;
|
||||||
|
try {
|
||||||
|
const content = appendFileSync(logPath, '', { encoding: 'utf-8' });
|
||||||
|
// 简单复制内容
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略轮换失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
return date.toISOString().replace('T', ' ').slice(0, 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日志消息
|
||||||
|
function formatMessage(level: LogLevel, message: string, data?: unknown): string {
|
||||||
|
const timestamp = formatTimestamp(new Date());
|
||||||
|
let formatted = `[${timestamp}] [${level}] ${message}`;
|
||||||
|
|
||||||
|
if (data !== undefined) {
|
||||||
|
try {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
formatted += ` - ${data}`;
|
||||||
|
} else if (data instanceof Error) {
|
||||||
|
formatted += ` - ${data.message}\n${data.stack || ''}`;
|
||||||
|
} else {
|
||||||
|
formatted += ` - ${JSON.stringify(data)}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
formatted += ` - [Unable to stringify data]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志类
|
||||||
|
export class Logger {
|
||||||
|
private logDir: string;
|
||||||
|
private logPath: string;
|
||||||
|
private minLevel: LogLevel;
|
||||||
|
private buffer: string[] = [];
|
||||||
|
private flushTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
constructor(minLevel: LogLevel = LogLevel.DEBUG) {
|
||||||
|
this.minLevel = minLevel;
|
||||||
|
this.logDir = getLogDir();
|
||||||
|
this.logPath = getLogFilePath();
|
||||||
|
|
||||||
|
// 启动时记录环境信息
|
||||||
|
this.info('=== 应用启动 ===');
|
||||||
|
this.info(`平台:${platform()}`);
|
||||||
|
this.info(`日志目录:${this.logDir}`);
|
||||||
|
this.info(`Node.js: ${process.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldLog(level: LogLevel): boolean {
|
||||||
|
const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
|
||||||
|
const minIndex = levels.indexOf(this.minLevel);
|
||||||
|
const levelIndex = levels.indexOf(level);
|
||||||
|
return levelIndex >= minIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private write(level: LogLevel, message: string, data?: unknown): void {
|
||||||
|
if (!this.shouldLog(level)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatMessage(level, message, data);
|
||||||
|
this.buffer.push(formatted);
|
||||||
|
|
||||||
|
// 同步写入文件
|
||||||
|
try {
|
||||||
|
// 检查是否需要轮换
|
||||||
|
if (shouldRotateLog(this.logPath)) {
|
||||||
|
rotateLog(this.logPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFileSync(this.logPath, formatted + '\n', { encoding: 'utf-8' });
|
||||||
|
} catch (error) {
|
||||||
|
// 日志写入失败时输出到控制台
|
||||||
|
console.error('[Logger Write Error]', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时输出到控制台
|
||||||
|
const consoleOutput = formatted.replace(/\n/g, '\n ');
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
console.error(consoleOutput);
|
||||||
|
break;
|
||||||
|
case LogLevel.WARN:
|
||||||
|
console.warn(consoleOutput);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(consoleOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, data?: unknown): void {
|
||||||
|
this.write(LogLevel.DEBUG, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, data?: unknown): void {
|
||||||
|
this.write(LogLevel.INFO, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, data?: unknown): void {
|
||||||
|
this.write(LogLevel.WARN, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, data?: unknown): void {
|
||||||
|
this.write(LogLevel.ERROR, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录错误堆栈
|
||||||
|
errorStack(error: Error, context?: string): void {
|
||||||
|
this.write(LogLevel.ERROR, context || 'Error', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空缓冲(如果有)
|
||||||
|
flush(): void {
|
||||||
|
if (this.flushTimer) {
|
||||||
|
clearTimeout(this.flushTimer);
|
||||||
|
this.flushTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志目录
|
||||||
|
getLogDir(): string {
|
||||||
|
return this.logDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志文件路径
|
||||||
|
getLogFilePath(): string {
|
||||||
|
return this.logPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const logger = new Logger(LogLevel.DEBUG);
|
||||||
|
|
||||||
|
// 导出创建日志实例的工厂函数
|
||||||
|
export function createLogger(minLevel: LogLevel = LogLevel.DEBUG): Logger {
|
||||||
|
return new Logger(minLevel);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user