SpringBoot日志打印實踐過程
背景
在項目當(dāng)中,我們經(jīng)常需要打印一些日志埋點信息,這些日志埋點信息,在后續(xù)軟件的運維、穩(wěn)定性建設(shè)中發(fā)揮了巨大的作用:
- 問題追蹤:通過埋點日志中的關(guān)鍵信息,幫助定位系統(tǒng)異常原因
- 系統(tǒng)監(jiān)控:通過日志,監(jiān)控系統(tǒng)的運行情況,包括性能指標(biāo)、訪問頻率、錯誤等
- 數(shù)據(jù)分析:分析用戶行為、系統(tǒng)性能和業(yè)務(wù)趨勢等
- 調(diào)試:通過查看日志,幫助開發(fā)人員了解程序在執(zhí)行過程中的狀態(tài)和行為
SpringBoot整合Logback實現(xiàn)日志打印
SpringBoot默認使用Slf4j作為日志門面,并集成Logback作為日志實現(xiàn)。
要在springboot中實現(xiàn)日志打印,只需要引入下列依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
然后在配置文件中,配置對應(yīng)的日志級別:
logging: level: root: INFO
對某些特定的包,需要指定日志級別,則配置如下:
logging: level: com.example.demo: DEBUG
最后,我們創(chuàng)建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,打印結(jié)果如下:
日志打印工具類
在logback-spring.xml中,我們雖然能配置日志打印的格式,但是不夠靈活,因此,我們可以添加一個日志打印工具類,通過該工具類,來自定義項目中的日志打印格式,以方便后續(xù)更好地通過日志排查、定位問題。
首先創(chuàng)建一個日志打印抽象類,定義日志打印的格式:
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(); } }
然后創(chuàng)建日志打印實現(xiàn)類,在實現(xiàn)類中,定制實現(xiàn)日志打印的級別、分隔符等內(nèi)容
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 "<|>"; } }
同時,為了方便打印日志,創(chuàng)建一個日志打印創(chuàng)建者
package com.yang.core.infrastructure.log; public class PlatformLogger { public static AbstractLogPrinter build() { return new PlatformLogPrinter(); } }
上述內(nèi)容準備完畢后,我們在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,打印日志內(nèi)容如下:
日志分文件打印
一般情況下,我們的項目會分為不同的模塊,每一個模塊承擔(dān)不同的職責(zé),比如bussiness模塊,主要是負責(zé)業(yè)務(wù)邏輯代碼的實現(xiàn),業(yè)務(wù)邏輯編排等;web模塊主要負責(zé)http請求的接收,參數(shù)的校驗,入?yún)⑥D(zhuǎn)化為業(yè)務(wù)層入?yún)⒌?;而core模塊主要負責(zé)基礎(chǔ)能力實現(xiàn),比如持久化數(shù)據(jù)庫、領(lǐng)域服務(wù)實現(xiàn)等。
對于不同的模塊,我們希望將日志輸出到不同的文件當(dāng)中,從而協(xié)助我們后續(xù)定位問題以及建設(shè)不同模塊下的監(jiān)控,包括基礎(chǔ)服務(wù)監(jiān)控、業(yè)務(wù)成功率監(jiān)控等。
因此,我們在不同的模塊下,分別實現(xiàn)不同的日志打印工具類:
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下分別打印了不同的日志信息
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java泛型與數(shù)據(jù)庫應(yīng)用實例詳解
這篇文章主要介紹了Java泛型與數(shù)據(jù)庫應(yīng)用,結(jié)合實例形式詳細分析了java繼承泛型類實現(xiàn)增刪改查操作相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2019-08-08java編程實現(xiàn)獲取服務(wù)器IP地址及MAC地址的方法
這篇文章主要介紹了java編程實現(xiàn)獲取機器IP地址及MAC地址的方法,實例分析了Java分別針對單網(wǎng)卡及多網(wǎng)卡的情況下獲取服務(wù)器IP地址與MAC地址的相關(guān)技巧,需要的朋友可以參考下2015-11-11