文档章节

译:Spring Boot & Elastic Stack 记录日志

liululee
 liululee
发布于 05/27 17:35
字数 2281
阅读 23
收藏 10

在本文中,我将介绍我的日志库,专门用于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

@Configuration
public 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中,如下所示:

与响应日志每个条目都包含X-Correlation-IDX-Request-IDX-Response-TimeX-Response-Status头。

5.摘要

我的Spring logging library库可以在GitHub的https://github.com/piomin/spring-boot-logging.git中找到。我还在努力,所以非常欢迎任何反馈或建议。该库专用于基于微服务的体系结构,您的应用程序可以在容器内的许多实例中启动。在此模型中,将日志存储在文件中没有任何意义。这就是为什么与Elastic Stack集成非常重要的原因。 但是这个库最重要的特性是将HTTP请求/响应与完整正文和一些附加信息记录到此日志中,如相关ID或请求处理时间。库非常精简,包含在应用程序之后,所有都是开箱即用的。

原文链接:https://piotrminkowski.wordpress.com/2019/05/07/logging-with-spring-boot-and-elastic-stack/

作者: PiotrMińkowski

译者:Yunooa

关注公众号:锅外的大佬,每天分享国外最新技术文章,帮助开发者更好地成长!

本文转载自:http://www.spring4all.com/article/15002

liululee
粉丝 123
博文 49
码字总数 47801
作品 0
杭州
程序员
私信 提问
ElasticSearch开发问题汇总(不断更新中)

1、Mapping: [译]ElasticSearch数据类型--string类型已死, 字符串数据永生 ElasticSearch动态日期映射 2、Spring Data Elasticsearch: Spring Data Elasticsearch教程...

九州暮云
2018/07/18
0
0
Spring Boot2.x与Logstash 6.5.4整合

ELK可以说是当前对分布式服务器集群日志做汇总、分析、统计和检索操作的很好的一套系统了。而Spring Boot作为一套为微服务而生的框架,自然也免不了处理分布式日志的问题,通过ELK日志系统来...

北极南哥
03/06
0
0
使用 Spring Cloud Sleuth、Elastic Stack 和 Zipkin 做微服务监控

关于迁移微服务架构,最常被提及的挑战莫过于监控。每个微服务应独立于其他服务的运行环境,所以他们之间不会共享如数据源、日志文件等资源。 然而,较容易的查看服务的调用历史,并且能够查...

oschina
2018/04/03
2.7K
0
基于Docker部署ELK (Elasticsearch, Logstash, Kibana)集中日志处理平台,及在Spring Boot应用

当我们还是单体部署我们的Spring Boot项目的时候,日志通常都是放在我们的Linux服务器目录,使用简单的Spring Boot已经包含的Logback框架即可实现。当我们基于Docker部署我们的分布式或者集群...

ImWiki
2018/05/27
0
0
spring-data-elasticsearch 基本案例详解(三)

『 风云说:能分享自己职位的知识的领导是个好领导。 』 运行环境:JDK 7 或 8,Maven 3.0+ 技术栈:SpringBoot 1.5+, Spring Data Elasticsearch 1.5+ ,ElasticSearch 2.3.2 本文提纲 一、...

夜黑人模糊灬
2018/05/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux的基本命令

目录的操作命令(增删改查) 增: mkdir 目录名称; 查: ls 可以看到该目录下的所有的目录和文件 ls -a,可以看到该目录下的所有文件和目录,包括隐藏的 ls -l,可以看到该目录下的所有目录和...

凹凸凸
今天
2
0
在古老unix中增加新用户

Installing 4.3 BSD Quasijarus on SIMH 目标:要在4.3BSD中新增加用户dmr,指定目录/home/dmr,uid为10 gid=31(guest组,系统已建立) 4.3BSD还没有adduser或useradd 直接修改/etc/passwd...

wangxuwei
今天
2
0
Bootstrap(六)表单样式

基本样式 所有设置了 .form-control 类的 <input>、<textarea> 和 <select> 元素都将被默认设置宽度属性为 width: 100%;。 将 label 元素和前面提到的控件包裹在 .form-group 中可以获得最好...

ZeroBit
昨天
3
0
SSL 证书格式转换

SSL 证书格式转换 不同服务器情况下,需要不同的证书格式。 比如 pem 转 pfx。 pem在window 平台下可以导入,但是无法正常使用。 需要转换成pfx。 推荐在线转换工具,由中国数字证书网站提供...

DrChenXX
昨天
2
0
HAProxy

xx

Canaan_
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部