当前位置: 首页 > article >正文

AI智能日志分析系统

文章目录

    • 1.combinations-intelligent-analysis-starter
        • 1.目录结构
        • 2.pom.xml
        • 3.自动配置
          • 1.IntelligentAnalysisAutoConfiguration.java
          • 2.spring.factories
    • 2.combinations-intelligent-analysis-starter-demo
        • 1.目录结构
        • 2.pom.xml
        • 3.application.yml
        • 4.IntelligentAnalysisApplication.java 启动类
        • 5.工具类
          • 1.MailUtil.java 发送邮件
          • 2.MethodCallChainUtil.java 根据堆栈信息从Gitee获取源码并提取每个方法的调用链
          • 3.StringUtils.java 合并然后截取指定长度字符串
        • 6.ELKEntity.java ELK映射实体类
        • 7.RabbitMQConfig.java
        • 8.ElkListener.java 监听从Logstash中发送过来的日志消息
        • 9.DlxQueueListener.java 监听死信队列,确保消费者可靠性
        • 10.结果展示
          • 1.combinations-elk-starter-demo 直接抛出异常
          • 2.combinations-intelligent-analysis-starter-demo 开始监听,一旦发生异常,就进行ai分析
          • 3.AI分析的邮件
    • 3.Logstash的配置以及系统执行流程
        • 1.这个配置可以将消息发送到RabbitMQ
        • 2.AI智能日志分析系统执行流程
          • 1.Logstash采集日志,当日志为ERROR的时候发送到RabbitMQ
          • 2.RabbitMQ监听到日志进行处理
            • 1.通过javaparser根据异常堆栈来解析出所有自己项目的groupId下的类路径和方法名
            • 2.通过仓库名字+日志中的moudle名+类路径就可以从Gitee中获取这个类的代码
            • 3.再使用javaparser去获取到这个方法的调用链,就是当前方法以及调用了当前方法的内容
            • 4.将方法调用链和异常堆栈进行截取后交给AI智能分析日志,给出解决方案
            • 5.为了解决OpenAI的接口调用速率限制,采用消费者指数退避重试机制加上死信队列的方式确保消息正常消费
            • 6.考虑成本问题,只有当方法调用链不为空的时候才进行AI的日志分析,其余情况(方法调用链为空和死信队列)就会直接将错误日志的消息以邮件的形式发送

1.combinations-intelligent-analysis-starter

1.目录结构

CleanShot 2025-01-02 at 21.30.55@2x

2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.sunxiansheng</groupId>
        <artifactId>sunrays-combinations</artifactId>
        <version>1.0.5</version>
    </parent>

    <version>1.0.5</version>

    <artifactId>combinations-intelligent-analysis-starter</artifactId>

    <dependencies>
        <!-- common-rabbitmq-starter -->
        <dependency>
            <groupId>com.sunxiansheng</groupId>
            <artifactId>common-rabbitmq-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <!-- common-openai-starter -->
        <dependency>
            <groupId>com.sunxiansheng</groupId>
            <artifactId>common-openai-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <!-- javaparser-core -->
        <dependency>
            <groupId>com.github.javaparser</groupId>
            <artifactId>javaparser-core</artifactId>
            <version>3.25.4</version>
        </dependency>
        <!-- jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- common-mail-starter -->
        <dependency>
            <groupId>com.sunxiansheng</groupId>
            <artifactId>common-mail-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.vladsch.flexmark</groupId>
            <artifactId>flexmark-all</artifactId>
            <version>0.62.2</version>
        </dependency>
    </dependencies>

</project>
3.自动配置
1.IntelligentAnalysisAutoConfiguration.java
package com.sunxiansheng.intelligent.analysis.config;

import org.springframework.context.annotation.Configuration;

/**
 * Description: 智能分析自动配置类
 *
 * @Author sun
 * @Create 2025/1/1 19:27
 * @Version 1.0
 */
