一、需求描述
在实际开发中,日志系统需要满足以下需求:
- 区分日志级别: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
- secret3. 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%或千分之一 |
性能优化要点
- 异步日志:asyncappender + disruptor,qps提升5-10倍
- 条件日志:使用
log.isdebugenabled()避免参数计算 - 合理级别:生产环境建议info,debug仅开发/测试环境
- 避免打印大对象:大list/大json需要截断或只打印长度
监控告警建议
- error日志数量突增 → 钉钉/企微告警
- 慢请求日志(>3s) → 性能监控
- 特定业务日志(登录失败频繁) → 安全告警
日志规范checklist
- 禁止使用
system.out.println() - 禁止打印密码、token等敏感信息
- 日志消息清晰,包含关键业务标识(userid、orderid)
- 异常日志必须包含堆栈信息
- 关键业务操作必须有业务日志
- 日志文件配置滚动策略和保留时长(建议30天)
- 生产环境关闭debug日志
通过以上方案,可以实现生产级的日志系统,既保证性能又便于问题排查和监控告警。
以上就是springboot中构建生产级日志系统的优雅指南的详细内容,更多关于springboot日志系统的资料请关注代码网其它相关文章!
发表评论