上一篇讲过了将Springboot项目中logback日志插入到ELK日志平台,它只是个示例。这一篇来看一下实际使用中,我们应该怎样通过aop切面,拦截所有请求日志插入到ELK日志系统。同时,由于往往我们有很多个服务,都需要记录日志,为每个服务都搭建一个ELK并不现实,所以我们采用集中化管理日志,将所有日志都插到同一个ELK中。这样又会遇到另一个问题,就是ES中的Index如果只有一个,那么所有日志都混杂在一个Index里,为日后的检索带来不便,我们希望能根据不同的应用来拆分为不同的ES的Index。
下面看看怎么做。
配置logback
同样的,新建一个Springboot项目,勾选aop。
首先我们来配置logback,见logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/base.xml"/> <!-- 本地日志文件 --> <appender name="MY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <Prudent>true</Prudent> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{yyyy-MM-dd HH:mm:ss} -%msg%n</Pattern> </layout> <encoder> <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <!-- Logstash日志 --> <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <!-- 远程Logstash的IP:PORT --> <destination>IP:4560</destination> <!-- encoder必须要有,是logback和logstash的纽带 --> <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder"> <!-- 用户自定义Json字符串,用于向logstash提交额外的自定义属性。可在logstash的input里接收该参数 --> <customFields>{"appName":"testelk"}</customFields> </encoder> </appender> <root level="INFO"> <appender-ref ref="LOGSTASH"/> <appender-ref ref="CONSOLE"/> <appender-ref ref="MY_FILE"/> </root> </configuration>
为了演示,我们将日志保存3份,一份在控制台打印,即appender-ref中的CONSOLE,再一份存文件,即MY_FILE,这两个在很早之前的文章Springboot使用logback详解中有介绍。
重点看一下LOGSTASH的配置,destination配置你的Logstash的ip和端口,请修改为自己服务器的ip地址,4560是Logstash默认的端口。通过LogstashTcpSocketAppender继承的类源码可以看到
注意里面有destinations的集合,用于接收xml配置的ip,说明可以配置多个。然后还有一个encoder属性,接收一个Encoder对象。见xml里配置的encoder
encoder可以接收一个customFields的属性,见xml配置,这是一个json字符串,用于给Logstash传递自定义的值,设置之后就会在kibana里默认给每条数据都带着这个属性。
这个appName就是在xml里配置的自定义属性。之前有讲过,要使用kibana管理ES,ES的index必须要有timestamp字段。如果是不通过Logstash插入ES的话,自定义的model必须要有FieldType.DATE字段,使用Logstash的话,就可以不加这个字段。原因就是见源码里的,里面有timezone,version等都是该encoder源码自动给我们补上的。
设置appName意义何在?下面请看。
配置Logstash
创建编辑一个Logstash的配置文件,不会的去看前两篇
input { # For detail config for log4j as input, # See: https://www.elastic.co/guide/en/logstash/current/plugins-inputs-log4j.html tcp { mode => "server" host => "0.0.0.0" port => 4560 codec => json_lines } } filter { #Only matched data are send to output. } output { # For detail config for elasticsearch as output, # See: https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html elasticsearch { index => "%{[appName]}" #The operation on ES hosts => "localhost:9200" #ElasticSearch host, can be array. action => "index" #The index to write data to. } }
input没什么说的,设置端口和codec即可,主要是output,我们看elasticsearch的index标签,这个代表将日志插入到ES的哪个index里,这里我们不写死,而是使用获取客户端传来的appName来指定index,Logstash通过动态指定ES的index,就可以完成不同的项目存储到不同的ES的index。这样既完成了日志统一管理,又区分开了不同的项目。
配置完毕,启动Logstash即可。
设置aop拦截
logging: file: ./logback.log
创建aop切面类,拦截所有Controller。
package com.tianyalei.testelk.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.validation.ObjectError; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; /** * Created by wuwf on 17/4/27. * 日志切面 */ @Aspect @Component public class LogAspect { private Logger logger = LoggerFactory.getLogger(getClass().getName()); private ObjectError error; @Pointcut("execution(public * com.tianyalei.testelk.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void deBefore(JoinPoint joinPoint) throws Throwable { // 接收到请求,记录请求内容 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); logger.info("-------------------用户发起请求-----------------"); // 记录下请求内容 logger.info("URL : " + request.getRequestURL().toString()); logger.info("HTTP_METHOD : " + request.getMethod()); //如果是表单,参数值是普通键值对。如果是application/json,则request.getParameter是取不到的。 logger.info("HTTP_HEAD Type : " + request.getHeader("Content-Type")); logger.info("IP : " + request.getRemoteAddr()); logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); if ("application/json".equals(request.getHeader("Content-Type"))) { //记录application/json时的传参,SpringMVC中使用@RequestBody接收的值 logger.info(getRequestPayload(request)); } else { //记录请求的键值对 for (String key : request.getParameterMap().keySet()) { logger.info(key + "----" + request.getParameter(key)); } } } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { // 处理完请求,返回内容 logger.info("方法的返回值 : " + ret); logger.info("------------------请求结束------------------"); } //后置异常通知 @AfterThrowing(throwing = "ex", pointcut = "webLog()") public void throwss(JoinPoint jp, Exception ex){ logger.info("方法异常时执行....."); } //后置最终通知,final增强,不管是抛出异常或者正常退出都会执行 @After("webLog()") public void after(JoinPoint jp){ // logger.info("方法最后执行....."); } private String getRequestPayload(HttpServletRequest req) { StringBuilder sb = new StringBuilder(); try(BufferedReader reader = req.getReader()) { char[]buff = new char[1024]; int len; while((len = reader.read(buff)) != -1) { sb.append(buff,0, len); } }catch (IOException e) { e.printStackTrace(); } return sb.toString(); } }
在这里获取用户的请求地址、参数和返回值。
package com.tianyalei.testelk.controller; import com.tianyalei.testelk.service.LoginService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class IndexController { private Logger logger = LoggerFactory.getLogger(getClass().getName()); @Autowired LoginService loginService; @GetMapping("index") public Object index() { logger.info("进入了index方法"); logger.info("开始执行业务逻辑"); logger.info("index方法结束"); return "success"; } @PostMapping("login") public Object login() { logger.info("进入了login方法"); loginService.login(); logger.info("登录成功"); return "success"; } }
Service如下
package com.tianyalei.testelk.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class LoginService { private Logger logger = LoggerFactory.getLogger(getClass().getName()); public void login() { logger.info("进入Service方法,执行登录查询"); } }
OK了,如果你的spring-logback.xml里配置的ip和port是对的,那么日志就能进到ELK了。
2017-08-08 17:11:52.603 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : -------------------用户发起请求----------------- 2017-08-08 17:11:52.604 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : URL : http://localhost:8080/login 2017-08-08 17:11:52.604 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : HTTP_METHOD : POST 2017-08-08 17:11:52.605 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : HTTP_HEAD Type : application/x-www-form-urlencoded 2017-08-08 17:11:52.605 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : IP : 0:0:0:0:0:0:0:1 2017-08-08 17:11:52.607 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : CLASS_METHOD : com.tianyalei.testelk.controller.IndexController.login 2017-08-08 17:11:52.607 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : key:name----value:wolf 2017-08-08 17:11:52.607 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : key:pass----value:123456 2017-08-08 17:11:52.610 INFO 89159 --- [nio-8080-exec-1] c.t.testelk.controller.IndexController : 进入了login方法 2017-08-08 17:11:52.611 INFO 89159 --- [nio-8080-exec-1] c.t.testelk.service.LoginService : 进入Service方法,执行登录查询 2017-08-08 17:11:52.611 INFO 89159 --- [nio-8080-exec-1] c.t.testelk.controller.IndexController : 登录成功 2017-08-08 17:11:52.611 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : 方法的返回值 : success 2017-08-08 17:11:52.613 INFO 89159 --- [nio-8080-exec-1] com.tianyalei.testelk.aop.LogAspect : ------------------请求结束------------------
查看ES数据:
我们可以看到index名为testelk的数据如图。说明自定义的index已经成功创建,值也插入成功了。
可以看到记录的日志了。