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

news2024/11/18 9:42:56

先介绍整个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请求

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

结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/791650.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【指针一:穿越编程边界的超能力】

本章重点 字符指针 数组指针 指针数组 数组传参和指针传参 先回顾一下指针的概念有哪些&#xff1f; 指针就是个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识一块内存空间。 指针的大小是固定的4/8个字节&#xff08;32位平台/64位平台&#xff09;。 指针是有类型&…

【java实习评审】对小说详情模块的更新判断以及数据库的商用字段设置比较到位

大家好&#xff0c;本篇文章分享一下【校招VIP】免费商业项目“推推”第一期书籍详情模块java同学的文档作品。该同学来自【山西农业大学】软件工程专业。 本项目亮点难点&#xff1a; 1 热门书籍在更新点的访问压力 2 书籍更新通知的及时性和有效性 3 书荒:同好推荐的可能性 4…

内部web服务器,部署HTTPS(nginx + 宝塔面板设置)

1、需求 在BS应用研发过程中&#xff0c;很多浏览器的功能需要在开启web服务器的https功能才能实现&#xff0c;例如chrome浏览器的系统剪切板&#xff08;CtrlC/CtrlV&#xff09;的使用&#xff0c;但是开发过程中&#xff0c;一般使用内部的服务器&#xff0c;只有IP地址&a…

vue3 - element-plus 上传各种 word pdf 文件、图片视频并上传到服务器功能效果,示例代码开箱即用。

效果图 在 vue3 项目中,使用 element plus 组件库的 el-upload 上传组件,进行文件、图片图像的上传功能示例。 完整代码 可直接复制,再改个接口地址。 在这里上传图片和文件是分成

软件测试简历项目经验该怎么写?【两年经验】

在写简历之前&#xff0c;我们先来看看失败者的简历和成功者的简历之间有什么区别。为什么成功者的简历可以在求职中起到“四两拨千斤”的作用&#xff0c;而失败者的简历却被丢进了垃圾桶&#xff0c;这两者到底有什么不同&#xff1f; 成功的简历与失败的简历 我们发现&…

google浏览器启用es6语法支持, 无需node webpack 浏览器端模块化加载

注意&#xff1a;最新版本的chrome浏览器已支持module语法&#xff0c;需要在web服务器环境下运行&#xff01; 设置浏览器启用es6语法功能&#xff1a; 1.在浏览器的url中输入&#xff1a;chrome://flags/ 设置下面选项为enable&#xff0c;重启浏览器。 准备代码&#xff…

FAP-inhibitors,FAP是一种II型跨膜丝氨酸蛋白酶,的反应特点及性质研究

资料编辑|陕西新研博美生物科技有限公司小编MISSwu PART1----产品描述 FAP inhibitors&#xff0c;其中FAP&#xff08;也称为seprase&#xff09;是一种II型跨膜丝氨酸蛋白酶。质膜和可溶性形式都表现出脯氨酸切割后的内肽酶活性&#xff0c;对Ala/Ser-Gly-Pro-Ser/Asn/Ala共…

【导航人机交互(HMI)模块功能】

在工业自动化领域&#xff0c;HMI是人机界面的缩写。在工业中使用HMI来控制和监控机器。如果没有HMI&#xff0c;就很难在工业中拥有良好的自动化流程。 人机界面的定义 人机界面 (HMI) 是一种用户界面&#xff0c;允许人类操作员与机器或过程进行交互。HMI通常用于工业环境&…

聚焦青少年近视管理现状,蔡司光学点亮乡村学童圆梦之旅

青少年近视管理问题一直是社会各界热议的焦点&#xff0c;尤其是近年来我国青少年近视率高居不下的问题突出&#xff0c;近视已经成为影响学生日常生活和学习用眼的重要因素之一。为贯彻落实关于学生近视问题的重要指示精神&#xff0c;并充分承担企业本身应具备的社会责任和使…

深入解析直播带货系统源码:打造高效互动的电商平台

