[第35周]全栈之路: TS报错与最佳日志实践

本文作者: 姚泽源

栏目简介

介绍本周值得分享的技术内容, 周四发布

判断文件类型

mime-types, 根据文件名快速推断文件mime-type. 在上传CDN文件时, 经常需要提供文件的content-type以确认缓存方案, 例如: ‘application/json’ / ‘text/markdown’ / “image/bmp”, mime-types就可以解决这个问题.

TS诡异报错

在使用TSC编译代码时, 意外遇到了TypeError: Class constructor Command cannot be invoked without ‘new’报错.

谷歌之后发现原因是将编译的target写成了es5.

在@阮一峰 的[ES6手册-class][es6.ruanyifeng.com/?search=con…, ES6新增的class类里, constructor方法只能通过new调用, 否则会报错.

但是, ES5中并没有这条规定, 而TSC也正是使用了apply/call方法模拟了new Class调用, 导致报错. 修复方法也很简单, 把tsconfig.json中的target改为ES6就可以了.

详情如下:

原代码 =>

image.png

ES5转换报错(模拟了类的实例化过程) =>

image.png

ES6转换后正常 =>

image.png

最佳日志实践

本周负责调研监控系统的日志实现. 收集了目前网络上比较好的几篇日志实践总结. 汇总如下

  1. 日志从功能讲, 可分为诊断日志、统计日志、审计日志

诊断日志, 典型的有:

请求入口和出口 外部服务调用和返回 资源消耗操作: 如读写文件等 容错行为: 如云硬盘的副本修复操作 程序异常: 如数据库无法连接 后台操作:定期执行删除的线程 启动、关闭、配置加载

统计日志:

用户访问统计:用户IP、上传下载的数据量,请求耗时等 计费日志(如记录用户使用的网络资源或磁盘占用,格式较为严格,便于统计)

审计日志:

管理操作
对于简单的系统,可以将所有的日志输出到同一个日志文件中,并通过不同的关键字进行区分。而对于复杂的系统,将不同需求的日志输出到不同的日志文> 件中是必要的,通过对不同类型的文件采用不同的日志格式(例如对于计费日志可以直接输出为Json格式),可以方便接入其他的子系统。

  1. 理想的日志中应该记录不多不少的信息.

所谓不多,是指不要在日志中记录无用的信息。 所谓不少,是指对于日志的使用者,能够从日志中得到所有需要的信息

  1. 通常情况下会在日志中遗漏以下数据项, 需要注意

系统的配置参数

后台定期执行的任务

异常处理逻辑

日志中需要记录关键参数,出错时的关键原因等

  1. 日志监控
  1. 能不报警的就不报警,只有需要运维马上处理的错误才需要发送报警。这样做的原因是避免长期的报警骚扰让运维人员对报警不再敏感,最后真的报警来了时,变成了狼来了的传说;
  2. 明确报警关键字,例如用ERROR作为报警的关键字,而不是各种各样的复杂规则。这样做的原因是日志监控本质上是不断的进行字符串匹配操作,如果规则太多太复杂,就可能对线上服务产生影响;
  3. 对于一些预警操作,例如某个服务需要重试多次才能成功,或者某个用户的配额快用完等等,可以通过每天一封报警邮件的方式来反馈;每一次系统出现故障,都需要及时检查日志报警是否灵敏,日志报警的描述是否准确等,不断优化日志报警;

以上内容来自于@王健的最佳日志实践(v2.0)

但在实践中, 我们知道. 打出来的日志是这样的(来自log4j):

19:08:07.062 TRACE com.test.TestService 27 exampleException – entry
19:08:07.077 DEBUG com.test.TestService 32 exampleException – catching
java.lang.ArrayIndexOutOfBoundsException: 3
at com.test.TestService.exampleException(TestService.java:29) [classes/:?]
at com.test.App.main(App.java:9) [classes/:?]
at com.test.AppTest.testApp(AppTest.java:15) [test-classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.6.0_29]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) > ~[?:1.6.0_29]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) ~[?:1.6.0_29]
at java.lang.reflect.Method.invoke(Method.java:597) ~[?:1.6.0_29]
at org.junit.internal.runners.TestMethodRunner.executeMethodBody(TestMethodRunner.java:99) [junit-4.3.1.jar:?]

所以, 在最终制订日志方案时, 我们选择的是: 监控日志/运行日志分别打点.

业务日志使用通用格式, 监控日志根据类别不同, 分别制订类型规范, 并使用TS定义打点日志类型. 具体使用时, 根据 type + version 字段即可唯一确定数据类型结构, 利用TS的类型声明功能即可实现代码提示, 以便分析

示例如下

/**
 * 参考log4j2
 */
type LogLevel = "debug" | "info" | "warn" | "error";
/**
 * 根据 Type 决定日志类别
 */
type LogType =
  | "server_start"
  | "server_runtime"
  | "common_log";


interface BaseLoggerType {
  /**
   * 日志的具体类型.
   */
  type: LogType | String;

  /**
   * 标记日志版本. 当数据结构发生改变时, 通过修改version区分数据记录
   * 参考http协议, 由于打点结构基本不变, 因此只需要两位版本号即可
   */
  version: "0.1";

  /**
   * 日志记录时间, 毫秒级
   */
  timestamp_ms: 1566788860106 | number;

  /**
   * 项目代码, 用于唯一区分项目
   */
  project_code: String;

  /**
   * 当前打点环境
   */
  env: 'dev' | 'st' | 'prod'

  /**
   * 是否忽略该打点记录, 为加快解析速度, 使用关键字表示需要忽略的打点记录, 匹配到关键字即忽略该打点日志
   */
  is_ignore: '' | '61013998c3dd0ca3bb6b8fd1c325086b'

  /**
   * 详情数据, 不同数据类别数据不同
   */
  detail: {
    // 强约束的key=>value键值对
    // 每种日志类别自行规定key值. 通过type + version字段唯一确定数据类型
    [key: string]: String | Number | Object;
  };

  /**
   * 扩展字段
   */
  extra: {
    // 由业务方/sdk传入任意key=>value键值对, 仅供记录. 不解析
    [key: string]: String | Number | Object;
  };
}


interface ServerStartInfoLog extends BaseLoggerType {
  /**
   * 服务器启动时, 记录相关信息
   */
  type: "server_start";
  detail: {
    /**
     * CPU数, 大于1即为多核
     */
    cpu_count: number;
    /**
     * 服务进程启动时间(毫秒级时间戳)
     */
    start_at_ms: 1566788860106 | number;
  };
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享