前端日志工具小结

在腾讯实习期间的一个主要工作是负责一个前端日志工具库以及底层脚手架工具的重构

项目内容

Feflow

Feflow是腾讯开发的一款基于模板进行快速开发的开源脚手架工具,主要目的是为了提升工程效率和保障工作流的规范性。

Feflow提供了基础的init、dev、build、test和deploy等命令,同时在模板的基础上,利用扩展的插件模块实现零配置快速上手、自定义命令等目的,具体文档可参考上文链接。

Feflow底层插件

第一个接触的项目是feflow的一个底层插件模块, 这个库主要用于支持纯JS库代码打包(rollup)的各种script命令。

我的工作是为该插件提供ts检查lintunit test等功能,具体的代码实现并无太大问题,只是由平时常用的CLI命令改为编写相应的script脚本,通过API的方式来调用。

只遇到了一个配置文件路径的坑:

配置文件路径问题

首先来看一下项目文件路径的设计:

npm包依赖被安装在node_modules文件夹中,feflow插件自然也不例外,其中存放着我们已经写好的ts、lint等配置文件,保证使用到这个模块的各个上层项目都能保持一致的代码规范。

但在一些特殊情况下也需要开发者可以在外层项目通过配置文件来覆盖内层规则。

所以具体的配置文件规则需要我们由外至内做判断。

基本原理如上所述,但在具体实现时出现了一些问题:

ts编译,通过脚本调用API读取配置文件进行编译。但ts配置文件只支持tsconfig.json
我们需要的是通过js来动态获取具体的配置文件路径,json文件格式显然无法满足。

既然无法直接配置,那我们只能考虑在编译前做些“手脚”:

  • 我们可以通过node脚本在编译前即时编写出一个tsconfig.json供ts读取检查

  • 采用一些第三方插件,有两个方向:

    • babel(@babel/preset-typescript

    • rollup(rollup-plugin-typescript2 / @rollup/plugin-typescript

babel插件在打包时会忽略掉ts检查,如果要进行ts检查需要CLI手动调用tsc命令;

故采用rollup插件,通过此插件API的include参数传入配置文件路径。

日志工具函数

第二个项目是工具函数库,下层采用feflow插件进行项目规范化,上层为日志库提供常用工具集,如AOPEvent等。除了功能代码,还基于上个项目为其编写ts、unit test等。

日志库

第三个项目是日志库,主要对于前端页面进行监听,采集加载数据、请求和用户行为用于优化页面,支持全监听和手动埋点。

其实前端日志包括开源产品已经有很多成熟方案了,但在具体项目使用中总有一些不如意的地方,比如:

  • 内部实现高耦合,不易扩展
  • 日志数据固定、取均值随机采样,无法获取精准、自定义数据
  • 不支持各类型日志数据的不同处理
  • 前后端高耦合,不易拆分

另一方面,这些工具大都提供一站式服务,对于希望使用多种不同工具的项目来说又显得过于臃肿。

所以这个库的目的不只是为了重复造轮子,除了具体功能外,还想要提供一种轻量级地前端日志上报的通用化解决方案

为了实现上述需求,一个大体的思路如下:

  • 内部功能模块采用插件化设计模式,支持自定义、自由替换
  • 将日志生命周期划分为收集处理存储上报等阶段,生命周期具体实现可以插件化替换
  • 生命周期通过参数式策略模式组合为一个完整的日志流;
  • 生命周期之间做低耦合处理,统一在日志流类中做观察者模式的注册,这样实现生命周期的链式注册、链式调用
  • 对不同类型日志如客户端指纹首屏加载用户交互资源请求等提供内置功能模块,其他类型扩展提供内部暴露的抽象类虚函数来继承覆写

日志上报

第四部分是处理日志上报:

  • 通过编写腾讯云SCF函数,将日志上报到私有网络,SCF相当于一个网关,避免后端服务直接暴露到公网中
  • 后端日志存储和可视化采用Elastic产品栈:Elasticsearch Service存储日志,Kibana用作可视化页面展示
  • 为保护后端服务和数据,Elasticsearch Service只开放内网接口,由SCF进行远程并行调用,只支持上报操作

前端日志上报原理

数据采集

根据不同的数据类型,采取不同的采集方式,一般来讲比较通用的数据如下:

  • 用户身份、设备、网络环境、PV等环境因素,直接采集上报

  • 页面首屏加载性能:

    • 传统多页面首屏性能可以直接由performance API采集数据,加工后上报

    • SPA单页面比较麻烦,因为前端请求完成后得到的只是一个js文件,组件加载时可能还要请求具体数据,所以我们说的首屏是指首次有效绘制

      • 如果支持ssr,类似多页面直接由performance API采集数据即可

      • 否则只能去侵入式地在生命周期钩子埋点或监听首屏的图片加载等较慢请求,或者手动埋点

      • 既然是首次有效绘制,应该和页面视图(DOM)紧密相关,那可不可以通过这个来判断呢?
        Chrome力推的以用户为中心的性能指标就基于此,详见捕获FMP的原理

  • 页面请求,采用AOP方式,在不修改原API的前提下对请求接口进行封装,可在before、after钩子做日志埋点。
    如果要记录参数和返回结果,注意对数据的去敏

  • 页面事件监听:

    • 全监听

      • 根元素冒泡监听,如有必要还可对stopPropagation进行AOP,以免遗漏数据;缺点:

        • 杂乱信息太多需要过滤,也可根据选择器dataset等准确定位
        • 一些事件如focus不会冒泡需要特殊处理
        • 事件发生后,若原dom被删除,便无法对元素进行定位
      • Element.prototype.addEventListener进行AOP

    • 手动埋点

  • 页面关闭事件需注意移动端的兼容问题

  • 页面错误

    • 全监听可监听errorunhandledrejection等事件
    • 手动埋点需手动进行catch捕获
    • 请求错误在请求函数部分进行处理
    • 除此之外还可能有跨域脚本代码压缩等问题,不再一一赘述

数据规范化

在查找资料时翻到这句话:WHO did WHAT and get WHICH exception in WHICH environment?
即要对日志数据做规范化处理,详细记录每条事件发生的具体时间、环境、人员等,方便后期的查询、定位。

可以定义不同类型的元数据,一方面对整个页面全监听很容易产生大量无用的脏数据,要对其进行过滤;另一方面对数据进行规范化,检测数据是否存在、数据值是否规范等。

数据上报

上报函数有FetchXHRsendBeacon等方式

上报周期如下:

  • 实时上报,网络IO占用多、性能差
  • 本地离线存储,自定义上报周期,数据可能不太即时
  • 考虑上述情况,可以采用实时 + 本地存储,核心是节流函数
    • 新日志生成直接上报,设置上报周期
    • 上报周期内再次生成新日志,存储到本地,等上报周期结束后,上报所有本地日志

数据检测中间层

如果有需要可在后端再次进行日志数据检测,一方面避免无效数据,另一方面由于日志上报并无身份认证,可能会导致恶意人员利用接口发送大量恶意数据。

报警 / 预警

告警也可分为两部分:

  • 硬件等工作环境的监控告警,可通过调控CPU、内存、磁盘读写、带宽等数值的阈值进行监控
  • 具体业务数据的监控告警,可自行实现通知代码,业界也有比较成熟的报警工具。
    Elastic产品栈中,可以选择使用ElastAlert,简单易上手,支持邮件、钉钉、微信、自定义等多种告警方式,且能灵活从Elasticsearch Service中查询业务数据。

数据可视化

一般成熟的日志服务会自带相应的可视化界面,不再赘述

参考