@Configuration
public class IntelligentAnalysisAutoConfiguration {

}
2.spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sunxiansheng.intelligent.analysis.config.IntelligentAnalysisAutoConfiguration

2.combinations-intelligent-analysis-starter-demo

1.目录结构

CleanShot 2025-01-02 at 21.33.01@2x

2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.sunxiansheng</groupId>
        <artifactId>sunrays-demo</artifactId>
        <version>1.0.5</version>
    </parent>

    <version>1.0.5</version>

    <artifactId>combinations-intelligent-analysis-starter-demo</artifactId>

    <dependencies>
        <!-- combinations-intelligent-analysis-starter -->
        <dependency>
            <groupId>com.sunxiansheng</groupId>
            <artifactId>combinations-intelligent-analysis-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <!-- common-log4j2-starter -->
        <dependency>
            <groupId>com.sunxiansheng</groupId>
            <artifactId>common-log4j2-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
    </dependencies>
</project>
3.application.yml
spring:
  # 邮件配置
  mail:
    host: smtp.126.com  # 邮箱服务商的SMTP服务器地址
    username: guest@126.com  # 邮箱账户
    password: guest  # 邮箱授权码或密码
  # RabbitMQ 配置
  rabbitmq:
    # 服务器地址
    host: guest
    # 用户名
    username: guest
    # 密码
    password: guest
    # 虚拟主机
    virtual-host: /
    # 端口
    port: 6783
    # 消费者配置
    listener:
      simple:
        acknowledge-mode: auto # 自动确认模式(消费者确认机制)
        retry:
          enabled: true # 开启重试机制
          max-attempts: 3 # 最大尝试次数
          initial-interval: 5000ms # 重试间隔时间(5s)
          multiplier: 2.0 # 重试时间间隔倍数
          stateless: true # false:有状态,true:无状态,如果是有状态的,每次重试都会发送到同一个队列
openai:
  api-key: guest
4.IntelligentAnalysisApplication.java 启动类
package com.sunxiansheng.intelligent.analysis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Description: 智能分析启动类
 *
 * @Author sun
 * @Create 2025/1/1 19:29
 * @Version 1.0
 */
@SpringBootApplication
public class IntelligentAnalysisApplication {

    public static void main(String[] args) {
        SpringApplication.run(IntelligentAnalysisApplication.class, args);
    }
}
5.工具类
1.MailUtil.java 发送邮件
package com.sunxiansheng.intelligent.analysis.utils;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.io.UnsupportedEncodingException;

/**
 * Description: 邮件工具类
 *
 * @Author sun
 * @Create 2025/1/2 18:36
 * @Version 1.0
 */
@Component
public class MailUtil {

