Spring Boot日志收集及鏈路追蹤實(shí)現(xiàn)示例
正文
Spring Boot版本:2.3.4.RELEASE
最基本的日志功能及自定義日志
添加logback依賴:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.3</version>
</dependency>
在application配置文件中設(shè)置日志保存路徑:
server:
port: 8888
# 日志保存路徑
logging:
file:
path: _logs/mylog-${server.port}.logs
關(guān)于logback的配置文件
logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--解決在項(xiàng)目目錄中生成LOG_PATH_IS_UNDEFINED文件-->
<property name="LOG_PATH" value="${LOG_PATH:-${java.io.tmpdir:-/logs}}"/>
<!-- 引入SpringBoot的默認(rèn)配置文件defaults.xml -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 引入SpringBoot中內(nèi)置的控制臺(tái)輸出配置文件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"/>
<!-- 設(shè)置root logger的級(jí)別為INFO,并將控制臺(tái)輸出和文件輸出中的appender都添加到root logger下 -->
<root level="INFO">
<!--沒有這行,控制臺(tái)將不會(huì)有輸出,完全由日志進(jìn)行輸出-->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- jmx可以動(dòng)態(tài)管理logback配置-->
<jmxConfigurator/>
</configuration>
logback-spring-file.level.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用來(lái)標(biāo)記分割日志的序號(hào) -->
<fileNamePattern>${LOG_PATH}.INFOLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 單個(gè)日志文件最大maxFileSizeMB, 保存maxHistory天的歷史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 經(jīng)過(guò)試驗(yàn),maxHistory是指指定天數(shù)內(nèi),而不是多少天-->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的級(jí)別過(guò)濾器,只保留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用來(lái)標(biāo)記分割日志的序號(hào) -->
<fileNamePattern>${LOG_PATH}.WARNLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 單個(gè)日志文件最大maxFileSizeMB, 保存maxHistory天的歷史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--過(guò)濾級(jí)別-->
<level>WARN</level>
<!--onMatch:符合過(guò)濾級(jí)別的日志。ACCEPT:立即處理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合過(guò)濾級(jí)別的日志。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用來(lái)標(biāo)記分割日志的序號(hào) -->
<fileNamePattern>${LOG_PATH}.ERRORLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 單個(gè)日志文件最大maxFileSizeMB, 保存maxHistory天的歷史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
<!--<cleanHistoryOnStart>true</cleanHistoryOnStart>-->
</rollingPolicy>
<!--對(duì)指定級(jí)別的日志進(jìn)行過(guò)濾-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--過(guò)濾級(jí)別-->
<level>ERROR</level>
<!--onMatch:符合過(guò)濾級(jí)別的日志。ACCEPT:立即處理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合過(guò)濾級(jí)別的日志。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用來(lái)標(biāo)記分割日志的序號(hào) -->
<fileNamePattern>${LOG_PATH}.MYLOGGERLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 單個(gè)日志文件最大maxFileSizeMB, 保存maxHistory天的歷史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 經(jīng)過(guò)試驗(yàn),maxHistory是指指定天數(shù)內(nèi),而不是多少天-->
<maxFileSize>300MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>300MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的級(jí)別過(guò)濾器,只保留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>
配置文件里的注釋比較詳細(xì),可以根據(jù)需要自行修改,配置里有一個(gè)“traceId”,這不是logback自帶的,是我為了實(shí)現(xiàn)日志追蹤而添加的,后面會(huì)說(shuō)到。
寫接口來(lái)測(cè)試一下:
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 "輸出 自定義 日志";
}
}
啟動(dòng)項(xiàng)目,分別執(zhí)行測(cè)試接口,然后我們就可以在_logs文件夾內(nèi)看到4個(gè)日志文件,分別是記錄啟動(dòng)信息的INFO日志、記錄警告的WARN日志、記錄錯(cuò)誤的ERROR日志以及自定義的MYLOGGER日志。
日志鏈路追蹤
我們給HTTP請(qǐng)求賦予一個(gè)traceId,這個(gè)traceId將貫穿整個(gè)請(qǐng)求,請(qǐng)求過(guò)程中所有的日志都會(huì)記錄traceId,由此達(dá)到快速定位問題和過(guò)濾無(wú)關(guān)日志的效果。
為了好看些,我們定義一個(gè)常量類:
package com.cc.config.logback;
/**
* Logback常量定義
* @author cc
* @date 2021-07-12 10:41
*/
public interface LogbackConstant {
String TRACT_ID = "traceId";
}
然后是logback過(guò)濾器:
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,現(xiàn)在再調(diào)用測(cè)試接口,可以看到日志中附帶的traceId。
EFK日志收集系統(tǒng)
EFK是指Elasticsearch、Filebeat、和Kibana,原本還有個(gè)logstash,但是logstash的使用沒有filebeat簡(jiǎn)單,并且它的內(nèi)容過(guò)濾功能并不是剛需,所以就不加上了,但是后面仍然會(huì)附帶logstash的簡(jiǎn)要介紹。
先說(shuō)明一下EKF的工作流程
- Spring Boot應(yīng)用的日志會(huì)保存在指定的路徑
- filbeat會(huì)檢測(cè)到日志文件的變化,并將內(nèi)容發(fā)送到elasticsearch
如果使用logstash,則會(huì)將內(nèi)容發(fā)送到logstash
logstash將內(nèi)容進(jìn)行過(guò)濾分析以及格式轉(zhuǎn)換等操作,再發(fā)送給elasticsearch,這種處理會(huì)使日志數(shù)據(jù)在kibana上顯示的更加詳細(xì)。
- 訪問kibana可視化界面,在kibana中操作或查看elasticseach的保存的日志數(shù)據(jù)
環(huán)境準(zhǔn)備
因?yàn)槲业腅FK環(huán)境是搭建在虛擬機(jī)的docker上,本機(jī)是Windows,所以為了讓docker上的filebeat容器能檢測(cè)到我的日志文件變化,我有兩種方案:
- 將項(xiàng)目部署成jar包在虛擬機(jī)的Linux上運(yùn)行,并將日志保存路徑設(shè)置到指定位置
- 本機(jī)和虛擬機(jī)建立共享文件夾
因?yàn)閂MWare建立共享文件夾十分簡(jiǎn)單,并且我也能在本地開發(fā)環(huán)境實(shí)時(shí)更新代碼,所以選擇了方案1。
容器創(chuàng)建
這里假設(shè)讀者對(duì)docker有一定的了解,畢竟關(guān)于docker的介紹篇幅不小,而且也與主題無(wú)關(guān),就不在這里細(xì)說(shuō)了。
# 創(chuàng)建一個(gè)網(wǎng)絡(luò),用于容器間的通訊 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的容器文件映射路徑要注意,映射到了我的共享文件夾,所以不一定和大家一樣,按需修改即可。
并且為了方便,我們直接映射了一個(gè)filebeat.yml配置文件到filebeat容器內(nèi),省的后面再進(jìn)去修改了。
filebeat.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"]
配置文件說(shuō)明:
filebeat.inputs: // 輸入源
- type: log // 標(biāo)注這是日志類型
enabled: true // 啟用功能
paths: // 路徑
- /data/*.log // filebeat容器內(nèi)的/data文件夾下,所有后綴為.log的文件
output.elasticsearch: // 輸出位置:elasticsearch,后面簡(jiǎn)稱es
hosts: ['myes'] // es的鏈接,因?yàn)槲覀冏隽司W(wǎng)段所以可以通過(guò)容器名進(jìn)行通訊
index: "filebeat-%{+yyyy-MM-dd}" // 自定義es索引
setup.template.name: "filebeat" // 配置了索引,就需要設(shè)置這兩項(xiàng)
setup.template.pattern: "filebeat-*" // 配置了索引,就需要設(shè)置這兩項(xiàng)
processors: // 處理器
- drop_fields: // 過(guò)濾或者叫移除指定字段,因?yàn)檫M(jìn)入es的數(shù)據(jù)默認(rèn)會(huì)帶上這些
fields: ["log","input","host","agent","ecs"]
讓kibana連接到elasticsearch
進(jìn)入kibana容器中,修改配置文件并重啟:
docker exec -it mykibana bash
cd config/
vi kibana.yml
原內(nèi)容:
server.name: kibana server.host: "0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] monitoring.ui.container.elasticsearch.enabled: true
修改成:
server.name: kibana server.host: "0" elasticsearch.hosts: [ "http://myes:9200" ] monitoring.ui.container.elasticsearch.enabled: true
可以看出,如果es的容器名就是elasticsearch的話,就可以不用改。
測(cè)試容器有效性
elasticsearch:訪問 http://ip:9200 ,有json內(nèi)容出現(xiàn)則成功
kibana:訪問 http://ip:5601 ,沒有報(bào)錯(cuò),出現(xiàn)可視化UI界面則成功,如果失敗,基本是連接問題,請(qǐng)確認(rèn)配置文件內(nèi)連接elasticsearch的內(nèi)容是否正確,確認(rèn)容器間是否在同一個(gè)網(wǎng)段可以進(jìn)行通訊,調(diào)試時(shí)可以在容器內(nèi)互相ping進(jìn)行確認(rèn)。
filebeat:等會(huì)進(jìn)kibana可視化界面就能知道
測(cè)試效果
調(diào)用接口 localhost:8888/w,或者是e/m接口,以輸出日志內(nèi)容到指定位置。此時(shí)filebeat已經(jīng)能檢測(cè)到文件內(nèi)容變更并推送到elasticsearch
- 在指定的目錄可以看到輸出的日志文件,則說(shuō)明日志文件保存成功。
- 打開kibana可視化面板:IP:5061,點(diǎn)擊左上角的三橫線圖標(biāo),顯示菜單,找到Analytics-Discover,第一次進(jìn)需要?jiǎng)?chuàng)建Index Patterns,因?yàn)槲覀冊(cè)趂ilebeat.yml中設(shè)置的索引是filebeat,所以這里也要用上,填寫了filebeat之后可以看到有匹配項(xiàng),下一步,步驟2選擇時(shí)間過(guò)濾器,然后確定即可。此時(shí)已經(jīng)可以看到logback->filebeat->elasticsearch的日志內(nèi)容,然后借助kibana面板就能方便的進(jìn)行數(shù)據(jù)檢索了。
至此,EFK入門級(jí)部署完成。
用Golang手?jǐn)]一個(gè)輕量級(jí)日志收集工具
EFK使用方便,界面美觀,并且還支持分布式,可以說(shuō)十分好用了,但是因?yàn)槲业姆?wù)器內(nèi)存沒有那么充裕,用EFK的話要消耗接近1G,所以我選擇了另一種方案:用Golang寫一個(gè)服務(wù),結(jié)合Linux的grep指令,從日志文件中提取匹配的內(nèi)容。這種方案好處是用Golang寫,內(nèi)存占用很低,缺點(diǎn)是搜索效率低,但是對(duì)于我的小項(xiàng)目來(lái)說(shuō)正合適。
附上Golang的代碼,其實(shí)原理很簡(jiǎn)單,就是使用Gin框架啟動(dòng)一個(gè)Web服務(wù),然后調(diào)用shell腳本提取內(nèi)容:
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:
cd /Users/chen/Desktop/mycontainers/mall-business/data/logs id=$1 grep $id *.log%
將這個(gè)Golang應(yīng)用打包到指定平臺(tái)運(yùn)行即可。
以上就是Spring Boot日志收集及鏈路追蹤實(shí)現(xiàn)示例的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot日志收集鏈路追蹤的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis實(shí)現(xiàn)多表聯(lián)合查詢r(jià)esultType的返回值
這篇文章主要介紹了MyBatis多表聯(lián)合查詢r(jià)esultType的返回值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringBoot中的@Configuration注解詳解
這篇文章主要介紹了SpringBoot中的@Configuration注解詳解,Spring Boot推薦使用JAVA配置來(lái)完全代替XML 配置,JAVA配置就是通過(guò) @Configuration和 @Bean兩個(gè)注解實(shí)現(xiàn)的,需要的朋友可以參考下2023-08-08
java學(xué)生信息管理系統(tǒng)設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了java學(xué)生信息管理系統(tǒng)設(shè)計(jì),學(xué)生信息添加進(jìn)入數(shù)據(jù)庫(kù)的事務(wù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Spring Boot使用AOP防止重復(fù)提交的方法示例
這篇文章主要介紹了Spring Boot使用AOP防止重復(fù)提交的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05
使用springboot activiti關(guān)閉驗(yàn)證自動(dòng)部署方式
這篇文章主要介紹了使用springboot activiti關(guān)閉驗(yàn)證自動(dòng)部署方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
RSA加密算法java簡(jiǎn)單實(shí)現(xiàn)方法(必看)
下面小編就為大家?guī)?lái)一篇RSA加密算法java簡(jiǎn)單實(shí)現(xiàn)方法(必看)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
springBoot啟動(dòng)輸出三行日志控制臺(tái)自動(dòng)停止操作
這篇文章主要介紹了springBoot啟動(dòng)輸出三行日志控制臺(tái)自動(dòng)停止操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

