⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

背景

一个生产环境的应用可能每天产生数GB的日志。当系统出现问题时,开发者往往面临这样的困境:

  • 单个异常可能在短时间内重复出现成千上万次
  • 相同的错误堆栈信息淹没在海量日志中
  • 定位问题需要花费大量时间筛选重复信息
  • 传统日志系统缺乏智能聚合能力

特别是在高并发场景下,一个NPE可能在短时间内产生上万条相同的错误日志,严重影响问题定位效率。

痛点分析

1. 日志爆炸问题

// 典型场景:相同异常重复记录
2024-09-27 14:32:01 ERROR [trace-123] UserService - Cannot invoke "String.length()"
2024-09-27 14:32:01 ERROR [trace-124] UserService - Cannot invoke "String.length()"
2024-09-27 14:32:01 ERROR [trace-125] UserService - Cannot invoke "String.length()"
// ... 重复数千次

2. 问题定位困难

信息冗余:相同异常占用大量存储空间

检索效率低:在重复信息中找到关键线索耗时

上下文丢失:重要的链路追踪信息被淹没

3. 运维成本高

存储成本:重复日志占用大量磁盘空间

网络传输:日志收集系统压力巨大

分析时间:人工分析效率极低

解决思路

核心理念:错误指纹聚类

通过为每个异常生成唯一的”指纹”,将相同根因的错误聚合在一起,实现:

智能去重:相同异常只记录关键信息

统计聚合:展示错误频次和趋势

快速定位:通过指纹直接跳转到问题位置

链路保留:维护TraceId索引便于追踪

技术方案

异常发生 → 指纹生成 → LRU缓存 → 智能聚合 → 前端展示
↓ ↓ ↓ ↓ ↓
堆栈解析 MD5哈希 去重计数 阈值控制 可视化

核心实现

1. 错误指纹生成算法

@Component
public class ErrorFingerprintGenerator {

public String generateFingerprint(Throwable throwable) {
// 获取异常发生的根本位置
StackTraceElement rootCause = getRootCauseLocation(throwable);

StringBuilder fingerprint = new StringBuilder()
.append(throwable.getClass().getSimpleName())
.append("|")
.append(rootCause.getClassName())
.append("#")
.append(rootCause.getMethodName())
.append(":")
.append(rootCause.getLineNumber());

// 过滤异常消息中的动态值,保留结构特征
String filteredMessage = filterDynamicValues(throwable.getMessage());
if (StringUtils.isNotBlank(filteredMessage)) {
fingerprint.append("|").append(filteredMessage);
}

return DigestUtils.md5Hex(fingerprint.toString());
}

private String filterDynamicValues(String message) {
if (message == null) return "";

return message
// 过滤数字ID
.replaceAll("\\b\\d{4,}\\b", "NUM")
// 过滤UUID
.replaceAll("\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b", "UUID")
// 过滤时间戳
.replaceAll("\\b\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2}\\b", "TIMESTAMP")
// 限制长度
.substring(0, Math.min(message.length(), 100));
}
}

指纹算法特点

位置精确:基于异常类型+发生位置生成唯一标识

动态值过滤:自动过滤ID、时间戳等变化值

细粒度聚合:不同位置的相同异常类型产生不同指纹

2. LRU指纹缓存系统

@Component
public class ErrorFingerprintCache {

private final Map<String, ErrorFingerprint> cache =
Collections.synchronizedMap(new LinkedHashMap<String, ErrorFingerprint>(1000, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, ErrorFingerprint> eldest) {
return size() > 1000; // LRU淘汰
}
});

public boolean shouldLog(String fingerprint, String traceId, String exceptionType, String stackTrace) {
ErrorFingerprint errorInfo = cache.computeIfAbsent(fingerprint,
k -> new ErrorFingerprint(fingerprint, traceId, exceptionType, stackTrace));

long count = errorInfo.incrementAndGet();
errorInfo.setLastOccurrence(LocalDateTime.now());
errorInfo.addRecentTraceId(traceId);

// 首次出现或达到阈值时记录日志
return count == 1 || count % 10 == 0;
}
}

缓存策略优势

内存控制:LRU算法自动淘汰老旧指纹

智能阈值:每10次相同错误才输出一次完整日志

链路保留:保存最近5个TraceId用于追踪

3. 自定义Logback Appender