    @Resource
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    /**
     * 发送html邮件,没报错就是发送成功了
     *
     * @param to          收件人
     * @param name        发件人名称
     * @param subject     邮件主题
     * @param htmlContent 邮件内容
     */
    public void sendHtmlMessage(String to, String name, String subject, String htmlContent) throws UnsupportedEncodingException, MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        // 创建邮件发送者地址
        helper.setFrom(new InternetAddress(MimeUtility.encodeText(name) + "<" + from + ">"));
        // 创建邮件发送者地址
        helper.setTo(new InternetAddress(MimeUtility.encodeText("接收方") + "<" + to + ">"));
        // 标题
        helper.setSubject(subject);
        // 第二个参数指定发送的是HTML格式
        helper.setText(htmlContent, true);
        mailSender.send(message);
    }
}
2.MethodCallChainUtil.java 根据堆栈信息从Gitee获取源码并提取每个方法的调用链
package com.sunxiansheng.intelligent.analysis.utils;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MethodCallChainUtil {

    /**
     * 从堆栈信息中提取每个方法的调用链
     *
     * @param stackTrace  堆栈信息
     * @param giteeRepo   Gitee 仓库地址
     * @param moduleName  模块名
     * @param branchName  分支名称
     * @param classPrefix 类路径前缀过滤器
     * @return 每个方法的调用链列表
     * @throws Exception 异常信息
     */
    public static List<String> extractMethodCallChainsFromStackTrace(
            String stackTrace,
            String giteeRepo,
            String moduleName,
            String branchName,
            String classPrefix) throws Exception {
        List<String> callChains = new ArrayList<>();

        // 正则匹配堆栈中的类路径和方法名
        Pattern pattern = Pattern.compile("at ([\\w\\.]+)\\.([\\w]+)\\((\\w+\\.java):(\\d+)\\)");
        Matcher matcher = pattern.matcher(stackTrace);

        while (matcher.find()) {
            String classPath = matcher.group(1); // 类路径
            String methodName = matcher.group(2); // 方法名

            // 过滤掉不符合指定前缀的类
            if (!classPath.startsWith(classPrefix)) {
                continue;
            }

            // 从 Gitee 仓库获取类文件内容
            try {
                String classContent = readClassFileFromGitee(classPath, giteeRepo, moduleName, branchName);
                // 获取方法的调用链
                String methodCallChain = extractMethodCallChain(classContent, methodName);
                callChains.add("类: " + classPath + "\n" + methodCallChain);
            } catch (Exception e) {
                System.err.println("无法解析方法 " + methodName + " 于类: " + classPath);
            }
        }

        return callChains;
    }

    /**
     * 从 Gitee 仓库中读取类文件内容
     *
     * @param classPath  类路径
     * @param giteeRepo  Gitee 仓库地址
     * @param moduleName 模块名
     * @param branchName 分支名称
     * @return 类文件内容字符串
     * @throws Exception 如果类文件不存在或读取失败
     */
    private static String readClassFileFromGitee(
            String classPath,
            String giteeRepo,
            String moduleName,
            String branchName) throws Exception {
        String filePath = "src/main/java/" + classPath.replace(".", "/") + ".java";
        String url = String.format("%s/raw/%s/%s/%s", giteeRepo, branchName, moduleName, filePath);

        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");

        if (connection.getResponseCode() != 200) {
            throw new IllegalArgumentException("无法从 Gitee 获取类文件: " + url);
        }

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            return reader.lines().reduce((a, b) -> a + "\n" + b).orElse("");
        }
    }

    /**
     * 提取方法的调用链
     *
     * @param classContent 类文件内容
     * @param methodName   方法名
     * @return 方法调用链
     * @throws Exception 如果解析失败或方法未找到
     */
    private static String extractMethodCallChain(String classContent, String methodName) throws Exception {
        JavaParser javaParser = new JavaParser();
        ParseResult<CompilationUnit> parseResult = javaParser.parse(classContent);

        if (!parseResult.isSuccessful() || !parseResult.getResult().isPresent()) {
            throw new IllegalArgumentException("无法解析类文件内容");
        }

        CompilationUnit compilationUnit = parseResult.getResult().get();
        Optional<ClassOrInterfaceDeclaration> classDeclarationOpt = compilationUnit.findFirst(ClassOrInterfaceDeclaration.class);
        if (!classDeclarationOpt.isPresent()) {
            throw new IllegalArgumentException("未找到类定义");
        }

        ClassOrInterfaceDeclaration classDeclaration = classDeclarationOpt.get();

        // 使用队列递归查找调用链
        Queue<String> methodQueue = new LinkedList<>();
        Set<String> processedMethods = new HashSet<>();
        methodQueue.add(methodName);

        StringBuilder callChain = new StringBuilder();
        callChain.append("调用链:\n");

        // 开始递归提取方法内容及调用链
        while (!methodQueue.isEmpty()) {
            String currentMethodName = methodQueue.poll();
            if (processedMethods.contains(currentMethodName)) {
                continue; // 防止重复处理方法
            }
            processedMethods.add(currentMethodName);

            Optional<MethodDeclaration> methodOpt = classDeclaration.findAll(MethodDeclaration.class).stream()
                    .filter(method -> method.getNameAsString().equals(currentMethodName))
                    .findFirst();

            if (!methodOpt.isPresent()) {
                callChain.append("未找到方法: ").append(currentMethodName).append("\n");
                continue;
            }

            MethodDeclaration method = methodOpt.get();
            callChain.append("方法: ").append(currentMethodName).append("\n")
                    .append(method).append("\n\n");

            // 查找调用此方法的其他方法
            for (MethodDeclaration callerMethod : classDeclaration.findAll(MethodDeclaration.class)) {
                if (!processedMethods.contains(callerMethod.getNameAsString())) {
                    boolean callsTarget = callerMethod.findAll(MethodCallExpr.class).stream()
                            .anyMatch(call -> call.getNameAsString().equals(currentMethodName));
                    if (callsTarget) {
                        methodQueue.add(callerMethod.getNameAsString());
                        callChain.append("方法 '").append(callerMethod.getNameAsString())
                                .append("' 调用了方法 '").append(currentMethodName).append("':\n");
                        callChain.append(callerMethod).append("\n\n");
                    }
                }
            }
        }

        return callChain.toString();
    }
}
3.StringUtils.java 合并然后截取指定长度字符串
package com.sunxiansheng.intelligent.analysis.utils;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * Description: 字符串工具类
 *
 * @Author sun
 * @Create 2025/1/2 15:26
 * @Version 1.0
 */
