如何编写精简异常堆栈信息脚本

wen 实用脚本 28

本文目录导读:

如何编写精简异常堆栈信息脚本

  1. Shell脚本 - 保留关键堆栈行
  2. Python脚本 - 智能堆栈合并
  3. Java工具 - 运行时精简
  4. Node.js/JavaScript 版本
  5. 使用建议

Shell脚本 - 保留关键堆栈行

#!/bin/bash
# 文件名: stack_filter.sh
# 过滤堆栈,保留特定包路径的行
filter_stack() {
    local input_file="${1:-/dev/stdin}"
    local packages="${2:-com.example,org.springframework}"
    # 将包名转为grep模式
    local pattern=$(echo "$packages" | tr ',' '|')
    awk -v pattern="$pattern" '
    BEGIN {
        # 分割设置
        FS="\t"
        show_line = 0
    }
    # 保留异常类型和消息
    /^[a-zA-Z]+\.[a-zA-Z]+.*Exception/ {
        print $0
        show_line = 1
        next
    }
    # 保留匹配的关键包堆栈
    $0 ~ pattern && /at / {
        if (show_line) {
            print $0
            show_line = 0
        } else {
            print $0
        }
        next
    }
    # 保留"Caused by"行
    /^Caused by:/ {
        print $0
        show_line = 1
        next
    }
    # 保留堆栈结束标记
    /^\.\.\. [0-9]+ more/ {
        print $0
        show_line = 0
    }
    ' "$input_file"
}
# 使用示例
cat << 'EOF' | filter_stack "" "com.example"
java.lang.NullPointerException
    at com.example.service.UserService.getUser(UserService.java:25)
    at com.example.controller.UserController.findById(UserController.java:12)
    at com.example.config.AopConfig.invoke(AopConfig.java:45)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
    ... 23 more
EOF

Python脚本 - 智能堆栈合并

#!/usr/bin/env python3
# 文件名: stack_minifier.py
import sys
import re
from collections import defaultdict
class StackMinifier:
    def __init__(self, packages_to_keep=None):
        """
        packages_to_keep: 需要保留的包路径列表,如 ['com.example', 'org.springframework']
        """
        self.packages_to_keep = packages_to_keep or ['com.example', 'org.springframework']
        self.patterns = [re.compile(f'^{pkg}') for pkg in self.packages_to_keep]
    def process_stacktrace(self, text):
        """处理堆栈信息"""
        lines = text.split('\n')
        result = []
        i = 0
        while i < len(lines):
            line = lines[i]
            # 保留异常信息行
            if re.match(r'^[a-zA-Z]+\.[a-zA-Z]+.*Exception', line):
                result.append(line)
                i += 1
                continue
            # 处理at行
            if 'at ' in line:
                frame_info = self._extract_frame_info(line)
                if self._should_keep(frame_info['class']):
                    result.append(line)
                    # 检查是否有连续的堆栈行
                    j = i + 1
                    skipped = 0
                    while j < len(lines) and 'at ' in lines[j]:
                        next_frame = self._extract_frame_info(lines[j])
                        if self._should_keep(next_frame['class']):
                            result.append(lines[j])
                        else:
                            skipped += 1
                        j += 1
                    if skipped > 0:
                        result.append(f"    ... {skipped} more [filtered]")
                    i = j
                    continue
            # 保留Caused by行
            elif line.startswith('Caused by:'):
                result.append(line)
            i += 1
        return '\n'.join(result)
    def _extract_frame_info(self, line):
        """提取堆栈帧信息"""
        # 匹配格式: at com.example.Class.method(File.java:line)
        match = re.search(r'at\s+([\w.]+)\.(\w+)\(([^:]+):(\d+)\)', line)
        if match:
            return {
                'full_class': match.group(1),
                'class': match.group(1).rsplit('.', 1)[0] if '.' in match.group(1) else '',
                'method': match.group(2),
                'file': match.group(3),
                'line': match.group(4)
            }
        return {'full_class': '', 'class': '', 'method': '', 'file': '', 'line': ''}
    def _should_keep(self, class_name):
        """判断是否应该保留该堆栈帧"""
        for pattern in self.patterns:
            if pattern.search(class_name):
                return True
        return False
def main():
    # 从命令行参数读取需要保留的包
    import argparse
    parser = argparse.ArgumentParser(description='精简Java异常堆栈信息')
    parser.add_argument('file', nargs='?', help='输入文件路径,默认从标准输入读取')
    parser.add_argument('--packages', '-p', 
                       default='com.example,org.springframework',
                       help='需要保留的包前缀,逗号分隔')
    args = parser.parse_args()
    # 读取输入
    if args.file:
        with open(args.file, 'r') as f:
            input_text = f.read()
    else:
        input_text = sys.stdin.read()
    # 处理堆栈
    packages = args.packages.split(',')
    minifier = StackMinifier(packages)
    output = minifier.process_stacktrace(input_text)
    print(output)
if __name__ == '__main__':
    main()

Java工具 - 运行时精简