public class ErrorFingerprintAppender extends AppenderBase<ILoggingEvent> {

private ErrorFingerprintGenerator fingerprintGenerator;
private ErrorFingerprintCache fingerprintCache;

@Override
protected void append(ILoggingEvent event) {
if (isErrorEvent(event)) {
handleErrorEvent(event, getCurrentTraceId());
} else {
consoleAppender.doAppend(event);
}
}

private void handleErrorEvent(ILoggingEvent event, String traceId) {
Throwable throwable = convertToThrowable(event.getThrowableProxy());
String fingerprint = fingerprintGenerator.generateFingerprint(throwable);

if (fingerprintCache.shouldLog(fingerprint, traceId,
throwable.getClass().getSimpleName(), getStackTraceString(throwable))) {
// 输出增强的日志,包含指纹和统计信息
ILoggingEvent enhancedEvent = createEnhancedEvent(event, fingerprint, traceId);
consoleAppender.doAppend(enhancedEvent);
}
}
}

4. 可视化管理界面

实现了完整的Web管理界面,支持:

实时统计:总指纹数、总错误数、缓存使用率

错误列表:按频次排序的异常聚合信息

详情查看:完整堆栈跟踪、链路ID、时间分布

错误模拟:支持NPE、IAE、IOException等类型测试

应用场景示例

场景1:高并发NPE定位

传统方式

# 10万条重复日志中查找
grep "NullPointerException" app.log | wc -l
# 输出:100000

# 需要人工去重和分析
grep -n "NullPointerException" app.log | head -20

指纹聚类方式

# 系统自动聚合
[FINGERPRINT:8c91b4b7][COUNT:100000][TRACE:abc123] UserService - NPE at line 45
[SIMILAR_ERRORS:100000][FIRST_SEEN:2024-09-27 14:32:01]

# 5秒直达问题位置:UserService.java:45

场景2:分布式系统异常分析

在微服务架构中,同一个错误可能在多个实例中出现:

// 服务实例1
[FINGERPRINT:a1b2c3d4][COUNT:1500] OrderService.calculateTotal:78 - IAE
// 服务实例2
[FINGERPRINT:a1b2c3d4][COUNT:800] OrderService.calculateTotal:78 - IAE
// 服务实例3
[FINGERPRINT:a1b2c3d4][COUNT:1200] OrderService.calculateTotal:78 - IAE

通过指纹聚合,立即识别这是同一个问题在不同实例的表现。

场景3:问题识别

通过错误频次统计,快速识别系统热点问题:

// 错误频次TOP5
1. NPE in UserService.getProfile:45 → 50000次/小时
2. IAE in OrderService.validate:120 → 30000次/小时
3. IOException in PaymentService:88 → 15000次/小时

性能与效果

控制台输出优化

去重效果:相同异常每10次才输出一次完整日志

信息增强:每条日志包含指纹、计数、链路信息

噪音减少:避免重复异常信息干扰问题定位

开发效率

问题定位:从”大海捞针”到”精准制导”

上下文保留:完整堆栈+链路追踪+频次统计

趋势分析:错误频次和时间分布一目了然

运维优势

告警优化:基于指纹去重,避免重复告警干扰

健康度评估:准确统计系统异常类型和频次

根因分析:快速识别影响最大的核心问题

总结

基于Spring Boot的错误指纹聚类系统,通过MD5指纹算法和LRU缓存机制,将重复异常智能聚合,提高问题分析排查效率。

同时可以扩展到FileAppender(本示例为方便演示使用的ConsoleAppender)实现错误日志去重输出,降低异常堆栈的日志存储量。

https://github.com/yuboon/java-examples/tree/master/springboot-exception-group

文章目录
  1. 1. 背景
  2. 2. 痛点分析
    1. 2.0.1. 1. 日志爆炸问题
    2. 2.0.2. 2. 问题定位困难
    3. 2.0.3. 3. 运维成本高
  • 3. 解决思路
    1. 3.0.1. 核心理念:错误指纹聚类
    2. 3.0.2. 技术方案
  • 4. 核心实现
    1. 4.0.1. 1. 错误指纹生成算法
    2. 4.0.2. 2. LRU指纹缓存系统
    3. 4.0.3. 3. 自定义Logback Appender
    4. 4.0.4. 4. 可视化管理界面
  • 5. 应用场景示例
    1. 5.0.1. 场景1:高并发NPE定位
    2. 5.0.2. 场景2:分布式系统异常分析
    3. 5.0.3. 场景3:问题识别
  • 6. 性能与效果
    1. 6.0.1. 控制台输出优化
    2. 6.0.2. 开发效率
    3. 6.0.3. 运维优势
  • 7. 总结