一、统一日志格式配置策略
1.1 基本原理
统一的日志格式是团队协作的基础,可以提高日志的可读性和可分析性。
springboot允许开发者自定义日志输出格式,包括时间戳、日志级别、线程信息、类名和消息内容等。
1.2 实现方式
1.2.1 配置文件方式
在application.properties
或application.yml
中定义日志格式:
# application.properties # 控制台日志格式 logging.pattern.console=%clr(%d{yyyy-mm-dd hh:mm:ss.sss}){faint} %clr(${log_level_pattern:-%5p}) %clr(${pid:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${log_exception_conversion_word:-%wex} # 文件日志格式 logging.pattern.file=%d{yyyy-mm-dd hh:mm:ss.sss} ${log_level_pattern:-%5p} ${pid:- } --- [%t] %-40.40logger{39} : %m%n${log_exception_conversion_word:-%wex}
yaml格式配置:
logging: pattern: console: "%clr(%d{yyyy-mm-dd hh:mm:ss.sss}){faint} %clr(${log_level_pattern:-%5p}) %clr(${pid:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${log_exception_conversion_word:-%wex}" file: "%d{yyyy-mm-dd hh:mm:ss.sss} ${log_level_pattern:-%5p} ${pid:- } --- [%t] %-40.40logger{39} : %m%n${log_exception_conversion_word:-%wex}"
1.2.2 自定义logback配置
对于更复杂的配置,可以使用logback-spring.xml
:
<?xml version="1.0" encoding="utf-8"?> <configuration> <property name="console_log_pattern" value="%d{yyyy-mm-dd hh:mm:ss.sss} [%thread] %-5level %logger{50} - %msg%n"/> <property name="file_log_pattern" value="%d{yyyy-mm-dd hh:mm:ss.sss} [%thread] %-5level %logger{50} - %msg%n"/> <appender name="console" class="ch.qos.logback.core.consoleappender"> <encoder> <pattern>${console_log_pattern}</pattern> <charset>utf-8</charset> </encoder> </appender> <appender name="file" class="ch.qos.logback.core.rolling.rollingfileappender"> <file>logs/application.log</file> <encoder> <pattern>${file_log_pattern}</pattern> <charset>utf-8</charset> </encoder> <rollingpolicy class="ch.qos.logback.core.rolling.sizeandtimebasedrollingpolicy"> <filenamepattern>logs/archived/application.%d{yyyy-mm-dd}.%i.log</filenamepattern> <maxfilesize>10mb</maxfilesize> <maxhistory>30</maxhistory> <totalsizecap>3gb</totalsizecap> </rollingpolicy> </appender> <root level="info"> <appender-ref ref="console" /> <appender-ref ref="file" /> </root> </configuration>
1.2.3 json格式日志配置
对于需要集中式日志分析的系统,配置json格式日志更有利于日志处理:
<dependency> <groupid>net.logstash.logback</groupid> <artifactid>logstash-logback-encoder</artifactid> <version>7.2</version> </dependency>
<appender name="json_file" class="ch.qos.logback.core.rolling.rollingfileappender"> <file>logs/application.json</file> <encoder class="net.logstash.logback.encoder.logstashencoder"> <includemdckeyname>requestid</includemdckeyname> <includemdckeyname>userid</includemdckeyname> <customfields>{"application":"my-service","environment":"${environment:-development}"}</customfields> </encoder> <rollingpolicy class="ch.qos.logback.core.rolling.sizeandtimebasedrollingpolicy"> <filenamepattern>logs/archived/application.%d{yyyy-mm-dd}.%i.json</filenamepattern> <maxfilesize>10mb</maxfilesize> <maxhistory>30</maxhistory> <totalsizecap>3gb</totalsizecap> </rollingpolicy> </appender>
1.3 最佳实践
- 环境区分:为不同环境配置不同的日志格式(开发环境可读性高,生产环境机器可解析)
<springprofile name="dev"> <!-- 开发环境配置 --> <appender name="console" class="ch.qos.logback.core.consoleappender"> <encoder> <pattern>%d{hh:mm:ss.sss} %highlight(%-5level) %cyan(%logger{15}) - %msg%n</pattern> </encoder> </appender> </springprofile> <springprofile name="prod"> <!-- 生产环境配置 --> <appender name="json_console" class="ch.qos.logback.core.consoleappender"> <encoder class="net.logstash.logback.encoder.logstashencoder"/> </appender> </springprofile>
- 添加关键信息:确保日志中包含足够的上下文信息
%d{yyyy-mm-dd hh:mm:ss.sss} [%x{requestid}] [%x{userid}] %-5level [%thread] %logger{36} - %msg%n
- 注意敏感信息:避免记录密码、令牌等敏感信息,必要时进行脱敏处理
二、分级日志策略
2.1 基本原理
合理使用日志级别可以帮助区分不同重要程度的信息,便于问题定位和系统监控。
springboot支持标准的日志级别:trace、debug、info、warn、error。
2.2 实现方式
2.2.1 配置不同包的日志级别
# 全局日志级别 logging.level.root=info # 特定包的日志级别 logging.level.org.springframework.web=debug logging.level.org.hibernate=error logging.level.com.mycompany.app=debug
2.2.2 基于环境的日志级别配置
# application.yml spring: profiles: active: dev --- spring: config: activate: on-profile: dev logging: level: root: info com.mycompany.app: debug org.springframework: info --- spring: config: activate: on-profile: prod logging: level: root: warn com.mycompany.app: info org.springframework: warn
2.2.3 编程式日志级别管理
@restcontroller @requestmapping("/api/logs") public class loggingcontroller { @autowired private loggingsystem loggingsystem; @putmapping("/level/{package}/{level}") public void changeloglevel( @pathvariable("package") string packagename, @pathvariable("level") string level) { loglevel loglevel = loglevel.valueof(level.touppercase()); loggingsystem.setloglevel(packagename, loglevel); } }
2.3 日志级别使用规范
建立清晰的日志级别使用规范对团队协作至关重要:
- error:系统错误、应用崩溃、服务不可用等严重问题
try { // 业务操作 } catch (exception e) { log.error("failed to process payment for order: {}", orderid, e); throw new paymentprocessingexception("payment processing failed", e); }
- warn:不影响当前功能但需要注意的问题
if (retrycount > maxretries / 2) { log.warn("high number of retries detected for operation: {}, current retry: {}/{}", operationtype, retrycount, maxretries); }
- info:重要业务流程、系统状态变更等信息
log.info("order {} has been successfully processed with {} items", order.getid(), order.getitems().size());
- debug:调试信息,详细的处理流程
log.debug("processing product with id: {}, name: {}, category: {}", product.getid(), product.getname(), product.getcategory());
- trace:最详细的追踪信息,一般用于框架内部
log.trace("method execution path: class={}, method={}, params={}", classname, methodname, arrays.tostring(args));
2.4 最佳实践
- 默认使用info级别:生产环境默认使用info级别,开发环境可使用debug
- 合理划分包结构:按功能或模块划分包,便于精细控制日志级别
- 避免日志爆炸:谨慎使用debug和trace级别,避免产生大量无用日志
- 条件日志:使用条件判断减少不必要的字符串拼接开销
// 推荐方式 if (log.isdebugenabled()) { log.debug("complex calculation result: {}", calculatecomplexresult()); } // 避免这样使用 log.debug("complex calculation result: " + calculatecomplexresult());
三、日志切面实现策略
3.1 基本原理
使用aop(面向切面编程)可以集中处理日志记录,避免在每个方法中手动编写重复的日志代码。尤其适合api调用日志、方法执行时间统计等场景。
3.2 实现方式
3.2.1 基础日志切面
@aspect @component @slf4j public class loggingaspect { @pointcut("execution(* com.mycompany.app.service.*.*(..))") public void servicelayer() {} @around("servicelayer()") public object logmethodexecution(proceedingjoinpoint joinpoint) throws throwable { string classname = joinpoint.getsignature().getdeclaringtypename(); string methodname = joinpoint.getsignature().getname(); log.info("executing: {}.{}", classname, methodname); long starttime = system.currenttimemillis(); try { object result = joinpoint.proceed(); long executiontime = system.currenttimemillis() - starttime; log.info("executed: {}.{} in {} ms", classname, methodname, executiontime); return result; } catch (exception e) { log.error("exception in {}.{}: {}", classname, methodname, e.getmessage(), e); throw e; } } }
3.2.2 api请求响应日志切面
@aspect @component @slf4j public class apiloggingaspect { @pointcut("@annotation(org.springframework.web.bind.annotation.requestmapping) || " + "@annotation(org.springframework.web.bind.annotation.getmapping) || " + "@annotation(org.springframework.web.bind.annotation.postmapping) || " + "@annotation(org.springframework.web.bind.annotation.putmapping) || " + "@annotation(org.springframework.web.bind.annotation.deletemapping)") public void apimethods() {} @around("apimethods()") public object logapicall(proceedingjoinpoint joinpoint) throws throwable { httpservletrequest request = ((servletrequestattributes) requestcontextholder .currentrequestattributes()).getrequest(); string requesturi = request.getrequesturi(); string httpmethod = request.getmethod(); string clientip = request.getremoteaddr(); log.info("api request - method: {} uri: {} client: {}", httpmethod, requesturi, clientip); long starttime = system.currenttimemillis(); try { object result = joinpoint.proceed(); long duration = system.currenttimemillis() - starttime; log.info("api response - method: {} uri: {} duration: {} ms status: success", httpmethod, requesturi, duration); return result; } catch (exception e) { long duration = system.currenttimemillis() - starttime; log.error("api response - method: {} uri: {} duration: {} ms status: error message: {}", httpmethod, requesturi, duration, e.getmessage(), e); throw e; } } }
3.2.3 自定义注解实现有选择的日志记录
@retention(retentionpolicy.runtime) @target({elementtype.method}) public @interface logexecutiontime { string description() default ""; }
@aspect @component @slf4j public class customlogaspect { @around("@annotation(logexecutiontime)") public object logexecutiontime(proceedingjoinpoint joinpoint, logexecutiontime logexecutiontime) throws throwable { string description = logexecutiontime.description(); string methodname = joinpoint.getsignature().getname(); log.info("starting {} - {}", methodname, description); long starttime = system.currenttimemillis(); try { object result = joinpoint.proceed(); long executiontime = system.currenttimemillis() - starttime; log.info("completed {} - {} in {} ms", methodname, description, executiontime); return result; } catch (exception e) { long executiontime = system.currenttimemillis() - starttime; log.error("failed {} - {} after {} ms: {}", methodname, description, executiontime, e.getmessage(), e); throw e; } } }
使用示例:
@service public class orderservice { @logexecutiontime(description = "process order payment") public paymentresult processpayment(order order) { // 处理支付逻辑 } }
3.3 最佳实践
- 合理定义切点:避免过于宽泛的切点定义,防止产生过多日志
- 注意性能影响:记录详细参数和结果可能带来性能开销,需权衡取舍
- 异常处理:确保日志切面本身不会抛出异常,影响主业务流程
- 避免敏感信息:敏感数据进行脱敏处理后再记录
// 敏感信息脱敏示例 private string maskcardnumber(string cardnumber) { if (cardnumber == null || cardnumber.length() < 8) { return "***"; } return "******" + cardnumber.substring(cardnumber.length() - 4); }
四、mdc上下文跟踪策略
4.1 基本原理
mdc (mapped diagnostic context) 是一种用于存储请求级别上下文信息的工具,它可以在日志框架中保存和传递这些信息,特别适合分布式系统中的请求跟踪。
4.2 实现方式
4.2.1 配置mdc过滤器
@component @order(ordered.highest_precedence) public class mdcloggingfilter extends onceperrequestfilter { @override protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain) throws servletexception, ioexception { try { // 生成唯一请求id string requestid = uuid.randomuuid().tostring().replace("-", ""); mdc.put("requestid", requestid); // 添加用户信息(如果有) authentication authentication = securitycontextholder.getcontext().getauthentication(); if (authentication != null && authentication.isauthenticated()) { mdc.put("userid", authentication.getname()); } // 添加请求信息 mdc.put("clientip", request.getremoteaddr()); mdc.put("useragent", request.getheader("user-agent")); mdc.put("httpmethod", request.getmethod()); mdc.put("requesturi", request.getrequesturi()); // 设置响应头,便于客户端跟踪 response.setheader("x-request-id", requestid); filterchain.dofilter(request, response); } finally { // 清理mdc上下文,防止内存泄漏 mdc.clear(); } } }
4.2.2 日志格式中包含mdc信息
<property name="console_log_pattern" value="%d{yyyy-mm-dd hh:mm:ss.sss} [%x{requestid}] [%x{userid}] %-5level [%thread] %logger{36} - %msg%n"/>
4.2.3 分布式追踪集成
与spring cloud sleuth和zipkin集成,实现全链路追踪:
<dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-sleuth</artifactid> </dependency> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-sleuth-zipkin</artifactid> </dependency>
spring.application.name=my-service spring.sleuth.sampler.probability=1.0 spring.zipkin.base-url=http://localhost:9411
4.2.4 手动管理mdc上下文
@service public class backgroundjobservice { private static final logger log = loggerfactory.getlogger(backgroundjobservice.class); @async public completablefuture<void> processjob(string jobid, map<string, string> context) { // 保存原有mdc上下文 map<string, string> previouscontext = mdc.getcopyofcontextmap(); try { // 设置新的mdc上下文 mdc.put("jobid", jobid); if (context != null) { context.foreach(mdc::put); } log.info("starting background job processing"); // 执行业务逻辑 // ... log.info("completed background job processing"); return completablefuture.completedfuture(null); } finally { // 恢复原有mdc上下文或清除 if (previouscontext != null) { mdc.setcontextmap(previouscontext); } else { mdc.clear(); } } } }
4.3 最佳实践
- 唯一请求标识:为每个请求生成唯一id,便于追踪完整请求链路
- 传递mdc上下文:在异步处理和线程池中正确传递mdc上下文
- 合理选择mdc信息:记录有价值的上下文信息,但避免过多信息造成日志膨胀
- 与分布式追踪结合:与sleuth、zipkin等工具结合,提供完整的分布式追踪能力
// 自定义线程池配置,传递mdc上下文 @configuration public class asyncconfig implements asyncconfigurer { @override public executor getasyncexecutor() { threadpooltaskexecutor executor = new threadpooltaskexecutor(); executor.setcorepoolsize(5); executor.setmaxpoolsize(10); executor.setqueuecapacity(25); executor.setthreadnameprefix("myasync-"); // 包装原始executor,传递mdc上下文 executor.settaskdecorator(runnable -> { map<string, string> contextmap = mdc.getcopyofcontextmap(); return () -> { try { if (contextmap != null) { mdc.setcontextmap(contextmap); } runnable.run(); } finally { mdc.clear(); } }; }); executor.initialize(); return executor; } }
五、异步日志策略
5.1 基本原理
在高性能系统中,同步记录日志可能成为性能瓶颈,特别是在i/o性能受限的环境下。
异步日志通过将日志操作从主线程中分离,可以显著提升系统性能。
5.2 实现方式
5.2.1 logback异步配置
<configuration> <!-- 定义日志内容和格式 --> <appender name="file" class="ch.qos.logback.core.rolling.rollingfileappender"> <!-- 配置详情... --> </appender> <!-- 异步appender --> <appender name="async" class="ch.qos.logback.classic.asyncappender"> <appender-ref ref="file" /> <queuesize>512</queuesize> <discardingthreshold>0</discardingthreshold> <includecallerdata>false</includecallerdata> <neverblock>false</neverblock> </appender> <root level="info"> <appender-ref ref="async" /> </root> </configuration>
5.2.2 log4j2异步配置
添加依赖:
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-log4j2</artifactid> </dependency> <dependency> <groupid>com.lmax</groupid> <artifactid>disruptor</artifactid> <version>3.4.4</version> </dependency>
配置log4j2:
<configuration status="warn"> <appenders> <console name="console" target="system_out"> <patternlayout pattern="%d{hh:mm:ss.sss} [%t] %-5level %logger{36} - %msg%n"/> </console> <rollingfile name="rollingfile" filename="logs/app.log" filepattern="logs/app-%d{mm-dd-yyyy}-%i.log.gz"> <patternlayout pattern="%d{hh:mm:ss.sss} [%t] %-5level %logger{36} - %msg%n"/> <policies> <timebasedtriggeringpolicy /> <sizebasedtriggeringpolicy size="10 mb"/> </policies> <defaultrolloverstrategy max="20"/> </rollingfile> <!-- 异步appender --> <async name="asyncfile"> <appenderref ref="rollingfile"/> <buffersize>1024</buffersize> </async> </appenders> <loggers> <root level="info"> <appenderref ref="console"/> <appenderref ref="asyncfile"/> </root> </loggers> </configuration>
5.2.3 性能优化配置
针对log4j2进行更高级的性能优化:
<configuration status="warn" packages="com.mycompany.logging"> <properties> <property name="log_pattern">%d{yyyy-mm-dd hh:mm:ss.sss} [%t] %-5level %logger{36} - %msg%n</property> </properties> <appenders> <!-- 使用mappedfile提高i/o性能 --> <rollingrandomaccessfile name="rollingfile" filename="logs/app.log" filepattern="logs/app-%d{mm-dd-yyyy}-%i.log.gz"> <patternlayout pattern="${log_pattern}"/> <policies> <timebasedtriggeringpolicy /> <sizebasedtriggeringpolicy size="25 mb"/> </policies> <defaultrolloverstrategy max="20"/> </rollingrandomaccessfile> <!-- 使用更高性能的async配置 --> <async name="asyncfile" buffersize="2048"> <appenderref ref="rollingfile"/> <disruptorblockingqueue /> </async> </appenders> <loggers> <!-- 降低某些高频日志的级别 --> <logger name="org.hibernate.sql" level="debug" additivity="false"> <appenderref ref="asyncfile" level="debug"/> </logger> <root level="info"> <appenderref ref="asyncfile"/> </root> </loggers> </configuration>
5.2.4 自定义异步日志记录器
对于特殊需求,可以实现自定义的异步日志记录器:
@component public class asynclogger { private static final logger log = loggerfactory.getlogger(asynclogger.class); private final executorservice logexecutor; public asynclogger() { this.logexecutor = executors.newsinglethreadexecutor(r -> { thread thread = new thread(r, "async-logger"); thread.setdaemon(true); return thread; }); // 确保应用关闭时处理完所有日志 runtime.getruntime().addshutdownhook(new thread(() -> { logexecutor.shutdown(); try { if (!logexecutor.awaittermination(5, timeunit.seconds)) { log.warn("asynclogger executor did not terminate in the expected time."); } } catch (interruptedexception e) { thread.currentthread().interrupt(); } })); } public void info(string format, object... arguments) { logexecutor.submit(() -> log.info(format, arguments)); } public void warn(string format, object... arguments) { logexecutor.submit(() -> log.warn(format, arguments)); } public void error(string format, object... arguments) { throwable throwable = extractthrowable(arguments); if (throwable != null) { logexecutor.submit(() -> log.error(format, arguments)); } else { logexecutor.submit(() -> log.error(format, arguments)); } } private throwable extractthrowable(object[] arguments) { if (arguments != null && arguments.length > 0) { object lastarg = arguments[arguments.length - 1]; if (lastarg instanceof throwable) { return (throwable) lastarg; } } return null; } }
5.3 最佳实践
- 队列大小设置:根据系统吞吐量和内存情况设置合理的队列大小
- 丢弃策略配置:在高负载情况下,可以考虑丢弃低优先级的日志
<asyncappender name="async" queuesize="512" discardingthreshold="20"> <!-- 当队列剩余容量低于20%时,会丢弃trace, debug和info级别的日志 --> </asyncappender>
- 异步日志的注意事项:
- 异步日志可能导致异常堆栈信息不完整
- 系统崩溃时可能丢失最后一批日志
- 需要权衡性能和日志完整性
- 合理使用同步与异步:
- 关键操作日志(如金融交易)使用同步记录确保可靠性
- 高频但不关键的日志(如访问日志)使用异步记录提高性能
// 同步记录关键业务日志 log.info("transaction completed: id={}, amount={}, status={}", transaction.getid(), transaction.getamount(), transaction.getstatus()); // 异步记录高频统计日志 asynclogger.info("api usage stats: endpoint={}, count={}, avgresponsetime={}ms", endpoint, requestcount, avgresponsetime);
另外,性能要求较高的应用推荐使用log4j2的异步模式,性能远高于logback。
六、总结
这些策略不是相互排斥的,而是可以结合使用,共同构建完整的日志体系。
在实际应用中,应根据项目规模、团队情况和业务需求,选择合适的日志规范策略组合。
好的日志实践不仅能帮助开发者更快地定位和解决问题,还能为系统性能优化和安全审计提供重要依据。
以上就是springboot中日志输出规范的五种策略的详细内容,更多关于springboot日志输出规范的资料请关注代码网其它相关文章!
发表评论