直播带货成为了电商领域的热门趋势&#xff0c;为了构建一个高效互动的电商平台&#xff0c;我们需要深入解析直播带货系统的源码&#xff0c;探讨其实现的关键要素。 1. 初始化直播房间 首先&#xff0c;我们需要创建一个直播房间的功能&#xff0c;这样主播可以进入房间进…

获取对象中的第一个或者最后一个值

示例&#xff1a; 获取第一个值&#xff1a; ①可以先把它置换成数组使用Object.values(obj) 直接取第0个值就可以&#xff1a; Object.values(obj)[0] ②可以使用循环对象&#xff0c;使用break的原因是在它循环第一次的时候就停止(不推荐&#xff0c;因为后续值操作不了) …

IDEA 创建 python 项目

工具&#xff1a;IDEA 2023 一、安装插件 安装 python 插件 新建python项目 其中的Environment type 选择 virtualenv &#xff0c;表示为这个项目创建一个虚拟Python虚拟环境。至于为什么看下面。 每个项目使用的框架库并不一样&#xff0c;或使用框架的版本不一样&…

python中怎么使用作用域

目录 什么是作用域 作用域什么时候使用 python中怎么使用作用域 什么是作用域 作用域&#xff08;Scope&#xff09;是指在程序中变量、函数或对象的可访问性和可见性的范围。它决定了在代码中的哪些位置可以引用或操作某个标识符&#xff08;变量、函数、对象等&#xff09…

Shell输出帮助手册

代码&#xff1a; 帮助手册雏形 function help(){echo -e "Help manual":echo -e " -h. -- help View the help manual"echo -e " install Installation"echo -e " uninstall Uninstall" }case "$1&qu…

secureCRT软件菜单不见了的解决方法

1、打开securecrt软件 2、在方框处&#xff0c;选择右键&#xff0c;点击勾选 3、菜单即可正常显示了

AI绘画教程:为艺术而生的算法,你还在烦恼小红书与公众号的配图吗?

大家好&#xff0c;我是千寻哥&#xff0c;上一次我给大家解释了三种关于图像分割的SAM大模型的文章。 很多星友表示&#xff0c;千寻哥啊&#xff0c;你写的图像分割大模型的教程确实不错&#xff0c;但是现在经济略微不景气&#xff0c;还是想学习通过AI工具过着ChatGPT能靠赚…

粮食储备库电力配电监控系统的设计与应用 安科瑞 许敏

安科瑞许敏18706168252 摘要&#xff1a;本文主要介绍粮库电力监控系统的结构、基本功能&#xff0c;包括实时数据的采集与处理、数据库的建立 与维护、报警处理、画面生成及显示、在线计算及制表及系统自诊断&#xff0c;以及主要技术指标等 关键词&#xff1a;电力监控系统…

C语言每日一题:5.至少是其他数字的两倍+两个数组的交集。

第一题&#xff1a;至少是两倍其他数字的最大数 第一题&#xff1a; 思路一&#xff1a; 1.需要我们返回最大数值的下标&#xff0c;所以先循环遍历我们的这个数组记录一下最大的数值和下标位置。 2.使用qsort排序&#xff08;总是存在唯一的最大整数&#xff09; 3所以排序之…

伦敦金在非农双向挂单

对伦敦金投资有一定经验的投资者都知道&#xff0c;在非农时期&#xff0c;伦敦金市场会出现很大的波动&#xff0c;那么我们如何才能抓住这些波动呢&#xff1f;答案是很难的。但是&#xff0c;有些投资者在多年实践中发明了一种双向挂单的方法&#xff0c;这里和大家一切分享…

文艺类《匠心》简介及投稿要求

文艺类《匠心》简介及投稿要求 《匠心》期刊简介&#xff1a; 主管单位&#xff1a;内蒙古画报社 主办单位&#xff1a;内蒙古画报社 国际刊号&#xff1a;ISSN:1672-9099 国内刊号&#xff1a;CN:15-1383/J 发行周期&#xff1a;月刊;收录网站&#xff1a;中国知网收录 …