开发一个二方包,优雅地为系统接入ELK(elasticsearch+logstash+kibana) ...

打印 上一主题 下一主题

主题 648|帖子 648|积分 1944

去年公司由于不断发展,内部自研系统越来越多,所以后来搭建了一个日志收集平台,并将日志收集功能以二方包形式引入各个自研系统,避免每个自研系统都要建立一套自己的日志模块,节约了开发时间,管理起来也更加容易。
这篇文章主要介绍如何编写二方包,并整合到各个系统中。
先介绍整个ELK日志平台的架构。其中xiaobawang-log就是今天的主角。

xiaobawang-log主要收集三种日志类型:


  • 系统级别日志: 收集系统运行时产生的各个级别的日志(ERROR、INFO、WARN、DEBUG和TRACER),其中ERROR级别日志是我们最关心的。
  • 用户请求日志: 主要用于controller层的请求,捕获用户请求信息和响应信息、以及来源ip等,便于分析用户行为。
  • 自定义操作日志: 顾名思义,就是收集手动打的日志。比如定时器执行开始,都会习惯性写一个log.info("定时器执行开始!")的描述,这种就是属于自定义操作日志的类型。
二方包开发

先看目录结构

废话不多说,上代码。
1、首先创建一个springboot项目,引入如下包:
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>net.logstash.logback</groupId>
  7.     <artifactId>logstash-logback-encoder</artifactId>
  8.     <version>7.0.1</version>
  9. </dependency>
  10. <dependency>
  11.     <groupId>ch.qos.logback</groupId>
  12.     <artifactId>logback-core</artifactId>
  13.     <version>1.2.10</version>
  14. </dependency>
  15. <dependency>
  16.     <groupId>ch.qos.logback</groupId>
  17.     <artifactId>logback-classic</artifactId>
  18.     <version>1.2.10</version>
  19. </dependency>
  20. <dependency>
  21.     <groupId>ch.qos.logback</groupId>
  22.     <artifactId>logback-access</artifactId>
  23.     <version>1.2.10</version>
  24. </dependency>
  25. <dependency>
  26.     <groupId>cn.hutool</groupId>
  27.     <artifactId>hutool-all</artifactId>
  28.     <version>5.7.18</version>
  29. </dependency>
  30. <dependency>
  31.     <groupId>org.projectlombok</groupId>
  32.     <artifactId>lombok</artifactId>
  33.     <optional>true</optional>
  34.     <version>1.18.26</version>
  35. </dependency>
  36. <dependency>
  37.     <groupId>org.springframework.boot</groupId>
  38.     <artifactId>spring-boot-starter-aop</artifactId>
  39. </dependency>
复制代码
SysLog实体类
  1. public class SysLog {
  2.     /**
  3.      * 日志名称
  4.      */
  5.     private String logName;
  6.     /**
  7.      * ip地址
  8.      */
  9.     private String ip;
  10.     /**
  11.      * 请求参数
  12.      */
  13.     private String requestParams;
  14.     /**
  15.      * 请求地址
  16.      */
  17.     private String requestUrl;
  18.     /**
  19.      * 用户ua信息
  20.      */
  21.     private String userAgent;
  22.     /**
  23.      * 请求时间
  24.      */
  25.     private Long useTime;
  26.     /**
  27.      * 请求时间
  28.      */
  29.     private String exceptionInfo;
  30.     /**
  31.      * 响应信息
  32.      */
  33.     private String responseInfo;
  34.     /**
  35.      * 用户名称
  36.      */
  37.     private String username;
  38.     /**
  39.      * 请求方式
  40.      */
  41.     private String requestMethod;
  42. }
复制代码
LogAction

创建一个枚举类,包含三种日志类型。
  1. public enum LogAction {
  2.     USER_ACTION("用户日志", "user-action"),
  3.     SYS_ACTION("系统日志", "sys-action"),
  4.     CUSTON_ACTION("其他日志", "custom-action");
  5.     private final String action;
  6.     private final String actionName;
  7.     LogAction(String action,String actionName) {
  8.         this.action = action;
  9.         this.actionName = actionName;
  10.     }
  11.     public String getAction() {
  12.         return action;
  13.     }
  14.     public String getActionName() {
  15.         return actionName;
  16.     }
  17. }
复制代码
配置logstash

