一个健壮的桌面应用,日志体系不是附属品,而是线上问题定位的生命线。Electron 应用的日志需求通常比纯 Node.js 服务更复杂:既要记录主进程中发生的系统调用错误,也要捕捉渲染进程中的 JavaScript 异常,还要考虑日志文件无限增长占满磁盘的风险,最后还需要在必要时将关键错误上报到远程服务器,让开发者在用户反馈之前就感知到问题。
本节将围绕“分级、切割、上报”三个核心维度,构建一套可直接用于生产环境的日志方案。我们使用 electron-log 作为基础库,它专为 Electron 设计,开箱即用地支持多进程写入同一个日志文件、自动按大小切割、默认输出到操作系统推荐的应用数据目录,并且可通过 transports 轻松接入远程上报。
21.4.1 分级日志:让每一条信息都有轻重
electron-log 提供了与平时写前端 console 几乎一致的调用方式,只是额外加上了日志级别。默认支持四个级别:
error:应用中的错误,通常是需要立即关注的异常。warn:警告信息,比如某个功能降级运行或数据格式不符合预期。info:一般的业务流程记录,例如窗口创建、文件打开、服务启动。debug:开发调试用的详细信息,线上环境通常关闭。verbose/silly(v5 之后):更细粒度的调试输出。
1. 基础用法
在主进程和渲染进程(通过 preload 暴露后)中都可以直接使用:
// main.js (主进程)
const log = require('electron-log');
log.info('App starting...');
log.debug('Loading window with options:', windowOptions);
log.warn('An unusual condition occurred', { userId: 123 });
log.error('Failed to write file', err);
渲染进程中,出于安全考虑,不应直接 require('electron-log'),而应通过 preload 暴露有限的日志接口:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('log', {
info: (msg) => ipcRenderer.send('log-info', msg),
error: (msg) => ipcRenderer.send('log-error', msg),
});
但更常见也推荐的做法是:在渲染进程中,使用前端自己的 console,然后由主进程通过钩子统一收集。electron-log 提供了 log.initialize() 之后自动捕获所有 console 输出的能力,只需在主进程入口启用:
// 主进程 main.js
const log = require('electron-log');
log.initialize(); // 启用捕获渲染进程的 console 输出
这样,所有渲染进程中的 console.log、console.error 等都会自动进入主进程的日志流水线,既方便又安全。
2. 控制输出级别
线上构建时,你通常只想记录 info 及以上的日志;而在本地开发或故障排查时,才需要打开 debug。electron-log 提供了 log.transports.console.level 和 log.transports.file.level 分别控制控制台和文件的日志级别:
const isDev = !app.isPackaged;
log.transports.console.level = isDev ? 'debug' : 'info';
log.transports.file.level = 'info'; // 文件始终记录 info 及以上
log.transports.file.maxDepth = 5; // 对象序列化深度,防止循环引用
这样设置后,debug 日志只在开发工具的控制台中出现,不会写入文件或干扰线上日志的简洁度。当你需要远程排查某个用户的问题时,可以通过应用内置的“开启调试日志”选项动态修改文件级别为 debug,复现后收集详细日志文件。
21.4.2 日志切割:防止磁盘被日志撑爆
桌面应用往往会 7×24 小时运行,如果不做任何限制,一个日志文件可以在几个月内膨胀到数 GB。electron-log 内置了文件切割(rotation)机制,配置非常简单:
// 日志文件切割配置
log.transports.file.maxSize = 10 * 1024 * 1024; // 单个文件最大 10MB
log.transports.file.maxOldFiles = 5; // 保留最近 5 个旧日志文件
log.transports.file.archiveLogFn = (oldLogFile) => {
// 可选:自定义归档逻辑,比如将旧日志备份到专门目录或压缩
const dest = path.join(app.getPath('logs'), 'archive', path.basename(oldLogFile));
fs.renameSync(oldLogFile, dest);
};
当日志文件 main.log 达到 10MB 时,electron-log 会自动将其重命名为 main.1.log,新建一个 main.log 继续写入。如果 main.1.log 已存在,则顺延为 main.2.log,依此类推。最多保留 5 个旧文件,超出数量的最旧日志会被自动删除。这样就形成了一个“环形缓冲”,确保日志总磁盘占用上限在 60MB 左右。
除了按大小切割,也可以结合 electron-log 的 date 格式化,按天写入不同文件(通过设置 log.transports.file.resolvePath 动态返回路径)。但对于多数桌面应用,按大小切割已足够实用,且避免了每天切换造成的日志分散问题。
一个完整的主进程日志初始化示例:
const { app } = require('electron');
const log = require('electron-log');
const path = require('path');
const isDev = !app.isPackaged;
log.transports.console.level = isDev ? 'debug' : 'info';
log.transports.file.level = isDev ? 'debug' : 'info';
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB
log.transports.file.maxOldFiles = 5;
// 日志文件存放路径:应用 userData 下 logs/ 目录
log.transports.file.resolvePath = (variables) => {
return path.join(app.getPath('userData'), 'logs', `${variables.fileName}.log`);
};
log.initialize(); // 捕获渲染进程的 console
app.on('ready', () => {
log.info('App started', { version: app.getVersion(), platform: process.platform });
});
21.4.3 日志上报:在用户之前知道错误
桌面应用不同于服务端,错误往往发生在用户本地,开发者无法主动感知。日志上报的目标就是把 error 和 warn 级别的重要信息主动推送到远程服务器,让开发者在用户联系支持团队之前就能看到错误详情。
electron-log 的 transports 体系允许我们添加自定义传输函数,最简单的上报方式就是利用 log.hooks 拦截所有日志:
// 自定义远程上报传输
log.transports.remote = ({ message, level, data }) => {
// 只上报 error 和 warn 级别
if (level === 'error' || level === 'warn') {
const payload = {
appVersion: app.getVersion(),
platform: process.platform,
level,
message,
data, // 可能包含错误堆栈、上下文
timestamp: new Date().toISOString(),
userId: global.userId, // 如果能获取
};
// 使用 fire-and-forget 方式发送,避免阻塞主进程
fetch('https://your-server.com/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {}); // 上报失败不能影响应用本身
}
};
或者使用更完善的第三方服务,如 Sentry。@sentry/electron 专门为 Electron 优化了采集和上报,可以自动捕获主进程和渲染进程的错误、崩溃以及 IPC 异常,同时也允许手动记录日志事件。集成方式大致为:
const Sentry = require('@sentry/electron');
Sentry.init({
dsn: 'your-dsn',
environment: isDev ? 'development' : 'production',
release: app.getVersion(),
// 采样率控制
sampleRate: 1.0,
// 可以忽略一些非关键错误
ignoreErrors: ['ResizeObserver loop limit exceeded'],
});
然后在整个应用的任何地方,你可以使用 Sentry.captureMessage('Something went wrong', 'warning') 来上报自定义事件,或者通过 Sentry.captureException(error) 上报异常。
上报策略需要考虑的几个真实问题:
- 网络限制:应用可能处于离线环境,上报请求失败不能阻塞或导致错误循环。应采用静默丢弃或本地缓冲重试机制。
- 隐私合规:日志可能包含用户文件路径、系统信息甚至个人数据。上报前务必做脱敏处理(如截断路径、过滤敏感字段),并在隐私政策中明确说明。
- 频率控制:某个错误如果触发频率极高(例如 1 秒内 100 次),应该本地聚合后再上报,避免对服务器造成压力或产生巨额费用。
一个折中的实用方案是:先用 electron-log 全量记录到本地文件,然后使用一个独立的 log.transport 只上报 error 和 warn,同时在上报侧加一个简单的内存计数器,60 秒内相同错误消息只上报一次。这样兼顾了问题发现的及时性与网络资源的节省。
通过以上配置,你的 Electron 应用就拥有了一套有层次、不撑爆磁盘、且能主动将关键错误告知开发者的日志体系。接下来你可以在应用的任何地方安心地使用 log.info、log.error,而无需担心这些日志会成为日后维护的负担。