背景
在项目当中,我们经常需要打印一些日志埋点信息,这些日志埋点信息,在后续软件的运维、稳定性建设中发挥了巨大的作用:
- 问题追踪:通过埋点日志中的关键信息,帮助定位系统异常原因
- 系统监控:通过日志,监控系统的运行情况,包括性能指标、访问频率、错误等
- 数据分析:分析用户行为、系统性能和业务趋势等
- 调试:通过查看日志,帮助开发人员了解程序在执行过程中的状态和行为
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下分别打印了不同的日志信息
参考文档
【Spring Boot】深入解密Spring Boot日志:最佳实践与策略解析-腾讯云开发者社区-腾讯云 (tencent.com)