前端日志, 追踪行为 @bolt/logger & @bolt/trace

「要多想。」

「想了以后呢?」

「北海, 我只能告诉你在那(初始化项目)以前要多想。」

-- 『三体』

@bolt/logger Api 设计

基本使用

创建一个 logger 实例
TypeScript 复制代码
const { createLoggerFriendly, use } = createInstance(instanceOption);

/**
 * === 通过 use(BaseLogger) 来兼容各平台 ===
 */

/** production 环境禁用 console */
process.env.NODE_ENV === "development" && use(consoleLogger);

use(debugLogger);
use(teaLogger);
use(slardarLogger);
为各 namespace 创建 logger
TypeScript 复制代码
const defaultLogger = createLoggerFriendly("[default]");
export default defaultLogger;

export const apiLogger = createLoggerFriendly('api_fetch', createOption);
export const fgLogger = createLoggerFriendly("[fg]");
约束 logger 调用参数
TypeScript 复制代码
defaultLogger.info(content, data, option);
defaultLogger.debug(content, data, option);
defaultLogger.warn(content, data, option);
defaultLogger.error(content, data, error, option);
  • content 为 string, 描述当前的行为

  • data 为扩展字段, 上报需要记录的数据

  • option 与 createOption, instanceOption 类型一致

增加一个平台的兼容

定义 Baselogger
TypeScript 复制代码
import { defineBaseLogger } from "@bolt/logger";
import Tea from "byted-tea-sdk";

export default defineBaseLogger(async (option) => {
  const { level, content, namespace, data } = option;
  Tea.event("bolt_logger", {
    level,
    content,
    namespace,
    ...data,
  });
});
通过 ts 扩展 option

还记得 @bolt/api 吗, 基于洋葱模型的工程化网络请求库: @bolt/api

自定义配置可追溯的优势我们并没有丢掉

创建 instance 时通过泛型约束 BaseLogger 可以用的所有自定义 Option 强制约束中间件声明所用到的 Option 字段 创建 logger 时提示所有可选 Option
一个文件了解所需内容, 知道自定义配置字段从哪儿来 再复杂的日志平台也不怕 VSCode 可以通过 ts 快速定位到类型定义

设计一款追踪行为的日志工具

基本使用

创建一个 trace 实例
TypeScript 复制代码
export const { createTrace } = createInstance();
针对业务场景定义 trace
TypeScript 复制代码
import { createTrace } from "./instance";

/** jsReady -> 上报 tti */
export const ttiTrace = createTrace<{
  page: "list" | "detail";
}>("ttiTrace");

/** list 页面进入 -> list 页面跳出 */
export const listTrace = createTrace("listTrace");

/** list 卡片点击 -> detail 加载完成 */
export const listEnterDetailTrace = createTrace("listEnterDetailTrace");

/** detail 页面进入 -> detail 页面跳出 */
export const detailTrace = createTrace("detailTrace");
在业务生命周期里调用
TypeScript 复制代码
listTrace.start('准备打开list了', { xxx: '一些自定义数据' })
listTrace.mark('记录一些中间行为日志', { xxx: '一些自定义数据' })
listTrace.mark('记录一些中间行为日志', { xxx: '一些自定义数据' })
listTrace.finish('list已加载完成', { xxx: '一些自定义数据' })

// 如果失败了不会调用 finish, 而是
listTrace.fail('list加载失败', error, { xxx: '一些自定义数据' })

trace 从 start 到 finish 都做了什么

trace 基于 logger 实现, 也就是通过 logger 上报日志到各平台

  • 默认了一些内置上报的数据, 如统计时间间隔
TypeScript 复制代码
/**
 * === trace 统计时间字段说明 ===
 * - t: 当前时间
 * - td: (第一个)开始 -> 结束的时间间隔
 * - tdx: (最后一个)开始 -> 结束的时间间隔
 *    - 只有一个开始时, tdx === td
 *    - 多个开始时, trace 上报为 warn
 * - ti: @bolt/trace 中没有实现, 用户可以在 mark 中传入自定义参数, 自行统计两个 mark 之间的时间差
 */
  • 收集从 start 到 finish(或 fail) 的完整路径, 通过时间戳方便在 tea 或 slardar 定位相关日志 data 数据
TypeScript 复制代码
"start:接口==>开始@1658308526614;mark:请求->开始@1658308526616;mark:请求<-完成@1658308527392;finish:接口<==完成@1658308527392"

// 格式化后好看一点
"start:接口==>开始@1658308526614;
mark:请求->开始@1658308526616;
mark:请求<-完成@1658308527392;
finish:接口<==完成@1658308527392"
  • finish 时会重置 start 和 trace 路径, 方便下次 start 重新记录

接口请求监控: 实现一款 @bolt/api 的中间件

  • 统计接口响应速度

  • 监控接口报错

打算如何使用
TypeScript 复制代码
import { createInstance } from "@bolt/api";

const { createApi, use } = createInstance();