import java.util.*;
import java.util.stream.*;
import java.util.function.Predicate;
public class StackTraceSimplifier {
    private final Set<String> packagesToKeep;
    public StackTraceSimplifier(String... packages) {
        this.packagesToKeep = new HashSet<>(Arrays.asList(packages));
    }
    /**
     * 简化异常堆栈
     */
    public String simplify(Throwable throwable) {
        StringBuilder sb = new StringBuilder();
        simplifyThrowable(throwable, sb, 0);
        return sb.toString();
    }
    private void simplifyThrowable(Throwable throwable, StringBuilder sb, int depth) {
        if (throwable == null || depth > 3) { // 限制chained exception深度
            return;
        }
        // 添加异常信息
        sb.append(throwable.getClass().getName())
          .append(": ")
          .append(throwable.getMessage())
          .append("\n");
        // 过滤堆栈
        List<StackTraceElement> filteredStack = Arrays.stream(throwable.getStackTrace())
            .filter(this::shouldKeep)
            .collect(Collectors.toList());
        // 获取被过滤的数量
        long filteredCount = throwable.getStackTrace().length - filteredStack.size();
        // 输出过滤后的堆栈
        for (StackTraceElement element : filteredStack) {
            sb.append("\tat ")
              .append(element.getClassName())
              .append(".")
              .append(element.getMethodName())
              .append("(")
              .append(element.getFileName())
              .append(":")
              .append(element.getLineNumber())
              .append(")\n");
        }
        if (filteredCount > 0) {
            sb.append("\t... ").append(filteredCount).append(" more [filtered]\n");
        }
        // 处理cause
        if (throwable.getCause() != null) {
            sb.append("Caused by: ");
            simplifyThrowable(throwable.getCause(), sb, depth + 1);
        }
    }
    private boolean shouldKeep(StackTraceElement element) {
        String className = element.getClassName();
        return packagesToKeep.stream().anyMatch(className::startsWith);
    }
    // 使用示例
    public static void main(String[] args) {
        try {
            // 模拟异常
            throw new RuntimeException("Test exception", 
                new IllegalArgumentException("Root cause"));
        } catch (Exception e) {
            StackTraceSimplifier simplifier = 
                new StackTraceSimplifier("com.example", "org.springframework");
            System.out.println(simplifier.simplify(e));
        }
    }
}

Node.js/JavaScript 版本

#!/usr/bin/env node
// 文件名: stack-minifier.js
class StackMinifier {
  constructor(options = {}) {
    this.modulesToKeep = options.modules || ['app/', 'src/'];
    this.maxDepth = options.maxDepth || 20;
  }
  /**
   * 精简Node.js异常堆栈
   */
  simplifyError(error) {
    if (!error || !error.stack) return '';
    const lines = error.stack.split('\n');
    const result = [];
    // 保留错误消息行
    result.push(lines[0]);
    // 过滤堆栈行
    let currentDepth = 0;
    let skippedCount = 0;
    for (let i = 1; i < lines.length; i++) {
      const line = lines[i].trim();
      if (currentDepth >= this.maxDepth) {
        skippedCount++;
        continue;
      }
      // 检查是否是内部模块
      if (this.shouldKeep(line)) {
        result.push('    ' + line);
        currentDepth++;
      } else {
        skippedCount++;
      }
    }
    if (skippedCount > 0) {
      result.push(`    ... ${skippedCount} frames were omitted`);
    }
    return result.join('\n');
  }
  shouldKeep(stackLine) {
    // 过滤node_modules和原生模块
    if (stackLine.includes('node_modules/')) return false;
    if (stackLine.includes('(native)')) return false;
    if (stackLine.includes('internal/')) return false;
    // 保留指定模块
    return this.modulesToKeep.some(module => stackLine.includes(module));
  }
}
// 命令行工具
const fs = require('fs');
if (process.argv.length > 2) {
  const inputFile = process.argv[2];
  const content = fs.readFileSync(inputFile, 'utf-8');
  const minifier = new StackMinifier({ 
    modules: process.argv[3]?.split(',') || ['app/', 'src/']
  });
  // 假设内容中可能包含多个异常堆栈
  const stacks = content.split(/(?=\w+Error:)/);
  stacks.forEach(stack => {
    if (stack.trim()) {
      console.log(minifier.simplifyError({ stack: stack.trim() }));
    }
  });
} else {
  // 从stdin读取
  const chunks = [];
  process.stdin.on('data', chunk => chunks.push(chunk));
  process.stdin.on('end', () => {
    const content = Buffer.concat(chunks).toString();
    const minifier = new StackMinifier();
    console.log(minifier.simplifyError({ stack: content }));
  });
}

使用建议

  1. 生产环境日志处理:结合日志收集系统使用
  2. 开发调试:保留完整堆栈,仅在输出时精简
  3. 监控告警:只保留关键业务包(如com.example)的堆栈
  4. 集成到构建工具:作为post-processing步骤

这些脚本可以根据你的具体需求调整过滤规则和输出格式。

抱歉,评论功能暂时关闭!