public class StringUtils {

    /**
     * 截取字符串的前n个字符。
     * 如果字符串长度小于n,则返回原字符串。
     *
     * @param input 原字符串
     * @param n     截取的字符数
     * @return 截取后的字符串
     */
    public static String truncate(String input, int n) {
        if (input == null || n <= 0) {
            return "";
        }
        return input.length() > n ? input.substring(0, n) : input;
    }

    /**
     * 按字节数截取字符串(支持多字节字符)。
     * 如果字符串总字节数小于限制,直接返回原字符串。
     *
     * @param input     原字符串
     * @param byteLimit 最大字节数
     * @return 截取后的字符串
     */
    public static String truncateByBytes(String input, int byteLimit) {
        if (input == null || byteLimit <= 0) {
            return "";
        }
        byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
        if (bytes.length <= byteLimit) {
            return input;
        }

        // 按字节截取字符串
        int endIndex = 0;
        int currentBytes = 0;
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);
            // UTF-8编码:ASCII占1字节,其他占2或3字节
            currentBytes += (c <= 0x7F) ? 1 : (c <= 0x7FF ? 2 : 3);
            if (currentBytes > byteLimit) {
                break;
            }
            endIndex = i + 1;
        }
        return input.substring(0, endIndex);
    }

    /**
     * 截取字符串的前n个字符并在超长时添加省略号(...)。
     *
     * @param input 原字符串
     * @param n     截取的字符数
     * @return 截取后的字符串(可能包含省略号)
     */
    public static String truncateWithEllipsis(String input, int n) {
        if (input == null || n <= 0) {
            return "";
        }
        if (input.length() <= n) {
            return input;
        }
        return input.substring(0, n) + "...";
    }

    /**
     * 按字节截取字符串并添加省略号(...)。
     *
     * @param input     原字符串
     * @param byteLimit 最大字节数
     * @return 截取后的字符串(可能包含省略号)
     */
    public static String truncateByBytesWithEllipsis(String input, int byteLimit) {
        if (input == null || byteLimit <= 0) {
            return "";
        }
        String truncated = truncateByBytes(input, byteLimit - 3);
        return truncated.length() < input.length() ? truncated + "..." : truncated;
    }

    /**
     * 合并字符串列表,截取指定长度,并在超长时添加省略号。
     *
     * @param stringList 字符串列表
     * @param maxLength  最大字符数
     * @return 截取后的字符串(可能包含省略号)
     */
    public static String mergeAndTruncateWithEllipsis(List<String> stringList, int maxLength) {
        if (stringList == null || stringList.isEmpty() || maxLength <= 0) {
            return "";
        }

        // 合并字符串
        StringBuilder merged = new StringBuilder();
        for (String str : stringList) {
            if (str != null) {
                merged.append(str);
            }
        }

        // 截取字符串
        String result = merged.toString();
        if (result.length() > maxLength) {
            return result.substring(0, maxLength) + "...";
        }
        return result;
    }
}
6.ELKEntity.java ELK映射实体类
package com.sunxiansheng.intelligent.analysis.entity;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * Description: ELK实体类
 *
 * @Author sun
 * @Create 2025/1/1 18:08
 * @Version 1.0
 */
