日志是系统运行的“黑匣子”,记录着接口调用、数据流转、异常发生的全过程。但很多项目的日志还停留在“混乱打印”阶段——关键信息缺失、格式不统一、冗余日志泛滥,导致问题排查时“大海捞针”。一个设计良好的日志体系能将分散的日志转化为“可观测性中枢”,支持快速定位问题、分析性能瓶颈、追溯业务流程,是系统运维和优化的“核心依据”。
日志体系的核心价值与设计原则
为什么需要规范日志?
- 问题定位:当接口报错时,通过日志快速还原调用链和上下文(如“用户A在10:05调用下单接口失败,原因是库存不足”)
- 性能分析:统计接口响应时间、数据库查询耗时,识别性能瓶颈(如“订单接口90%的耗时在支付调用”)
- 业务追溯:跟踪关键业务流程(如下单→支付→发货),满足审计和排查需求
- 异常监控:通过错误日志触发告警(如“5分钟内出现10次支付超时”)
日志设计的“3C原则”
- Clear(清晰):日志内容明确,包含“谁(Who)、何时(When)、做了什么(What)、结果如何(Result)”
- Consistent(一致):格式统一(如时间戳格式、字段顺序),便于机器解析
- Concise(简洁):只记录必要信息,避免冗余(如不重复打印完整请求体)
日志的分类与内容规范
1. 访问日志:记录接口调用的“流水账”
记录所有接口的调用情况,包含基础访问信息:
@Aspect
@Component
public class AccessLogAspect {
private static final Logger accessLogger = LoggerFactory.getLogger("ACCESS_LOG");
@Around("execution(* com.example.controller..*(..)) && @annotation(requestMapping)")
public Object logAccess(ProceedingJoinPoint joinPoint, RequestMapping requestMapping) throws Throwable {
// 1. 收集请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestId = UUID.randomUUID().toString(); // 唯一请求ID,用于追踪
String method = request.getMethod(); // 请求方法GET/POST
String url = request.getRequestURI(); // 请求路径
String ip = request.getRemoteAddr(); // 客户端IP
String userId = request.getHeader("X-User-Id"); // 用户ID
long startTime = System.currentTimeMillis();
// 2. 执行目标方法
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
// 3. 记录异常信息
long cost = System.currentTimeMillis() - startTime;
accessLogger.info("requestId={}, ip={}, userId={}, method={}, url={}, cost={}ms, result=ERROR, error={}",
requestId, ip, userId, method, url, cost, e.getMessage());
throw e; // 继续抛出异常
}
// 4. 记录正常响应
long cost = System.currentTimeMillis() - startTime;
accessLogger.info("requestId={}, ip={}, userId={}, method={}, url={}, cost={}ms, result=SUCCESS",
requestId, ip, userId, method, url, cost);
return result;
}
}
访问日志关键字段:
- 基础标识:
requestId(唯一请求ID)、timestamp(时间戳) - 访问信息:
ip(客户端IP)、userId(用户标识)、method(HTTP方法)、url(接口路径) - 性能指标:
cost(耗时,毫秒) - 结果状态:
result(SUCCESS/ERROR)、error(错误信息,仅异常时记录)
2. 业务日志:记录核心流程的“关键节点”
针对核心业务流程(如下单、支付),记录关键步骤和状态变化:
@Service
public class OrderService {
private static final Logger businessLogger = LoggerFactory.getLogger("BUSINESS_LOG");
public Long createOrder(OrderCreateDTO dto) {
// 生成订单号
String orderNo = generateOrderNo();
// 记录业务开始
businessLogger.info("orderNo={}, userId={}, action=CREATE_ORDER_START, productId={}, quantity={}",
orderNo, dto.getUserId(), dto.getProductId(), dto.getQuantity());
try {
// 扣减库存
productService.reduceStock(dto.getProductId(), dto.getQuantity());
businessLogger.info("orderNo={}, action=REDUCE_STOCK_SUCCESS", orderNo);
// 创建订单记录
Order order = new Order();
// ... 订单信息设置
orderMapper.insert(order);
businessLogger.info("orderNo={}, action=ORDER_CREATED, orderId={}", orderNo, order.getId());
// 调用支付服务
PaymentResult paymentResult = paymentService.createPayment(order);
businessLogger.info("orderNo={}, action=PAYMENT_CREATED, paymentId={}", orderNo, paymentResult.getPaymentId());
return order.getId();
} catch (Exception e) {
// 记录业务异常
businessLogger.error("orderNo={}, action=CREATE_ORDER_FAILED, error={}", orderNo, e.getMessage(), e);
throw e;
}
}
}
业务日志关键字段:
- 业务标识:
orderNo(订单号)、userId(用户ID)等业务唯一ID - 操作信息:
action(操作名称,如CREATE_ORDER_START) - 上下文数据:关键业务参数(如商品ID、数量)
- 状态变化:记录流程节点的状态(如库存扣减成功、订单创建完成)
3. 异常日志:记录错误的“完整上下文”
异常日志需包含足够的上下文信息,便于复现和定位问题:
@Service
public class PaymentService {
private static final Logger errorLogger = LoggerFactory.getLogger("ERROR_LOG");
public PaymentResult callThirdPartyPayment(PaymentDTO dto) {
try {
// 调用第三方支付接口
String response = restTemplate.postForObject(
"https://third-party/pay",
dto,
String.class
);
return parsePaymentResult(response);
} catch (RestClientException e) {
// 记录异常详情:包含参数、异常栈
errorLogger.error("payment failed, orderNo={}, amount={}, thirdPartyUrl={}, error={}",
dto.getOrderNo(), dto.getAmount(), "https://third-party/pay", e.getMessage(), e);
throw new BusinessException("支付接口调用失败");
}
}
}
异常日志关键原则:
- 包含触发异常的关键参数(如订单号、金额)
- 记录完整的异常堆栈(便于定位代码位置)
- 区分异常类型(如网络超时、参数错误)
日志的输出与存储策略
1. 日志格式标准化
使用JSON格式输出日志,便于日志系统(如ELK)解析和检索:
<!-- logback.xml配置JSON格式输出 -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/app/access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/app/access.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory> <!-- 保留7天日志 -->
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- 自定义字段 -->
<customFields>{"app":"order-service","env":"prod"}</customFields>
<!-- 包含MDC中的requestId -->
<includeMdcKeyName>requestId</includeMdcKeyName>
</encoder>
</appender>
<!-- 关联日志器 -->
<logger name="ACCESS_LOG" level="INFO" additivity="false">
<appender-ref ref="JSON_FILE" />
</logger>
标准化JSON日志示例:
{
"timestamp": "2024-05-20T10:30:00.123+08:00",
"level": "INFO",
"logger": "ACCESS_LOG",
"app": "order-service",
"env": "prod",
"requestId": "a1b2c3d4",
"ip": "192.168.1.1",
"userId": "1001",
"method": "POST",
"url": "/api/orders",
"cost": 150,
"result": "SUCCESS"
}
2. 日志分级存储与轮转
- 按级别分离:将ERROR级别日志单独存储,便于快速查看错误
- 按时间轮转:每日生成新日志文件,避免单个文件过大
- 按大小轮转:单文件达到阈值(如1GB)时切割,便于处理
- 定期清理:设置日志保留期限(如生产环境保留30天,测试环境7天)
3. 分布式追踪:关联跨服务日志
在微服务架构中,一个请求可能经过多个服务,通过requestId串联所有日志:
// 网关层生成requestId并传递
@Component
public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String requestId = UUID.randomUUID().toString();
// 放入MDC(日志上下文)
MDC.put("requestId", requestId);
// 添加到响应头,便于前端排查
((HttpServletResponse) response).setHeader("X-Request-Id", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("requestId"); // 清除MDC
}
}
}
// 服务间调用时传递requestId
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {
// 从当前线程MDC中获取requestId,添加到请求头
String requestId = MDC.get("requestId");
if (requestId != null) {
request.getHeaders().add("X-Request-Id", requestId);
}
return execution.execute(request, body);
}));
return restTemplate;
}
}
通过requestId可在日志系统中检索到一个请求经过的所有服务的日志,完整还原调用链。
日志的分析与监控
1. 日志收集与检索(ELK Stack)
- Elasticsearch:存储日志并提供全文检索
- Logstash:收集、过滤、转换日志
- Kibana:可视化查询和分析日志
通过Kibana查询示例:
- 查找
requestId="a1b2c3d4"的所有日志(追踪完整调用链) - 统计
url="/api/orders"的平均响应时间(分析性能) - 筛选
level="ERROR"且env="prod"的日志(监控错误)
2. 关键指标监控与告警
基于日志数据提取关键指标,设置告警阈值:
- 错误率:当
/api/pay接口错误率超过1%时告警 - 响应时间:当
/api/orders接口P95响应时间超过500ms时告警 - 异常频次:当“库存不足”异常5分钟内出现10次时告警
避坑指南
- 不要记录敏感信息:日志中禁止出现密码、手机号、身份证号等敏感数据(需脱敏)
- 避免日志泛滥:非核心流程减少INFO级别日志,避免“日志风暴”拖垮系统
- 不要重复打印:同一信息不要在多层级日志中重复记录(如接口层已记录,业务层无需再记)
- 本地日志与线上分离:开发环境日志可详细,生产环境需控制量和级别
日志体系的核心是“记录有价值的信息”——既不能因缺失关键信息导致问题无法排查,也不能因冗余信息造成资源浪费。一个完善的日志体系,能让系统从“黑盒”变为“透明可控”,这是后端系统“可观测性”的基础,也是保障系统稳定运行的关键支撑。