Spring Boot应用中使用AOP实现追踪和日志记录的方法
在现代应用程序中,特别是那些使用微服务构建的应用程序中,追踪和日志在追踪请求通过各个服务时起着至关重要的作用。追踪帮助开发人员排查问题,监控性能表现,并了解用户在多个系统间的流转。
在这篇博客里,我们将一步步讲解如何在 Spring Boot 应用程序中实现通过前端生成的 traceId
进行跟踪和日志记录,并探讨在微服务架构中分布式跟踪的方案。
跟踪是指为一个请求分配一个唯一的标识符(通常称为 traceId
),并确保该标识符随着请求在应用程序的不同层级或服务之间传递过程。这对于理解请求的流程非常关键,特别是在调试错误或优化性能瓶颈时。
traceId
(追踪ID):
在微服务架构中,请求通常会经过多个服务和组件。这样做有几个好处:在前端生成 traceId
具有以下几个优点:
- 端到端可见性:
traceId
从用户的交互开始,使得追踪请求从前端到后端服务变得容易。这确保了用户在整个系统中的动作具有完整的可见性。 - 一致的追踪:通过在前端生成
traceId
并将其包含在每个请求中,相同traceId
将传播到所有服务。这确保在多个 API 调用和服务之间保持一致的追踪。 - 会话级追踪:当用户会话开始时,前端可以生成一个
traceId
并在该会话的所有请求中使用它。这使得开发者可以追踪用户在会话中进行的所有操作。
traceId
我们先从在React前端生成一个traceId
做起,并随每个API请求一起发送。
traceId
:
注:traceId
为技术术语。
我们将使用 uuid
库来为每个会话生成唯一标识符。
npm install uuid
这个命令是用来安装uuid包的。
然后,创建一个工具函数来生成或取回 traceId
:
import { v4 as uuidv4 } from 'uuid';
function getOrCreateTraceId() {
let traceId = localStorage.getItem('traceId');
if (!traceId) {
traceId = uuidv4(); // 生成一个新的 UUID 作为新的 traceId
localStorage.setItem('traceId', traceId); // 以便后续请求使用
}
return traceId;
}
在 Axios 拦截器中添加 traceId
为了确保每个API请求都自动包含追踪ID (traceId
),我们更新了Axios拦截器。
const axios = require('axios');
import { getOrCreateTraceId } from './traceIdUtil';
const instanceUrl = axios.create({
baseURL: 'http://localhost:8080/',
transformRequest: [
function (data, headers) {
let jwt = localStorage.getItem('jwt');
if (jwt) {
headers.Authorization = 'Bearer ' + jwt;
}
// 在每个请求头中加入 traceId
headers['X-Trace-Id'] = getOrCreateTraceId();
return JSON.stringify(data);
}
],
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Pragma: 'no-cache'
}
});
现在,每次从 React 前端发出的每个请求都将在 X-Trace-Id
头部包含 traceId
。
traceId
在 Spring Boot 中
在 Spring Boot 后端,我们需要捕获这个 traceId
值,并确保在日志记录中一致使用它。在整个应用的生命周期里,我们利用 Spring 的 Mapped Diagnostic Context (MDC)
来存储这两个标识符:traceId
和 userId
。
我们将使用 Spring Boot 中的一个过滤器来拦截并处理每一个传入的请求,从请求头中提取 traceId
并将其存入 MDC,以便后续使用。
import org.slf4j.MDC;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceAndUserFilter extends HttpFilter {
private static final String TRACE_ID = "traceId";
private static final String USER_ID = "userId";
private static final String HEADER_TRACE_ID = "X-Trace-Id";
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 从请求头中获取 traceId,如果不存在则生成新的
String traceId = request.getHeader(HEADER_TRACE_ID);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}
MDC.put(TRACE_ID, traceId);
// 从安全上下文中提取 userId
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String userId = (authentication != null && authentication.isAuthenticated()) ? authentication.getName() : "ANONYMOUS";
MDC.put(USER_ID, userId);
try {
chain.doFilter(request, response); // 继续过滤器链中的下一个
} finally {
// 请求处理完毕后,从 MDC 移除 traceId 和 userId
MDC.remove(TRACE_ID);
MDC.remove(USER_ID);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
}
@Override
public void destroy() {
super.destroy();
}
}
步骤三:使用 AOP(面向切面编程)记录日志
有了 traceId
和 userId
放在 MDC 中,我们可以使用 Spring AOP 来记录方法的入口和出口。这样我们就能在调用方法时自动记录日志,同时不会影响业务逻辑。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class LoggingAspect {
private static final String TRACE_ID = "traceId";
private static final String USER_ID = "userId";
@Around("execution(* com.example.application..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String traceId = MDC.get(TRACE_ID);
String userId = MDC.get(USER_ID);
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
// 记录进入函数
log.info("TraceId: {}, UserId: {}, 类: {}, 进入函数 {} 方法参数 {}", traceId, userId, className, methodName, joinPoint.getArgs());
Object result;
try {
result = joinPoint.proceed(); // 继续执行过程
} catch (Throwable throwable) {
log.error("TraceId: {}, UserId: {}, 类: {}, 函数 {} 发生异常: {}", traceId, userId, className, methodName, throwable.getMessage());
throw throwable;
}
// 记录退出函数
if (result != null) {
log.info("TraceId: {}, UserId: {}, 类: {}, 退出函数 {} 返回值 {}", traceId, userId, className, methodName, result);
} else {
log.info("TraceId: {}, UserId: {}, 类: {}, 退出函数 {} 无返回值", traceId, userId, className, methodName);
}
return result;
}
}
第4步:将日志配置为包含跟踪ID (traceId
) 和用户ID (userId
)
配置您的日志系统以在所有日志条目中包含 traceId
和 userId
。例如,您可以在 Logback 的 logback.xml
文件中添加这些字段。
<配置>
<追加器 name="STDOUT" 类名="ch.qos.logback.core.ConsoleAppender">
<编码器>
<模式>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5日志级别 %logger{36} - %msg [traceId=%X{traceId}] [userId=%X{userId}]%n</模式>
</编码器>
</追加器>
<根>
<追加器引用 ref="STDOUT" />
</根>
</配置>
步骤五:分布式跟踪选项:
发送 traceId
从前端发送有助于追踪用户在系统中的旅程,但在更复杂的微服务架构下,通常需要分布式追踪系统才能全面了解跨多个服务的情况。
以下是在微服务环境中可用的几个分布式跟踪选项:
- Jaeger:由 Uber 开发的一个开源分布式追踪工具,很好地与 Spring Boot 集成,帮助你更直观地看到请求在不同服务间的传播过程。
Jaeger 允许你监控服务的延迟和性能,并提供服务交互的详细视图。 - Zipkin:一个流行的开源追踪系统,帮助收集解决微服务架构中延迟问题所需的时间数据。Zipkin 捕获追踪信息并帮助可视化服务间的依赖关系。
Zipkin 易于设置,广泛应用于分布式系统的性能监控。 - OpenTelemetry:一个高度灵活的框架,用于收集和导出追踪数据。OpenTelemetry 支持多个后端(如 Jaeger、Zipkin、Prometheus 等),并且轻松地集成到 Spring Boot 中。
OpenTelemetry 正在成为分布式系统中追踪和指标收集的标准工具。
通过从前端发送traceId
并在后端通过Spring Boot和AOP进行传播,我们可以实现端到端可见性的有效跟踪。此外,像Jaeger、Zipkin和OpenTelemetry等工具可以帮助你追踪跨多个服务的请求,从而使微服务的调试和监控变得更为简单。
通过这样的设置,您可以跨整个系统中跟踪请求,不论是单个应用还是复杂的微服务网络,确保问题容易被追踪,性能瓶颈能够被迅速发现。
共同学习,写下你的评论
评论加载中...
作者其他优质文章