【前端监控】离线日志

2021-09-09 15:02:28 浏览数 (1)

前端监控系列,SDK,服务、存储 ,会全部总结一遍,写文不易,点个赞吧

离线日志,一般指的是用户离线时产生的日志。

离线日志的作用主要有两点

第一,保证日志完整性。用户没有网络的时候,日志数据无法上传,为了防止日志丢失,会在用户端存一份离线日志数据,等待网络恢复的时候,重新上传。

第二,优化上报日志过多。每个页面有很多上报点,如果用户基数大,将会产生海量数据,存储爆炸。所以我们会对日志做等级的划分,某个等级之上的数据,会自动上传,其余的存到本地,用户反馈问题的时候,引导用户上传。也避免了一些非必要性的数据上报,比如代码的调试信息等

所以可以看出来,离线日志其实就是在用户端存一份数据,需要的时候再上传。

本文分4部分

1、基本思路

2、api简介

3、具体处理

4、代码仓库

基本思路

最简化的说法就是,监控的数据存在本地

当然不是一股脑存了,也是有条件的。

1、上报失败的时候,把监控的数据存在本地,用于后续重试上报

2、用户离线 or 服务不稳定。减少频繁上报

3、上报等级不高的数据,会存在本地,提供方法供用户手动上传,定位更加细致的问题。

而存在本地的数据,什么时候会读取?

1、每次上报数据的时候,会顺便读取本地数据,如果有数据,就带上并上报

2、收到用户反馈的时候,引导用户上传,把本地日志打包成 zip 并上传,以便开发下载排查日志

自动上传的大致的流程图如下

用户上传的流程如下

API 简介

在上面中,大概两个主要操作

1、存数据

2、打包数据成 zip

存数据使用 indexDB,而 打包数据成zip,我们则会使用 JSZip 库

下面就来简单介绍下这两个东西

1indexDB

浏览器提供的本地数据库,H5的新特性。

可能没怎么用过,但是一定有了解

为什么用它,简单说一下它的主要特性

1、键值对存储。可以直接存一个js对象,数据都有一个独一无二的key,根据这个key就能拿到对应的value

2、异步操作。存取操作都是异步的,不会锁死浏览器,利于大数据读写,而 localstorage 则是同步的

3、同源策略。网页只能访问自身域名下的indexdb,无法跨域访问

4、存储空间大。一般有 250MB 以上,localstorage 只有5MB

下面我们来简单说下 indexDB 的使用

其实把 indexdb 介绍完都能写一篇文章了,因为知识点实在挺多的,所以本文只会简单讲使用到的 ,详细了解看MDN 吧

https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

我们按步骤来说使用到的 方法

1、连接数据库。有则连接,没有则新建,返回一个实例,用于监听事件

代码语言:javascript复制
const dbRequest = window.indexedDB.open(
  "LogDB" /* 数据库名*/, 
  1 /*数据库版本号*/
)

2、创建表。一般叫做对象仓库。

需要在上面返回的实例中监听 onupgradeneeded 事件,该事件只有在新建数据库的时候才会触发

代码语言:javascript复制
dbRequest.onupgradeneeded = (e) => {
  const db = e.target.result;
  const objectStore = db.createObjectStore("logs" /* 表名*/, {
    autoIncrement: true, // 自动生成唯一key
  });
};

3、创建索引

为我们的数据多创建索引,就能有多种方式查找数据,类似于增加搜索条件。

比如我存了很多条对象数据

代码语言:javascript复制
{project:"a",time:1}
{project:"b",time:1}
{project:"c",time:1}
{project:"d",time:1}

其中存入数据库的时候,他们每条数据都会有一个唯一 key

没有索引,我们只有唯一一种方式根据key 可以找到他们

但是我们想通过里面的值去找到他们,比如查找 project 是 a 的数据

就需要把 project 这个 keyName 设置为索引,我们才能查找到

代码语言:javascript复制
dbRequest.onupgradeneeded = (e) => {
  const db = e.target.result;
  const objectStore = db.createObjectStore("logs", {
    autoIncrement: true,
  });
  objectStore.createIndex(
    "project_idx" /*索引别名,随便命名*/, 
    "project" /*索引key,对象的key*/, 
    {
      unique: false, // 该索引不会唯一,有相同值的数据
    }
  );
  objectStore.createIndex(
    "log_type_idx", 
    "log_type", 
   { unique: false }
  );
}

看下我们创建的索引

4、插入数据

插入数据,我们需要监听success 事件,在 连接数据库返回的实例中。

不能在 onupgradeneeded 事件中,不然会报错

并且需要先创建一个事务,然后再插入数据

代码语言:javascript复制
dbRequest.onsuccess = (e) => {
  var db = e.target.result;
  var transaction = db.transaction(["log"] /*表名*/, "readwrite" /*操作权限 */);
  var objectStore = transaction.objectStore("log" /*表名,和上面一致*/);

  // 插入数据
  const addRequest = objectStore.add({ project: "act", time: 11 })

  addRequest.onsuccess = ()=>{} // 成功事件
  addRequest.onerror= ()=>{} // 失败事件
}

