NodeJS日志记录(Electron项目)

普通日志收集

主进程

log4js

下载依赖

1
npm install log4js --save

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const log4js = require('log4js');
log4js.configure({
appenders: {
myLogFile: {
type: "dateFile",
filename: './logs/mylog',//您要写入日志文件的路径及文件名前缀
pattern: "yyyy-MM-dd-hh.log",//(可选,默认为.yyyy-MM-dd) - 文件名后缀。格式:.yyyy-MM-dd-hh:mm:ss.log
alwaysIncludePattern: true,//(默认为false) - 将模式包含在当前日志文件的名称以及备份中
compress: false,//(默认为false) - 在滚动期间压缩备份文件(备份文件将具有.gz扩展名)
encoding: 'utf-8',//default "utf-8",文件的编码
maxLogSize: 1024 * 1024 // 1M 文件最大存储空间,当文件内容超过文件存储空间会自动生成一个文件xxx.log.1的序列自增长的文件
},
myLogConsole: {
type: 'console'
}
},
categories: {
default: {
appenders: ['myLogFile', 'myLogConsole'],
level: log4js.levels.ALL
},
myLogFile: {
appenders: ['myLogFile'],
level: log4js.levels.ALL
},
myLogConsole: {
appenders: ['myLogConsole'],
level: log4js.levels.ALL
}
}
});
let log = log4js.getLogger('myLogFile');
module.exports.logger = log;

开发时在Console窗口中显示日志

1
log4js.getLogger('myLogConsole');

正式部署后改为

1
log4js.getLogger('myLogFile');

使用

1
2
const logger = require('./assets/js/MyLog').logger;
logger.info("app init");

logger对象暴露到全局(main.js)

1
global.zlog = logger;

注意

使用Electron和Webpack结合的时候,建议日志在main.js中引用,页面中通过ipc方式进行调用。

工具类MyLog.js我是配置在assets/js/MyLog.js

package.json中配置build下的files配置。

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"name": "xhlive",
"productName": "我的博客",
"version": "1.3.6",
"description": "",
"main": "main.js",
"build": {
"asar": true,
"files": [
"build/*",
"main.js",
"*.html",
"image",
"assets",
"app.ico",
"node_modules/**/*"
],
"appId": "cn.psvmc.myblog",
"win": {
"icon": "app.ico",
"target": [
"zip"
]
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "app.ico",
"uninstallerIcon": "app.ico",
"installerHeaderIcon": "app.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"license": "LICENSE.txt"
},
"extraResources": [
],
"mac": {
"hardenedRuntime": false
}
},
"scripts": {
"start": "webpack --mode development && cross-env ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
"webpack": "webpack --mode development",
"dist": "webpack --mode development && electron-builder --win --ia32",
"dist_dir": "webpack --mode development && electron-builder --dir --win --ia32"
},
"devDependencies": {
},
"dependencies": {
}
}

Electron-log(推荐)

Electron-log日志记录工具

首先我们安装依赖:

1
npm i electron-log --save

在项目里面引入依赖项:

1
2
3
4
5
6
7
8
9
const zlog = require('electron-log');
let filepath = path.join(app.getPath("documents"), 'xhlive/logs/');
let nowdate = new Date();
let nowdate_str = nowdate.getFullYear() + "_" + (nowdate.getMonth() + 1) + "_" + nowdate.getDate() + "_" + nowdate.getHours();
let filename = "mylog_" + nowdate_str + ".log";
zlog.transports.file.resolvePath = () => path.join(filepath, filename);
zlog.transports.file.level = true;
zlog.transports.console.level = false;
global.zlog = zlog;

然后在我们的主线程加入以下代码:

1
2
3
zlog.info('这是个提示日志');
zlog.warn('这是个警告日志');
zlog.error('这是个错误日志');

electron-log supports the following log levels:

1
error, warn, info, verbose, debug, silly

以上代码通过不同级别记录日志,默认情况下会在控制台打印出和保存到本地文件,

日志默认保存在app.getPath('userData')目录下的log.log文件中,

这个时候你会发现日志的时间和日志级别,日志内容都记录下来了,有这些信息我们就可以更好的跟踪bug等信息了。当然这个依赖不止这些功能:

我们可以设置log路径和文件名:

1
zlog.transports.file.resolvePath = () => path.join(filepath, filename);

我们可以格式化日志内容:

1
zlog.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'

也可以通过log.transports.file.levellog.transports.console.level来分别设置日志输出目标和日志输出等级。

如果想禁止Console中输出可以设置对应项为false

1
2
zlog.transports.file.level = true;
zlog.transports.console.level = false;

虽然有了这些日志信息,但是都在不同用户的电脑上,我们可以开发一个程序,在适当的时候把用户日志传送到web服务器上去,这样让我们更好的跟踪!

渲染进程

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const remote = window.require("electron").remote;
const app = remote.app;
let zlog = remote.getGlobal('zlog');
const isDevelopment = !app.isPackaged;
window.zlog = zlog;

