Electron 如何存储 log?
· 5 min read
Electron 日志的三个核心决策点:存哪、怎么切、怎么查。
- 存哪:主进程写
app.getPath('userData')/logs,渲染进程通过 IPC 转交,不要直接写文件系统 - 怎么切:按大小滚动 + 按天滚动,绝对不要单文件无上限写入
- 怎么查:开发环境直接
tail,生产环境用electron-log的文件路径 + 崩溃堆栈上报 - 级别分级:error/warn/info 写文件,debug/trace 仅本地 console
- 崩溃捕获:JS 异常走
uncaughtException,native 崩溃走crashReporter,两条路不能混 - 敏感信息:token / cookie / 密码进日志前必须
redact()脱敏
Electron 项目的日志不是"打几个 console.log 就完事"的事。日志写错地方会丢,写太快会爆,写太多会吃满磁盘。这一篇把 Electron 日志的工程实践一次讲透。
一、日志该存在哪?
Electron 的两条进程有不同的文件系统权限,日志必须由主进程统一管理。
| 进程 | 推荐做法 | 原因 |
|---|---|---|
| main 进程 | 直接 fs.appendFile 到 app.getPath('userData')/logs/ | 有完整 Node.js 权限,可控 |
| renderer 进程 | 通过 IPC 把日志消息转发给 main,不直接落盘 | 沙箱里 fs 不可用或被禁用 |
app.getPath('userData') 是 OS 推荐的写入位置:
| 平台 | 路径 |
|---|---|
| Windows | %APPDATA%/<appName> |
| macOS | ~/Library/Application Support/<appName> |
| Linux | ~/.config/<appName> |
// 主进程
import { app } from 'electron'
import * as path from 'path'
import * as fs from 'fs'
const logDir = path.join(app.getPath('userData'), 'logs')
fs.mkdirSync(logDir, { recursive: true })
function writeLog(level: string, msg: string) {
const file = path.join(logDir, `${new Date().toISOString().slice(0, 10)}.log`)
fs.appendFileSync(file, `[${level}] ${new Date().toISOString()} ${msg}\n`)
}
绝不要往
process.cwd()、__dirname这种"看起来合理"的位置写日志——打包后__dirname是app.asar内部,只读;cwd在不同启动方式下指向不同位置(自动启动 vs 双击图标 vs 命令行启动)。
二、日志怎么切?
单文件无上限写入 = 定时炸弹。用户用一年,单个 log 文件轻松上 GB,编辑器打不开、tail 直接卡死。
推荐组合:按天滚动 + 按大小二次切割。
// 简单实现
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
function writeLog(level: string, msg: string) {
const day = new Date().toISOString().slice(0, 10)
const base = path.join(logDir, `${day}.log`)
const stat = fs.existsSync(base) ? fs.statSync(base) : { size: 0 }
if (stat.size > MAX_FILE_SIZE) {
// 滚到 .1.log,再写新的 .log
fs.renameSync(base, path.join(logDir, `${day}.1.log`))
}
fs.appendFileSync(base, `[${level}] ${new Date().toISOString()} ${msg}\n`)
}
实际项目用 electron-log 一行搞定:
import log from 'electron-log/main'
log.transports.file.maxSize = 10 * 1024 * 1024 // 10 MB
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
log.transports.file.resolvePathFn = () =>
path.join(app.getPath('userData'), 'logs', 'app.log')
log.info('main process started')
三、日志级别和敏感信息
生产环境日志该有的级别:
| 级别 | 用途 | 生产默认 |
|---|---|---|
error | 异常、崩溃 | 写文件 |
warn | 降级、过期 API 提示 | 写文件 |
info | 启动、配置、关键业务流 | 写文件 |
debug | 详细调试 | 不写文件 |
trace | 每一步执行 | 不写文件 |
敏感信息必须脱敏
日志里出现过的 token / cookie / 用户密码,等于把钥匙贴在公告栏。
function redact(msg: string): string {
return msg
.replace(/sk-[a-zA-Z0-9_-]{10,}/g, '<REDACTED>')
.replace(/Bearer\s+[A-Za-z0-9._-]+/g, 'Bearer <REDACTED>')
.replace(/([?&])(token|key|secret)=[^&]+/g, '$1$2=<REDACTED>')
}
四、崩溃日志
Electron 崩溃有两种,处理方式完全不同。
// 1. JS 异常 —— 主进程能 catch
process.on('uncaughtException', (err) => {
log.error('uncaughtException', err.stack)
})
process.on('unhandledRejection', (reason) => {
log.error('unhandledRejection', reason)
})
// 2. 渲染进程崩溃 —— 主进程要主动监听
app.on('render-process-gone', (event, webContents, details) => {
log.error('render-process-gone', details)
})
// 3. 整个 app 崩溃(GPU 进程挂掉等)—— 用 crashReporter
import { crashReporter } from 'electron'
crashReporter.start({ uploadToServer: false }) // 本地存堆栈
崩溃堆栈默认在 app.getPath('crashDumps'),是 .dmp 二进制文件——普通文本编辑器打不开,但配合 electron-log 的 crashReporter.start 拿到的是 minidump,配合 symbol 上传到 Sentry 之类的服务才能符号化。
五、调试技巧
# macOS 看实时日志
tail -f "$HOME/Library/Application Support/<appName>/logs/2026-08-04.log"
# Windows
type "%APPDATA%\<appName>\logs\2026-08-04.log"
# Linux
tail -f "$HOME/.config/<appName>/logs/2026-08-04.log"
开发环境想看 renderer 进程的 console 输出?在主进程里加:
import log from 'electron-log/main'
log.initialize() // 接管 console
这样 renderer 里 console.log 的内容会同步进主进程的日志文件,不需要开 DevTools。
References
- electron-log 文档 —— 事实上的 Electron 日志标配
- Electron app 模块文档 ——
getPath('userData')/getPath('crashDumps')官方说明