「要多想。」
「想了以后呢?」
「北海, 我只能告诉你在那(初始化项目)以前要多想。」
-- 『三体』
@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 仓库