概述
对于任何的客户端应用,开发者都希望能够在用户上的手上记录下相关信息以便了解真实的使用情况。
一般情况下,分为以下两种信息:
-
正常日志
在不涉及隐私的情况下,让开发者了解用户使用客户端的详细情况,从这些情况中提炼的信息能够让开发者根据用户的使用情况更好地优化产品
-
崩溃日志
用户的使用环境千差万别,有时候可能会让客户端崩溃。崩溃日志的收集,有利于让开发人员更好地定位,解决问题
对于日志的处理,一般分为 收集 、上报、分析等多个步骤,本文主要细分讲述 Electron 客户端应用的日志收集步骤
正常日志收集
流程
正常日志,可以理解成能够被开发者应用代码正常捕获的行为日志
按照 Electron 应用的结构,日志收集的结构情况如下图:
解释:
-
main process 为主进程,render processes 为各个窗口的渲染进程
-
logger 为 main process 下的日志收集组件,可以把收集到的日志信息保存到本地 和 上报到远程服务器
-
Electron.remote 是 Electron 框架自带的对象,可以作为主进程与渲染进程的桥接,让渲染进程能够访问到主进程的 logger 组件,底层原理是基于 ipc 信息的封装
-
render processes 通过 Electron.remote 访问到 logger,然后就可以把自己的日志信息通过 logger 进行收集
-
渲染进程中的 worker 和 service worker 比较特别,它们是独立于渲染进程,无法直接从 Electron.remote 中访问 logger,但是它们可以通过 ipc message 把日志信息传递到特定的渲染进程,然后再通过渲染进程通过 logger 记录日志信息
-
主进程下的子进程 child processes,无法直接访问 Electron.remote,通过 ipc message,可以把收集的日志信息传递给主进程,主进程再通过 logger 记录日志信息
方案
-
针对应用的需求,设置好 logger 组件
/** * @param {string} level * @param {string} text */ function log(level, text) { var args = Array.prototype.slice.call(arguments, 1); args = args.map(function(arg) { return arg instanceof Error ? arg.stack + EOL : arg; }); text = util.format.apply(util, args); var msg = { level: level, text: text, date: new Date() }; var transports = module.exports.transports; for (var i in transports) { // jshint -W089 if (!transports.hasOwnProperty(i) || typeof transports[i] !== 'function') { continue; } if (!compareLevels(transports[i].level, level)) { continue; } try{ transports[i].call(module.exports, msg); }catch(err){ console.error('Logger Error: ', err); } } }
-
把 logger 对象暴露到全局
global.log = require('./src/base/log');
-
主进程记录日志
// GPU进程崩溃 app.on('gpu-process-crashed', function(){ log.error('GPU进程崩溃,程序退出'); app.exit(0); }); // 当所有窗口被关闭了,退出 app.on('window-all-closed', function() { // 在 OS X 上,通常用户在明确地按下 Cmd + Q 之前 // 应用会保持活动状态 if (process.platform != 'darwin') { app.quit(); } });
-
渲染进程记录日志
通过 remote 获取 logger 对象
window.logger = remote.getGlobal('log');
在渲染进程上,把 logger 的方法与 console 方法进行绑定
function extendConsole(){ const logFn = console.log; console.log = function(...args){ logFn(args); if(logger && logger.log){ logger.info(args); } } // ... }
-
service worker 通过 ipc message 通讯记录日志
向接管了Service Worker的渲染进程传输日志信息
//service worker function sendMsg(type, ...args){ self.clients.matchAll().then( clientList => { clientList.forEach(client => { client.postMessage({ event: type, data: args }); }) } ) } //目标渲染进程 navigator.serviceWorker.addEventListener('message', function(swe){ // log swe.data });
崩溃日志收集
流程
崩溃日志,可以理解为不受开发人员应用所能控制的崩溃
开发人员的应用无法直接捕获这种崩溃错误,需要借助框架和操作系统底层的收集机制进行日志的收集
Electron 已经提供了崩溃日志的收集机制,详情可以查看 Electron 的 官方文档:崩溃日志
大致流程如下图:
解释:
-
crash reporter 为 Electron 提供的收集机制,它可以收集应用程序无法捕获的崩溃错误,收集后,可以把信息保存到本地或者上传到指定服务器
-
主进程 与 渲染进程 的崩溃错误都可以被正确收集
-
注意点:任何进程,想要能够被收集崩溃信息,都必须在进程上显式调用 crashReporter.start 方法,以初始化收集处理
-
主进程可以直接访问 crashReporter,渲染进程可以通过 Electron.remote 访问 crashReporter,子进程可以通过 process.crashReporter 访问崩溃收集对象
方案
-
每个进程都尽可能添加上崩溃收集;
-
崩溃收集的信息统一上报到服务器上,方便开发人员收集统计;
如下图(每份崩溃报告都有一个对应的 json文件 与 对应的 minidump文件):
-
获取到对应的崩溃文件后,需要对 dump 文件进行分析,推荐工具为 google 的 breakpad。关于 breakpad,不同平台有不同的安装方法,需要各位看官通过官方文档仔细安装(mac 要用 xcode 进行编译,windows需要额外安装 gcc)
-
假设已经安装好了 breakpad,那么我们会有两个工具:dump_syms 和 minidump_stackwalk,在这里,我们关键会用到 minidump_stackwalk这个工具
通过如下命令行,把 minidump 文件解析并存储结果到 output.txt 中
minidump_stackwalk 15dcad6faa9e9914ae9016d794c391a8 > ./output.txt
结果如下,通过输出的详细信息,开发人员就能更好解决问题~