/**
 * 使用 trace 代替 logger, 能够记录接口最终耗时
 * 必须放在其它中间件之前, 用来最接近用户真实感受
 */
use<TraceOption>(trace);
// use(logger);
/**
 * 静默登录&重登中间件
 */
use<LoginNeedOption>(
  createLoginMiddleware({
    checkLoginError,
  })
);
/**
 * 使用 trace.mark 记录真实接口耗时, 不包括登录&重登耗时
 * 必须放在其它中间件之后, 且在<错误码分类中间件>之前
 */
use<TraceOption & LoginNeedOption>(tracePureApi);
/**
 * 对后端错误码分类, 抛出各类错误
 * 中间件必须放在最后一位
 */
use<UnwrapOption>(unwrap);
代码实现
TypeScript 复制代码
export type CustomOption = {
  trace?: TraceObj;
};

export const trace = defineMiddleware<CustomOption>(async (option, next) => {
  const apiTrace = createTrace(`apiTrace:${option.path}`);
  option.trace = apiTrace;
  try {
    apiTrace.start('接口==>开始', { option });
    const res = await next();
    apiTrace.finish('接口<==完成', { option });
    return res;
  } catch (error) {
    apiTrace.fail('接口<xx失败', { option }, error);
    throw error;
  }
});

/** 基于 loginMiddleware 记录 relogin mark */
export const tracePureApi = defineMiddleware<CustomOption & LoginNeedOption>(
  async (option, next) => {
    const apiTrace = option.trace
    const prefix =
      option.needLogin && option.reloginCount !== 0
        ? `重发${option.reloginCount}`
        : "请求";
    try {
      const t = apiTrace?.mark(`${prefix}->开始`, { option }) || Date.now();
      const res = await next();
      const ti = Date.now() - t;
      apiTrace?.mark(`${prefix}<-完成`, { option, ti });
      return res;
    } catch (error) {
      apiTrace?.fail(`${prefix}<x失败`, { option }, error);
      throw error;
    }
  }
);
多实例 trace 介绍
  • 对于业务行为来说, 如打开详情, start 之后, 第二次 start 必定在第一次 finish 之后, 如果存在两次 start, 说明代码有多次 render 风险(React 尤其注意), 因此多次 start 可以直接通过 logger.warn 记录警告风险.

  • 而对于接口来说, 如果创建了一个 apiTrace, 多个请求并发, 必定会先多次 start, 然后多次 finish, 那么对于一个 trace 的时间记录就不准确了

    • 同一个 path 的接口也是会有并发的可能, 因此必须在 api 中间件中针对每次请求单独创建 trace
为何导出了两个中间件

正如注释中提到的, 我们实际调用 api 方法到其返回的时间并不代表是后端接口响应的时间, 因为要考虑中间还有中间件的耗时, 尤其是重登等多次重试直到成功后的返回.

例如重试的 trace 路径为:

TypeScript 复制代码
"start:接口==>开始@1658310509100;mark:请求->开始@1658310509101;fail:请求<x失败@1658310509274;mark:重发1->开始@1658310509275;fail:重发1<x失败@1658310509448;mark:重发2->开始@1658310509449;fail:重发2<x失败@1658310509790;mark:重发3->开始@1658310509791;fail:重发3<x失败@1658310509868;fail:接口<xx失败@1658310509870"

// 格式化后好看一点
"start:接口==>开始@1658310509100;
mark:请求->开始@1658310509101;
fail:请求<x失败@1658310509274;
mark:重发1->开始@1658310509275;
fail:重发1<x失败@1658310509448;
mark:重发2->开始@1658310509449;
fail:重发2<x失败@1658310509790;
mark:重发3->开始@1658310509791;
fail:重发3<x失败@1658310509868;
fail:接口<xx失败@1658310509870"

这里可知:

  • 单次请求后端接口只需173ms(= 1658310509274 - 1658310509101) 就返回请求错误了

  • 而业务 api 调用在 770ms(= 1658310509870 - 1658310509100) 后才真正将报错抛出

声明: 本文说提到的 @bolt/logger @bolt/trace 为虚构, 并未发布到 npm 仓库

相关推荐
雪碧聊技术3 分钟前
01-Ajax入门与axios使用、URL知识
前端·javascript·ajax·url·axios库
adminIvan8 分钟前
Element plus使用menu时候如何在折叠时候隐藏掉组件自带的小箭头
前端·javascript·vue.js
会发光的猪。27 分钟前
【 ElementUI 组件Steps 步骤条使用新手详细教程】
前端·javascript·vue.js·elementui·前端框架
我家媳妇儿萌哒哒28 分钟前
el-table合并单元格之后,再进行隔行换色的且覆盖表格行鼠标移入的背景色的实现
前端·javascript·elementui
前端青山44 分钟前
webpack指南
开发语言·前端·javascript·webpack·前端框架
还是大剑师兰特2 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~3 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding3 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT3 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓3 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js