本文作者: 姚泽源
栏目简介
介绍本周值得分享的技术内容, 周四发布
判断文件类型
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就可以了.
详情如下:
原代码 =>
ES5转换报错(模拟了类的实例化过程) =>
ES6转换后正常 =>
最佳日志实践
本周负责调研监控系统的日志实现. 收集了目前网络上比较好的几篇日志实践总结. 汇总如下
- 日志从功能讲, 可分为诊断日志、统计日志、审计日志
诊断日志, 典型的有:
请求入口和出口 外部服务调用和返回 资源消耗操作: 如读写文件等 容错行为: 如云硬盘的副本修复操作 程序异常: 如数据库无法连接 后台操作:定期执行删除的线程 启动、关闭、配置加载
统计日志:
用户访问统计:用户IP、上传下载的数据量,请求耗时等 计费日志(如记录用户使用的网络资源或磁盘占用,格式较为严格,便于统计)
审计日志:
管理操作
对于简单的系统,可以将所有的日志输出到同一个日志文件中,并通过不同的关键字进行区分。而对于复杂的系统,将不同需求的日志输出到不同的日志文> 件中是必要的,通过对不同类型的文件采用不同的日志格式(例如对于计费日志可以直接输出为Json格式),可以方便接入其他的子系统。
- 理想的日志中应该记录不多不少的信息.
所谓不多,是指不要在日志中记录无用的信息。 所谓不少,是指对于日志的使用者,能够从日志中得到所有需要的信息
- 通常情况下会在日志中遗漏以下数据项, 需要注意
系统的配置参数
后台定期执行的任务
异常处理逻辑
日志中需要记录关键参数,出错时的关键原因等
- 日志监控
- 能不报警的就不报警,只有需要运维马上处理的错误才需要发送报警。这样做的原因是避免长期的报警骚扰让运维人员对报警不再敏感,最后真的报警来了时,变成了狼来了的传说;
- 明确报警关键字,例如用ERROR作为报警的关键字,而不是各种各样的复杂规则。这样做的原因是日志监控本质上是不断的进行字符串匹配操作,如果规则太多太复杂,就可能对线上服务产生影响;
- 对于一些预警操作,例如某个服务需要重试多次才能成功,或者某个用户的配额快用完等等,可以通过每天一封报警邮件的方式来反馈;每一次系统出现故障,都需要及时检查日志报警是否灵敏,日志报警的描述是否准确等,不断优化日志报警;
以上内容来自于@王健的最佳日志实践(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;
};
}
复制代码