SpringBoot日志打印实践过程
作者:沉河不浮
背景
在项目当中,我们经常需要打印一些日志埋点信息,这些日志埋点信息,在后续软件的运维、稳定性建设中发挥了巨大的作用:
- 问题追踪:通过埋点日志中的关键信息,帮助定位系统异常原因
- 系统监控:通过日志,监控系统的运行情况,包括性能指标、访问频率、错误等
- 数据分析:分析用户行为、系统性能和业务趋势等
- 调试:通过查看日志,帮助开发人员了解程序在执行过程中的状态和行为
SpringBoot整合Logback实现日志打印
SpringBoot默认使用Slf4j作为日志门面,并集成Logback作为日志实现。
要在springboot中实现日志打印,只需要引入下列依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
然后在配置文件中,配置对应的日志级别:
logging: level: root: INFO
对某些特定的包,需要指定日志级别,则配置如下:
logging: level: com.example.demo: DEBUG
最后,我们创建logback-spring.xml,来自定义日志的配置信息,包括日志输出文件、日志格式等
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_PATH" value="logs"/> <property name="LOG_FILE" value="${LOG_PATH}/spring-boot-logger.log"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>common.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/spring-boot-logger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </configuration>
然后,我们在需要打印日志的类,加上Slf4j注解,然后使用log来打印日志信息即可,如下代码所示:
package com.yang.web.controller; import com.yang.api.common.ResultT; import com.yang.api.common.command.RegisterCommand; import com.yang.api.common.dto.UserDTO; import com.yang.api.common.facade.UserFacade; import com.yang.web.request.RegisterRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(value = "/user") @Slf4j public class UserController { @Autowired private UserFacade userFacade; @GetMapping(value = "/{id}") public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) { log.info("queryById==========="); return userFacade.getById(id); } @PostMapping(value = "/register") public ResultT<String> register(@RequestBody RegisterRequest registerRequest) { RegisterCommand registerCommand = convert2RegisterCommand(registerRequest); return userFacade.register2(registerCommand); } private RegisterCommand convert2RegisterCommand(RegisterRequest registerRequest) { RegisterCommand registerCommand = new RegisterCommand(); registerCommand.setLoginId(registerRequest.getLoginId()); registerCommand.setEmail(registerRequest.getEmail()); registerCommand.setPassword(registerRequest.getPassword()); registerCommand.setExtendMaps(registerRequest.getExtendMaps()); return registerCommand; } }
然后访问queryById,打印结果如下:
日志打印工具类
在logback-spring.xml中,我们虽然能配置日志打印的格式,但是不够灵活,因此,我们可以添加一个日志打印工具类,通过该工具类,来自定义项目中的日志打印格式,以方便后续更好地通过日志排查、定位问题。
首先创建一个日志打印抽象类,定义日志打印的格式:
package com.yang.core.infrastructure.log; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; public abstract class AbstractLogPrinter { protected String bizCode; protected List<String> params = new ArrayList<>(); protected String msg; protected Throwable e; public AbstractLogPrinter addBizCode(String bizCode) { this.bizCode = bizCode; return this; } public AbstractLogPrinter addMsg(String msg) { this.msg = msg; return this; } public AbstractLogPrinter addParam(String key, String value) { this.params.add(key); this.params.add(value); return this; } public AbstractLogPrinter addThrowable(Throwable e) { this.e = e; return this; } public abstract void printBizLog(); public abstract void printErrorLog(); public abstract String getSeparator(); public String commonContent() { StringBuilder stringBuilder = new StringBuilder(); String separator = getSeparator(); stringBuilder.append("bizCode").append(":") .append(this.bizCode).append(separator); if (!CollectionUtils.isEmpty(params)) { for (int i = 0; i < params.size(); i += 2) { stringBuilder.append(params.get(i)) .append(":") .append(params.get(i + 1)) .append(separator); } } if (StringUtils.isNotEmpty(msg)) { stringBuilder.append("msg").append(":") .append(msg).append(separator); } return stringBuilder.toString(); } }
然后创建日志打印实现类,在实现类中,定制实现日志打印的级别、分隔符等内容
package com.yang.core.infrastructure.log; import lombok.extern.slf4j.Slf4j; @Slf4j public class PlatformLogPrinter extends AbstractLogPrinter { public void printBizLog() { log.info(commonContent()); } public void printErrorLog() { if (e != null) { log.error(commonContent(), e); } else { log.error(commonContent()); } } @Override public String getSeparator() { return "<|>"; } }
同时,为了方便打印日志,创建一个日志打印创建者
package com.yang.core.infrastructure.log; public class PlatformLogger { public static AbstractLogPrinter build() { return new PlatformLogPrinter(); } }
上述内容准备完毕后,我们在controller中,使用PlatformLogger来打印日志,修改后的代码如下:
package com.yang.web.controller; import com.yang.api.common.ResultT; import com.yang.api.common.command.RegisterCommand; import com.yang.api.common.dto.UserDTO; import com.yang.api.common.facade.UserFacade; import com.yang.core.infrastructure.log.PlatformLogger; import com.yang.web.request.RegisterRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(value = "/user") public class UserController { @Autowired private UserFacade userFacade; @GetMapping(value = "/{id}") public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) { PlatformLogger.build() .addBizCode("queryById") .addParam("id", id.toString()) .addMsg("query by id") .printBizLog(); return userFacade.getById(id); } @GetMapping(value = "/error/{id}") public ResultT testError(@PathVariable("id") Integer id) { try { int i = 1 / 0; } catch (Throwable t) { PlatformLogger.build() .addBizCode("testError") .addParam("id", id.toString()) .addMsg("test error print") .addThrowable(t) .printErrorLog(); } return ResultT.fail(); } @PostMapping(value = "/register") public ResultT<String> register(@RequestBody RegisterRequest registerRequest) { RegisterCommand registerCommand = convert2RegisterCommand(registerRequest); return userFacade.register2(registerCommand); } private RegisterCommand convert2RegisterCommand(RegisterRequest registerRequest) { RegisterCommand registerCommand = new RegisterCommand(); registerCommand.setLoginId(registerRequest.getLoginId()); registerCommand.setEmail(registerRequest.getEmail()); registerCommand.setPassword(registerRequest.getPassword()); registerCommand.setExtendMaps(registerRequest.getExtendMaps()); return registerCommand; } }
启动项目,分别访问queryById和testError,打印日志内容如下:
日志分文件打印
一般情况下,我们的项目会分为不同的模块,每一个模块承担不同的职责,比如bussiness模块,主要是负责业务逻辑代码的实现,业务逻辑编排等;web模块主要负责http请求的接收,参数的校验,入参转化为业务层入参等;而core模块主要负责基础能力实现,比如持久化数据库、领域服务实现等。
对于不同的模块,我们希望将日志输出到不同的文件当中,从而协助我们后续定位问题以及建设不同模块下的监控,包括基础服务监控、业务成功率监控等。
因此,我们在不同的模块下,分别实现不同的日志打印工具类:
package com.yang.web.log; import com.yang.core.infrastructure.log.AbstractLogPrinter; public class WebLogger { public static AbstractLogPrinter build() { return new WebLogPrinter(); } } package com.yang.web.log; import com.yang.core.infrastructure.log.AbstractLogPrinter; import lombok.extern.slf4j.Slf4j; @Slf4j public class WebLogPrinter extends AbstractLogPrinter { @Override public void printBizLog() { log.info(commonContent()); } @Override public void printErrorLog() { if (this.e != null) { log.error(commonContent(), e); } else { log.error(commonContent()); } } @Override public String getSeparator() { return "<|>"; } } package com.yang.business.log; public class BusinessLogger { public static BusinessLogPrinter build() { return new BusinessLogPrinter(); } } package com.yang.business.log; import com.yang.core.infrastructure.log.AbstractLogPrinter; import lombok.extern.slf4j.Slf4j; @Slf4j public class BusinessLogPrinter extends AbstractLogPrinter { @Override public void printBizLog() { log.info(commonContent()); } @Override public void printErrorLog() { if (this.e != null) { log.error(commonContent(), e); } else { log.error(commonContent()); } } @Override public String getSeparator() { return "<|>"; } }
然后我们修改logback-spring.xml文件,将不同的日志打印工具类,输出到不同的日志文件中
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_PATH" value="logs"/> <property name="LOG_FILE" value="${LOG_PATH}/spring-boot-logger.log"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>common.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/spring-boot-logger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <appender name="PLATFORM_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>platform.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/platform-logger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <appender name="BUSINESS_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>business.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/business-logger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <appender name="WEB_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>web.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/web-logger.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> <!-- 工具类PlatformLogPrinter的logger --> <logger name="com.yang.core.infrastructure.log.PlatformLogPrinter" level="INFO" additivity="false"> <appender-ref ref="PLATFORM_FILE" /> </logger> <!-- 工具类BusinessLogPrinter的logger --> <logger name="com.yang.business.log.BusinessLogPrinter" level="INFO" additivity="false"> <appender-ref ref="BUSINESS_FILE" /> </logger> <!-- 工具类WebLogPrinter的logger --> <logger name="com.yang.web.log.WebLogPrinter" level="INFO" additivity="false"> <appender-ref ref="WEB_FILE" /> </logger> </configuration>
最后,分别在web模块、business模块和core模块下,添加埋点日志
// WEB模块 @GetMapping(value = "/{id}") public ResultT<UserDTO> queryById(@PathVariable("id") Integer id) { WebLogger.build() .addBizCode("userController_queryById") .addParam("id", id.toString()) .addMsg("query by id") .printBizLog(); return userFacade.getById(id); } // Business模块 @Override public ResultT<UserDTO> getById(Integer id) { UserQueryDomainRequest userQueryDomainRequest = new UserQueryDomainRequest.UserQueryDomainRequestBuilder() .queryMessage(id.toString()) .userQueryType(UserQueryType.ID) .build(); UserQueryDomainResponse userQueryDomainResponse = userDomainService.query(userQueryDomainRequest); List<UserAccount> userAccountList = userQueryDomainResponse.getUserAccountList(); UserDTO userDTO = null; if (!CollectionUtils.isEmpty(userAccountList)) { UserAccount userAccount = userAccountList.get(0); userDTO = userDTOConvertor.convert2DTO(userAccount); } BusinessLogger.build() .addBizCode("userFacade_getById") .addParam("id", id.toString()) .addParam("userDTO", JSONObject.toJSONString(userDTO)) .addMsg("get by id") .printBizLog(); return ResultT.success(userDTO); } // core模块 public UserQueryDomainResponse query(UserQueryDomainRequest userQueryDomainRequest) { UserQueryType userQueryType = userQueryDomainRequest.getUserQueryType(); UserDO userDO = null; switch (userQueryType) { case ID: userDO = queryById(Integer.valueOf(userQueryDomainRequest.getQueryMessage())); break; case EMAIL: userDO = queryByEmail(userQueryDomainRequest.getQueryMessage()); break; case LOGIN_ID: userDO = queryByLoginId(userQueryDomainRequest.getQueryMessage()); break; } if (userDO == null) { return new UserQueryDomainResponse(); } UserAccount userAccount = new UserAccount(); userAccount.setId(userDO.getId()); userAccount.setLoginId(userDO.getLoginId()); userAccount.setEmail(userDO.getEmail()); userAccount.setFeatureMap(FeatureUtils.convert2FeatureMap(userDO.getFeatures())); userAccount.setCreateTime(userDO.getCreateTime()); userAccount.setUpdateTime(userDO.getUpdateTime()); UserQueryDomainResponse userQueryDomainResponse = new UserQueryDomainResponse(); List<UserAccount> userAccounts = new ArrayList<>(); userAccounts.add(userAccount); userQueryDomainResponse.setUserAccountList(userAccounts); PlatformLogger.build() .addBizCode("userDomainService_query") .addParam("queryMsg", userQueryDomainRequest.getQueryMessage()) .addParam("queryType", userQueryDomainRequest.getUserQueryType().name()) .printBizLog(); return userQueryDomainResponse; }
启动项目,访问queryById接口,可以看到在web.log,business.log和platform.log下分别打印了不同的日志信息
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。