文章目录
- 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.目录结构

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><dependency><groupId>com.sunxiansheng</groupId><artifactId>common-rabbitmq-starter</artifactId><version>1.0.5</version></dependency><dependency><groupId>com.sunxiansheng</groupId><artifactId>common-openai-starter</artifactId><version>1.0.5</version></dependency><dependency><groupId>com.github.javaparser</groupId><artifactId>javaparser-core</artifactId><version>3.25.4</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><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;
@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.目录结构

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><dependency><groupId>com.sunxiansheng</groupId><artifactId>combinations-intelligent-analysis-starter</artifactId><version>1.0.5</version></dependency><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 username: guest@126.com password: guest rabbitmq:host: guestusername: guestpassword: guestvirtual-host: /port: 6783listener:simple:acknowledge-mode: auto retry:enabled: true max-attempts: 3 initial-interval: 5000ms multiplier: 2.0 stateless: true
openai:api-key: guest
4.IntelligentAnalysisApplication.java 启动类
package com.sunxiansheng.intelligent.analysis;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@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;
@Component
public class MailUtil {@Resourceprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String from;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);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 {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;}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;}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("");}}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;
public class StringUtils {public static String truncate(String input, int n) {if (input == null || n <= 0) {return "";}return input.length() > n ? input.substring(0, n) : input;}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);currentBytes += (c <= 0x7F) ? 1 : (c <= 0x7FF ? 2 : 3);if (currentBytes > byteLimit) {break;}endIndex = i + 1;}return input.substring(0, endIndex);}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) + "...";}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;}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;
@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;
@Configuration
public class RabbitMQConfig {@Beanpublic DirectExchange dlxExchange() {return new DirectExchange("dlxExchange");}@Beanpublic Queue dlxQueue() {return QueueBuilder.durable("dlxQueue").build();}@Beanpublic Binding dlxBinding() {return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx.elk");}@Beanpublic FanoutExchange elkExchange() {return new FanoutExchange("elk.exchange");}@Beanpublic Queue elkQueue() {return QueueBuilder.durable("elkQueue").withArgument("x-dead-letter-exchange", "dlxExchange").withArgument("x-dead-letter-routing-key", "dlx.elk").lazy().build();}@Beanpublic 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;
@Component
@Slf4j
public class ElkListener {@Resourceprivate OpenAiClient openAiClient;@Resourceprivate 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);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 {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) {Parser parser = Parser.builder().build();HtmlRenderer renderer = HtmlRenderer.builder().build();String analysisHtml = renderer.render(parser.parse(analysisResult));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;
@Component
@Slf4j
public class DlxQueueListener {@Resourceprivate MailUtil mailUtil;private static final String to = "sunxiansehng@gmail.com";@RabbitListener(queues = "dlxQueue")public void receiveDlxMessage(Message message) {log.error("DlxQueueListener:接收到死信消息");String messageBody = new String(message.getBody(), StandardCharsets.UTF_8);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 {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) {Parser parser = Parser.builder().build();HtmlRenderer renderer = HtmlRenderer.builder().build();String analysisHtml = renderer.render(parser.parse(analysisResult));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 直接抛出异常

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


3.AI分析的邮件


3.Logstash的配置以及系统执行流程
1.这个配置可以将消息发送到RabbitMQ
input {tcp {port => 9601codec => multiline {# 匹配日志行的开始(时间戳 + 分隔符 XYZ123DELIMITERXYZ123)pattern => "^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} XYZ123DELIMITERXYZ123"negate => truewhat => "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 的日志发送到 RabbitMQif [level] == "ERROR" {rabbitmq {host => "guest"port => 6783user => "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的日志分析,其余情况(方法调用链为空和死信队列)就会直接将错误日志的消息以邮件的形式发送