更改logstash配置文件,将index名称更改为log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]},其中appname为系统名称,action为日志类型。
整个es索引名称是以“系统名称+日期+日志类型”的形式。比如“mySystem-2023.03.05-system-action”表示这个索引,是由mySystem在2023年3月5日产生的系统级别的日志。
  1. # 输入端
  2. input {
  3.   stdin { }
  4.   #为logstash增加tcp输入口,后面springboot接入会用到
  5.   tcp {
  6.       mode => "server"
  7.       host => "0.0.0.0"
  8.       port => 5043
  9.       codec => json_lines
  10.   }
  11. }
  12. #输出端
  13. output {
  14.   stdout {
  15.     codec => rubydebug
  16.   }
  17.   elasticsearch {
  18.     hosts => ["http://你的虚拟机ip地址:9200"]
  19.     # 输出至elasticsearch中的自定义index名称
  20.     index => "log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]}"
  21.   }
  22.   stdout { codec => rubydebug }
  23. }
复制代码
AppenderBuilder

使用编程式配置logback,AppenderBuilder用于创建appender。

  • 这里会创建两种appender。consoleAppender负责将日志打印到控制台,这对开发来说是十分有用的。而LogstashTcpSocketAppender则负责将日志保存到ELK中。
  • setCustomFields中的参数,对应上面logstash配置文件的参数[appname]和[action]。
  1. @Component
  2. public class AppenderBuilder {
  3.     public static final String SOCKET_ADDRESS = "你的虚拟机ip地址";
  4.     public static final Integer PORT = 5043;//logstash tcp输入端口
  5.     /**
  6.      * logstash通信Appender
  7.      * @param name
  8.      * @param action
  9.      * @param level
  10.      * @return
  11.      */
  12.     public LogstashTcpSocketAppender logAppenderBuild(String name, String action, Level level) {
  13.         LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
  14.         LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
  15.         appender.setContext(context);
  16.         //设置logstash通信地址
  17.         InetSocketAddress inetSocketAddress = new InetSocketAddress(SOCKET_ADDRESS, PORT);
  18.         appender.addDestinations(inetSocketAddress);
  19.         LogstashEncoder logstashEncoder = new LogstashEncoder();
  20.         //对应前面logstash配置文件里的参数
  21.         logstashEncoder.setCustomFields("{"appname":"" + name + "","action":"" + action + ""}");
  22.         appender.setEncoder(logstashEncoder);
  23.         //这里设置级别过滤器
  24.         LevelFilter levelFilter = new LevelFilter();
  25.         levelFilter.setLevel(level);
  26.         levelFilter.setOnMatch(ACCEPT);
  27.         levelFilter.setOnMismatch(DENY);
  28.         levelFilter.start();
  29.         appender.addFilter(levelFilter);
  30.         appender.start();
  31.         return appender;
  32.     }
  33.    
  34.    
  35.     /**
  36.      * 控制打印Appender
  37.      * @return
  38.      */
  39.     public ConsoleAppender consoleAppenderBuild() {
  40.         ConsoleAppender consoleAppender = new ConsoleAppender();
  41.         LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
  42.         PatternLayoutEncoder encoder = new PatternLayoutEncoder();
  43.         encoder.setContext(context);
  44.         //设置格式
  45.         encoder.setPattern("%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)");
  46.         encoder.start();
  47.         consoleAppender.setEncoder(encoder);
  48.         consoleAppender.start();
  49.         return consoleAppender;
  50.     }
复制代码
LoggerBuilder

LoggerBuilder主要用于创建logger类。创建步骤如下:

  • 获取logger上下文。
  • 从上下文获取logger对象。创建过的logger会保存在LOGCONTAINER中,保证下次获取logger不会重复创建。这里使用ConcurrentHashMap防止出现并发问题。
  • 创建appender,并将appender加入logger对象中。
  1. @Component
  2. public class LoggerBuilder {
  3.     @Autowired
  4.     AppenderBuilder appenderBuilder;
  5.     @Value("${spring.application.name:unknow-system}")
  6.     private String appName;
  7.     private static final Map<String, Logger> LOGCONTAINER = new ConcurrentHashMap<>();
  8.     public Logger getLogger(LogAction logAction) {
  9.         Logger logger = LOGCONTAINER.get(logAction.getActionName() + "-" + appName);
  10.         if (logger != null) {
  11.             return logger;
  12.         }
  13.         logger = build(logAction);
  14.         LOGCONTAINER.put(logAction.getActionName() + "-" + appName, logger);
  15.         return logger;
  16.     }
  17.     public Logger getLogger() {
  18.         return getLogger(LogAction.CUSTON_ACTION);
  19.     }
  20.     private Logger build(LogAction logAction) {
  21.         //创建日志appender
  22.         List<LogstashTcpSocketAppender> list = createAppender(appName, logAction.getActionName());
  23.         LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
  24.         Logger logger = context.getLogger(logAction.getActionName() + "-" + appName);
  25.         logger.setAdditive(false);
  26.         //打印控制台appender
  27.         ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
  28.         logger.addAppender(consoleAppender);
  29.         list.forEach(appender -> {
  30.             logger.addAppender(appender);
  31.         });
  32.         return logger;
  33.     }
  34.     /**
  35.      * LoggerContext上下文中的日志对象加入appender
  36.      */
  37.     public void addContextAppender() {
  38.         //创建四种类型日志
  39.         String action = LogAction.SYS_ACTION.getActionName();
  40.         List<LogstashTcpSocketAppender> list = createAppender(appName, action);
  41.         LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
  42.         //打印控制台
  43.         ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild();
  44.         context.getLoggerList().forEach(logger -> {
  45.             logger.setAdditive(false);
  46.             logger.addAppender(consoleAppender);
  47.             list.forEach(appender -> {
  48.                 logger.addAppender(appender);
  49.             });
  50.         });
  51.     }
  52.     /**
  53.      * 创建连接elk的appender,每一种级别日志创建一个appender
  54.      *
  55.      * @param name
  56.      * @param action
  57.      * @return
  58.      */
  59.     public List<LogstashTcpSocketAppender> createAppender(String name, String action) {
  60.         List<LogstashTcpSocketAppender> list = new ArrayList<>();
  61.         LogstashTcpSocketAppender errorAppender = appenderBuilder.logAppenderBuild(name, action, Level.ERROR);
  62.         LogstashTcpSocketAppender infoAppender = appenderBuilder.logAppenderBuild(name, action, Level.INFO);
  63.         LogstashTcpSocketAppender warnAppender = appenderBuilder.logAppenderBuild(name, action, Level.WARN);
  64.         LogstashTcpSocketAppender debugAppender = appenderBuilder.logAppenderBuild(name, action, Level.DEBUG);
  65.         LogstashTcpSocketAppender traceAppender = appenderBuilder.logAppenderBuild(name, action, Level.TRACE);
  66.         list.add(errorAppender);
  67.         list.add(infoAppender);
  68.         list.add(warnAppender);
  69.         list.add(debugAppender);
  70.         list.add(traceAppender);
  71.         return list;
  72.     }
  73. }
复制代码
LogAspect

使用spring aop,实现拦截用户请求,记录用户日志。比如ip、请求参数、请求用户等信息,需要配合下面的XiaoBaWangLog注解使用。
这里拦截上面所说的第二种日志类型。
  1. @Aspect
  2. @Component
  3. public class LogAspect {
  4.     @Autowired
  5.     LoggerBuilder loggerBuilder;
  6.     private ThreadLocal<Long> startTime = new ThreadLocal<>();
  7.     private SysLog sysLog;
  8.     @Pointcut("@annotation(com.xiaobawang.common.log.annotation.XiaoBaWangLog)")
  9.     public void pointcut() {
  10.     }
  11.     /**
  12.      * 前置方法执行
  13.      *
  14.      * @param joinPoint
  15.      */
  16.     @Before("pointcut()")
  17.     public void before(JoinPoint joinPoint) {
  18.         startTime.set(System.currentTimeMillis());
  19.         //获取请求的request
  20.         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  21.         HttpServletRequest request = attributes.getRequest();
  22.         String clientIP = ServletUtil.getClientIP(request, null);
  23.         if ("0.0.0.0".equals(clientIP) || "0:0:0:0:0:0:0:1".equals(clientIP) || "localhost".equals(clientIP) || "127.0.0.1".equals(clientIP)) {
  24.             clientIP = "127.0.0.1";
  25.         }
  26.         sysLog = new SysLog();
  27.         sysLog.setIp(clientIP);
  28.         String requestParams = JSONUtil.toJsonStr(getRequestParams(request));
  29.         sysLog.setRequestParams(requestParams.length() > 5000 ? ("请求参数过长,参数长度为:" + requestParams.length()) : requestParams);
  30.         MethodSignature ms = (MethodSignature) joinPoint.getSignature();
  31.         Method method = ms.getMethod();
  32.         String logName = method.getAnnotation(XiaoBaWangLog.class).value();
  33.         sysLog.setLogName(logName);
  34.         sysLog.setUserAgent(request.getHeader("User-Agent"));
  35.         String fullUrl = request.getRequestURL().toString();
  36.         if (request.getQueryString() != null && !"".equals(request.getQueryString())) {
  37.             fullUrl = request.getRequestURL().toString() + "?" + request.getQueryString();
  38.         }
  39.         sysLog.setRequestUrl(fullUrl);
  40.         sysLog.setRequestMethod(request.getMethod());
  41.         //tkSysLog.setUsername(JwtUtils.getUsername());
  42.     }
  43.     /**
  44.      * 方法返回后执行
  45.      *
  46.      * @param ret
  47.      */
  48.     @AfterReturning(returning = "ret", pointcut = "pointcut()")
  49.     public void after(Object ret) {
  50.         Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
  51.         String retJsonStr = JSONUtil.toJsonStr(ret);
  52.         if (retJsonStr != null) {
  53.             sysLog.setResponseInfo(retJsonStr.length() > 5000 ? ("响应参数过长,参数长度为:" + retJsonStr.length()) : retJsonStr);
  54.         }
  55.         sysLog.setUseTime(System.currentTimeMillis() - startTime.get());
  56.         logger.info(JSONUtil.toJsonStr(sysLog));
  57.     }
  58.     /**
  59.      * 环绕通知,收集方法执行期间的错误信息
  60.      *
  61.      * @param proceedingJoinPoint
  62.      * @return
  63.      * @throws Throwable
  64.      */
  65.     @Around("pointcut()")
  66.     public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
  67.         try {
  68.             Object obj = proceedingJoinPoint.proceed();
  69.             return obj;
  70.         } catch (Exception e) {
  71.             e.printStackTrace();
  72.             sysLog.setExceptionInfo(e.getMessage());
  73.             Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION);
  74.             logger.error(JSONUtil.toJsonStr(sysLog));
  75.             throw e;
  76.         }
  77.     }
  78.     /**
  79.      * 获取请求的参数
  80.      *
  81.      * @param request
  82.      * @return
  83.      */
  84.     private Map getRequestParams(HttpServletRequest request) {
  85.         Map map = new HashMap();
  86.         Enumeration paramNames = request.getParameterNames();
  87.         while (paramNames.hasMoreElements()) {
  88.             String paramName = (String) paramNames.nextElement();
  89.             String[] paramValues = request.getParameterValues(paramName);
  90.             if (paramValues.length == 1) {
  91.                 String paramValue = paramValues[0];
  92.                 if (paramValue.length() != 0) {
  93.                     map.put(paramName, paramValue);
  94.                 }
  95.             }
  96.         }
  97.         return map;
  98.     }
  99. }
