Spring Boot日志收集以及链路追踪

gitee链接

Spring Boot版本:2.3.4.RELEASE

目录

  • 最基本的日志功能及自定义日志
  • 日志链路追踪
  • EFK日志收集系统
  • 用Golang手撸一个轻量级日志收集工具

最基本的日志功能及自定义日志

添加logback依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.3</version>
</dependency>

在application配置文件中设置日志保存路径:

1
2
3
4
5
6
7
yml复制代码server:
port: 8888

# 日志保存路径
logging:
file:
path: _logs/mylog-${server.port}.logs

关于logback的配置文件

logback-spring.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--解决在项目目录中生成LOG_PATH_IS_UNDEFINED文件-->
<property name="LOG_PATH" value="${LOG_PATH:-${java.io.tmpdir:-/logs}}"/>
<!-- 引入SpringBoot的默认配置文件defaults.xml -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 引入SpringBoot中内置的控制台输出配置文件console-appender.xml -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 引入自定义的文件输出配置文件logback-spring-file-level.xml -->
<include resource="logback-spring-file-level.xml"/>

<!-- 设置root logger的级别为INFO,并将控制台输出和文件输出中的appender都添加到root logger下 -->
<root level="INFO">
<!--没有这行,控制台将不会有输出,完全由日志进行输出-->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>

<!-- jmx可以动态管理logback配置-->
<jmxConfigurator/>
</configuration>

