本文目录导读:

- 通用核心原则
- 后端(Java/Python/Go)实现方案
- 前端(JavaScript/TypeScript)实现方案
- 移动端(iOS/Android)实现方案
- 高级留存策略:快照(Snapshot)技术
- 最佳实践总结
实现“留存异常上下文信息”的核心目标是:当程序发生异常(如崩溃、错误、超时)时,能够捕获并保存导致异常发生时的环境状态、数据快照、调用链路等关键信息,以便事后排查根因。
根据不同的技术场景(后端、前端、移动端、嵌入式),实现方式差异较大,以下是几种主流场景的详细实现方案:
通用核心原则
无论哪种语言或平台,留存上下文通常包含以下3类信息:
- 静态上下文:版本号、环境(生产/测试)、用户ID、请求ID、设备信息。
- 动态上下文:当前线程堆栈、调用链(TraceID/SpanID)、函数入参、关键变量值。
- 资源上下文:内存使用率、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.LoggerAdapter 或 structlog。
实现步骤:
-
自定义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:使用
jcmd或JMX在监控到内存阈值时自动 dump 堆(-XX:+HeapDumpOnOutOfMemoryError)。 - 数据库:在事务失败时,留存该事务的 Undo Log 或 Redo Log 快照。
- 分布式系统:使用 OpenTelemetry 记录 Span Events,在异常Span上追加“异常上下文属性”。
最佳实践总结
- 自动注入 > 手动注入:使用中间件/切面自动收集(URL、用户、请求ID),避免业务代码手动传递。
- 敏感信息脱敏:永远不要记录密码、Token、银行卡号,使用
replace("password", "***")过滤。 - 限制上下文大小:入参超过1KB可截断;堆栈超过20层可压缩。
- 异步留存:使用 消息队列 或
sendBeacon/async writer将上下文发送到日志系统,不要阻塞主业务线程。 - 结构化存储:使用 JSON 或 Protobuf 格式存储,方便后续搜索
traceId或userId。
如果你有具体的语言或框架(如 Spring Boot, Express, Flutter),我可以提供更针对性的代码示例。