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 {@Autowiredprivate 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());}}@Overridepublic 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 {@Autowiredprivate 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 {@Overridepublic void printBizLog() {log.info(commonContent());}@Overridepublic void printErrorLog() {if (this.e != null) {log.error(commonContent(), e);} else {log.error(commonContent());}}@Overridepublic 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 {@Overridepublic void printBizLog() {log.info(commonContent());}@Overridepublic void printErrorLog() {if (this.e != null) {log.error(commonContent(), e);} else {log.error(commonContent());}}@Overridepublic 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模块
@Overridepublic 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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/438425.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

移动硬盘传输中断后无法识别:问题解析与数据恢复策略

一、移动硬盘传输中断后的无法识别现象 在日常的数据传输过程中&#xff0c;移动硬盘作为便携式的存储介质&#xff0c;扮演着举足轻重的角色。然而&#xff0c;当传输过程被意外中断&#xff0c;且移动硬盘随后无法被系统识别时&#xff0c;这无疑会给用户带来极大的困扰。你…

Stable Diffusion绘画 | 插件-Deforum:动态视频生成(上篇)

Deforum 与 AnimateDiff 不太一样&#xff0c; AnimateDiff 是生成丝滑变化视频的&#xff0c;而 Deforum 的丝滑程度远远没有 AnimateDiff 好。 它是根据对比前面一帧的画面&#xff0c;然后不断生成新的相似图片&#xff0c;来组合成一个完整的视频。 Deforum 的优点在于可…

Pikachu-Sql Inject-宽字节注入

基本概念 宽字节是相对于ascII这样单字节而言的&#xff1b;像 GB2312、GBK、GB18030、BIG5、Shift_JIS 等这些都是常说的宽字节&#xff0c;实际上只有两字节 GBK 是一种多字符的编码&#xff0c;通常来说&#xff0c;一个 gbk 编码汉字&#xff0c;占用2个字节。一个…

【一文理解】conda install pip install 区别

大部分情况下&#xff0c;conda install & pip install 二者安装的package都可以正常work&#xff0c;但是混装多种package后容易版本冲突&#xff0c;出现各种报错。 目录 检查机制 支持语言 库的位置 环境隔离 编译情况 检查机制 conda有严格的检查机制&#xff0c…

【C++】模拟实现红黑树

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:实战项目集 ⚙️操作环境:Visual Studio 2022 目录 一.了解项目功能 二.逐步实现项目功能模块及其逻辑详解 &#x1f4cc;实现RBTreeNode类模板 &#x1f38f;构造RBTreeNode类成员变量 &#x1f38f;实现RBTreeNode类构…

图解C#高级教程(二):事件

在现实生活当中&#xff0c;有一些事情发生时&#xff0c;会连带另一些事情的发生。例如&#xff0c;当某国的总统发生换届时&#xff0c;不同党派会表现出不同的行为。两者构成了“因果”关系&#xff0c;因为发生了A&#xff0c;所以发生了B。在编程语言当中&#xff0c;具有…

Android问题笔记五十:构建错误-AAPT2 aapt2-7.0.2-7396180-windows Daemon

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

Visual Studio 字体与主题推荐

个人推荐&#xff0c;仅供参考&#xff1a; 主题&#xff1a;One Monokai VS Theme 链接&#xff1a;One Monokai VS Theme - Visual Studio Marketplacehttps://marketplace.visualstudio.com/items?itemNameazemoh.onemonokai 效果&#xff1a; 字体&#xff1a;JetBrain…

SpringBoot项目请求不中断动态更新代码

在开发中&#xff0c;有时候不停机动态更新代码热部署是一项至关重要的功能&#xff0c;它可以在请求不中断的情况下下更新代码。这种方式不仅提高了开发效率&#xff0c;还能加速测试和调试过程。本文将详细介绍如何在 Spring Boot 项目在Linux系统中实现热部署&#xff0c;特…

《业务三板斧:定目标、抓过程、拿结果》读书笔记1

这个书是24年新书&#xff0c;来自阿里系的人的作品&#xff0c;还可以。今天先看前沿部分的精彩部分&#xff1a; 我们在服务企业的过程中&#xff0c;发现了一个常见的管理现象&#xff1a;管理者自 己承担了团队里重要的项目&#xff0c;把风险和压力都集中在自己身上。因 此…

报刊订阅系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;报刊类型管理&#xff0c;报刊信息管理&#xff0c;报刊订阅管理&#xff0c;订阅发送管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;报刊信息&a…

<<迷雾>> 第7章 会变魔术的触发器(1)--连着两个按键开关的逻辑电路 示例电路

info::操作说明 鼠标单击开关切换开合状态 A 能使灯点亮并保持; B 则点亮的灯熄灭. 注: 此处使用的是 按钮开关, 松开鼠标后开关会自己断开, 类似于手机和电脑上的电源按钮 因系统原因, 此类开关与普通开关在外观上并无差别. primary::在线交互操作链接 https://cc.xiaogd.net/…

【Android】获取备案所需的公钥以及签名MD5值

目录 重要前提 获取签名MD5值 获取公钥 重要前提 生成jks文件以及gradle配置应用该文件。具体步骤请参考我这篇文章&#xff1a;【Android】配置Gradle打包apk的环境_generate signed bundle or apk-CSDN博客 你只需要从头看到该文章的配置build.gradle&#xff08;app&…

HTML流光爱心

文章目录 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色动态爱心9HTML跳动的爱心&#xff08;双心版&#xff09;1…

回归预测 | Matlab基于POA-SVR鹈鹕算法优化支持向量机的数据多输入单输出回归预测

回归预测 | Matlab基于POA-SVR鹈鹕算法优化支持向量机的数据多输入单输出回归预测 目录 回归预测 | Matlab基于POA-SVR鹈鹕算法优化支持向量机的数据多输入单输出回归预测预测效果基本描述程序设计参考资料 预测效果 基本描述 1.Matlab基于POA-SVR鹈鹕算法优化支持向量机的数据…

检查jar冲突,查找存在相同class的jar

写在前面 本文看下如何查找jar冲突&#xff0c;即查找哪些jar包中存在相同的class。如果是存在相同jar的不同版本&#xff0c;基本一眼就能看出来&#xff0c;然后结合maven的依赖关系将其剔除掉即可&#xff0c;但是当你遇到了有人手动拷贝某些class到jar包中导致冲突的情况时…

wpf实现新用户页面引导

第一步 第二部 部分代码: private void show(int xh, FrameworkElement fe, string con, Visibility vis Visibility.Visible) {Point point fe.TransformToAncestor(Window.GetWindow(fe)).Transform(new Point(0, 0));//获取控件坐标点RectangleGeometry rg new Rectangl…

FP7209: 用于紫外线消毒灯的 升压LED恒流驱动芯片

现在社会对于居家消毒也越发重视起来。而居家消毒除了75%浓度酒精及各类消毒液外&#xff0c;利用紫外线灯给衣物表面、房间消毒也是一种很好的选择。FP7209 定位于低压线性恒流驱动&#xff0c;精度高、外围电路简单、使用方便且可靠性高&#xff0c;更可广泛应用于商业照明系…

鸿蒙harmonyos next flutter通信之BasicMessageChannel获取app版本号

本文将通过BasicMessageChannel获取app版本号&#xff0c;以此来演练BasicMessageChannel用法。 建立channel flutter代码&#xff1a; //建立通道 BasicMessageChannel basicMessageChannel BasicMessageChannel("com.xmg.basicMessageChannel",StringCodec());…

STM32自动下载电路分享及注意事项

文章目录 简介ISP下载启动配置 USB转串口芯片CH340C手动isp下载自动isp下载RTS、DTR电平变化分析注意事项 简介 在嵌入式开发中&#xff0c;使用STM32下载程序&#xff0c;可以通过仿真器下载&#xff0c;也可以通过串口下载。在stm32串口下载时&#xff0c;我们需要手动配置启…