去年公司由于不断发展,内部自研系统越来越多,所以后来搭建了一个日志收集平台,并将日志收集功能以二方包形式引入各个自研系统,避免每个自研系统都要建立一套自己的日志模块,节约了开发时间,管理起来也更加容易。
这篇文章主要介绍如何编写二方包,并整合到各个系统中。
先介绍整个ELK日志平台的架构。其中xiaobawang-log就是今天的主角。
xiaobawang-log主要收集三种日志类型:
- 系统级别日志: 收集系统运行时产生的各个级别的日志(ERROR、INFO、WARN、DEBUG和TRACER),其中ERROR级别日志是我们最关心的。
- 用户请求日志: 主要用于controller层的请求,捕获用户请求信息和响应信息、以及来源ip等,便于分析用户行为。
- 自定义操作日志: 顾名思义,就是收集手动打的日志。比如定时器执行开始,都会习惯性写一个log.info(“定时器执行开始!”)的描述,这种就是属于自定义操作日志的类型。
二方包开发
先看目录结构
废话不多说,上代码。
1、首先创建一个springboot项目,引入如下包:
org.springframework.boot spring-boot-starter-web net.logstash.logback logstash-logback-encoder 7.0.1 ch.qos.logback logback-core 1.2.10 ch.qos.logback logback-classic 1.2.10 ch.qos.logback logback-access 1.2.10 cn.hutool hutool-all 5.7.18 org.projectlombok lombok true 1.18.26 org.springframework.boot spring-boot-starter-aop
SysLog实体类
public class SysLog { /** * 日志名称 */ private String logName; /** * ip地址 */ private String ip; /** * 请求参数 */ private String requestParams; /** * 请求地址 */ private String requestUrl; /** * 用户ua信息 */ private String userAgent; /** * 请求时间 */ private Long useTime; /** * 请求时间 */ private String exceptionInfo; /** * 响应信息 */ private String responseInfo; /** * 用户名称 */ private String username; /** * 请求方式 */ private String requestMethod;}
LogAction
创建一个枚举类,包含三种日志类型。
public enum LogAction { USER_ACTION("用户日志", "user-action"), SYS_ACTION("系统日志", "sys-action"), CUSTON_ACTION("其他日志", "custom-action"); private final String action; private final String actionName; LogAction(String action,String actionName) { this.action = action; this.actionName = actionName; } public String getAction() { return action; } public String getActionName() { return actionName; }}
配置logstash
更改logstash配置文件,将index名称更改为log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]},其中appname为系统名称,action为日志类型。
整个es索引名称是以“系统名称+日期+日志类型”的形式。比如“mySystem-2023.03.05-system-action”表示这个索引,是由mySystem在2023年3月5日产生的系统级别的日志。
# 输入端input { stdin { } #为logstash增加tcp输入口,后面springboot接入会用到 tcp { mode => "server" host => "0.0.0.0" port => 5043 codec => json_lines }} #输出端output { stdout { codec => rubydebug } elasticsearch { hosts => ["http://你的虚拟机ip地址:9200"] # 输出至elasticsearch中的自定义index名称 index => "log-%{[appname]}-%{+YYYY.MM.dd}-%{[action]}" } stdout { codec => rubydebug }}
AppenderBuilder
使用编程式配置logback,AppenderBuilder用于创建appender。
- 这里会创建两种appender。consoleAppender负责将日志打印到控制台,这对开发来说是十分有用的。而LogstashTcpSocketAppender则负责将日志保存到ELK中。
- setCustomFields中的参数,对应上面logstash配置文件的参数[appname]和[action]。
@Componentpublic class AppenderBuilder { public static final String SOCKET_ADDRESS = "你的虚拟机ip地址"; public static final Integer PORT = 5043;//logstash tcp输入端口 /** * logstash通信Appender * @param name * @param action * @param level * @return */ public LogstashTcpSocketAppender logAppenderBuild(String name, String action, Level level) { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender(); appender.setContext(context); //设置logstash通信地址 InetSocketAddress inetSocketAddress = new InetSocketAddress(SOCKET_ADDRESS, PORT); appender.addDestinations(inetSocketAddress); LogstashEncoder logstashEncoder = new LogstashEncoder(); //对应前面logstash配置文件里的参数 logstashEncoder.setCustomFields("{\"appname\":\"" + name + "\",\"action\":\"" + action + "\"}"); appender.setEncoder(logstashEncoder); //这里设置级别过滤器 LevelFilter levelFilter = new LevelFilter(); levelFilter.setLevel(level); levelFilter.setOnMatch(ACCEPT); levelFilter.setOnMismatch(DENY); levelFilter.start(); appender.addFilter(levelFilter); appender.start(); return appender; } /** * 控制打印Appender * @return */ public ConsoleAppender consoleAppenderBuild() { ConsoleAppender consoleAppender = new ConsoleAppender(); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setContext(context); //设置格式 encoder.setPattern("%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)"); encoder.start(); consoleAppender.setEncoder(encoder); consoleAppender.start(); return consoleAppender; }
LoggerBuilder
LoggerBuilder主要用于创建logger类。创建步骤如下:
- 获取logger上下文。
- 从上下文获取logger对象。创建过的logger会保存在LOGCONTAINER中,保证下次获取logger不会重复创建。这里使用ConcurrentHashMap防止出现并发问题。
- 创建appender,并将appender加入logger对象中。
@Componentpublic class LoggerBuilder { @Autowired AppenderBuilder appenderBuilder; @Value("${spring.application.name:unknow-system}") private String appName; private static final Map LOGCONTAINER = new ConcurrentHashMap(); public Logger getLogger(LogAction logAction) { Logger logger = LOGCONTAINER.get(logAction.getActionName() + "-" + appName); if (logger != null) { return logger; } logger = build(logAction); LOGCONTAINER.put(logAction.getActionName() + "-" + appName, logger); return logger; } public Logger getLogger() { return getLogger(LogAction.CUSTON_ACTION); } private Logger build(LogAction logAction) { //创建日志appender List list = createAppender(appName, logAction.getActionName()); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger logger = context.getLogger(logAction.getActionName() + "-" + appName); logger.setAdditive(false); //打印控制台appender ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild(); logger.addAppender(consoleAppender); list.forEach(appender -> { logger.addAppender(appender); }); return logger; } /** * LoggerContext上下文中的日志对象加入appender */ public void addContextAppender() { //创建四种类型日志 String action = LogAction.SYS_ACTION.getActionName(); List list = createAppender(appName, action); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); //打印控制台 ConsoleAppender consoleAppender = appenderBuilder.consoleAppenderBuild(); context.getLoggerList().forEach(logger -> { logger.setAdditive(false); logger.addAppender(consoleAppender); list.forEach(appender -> { logger.addAppender(appender); }); }); } /** * 创建连接elk的appender,每一种级别日志创建一个appender * * @param name * @param action * @return */ public List createAppender(String name, String action) { List list = new ArrayList(); LogstashTcpSocketAppender errorAppender = appenderBuilder.logAppenderBuild(name, action, Level.ERROR); LogstashTcpSocketAppender infoAppender = appenderBuilder.logAppenderBuild(name, action, Level.INFO); LogstashTcpSocketAppender warnAppender = appenderBuilder.logAppenderBuild(name, action, Level.WARN); LogstashTcpSocketAppender debugAppender = appenderBuilder.logAppenderBuild(name, action, Level.DEBUG); LogstashTcpSocketAppender traceAppender = appenderBuilder.logAppenderBuild(name, action, Level.TRACE); list.add(errorAppender); list.add(infoAppender); list.add(warnAppender); list.add(debugAppender); list.add(traceAppender); return list; }}
LogAspect
使用spring aop,实现拦截用户请求,记录用户日志。比如ip、请求参数、请求用户等信息,需要配合下面的XiaoBaWangLog注解使用。
这里拦截上面所说的第二种日志类型。
@Aspect@Componentpublic class LogAspect { @Autowired LoggerBuilder loggerBuilder; private ThreadLocal startTime = new ThreadLocal(); private SysLog sysLog; @Pointcut("@annotation(com.xiaobawang.common.log.annotation.XiaoBaWangLog)") public void pointcut() { } /** * 前置方法执行 * * @param joinPoint */ @Before("pointcut()") public void before(JoinPoint joinPoint) { startTime.set(System.currentTimeMillis()); //获取请求的request ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String clientIP = ServletUtil.getClientIP(request, null); 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)) { clientIP = "127.0.0.1"; } sysLog = new SysLog(); sysLog.setIp(clientIP); String requestParams = JSONUtil.toJsonStr(getRequestParams(request)); sysLog.setRequestParams(requestParams.length() > 5000 ? ("请求参数过长,参数长度为:" + requestParams.length()) : requestParams); MethodSignature ms = (MethodSignature) joinPoint.getSignature(); Method method = ms.getMethod(); String logName = method.getAnnotation(XiaoBaWangLog.class).value(); sysLog.setLogName(logName); sysLog.setUserAgent(request.getHeader("User-Agent")); String fullUrl = request.getRequestURL().toString(); if (request.getQueryString() != null && !"".equals(request.getQueryString())) { fullUrl = request.getRequestURL().toString() + "?" + request.getQueryString(); } sysLog.setRequestUrl(fullUrl); sysLog.setRequestMethod(request.getMethod()); //tkSysLog.setUsername(JwtUtils.getUsername()); } /** * 方法返回后执行 * * @param ret */ @AfterReturning(returning = "ret", pointcut = "pointcut()") public void after(Object ret) { Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION); String retJsonStr = JSONUtil.toJsonStr(ret); if (retJsonStr != null) { sysLog.setResponseInfo(retJsonStr.length() > 5000 ? ("响应参数过长,参数长度为:" + retJsonStr.length()) : retJsonStr); } sysLog.setUseTime(System.currentTimeMillis() - startTime.get()); logger.info(JSONUtil.toJsonStr(sysLog)); } /** * 环绕通知,收集方法执行期间的错误信息 * * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("pointcut()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { try { Object obj = proceedingJoinPoint.proceed(); return obj; } catch (Exception e) { e.printStackTrace(); sysLog.setExceptionInfo(e.getMessage()); Logger logger = loggerBuilder.getLogger(LogAction.USER_ACTION); logger.error(JSONUtil.toJsonStr(sysLog)); throw e; } } /** * 获取请求的参数 * * @param request * @return */ private Map getRequestParams(HttpServletRequest request) { Map map = new HashMap(); Enumeration paramNames = request.getParameterNames(); while (paramNames.hasMoreElements()) { String paramName = (String) paramNames.nextElement(); String[] paramValues = request.getParameterValues(paramName); if (paramValues.length == 1) { String paramValue = paramValues[0]; if (paramValue.length() != 0) { map.put(paramName, paramValue); } } } return map; }}
XiaoBaWangLog
LoggerLoad主要是实现用户级别日志的收集功能。
这里定义了一个注解,在controller方法上加上@XiaoBaWangLog(“操作内容”),即可拦截并生成请求日志。
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface XiaoBaWangLog { String value() default "";}
LoggerLoad
LoggerLoad主要是实现系统级别日志的收集功能。
继承ApplicationRunner,可以在springboot执行后,自动创建系统级别日志logger对象。
@Component@Order(value = 1)@Slf4jpublic class LoggerLoad implements ApplicationRunner { @Autowired LoggerBuilder loggerBuilder; @Override public void run(ApplicationArguments args) throws Exception { loggerBuilder.addContextAppender(); log.info("加载日志模块成功"); }}
LogConfig
LogConfig主要实现自定义级别日志的收集功能。
生成一个logger对象交给spring容器管理。后面直接从容器取就可以了。
@Configurationpublic class LogConfig { @Autowired LoggerBuilder loggerBuilder; @Bean public Logger loggerBean(){ return loggerBuilder.getLogger(); }}
代码到现在已经全部完成,怎么将上述的所有Bean加入到spring呢?这个时候就需要用到spring.factories了。
spring.factories
在EnableAutoConfiguration中加入类的全路径名,在项目启动的时候,SpringFactoriesLoader会初始化spring.factories,包括pom中引入的jar包中的配置类。
注意,spring.factories在2.7开始已经不推荐使用,3.X版本的springBoot是不支持使用的。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.xiaobawang.common.log.config.AppenderBuilder,\ com.xiaobawang.common.log.config.LoggerBuilder,\ com.xiaobawang.common.log.load.LoggerLoad,\ com.xiaobawang.common.log.aspect.LogAspect,\ com.xiaobawang.common.log.config.LogConfig
测试
先将xiaobawang进行打包
新建一个springboot项目,引入打包好的xiaobawang-log.
运行springboot,出现“加载日志模块成功”表示日志模块启动成功。
接着新建一个controller请求
访问请求后,可以看到了三种不同类型的索引了
结束
还有很多需要优化的地方,比如ELK设置用户名密码登录等,对ELK比较了解的童鞋可以自己尝试优化!
如果这篇文章对你有帮助,记得一键三连~