复制代码
XiaoBaWangLog

LoggerLoad主要是实现用户级别日志的收集功能。
这里定义了一个注解,在controller方法上加上@XiaoBaWangLog("操作内容"),即可拦截并生成请求日志。
  1. @Target({ElementType.TYPE, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Component
  5. public @interface XiaoBaWangLog {
  6.     String value() default "";
  7. }
复制代码
LoggerLoad

LoggerLoad主要是实现系统级别日志的收集功能。
继承ApplicationRunner,可以在springboot执行后,自动创建系统级别日志logger对象。
  1. @Component
  2. @Order(value = 1)
  3. @Slf4j
  4. public class LoggerLoad implements ApplicationRunner {
  5.     @Autowired
  6.     LoggerBuilder loggerBuilder;
  7.     @Override
  8.     public void run(ApplicationArguments args) throws Exception {
  9.         loggerBuilder.addContextAppender();
  10.         log.info("加载日志模块成功");
  11.     }
  12. }
复制代码
LogConfig

LogConfig主要实现自定义级别日志的收集功能。
生成一个logger对象交给spring容器管理。后面直接从容器取就可以了。
  1. @Configuration
  2. public class LogConfig {
  3.     @Autowired
  4.     LoggerBuilder loggerBuilder;
  5.     @Bean
  6.     public Logger loggerBean(){
  7.         return loggerBuilder.getLogger();
  8.     }
  9. }
复制代码
代码到现在已经全部完成,怎么将上述的所有Bean加入到spring呢?这个时候就需要用到spring.factories了。
spring.factories

在EnableAutoConfiguration中加入类的全路径名,在项目启动的时候,SpringFactoriesLoader会初始化spring.factories,包括pom中引入的jar包中的配置类。
注意,spring.factories在2.7开始已经不推荐使用,3.X版本的springBoot是不支持使用的。
  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2.   com.xiaobawang.common.log.config.AppenderBuilder,\
  3.   com.xiaobawang.common.log.config.LoggerBuilder,\
  4.   com.xiaobawang.common.log.load.LoggerLoad,\
  5.   com.xiaobawang.common.log.aspect.LogAspect,\
  6.   com.xiaobawang.common.log.config.LogConfig
复制代码
测试

先将xiaobawang进行打包
新建一个springboot项目,引入打包好的xiaobawang-log.

运行springboot,出现“加载日志模块成功”表示日志模块启动成功。
接着新建一个controller请求

访问请求后,可以看到了三种不同类型的索引了

结束

还有很多需要优化的地方,比如ELK设置用户名密码登录等,对ELK比较了解的童鞋可以自己尝试优化!
如果这篇文章对你有帮助,记得一键三连~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小秦哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表