@Data
public class ELKEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    private String traceId;

    private String thread;

    private String logger;

    private String throwable;

    private String module;

    private String level;

    private String timestamp;

    private String host;

    @JsonProperty("log_message")
    private String logMessage;
}
7.RabbitMQConfig.java
package com.sunxiansheng.intelligent.analysis.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Description:  RabbitMQ配置类
 *
 * @Author sun
 * @Create 2025/1/1 17:09
 * @Version 1.0
 */
@Configuration
public class RabbitMQConfig {

    /**
     * 死信交换机
     *
     * @return
     */
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlxExchange");
    }

    /**
     * 死信队列
     *
     * @return
     */
    @Bean
    public Queue dlxQueue() {
        return QueueBuilder.durable("dlxQueue")
                .build();
    }

    /**
     * 死信队列绑定死信交换机
     *
     * @return
     */
    @Bean
    public Binding dlxBinding() {
        return BindingBuilder
                .bind(dlxQueue())
                .to(dlxExchange())
                .with("dlx.elk");
    }

    /**
     * 创建一个fanout类型的elk交换机
     *
     * @return
     */
    @Bean
    public FanoutExchange elkExchange() {
        return new FanoutExchange("elk.exchange");
    }

    /**
     * 创建一个elk队列,并设置死信交换机和死信路由键
     *
     * @return
     */
    @Bean
    public Queue elkQueue() {
        return QueueBuilder.durable("elkQueue")
                .withArgument("x-dead-letter-exchange", "dlxExchange")
                .withArgument("x-dead-letter-routing-key", "dlx.elk")
                .lazy()
                .build();
    }

    /**
     * 交换机和队列绑定
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(elkQueue()).to(elkExchange());
    }
}
8.ElkListener.java 监听从Logstash中发送过来的日志消息
package com.sunxiansheng.intelligent.analysis.consumer;

import com.sunxiansheng.intelligent.analysis.entity.ELKEntity;
import com.sunxiansheng.intelligent.analysis.utils.MailUtil;
import com.sunxiansheng.intelligent.analysis.utils.MethodCallChainUtil;
import com.sunxiansheng.intelligent.analysis.utils.StringUtils;
import com.sunxiansheng.openai.client.OpenAiClient;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * Description: elk消息监听
 *
 * @Author sun
 * @Create 2025/1/1 17:17
 * @Version 1.0
 */
@Component
@Slf4j
public class ElkListener {

    @Resource
    private OpenAiClient openAiClient;

    @Resource
    private MailUtil mailUtil;

    private static final String to = "sunxiansehng@gmail.com";

    @RabbitListener(queues = "elkQueue")
    public void receive(ELKEntity message) throws Exception {
        String timestamp = message.getTimestamp();
        String logger = message.getLogger();
        String module = message.getModule();
        String throwable = message.getThrowable();
        String host = message.getHost();
        String logMessage = message.getLogMessage();

        analyze(throwable, module, timestamp, logger, host, logMessage);
    }

