文章 601
评论 5
浏览 235764
日志打得好排查没烦恼:5 条生产环境日志规范

日志打得好排查没烦恼:5 条生产环境日志规范

一、引言

日志是生产环境排查问题的眼睛。写得好的日志能让你快速定位问题,写得不好的日志只会增加排查难度。

本文分享 5 条生产环境日志规范,每条都有反面示例和正面示例,看完就能用。


二、规范 1:每条日志必须带 TraceId

2.1 反面示例

@Slf4j
@RestController
public class OrderController {
    
    @GetMapping("/order/{id}")
    public OrderDTO getOrder(@PathVariable Long id) {
        log.info("开始查询订单,id={}", id);
        Order order = orderService.findById(id);
        log.info("查询订单完成,order={}", order);
        return OrderDTO.from(order);
    }
}

问题:没有 TraceId,无法串联一次请求的所有日志。当多个请求同时进来时,日志会混在一起,很难分辨哪些日志属于同一个请求。

2.2 正面示例

@Slf4j
@RestController
public class OrderController {
    
    @GetMapping("/order/{id}")
    public OrderDTO getOrder(@PathVariable Long id) {
        log.info("开始查询订单,id={}", id);
        Order order = orderService.findById(id);
        log.info("查询订单完成,order={}", order);
        return OrderDTO.from(order);
    }
}

配置 logback.xml

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

配置 TraceIdFilter(详见完整配置示例):

@Component
public class TraceIdFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("traceId");
        }
    }
}

效果:每条日志都会自动带上 TraceId,无需手动传递。

2024-01-15 10:30:45.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.c.OrderController - 开始查询订单,id=1
2024-01-15 10:30:45.145 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.s.OrderService - 查询订单,id=1
2024-01-15 10:30:45.167 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.c.OrderController - 查询订单完成,order=Order(id=1, ...)

2.3 为什么这样设计

  • 请求串联:通过 TraceId 可以快速找到一次请求的所有日志
  • 无需手动传递:使用 MDC + AOP 自动注入,业务代码零侵入
  • 微服务链路追踪:配合 Zipkin、SkyWalking 等工具,实现跨服务追踪

三、规范 2:入参出参分开打

3.1 反面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        log.info("创建订单,request={}, result={}", request, doCreate(request));
        return doCreate(request);
    }
    
    private OrderDTO doCreate(OrderCreateRequest request) {
        // 业务逻辑
        return new OrderDTO();
    }
}

问题:入参和出参混在一起打印,而且 doCreate() 被调用了两次。更重要的是,如果 doCreate() 抛出异常,入参日志也不会打印。

3.2 正面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        log.info("创建订单入参,userId={}, productId={}, quantity={}", 
                request.getUserId(), request.getProductId(), request.getQuantity());
        
        OrderDTO result = doCreate(request);
        
        log.debug("创建订单出参,orderId={}, amount={}", 
                result.getOrderId(), result.getAmount());
        
        return result;
    }
    
    private OrderDTO doCreate(OrderCreateRequest request) {
        // 业务逻辑
        return new OrderDTO();
    }
}

效果

2024-01-15 10:30:45.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.s.OrderService - 创建订单入参,userId=1001, productId=2001, quantity=2
2024-01-15 10:30:45.167 [http-nio-8080-exec-1] [a1b2c3d4e5f6] DEBUG c.e.s.OrderService - 创建订单出参,orderId=3001, amount=99.98

3.3 为什么这样设计

  • 入参用 INFO:方便排查问题时快速找到请求参数
  • 出参用 DEBUG:生产环境通常关闭 DEBUG,避免日志过多
  • 入参出参分开:即使出参异常,入参也能正常打印
  • 避免重复计算:先计算结果,再打印日志

四、规范 3:异常日志必须包含完整堆栈和业务上下文

4.1 反面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        try {
            return doCreate(request);
        } catch (Exception e) {
            log.error("创建订单失败");
            throw e;
        }
    }
}

问题:只打印了错误信息,没有堆栈,也没有业务上下文。排查问题时无法知道是哪一行代码出错,也不知道是哪个用户的请求。

4.2 正面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        try {
            return doCreate(request);
        } catch (Exception e) {
            log.error("创建订单失败,userId={}, productId={}, quantity={}", 
                    request.getUserId(), request.getProductId(), request.getQuantity(), e);
            throw new BusinessException("创建订单失败", e);
        }
    }
}

