背景
在项目当中,我们经常需要打印一些日志埋点信息,这些日志埋点信息,在后续软件的运维、稳定性建设中发挥了巨大的作用:
- 问题追踪:通过埋点日志中的关键信息,帮助定位系统异常原因
- 系统监控:通过日志,监控系统的运行情况,包括性能指标、访问频率、错误等
- 数据分析:分析用户行为、系统性能和业务趋势等
- 调试:通过查看日志,帮助开发人员了解程序在执行过程中的状态和行为
SpringBoot整合Logback实现日志打印
SpringBoot默认使用Slf4j作为日志门面,并集成Logback作为日志实现。要在springboot中实现日志打印,只需要引入下列依赖:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
|
然后在配置文件中,配置对应的日志级别:
1 2 3
| logging: level: root: INFO
|
对某些特定的包,需要指定日志级别,则配置如下:
1 2 3
| logging: level: com.example.demo: DEBUG
|
最后,我们创建logback-spring.xml,来自定义日志的配置信息,包括日志输出文件、日志格式等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?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来打印日志信息即可,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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中,我们虽然能配置日志打印的格式,但是不够灵活,因此,我们可以添加一个日志打印工具类,通过该工具类,来自定义项目中的日志打印格式,以方便后续更好地通过日志排查、定位问题。
首先创建一个日志打印抽象类,定义日志打印的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| 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(); } }
|
然后创建日志打印实现类,在实现类中,定制实现日志打印的级别、分隔符等内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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 "<|>"; } }
|
同时,为了方便打印日志,创建一个日志打印创建者
1 2 3 4 5 6 7 8
| package com.yang.core.infrastructure.log;
public class PlatformLogger {
public static AbstractLogPrinter build() { return new PlatformLogPrinter(); } }
|
上述内容准备完毕后,我们在controller中,使用PlatformLogger来打印日志,修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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模块主要负责基础能力实现,比如持久化数据库、领域服务实现等。
对于不同的模块,我们希望将日志输出到不同的文件当中,从而协助我们后续定位问题以及建设不同模块下的监控,包括基础服务监控、业务成功率监控等。
因此,我们在不同的模块下,分别实现不同的日志打印工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| 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文件,将不同的日志打印工具类,输出到不同的日志文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| <?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模块下,添加埋点日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| // 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下分别打印了不同的日志信息
参考文档
【Spring Boot】深入解密Spring Boot日志:最佳实践与策略解析-腾讯云开发者社区-腾讯云 (tencent.com)