    public void analyze(String throwable, String module, String timestamp, String logger,
                        String host, String logMessage) throws Exception {

        List<String> methodCallChains = MethodCallChainUtil.extractMethodCallChainsFromStackTrace(
                throwable,
                "https://gitee.com/qyxinhua_0/sunrays-framework",
                module,
                "master",
                "com.sunxiansheng");

        // 如果方法调用链为空,则直接返回
        if (methodCallChains.isEmpty()) {
            sendMailWithDetails("线上报错(无AI分析)", "无", timestamp, logger, module, host, logMessage, throwable);
            log.info("方法调用链为空,无法分析问题");
            return;
        }

        String pattern = "        问题: 在这里总结一个问题标题\n" +
                "        ----------------------------------------\n" +
                "        1. 问题产生原因:\n" +
                "           在这里写原因\n" +
                "        ----------------------------------------\n" +
                "        2. 问题解决方式:\n" +
                "           在这里写解决方式\n" +
                "        ----------------------------------------\n";

        String info = String.format("方法调用链:%s 异常信息:%s",
                StringUtils.mergeAndTruncateWithEllipsis(methodCallChains, 500),
                StringUtils.truncateWithEllipsis(throwable, 500)
        );

        String question = String.format(
                "我会给你我的方法调用链以及异常信息:\n%s\n" +
                        "请帮我按照下面的格式去分析一下问题产生的原因和解决方式:\n%s",
                info,
                pattern
        );

        log.info("问题:{}", question);
        String aiAns = openAiClient.askAI("gpt-4o", question, false);

        log.info("AI回答:{}", aiAns);

        // 发送AI分析邮件
        sendMailWithDetails("线上报错(有AI分析)", aiAns, timestamp, logger, module, host, logMessage, throwable);
    }

    private void sendMailWithDetails(String subject, String analysisResult, String timestamp, String logger,
                                     String module, String host, String logMessage, String throwable) throws Exception {
        // 构建邮件的HTML格式内容
        String htmlContent = buildHtmlContent(subject, analysisResult, timestamp, logger, module, host, logMessage, throwable);

        // 发送邮件
        mailUtil.sendHtmlMessage(to, "SunRays-Framework", subject, htmlContent);
    }

    public String buildHtmlContent(String subject, String analysisResult, String timestamp, String logger,
                                   String module, String host, String logMessage, String throwable) {
        // 使用 Flexmark 将 analysisResult 转换为 HTML
        Parser parser = Parser.builder().build();
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        String analysisHtml = renderer.render(parser.parse(analysisResult));

        // 构建 HTML 内容
        return "<html><body>" +
                "<h2>" + subject + "</h2>" +
                "<table border='1' cellpadding='10' cellspacing='0'>" +
                "<tr><td><strong>时间戳</strong></td><td>" + timestamp + "</td></tr>" +
                "<tr><td><strong>日志器</strong></td><td>" + logger + "</td></tr>" +
                "<tr><td><strong>模块</strong></td><td>" + module + "</td></tr>" +
                "<tr><td><strong>主机</strong></td><td>" + host + "</td></tr>" +
                "<tr><td><strong>日志信息</strong></td><td>" + logMessage + "</td></tr>" +
                "<tr><td><strong>异常信息</strong></td><td><pre>" + throwable + "</pre></td></tr>" +
                "</table>" +
                "<h3>AI分析结果:</h3>" +
                "<div>" + analysisHtml + "</div>" +  // 渲染后的分析结果
                "</body></html>";
    }
}
9.DlxQueueListener.java 监听死信队列,确保消费者可靠性
package com.sunxiansheng.intelligent.analysis.consumer;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sunxiansheng.intelligent.analysis.entity.ELKEntity;
import com.sunxiansheng.intelligent.analysis.utils.MailUtil;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;

/**
 * Description: 死信队列监听
 *
 * @Author sun
 * @Create 2025/1/2 12:39
 * @Version 1.0
 */
@Component
@Slf4j
public class DlxQueueListener {

    @Resource
    private MailUtil mailUtil;

    private static final String to = "sunxiansehng@gmail.com";

