当前位置: 代码网 > it编程>编程语言>Java > SpringBoot中构建生产级日志系统的优雅指南

SpringBoot中构建生产级日志系统的优雅指南

2026年04月16日 Java 我要评论
一、需求描述在实际开发中,日志系统需要满足以下需求:区分日志级别:debug/info/warn/error 各司其职性能友好:避免日志序列化开销,支持异步输出链路追踪:一次请求全链路可追踪(requ

一、需求描述

在实际开发中,日志系统需要满足以下需求:

  • 区分日志级别:debug/info/warn/error 各司其职
  • 性能友好:避免日志序列化开销,支持异步输出
  • 链路追踪:一次请求全链路可追踪(requestid)
  • 敏感信息脱敏:手机号、身份证等自动脱敏
  • 日志分类:业务日志、错误日志、慢sql日志分开存储
  • 输出规范:json格式,便于elk采集分析
  • 开发/生产环境差异化:开发环境输出控制台,生产环境输出文件

二、详细步骤与代码实现

1. 添加依赖

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>
    <!-- 性能优化:异步日志需要disruptor -->
    <dependency>
        <groupid>com.lmax</groupid>
        <artifactid>disruptor</artifactid>
        <version>3.4.4</version>
    </dependency>
    <!-- 链路追踪 -->
    <dependency>
        <groupid>org.springframework.cloud</groupid>
        <artifactid>spring-cloud-starter-sleuth</artifactid>
        <version>3.1.5</version>
    </dependency>
    <!-- 简化代码:lombok -->
    <dependency>
        <groupid>org.projectlombok</groupid>
        <artifactid>lombok</artifactid>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 配置文件(application.yml)

# application.yml
spring:
  application:
    name: demo-service
  # 链路追踪配置
  sleuth:
    web:
      enabled: true
    sampler:
      probability: 1.0  # 生产环境建议0.1
# 日志配置
logging:
  # 日志文件路径
  file:
    path: ./logs
    name: ${logging.file.path}/${spring.application.name}.log
  # 日志级别
  level:
    root: info
    com.example: debug
    org.springframework.web: info
    org.hibernate: warn
  # 日志格式
  pattern:
    console: "%clr(%d{yyyy-mm-dd hh:mm:ss.sss}){faint} %clr(%5p) %clr(${pid:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"
    file: "%d{yyyy-mm-dd hh:mm:ss.sss} %5p ${pid:- } --- [%t] %-40.40logger{39} : %m%n%wex"
# 自定义日志配置
log:
  # 敏感字段列表
  sensitive-fields:
    - password
    - oldpassword
    - newpassword
    - idcard
    - phone
    - bankcard
    - token
    - secret

3. logback配置(logback-spring.xml)

<?xml version="1.0" encoding="utf-8"?>
<configuration scan="true" scanperiod="60 seconds">
    <!-- 引入spring环境配置 -->
    <springproperty scope="context" name="app_name" source="spring.application.name" defaultvalue="app"/>
    <springproperty scope="context" name="log_path" source="logging.file.path" defaultvalue="./logs"/>
    <!-- 彩色日志依赖 -->
    <conversionrule conversionword="clr" converterclass="org.springframework.boot.logging.logback.colorconverter"/>
    <!-- 日志格式 -->
    <property name="console_log_pattern" 
              value="%clr(%d{yyyy-mm-dd hh:mm:ss.sss}){faint} %clr(%5p) %clr(${pid:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wex"/>
    <property name="file_log_pattern" 
              value="%d{yyyy-mm-dd hh:mm:ss.sss} %5p ${pid:- } --- [%t] %-40.40logger{39} : %m%n%wex"/>
    <!-- json格式(用于生产环境elk) -->
    <property name="json_log_pattern" 
              value='{"timestamp":"%d{yyyy-mm-dd hh:mm:ss.sss}","level":"%p","app":"${app_name}","traceid":"%x{traceid}","thread":"%t","logger":"%logger","message":"%m","exception":"%wex"}%n'/>
    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.consoleappender">
        <encoder>
            <pattern>${console_log_pattern}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <!-- 异步控制台输出(性能优化) -->
    <appender name="async_console" class="ch.qos.logback.classic.asyncappender">
        <discardingthreshold>0</discardingthreshold>
        <queuesize>1024</queuesize>
        <appender-ref ref="console"/>
    </appender>
    <!-- 普通日志文件 -->
    <appender name="file" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_path}/${app_name}.log</file>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
            <filenamepattern>${log_path}/${app_name}.%d{yyyy-mm-dd}.log</filenamepattern>
            <maxhistory>30</maxhistory>
            <totalsizecap>10gb</totalsizecap>
        </rollingpolicy>
        <encoder>
            <pattern>${file_log_pattern}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <!-- 错误日志单独文件 -->
    <appender name="error_file" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_path}/${app_name}-error.log</file>
        <filter class="ch.qos.logback.classic.filter.thresholdfilter">
            <level>error</level>
        </filter>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
            <filenamepattern>${log_path}/${app_name}-error.%d{yyyy-mm-dd}.log</filenamepattern>
            <maxhistory>90</maxhistory>
        </rollingpolicy>
        <encoder>
            <pattern>${file_log_pattern}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <!-- 业务日志文件 -->
    <appender name="biz_file" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_path}/biz.log</file>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
            <filenamepattern>${log_path}/biz.%d{yyyy-mm-dd}.log</filenamepattern>
            <maxhistory>30</maxhistory>
        </rollingpolicy>
        <encoder>
            <pattern>${file_log_pattern}</pattern>
        </encoder>
    </appender>
    <!-- 异步文件输出(性能优化) -->
    <appender name="async_file" class="ch.qos.logback.classic.asyncappender">
        <queuesize>2048</queuesize>
        <discardingthreshold>0</discardingthreshold>
        <includecallerdata>true</includecallerdata>
        <appender-ref ref="file"/>
    </appender>
    <appender name="async_error_file" class="ch.qos.logback.classic.asyncappender">
        <queuesize>1024</queuesize>
        <appender-ref ref="error_file"/>
    </appender>
    <appender name="async_biz_file" class="ch.qos.logback.classic.asyncappender">
        <queuesize>1024</queuesize>
        <appender-ref ref="biz_file"/>
    </appender>
    <!-- 业务日志 logger -->
    <logger name="biz_logger" level="info" additivity="false">
        <appender-ref ref="async_biz_file"/>
    </logger>
    <!-- root logger -->
    <root level="info">
        <appender-ref ref="async_console"/>
        <appender-ref ref="async_file"/>
        <appender-ref ref="async_error_file"/>
    </root>
