SpringBoot日志打印實(shí)踐過程
背景
在項(xiàng)目當(dāng)中,我們經(jīng)常需要打印一些日志埋點(diǎn)信息,這些日志埋點(diǎn)信息,在后續(xù)軟件的運(yùn)維、穩(wěn)定性建設(shè)中發(fā)揮了巨大的作用:
- 問題追蹤:通過埋點(diǎn)日志中的關(guān)鍵信息,幫助定位系統(tǒng)異常原因
- 系統(tǒng)監(jiān)控:通過日志,監(jiān)控系統(tǒng)的運(yùn)行情況,包括性能指標(biāo)、訪問頻率、錯(cuò)誤等
- 數(shù)據(jù)分析:分析用戶行為、系統(tǒng)性能和業(yè)務(wù)趨勢(shì)等
- 調(diào)試:通過查看日志,幫助開發(fā)人員了解程序在執(zhí)行過程中的狀態(tài)和行為
SpringBoot整合Logback實(shí)現(xiàn)日志打印
SpringBoot默認(rèn)使用Slf4j作為日志門面,并集成Logback作為日志實(shí)現(xiàn)。
要在springboot中實(shí)現(xiàn)日志打印,只需要引入下列依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>
然后在配置文件中,配置對(duì)應(yīng)的日志級(jí)別:
logging: level: root: INFO
對(duì)某些特定的包,需要指定日志級(jí)別,則配置如下:
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>
然后,我們?cè)谛枰蛴∪罩镜念?,加上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中,我們雖然能配置日志打印的格式,但是不夠靈活,因此,我們可以添加一個(gè)日志打印工具類,通過該工具類,來自定義項(xiàng)目中的日志打印格式,以方便后續(xù)更好地通過日志排查、定位問題。
首先創(chuàng)建一個(gè)日志打印抽象類,定義日志打印的格式:
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)建日志打印實(shí)現(xiàn)類,在實(shí)現(xiàn)類中,定制實(shí)現(xiàn)日志打印的級(jí)別、分隔符等內(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 "<|>"; } }
同時(shí),為了方便打印日志,創(chuàng)建一個(gè)日志打印創(chuàng)建者
package com.yang.core.infrastructure.log; public class PlatformLogger { public static AbstractLogPrinter build() { return new PlatformLogPrinter(); } }
上述內(nèi)容準(zhǔn)備完畢后,我們?cè)赾ontroller中,使用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; } }
啟動(dòng)項(xiàng)目,分別訪問queryById和testError,打印日志內(nèi)容如下:
日志分文件打印
一般情況下,我們的項(xiàng)目會(huì)分為不同的模塊,每一個(gè)模塊承擔(dān)不同的職責(zé),比如bussiness模塊,主要是負(fù)責(zé)業(yè)務(wù)邏輯代碼的實(shí)現(xiàn),業(yè)務(wù)邏輯編排等;web模塊主要負(fù)責(zé)http請(qǐng)求的接收,參數(shù)的校驗(yàn),入?yún)⑥D(zhuǎn)化為業(yè)務(wù)層入?yún)⒌?;而core模塊主要負(fù)責(zé)基礎(chǔ)能力實(shí)現(xiàn),比如持久化數(shù)據(jù)庫、領(lǐng)域服務(wù)實(shí)現(xiàn)等。
對(duì)于不同的模塊,我們希望將日志輸出到不同的文件當(dāng)中,從而協(xié)助我們后續(xù)定位問題以及建設(shè)不同模塊下的監(jiān)控,包括基礎(chǔ)服務(wù)監(jiān)控、業(yè)務(wù)成功率監(jiān)控等。
因此,我們?cè)诓煌哪K下,分別實(shí)現(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模塊下,添加埋點(diǎn)日志
// 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; }
啟動(dòng)項(xiàng)目,訪問queryById接口,可以看到在web.log,business.log和platform.log下分別打印了不同的日志信息
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決Nacos集群?jiǎn)?dòng)失敗:java版本問題
這篇文章主要介紹了解決Nacos集群?jiǎn)?dòng)失敗:java版本問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06SpringBoot實(shí)現(xiàn)文件下載的限速功能
在SpringBoot項(xiàng)目中,實(shí)現(xiàn)文件下載的限速功能可以有效控制服務(wù)器帶寬的占用,并防止單個(gè)用戶消耗過多的資源,本文將通過具體的代碼示例和詳細(xì)的流程解釋,介紹如何在SpringBoot項(xiàng)目中實(shí)現(xiàn)文件下載的限速功能,需要的朋友可以參考下2024-07-07Java泛型與數(shù)據(jù)庫應(yīng)用實(shí)例詳解
這篇文章主要介紹了Java泛型與數(shù)據(jù)庫應(yīng)用,結(jié)合實(shí)例形式詳細(xì)分析了java繼承泛型類實(shí)現(xiàn)增刪改查操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-08-08Java 中一個(gè)類提供一個(gè)默認(rèn)對(duì)象的多種方法
這篇文章主要介紹了Java 中一個(gè)類提供一個(gè)默認(rèn)對(duì)象的多種方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07詳述 DB2 分頁查詢及 Java 實(shí)現(xiàn)的示例
本篇文章主要介紹了詳述 DB2 分頁查詢及 Java 實(shí)現(xiàn)的示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析
這篇文章主要介紹了Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的定時(shí)器代碼解析,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12java編程實(shí)現(xiàn)獲取服務(wù)器IP地址及MAC地址的方法
這篇文章主要介紹了java編程實(shí)現(xiàn)獲取機(jī)器IP地址及MAC地址的方法,實(shí)例分析了Java分別針對(duì)單網(wǎng)卡及多網(wǎng)卡的情況下獲取服務(wù)器IP地址與MAC地址的相關(guān)技巧,需要的朋友可以參考下2015-11-11