5、读取数据

先根据唯一 key 值读取数据

代码语言:javascript复制
dbRequest.onsuccess = (e) => {
  var db = e.target.result;
  var transaction = db.transaction(["log"] /*表名*/, "readwrite" /*操作权限 */);
  var objectStore = transaction.objectStore("log" /*表名,和上面一致*/);

  // 根据唯一 key 读取数据
  const readReq = objectStore.get(1);

  readReq.onsuccess = ()=>{ // 成功事件
    const cursor = e.target.result // 读取的值 {"project":"act","time":11}
  } 
  readReq.onerror= ()=>{} // 失败事件
}

根据索引来读取数据

代码语言:javascript复制
request.onsuccess = (e) => {
  var db = e.target.result;
  var transaction = db.transaction(["log"] /*表名*/, "readwrite" /*操作权限 */);
  var objectStore = transaction.objectStore("log" /*表名,和上面一致*/);

  var index = objectStore.index("project"); // 指定索引
  const range = window.IDBKeyRange.only("act" /*value值*/);
  const readReq = index.openCursor(range); // 索引配合上 value 值,绝了

  readReq.onsuccess = function (e) {
    const cursor = e.target.result.value; // 加多一层 value
  };
}

如果匹配到多条值,默认返回第一条,如果要拿到所有数据,需要调用方法,如下

代码语言:javascript复制
readReq.onsuccess = function (e) {
  const cursor = e.target.result;
  if (cursor) { 
    cursor.continue();
  }
}

这样就会循环触发 onsuccess 事件,直到读取所有数据

indexdb 的内容差不多就说到这里

2JSZip

用来读取本地日志,然后打包成zip,一次性上传

1、引入 jsZip 文件

2、打包压缩

比较简单,像这样

代码语言:javascript复制
const zip = new JSZip();
zip.file(
  `a.log` /*文件名*/, 
  "abcdefg"/**压缩内容 */
);
zip.generateAsync()
.then(zipFile=>{ /*zipFile 是压缩的文件*/ })

最后压缩包解压就会有一个a.log文件

具体使用参考官网:https://github.com/Stuk/jszip

具体处理

我们知道了基本思路和使用的api,现在来说下具体处理过程

主要分为这3个部分

1、怎么建数据库

2、怎么存

3、什么时候取

1怎么建数据库

首先看一下我们上报的数据,简单列一下

代码语言:javascript复制
{
 project:"xx", 
 level: 10, 
 offline_type:"fail_log", 
 time:"1630302494412"
 ....
}

其中看到一个字段 level,表示日志的等级

我们把日志分为下面几个等级

1、trace: 10

2、debug: 20

3、info: 30

4、warn: 40

5、error: 50

6、fatal: 60

以此来区分 日志的 重要程度,减少非必要性数据上报

默认只上报 error 等级,支持自定义,初始化的时候传入 最低上报等级,比如 info,那么 等级为 info 以上的都会上报

另外,为了区分离线日志类型,还有一个字段 offline_type

值为 fail_log,表示上报失败的日志

值为 common_log,表示等级不高存本地的日志

这个字段只是为了方便本地区分 离线日志,对于监控数据没有意义,所以并不会上报这个字段上去

为了能快速查找出不同的离线日志,我们需要对 offline_type 设置索引

这样 offline_type = fail_log 的日志需要自动上报,offline_type = common_log 则是用户手动上传

因为创建索引只有在 创建数据库的时候(除非改变版本号),所以还是要对可能用到的字段设置索引

比如 project、time 、level 等等,比如可能以后只获取 某个项目,或者某个时间段 的日志

代码语言:javascript复制
const dbReq = window.indexedDB.open('LogDB', 1);

dbReq.onupgradeneeded = (e) => {
  const db = e.target.result;
  const objectStore = db.createObjectStore('logs', {
    autoIncrement: true,
  });

 // 创建 索引
  objectStore.createIndex(
   'project_idx', 'project', 
   { unique: false }
  );

  objectStore.createIndex(
    'log_time_idx', 'time', 
    { unique: false }
  );

  objectStore.createIndex(
    'level_idx', 'level', 
    { unique: false }
  );

  objectStore.createIndex(
    'offline_type_idx', 'offline_type', 
    { unique: false }
  );
}

2怎么存

比如我们的上报都是调用一个 report 方法,如果不做判断就直接请求了

代码语言:javascript复制
function report(data){
     fetch("www.test.com/report",{
        body: JSON.stringify(data)
     })   
}

这是完全不管不顾的上报方式,我们需要在其中加上一些逻辑

在调取上报方法的时候,拿到上报的数 日志等级 以及 当前的上报网络状态,判断当前是否应该直接请求 还是存在本地

1、日志等级小于 设置的等级,存本地

2、用户离线 or 服务不稳定,存本地。

3、上报请求错误了,存本地

其中怎么判断用户离线呢?

