怎样实现留存异常上下文信息内容

wen 实用脚本 25

本文目录导读:

怎样实现留存异常上下文信息内容

  1. 通用核心原则
  2. 后端(Java/Python/Go)实现方案
  3. 前端(JavaScript/TypeScript)实现方案
  4. 移动端(iOS/Android)实现方案
  5. 高级留存策略:快照(Snapshot)技术
  6. 最佳实践总结

实现“留存异常上下文信息”的核心目标是:当程序发生异常(如崩溃、错误、超时)时,能够捕获并保存导致异常发生时的环境状态、数据快照、调用链路等关键信息,以便事后排查根因。

根据不同的技术场景(后端、前端、移动端、嵌入式),实现方式差异较大,以下是几种主流场景的详细实现方案:

通用核心原则

无论哪种语言或平台,留存上下文通常包含以下3类信息:

  1. 静态上下文:版本号、环境(生产/测试)、用户ID、请求ID、设备信息。
  2. 动态上下文:当前线程堆栈、调用链(TraceID/SpanID)、函数入参、关键变量值。
  3. 资源上下文:内存使用率、CPU负载、数据库连接池状态、最近一次网络请求的URL和耗时。

后端(Java/Python/Go)实现方案

Java(最常用:日志框架 + MDC + 异常拦截器)

核心工具SLF4J + Logback/Log4j2 + ThreadLocal(通过MDC实现)。

实现步骤

  • 在入口处(Filter/Interceptor)注入上下文

    import org.slf4j.MDC;
    public class LoggingFilter implements Filter {
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
            try {
                // 1. 生成TraceID(全链路唯一)
                String traceId = UUID.randomUUID().toString();
                MDC.put("traceId", traceId);
                MDC.put("userId", extractUserId(request));
                MDC.put("requestURI", ((HttpServletRequest) request).getRequestURI());
                // 2. 执行业务逻辑
                chain.doFilter(request, response);
            } finally {
                // 3. 清除MDC(防止内存泄漏)
                MDC.clear();
            }
        }
    }
  • 在异常拦截器中捕获详细数据

    @ControllerAdvice
    public class GlobalExceptionHandler {
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public ErrorResponse handleException(Exception e, HttpServletRequest request) {
            // 1. 提取上下文
            Map<String, String> context = new HashMap<>();
            context.put("threadName", Thread.currentThread().getName());
            context.put("stackTrace", ExceptionUtils.getStackTrace(e)); // 完整堆栈
            context.put("method", request.getMethod());
            context.put("queryString", request.getQueryString());
            // 2. 如果是业务异常,获取请求体(注意只消费一次,需缓存)
            // context.put("body", getCachedBody(request));
            // 3. 留存:写入专门的错误日志文件
            log.error("Business Exception with context: {}", JSON.toJSONString(context), e);
            return new ErrorResponse(e.getMessage());
        }
    }
  • 日志配置(logback.xml)

    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/error-context.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

Python(使用 logging + 自定义适配器 + Sentry)

核心工具logging.LoggerAdapterstructlog

实现步骤

  • 自定义Adapter传递上下文

    import logging
    from logging import LoggerAdapter
    class ContextAdapter(LoggerAdapter):
        def process(self, msg, kwargs):
            # 将extra参数自动附加到日志
            extra = self.extra.copy()
            if 'extra' in kwargs:
                extra.update(kwargs['extra'])
            kwargs['extra'] = extra
            return msg, kwargs
    # 初始化
    logger = ContextAdapter(logging.getLogger(__name__), 
                           {'trace_id': trace_id, 'user': user_id})
    logger.error("数据库查询失败", extra={'sql': sql_query, 'params': params})
  • 使用Sentry(推荐)

    import sentry_sdk
    from sentry_sdk import configure_scope
    with configure_scope() as scope:
        scope.set_user({"id": user_id, "email": email})
        scope.set_tag("service", "payment-api")
        scope.set_context("request", {
            "method": request.method,
            "url": request.url,
            "body": request.json  # 小心敏感数据脱敏
        })
    # 触发异常时自动携带上述上下文

Go(使用 context.Context + 自定义中间件)

核心思想:将上下文信息通过 context.Context 传递。