function get_log_func(m_args) {
let temp_arr = [];
for (let m_arg of m_args) {
if (m_arg) {
if (m_arg instanceof Object) {
temp_arr.push(JSON.stringify(m_arg));
} else {
temp_arr.push(m_arg);
}
}
}
return temp_arr.join(" | ");
}

let logger = {
log: function (...args) {
if (zlog) {
zlog.info(get_log_func(args));
}
if (isDevelopment) {
console.log(...args)
}
},
info: function (...args) {
if (zlog) {
zlog.info(get_log_func(args));
}
if (isDevelopment) {
console.info(...args)
}
},
warn: function (...args) {
if (zlog) {
zlog.warn(get_log_func(args));
}
if (isDevelopment) {
console.warn(...args)
}
},
error: function (...args) {
if (zlog) {
zlog.error(get_log_func(args));
}
if (isDevelopment) {
console.error(...args)
}
},
}

export {logger}

页面中引用

1
2
import {logger} from "./assets/js/mylog";
logger.info("登录初始化");

主进程网络日志

Electron有主进程和渲染进程,一般呢我们通过渲染进程的控制台network就可以看到程序发起的网络请求。但是使用这个方法呢会有三个问题:

  1. 无法监控主进程发起的网络请求;

  2. Electron呢可能会有多个窗口,不同的窗口发起不同的请求可能存在关联关系,就需要联合监控,就非常麻烦。

  3. 无法精确的分析某个时段的网络请求。

为了弥补这方面的不足,Electron提供了netLog模块,允许开发人员通过编程的方式记录网络请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { remote } = require('electron') 
let filepath = path.join(app.getPath("documents"), 'xhlive/logs/');
let nowdate = new Date();
let nowdate_str = nowdate.getFullYear() + "_" + (nowdate.getMonth() + 1) + "_" + nowdate.getDate() + "_" + nowdate.getHours();
let filename = "mynet_" + nowdate_str + ".log";
await remote.netLog.startLogging(path.join(filepath, filename));
let ses = remote.getCurrentWebContents().session;
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.baidu.com');
xhr.onload = async () => {
log.info(xhr.responseText);
await remote.netLog.stopLogging()
}
xhr.send()

netLog模块是一个主进程模块,所以我们需要通过remote来使用他,

它的startLogging接收两个参数,第一个参数是日志文件记录的路径,也可以通过app.getPath('userData')来指定路径,第二个参数是一个配置对象,具体参考文档。

接着我们发起一个网络请求,得到响应后,我们通过stopLogging()来停止网络监控。

tips: 低版本的Electron可以使用以下方法:

app.commandLine.appendSwitch('log-net-log', 'net-log'),net-log为文件名称,可以自定义,文件会保存在项目根目录下,

也可以根据app.commandLine.hasSwitch('log-net-log')查看网络日志开关是否被打开,返回truefalse

崩溃日志收集

官方文档:https://www.electronjs.org/docs/api/crash-reporter#crashreporter

1
2
3
4
5
6
7
8
9
10
const {crashReporter} = require('electron')

crashReporter.start({
productName:'myblog',
companyName:'psvmc',
submitURL:'https://www.psvmc.cn',
uploadToServer:false
})

console.info(crashReporter.getCrashesDirectory());

可以调用以下方法模拟崩溃

1
process.crash()

官方说可以设置崩溃日志的目录,但是我这里设置无效

1
2
3
let logpath = path.resolve(app.getPath("documents") + '/myblog/logs/');
console.info(logpath);
app.setPath('crashDumps', logpath)

官方说的设置uploadToServer:false,就不需要设置submitURL,但是实际测试并非如此。

另外这种方法生成的错误日志也没法通过文本文档查看,所以我就没有使用。

但是我们可以监听事件child-process-gone

https://www.electronjs.org/docs/api/app#%E4%BA%8B%E4%BB%B6-render-process-gone

代码

1
2
3
4
5
const { app } = require('electron')

app.on('child-process-gone', (event, details) => {
console.log(details);
})

崩毁重启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { BrowserWindow, app, dialog} from 'electron';

const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed', () => {
const options = {
type: 'error',
title: '进程崩溃了',
message: '这个进程已经崩溃.',
buttons: ['重载', '退出'],
};
recordCrash().then(() => {
dialog.showMessageBox(options, (index) => {
if (index === 0) reloadWindow(mainWindow);
else app.quit();
});
}).catch((e) => {
console.log('err', e);
});
})

function recordCrash() {
return new Promise(resolve => {
// 崩溃日志请求成功....
resolve();
})
}

function reloadWindow(mainWin) {
if (mainWin.isDestroyed()) {
app.relaunch();
app.exit(0);
} else {
BrowserWindow.getAllWindows().forEach((w) => {
if (w.id !== mainWin.id) w.destroy();
});
mainWin.reload();
}
}