前端日志, 追踪行为 @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 仓库

相关推荐
小镇程序员2 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪3 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背3 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M3 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc3 小时前
学习electron
javascript·学习·electron
想自律的露西西★3 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳4 小时前
vue3:瀑布流
前端·javascript·vue.js
霍先生的虚拟宇宙网络4 小时前
webp 网页如何录屏?
开发语言·前端·javascript
温吞-ing4 小时前
第十章JavaScript的应用
开发语言·javascript·ecmascript