Spring Boot + Elastic stack 記錄日誌
原文連結:https://piotrminkowski.wordpress.com/2019/05/07/logging-with-spring-boot-and-elastic-stack/
作者: PiotrMińkowski
譯者:Yunooa
在本文中,我將介紹我的日誌庫,專門用於Spring Boot RESTful Web
應用程式。關於這個庫的主要設想是:
- 使用完整正文記錄所有傳入的HTTP請求和傳出的HTTP響應
- 使用
logstash-logback-encoder
庫和Logstash
與Elastic Stack
整合 - 對於
RestTemplate``和OpenFeign
- 在單個API端點呼叫中跨所有通訊生成和傳遞關聯Id(correlationId)
- 計算和儲存每個請求的執行時間
- 可自動配置的庫——除了引入依賴項之外,不必執行任何操作,就能正常工作
1.簡述
我想在閱讀了文章的前言後,你可能會問為什麼我決定構建一個Spring Boot
已有功能的庫。但問題是它真的具有這些功能?你可能會感到驚訝,因為答案是否定的。雖然可以使用一些內建的Spring
元件例如CommonsRequestLoggingFilter
輕鬆地記錄HTTP
請求,但是沒有任何用於記錄響應主體(response body)的開箱即用機制。當然你可以基於Spring HTTP攔截器(HandlerInterceptorAdapter)或過濾器(OncePerRequestFilter)實現自定義解決方案,但這並沒有你想的那麼簡單。第二種選擇是使用Zalando Logbook
我的目標是建立一個簡單的庫,它不僅記錄請求和響應,還提供自動配置,以便將這些日誌傳送到
Logstash
並關聯它們。它還會自動生成一些有價值的統計資訊,例如請求處理時間。所有這些值都應該傳送到Logstash
。我們繼續往下看。
2.實現
從依賴開始吧。我們需要一些基本的Spring庫,它們包含spring-web
,spring-context
在內,並提供了一些額外的註解。為了與Logstash
logstash-logback-encoder
庫。Slf4j
包含用於日誌記錄的抽象,而javax.servlet-api
用於HTTP通訊。Commons IO
不是必需的,但它提供了一些操作輸入和輸出流的有用方法。
<properties>
<java.version>11</java.version>
<commons-io.version>2.6</commons-io.version>
<javax-servlet.version>4.0.1</javax-servlet.version>
<logstash-logback.version>5.3</logstash-logback.version>
<spring.version>5.1.6.RELEASE</spring.version>
<slf4j.version>1.7.26</slf4j.version></properties><dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash-logback.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax-servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency></dependencies>
第一步是實現HTTP請求和響應包裝器。我們必須這樣做,因為無法讀取HTTP流兩次。如果想記錄請求或響應正文,在處理輸入流或將其返回給客戶端之前,首先必須讀取輸入流。Spring提供了HTTP請求和響應包裝器的實現,但由於未知原因,它們僅支援某些特定用例,如內容型別application/x-www-form-urlencoded
。因為我們通常在RESTful應用程式之間的通訊中使用aplication/json
內容型別,所以Spring ContentCachingRequestWrapper
和ContentCachingResponseWrapper
在這沒什麼用。
這是我的HTTP請求包裝器的實現,可以通過各種方式完成。這只是其中之一:
public class SpringRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public SpringRequestWrapper(HttpServletRequest request) {
super(request);
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException ex) {
body = new byte[0];
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
public boolean isFinished() {
return false;
}
public boolean isReady() {
return true;
}
public void setReadListener(ReadListener readListener) {
}
ByteArrayInputStream byteArray = new ByteArrayInputStream(body);
@Override
public int read() throws IOException {
return byteArray.read();
}
};
}
}
輸出流必須做同樣的事情,這個實現有點複雜:
public class SpringResponseWrapper extends HttpServletResponseWrapper {
private ServletOutputStream outputStream;
private PrintWriter writer;
private ServletOutputStreamWrapper copier;
public SpringResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}
if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new ServletOutputStreamWrapper(outputStream);
}
return copier;
}
@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}
if (writer == null) {
copier = new ServletOutputStreamWrapper(getResponse().getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
}
else if (outputStream != null) {
copier.flush();
}
}
public byte[] getContentAsByteArray() {
if (copier != null) {
return copier.getCopy();
}
else {
return new byte[0];
}
}
}
我將ServletOutputStream
包裝器實現放到另一個類中:
public class ServletOutputStreamWrapper extends ServletOutputStream {
private OutputStream outputStream;
private ByteArrayOutputStream copy;
public ServletOutputStreamWrapper(OutputStream outputStream) {
this.outputStream = outputStream;
this.copy = new ByteArrayOutputStream();
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
copy.write(b);
}
public byte[] getCopy() {
return copy.toByteArray();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
因為我們需要在處理之前包裝HTTP請求流和響應流,所以我們應該使用HTTP過濾器。Spring提供了自己的HTTP過濾器實現。我們的過濾器擴充套件了它,並使用自定義請求和響應包裝來記錄有效負載。此外,它還生成和設定X-Request-ID
,X-Correlation-ID
header和請求處理時間。
public class SpringLoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringLoggingFilter.class);
private UniqueIDGenerator generator;
public SpringLoggingFilter(UniqueIDGenerator generator) {
this.generator = generator;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
generator.generateAndSetMDC(request);
final long startTime = System.currentTimeMillis();
final SpringRequestWrapper wrappedRequest = new SpringRequestWrapper(request);
LOGGER.info("Request: method={}, uri={}, payload={}", wrappedRequest.getMethod(),
wrappedRequest.getRequestURI(), IOUtils.toString(wrappedRequest.getInputStream(),
wrappedRequest.getCharacterEncoding()));
final SpringResponseWrapper wrappedResponse = new SpringResponseWrapper(response);
wrappedResponse.setHeader("X-Request-ID", MDC.get("X-Request-ID"));
wrappedResponse.setHeader("X-Correlation-ID", MDC.get("X-Correlation-ID"));
chain.doFilter(wrappedRequest, wrappedResponse);
final long duration = System.currentTimeMillis() - startTime;
LOGGER.info("Response({} ms): status={}, payload={}", value("X-Response-Time", duration),
value("X-Response-Status", wrappedResponse.getStatus()),
IOUtils.toString(wrappedResponse.getContentAsByteArray(), wrappedResponse.getCharacterEncoding()));
}
}
3.自動配置
完成包裝器和HTTP過濾器的實現後,我們可以為庫準備自動配置。第一步是建立@Configuration
包含所有必需的bean。我們必須註冊自定義HTTP過濾器SpringLoggingFilter
,以及用於與Logstash
和RestTemplate
HTTP客戶端攔截器整合的logger appender
:
@Configurationpublic class SpringLoggingAutoConfiguration {
private static final String LOGSTASH_APPENDER_NAME = "LOGSTASH";
@Value("${spring.logstash.url:localhost:8500}")
String url;
@Value("${spring.application.name:-}")
String name;
@Bean
public UniqueIDGenerator generator() {
return new UniqueIDGenerator();
}
@Bean
public SpringLoggingFilter loggingFilter() {
return new SpringLoggingFilter(generator());
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptorList = new ArrayList<ClientHttpRequestInterceptor>();
restTemplate.setInterceptors(interceptorList);
return restTemplate;
}
@Bean
public LogstashTcpSocketAppender logstashAppender() {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
LogstashTcpSocketAppender logstashTcpSocketAppender = new LogstashTcpSocketAppender();
logstashTcpSocketAppender.setName(LOGSTASH_APPENDER_NAME);
logstashTcpSocketAppender.setContext(loggerContext);
logstashTcpSocketAppender.addDestination(url);
LogstashEncoder encoder = new LogstashEncoder();
encoder.setContext(loggerContext);
encoder.setIncludeContext(true);
encoder.setCustomFields("{\"appname\":\"" + name + "\"}");
encoder.start();
logstashTcpSocketAppender.setEncoder(encoder);
logstashTcpSocketAppender.start();
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(logstashTcpSocketAppender);
return logstashTcpSocketAppender;
}
}
庫中的配置集合由Spring Boot
載入。Spring Boot
會檢查已釋出jar
中是否存在META-INF/spring.factories
檔案。該檔案應列出key等於EnableAutoConfiguration
的配置類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pl.piomin.logging.config.SpringLoggingAutoConfiguration
4.與Logstash整合
通過自動配置的日誌記錄追加器(logging appender)實現與Logstash
整合。我們可以通過在application.yml
檔案中設定屬性spring.logstash.url
來覆蓋Logstash
目標URL:
spring: application: name: sample-app logstash: url: 192.168.99.100:5000
要在應用程式中啟用本文中描述的所有功能,只需要將我的庫包含在依賴項中:
<dependency>
<groupId>pl.piomin</groupId>
<artifactId>spring-boot-logging</artifactId>
<version>1.0-SNAPSHOT</version></dependency>
在執行應用程式之前,您應該在計算機上啟動Elastic Stack
。最好的方法是通過Docker
容器。但首先要建立Docker
網路,以通過容器名稱啟用容器之間的通訊。
$ docker network create es
現在,在埠9200啟動Elasticsearch
的單個節點例項,我使用版本為6.7.2
的Elastic Stack工具:
$ docker run -d --name elasticsearch --net es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.7.2
執行Logstash
時,需要提供包含輸入和輸出定義的其他配置。我將使用JSON編解碼器啟動TCP輸入,預設情況下不啟用。Elasticsearch
URL設定為輸出。它還將建立一個包含應用程式名稱的索引。
input {
tcp {
port => 5000
codec => json
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "micro-%{appname}"
}
}
現在我們可以使用Docker
容器啟動Logstash
。它在埠5000上公開並從logstash.conf
檔案中讀取配置:
docker run -d --name logstash --net es -p 5000:5000 -v ~/logstash.conf:/usr/share/logstash/pipeline/logstash.conf docker.elastic.co/logstash/logstash:6.7.2
最後,我們可以執行僅用於顯示日誌的Kibana
:
$ docker run -d --name kibana --net es -e "ELASTICSEARCH_URL=http://elasticsearch:9200" -p 5601:5601 docker.elastic.co/kibana/kibana:6.7.2
啟動使用spring-boot-logging
庫的示例應用程式後,POST
請求中的日誌將顯示在Kibana
中,如下所示:
相關推薦
Spring Boot + Elastic stack 記錄日誌
原文連結:https://piotrminkowski.wordpress.com/2019/05/07/logging-with-spring-boot-and-elastic-stack/ 作者: PiotrMińkowski 譯者:Yunooa 在本文中,我將介紹我的日誌
spring-boot 整合 log4j 記錄日誌
1.pom檔案中移除和新增依賴 <!-- 移除boot—starter 的log4j --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>sprin
Spring Boot 使用AOP記錄操作日誌
前言 在寫Aurora這個專案之前,我是通過攔截器去記錄使用者操作日誌,而這裡講解如何使用AOP配合自定義註解去實現使用者日誌記錄,使用攔截器的方式,下一章貼出 匯入依賴 <!-- aop依賴 --> <dependency> &
Spring中使用Log4j記錄日誌
() 歸檔 msg 含義 多個 appenders policy rop git 以下內容引用自http://wiki.jikexueyuan.com/project/spring/logging-with-log4j.html: 例子: pom.xml: <
spring boot自定義log4j2日誌文件
-m logs cnblogs port evel 一個 efault 實踐 示例 背景:因為從 spring boot 1.4開始的版本就要用log4j2 了,支持的格式有json和xml兩種格式,此次實踐主要使用的是xml的格式定義日誌說明。 spring boot 1
spring boot 系列學習記錄
初步 jpa 系列 數據庫連接池 學習記錄 工程結構 json數據 ide json ——初始篇 結束了短學期的課程,初步學習了ssm框架,憑借這些學到的知識完成了短學期的任務-----點餐系統。 通過學長了解到了spring boot ,自己對spring cloud
Spring Boot 集成 logback日誌
AS con enc files console 格式化 Coding utf 默認 application.properties 配置logback.xml 路徑註:如果logback.xml在默認的 src/main/resources 目錄下則不需要配置applic
spring boot 集成logstash 日誌
encode clu bug ces logback search 1.0 地址 evel 1、logstash 插件配置 logstash下config文件夾下添加 test.conf 文件內容: input{ tcp {
Spring Boot 報錯記錄
odi host localhost char jdb name pri exclude encoding Spring Boot 報錯記錄 由於新建的項目沒有配置數據庫連接啟動報錯,可以通過取消自動數據源自動配置來解決 解決方案1: @SpringBootAppli
spring boot 列印mybatis sql日誌資訊
如果使用的是application.properties檔案,加入如下配置: logging.level.com.example.demo.dao=debug logging.level.com,後面的路徑指的是mybatis對應的方法介面所在的包。並不是mapper.xml所在的包。
Spring boot 使用Logback列印日誌
Logback是什麼 Logback是由log4j創始人設計的又一個開源日誌元件,它的使用非常簡單靈活,是目前主流的日誌記錄工具。 slf4j log4j logback關係 籠統的講就是slf4j是一系列的日誌介面,而log4j logback是具體實現了的日誌框架。官網文件中
spring-boot mybatis 配置log4j2日誌
<!--配置log4j2--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-l
Spring Boot(08)——日誌輸出
日誌輸出 使用了spring-boot-starter後,將新增spring-boot-starter-logging依賴,此時Spring Boot將使用logback進行日誌輸出,預設只會輸出INFO級別以上的日誌資訊,且只會輸出到控制檯。預設的日誌格式是如下這樣的。前面是日誌輸出時間,INFO是日誌級
IDEA編輯Spring-Boot Web,設定日誌級別,並列印到相應的目錄下。
我使用的日誌框架是logback。我們的目的是把INFO日誌和ERROR分開到不同的檔案中,並且能夠每日形成一個日誌檔案。 第一步,使用idea建立一個Spring-Boot的專案 一直預設,有關SpringBoot基礎建立都不清楚的話,可以先去看看入門教程,到這步的
【Spring Boot課程】4.日誌
1 日誌框架的選擇 1.1 框架一覽 JUL、JCL、JBoss-logging、log4j、log4j2、slf4j等。 日誌門面(抽象層) 日誌實現 JCL(Jakra Commons
eclipse spring-boot-mybatis 的記錄
例子來源: https://gitee.com/lfalex/spring-boot-example.git spring-boot-mybatis 例子使用 mysql5.1.46 版本; 環境:eclipse+Spring Tools+mysql8.
Spring Boot 微服務叢集日誌等
API spring cloud gateway 日誌搜尋 elk(ElasticSearch,Logstash,Kibana) 持續整合 Jenkins 程式碼質量
Spring Boot 整合 log4j2 實現日誌管理
摘要:上一篇,我們講了Spring Boot 整合 log4j實現日誌管理,這一篇接著說一下Spring Boot 整合 log4j2,。 一:還是新建一個java工程: 二:增加log4j2的pom.xml配置,如下: <project xmlns="htt
Spring Boot踩坑記錄
一.啟動的時候報錯 Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package. 導致原因:翻譯一下就是由於預設包的@ComponentScan,您的Applicati
spring boot elastic job 整合
1、引入jar <dependency> <groupId>com.github.kuhn-he</groupId> <artifactId>elastic-job-