Skip to main content

Electron 如何存储 log?

· 5 min read

Electron 日志的个核心决策点:存哪、怎么切、怎么查。

  1. 存哪:主进程写 app.getPath('userData')/logs,渲染进程通过 IPC 转交,不要直接写文件系统
  2. 怎么切:按大小滚动 + 按天滚动,绝对不要单文件无上限写入
  3. 怎么查:开发环境直接 tail,生产环境用 electron-log 的文件路径 + 崩溃堆栈上报
  4. 级别分级error/warn/info 写文件,debug/trace 仅本地 console
  5. 崩溃捕获:JS 异常走 uncaughtExceptionnative 崩溃crashReporter,两条路不能混
  6. 敏感信息token / cookie / 密码进日志前必须 redact() 脱敏

Electron 项目的日志不是"打几个 console.log 就完事"的事。日志写错地方会丢,写太快会爆,写太多会吃满磁盘。这一篇把 Electron 日志的工程实践一次讲透。

一、日志该存在哪?

Electron 的两条进程有不同的文件系统权限,日志必须由主进程统一管理

进程推荐做法原因
main 进程直接 fs.appendFileapp.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 这种"看起来合理"的位置写日志——打包后 __dirnameapp.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-logcrashReporter.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

  1. electron-log 文档 —— 事实上的 Electron 日志标配
  2. Electron app 模块文档 —— getPath('userData') / getPath('crashDumps') 官方说明