</configuration>

4. 日志工具类封装

package com.example.log.util;

import lombok.extern.slf4j.slf4j;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
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.uuid;

/**
 * 日志工具类
 */
@slf4j
@component
public class logutil {
    
    // 业务日志专用logger
    private static final logger biz_logger = loggerfactory.getlogger("biz_logger");
    
    /**
     * 获取当前请求的traceid
     */
    public static string gettraceid() {
        try {
            servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
            if (attributes != null) {
                httpservletrequest request = attributes.getrequest();
                string traceid = request.getheader("x-trace-id");
                if (traceid == null || traceid.isempty()) {
                    traceid = uuid.randomuuid().tostring().replace("-", "");
                }
                return traceid;
            }
        } catch (exception e) {
            log.warn("获取traceid失败", e);
        }
        return uuid.randomuuid().tostring().replace("-", "");
    }
    
    /**
     * 业务日志(关键操作记录)
     */
    public static void bizlog(string operation, string userid, object... params) {
        string traceid = gettraceid();
        string message = string.format("[biz][%s][%s][%s] params: %s", 
            traceid, operation, userid, params);
        biz_logger.info(message);
    }
    
    /**
     * 接口调用日志(简洁版)
     */
    public static void apilog(string apiname, long costtime, object request, object response) {
        if (costtime > 3000) {
            // 慢接口使用warn级别
            log.warn("[api][{}] cost: {}ms, request: {}, response: {}", 
                apiname, costtime, request, response);
        } else {
            log.info("[api][{}] cost: {}ms", apiname, costtime);
        }
    }
    