效果

2024-01-15 10:30:45.145 [http-nio-8080-exec-1] [a1b2c3d4e5f6] ERROR c.e.s.OrderService - 创建订单失败,userId=1001, productId=2001, quantity=2
java.lang.NullPointerException: null
    at com.example.service.OrderService.doCreate(OrderService.java:45)
    at com.example.service.OrderService.createOrder(OrderService.java:28)
    at com.example.controller.OrderController.createOrder(OrderController.java:35)
    ...

4.3 为什么这样设计

  • 完整堆栈:可以快速定位到出错的代码行
  • 业务上下文:知道是哪个用户的请求出错,方便排查业务问题
  • 异常传递:使用自定义异常包装,保留原始堆栈信息

五、规范 4:敏感信息必须脱敏后再打日志

5.1 反面示例

@Slf4j
@Service
public class UserService {
    
    public UserDTO getUser(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();
        log.info("查询用户成功,user={}", user);
        return UserDTO.from(user);
    }
}

@Data
@Entity
public class User {
    private Long id;
    private String name;
    private String phone;
    private String idCard;
    private String address;
}

问题:直接打印整个 User 对象,会把手机号、身份证等敏感信息暴露在日志中,违反数据安全法规。

5.2 正面示例

@Slf4j
@Service
public class UserService {
    
    public UserDTO getUser(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();
        log.info("查询用户成功,id={}, name={}, phone={}, idCard={}", 
                user.getId(), 
                user.getName(), 
                DesensitizeUtils.maskPhone(user.getPhone()), 
                DesensitizeUtils.maskIdCard(user.getIdCard()));
        return UserDTO.from(user);
    }
}

public class DesensitizeUtils {
    
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 11) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 18) {
            return idCard;
        }
        return idCard.substring(0, 6) + "**********" + idCard.substring(16);
    }
    
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String username = parts[0];
        if (username.length() <= 2) {
            return username + "****@" + parts[1];
        }
        return username.substring(0, 2) + "****" + "@" + parts[1];
    }
}

效果

2024-01-15 10:30:45.123 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.s.UserService - 查询用户成功,id=1001, name=张三, phone=138****1234, idCard=110101**********12

5.3 为什么这样设计

  • 数据安全:保护用户隐私,符合 GDPR、个人信息保护法等法规要求
  • 避免泄露:即使日志被泄露,敏感信息也无法被识别
  • 统一工具类:使用工具类统一处理脱敏逻辑,避免重复代码

六、规范 5:关键路径加耗时日志

6.1 反面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        // 业务逻辑
        Order order = new Order();
        order.setUserId(request.getUserId());
        // ...
        order = orderRepository.save(order);
        
        // 调用支付服务
        paymentService.pay(order.getId(), order.getAmount());
        
        // 发送消息
        messageService.sendOrderCreated(order.getId());
        
        return OrderDTO.from(order);
    }
}

问题:没有记录各个环节的耗时,当接口响应变慢时,无法知道是哪个环节出了问题。

6.2 正面示例

@Slf4j
@Service
public class OrderService {
    
    public OrderDTO createOrder(OrderCreateRequest request) {
        StopWatch stopWatch = new StopWatch("createOrder");
        
        stopWatch.start("保存订单");
        Order order = new Order();
        order.setUserId(request.getUserId());
        // ...
        order = orderRepository.save(order);
        stopWatch.stop();
        
        stopWatch.start("调用支付");
        paymentService.pay(order.getId(), order.getAmount());
        stopWatch.stop();
        
        stopWatch.start("发送消息");
        messageService.sendOrderCreated(order.getId());
        stopWatch.stop();
        
        log.info("创建订单耗时统计:{}", stopWatch.prettyPrint());
        
        return OrderDTO.from(order);
    }
}

效果

2024-01-15 10:30:45.167 [http-nio-8080-exec-1] [a1b2c3d4e5f6] INFO  c.e.s.OrderService - 创建订单耗时统计:StopWatch 'createOrder': running time = 4567800 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
1234500  26.9%  保存订单
2345600  51.4%  调用支付
0987700  21.6%  发送消息

6.3 AOP 自动记录耗时

@Aspect
@Component
public class MethodTimeAspect {
    
