1. 程式人生 > >Spring Boot + Elastic stack 記錄日誌

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庫和LogstashElastic Stack整合
  • 對於RestTemplate``和OpenFeign
    ,記錄所有可能發生的日誌
  • 在單個API端點呼叫中跨所有通訊生成和傳遞關聯Id(correlationId)
  • 計算和儲存每個請求的執行時間
  • 可自動配置的庫——除了引入依賴項之外,不必執行任何操作,就能正常工作

1.簡述

我想在閱讀了文章的前言後,你可能會問為什麼我決定構建一個Spring Boot已有功能的庫。但問題是它真的具有這些功能?你可能會感到驚訝,因為答案是否定的。雖然可以使用一些內建的Spring元件例如CommonsRequestLoggingFilter輕鬆地記錄HTTP請求,但是沒有任何用於記錄響應主體(response body)的開箱即用機制。當然你可以基於Spring HTTP攔截器(HandlerInterceptorAdapter)或過濾器(OncePerRequestFilter)實現自定義解決方案,但這並沒有你想的那麼簡單。第二種選擇是使用Zalando Logbook

,它是一個可擴充套件的Java庫,可以為不同的客戶端和伺服器端技術啟用完整的請求和響應日誌記錄。這是一個非常有趣的庫,專門用於記錄HTTP請求和響應,它提供了許多自定義選項並支援不同的客戶端。因此,為了更高階, 你可以始終使用此庫。
我的目標是建立一個簡單的庫,它不僅記錄請求和響應,還提供自動配置,以便將這些日誌傳送到Logstash並關聯它們。它還會自動生成一些有價值的統計資訊,例如請求處理時間。所有這些值都應該傳送到Logstash。我們繼續往下看。

2.實現

從依賴開始吧。我們需要一些基本的Spring庫,它們包含spring-webspring-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 ContentCachingRequestWrapperContentCachingResponseWrapper在這沒什麼用。
這是我的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-IDX-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,以及用於與LogstashRestTemplateHTTP客戶端攔截器整合的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-