首页 > 应用开发 > Java > LogParser-LLM 前缀树算法实现
根据 LogParser-LLM 的架构设计,前缀树(Prefix Tree / Prefix Parse Tree)的核心目标是作为 高速缓存,拦截 99% 的重复日志模式,仅在无法“严格匹配”时才调用 LLM。 以下是基于论文描述的 Java 实现方案。该实现涵盖了核心数据结构设计、日志匹配逻辑(Strict Match)以及模版更新机制。
核心设计思路
1. 节点设计 (TreeNode):
◦ 每个节点代表日志中的一个 Token(单词)。
◦ 节点分为两种路径:
静态路径(精确匹配单词)和
通配符路径(匹配动态参数,论文中用
<*> 表示)。
◦ 非根节点(不仅是叶子节点)都可以指向一个或多个 LogCluster(日志簇),因为不同长度的日志可能共享前缀。
2. 工作流程:
◦
插入 (Update):当 LLM 提取出新模板(例如
User <*> login)后,将模板分词并插入树中。
◦
查询 (Search):新日志进来后,逐词遍历树。如果能完全走通某条路径(静态词匹配或对应位置有
<*> 节点),则判定为
Strict Match(严格匹配)。
--------------------------------------------------------------------------------
1. 定义基础类:LogCluster 和 TreeNode
import java.util.*;// [6] Log Cluster: 存储日志模板信息的容器class LogCluster { String template; // LLM 提取出的模板,例如 "Connection from <*>"
String clusterId; public LogCluster(String template, String clusterId) { this.template = template; this.clusterId = clusterId;
} @Override
public String toString() { return "Cluster[" + clusterId + ": " + template + "]";
}
}// [4] Prefix Parse Tree Nodeclass TreeNode { // 静态Token的子节点映射 (例如 "Connection" -> nextNode)
Map<String, TreeNode> children = new HashMap<>();
// [4] 通配符节点:用于处理模板中的 <*>,匹配任意动态参数
TreeNode wildcardChild = null;
// [4] 该节点可能关联的日志簇(非叶子节点也可能有簇)
List<LogCluster> clusters = new ArrayList<>(); // 获取或创建普通子节点
public TreeNode getOrCreateChild(String token) { return children.computeIfAbsent(token, k -> new TreeNode());
} // 获取或创建通配符子节点
public TreeNode getOrCreateWildcardChild() { if (wildcardChild == null) {
wildcardChild = new TreeNode();
} return wildcardChild;
}
}
2. 实现前缀树逻辑:LogPrefixTree
public class LogPrefixTree { private final TreeNode root; private static final String WILDCARD = "<*>"; // [4] 论文定义的通配符
public LogPrefixTree() { this.root = new TreeNode();
} /**
* [7] 更新树 (Update Tree)
* 当 LLM 返回一个新的模板时,将其解析并存入树中
* @param template 例如 "User <*> logged in"
* @param cluster 对应的日志簇对象
*/
public void insertTemplate(String template, LogCluster cluster) { // [8] 简单的分词,论文中使用空格分割
String[] tokens = template.trim().split("\\s+"); TreeNode current = root; for (String token : tokens) { if (token.equals(WILDCARD)) { // 如果模板当前位置是 <*>, 走通配符路径
current = current.getOrCreateWildcardChild();
} else { // 否则走精确匹配路径
current = current.getOrCreateChild(token);
}
} // 在路径末尾绑定簇信息
current.clusters.add(cluster);
} /**
* [5, 9] 匹配搜索 (Cluster Matching)
* 尝试为新日志寻找 "Strict Match" (严格匹配)
* @param logMsg 原始日志消息
* @return 匹配到的 LogCluster,如果没有严格匹配则返回 null
*/
public LogCluster search(String logMsg) { String[] tokens = logMsg.trim().split("\\s+"); return searchRecursive(root, tokens, 0);
} // 递归搜索以处理可能的路径分支
private LogCluster searchRecursive(gszq.top2001.cn TreeNode node, String[] tokens, int index) { // 1. 终止条件:日志所有 Token 处理完毕
if (index == tokens.length) { // 检查当前节点是否有关联的簇
if (!node.clusters.isEmpty()) { // [5] 严格匹配要求 Token 数量一致。
// 简单实现中,如果路径走完且节点有簇,通常即为匹配。
// 实际可能需要校验 Cluster 里的模板长度。
return node.clusters.get(0);
} return null;
} String currentLogToken = tokens[index]; LogCluster result = null; // 2. 优先尝试:精确匹配 (Static Match)
// [5] 检查是否存在与当前日志 Token 完全一致的路径
if (node.children.containsKey(currentLogToken)) {
result = searchRecursive(node.children.get(currentLogToken), tokens, index + 1); if (result != null) return result;
} // 3. 备选尝试:通配符匹配 (Wildcard Match)
// [5] 如果精确匹配失败,检查当前是否有 <*> 路径
// 逻辑:模板里的 <*> 可以匹配日志里的任何 Token (如 IP, ID 等)
if (node.wildcardChild != null) {
result = searchRecursive(node.wildcardChild, tokens, index + 1); if (result != null) return result;
} // 4. 无法匹配 (No Match)
return null;
}
}
3. 模拟运行流程
以下代码展示了系统如何利用前缀树处理日志,并在匹配失败时模拟调用 LLM 的过程。
public class LogParserLLMDemo { public static void main(String[] args) { LogPrefixTree parser = new LogPrefixTree(); // 模拟场景:
// 1. 系统刚启动,树是空的
String log1 = "User 1001 logged in";
System.out.println("Processing: " + log1);
LogCluster match1 = parser.search(log1); if (match1 == null) {
System.out.println("-> No Match. Calling LLM..."); // [10] 模拟 LLM 提取模板
String llmGeneratedTemplate = "User <*> logged in";
LogCluster newCluster = new LogCluster(llmGeneratedTemplate, "C1");
// [7] 更新树
parser.insertTemplate(llmGeneratedTemplate, newCluster);
System.out.println("-> Tree Updated with: " + llmGeneratedTemplate);
} // 2. 相同的模式,但参数不同 (Strict Match)
String log2 = "User 9999 logged in";
System.out.println("bdzh.top2001.cn" + log2);
LogCluster match2 = parser.search(log2); if (match2 != null) { // [5] 严格匹配成功,无需调用 LLM
System.out.println("-> Strict Match Found! " + match2);
} else {
System.out.println("-> No Match.");
}
// 3. 全新的日志模式
String log3 = "Connection failed at 10.0.0.1";
System.out.println("\nProcessing: " + log3); LogCluster match3 = parser.search(log3); // 应该返回 null
if (match3 == null) {
System.out.println("-> No Match. Calling LLM..."); // 模拟流程继续...
}
}
}
1. Strict Match (严格匹配) 的判定:
◦ 在
searchRecursive 方法中,代码逻辑优先寻找
children (精确匹配)。
◦ 如果找不到精确匹配,但存在
wildcardChild (即模板中该位置是
<*>),则代码会进入通配符分支。
◦ 只有当日志的 Token 序列能够完美对应树上的一条路径(静态词对应静态节点,动态参数对应通配符节点),且长度一致(走完路径到达存有 Cluster 的节点)时,才返回结果。这对应了论文中 无需调用 LLM 的场景。
2. Loose Match (宽松匹配) 的处理:
◦ 上述代码实现了最核心的树结构。论文提到的“宽松匹配”(Loose Match)是指当严格匹配失败时,系统会尝试寻找“部分匹配”的簇(例如长度相同但某些静态词不匹配)。
◦ 在实际工程中,如果
search 返回
null,你可以遍历所有 Cluster 或基于 Token 长度进行二次筛选(Candidate Selection),如果发现相似度够高,再交给 LLM 决定是否 Merge(合并)。
3. 分词策略:
◦ LogParser-LLM 明确指出使用简单的空格分词(
split("\\s+")),不进行复杂的正则预处理(如把 IP 替换成
*),保留原始信息给 LLM 理解。前缀树也遵循这一原则。
线程安全和
迭代搜索,这里提供一个针对
search 方法的优化思路
// 使用 ConcurrentHashMap 保证并发读写的安全性 (基础保障)
import java.util.concurrent.ConcurrentHashMap;
class TreeNode {
// 替换为线程安全的 Map
Map<String, TreeNode> children = wrdmc.top2001.cn new ConcurrentHashMap<>();
volatile TreeNode wildcardChild = null; // 保证可见性
// ...
}
// 迭代式搜索 (防止栈溢出,稍微复杂但更健壮)
public LogCluster searchIterative(String logMsg) {
String[] tokens = logMsg.trim().split("\\s+");
// 使用栈来保存回溯点:存储 (当前节点, 当前Token索引)
Deque<Pair<TreeNode, Integer>> stack = new ArrayDeque<>();
stack.push(new Pair<>(root, 0));
while (!stack.isEmpty()) {
Pair<TreeNode, Integer> current = stack.pop();
TreeNode node = forxwt.top2001.cn current.getKey();
int index = current.getValue();
// 1. 匹配完成
if (index == tokens.length) {
if (!node.clusters.isEmpty()) return node.clusters.get(0);
continue; // 路径走通但没有簇(可能是中间节点),回溯
}
String token = tokens[index];
// 2. 压入通配符路径 (作为后备选项,先入栈后出)
// 注意:栈是后进先出,所以如果我们希望“静态优先”,应该先压入通配符,再压入静态
if (node.wildcardChild != null) {
stack.push(jtdk.top2001.cn new Pair<>(node.wildcardChild, index + 1));
}
// 3. 压入静态路径 (优先处理)
if (node.children.containsKey(token)) {
stack.push(new Pair<>(node.children.get(token), index + 1));
}
}
return null;
}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048949/viewspace-3109710/ ,如需转载,请注明出处,否则将追究法律责任。