package main
import (
    "context"
    "log"
    "net/http"
    "runtime/debug"
)
type ContextKey string
const TraceIDKey ContextKey = "trace_id"
func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 1. 注入上下文
        ctx := context.WithValue(r.Context(), TraceIDKey, r.Header.Get("X-Trace-ID"))
        ctx = context.WithValue(ctx, "user_id", extractUser(r))
        // 2. 异常恢复中间件(捕获panic)
        defer func() {
            if rec := recover(); rec != nil {
                // 3. 提取堆栈
                stack := debug.Stack()
                // 4. 留存上下文
                log.Printf("[CRASH] TraceID: %v, UserID: %v, Error: %v, Stack: %s",
                    ctx.Value(TraceIDKey), ctx.Value("user_id"), rec, stack)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

前端(JavaScript/TypeScript)实现方案

全局Error Handler + 自定义上下文

// 1. 定义一个全局上下文对象(实时更新)
const globalContext = {
  userId: null,
  pageUrl: window.location.href,
  userAgent: navigator.userAgent,
  lastApiCall: null,
};
// 2. 重写XMLHttpRequest或fetch,捕获请求上下文
const originalFetch = window.fetch;
window.fetch = async (...args) => {
  globalContext.lastApiCall = args[0];
  try {
    return await originalFetch(...args);
  } catch (error) {
    // 这里会触发全局错误
    throw error;
  }
};
// 3. 全局异常捕获
window.onerror = function(message, source, lineno, colno, error) {
  const errorContext = {
    ...globalContext,
    timestamp: new Date().toISOString(),
    errorMessage: message,
    stackTrace: error?.stack,
    componentStack: error?.componentStack // React等框架提供
  };
  // 4. 发送到后端日志服务
  navigator.sendBeacon('/api/log/error', JSON.stringify(errorContext));
  return true; // 阻止默认浏览器错误提示
};

React Error Boundary(组件级)

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    // 留存组件栈
    const context = {
      error,
      componentStack: errorInfo.componentStack,
      reduxState: store.getState(), // 如果用了Redux
      props: this.props
    };
    logToServer(context);
  }
  render() {
    return this.props.children;
  }
}

移动端(iOS/Android)实现方案

iOS (Swift) - NSSetUncaughtExceptionHandler + 自定义上下文

// 1. 设置未捕获异常处理器
func setupExceptionHandler() {
    NSSetUncaughtExceptionHandler { exception in
        let context: [String: Any] = [
            "name": exception.name.rawValue,
            "reason": exception.reason ?? "",
            "callStackSymbols": exception.callStackSymbols,
            "userInfo": exception.userInfo ?? [:],
            "currentViewController": getTopViewController()?.description ?? "unknown",
            "threadInfo": Thread.current.description
        ]
        // 写入本地文件
        saveCrashReport(context)
    }
}
// 2. 捕获信号量崩溃(如EXC_BAD_ACCESS)
signal(SIGABRT) { _ in /* 处理 */ }

Android (Java/Kotlin) - Thread.setDefaultUncaughtExceptionHandler

class CustomExceptionHandler : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(t: Thread, e: Throwable) {
        val context = mutableMapOf<String, Any>()
        context["thread_name"] = t.name
        context["stack_trace"] = Log.getStackTraceString(e)
        context["activity"] = currentActivity?.localClassName
        context["time"] = System.currentTimeMillis()
        context["memory"] = getMemoryInfo()
        // 保存到SharedPreferences或本地文件
        saveToFile(context)
        // 交给系统默认处理器(保证应用能退出)
        defaultHandler?.uncaughtException(t, e)
    }
}
// 在Application中设置
Thread.setDefaultUncaughtExceptionHandler(CustomExceptionHandler())

高级留存策略:快照(Snapshot)技术

对于非常复杂的异常(如内存溢出前的状态),可以在异常发生前做增量快照

  • Java:使用 jcmdJMX 在监控到内存阈值时自动 dump 堆(-XX:+HeapDumpOnOutOfMemoryError)。
  • 数据库:在事务失败时,留存该事务的 Undo LogRedo Log 快照。
  • 分布式系统:使用 OpenTelemetry 记录 Span Events,在异常Span上追加“异常上下文属性”。

最佳实践总结

  1. 自动注入 > 手动注入:使用中间件/切面自动收集(URL、用户、请求ID),避免业务代码手动传递。
  2. 敏感信息脱敏永远不要记录密码、Token、银行卡号,使用 replace("password", "***") 过滤。
  3. 限制上下文大小:入参超过1KB可截断;堆栈超过20层可压缩。
  4. 异步留存:使用 消息队列sendBeacon/async writer 将上下文发送到日志系统,不要阻塞主业务线程。
  5. 结构化存储:使用 JSON 或 Protobuf 格式存储,方便后续搜索 traceIduserId

如果你有具体的语言或框架(如 Spring Boot, Express, Flutter),我可以提供更针对性的代码示例。

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