    /**
     * 方法调用日志(带耗时)
     */
    public static void methodlog(string methodname, long starttime) {
        long cost = system.currenttimemillis() - starttime;
        if (cost > 1000) {
            log.warn("[method][{}] cost: {}ms", methodname, cost);
        } else {
            log.debug("[method][{}] cost: {}ms", methodname, cost);
        }
    }
}

5. 敏感信息脱敏工具

package com.example.log.util;

import com.fasterxml.jackson.core.jsonprocessingexception;
import com.fasterxml.jackson.databind.objectmapper;
import com.fasterxml.jackson.databind.serializationfeature;
import lombok.extern.slf4j.slf4j;
import org.springframework.beans.factory.annotation.value;
import org.springframework.stereotype.component;

import javax.annotation.postconstruct;
import java.util.*;
import java.util.regex.pattern;

/**
 * 日志脱敏工具
 */
@slf4j
@component
public class desensitizationutil {
    
    @value("${log.sensitive-fields:}")
    private list<string> sensitivefields;
    
    private static final set<string> sensitive_fields = new hashset<>(arrays.aslist(
        "password", "oldpassword", "newpassword", "idcard", "phone", 
        "bankcard", "token", "secret", "authorization"
    ));
    
    private static final pattern phone_pattern = pattern.compile("(\\d{3})\\d{4}(\\d{4})");
    private static final pattern id_card_pattern = pattern.compile("(\\d{4})\\d{10}(\\d{4})");
    private static final pattern bank_card_pattern = pattern.compile("(\\d{4})\\d{10,12}(\\d{4})");
    
    private objectmapper objectmapper;
    
    @postconstruct
    public void init() {
        this.objectmapper = new objectmapper();
        this.objectmapper.enable(serializationfeature.indent_output);
        if (sensitivefields != null) {
            sensitive_fields.addall(sensitivefields);
        }
    }
    
    /**
     * 对象脱敏(json序列化前处理)
     */
    public string tojsonwithdesensitization(object obj) {
        try {
            if (obj == null) {
                return "null";
            }
            object desensitized = desensitize(obj);
            return objectmapper.writevalueasstring(desensitized);
        } catch (jsonprocessingexception e) {
            log.error("json序列化失败", e);
            return obj != null ? obj.tostring() : "null";
        }
    }
    
    /**
     * 递归脱敏
     */
    @suppresswarnings("unchecked")
    private object desensitize(object obj) {
        if (obj == null) {
            return null;
        }
        
        if (obj instanceof map) {
            map<string, object> map = (map<string, object>) obj;
            map<string, object> result = new hashmap<>();
            for (map.entry<string, object> entry : map.entryset()) {
                string key = entry.getkey();
                object value = entry.getvalue();
                
                if (sensitive_fields.contains(key.tolowercase())) {
                    result.put(key, "***");
                } else {
                    result.put(key, desensitize(value));
                }
            }
            return result;
        }
        
        if (obj instanceof list) {
            list<object> list = (list<object>) obj;
            list<object> result = new arraylist<>();
            for (object item : list) {
                result.add(desensitize(item));
            }
            return result;
        }
        
        // 字符串类型特殊处理
        if (obj instanceof string) {
            string str = (string) obj;
            // 手机号脱敏
            if (str.matches("^1[3-9]\\d{9}$")) {
                return phone_pattern.matcher(str).replaceall("$1****$2");
            }
            // 身份证脱敏
            if (str.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dxx]$")) {
                return id_card_pattern.matcher(str).replaceall("$1**********$2");
            }
        }
        
