一, 背景
大量项目在使用 logback 记日志, 有部分项目使用日志混乱, 格式不统一, 多数人搞不懂配置文件, 导致配置错误, 现在需要开发一套统一的, 少配置的日志组件, 方便使用.
二, 设计思路
尽量采用 0 配置, 无 logback.xml
日志格式统一, 方便后续日志分析系统
只有两个日志级别, 一个是正常日志, 一个是异常日志
提供 log4j,jcl,logback,commons-log 等桥接方案及版本兼容方案
提子线程, JSON 格式化输出, map 格式化, 数组格式化, 请求响应参数 (供耗时) 等便捷日志输出方法
支持 Redis,db,http 自动开关配置 **
新增日志类型(logger)
API 采用流式结构, 类似 StringBuffer
三, 概要设计
1, 零配置
调研代码
- java
- static LoggerContext lc;
- static {
- lc = (LoggerContext) LoggerFactory.getILoggerFactory();
- // 对应配置中的 appender
- ConsoleAppender ca = new ConsoleAppender();
- ca.setContext(lc);
- ca.setName("console");
- // 格式
- PatternLayoutEncoder pl = new PatternLayoutEncoder();
- pl.setContext(lc);
- pl.setPattern("%d{MMddHHmmss.SSS} [%thread] %-5level %logger{36} - %msg%n");
- pl.start();
- ca.setEncoder(pl);
- ca.start();
- // 对应配置中的 logger
- ch.qos.logback.classic.Logger rootLogger = lc.getLogger("com.test");
- rootLogger.addAppender(ca);}
上面代码等价于下面的 xml
%d{MMddHHmmss.SSS} [%thread] %-5level %logger{36} - %msg%n
由此可以随意把配置文件中的内容以代码形式编写, 理论已经可以实现 0 配置.
2, 输出路径
约定固定将日志输出到, 相对路径 log/xxx.yyyy-MM-dd-HH.log, 其中 xxx 为 logger 的 name
3, 日志格式
格式固定:
MMddHHmmss.SSS||id||[交易名子步骤] ||context ||[level][线程号]
例:
150000.311||N-XrUTQzIc1531897200311||[CiTeeFilterci 拦截器] ||ci 拦截器 请求的完整参数为:{"merchantId":["0012444"],"userId":["13112341232"]} ||[INFO][http-8091-7]
固定格式的核心代码, 拦截到日志请求, 按照格式拼装, 主要方法为继承 ThrowableProxyConverter 和 MessageConverter 来实现对日志的拦截, 并修改为想要的格式, 其中使用的例如 id 等放到本地变量内, 核心是对 MDC 的使用
4, 基础 logger
所有日志都默认输出到这里 logger name:service 系统初始化时, 定义这个 Logger 和 appender, 即这个 Logger 为 root log
5, 自定义的 logger
提供 addLogger 方法, 参数 packageName 包名, 例如: com.test 必输参数 如果 name 未设置时, name 默认为包名最后一个. 后面的字符 name 名字, 决定日志文件的名字 非必输 path 日志路径 非必输 additivity 是否输出到 root log 内
6, 特殊的 log
提供特殊组件的 log 配置, 例如: Redis 默认 ERROR http 默认 ERROR db 连接池 默认 ERROR kafka 默认 ERROR schedul 默认 ERROR spring 默认 ERROR
7, 异常, 换行日志处理
提供 exception 异常栈格式打印 提供带换行的格式化打印 代码思路: 继承 ThrowableProxyConverter, 获取异常栈, 在每行的前面插入固定格式文本
8, 普通日志 API(VirgoLog)
方法 | 方法描述 |
---|---|
setUniqKey(id) | 设置当前线程 id,线程开始时设置即可,后面无需设置 |
updateStep(trade, step) | 更新当前 id 的步骤信息 |
log(msg, param) | 记录普通日志,msg 替换规则,普通替换为 {},如果想替换为业务日志 api 中的格式,使用 `` 替换 |
logErr(msg, e) | 记录异常日志 |
log( trade, step, msg, param) | 记录普通日志,此方法会自动更新 id、trade、step,不建议使用 |
logErr(trade, step, msg, e) | 记录异常日志 |
log(cid, trade, step, msg, param) | 记录普通日志,此方法会自动更新 id、trade、step,不建议使用 |
logErr(cid, trade, step, msg, e) | 记录异常日志 |
debug(msg, param) | 记录 debug 级别日志,不建议使用 |
业务日志 API(VirgoLog)
平时记日志时, 如果某个类没有时间 toString 方法, 会无法正确打印出数据, 此时提供替换方法, 直接将 object 替换为 JSON 打印, 核心代码思路为
MessageFormatter 是处理 {} 替换的类, 重新写个类, 稍加改动即支持 {} 也支持 `` , 并判断替换为 JSON 还是 toString
API 如下
方法 | 方法描述 |
---|---|
begin(msg) | 记录开始 |
end(msg) | 记录完成,会打印本线程内上一个 begin 到现在的耗时 |
logJson(json, format) | 记录 json 格式化日志,format 表示是否换行 |
logMap(map, format) | 记录 map 格式化日志 |
logCollection(list, format) | 记录集合格式化日志 |
logArray(array, format) | 记录数组格式化日志 |
logObjct(obj, format) | 记录 Object 格式化日志 |
系统 API(LoggerHelper)
方法 | 方法描述 |
---|---|
getLogger() | 获取 logger,用于记日志 |
getLogger(name) | 通过 name 获取 logger |
addLogger() | 参考自定义 Logger,如果 logger 已经创建,则不再创建,一般不使用,除非想自定义日志名等 |
consoleOpen() | 打开控制台日志,系统启动时默认配置控制台日志 |
commonOpen(name, level) | 默认的组件都是 error 级别,这个方法可以变更日志级别,例如 redis http 等 |
9, 特殊的格式化
map: 即转化为 JSON, 然后再格式化
collection: 同上
array: 也同上
object: 同上
10, 问题
密码脱敏, 加解密有必要单独提取方法吗
提供父线程打印开关
11,maven 依赖
- com.cdc.ecliptic virgo 1.5_1.6-SNAPSHOT
- 12,demo
- public static void main(String[] args) throws InterruptedException {
- // 启动
- VirgoLancher.start("hahaha", "com.cdc.virgo", "D:/test/hahah.log");
- LoggerHelper.commonOpen("hahaha", LogLevel.DEBUG);
- Logger logger1 = LoggerFactory.getLogger("druid");
- // VirgoLancher.commonStart("abc", "com.cdc.virgo");
- // 打开控制台
- LoggerHelper.consoleOpen();
- // 设置 cid
- VirgoLog.setUniqKey(null);
- // 设置步骤名和交易名
- VirgoLog.updateStep("adfa", "saf");
- // 获取 Logger
- VirgoLog logger = VirgoLog.getLogger();
- // 打开 debug 级别(只有在开发阶段可以打开)
- // logger.changeLevel(LogLevel.DEBUG);
- // 记录换行
- logger.log("a");
- logger1.info("dddddddddd");
- logger1.error("dddddddddd");
- // logger1.info("sfdasfaf" +
- // "\nafafdasfd" +
- // "\nasfdasf");
- logger.log("sfdasfaf" +
- "\nafafdasfd" +
- "\nasfdasf");
- // logger1.info("b");
- // 正常日志
- // logger.log("我只有一行");
- Map map = new HashMap();
- map.put("asdf", "1");
- map.put("asdf2", "2");
- map.put("asdf3", "13");
- map.put("asdf4", "14");
- map.put("asdf5", "15");
- map.put("asdf6", "16");
- // // 异常日志也支持格式化
- // logger.logErr("我错了:{}, 你没错:~~", new Exception("asdfsaflk"), "啊", map);
- // logger.log("----------------------------------------------");
- // // {}替换普通对象, 调用 toString() ~~ 把对象转换为 JSON 并且格式化输出 `` 把对象转换为 JSON 不格式化输出
- logger.log("你好{}, 你是谁~~``,sd~xx {}", map, map, map, "tttt");
- VirgoLog.updateStep("saf2");
- // // 把对象转换为 JSON 输出
- // logger.logJson(map, false);
- // // 更新步骤名和交易名
- // VirgoLog.updateStep("bbbbb", "ccccc");
- // // 耗时日志打印
- logger.begin("处理内容");
- logger.begin("处理第二个");
- logger.begin("处理第三个");
- Thread.sleep(3000L);
- logger.end();
- Thread.sleep(1000L);
- logger.end();
- VirgoLog.updateStep("saf3");
- logger.end();
- // // 记录 debug 日志, 一般调试用
- // logger.logDebug("jajajajaja");
- // List l = new ArrayList();
- // B b = new B();
- // try {
- // b.b();
- // } catch (Exception e) {
- // logger.logErr("woqu", e);
- // }
- }
来源: https://yq.aliyun.com/articles/709429