SpringBoot架构实战:拦截器+全局异常+日志追踪一体化解决方案
概述
本文介绍企业级Java项目中常用的日志链路追踪、统一异常处理、权限拦截等核心功能的实现方案。通过AOP切面、自定义拦截器、线程上下文管理等技术手段,实现以下核心功能:
- 全链路日志追踪
- 统一异常处理机制
- 接口权限验证
- 请求耗时监控
- 标准化日志格式
一、核心功能模块
1.1 全局日志切面(WebControllerAop)
- 实现原理:基于Spring AOP的环绕通知
- 核心功能:
- 请求参数/响应结果格式化
- 接口耗时计算(精确到秒)
- 请求事件名称映射
- 文件类型请求特殊处理
- 线程上下文初始化
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.general.constants.ReqUrlConstants;
import ****.infrastructure.general.util.LogUtil;
import ****.infrastructure.general.util.ThreadLocalUtil;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @author: guohong
*/
@Slf4j
@Aspect
@Component
public class WebControllerAop {
/**
* 指定切点
* 匹配 com.example.demo.controller包及其子包下的所有类的所有方法
*/
@Pointcut("execution(public * ****.apis.controller.*.*(..)) || execution(public * ****.apis.*.controller.*.*(..))")
public void webLog() {
}
/**
* 环绕通知,环绕增强,相当于MethodInterceptor
*
* @param joinPoint
* @return
*/
@Around("webLog()")
public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取目标方法的参数信息
Signature signature = joinPoint.getSignature();
String args;
if (ReqUrlConstants.FILE_URL_MAP.containsKey(signature.getDeclaringTypeName())) {
args = "【文件流类型】";
} else {
args = formatArgs(joinPoint);
}
//AOP代理类的类(class)信息
signature.getDeclaringType();
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
Object o = joinPoint.proceed();
//记录日志
log.info("接收请求-{} \n 请求ID:{} \n 请求地址: {} \n token: {} \n 入参 : {} \n 出参 : {} \n 请求时间:{} \n 响应时间:{} \n 耗时:{}秒",
"", LogUtil.idEnd(), "", "", args, formatRet(o), DateUtil.date(startTime), DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000d);
ThreadLocalUtil.remove();
return o;
}
HttpServletRequest req = attributes.getRequest();
String logId = req.getHeader(Constants.PROCESS_ID);
if (StringUtils.isBlank(logId)) {
LogUtil.createId();
} else {
ThreadLocalUtil.set(Constants.THREAD_NO, logId);
}
String token = req.getHeader(Constants.CAR_TOKEN);
String eventName = getEventName(req.getRequestURL().toString());
String reqName = "系统接口:" + eventName + " " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName() + "()";
String reqUrl = req.getMethod() + " " + req.getRequestURL().toString();
HashMap<String, String> map = Maps.newHashMap();
map.put(Constants.THREAD_REQ_NAME, reqName);
map.put(Constants.THREAD_REQ_URL, reqUrl);
map.put(Constants.THREAD_REQ_PARAM, args);
map.put(Constants.THREAD_REQ_TIME, startTime + "");
ThreadLocalUtil.set(Constants.THREAD_REQ_PARAM, map);
//执行目标方法
Object o = joinPoint.proceed();
//记录日志
log.info("接收请求:{} \n 请求ID:{} \n 请求地址: {} \n token: {} \n 入参 : {} \n 出参 : {} \n 请求时间:{} \n 响应时间:{} \n 耗时:{}秒",
eventName, LogUtil.idEnd(), reqUrl, token, args, formatRet(o), DateUtil.date(startTime), DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000d);
//移除所有线程map,防止线程复用导致的变量错乱及内存溢出
ThreadLocalUtil.remove();
return o;
}
private String formatArgs(JoinPoint joinPoint) {
String args = Arrays.toString(joinPoint.getArgs());
try {
StringBuilder argsBuild = new StringBuilder();
Object[] argsArray = joinPoint.getArgs();
if (argsArray.length >= 1) {
argsBuild.append(JSON.toJSONString(argsArray[0]));
}
if (StringUtils.length(argsBuild.toString()) > 0) {
return argsBuild.toString();
}
} catch (Exception ignored) {
return args;
}
return args;
}
private Object formatRet(Object ret) {
try {
String retJson = JSON.toJSONString(ret);
if (StringUtils.isNotBlank(retJson)) {
return retJson;
}
} catch (Exception ignored) {
return ret;
}
return ret;
}
/**
* 根据请求url 获取对应的业务名称
*
* @param requestURL
* @return
*/
private static String getEventName(String requestURL) {
if (StringUtils.isBlank(requestURL)) {
return "未知URL";
}
for (Map.Entry<String, String> entry : ReqUrlConstants.MAP.entrySet()) {
if (StringUtils.contains(requestURL, entry.getKey())) {
return entry.getValue();
}
}
return "未配置事件";
}
}
1.2 统一异常处理(GlobalExceptionHandler)
- 异常分类处理:
- 文件上传异常(MultipartException)
- 参数校验异常(MethodArgumentNotValidException)
- 业务异常(ApiException)
- 未捕获异常兜底处理
- java代码:
import cn.hutool.core.date.DateUtil;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.general.util.LogUtil;
import ****.infrastructure.general.util.ThreadLocalUtil;
import ****commons.base.Result;
import ****commons.exception.ApiException;
import ****commons.exception.ErrorResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException;
import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;
import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // 设置最高优先级
public class GlobalExceptionHandler {
@Value(value = "${spring.servlet.multipart.max-file-size}")
String singleMaxFileSize;
@Value(value = "${spring.servlet.multipart.max-request-size}")
String maxRequestSize;
/**
* 文件上传错误
* @param req
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value = MultipartException.class)
@ResponseStatus(HttpStatus.OK)
public Result<Object> MultipartExceptionHandler(HttpServletRequest req, MultipartException ex) {
if (ex.getCause().getCause() instanceof FileSizeLimitExceededException) {
return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "单个文件上传大小不能超过" + singleMaxFileSize, ex);
} else if (ex.getCause().getCause() instanceof SizeLimitExceededException) {
return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "请求的总上传文件大小不能超过" + maxRequestSize, ex);
} else {
return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "上传文件失败", ex);
}
}
/**
* 其他错误
*/
@ResponseBody
@ExceptionHandler({Exception.class})
@ResponseStatus(HttpStatus.OK)
public Result<Object> exception(HttpServletRequest req, Exception ex) {
return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "系统异常:" + ex.getMessage(), ex);
}
//运行时异常
@ResponseBody
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.OK)
public Result<Object> runtimeExceptionHandler(HttpServletRequest req, RuntimeException ex) {
return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "执行异常:" + ex.getMessage(), ex);
}
/**
* 业务异常
*/
@ResponseBody
@ExceptionHandler({ApiException.class})
@ResponseStatus(HttpStatus.OK)
public Result<Object> ApiExceptionHandler(ApiException e) {
log.error("Message: {}", e.getMessage(), e);
return formatResult(e.getResultCode().getErrorCode(), e.getResultCode().getError(), e);
}
@ResponseBody
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseStatus(HttpStatus.OK)
public Result<Object> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
log.error("Message: {}", e.getMessage(), e);
return formatResult(ErrorResultCode.PARAM_REQUIRED.getErrorCode(), String.format(ErrorResultCode.PARAM_REQUIRED.getError(), e.getParameterName()), e);
}
@ResponseBody
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public Result<Object> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("Message: {}", e.getMessage(), e);
return formatResult(ErrorResultCode.PARAM_REQUIRED.getErrorCode(), String.format(ErrorResultCode.PARAM_REQUIRED.getError(), this.getBindingResultErrors(e.getBindingResult())), e);
}
@ResponseBody
@ExceptionHandler({HttpMediaTypeNotSupportedException.class})
@ResponseStatus(HttpStatus.OK)
public Result<Object> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) {
log.error("Message: {}", e.getMessage(), e);
return formatResult(ErrorResultCode.REQUEST_TYPE_ERROR.getErrorCode(), ErrorResultCode.REQUEST_TYPE_ERROR.getError(), e);
}
private <T extends Throwable> Result<Object> formatResult(String errorCode, String errorMsg, T ex) {
Map<String, String> reqMap = ThreadLocalUtil.get(Constants.THREAD_REQ_PARAM);
long startTime = System.currentTimeMillis();
String reqName = "内部处理";
String url = "";
String param = "";
if (!CollectionUtils.isEmpty(reqMap)){
if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_TIME))) {
startTime = Long.parseLong(reqMap.get(Constants.THREAD_REQ_TIME));
}
if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_NAME))) {
reqName = reqMap.get(Constants.THREAD_REQ_NAME);
}
if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_URL))) {
url = reqMap.get(Constants.THREAD_REQ_URL);
}
if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_PARAM))) {
param = reqMap.get(Constants.THREAD_REQ_PARAM);
}
}
//获取异常信息
StringJoiner err = new StringJoiner("/n/r ");
StackTraceElement[] stackTrace = ex.getStackTrace();
if (null != stackTrace) {
for (int i = 0; i < (Math.min(stackTrace.length, 100)); i++) {
err.add(stackTrace[i].toString());
}
}
//记录日志
log.info("响应异常-{} \n 请求ID:{} \n 请求url: {} \n 入参 : {} \n 出参 : {} \n 请求时间:{} \n 响应时间: {} \n 耗时:{}秒",
reqName, LogUtil.idEnd(),url, param,
"异常代码:" + errorCode + " 异常信息:" + errorMsg + "-" + ex.getMessage()+"/n/r" + err,
startTime, DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000);
//移除所有线程map,防止线程复用导致的变量错乱及内存溢出
ThreadLocalUtil.remove();
return new Result(errorCode, errorMsg);
}
private String getBindingResultErrors(BindingResult bindingResult) {
StringBuilder sb = new StringBuilder();
if (bindingResult.hasErrors()) {
List<ObjectError> list = bindingResult.getAllErrors();
Iterator var4 = list.iterator();
while(var4.hasNext()) {
ObjectError error = (ObjectError)var4.next();
sb.append(error.getDefaultMessage() + ",");
}
}
return sb.toString();
}
}
- 统一响应格式:
{"code":"ERROR_001","msg":"参数校验失败"}
1.3 权限拦截器(RequestInterceptor)
- 安全校验流程:
- Token有效性验证
- Redis缓存用户信息
- 权限服务远程调用
- 用户上下文传递
- 支持功能:
- 外部用户权限标识
- Token自动续期(60天)
- 用户信息线程级缓存
- **配置类:
import cn.hutool.extra.spring.SpringUtil;
import ****.aop.RequestInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//连接所有
registry.addInterceptor(SpringUtil.getBean(RequestInterceptor.class))
.addPathPatterns("/**")
.excludePathPatterns(
"/login"
,"/we-chat/**"
);
}
}
拦截器:
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import ****.apis.controller.BaseController;
import ****.client.model.UserAuthDto;
import ****.client.model.UserInfoDto;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.redis.RedisUtil;
import ****.util.JsonUtils;
import ****commons.exception.ApiException;
import ****commons.exception.ErrorResultCode;
import ****skyauth.dto.R;
import ****skyauth.dto.UserAuthBaseDto;
import ****skyauth.service.AuthService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class RequestInterceptor implements HandlerInterceptor {
@Autowired
RedisUtil redisUtil;
@Autowired
BaseController baseController;
@Autowired
AuthService authService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean result = false;
String token = request.getHeader(Constants.TOKEN);
if(ObjectUtil.isEmpty(token)){
ApiException.newThrow(ErrorResultCode.TOKEN_IS_NULL);
}
//从缓存中查找此token有没有存在
UserInfoDto userInfo = JSONObject.parseObject(String.valueOf(redisUtil.getCache(token)), UserInfoDto.class);
//如果redis缓存为空,去查一次天权系统查看token,然后放入redis中
if(ObjectUtil.isEmpty(userInfo)){
R r = authService.userPermission(Constants.SKYAUTH_APPID, token, new UserAuthBaseDto());
Integer code = (Integer)r.get("code");
//feign调用成功
if(****.infrastructure.general.constants.HttpStatus.SUCCESS==code){
Object data = r.get("data");
if(ObjectUtil.isNotEmpty(data)){
UserAuthDto userAuthDto = JSON.parseObject(JSON.toJSONString(data), UserAuthDto.class);
userInfo = new UserInfoDto();
//添加外部用户权限标识
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(userAuthDto.getDepts().toString());
if (JsonUtils.containsFuncNormalList(jsonNode, Constants.IS_EXT_USER_RIGHTS)) {
userInfo.setIsExternal("1");
}
BeanUtils.copyProperties(userAuthDto,userInfo);
//feign调用成功且token依然有效
redisUtil.setCacheWithExpiration(token,JSONObject.toJSONString(userInfo),60, TimeUnit.DAYS);
request.setAttribute(Constants.USER_NAME,userInfo.getUserName());
request.setAttribute(Constants.USER_ID,userInfo.getUserId());
request.setAttribute(Constants.IS_EXTERNAL, userInfo.getIsExternal());
result = true;
}else {
//这里是调用feign成功但是token校验失败了 返回401
ApiException.newThrow(ErrorResultCode.LOGIN_FAILED,"登陆已超时,请重新登录");
}
}else {
//feign调用失败
ApiException.newThrow(ErrorResultCode.CLIENT_DATA_EXE,(String)r.get("msg"));
}
}else{
request.setAttribute(Constants.USER_NAME, userInfo.getUserName());
request.setAttribute(Constants.USER_ID, userInfo.getUserId());
request.setAttribute(Constants.IS_EXTERNAL, userInfo.getIsExternal());
result = true;
}
return result;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
1.4 线程上下文管理
- ThreadLocalUtil:
- 线程安全的数据存储
- 支持嵌套调用上下文传递
- 自动清理机制
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 线程共享类
*/
public class ThreadLocalUtil {
private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() {
protected Map<String, Object> initialValue() {
return new HashMap(4);
}
};
public static Map<String, Object> getThreadLocal(){
return threadLocal.get();
}
public static <T> T get(String key) {
Map map = threadLocal.get();
return (T)map.get(key);
}
public static <T> T get(String key,T defaultValue) {
Map map = threadLocal.get();
return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
}
public static void set(String key, Object value) {
Map map = threadLocal.get();
map.put(key, value);
}
public static void set(Map<String, Object> keyValueMap) {
Map map = threadLocal.get();
map.putAll(keyValueMap);
}
public static void remove() {
threadLocal.remove();
}
public static <T> T remove(String key) {
Map map = threadLocal.get();
return (T)map.remove(key);
}
}
- LogUtil:
- 唯一日志ID生成(时间戳+UUID)
- 多线程ID继承机制
- 日志前缀自动追加
import cn.hutool.core.date.DateUtil;
import ****.infrastructure.general.constants.Constants;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
import java.util.UUID;
/**
* @Description: 线程日志处理类
*/
public class LogUtil {
/**
* 返回当前日志ID,如果为空则生成。拼接-和序号
*/
public static String id() {
String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
if (StringUtils.isEmpty(logId)) {
logId = DateUtil.format(new Date(), "yyyyMMddHHmmss") + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
}
Long seq = ThreadLocalUtil.get(Constants.THREAD_SEQ);
if (seq == null) {
seq = 1L;
ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
} else {
seq = seq + 1;
ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
}
return logId + "^" + seq +"-";
}
/**
* 返回当前日志ID,如果为空则生成。拼接-和序号
*/
public static String idEnd() {
return StringUtils.removeEnd(id(), "-");
}
/**
* 返回当前日志ID,如果为空则生成。拼接-和序号
*/
public static void createId() {
String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
if (StringUtils.isEmpty(logId)) {
logId = DateUtil.format(new Date(), "yyyyMMddHHmmss") + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
ThreadLocalUtil.set(Constants.THREAD_NO, logId);
}
}
}
调用示例:
@PostMapping("/pullWxChatRecordScheduleTask")
public String dingshirenwu(){
try{
//拉取聊天记录
wxCompanyChatService.pullWxChatRecord(null);
//查询时间倒序,配置时间内的(例如:三分钟)内里未被大模型扫描的群聊id集合
List<String> groupIdList = wxCompanyChatService.getGroupId();
log.info(LogUtil.id() + "获取有问题反馈的群ID {}", groupIdList);
//启动线程池任务
ExecutorService threadPool = LLmAnalysisChatRecordThread.getLlmAnalySisChatRecordThreadPool();
for (String groupId : groupIdList) {
String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
Long seq = ThreadLocalUtil.get(Constants.THREAD_SEQ);
threadPool.submit(() -> {
//日志ID :使用主线程日志ID + 子线程ID
ThreadLocalUtil.set(Constants.THREAD_NO, logId + "-" + Thread.currentThread().getId());
ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
wxGroupChatService.analysisGroupChatRecordByGroupId(groupId);
});
}
}catch (Exception e){
log.error(LogUtil.id() + "异常捕捉", e);
return "失败";
}
return "成功";
}
二、核心设计亮点
2.1 日志链路追踪方案
- 唯一TraceID生成规则:
yyyyMMddHHmmss+16位随机数
- 多线程ID继承:主线程ID + 子线程ID
- 日志要素包含:
[请求时间] [序号][响应时间][耗时][入参][出参][异常堆栈]
2.2 性能优化策略
- 文件类型请求参数特殊处理
- Redis缓存用户信息(60天)
- 异常堆栈智能截取(保留前100行)
2.3 安全控制
- 白名单路径配置
- 双重Token验证机制(本地缓存+远程验证)
- 外部用户权限标识隔离
三、使用示例
3.1 基础日志记录
log.info(LogUtil.id() + "获取有问题反馈的群ID {}", groupIdList);
3.2 多线程场景
threadPool.submit(() -> {
ThreadLocalUtil.set(Constants.THREAD_NO, logId + "-" + Thread.currentThread().getId());
// 业务逻辑
});
四、部署注意事项
- 需要配置Redis连接信息
- 权限服务地址配置
- 文件上传大小限制参数:
spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=100MB
五、总结
本方案实现了企业级项目的三个核心需求:
- 可追踪性:全链路日志追踪能力
- 稳定性:统一异常处理机制
- 安全性:完善的权限验证体系
通过合理的线程上下文管理、AOP切面设计、异常处理规范,显著提升了系统的可维护性和问题排查效率。实际使用中可根据业务需求扩展日志采集维度,集成APM系统实现更完善的监控体系。