        return obj;
    }
    
    /**
     * 快速脱敏手机号
     */
    public static string maskphone(string phone) {
        if (phone == null || phone.length() != 11) {
            return phone;
        }
        return phone.replaceall("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
    
    /**
     * 快速脱敏身份证
     */
    public static string maskidcard(string idcard) {
        if (idcard == null || idcard.length() < 18) {
            return idcard;
        }
        return idcard.replaceall("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
    }
}

6. 全局日志拦截器(aop实现)

package com.example.log.aspect;

import com.example.log.util.desensitizationutil;
import com.example.log.util.logutil;
import lombok.extern.slf4j.slf4j;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.beans.factory.annotation.autowired;
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.lang.reflect.method;
import java.util.arrays;
import java.util.stream.collectors;

/**
 * controller层日志切面
 */
@slf4j
@aspect
@component
public class controllerlogaspect {
    
    @autowired
    private desensitizationutil desensitizationutil;
    
    // 定义切点:所有controller类下的方法
    @pointcut("execution(* com.example..controller.*.*(..))")
    public void controllerpointcut() {}
    
    @around("controllerpointcut()")
    public object around(proceedingjoinpoint joinpoint) throws throwable {
        long starttime = system.currenttimemillis();
        
        // 获取请求信息
        servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes();
        httpservletrequest request = attributes != null ? attributes.getrequest() : null;
        
        // 获取方法信息
        methodsignature signature = (methodsignature) joinpoint.getsignature();
        method method = signature.getmethod();
        string classname = joinpoint.gettarget().getclass().getsimplename();
        string methodname = method.getname();
        
        // 获取参数(脱敏处理)
        object[] args = joinpoint.getargs();
        string params = "";
        if (args != null && args.length > 0) {
            params = arrays.stream(args)
                .map(arg -> desensitizationutil.tojsonwithdesensitization(arg))
                .collect(collectors.joining(", "));
        }
        
        // 请求信息日志
        if (request != null) {
            log.info("【请求开始】{} {} | 参数: {}", 
                request.getmethod(), request.getrequesturi(), params);
        } else {
            log.info("【方法调用】{}.{} 参数: {}", classname, methodname, params);
        }
        
        object result = null;
        try {
            result = joinpoint.proceed();
            long costtime = system.currenttimemillis() - starttime;
            
            // 响应日志(脱敏处理)
            string responsejson = desensitizationutil.tojsonwithdesensitization(result);
            log.info("【请求结束】{}.{} 耗时: {}ms | 响应: {}", 
                classname, methodname, costtime, responsejson);
            
            // 慢请求告警
            if (costtime > 3000) {
                log.warn("【慢请求】{}.{} 耗时: {}ms", classname, methodname, costtime);
            }
            
            return result;
        } catch (exception e) {
            long costtime = system.currenttimemillis() - starttime;
            log.error("【请求异常】{}.{} 耗时: {}ms 异常: {}", 
                classname, methodname, costtime, e.getmessage(), e);
            throw e;
        }
    }
}

7. mdc链路追踪过滤器

package com.example.log.filter;

import lombok.extern.slf4j.slf4j;
import org.slf4j.mdc;
import org.springframework.core.annotation.order;
import org.springframework.stereotype.component;

import javax.servlet.*;
import javax.servlet.http.httpservletrequest;
import java.io.ioexception;
import java.util.uuid;

/**
 * mdc过滤器:实现全链路追踪
 */
@slf4j
@component
@order(1)
public class traceidfilter implements filter {
    
    private static final string trace_id = "traceid";
    
    @override
    public void dofilter(servletrequest request, servletresponse response, filterchain chain)
            throws ioexception, servletexception {
        
        httpservletrequest httprequest = (httpservletrequest) request;
        string traceid = httprequest.getheader("x-trace-id");
        
        if (traceid == null || traceid.isempty()) {
            traceid = uuid.randomuuid().tostring().replace("-", "");
        }
        
        try {
            // 将traceid放入mdc,日志模板中可通过%x{traceid}引用
            mdc.put(trace_id, traceid);
            mdc.put("clientip", getclientip(httprequest));
            
            chain.dofilter(request, response);
        } finally {
            // 清理mdc,避免内存泄漏
            mdc.clear();
        }
    }
    
    private string getclientip(httpservletrequest request) {
        string ip = request.getheader("x-forwarded-for");
        if (ip == null || ip.isempty()) {
            ip = request.getremoteaddr();
        }
        return ip != null ? ip.split(",")[0].trim() : "unknown";
    }
}

8. 业务中使用示例

package com.example.demo.controller;

import com.example.demo.service.userservice;
import com.example.log.util.logutil;
import lombok.requiredargsconstructor;
import lombok.extern.slf4j.slf4j;
import org.springframework.web.bind.annotation.*;

import javax.validation.valid;
import java.util.hashmap;
import java.util.map;

@slf4j
@restcontroller
@requestmapping("/api/user")
@requiredargsconstructor
public class usercontroller {
    
    private final userservice userservice;
    
    @postmapping("/login")
    public map<string, object> login(@requestbody @valid loginrequest request) {
        // 使用lombok的@slf4j
        log.info("用户登录请求: username={}", request.getusername());
        
        long starttime = system.currenttimemillis();
        try {
            uservo user = userservice.login(request);
            
            // 记录业务日志
            logutil.bizlog("用户登录", user.getid(), "login", request.getusername());
            
            // 记录方法耗时
            logutil.methodlog("login", starttime);
            
            return map.of("success", true, "data", user);
        } catch (exception e) {
            log.error("用户登录失败: username={}, error={}", request.getusername(), e.getmessage(), e);
            throw e;
        }
    }
    
    @postmapping("/update")
    public map<string, object> updateuser(@requestbody userupdaterequest request) {
        // 这里password字段会被自动脱敏
        log.info("更新用户信息: {}", request);
        
        userservice.updateuser(request);
        return map.of("success", true);
    }
}

// 请求对象示例
@data
class loginrequest {
    private string username;
    private string password;  // 会被脱敏
}

@data
class userupdaterequest {
    private string userid;
    private string phone;
    private string idcard;
    private string password;
}

9. 异常统一处理中的日志

package com.example.demo.handler;

import lombok.extern.slf4j.slf4j;
import org.springframework.web.bind.annotation.exceptionhandler;
import org.springframework.web.bind.annotation.restcontrolleradvice;

@slf4j
@restcontrolleradvice
public class globalexceptionhandler {
    
    @exceptionhandler(businessexception.class)
    public result<?> handlebusinessexception(businessexception e) {
        // 业务异常:记录warn级别即可
        log.warn("业务异常: code={}, message={}", e.getcode(), e.getmessage());
        return result.error(e.getcode(), e.getmessage());
    }
    
    @exceptionhandler(exception.class)
    public result<?> handleexception(exception e) {
        // 系统异常:记录error级别,包含堆栈
        log.error("系统异常", e);
        return result.error(500, "系统繁忙,请稍后重试");
    }
}

三、总结

最佳实践总结

原则说明示例
级别正确debug调试、info业务流程、warn异常可恢复、error系统错误循环内用debug,关键节点用info
参数占位使用{}占位符,避免字符串拼接log.info("user: {}", user)
异常记录必须传入异常对象,输出堆栈log.error("错误", e)
异步输出生产环境必须配置asyncappender使用disruptor提升性能
链路追踪使用mdc传递traceid全链路可追踪
敏感脱敏密码、手机号等必须脱敏实现自定义脱敏工具
日志开关使用isdebugenabled()避免无效序列化if(log.isdebugenabled()){...}
合理采样高频日志需采样1%或千分之一

性能优化要点

  1. 异步日志:asyncappender + disruptor,qps提升5-10倍
  2. 条件日志:使用log.isdebugenabled()避免参数计算
  3. 合理级别:生产环境建议info,debug仅开发/测试环境
  4. 避免打印大对象:大list/大json需要截断或只打印长度

监控告警建议

  • error日志数量突增 → 钉钉/企微告警
  • 慢请求日志(>3s) → 性能监控
  • 特定业务日志(登录失败频繁) → 安全告警

日志规范checklist

  • 禁止使用system.out.println()
  • 禁止打印密码、token等敏感信息
  • 日志消息清晰,包含关键业务标识(userid、orderid)
  • 异常日志必须包含堆栈信息
  • 关键业务操作必须有业务日志
  • 日志文件配置滚动策略和保留时长(建议30天)
  • 生产环境关闭debug日志

通过以上方案,可以实现生产级的日志系统,既保证性能又便于问题排查和监控告警。

以上就是springboot中构建生产级日志系统的优雅指南的详细内容,更多关于springboot日志系统的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com