likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 19

「要多想。」

「想了以后呢?」

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

-- 『三体』

@bolt/logger Api 设计

基本使用

创建一个 logger 实例
const { createLoggerFriendly, use } = createInstance(instanceOption);

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

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

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

export const apiLogger = createLoggerFriendly('api_fetch', createOption);
export const fgLogger = createLoggerFriendly("[fg]");
约束 logger 调用参数
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
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
前端日志, 追踪行为 @bolt/logger & @bolt/trace前端日志, 追踪行为 @bolt/logger & @bolt/trace前端日志, 追踪行为 @bolt/logger & @bolt/trace
一个文件了解所需内容, 知道自定义配置字段从哪儿来再复杂的日志平台也不怕VSCode 可以通过 ts 快速定位到类型定义

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

基本使用

创建一个 trace 实例
export const { createTrace } = createInstance();
针对业务场景定义 trace
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");
在业务生命周期里调用
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 上报日志到各平台

  • 默认了一些内置上报的数据, 如统计时间间隔
/**
 * === trace 统计时间字段说明 ===
 * - t: 当前时间
 * - td: (第一个)开始 -> 结束的时间间隔
 * - tdx: (最后一个)开始 -> 结束的时间间隔
 *    - 只有一个开始时, tdx === td
 *    - 多个开始时, trace 上报为 warn
 * - ti: @bolt/trace 中没有实现, 用户可以在 mark 中传入自定义参数, 自行统计两个 mark 之间的时间差
 */

  • 收集从 start 到 finish(或 fail) 的完整路径, 通过时间戳方便在 tea 或 slardar 定位相关日志 data 数据
"start:接口==>开始@1658308526614;mark:请求->开始@1658308526616;mark:请求<-完成@1658308527392;finish:接口<==完成@1658308527392"

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

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

  • 统计接口响应速度

  • 监控接口报错

打算如何使用
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);

代码实现
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 路径为:

"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 仓库