日志服务利用sourceMap作错误栈解析

一般我们上传日志里面的错误都应该是经过混淆的,这样无法直观的看到错误栈的信息,故有此次需求。
前景提要:
react-native代码混淆后上报错误日志无法看到错误日志的错误栈信息,对我们根据日志进行错误定位有一定影响,考虑一下几个处理手段:

  1. 不混淆react native的bundle文件,看是否能正确定位
  2. 根据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微量增长)。遂进行了一次内存监控。以下:

这里有两个工具推荐一下:memeyeheapdump
起初我利用memeye进行单个进程的内存监控,发现服务在接收到错误日志的时候内存会有一次暴增,如下图:

日志服务利用sourceMap作错误栈解析_第1张图片
初次接收到错误日志

PS: memeye的链接,真的很方便!

这里有一点让我很在意,明明heaptotal增长很快就下来了,但是rss却持续居高不下,不明白为什么会导致rss过高。

通过pm2在本地多开进程发现,多进程与单进程内存情况一致。

通过heapdump进行各执行步骤的内存打点,将其载入chrome进行分析发现,有大量的buffer分配且占用过高(在sourcemap库的解析中),了解到rss暴涨的部分属于V8的堆外内存。


日志服务利用sourceMap作错误栈解析_第2张图片
sourcemap的内存占用

后通过查看sourcemap源码和了解sourcemap的解析规则发现需要位运算所以会有大量的buffer存在。

这里采用的因多进程导致内存过高处理办法是利用pm2新开一个进程(不同端口),然后将其他日志服务进程的错误日志重定向到该进程服务,这样单独一个进程处理错误日志。

你可能感兴趣的:(日志服务利用sourceMap作错误栈解析)