    /**
     * 死信队列消息消费方法
     *
     * @param message 死信队列中的消息
     */
    @RabbitListener(queues = "dlxQueue")
    public void receiveDlxMessage(Message message) {
        // 处理死信消息,通常是日志记录、报警或人工干预
        log.error("DlxQueueListener:接收到死信消息");
        // 获取消息体
        String messageBody = new String(message.getBody(), StandardCharsets.UTF_8);
        // 使用 Jackson 将消息体反序列化为 ELKEntity 对象
        ObjectMapper objectMapper = new ObjectMapper();
        // 忽略掉未知属性
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        try {
            ELKEntity elkEntity = objectMapper.readValue(messageBody, ELKEntity.class);
            String logger = elkEntity.getLogger();
            String throwable = elkEntity.getThrowable();
            String module = elkEntity.getModule();
            String timestamp = elkEntity.getTimestamp();
            String host = elkEntity.getHost();
            String logMessage = elkEntity.getLogMessage();
            sendMailWithDetails("线上报错(死信队列消息)", "无", timestamp, logger, module, host, logMessage, throwable);
        } catch (Exception e) {
            log.error("DlxQueueListene:反序列化消息失败", e);
        }
    }

    private void sendMailWithDetails(String subject, String analysisResult, String timestamp, String logger,
                                     String module, String host, String logMessage, String throwable) throws Exception {
        // 构建邮件的HTML格式内容
        String htmlContent = buildHtmlContent(subject, analysisResult, timestamp, logger, module, host, logMessage, throwable);

        // 发送邮件
        mailUtil.sendHtmlMessage(to, "SunRays-Framework", subject, htmlContent);
    }

    public String buildHtmlContent(String subject, String analysisResult, String timestamp, String logger,
                                   String module, String host, String logMessage, String throwable) {
        // 使用 Flexmark 将 analysisResult 转换为 HTML
        Parser parser = Parser.builder().build();
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        String analysisHtml = renderer.render(parser.parse(analysisResult));

        // 构建 HTML 内容
        return "<html><body>" +
                "<h2>" + subject + "</h2>" +
                "<table border='1' cellpadding='10' cellspacing='0'>" +
                "<tr><td><strong>时间戳</strong></td><td>" + timestamp + "</td></tr>" +
                "<tr><td><strong>日志器</strong></td><td>" + logger + "</td></tr>" +
                "<tr><td><strong>模块</strong></td><td>" + module + "</td></tr>" +
                "<tr><td><strong>主机</strong></td><td>" + host + "</td></tr>" +
                "<tr><td><strong>日志信息</strong></td><td>" + logMessage + "</td></tr>" +
                "<tr><td><strong>异常信息</strong></td><td><pre>" + throwable + "</pre></td></tr>" +
                "</table>" +
                "<h3>AI分析结果:</h3>" +
                "<div>" + analysisHtml + "</div>" +  // 渲染后的分析结果
                "</body></html>";
    }
}
10.结果展示
1.combinations-elk-starter-demo 直接抛出异常

CleanShot 2025-01-02 at 21.43.05@2x

2.combinations-intelligent-analysis-starter-demo 开始监听,一旦发生异常,就进行ai分析

CleanShot 2025-01-02 at 21.44.56@2x

CleanShot 2025-01-02 at 21.45.22@2x

3.AI分析的邮件

CleanShot 2025-01-02 at 21.46.06@2x

CleanShot 2025-01-02 at 21.46.31@2x

3.Logstash的配置以及系统执行流程

1.这个配置可以将消息发送到RabbitMQ
input {
  tcp {
    port => 9601
    codec => multiline {
      # 匹配日志行的开始(时间戳 + 分隔符 XYZ123DELIMITERXYZ123)
      pattern => "^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} XYZ123DELIMITERXYZ123"
      negate => true
      what => "previous"
      auto_flush_interval => 5
    }
  }
}

