企业级Electron应用异常监控平台搭建指南:从崩溃捕获到问题定位
引言
在企业级桌面应用开发中,异常监控是保障软件稳定性的关键环节。Electron作为跨平台桌面应用开发框架,其主进程与渲染进程分离的架构带来了独特的异常处理挑战。本文将详细介绍如何基于dromara/electron-egg框架构建完整的应用异常监控平台,涵盖日志系统配置、多进程异常捕获、崩溃报告生成和可视化监控面板实现等核心功能。
异常监控体系架构
异常监控平台需要覆盖Electron应用的全生命周期,包括主进程、渲染进程和预加载脚本的异常捕获。基于electron-egg框架的监控体系架构如下:
electron-egg框架已内置基础日志功能,通过electron/config/config.default.js配置文件可自定义日志行为。默认日志配置如下:
logger: {
level: 'INFO',
outputJSON: false,
appLogName: 'ee.log',
coreLogName: 'ee-core.log',
errorLogName: 'ee-error.log'
}
日志系统配置与优化
基础日志配置
electron-egg的日志系统由electron/config/config.default.js文件控制,默认将不同类型日志输出到独立文件:
- 应用日志:ee.log - 记录应用运行信息
- 核心日志:ee-core.log - 框架核心组件日志
- 错误日志:ee-error.log - 集中存储异常信息
要启用详细异常记录,需将日志级别调整为DEBUG:
// 修改配置文件 [electron/config/config.default.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/electron/config/config.default.js?utm_source=gitcode_repo_files)
logger: {
level: 'DEBUG', // 由INFO改为DEBUG
outputJSON: true, // 启用JSON格式便于日志解析
// 其他配置保持不变
}
自定义日志工具
创建专用异常记录工具,统一异常日志格式:
// 创建文件 electron/utils/errorMonitor.js
const { logger } = require('ee-core/log');
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
class ErrorMonitor {
constructor() {
this.errorLogPath = path.join(app.getPath('userData'), 'logs', 'ee-error.log');
}
/**
* 记录异常信息
* @param {Error} error - 错误对象
* @param {string} context - 错误发生上下文
*/
captureException(error, context = '') {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
context,
process: process.type, // 区分主进程/渲染进程
version: app.getVersion()
};
// 写入JSON格式日志
logger.error(JSON.stringify(errorInfo));
// 同时记录到专用错误文件
this.writeErrorReport(errorInfo);
}
/**
* 生成崩溃报告
*/
writeErrorReport(errorInfo) {
const reportPath = path.join(app.getPath('userData'), 'crash-reports',
`report-${Date.now()}.json`);
// 确保目录存在
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
// 写入报告文件
fs.writeFileSync(reportPath, JSON.stringify(errorInfo, null, 2));
}
}
module.exports = new ErrorMonitor();
多进程异常捕获实现
主进程异常捕获
Electron主进程异常包括未捕获的异常、未处理的Promise拒绝和进程崩溃事件。修改electron/main.js文件添加异常捕获逻辑:
// 修改 [electron/main.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/electron/main.js?utm_source=gitcode_repo_files)
const { app, BrowserWindow } = require('electron');
const errorMonitor = require('./utils/errorMonitor');
const { logger } = require('ee-core/log');
// 捕获未处理的异常
process.on('uncaughtException', (error) => {
logger.error('Main process uncaught exception:', error);
errorMonitor.captureException(error, '主进程未捕获异常');
// 可选:尝试重启应用
// app.relaunch();
// app.quit();
});
// 捕获未处理的Promise拒绝
process.on('unhandledRejection', (reason, promise) => {
logger.error('Main process unhandled rejection:', reason);
errorMonitor.captureException(reason, '主进程Promise拒绝');
});
// 窗口崩溃事件
app.on('render-process-gone', (event, webContents, details) => {
logger.error(`Render process gone: ${details.reason}`);
errorMonitor.captureException(
new Error(`渲染进程崩溃: ${details.reason}`),
`进程ID: ${webContents.id}, 原因: ${details.reason}`
);
// 重启窗口
if (details.reason === 'crashed') {
// 实现窗口重启逻辑
}
});
渲染进程异常捕获
前端Vue应用的异常需要通过预加载脚本建立与主进程的通信通道。修改electron/preload/bridge.js添加异常捕获API:
// 修改 [electron/preload/bridge.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/electron/preload/bridge.js?utm_source=gitcode_repo_files)
const { contextBridge, ipcRenderer } = require('electron');
const { logger } = require('ee-core/log');
// 暴露异常捕获API给渲染进程
contextBridge.exposeInMainWorld('errorMonitor', {
captureException: (error, context) => {
logger.error(`Render process error: ${error.message}`);
ipcRenderer.send('renderer-error', {
message: error.message,
stack: error.stack,
context
});
}
});
// 捕获window错误
window.addEventListener('error', (event) => {
event.preventDefault();
const error = {
message: event.error.message,
stack: event.error.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
};
logger.error(`Window error: ${JSON.stringify(error)}`);
ipcRenderer.send('renderer-error', {
...error,
context: 'window.onerror'
});
});
// 捕获未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
const reason = event.reason;
logger.error(`Unhandled rejection: ${reason.message || reason}`);
ipcRenderer.send('renderer-error', {
message: reason.message || reason.toString(),
stack: reason.stack,
context: 'unhandledrejection'
});
});
预加载脚本异常处理
预加载脚本作为连接主进程和渲染进程的桥梁,其异常处理尤为重要。修改electron/preload/lifecycle.js添加try/catch块:
// 修改 [electron/preload/lifecycle.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/electron/preload/lifecycle.js?utm_source=gitcode_repo_files)
const { logger } = require('ee-core/log');
const errorMonitor = require('../utils/errorMonitor');
class Lifecycle {
/**
* core app have been loaded
*/
async ready() {
try {
logger.info('[lifecycle] ready');
// 原有逻辑...
} catch (error) {
logger.error('[lifecycle] ready error:', error);
errorMonitor.captureException(error, 'lifecycle.ready');
}
}
/**
* electron app ready
*/
async electronAppReady() {
try {
logger.info('[lifecycle] electron-app-ready');
// 原有逻辑...
} catch (error) {
logger.error('[lifecycle] electronAppReady error:', error);
errorMonitor.captureException(error, 'lifecycle.electronAppReady');
}
}
// 其他方法类似添加try/catch...
}
module.exports = { Lifecycle };
异常监控面板实现
后端数据接口
创建异常监控控制器,提供日志数据API:
// 创建文件 electron/controller/monitorController.js
const { logger } = require('ee-core/log');
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
class MonitorController {
/**
* 获取错误日志列表
*/
async getErrorLogs() {
try {
const logDir = path.join(app.getPath('userData'), 'logs');
const errorLogPath = path.join(logDir, 'ee-error.log');
// 读取日志文件内容
const content = fs.readFileSync(errorLogPath, 'utf-8');
// 按行分割并解析JSON日志
const logs = content.split('\n')
.filter(line => line.trim())
.map(line => {
try {
return JSON.parse(line);
} catch (e) {
// 处理非JSON格式日志
return { message: line, timestamp: new Date().toISOString() };
}
})
.reverse(); // 最新的日志在前
return {
success: true,
data: logs.slice(0, 100) // 返回最近100条
};
} catch (error) {
logger.error('Failed to get error logs:', error);
return {
success: false,
message: error.message
};
}
}
/**
* 获取崩溃报告列表
*/
async getCrashReports() {
try {
const reportDir = path.join(app.getPath('userData'), 'crash-reports');
// 确保目录存在
if (!fs.existsSync(reportDir)) {
return { success: true, data: [] };
}
// 读取报告文件列表
const files = fs.readdirSync(reportDir)
.filter(file => file.endsWith('.json'))
.sort((a, b) => b.localeCompare(a)); // 按文件名倒序
// 读取报告内容
const reports = files.map(file => {
const content = fs.readFileSync(path.join(reportDir, file), 'utf-8');
return {
filename: file,
timestamp: file.replace('report-', '').replace('.json', ''),
data: JSON.parse(content)
};
});
return {
success: true,
data: reports
};
} catch (error) {
logger.error('Failed to get crash reports:', error);
return {
success: false,
message: error.message
};
}
}
}
module.exports = MonitorController;
前端监控面板
创建Vue组件实现异常监控可视化界面:
<!-- 创建文件 frontend/src/views/monitor/ErrorMonitor.vue -->
<template>
<div class="error-monitor-container">
<div class="header">
<h2>应用异常监控中心</h2>
<div class="stats">
<div class="stat-item">
<span class="stat-value">{{ errorCount }}</span>
<span class="stat-label">今日错误总数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ crashCount }}</span>
<span class="stat-label">崩溃次数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ processType }}</span>
<span class="stat-label">当前进程</span>
</div>
</div>
</div>
<div class="tabs">
<div class="tab" :class="{ active: activeTab === 'errors' }" @click="activeTab = 'errors'">
错误日志
</div>
<div class="tab" :class="{ active: activeTab === 'crashes' }" @click="activeTab = 'crashes'">
崩溃报告
</div>
<div class="tab" :class="{ active: activeTab === 'settings' }" @click="activeTab = 'settings'">
监控设置
</div>
</div>
<div class="tab-content">
<div v-if="activeTab === 'errors'">
<div class="filter-bar">
<input
type="text"
v-model="errorFilter"
placeholder="搜索错误信息..."
class="search-input"
>
<select v-model="errorLevel" class="filter-select">
<option value="all">所有级别</option>
<option value="ERROR">错误</option>
<option value="WARN">警告</option>
<option value="DEBUG">调试</option>
</select>
</div>
<div class="error-list">
<div
class="error-item"
v-for="(error, index) in filteredErrors"
:key="index"
:class="{ unread: !error.read }"
@click="showErrorDetail(error)"
>
<div class="error-header">
<span class="error-time">{{ formatTime(error.timestamp) }}</span>
<span class="error-level" :class="`level-${error.level || 'ERROR'}`">
{{ error.level || 'ERROR' }}
</span>
</div>
<div class="error-message">{{ error.message }}</div>
<div class="error-context">{{ error.context || '无上下文信息' }}</div>
</div>
</div>
</div>
<div v-if="activeTab === 'crashes'">
<div class="crash-list">
<div
class="crash-item"
v-for="(report, index) in crashReports"
:key="index"
@click="showCrashDetail(report)"
>
<div class="crash-header">
<span class="crash-time">{{ formatTimestamp(report.timestamp) }}</span>
<span class="crash-process">{{ report.data.process || '未知进程' }}</span>
</div>
<div class="crash-message">{{ report.data.message }}</div>
<div class="crash-version">应用版本: {{ report.data.version }}</div>
</div>
</div>
</div>
<div v-if="activeTab === 'settings'">
<div class="settings-form">
<div class="form-group">
<label>日志级别</label>
<select v-model="logLevel" class="form-control">
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARN">WARN</option>
<option value="ERROR">ERROR</option>
</select>
</div>
<div class="form-group">
<label>日志保留天数</label>
<input type="number" v-model="logRetentionDays" class="form-control" min="1" max="365">
</div>
<div class="form-group">
<label>自动发送错误报告</label>
<input type="checkbox" v-model="autoSendReports">
</div>
<button class="btn-save" @click="saveSettings">保存设置</button>
</div>
</div>
</div>
<!-- 错误详情弹窗 -->
<div class="modal" v-if="showErrorModal">
<div class="modal-content">
<div class="modal-header">
<h3>错误详情</h3>
<button class="close-btn" @click="showErrorModal = false">×</button>
</div>
<div class="modal-body">
<pre>{{ selectedError | stringify }}</pre>
</div>
<div class="modal-footer">
<button class="btn-close" @click="showErrorModal = false">关闭</button>
<button class="btn-save" @click="exportErrorReport">导出报告</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activeTab: 'errors',
errorFilter: '',
errorLevel: 'all',
errorCount: 0,
crashCount: 0,
processType: 'renderer',
errors: [],
crashReports: [],
showErrorModal: false,
selectedError: null,
logLevel: 'INFO',
logRetentionDays: 30,
autoSendReports: true
};
},
computed: {
filteredErrors() {
return this.errors.filter(error => {
const matchesFilter = error.message.toLowerCase().includes(this.errorFilter.toLowerCase()) ||
(error.stack && error.stack.toLowerCase().includes(this.errorFilter.toLowerCase()));
const matchesLevel = this.errorLevel === 'all' ||
(error.level && error.level === this.errorLevel);
return matchesFilter && matchesLevel;
});
}
},
filters: {
stringify(value) {
return JSON.stringify(value, null, 2);
}
},
async mounted() {
// 初始化时加载错误日志和崩溃报告
await this.loadErrorLogs();
await this.loadCrashReports();
// 设置定时刷新
this.refreshInterval = setInterval(async () => {
await this.loadErrorLogs();
await this.loadCrashReports();
}, 30000); // 每30秒刷新一次
// 获取当前进程类型
this.processType = window.process && window.process.type || 'renderer';
// 监听主进程发送的新错误事件
window.ipcRenderer.on('new-error', (event, error) => {
this.errors.unshift(error);
this.errorCount++;
});
},
beforeUnmount() {
clearInterval(this.refreshInterval);
window.ipcRenderer.removeAllListeners('new-error');
},
methods: {
async loadErrorLogs() {
try {
const result = await window.api.invoke('monitorController.getErrorLogs');
if (result.success) {
this.errors = result.data;
this.errorCount = result.data.length;
}
} catch (error) {
console.error('Failed to load error logs:', error);
}
},
async loadCrashReports() {
try {
const result = await window.api.invoke('monitorController.getCrashReports');
if (result.success) {
this.crashReports = result.data;
this.crashCount = result.data.length;
}
} catch (error) {
console.error('Failed to load crash reports:', error);
}
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString();
},
formatTimestamp(timestamp) {
if (!timestamp) return '';
const date = new Date(parseInt(timestamp));
return date.toLocaleString();
},
showErrorDetail(error) {
this.selectedError = error;
this.showErrorModal = true;
error.read = true;
},
async exportErrorReport() {
try {
await window.api.invoke('monitorController.exportErrorReport', this.selectedError);
alert('错误报告已导出');
} catch (error) {
console.error('Failed to export error report:', error);
alert('导出失败: ' + error.message);
}
},
async saveSettings() {
try {
await window.api.invoke('monitorController.saveSettings', {
logLevel: this.logLevel,
logRetentionDays: this.logRetentionDays,
autoSendReports: this.autoSendReports
});
alert('设置已保存');
} catch (error) {
console.error('Failed to save settings:', error);
alert('保存失败: ' + error.message);
}
}
}
};
</script>
<style scoped>
/* 样式内容省略 */
</style>
添加路由配置,使监控面板可访问:
// 修改 [frontend/src/router/routerMap.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/frontend/src/router/routerMap.js?utm_source=gitcode_repo_files)
export default [
// 原有路由...
{
path: '/monitor/error',
name: 'errorMonitor',
component: () => import('@/views/monitor/ErrorMonitor.vue'),
meta: {
title: '异常监控中心'
}
}
];
异常监控工作流程
完整的异常监控工作流程包括异常捕获、日志记录、报告生成和问题修复四个阶段:
当应用运行时,异常监控平台会自动捕获各类错误并展示在监控面板中:
高级功能:远程错误上报
对于企业级应用,通常需要将错误报告发送到中央服务器进行集中分析。实现远程上报功能:
// 修改 electron/utils/errorMonitor.js
const { ipcRenderer } = require('electron');
const { getConfig } = require('ee-core/config');
class ErrorMonitor {
// ... 原有代码 ...
/**
* 发送错误报告到远程服务器
*/
async sendErrorToServer(errorInfo) {
const config = getConfig();
// 检查是否启用远程上报
if (!config.remote.enable || !config.remote.url) {
logger.info('Remote error reporting is not enabled');
return false;
}
try {
const response = await fetch(`${config.remote.url}/api/errors/report`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-App-Version': app.getVersion()
},
body: JSON.stringify(errorInfo),
timeout: 5000
});
if (response.ok) {
logger.info('Error report sent successfully');
return true;
} else {
logger.error(`Failed to send error report: ${response.statusText}`);
return false;
}
} catch (error) {
logger.error('Network error while sending report:', error);
return false;
}
}
}
module.exports = new ErrorMonitor();
配置远程上报服务器地址:
// 修改 [electron/config/config.default.js](https://gitcode.com/dromara/electron-egg/blob/3a1f5bc42c936f42ffcb693e327005e65ec13c3e/electron/config/config.default.js?utm_source=gitcode_repo_files)
remote: {
enable: true, // 启用远程上报
url: 'https://your-error-monitor-server.com' // 替换为实际服务器地址
}
总结与最佳实践
异常监控最佳实践
- 全面覆盖:确保监控所有进程(主进程、渲染进程、预加载脚本)
- 分级处理:根据错误严重程度采取不同措施(记录、弹窗、重启)
- 详细日志:异常日志应包含时间戳、上下文、堆栈信息和版本号
- 用户体验:避免因异常监控影响应用性能,错误报告应异步发送
- 安全考虑:确保错误报告不包含敏感信息
常见问题解决方案
| 问题场景 | 解决方案 | 相关代码 |
|---|---|---|
| 渲染进程白屏 | 监控window-ready事件超时,实现自动刷新 | electron/preload/lifecycle.js |
| 主进程崩溃 | 使用electron-crash-reporter捕获崩溃转储 | electron/main.js |
| 日志文件过大 | 实现日志轮转,按大小或日期分割 | electron/config/config.default.js |
| 网络错误上报失败 | 实现本地缓存和重试机制 | electron/utils/errorMonitor.js |
通过本文介绍的方法,开发者可以基于electron-egg框架构建专业的应用异常监控平台,有效提升应用稳定性和用户体验。完整实现了从异常捕获、日志记录到问题定位的全流程解决方案,满足企业级桌面应用的监控需求。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