    @Around("execution(* com.example.service..*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        StopWatch stopWatch = new StopWatch(methodName);
        
        stopWatch.start(methodName);
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            log.info("方法耗时统计:{}", stopWatch.prettyPrint());
        }
    }
}

6.4 为什么这样设计

  • 性能排查:快速定位慢方法,找到性能瓶颈
  • 监控告警:结合监控系统,可以设置耗时阈值告警
  • 优化依据:有了耗时数据,才能针对性地进行优化

七、完整配置示例

7.1 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n" />
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <logger name="com.example" level="DEBUG" />
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

7.2 MDC TraceId 过滤器

@Component
public class TraceIdFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("traceId");
        }
    }
}

7.3 脱敏工具类

public final class DesensitizeUtils {
    
    private DesensitizeUtils() {}
    
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 11) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 18) {
            return idCard;
        }
        return idCard.substring(0, 6) + "**********" + idCard.substring(16);
    }
    
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String username = parts[0];
        if (username.length() <= 2) {
            return username + "****@" + parts[1];
        }
        return username.substring(0, 2) + "****" + "@" + parts[1];
    }
    
    public static String maskBankCard(String bankCard) {
        if (bankCard == null || bankCard.length() < 16) {
            return bankCard;
        }
        return bankCard.substring(0, 4) + "********" + bankCard.substring(bankCard.length() - 4);
    }
}

7.4 AOP 耗时记录切面

@Aspect
@Component
public class MethodTimeAspect {
    
    private static final Logger log = LoggerFactory.getLogger(MethodTimeAspect.class);
    
    @Around("execution(* com.example.service..*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        
        long startTime = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            
            if (duration > 100) {
                log.warn("方法耗时较长,className={}, methodName={}, duration={}ms", 
                        className, methodName, duration);
            } else {
                log.debug("方法耗时,className={}, methodName={}, duration={}ms", 
                        className, methodName, duration);
            }
        }
    }
}

八、总结

8.1 5 条规范总结

日志规范总结:

┌─────────────────────────────────────────────────────┐
│                                                     │
│  规范 1:每条日志必须带 TraceId                       │
│  └── 使用 MDC + AOP 自动注入,无需手动传递              │
│                                                     │
│  规范 2:入参出参分开打                               │
│  └── 入参用 INFO,出参用 DEBUG                        │
│                                                     │
│  规范 3:异常日志必须包含完整堆栈和业务上下文            │
│  └── 带上 userId、orderId 等关键信息                   │
│                                                     │
│  规范 4:敏感信息必须脱敏后再打日志                      │
│  └── 手机号、身份证、银行卡、邮箱等需脱敏                │
│                                                     │
│  规范 5:关键路径加耗时日志                            │
│  └── 使用 StopWatch 或 AOP 自动记录                    │
│                                                     │
└─────────────────────────────────────────────────────┘

8.2 日志级别使用原则

日志级别使用原则:

┌─────────────────────────────────────────────────────┐
│                                                     │
│  ERROR:系统错误,需要立即处理                         │
│  └── 数据库连接失败、消息发送失败、业务异常              │
│                                                     │
│  WARN:警告信息,需要关注但不需要立即处理                │
│  └── 方法耗时较长、配置过期、降级处理                    │
│                                                     │
│  INFO:关键业务流程,生产环境开启                       │
│  └── 请求入参、订单创建成功、用户登录                    │
│                                                     │
│  DEBUG:详细调试信息,生产环境关闭                      │
│  └── 请求出参、中间状态、详细流程                       │
│                                                     │
│  TRACE:最详细的追踪信息,仅开发环境使用                 │
│  └── 方法调用栈、变量详细值                            │
│                                                     │
└─────────────────────────────────────────────────────┘

8.3 写日志的黄金法则

写日志的黄金法则:

1. 可搜索:日志必须包含 TraceId、userId、orderId 等关键信息
2. 可读:日志格式清晰,信息完整
3. 安全:敏感信息必须脱敏
4. 适度:不要过多,也不要过少
5. 一致:团队使用统一的日志规范

💡 互动话题:你们团队的日志规范是什么?有没有遇到过日志写得不好导致排查困难的情况?欢迎在评论区分享!


标题:日志打得好排查没烦恼:5 条生产环境日志规范
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/07/22/1784446840543.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消