filter {
  dissect {
    mapping => {
      "message" => "%{timestamp} XYZ123DELIMITERXYZ123 [%{thread}] XYZ123DELIMITERXYZ123 %{level} XYZ123DELIMITERXYZ123 [PFTID:%{traceId}] XYZ123DELIMITERXYZ123 [Module:%{module}] XYZ123DELIMITERXYZ123 %{logger} XYZ123DELIMITERXYZ123 %{log_message} XYZ123DELIMITERXYZ123 %{throwable}"
    }
    remove_field => ["message"]
  }

  date {
    match => ["timestamp", "yyyy-MM-dd HH:mm:ss.SSS"]
    timezone => "Asia/Shanghai"   # 根据您的实际时区进行设置
    target => "@timestamp"
  }

}

output {
  elasticsearch {
    hosts => ["http://guest:9200"]  # 替换为您的 Elasticsearch 地址
    index => "java-logs-%{+YYYY.MM.dd}"      # 按日期创建索引
  }
  
  # 将 level 为 ERROR 的日志发送到 RabbitMQ
  if [level] == "ERROR" {
    rabbitmq {
      host => "guest"
      port => 6783
      user => "guest"
      password => "guest"
      vhost => "/"
      exchange => "elk.exchange"
      exchange_type => "fanout"
      message_properties => {
            "content_type" => "application/json"
             "priority" => 1
            }
      }
  }

  # 调试用,输出到控制台
  stdout {
    codec => rubydebug
  }
}

2.AI智能日志分析系统执行流程
1.Logstash采集日志,当日志为ERROR的时候发送到RabbitMQ
2.RabbitMQ监听到日志进行处理
1.通过javaparser根据异常堆栈来解析出所有自己项目的groupId下的类路径和方法名
2.通过仓库名字+日志中的moudle名+类路径就可以从Gitee中获取这个类的代码
3.再使用javaparser去获取到这个方法的调用链,就是当前方法以及调用了当前方法的内容
4.将方法调用链和异常堆栈进行截取后交给AI智能分析日志,给出解决方案
5.为了解决OpenAI的接口调用速率限制,采用消费者指数退避重试机制加上死信队列的方式确保消息正常消费
6.考虑成本问题,只有当方法调用链不为空的时候才进行AI的日志分析,其余情况(方法调用链为空和死信队列)就会直接将错误日志的消息以邮件的形式发送

http://www.kler.cn/a/520359.html

相关文章:

  • 使用Python和Qt6创建GUI应用程序--前言
  • leetcode刷题记录(一百)——121. 买卖股票的最佳时机
  • Objective-C语言的计算机基础
  • 【CSS入门学习】Flex布局设置div水平、垂直分布与居中
  • 关于使用PHP时WordPress排错——“这意味着您在wp-config.php文件中指定的用户名和密码信息不正确”的解决办法
  • 【Uniapp-Vue3】setTabBar设置TabBar和下拉刷新API
  • DiffuEraser: 一种基于扩散模型的视频修复技术
  • Science Advances 用于独立检测压力和温度的3D主动矩阵多模态传感器阵列
  • 单片机基础模块学习——DS18B20温度传感器芯片
  • k8s使用nfs持久卷
  • 【dash技巧】静默保存fact(feffery_antd_charts)的图表到本地目录
  • [论文阅读] SPOT: SE(3) Pose Trajectory Diffusion for Object-Centric Manipulation
  • MySQL分表自动化创建的实现方案(存储过程、事件调度器)
  • 【Axure高保真原型】商场流量大屏可视化分析案例
  • 2025_1_26 c++中关于构造和析构的顺序
  • 数论问题71一一兔子数列
  • 检测到联想鼠标自动调出运行窗口,鼠标自己作为键盘操作
  • WPF基础 | WPF 布局系统深度剖析:从 Grid 到 StackPanel
  • XSS知识点初步汇总
  • WPS计算机二级•幻灯片的段落排版
  • Harmony Next 跨平台开发入门
  • ubuntu22安装issac gym记录
  • 求阶乘(信息学奥赛一本通-2019)
  • 【MCAL实战】MCU模块配置实践
  • es 3期 第26节-文本分词深入探查
  • 基于迁移学习的ResNet50模型实现石榴病害数据集多分类图片预测