logback-spring-file.level.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<included>
<!--INFO Level的日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.INFOLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 经过试验,maxHistory是指指定天数内,而不是多少天-->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的级别过滤器,只保留INFO Level的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 格式化输出-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level -[%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--WARN Level的日志-->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.WARNLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--过滤级别-->
<level>WARN</level>
<!--onMatch:符合过滤级别的日志。ACCEPT:立即处理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合过滤级别的日志。DENY:立即抛弃-->
<onMismatch>DENY</onMismatch>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level -[%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--ERROR Level的日志-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.ERRORLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
<!--<cleanHistoryOnStart>true</cleanHistoryOnStart>-->
</rollingPolicy>
<!--对指定级别的日志进行过滤-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--过滤级别-->
<level>ERROR</level>
<!--onMatch:符合过滤级别的日志。ACCEPT:立即处理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合过滤级别的日志。DENY:立即抛弃-->
<onMismatch>DENY</onMismatch>
</filter>
<!--日志输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level - [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--自定义日志-->
<appender name="CUSTOM_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.MYLOGGERLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 经过试验,maxHistory是指指定天数内,而不是多少天-->
<maxFileSize>300MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>300MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的级别过滤器,只保留INFO Level的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 格式化输出-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"}\t%X{traceId}\t%msg%n</pattern>
</encoder>
</appender>
<!--自定义日志日志不用绑定在root下,只记录指定输出-->
<logger name="my_logger" additivity="false">
<appender-ref ref= "CUSTOM_FILE"/>
</logger>
</included>

配置文件里的注释比较详细,可以根据需要自行修改,配置里有一个“traceId”,这不是logback自带的,是我为了实现日志追踪而添加的,后面会说到。

写接口来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码package com.cc.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.logging.Logger;

@RestController
public class TestController {
Logger LOGGER = Logger.getLogger(this.getClass().toString());
Logger MyLogger = Logger.getLogger("my_logger");

@GetMapping("/w")
public String logWarning() {
LOGGER.warning("这是一段 warning 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 warning 日志";
}

@GetMapping("/e")
public String logError() {
LOGGER.severe("这是一段 error 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 error 日志";
}

@GetMapping("/m")
public String logMyLogger() {
MyLogger.info("这是一段 自定义 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 自定义 日志";
}
}

启动项目,分别执行测试接口,然后我们就可以在_logs文件夹内看到4个日志文件,分别是记录启动信息的INFO日志、记录警告的WARN日志、记录错误的ERROR日志以及自定义的MYLOGGER日志。

日志链路追踪

我们给HTTP请求赋予一个traceId,这个traceId将贯穿整个请求,请求过程中所有的日志都会记录traceId,由此达到快速定位问题和过滤无关日志的效果。

为了好看些,我们定义一个常量类:

1
2
3
4
5
6
7
8
9
10
java复制代码package com.cc.config.logback;

/**
* Logback常量定义
* @author cc
* @date 2021-07-12 10:41
*/
public interface LogbackConstant {
String TRACT_ID = "traceId";
}

然后是logback过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码package com.cc.config.logback;

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

/**
* 日志追踪id
* @author cc
* @date 2021-07-12 10:41
*/
@Component
public class LogbackFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
MDC.put(LogbackConstant.TRACT_ID, UUID.randomUUID().toString().replace("-", ""));
chain.doFilter(request, response);
} finally {
MDC.remove(LogbackConstant.TRACT_ID);
}
}

@Override
public void destroy() {}
}

我们用处理后的UUID作为traceId,现在再调用测试接口,可以看到日志中附带的traceId。

EFK日志收集系统

EFK是指ElasticsearchFilebeat、和Kibana,原本还有个logstash,但是logstash的使用没有filebeat简单,并且它的内容过滤功能并不是刚需,所以就不加上了,但是后面仍然会附带logstash的简要介绍。

先说明一下EKF的工作流程
  1. Spring Boot应用的日志会保存在指定的路径
  2. filbeat会检测到日志文件的变化,并将内容发送到elasticsearch
    1. 如果使用logstash,则会将内容发送到logstash
    2. logstash将内容进行过滤分析以及格式转换等操作,再发送给elasticsearch,这种处理会使日志数据在kibana上显示的更加详细。
  3. 访问kibana可视化界面,在kibana中操作或查看elasticseach的保存的日志数据
环境准备

因为我的EFK环境是搭建在虚拟机的docker上,本机是Windows,所以为了让docker上的filebeat容器能检测到我的日志文件变化,我有两种方案:

  1. 将项目部署成jar包在虚拟机的Linux上运行,并将日志保存路径设置到指定位置
  2. 本机和虚拟机建立共享文件夹

因为VMWare建立共享文件夹十分简单,并且我也能在本地开发环境实时更新代码,所以选择了方案1。

容器创建

这里假设读者对docker有一定的了解,毕竟关于docker的介绍篇幅不小,而且也与主题无关,就不在这里细说了。

1
2
3
4
5
6
7
8
bash复制代码# 创建一个网络,用于容器间的通讯
docker network create mynetwork

docker run --name myes -p 9200:9200 -p 9300:9300 -itd --restart=always -v /etc/localtime:/etc/localtime -v /home/mycontainers/myes/data:/data --net mynetwork -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms256m -Xmx256m" elasticsearch:7.12.0

docker run --name myfilebeat -itd --restart=always -v /etc/localtime:/etc/localtime -v /mnt/hgfs/myshare/_logs:/data --net mynetwork -v /home/filebeat.yml:/usr/share/filebeat/filebeat.yml elastic/filebeat:7.12.0

docker run --name mykibana -p 5601:5601 -itd --restart=always -v /etc/localtime:/etc/localtime --net mynetwork -m 512m --privileged=true kibana:7.12.0

上面filebeat的容器文件映射路径要注意,映射到了我的共享文件夹,所以不一定和大家一样,按需修改即可。

并且为了方便,我们直接映射了一个filebeat.yml配置文件到filebeat容器内,省的后面再进去修改了。

filebeat.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码filebeat.inputs:
- type: log
enabled: true
paths:
- /data/*.log

output.elasticsearch:
hosts: ['myes']
index: "filebeat-%{+yyyy-MM-dd}"
setup.template.name: "filebeat"
setup.template.pattern: "filebeat-*"

processors:
- drop_fields:
fields: ["log","input","host","agent","ecs"]

配置文件说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码filebeat.inputs:	// 输入源
- type: log // 标注这是日志类型
enabled: true // 启用功能
paths: // 路径
- /data/*.log // filebeat容器内的/data文件夹下,所有后缀为.log的文件

output.elasticsearch: // 输出位置:elasticsearch,后面简称es
hosts: ['myes'] // es的链接,因为我们做了网段所以可以通过容器名进行通讯
index: "filebeat-%{+yyyy-MM-dd}" // 自定义es索引
setup.template.name: "filebeat" // 配置了索引,就需要设置这两项
setup.template.pattern: "filebeat-*" // 配置了索引,就需要设置这两项

processors: // 处理器
- drop_fields: // 过滤或者叫移除指定字段,因为进入es的数据默认会带上这些
fields: ["log","input","host","agent","ecs"]
让kibana连接到elasticsearch

进入kibana容器中,修改配置文件并重启:

docker exec -it mykibana bash

cd config/

vi kibana.yml

原内容:

1
2
3
4
yml复制代码server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true

修改成:

1
2
3
4
yml复制代码server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://myes:9200" ]
monitoring.ui.container.elasticsearch.enabled: true

可以看出,如果es的容器名就是elasticsearch的话,就可以不用改。

测试容器有效性

elasticsearch:访问 http://ip:9200 ,有json内容出现则成功

kibana:访问 http://ip:5601 ,没有报错,出现可视化UI界面则成功,如果失败,基本是连接问题,请确认配置文件内连接elasticsearch的内容是否正确,确认容器间是否在同一个网段可以进行通讯,调试时可以在容器内互相ping进行确认。

filebeat:等会进kibana可视化界面就能知道

测试效果

调用接口 localhost:8888/w,或者是e/m接口,以输出日志内容到指定位置。此时filebeat已经能检测到文件内容变更并推送到elasticsearch

  • 在指定的目录可以看到输出的日志文件,则说明日志文件保存成功。
  • 打开kibana可视化面板:IP:5061,点击左上角的三横线图标,显示菜单,找到Analytics-Discover,第一次进需要创建Index Patterns,因为我们在filebeat.yml中设置的索引是filebeat,所以这里也要用上,填写了filebeat之后可以看到有匹配项,下一步,步骤2选择时间过滤器,然后确定即可。此时已经可以看到logback->filebeat->elasticsearch的日志内容,然后借助kibana面板就能方便的进行数据检索了。

至此,EFK入门级部署完成。

用Golang手撸一个轻量级日志收集工具

EFK使用方便,界面美观,并且还支持分布式,可以说十分好用了,但是因为我的服务器内存没有那么充裕,用EFK的话要消耗接近1G,所以我选择了另一种方案:用Golang写一个服务,结合Linux的grep指令,从日志文件中提取匹配的内容。这种方案好处是用Golang写,内存占用很低,缺点是搜索效率低,但是对于我的小项目来说正合适。

附上Golang的代码,其实原理很简单,就是使用Gin框架启动一个Web服务,然后调用shell脚本提取内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
go复制代码package main

import (
"fmt"
"os/exec"
"github.com/gin-gonic/gin"
)

func main() {
runServer()
}

func runServer() {
r := gin.Default()

r.GET("/log", func(c *gin.Context) {
id := c.Query("id")
result := runScript("./findLog.sh " + id)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, result)
})

r.Run(":18085")
}

func runScript(path string) string {
cmd := exec.Command("/bin/bash", "-c", path)

output, err := cmd.Output()
if err != nil {
fmt.Printf("Execute Shell:%s failed with error:%s", path, err.Error())
return err.Error()
}
fmt.Printf("Execute Shell:%s finished with output:\n%s", path, string(output))
return string(output)
}

findLog.sh:

1
2
3
sh复制代码cd /Users/chen/Desktop/mycontainers/mall-business/data/logs
id=$1
grep $id *.log%

将这个Golang应用打包到指定平台运行即可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%