一般我们上传日志里面的错误都应该是经过混淆的,这样无法直观的看到错误栈的信息,故有此次需求。
前景提要:
react-native代码混淆后上报错误日志无法看到错误日志的错误栈信息,对我们根据日志进行错误定位有一定影响,考虑一下几个处理手段:
- 不混淆react native的bundle文件,看是否能正确定位
- 根据sourceMap翻译错误栈内容
RN打包后的bundle文件即使不混淆也是一整个文件,无法按照源码的位置进行错误描述。故使用sourceMap进行翻译,考虑后将其做到服务端进行翻译。
服务端配置:
node的express服务,利用pm2集群模式多开进程增强并发访问处理。
错误栈解析代码如下:
const fs = require("fs")
const sourceMap = require('source-map');
const _ = require("lodash")
var LruCache = require("lru-cache")
const {HttpCore} = require("http-core")
const path = require('path')
const sourceMapPath = "../../sourcemap";
let sourceMapCache = new LruCache(20);
let http = new HttpCore({
withCredentials: false,
timeout: 30000
});
function getFileNameByUrl(url) {
return url.replace(/[^a-z0-9.]+|\./gi, "_")+'.bundle.map'
}
function getSourceMapObjByFileName(fileName) {
return new Promise((resolve, reject) => {
let filePath = path.join(__dirname, sourceMapPath, fileName);
try {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
reject(err)
}
resolve(new sourceMap.SourceMapConsumer(JSON.parse(data)))
});
} catch(err) {
reject(err);
}
})
}
function getSourceMapObjByUrl(url) {
return http.get(url, {}).then(data => {
let fileName = getFileNameByUrl(url);
let filePath = path.join(__dirname, sourceMapPath, fileName);
try {
let fileContent = JSON.stringify(data)
fs.writeFile(filePath, fileContent, (err) => {
err && console.log("getSourceMapObjByUrl error", err)
});
} catch(err) {
console.log("getSourceMapObjByUrl JSON stringify error", err)
}
return new sourceMap.SourceMapConsumer(data)
}).catch(err => {
throw(err)
})
}
function getRealStackLine(sourceMapObj, stackLine) {
// 期望格式: filepath:line:column
let stackLineBlockArr = stackLine.split(":");
// 符合期望格式
if (stackLineBlockArr.length >= 3 && _.isNumber(Number(stackLineBlockArr[1])) && _.isNumber(Number(stackLineBlockArr[2]))) {
let {source, line, column, name} = sourceMapObj.originalPositionFor({
line: Number(stackLineBlockArr[1]),
column: Number(stackLineBlockArr[2])
});
if (source && line && column) {
return [source, name, line, column].join(":")
} else {
return stackLine;
}
} else {
return stackLine;
}
}
function getRealStack(sourceMapObj, stack) {
let stackArr = stack.split("\n");
return stackArr.map(stackLine => getRealStackLine(sourceMapObj, stackLine)).join("\n");
}
function errorStackParser(url, log) {
let fileName = getFileNameByUrl(url);
let sourceMapObj = sourceMapCache.get(fileName);
return new Promise(resolve => {
// 如果缓存存在
if (sourceMapObj) {
resolve(sourceMapObj);
// 如果存在本地文件
} else if (fs.existsSync(path.join(__dirname, sourceMapPath, fileName))) {
resolve(getSourceMapObjByFileName(fileName))
// 如果只能从url获取
} else {
resolve(getSourceMapObjByUrl(url))
}
}).then((sourceMapObj) => {
// 更新缓存
sourceMapCache.set(fileName, sourceMapObj)
// 更新错误栈
log.data.stack = getRealStack(sourceMapObj, log.data.stack);
return log;
}).catch(err => {
// 如果发生错误,则返回原本的log日志,保证日志记录无误
console.log("errorStackParser error", err);
return log;
})
}
module.exports = errorStackParser;
为了避免频繁的读磁盘,使用内存LRU缓存来sourceMapObj对象的存储。但是这样做有个问题,在pm2多进程的情况下会导致每个进程中都有缓存,造成内存的极大浪费。(这里我们的sourceMap文件有10M大小)。尝试过使用redis作为缓存数据库存储sourceMap对象,但发现在多并发的情况下内存持续增长(rss持续增长,heaptotal和heapUsed微量增长)。遂进行了一次内存监控。以下:
这里有两个工具推荐一下:memeye
、heapdump
。
起初我利用memeye
进行单个进程的内存监控,发现服务在接收到错误日志的时候内存会有一次暴增,如下图:
PS: memeye的链接,真的很方便!
这里有一点让我很在意,明明heaptotal增长很快就下来了,但是rss却持续居高不下,不明白为什么会导致rss过高。
通过pm2在本地多开进程发现,多进程与单进程内存情况一致。
通过heapdump进行各执行步骤的内存打点,将其载入chrome进行分析发现,有大量的buffer分配且占用过高(在sourcemap库的解析中),了解到rss暴涨的部分属于V8的堆外内存。
后通过查看sourcemap源码和了解sourcemap的解析规则发现需要位运算所以会有大量的buffer存在。
这里采用的因多进程导致内存过高处理办法是利用pm2新开一个进程(不同端口),然后将其他日志服务进程的错误日志重定向到该进程服务,这样单独一个进程处理错误日志。