去年公司由于不断发展,内部自研系统越来越多,所以后来搭建了一个日志收集平台,并将日志收集功能以二方包形式引入各个自研系统,避免每个自研系统都要建立一套自己的日志模块,节约了开发时间,管理起来也更加容易。

这篇文章主要介绍如何编写二方包,并整合到各个系统中。

先介绍整个ELK日志平台的架构。其中xiaobawang-log就是今天的主角。

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

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

二方包开发

先看目录结构



废话不多说,上代码。

1、首先创建一个springboot项目,引入如下包:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.2.10</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.26</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

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]。
@Component
public 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类。创建步骤如下:

  1. 获取logger上下文。
  2. 从上下文获取logger对象。创建过的logger会保存在LOGCONTAINER中,保证下次获取logger不会重复创建。这里使用ConcurrentHashMap防止出现并发问题。
  3. 创建appender,并将appender加入logger对象中。
@Component
public class LoggerBuilder {
@Autowired
AppenderBuilder appenderBuilder; @Value("${spring.application.name:unknow-system}")
private String appName; private static final Map<String, Logger> 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<LogstashTcpSocketAppender> 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<LogstashTcpSocketAppender> 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<LogstashTcpSocketAppender> createAppender(String name, String action) {
List<LogstashTcpSocketAppender> 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
@Component
public class LogAspect { @Autowired
LoggerBuilder loggerBuilder; private ThreadLocal<Long> 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
@Component
public @interface XiaoBaWangLog { String value() default ""; }

LoggerLoad

LoggerLoad主要是实现系统级别日志的收集功能。

继承ApplicationRunner,可以在springboot执行后,自动创建系统级别日志logger对象。

@Component
@Order(value = 1)
@Slf4j
public class LoggerLoad implements ApplicationRunner {
@Autowired
LoggerBuilder loggerBuilder; @Override
public void run(ApplicationArguments args) throws Exception {
loggerBuilder.addContextAppender();
log.info("加载日志模块成功");
}
}

LogConfig

LogConfig主要实现自定义级别日志的收集功能。

生成一个logger对象交给spring容器管理。后面直接从容器取就可以了。

@Configuration
public 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比较了解的童鞋可以自己尝试优化!

如果这篇文章对你有帮助,记得一键三连~

开发一个二方包,优雅地为系统接入ELK(elasticsearch+logstash+kibana)的更多相关文章

  1. VC、OpenGL、ArcGIS Engine开发的二维三维结合的GIS系统

    一.前言 众所周知,二维GIS技术发展了近四十年,伴随着计算机软硬件以及关系型数据库的飞速发展,二维GIS技术已日臻完善.在对地理信息的分析功能上有着无可比拟的优势.一些宏观的地理信息,一维的地理信息 ...

  2. 使用ionic2开发一个二维码扫描功能

    界面添加一个按钮: <button ion-button block color="secondary" class="Scan-button" (cli ...

  3. 如何用HMS Nearby Service给自己的APP开发一个名片交换功能?

      在工作和生活中,遇见新的同事或者合作伙伴,交换名片是一个常见的用户需求,纸质名片常忘带.易丢失,是客户的一个痛点.因此,市场上出现了很多交换电子名片的APP和小程序.那么,如何给自己的APP开发一 ...

  4. 微信开发笔记(一)通过.net如何实现接入微信

    微信公众平台,给个人.企业和组织提供业务服务与用户管理能力的全新服务平台.现在基本上每个地方都可以看到微信存在,动不动就是让你扫一下加下微信. 经常遇到这样情况,“到一家餐馆吃饭,拿了个号,前台服务引 ...

  5. 如何开发一个npm包并发布

    一.安装nodejs 不多说了,网上教程多得是 二.创建自己的npm包 目录结构 npm-test a.js b.js package.json 开发 为了简单便于理解,就开发一个简单地hello程序 ...

  6. 开源低代码平台开发实践二:从 0 构建一个基于 ER 图的低代码后端

    前后端分离了! 第一次知道这个事情的时候,内心是困惑的. 前端都出去搞 SPA,SEO 们同意吗? 后来,SSR 来了. 他说:"SEO 们同意了!" 任何人的反对,都没用了,时代 ...

  7. 采用boosting思想开发一个解决二分类样本不平衡的多估计器模型

    # -*- coding: utf-8 -*- """ Created on Wed Oct 31 20:59:39 2018 脚本描述:采用boosting思想开发一个 ...

  8. 【Mac系统 + Python + Django】之开发一个发布会系统【Django视图(二)】

    此学习资料是通过虫师的python接口自动化出的书学习而来的,在此说明一下,想学习更多的自动化的同学可以找虫师的博客园,非广告,因为我python+selenium自动化也是跟虫师学的,学习效果很好的 ...

  9. 如何开发一个npm包并发布到npm中央仓库

    转自: https://liaolongdong.com/2019/01/24/publish-public-npm.html 如何开发一个npm包并发布到npm中央仓库需求背景:平时在项目工作中可能 ...

  10. 开发一个健壮的npm包

    项目地址:loan-calculate-utils npm包的发布.更新查看上一篇文章 开发一个基础的npm包 目前我们的目录是这个样子: . ├── source 源代码目录 │   └── ind ...

随机推荐

  1. NGINX+Lua模块编译安装

    NGINX+Lua 环境配置 目录 NGINX+Lua 环境配置 一.环境装备 二.解压安装相应的软件 测试Lua环境 上面都是经过安装的一些坑之后安装完成的,下面是安装过程中出现的坑 一.环境装备 ...

  2. 记住这个错误org.apache.ibatis.binding.BindingException: Type interface com.kuang.dao.UserDAO is not known to the MapperRegistry.

    错误: org.apache.ibatis.binding.BindingException: Type interface com.kuang.dao.UserDAO is not known to ...

  3. python3GUI--实用!B站视频下载工具(附源码)

    目录 一.准备工作 二.预览 1.启动 2.解析 3.下载中 4.下载完成 5.结果 三.设计流程 1.bilibili_video_spider 2.视频json的查找 四.源代码 1.Bilibi ...

  4. js 遍历对象属性

    function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj); for (let propKey of propKeys) { ...

  5. 在winodws server r2上安装AD域

    一.创建第一个域 服务器管理-管理--添加角色和功能 其他都是默认值,下一步 二.重启后,已域账号登录 三.检查AD域有没有安装成功 1.查看计算机名 更改计算机名 2.检查是否含有以下,AD管理中心 ...

  6. Matlab:4维、单目标、约束、粒子群优化算法

    % 主调用函数(求最大值) clc; clear; close all; % 初始化种群 N = 100; % 初始种群个数 D = 4; % 空间维数 iter = 50; % 迭代次数 x_lim ...

  7. Adams:一种使接触力(力矩等等)失效的方法

    1 第一步:点击"运行脚本". 2 第二步:右击选择"仿真脚本",点击"创建". 3 第三步:选择"脚本类型"为&quo ...

  8. 7 Free Energies: 7.4 Umbrella Sampling Example

    7.4 Umbrella Sampling Example计算丙氨酸二肽 Phi/Psi 旋转的 PMF    http://ambermd.org/tutorials/advanced/tutori ...

  9. React-Hook知识整理与总结

    1.useState:让函数式组件拥有状态 2.useEffect:副作用,取代生命周期 3.useContext:跨组件共享数据 4.useCallback:性能优化 5.useMemo:性能优化 ...

  10. ssh双击互信

    默认公钥文件/root/.ssh/id_rsa.pub默认私钥文件/root/.ssh/id_rsa 只有将公钥文件文件拷到其他的服务器上才能登录别的服务器.   服务器A 192.168.1.133 ...