通过 window.navigator.onLine ,这个属性值为true表示设备在线,为false表示设备离线。兼容性也不错,可以说是支持所有浏览器,IE6都支持。

但是设备在线,并不包括网络差的情况,为了不再网络差的时候占用用户网速,我们会加多一个另外的判断。

如果 15s 内上报错误大于3,那么数据会存在本地,会设置一个15s 定时器去重置错误次数。

同时这个判断也是为了解决服务不稳定或者服务器宕机,仍频繁请求的情况

逻辑代码如下(并非最终实现,理解主线逻辑)

代码语言:javascript复制
const LOG_LEVELS = {
  trace: 10,
  debug: 20,
  info: 30,
  warn: 40,
  error: 50,
  fatal: 60,
};

const autoUploadLevel = LOG_LEVELS.info; // 设置上报的最低等级是info
let failCount = 0;
let timer = null;

function report(data) {
  const { level } = data; 

  // 等级不够 || 10s 内错误大于3次,直接存本地
  if (!navigator.onLine || failCount >= 3 || level < autoUploadLevel) {
    indexdb.add(data); // 存本地
    return;
  }

  reportOfflineLog(); // 每次上报都会读取一次本地存量的日志,进行上报

  fetch('xxx', data).catch(() => {
    failCount  ;
    indexdb.add(data); // 存本地

    if (!timer) { // 10s 定时器
      timer = setTimeout(() => {
        failCount = 0;
      });
    }
  });
}

function  reportOfflineLog(){
  // 查找 offline_type = fail_log 的日志 进行上报
}

然后我们上报一条数据

代码语言:javascript复制
report({
  project: 'admin',
  log_time: Date.now(),
  level: LOG_LEVELS.debug,
})

这条数据等级不够,就会直接存在本地

3什么时候取

存在数据库的日志有两种类型

1、上报失败的日志

2、等级不足的日志

上报失败的日志

1、初始化的时候,会读取数据失败日志上报一次

2、之后每次调用上报方法的时候,会读取一次数据库存量的失败日志

等级不足的日志

提供方法供用户操作,然后把本地的日志打包上报这里会监听 键盘 Alt X 事件 以及 移动端手势4指点按操作事件

代码语言:javascript复制
const handler = () => {
  const ok = confirm('是否上传日志');
  if (ok) {
    packZipAndUpload();
  }
};

document.addEventListener('keydown', (e) => {
  if (e.keyCode === 88 && e.altKey) {
    handler();
  }
});

document.addEventListener('touchend', (e) => {
  // >=4指点按
  if (e.touches.length >= 3) {
    handler();
  }
})

主要是为了弹窗给用户进行确定,这里我简单用了 confirm 处理

用 JSZip 打包好之后,会上传到服务器

代码语言:javascript复制
function packZipAndUpload() {

  // 读取等级不足的日志
  indexdb_store
  .read(/* offline_type = common_log */)
  .then((result) => {

    zip = new JSZip();
    zip.file(
      `${Date.now()}.log` /*文件名*/,
      JSON.stringify(result) /**压缩内容 */
    );

    zip
      .generateAsync({
        type: 'blob', // 压缩的结果为二进制流,可用作文件上传
        compression: 'DEFLATE',
        compressionOptions: {
          level: 6,
        },
      })
      .then((zipFile) => {
        fetch(`http://www.test.com/upload`, {
          method: 'POST',
          body: zipFile,
        }).then(() => {
          // 成功之后清除本地日志
        });
      });
  });
}

我们会有一个日志系统,专门查看 上传的压缩日志,提供 下载,或者 在线预览

在线预览,则会对 zip 文件进行解压,然后解析处理里面的内容

解压使用了 JSZip( https://github.com/Stuk/jszip ) 和 JSZipUtils(https://github.com/Stuk/jszip-utils)

解压也很简单,就这么一段代码,只要拿到解压链接 以及 压缩包内的文件

代码语言:javascript复制
JSZipUtils.getBinaryContent(
  `http://wwww.test.com/xxxxxx.zip`, // zip 文件链接
  function (err, data) {
    JSZip.loadAsync(data).then((zip) => {
      zip
        .file('xxxx.log') // 需要读取的 zip 中的文件的名字
        .async('string')
        .then((content) => {
          // 拿到文件字符串内容,可以格式化成数组,然后显示
          console.log('content', content); 
        });
    });
  }
);

解压后拿到 文件的 字符串内容,格式化之后,渲染在页面上

代码仓库

代码demo把主要逻辑写出来,会忽略错误处理,代码规范,代码设计等问题,简化代码量,为了能可以快速理解主线逻辑

https://gitee.com/hoholove/study-code-snippet/blob/master/LOGGER/offlineLog.js

最后

每篇文章我都会尽量把主线逻辑讲出来,可能会忽略 一些优化的细节,这些细节大家都可以想得到

比如本地的日志需要有一个存储的日期,过期了需要清除 等等

毕竟看完文章你都不一定能写得出来,但是你一定要了然于胸,对这个东西了解一个主要思路

等到你需要的时候,你可以再去学

0 人点赞