版权声明:如果觉得我的博客对你有帮助, 请点赞或评论! https://blog.csdn.net/zongf0504/article/details/89001958
对于web应用, 笔者通常会在请求开始和结束的时候打印一行日志, 并记录接口的处理时间, 也就是说通常所说的接口响应时间. 这样当生产环境项目出现异常时, 可以通过接口的响应时间和次数初步推测有可能产生问题的接口, 从而更快定位和解决问题。
1. 日志格式
笔者习惯于使用logback 输出日志, logback 定义日志格式比较灵活, 笔者定义的格式为:
- 接口开始: level][ className][ requestId}-接口:[$url]-start
- 接口结束: level][ className][ requestId}-接口:[$url]-ended, 耗时:254ms
2019-04-01 13:38:20.591[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-start
2019-04-01 13:38:20.591[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-这是其它日志, 模拟不匹配
2019-04-01 13:38:22.367[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-ended, 访问时间:254ms
2. 解析日志
2.1 建立日志模型
将单行日志抽象为java 类, 这样解析之后便于后续做排序, 统计等操作。
public class LogLine {
// 日志输出时间
private String time;
// 日志级别
private String level;
// 日志线程名称
private String thread;
// 日志类名称
private String clz;
// 请求会话 id
private String sid;
// 请求id
private String reqid;
// 接口地址
private String uri;
// 日志类型: start-开始日志 ended,-结束日志
private String tag;
// 请求耗时
private int cost;
// 省略setter/getter 方法
}
2.2 日志解析工具类
笔者编写一个解析单行日志的工具类, 用于将单行日志转换为java 模型.
public class LogLineParserUtil {
/* 正则表达式模式说明:
匹配时间: ([^\[\]]*)
匹配日志级别,线程名称等被中括号包裹的字符串: \[([^\[\]]*)\]
精确匹配字符串start或ended: (start|ended,)
匹配剩余字符:(.*)
*/
// 日志匹配模式表达式. 用括号表示捕获匹配内容, 在后面可以时候用matcher.group(idx)获取捕获的字符串内容
private static final String pattenExp = "([^\\[\\]]*)\\[([^\\[\\]]*)\\]\\[([^\\[\\]]*)\\]\\[([^\\[\\]]*)\\]\\[([^\\[\\]]*)\\]\\[([^\\[\\]]*)\\]-接口:\\[([^\\[\\]]*)\\]\\-(start|ended,)(.*)";
private static Pattern pattern ;
static {
pattern = Pattern.compile(pattenExp);
}
/**
* @Description: 解析单行日志
* @param line 日志
* @return: LogLine
* @author: zongf
* @time: 2019-04-02 11:08:37
*/
public static LogLine parserLine(String line) {
// 匹配单行日志
Matcher matcher = pattern.matcher(line);
boolean isFind = matcher.find();
if (isFind) {
// 匹配成功, 则创建日志模型
LogLine logLine = new LogLine();
// 赋值每个字段
logLine.setTime(matcher.group(1));
logLine.setLevel(matcher.group(2));
logLine.setThread(matcher.group(3));
logLine.setClz(matcher.group(4));
logLine.setSid(matcher.group(5));
logLine.setReqid(matcher.group(6));
logLine.setUri(matcher.group(7));
logLine.setTag(matcher.group(8));
// 如果单行日志类型为ended, 则处理耗时字段
if ("ended,".equals(logLine.getTag())) {
String cost = StringUtils.substringBetween(matcher.group(9),":", "ms");
logLine.setCost(Integer.parseInt(cost));
}
return logLine;
}
//匹配失败, 返回空
return null;
}
}
2.3 测试用例
笔者这里只测试将单行日志解析为java 对象, 对于读取文件, 日志统计排序就不介绍了, 都是java 的基本功.
@Test
public void test_parseLog(){
String[] logs = new String[]{
"2019-04-01 13:38:20.591[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-start",
"2019-04-01 13:38:20.591[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-这是其它日志, 模拟不匹配",
"2019-04-01 13:38:22.367[INFO][http-127.0.0.1-8080-1][org.zongf.test.ApiAccessLogUtil][8BD0EF4739EB58C634D1853B2F7BAF96][67617bd63bb4437295eb6a49601d4b0f]-接口:[/article/1001]-ended, 访问时间:254ms"
};
for (String log : logs) {
LogLine logLine = LogLineParserUtil.parserLine(log);
System.out.println(logLine);
}
}
2.4 解析结果
笔者自定义toString方法, 只输出几个字段用于测试。 从输出结果来看, 由于第二行日志和笔者自定义的正则匹配模式不匹配, 所以并未做正确解析.
{time='2019-04-01 13:38:20.591', cost=0, uri='/article/1001', sid='8BD0EF4739EB58C634D1853B2F7BAF96', reqid='67617bd63bb4437295eb6a49601d4b0f'}
null
{time='2019-04-01 13:38:22.367', cost=254, uri='/article/1001', sid='8BD0EF4739EB58C634D1853B2F7BAF96', reqid='67617bd63bb4437295eb6a49601d4b0f'}