在本文中,我将介绍个人日志库,专门用于Spring Boot RESTful Web
应用程序。关于这个库的主要设想是:java
logstash-logback-encoder
库和Logstash
与Elastic Stack
集成RestTemplate``和OpenFeign
,记录全部可能发生的日志我想在阅读了文章的前言后,你可能会问为何我决定构建一个Spring Boot
已有功能的库。但问题是它真的具备这些功能?你可能会感到惊讶,由于答案是否认的。虽然可使用一些内置的Spring
组件例如CommonsRequestLoggingFilter
轻松地记录HTTP
请求,可是没有任何用于记录响应主体(response body)的开箱即用机制。固然你能够基于Spring HTTP拦截器(HandlerInterceptorAdapter)或过滤器(OncePerRequestFilter)实现自定义解决方案,但这并无你想的那么简单。第二种选择是使用Zalando Logbook
,它是一个可扩展的Java库,能够为不一样的客户端和服务器端技术启用完整的请求和响应日志记录。这是一个很是有趣的库,专门用于记录HTTP请求和响应,它提供了许多自定义选项并支持不一样的客户端。所以,为了更高级, 你能够始终使用此库。 个人目标是建立一个简单的库,它不只记录请求和响应,还提供自动配置,以便将这些日志发送到Logstash
并关联它们。它还会自动生成一些有价值的统计信息,例如请求处理时间。全部这些值都应该发送到Logstash
。咱们继续往下看。node
从依赖开始吧。咱们须要一些基本的Spring库,它们包含spring-web
,spring-context
在内,并提供了一些额外的注解。为了与Logstash
集成,咱们使用logstash-logback-encoder
库。Slf4j
包含用于日志记录的抽象,而javax.servlet-api
用于HTTP通讯。Commons IO
不是必需的,但它提供了一些操做输入和输出流的有用方法。git
<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请求包装器的实现,能够经过各类方式完成。这只是其中之一:github
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(); } }; } }
输出流必须作一样的事情,这个实现有点复杂:web
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
包装器实现放到另外一个类中:spring
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和请求处理时间。docker
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())); } }
完成包装器和HTTP过滤器的实现后,咱们能够为库准备自动配置。第一步是建立@Configuration
包含全部必需的bean。咱们必须注册自定义HTTP过滤器SpringLoggingFilter
,以及用于与Logstash
和RestTemplate
HTTP客户端拦截器集成的logger appender
:json
@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
的配置类:api
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ pl.piomin.logging.config.SpringLoggingAutoConfiguration
经过自动配置的日志记录追加器(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-ID
,X-Request-ID
,X-Response-Time
和X-Response-Status
头。
个人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
关注公众号:锅外的大佬,天天分享国外最新技术文章,帮助开发者更好地成长!