文章目录
- Java 日志详解及打印建议和应用场景
- 日志级别
- 日志内容解析
- 日志应用场景
- 常见业务自定义日志需求与代码实现
- 日志打印的建议
- 1. 选择合适的日志等级
- 2. 打印函数的入参、出参
- 3. 打印日志对象要做判空处理
- 4. 使用 Slf4j 而不是具体日志实现
- 5. 对低级别日志进行级别开关判断
- 6. 避免使用 e.printStackTrace()
- 7. 打印全部异常信息
- 8. 避免打印重复日志
- 9. 日志尽量使用英文
- 10. 在核心业务逻辑分支首行打印日志
- 11. 日志要携带上下文和链路信息
- 12. 捕获异常并记录完整的堆栈信息
- 13. 避免在日志中输出敏感信息
- 14. 日志级别与业务逻辑相匹配
- 15. 建议使用参数占位`{}`,而不是用`+`拼接
- 16. 建议使用异步的方式来输出日志
- 17. 日志文件按日期或大小分割
- 18. 合理的日志级别配置
- 19. 使用MDC(Mapped Diagnostic Context)传递上下文信息
- 20. 定期检查和分析日志文件
- 扩展
Java 日志详解及打印建议和应用场景
日志级别
Java中常见的日志级别包括TRACE、DEBUG、INFO、WARN、ERROR等,优先级别从低到高,
日志级别详情
- TRACE
- 级别最低:通常用于记录非常详细的跟踪信息,如方法调用的进入和退出、变量值的变化等。
- 应用场景:在需要深入跟踪程序执行流程、性能调优或诊断复杂问题时使用。
- DEBUG
- 用于调试:输出详细的调试信息,帮助开发者了解程序在特定点的状态。
- 应用场景:在开发阶段或测试阶段,用于诊断问题、验证代码逻辑等。
- INFO
- 信息性日志:记录程序运行过程中的重要信息,如系统启动、关闭、配置变更等。
- 应用场景:在生产环境中,用于监控程序运行状态、记录重要业务事件等。
- WARN
- 警告信息:表示程序可能出现的问题,但这些问题不会影响程序的正常运行。
- 应用场景:用于提醒开发者或运维人员注意潜在的问题,如资源使用接近上限、配置不当等。
- ERROR
- 错误日志:记录程序运行中的错误信息,这些错误可能导致程序部分功能失效或完全崩溃。
- 应用场景:在程序出现错误时,及时记录错误信息,以便快速定位和解决问题。
系统日志级别设置
在日常开发过程中,最常用的日志级别是 DEBUG,WARN,INFO(默认级别) 等,用于输出调试信息,通常通过系统文件配置,示例如下:
logback-config.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!--开发环境参考配置-->
<configuration>
<!--控制台输出-->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>logbk|%d|%level|%logger{0}|%msg%n</pattern>
</encoder>
</appender>
<!-- 三方日志级别设置 -->
<logger name="ch">
<level value="WARN" />
</logger>
<logger name="io">
<level value="WARN" />
</logger>
<logger name="reactor">
<level value="WARN" />
</logger>
<logger name="org">
<level value="WARN" />
</logger>
<logger name="com">
<level value="WARN" />
</logger>
<logger name="freemarker">
<level value="WARN" />
</logger>
<logger name="snsoft.ft">
<level value="DEBUG" />
</logger>
<logger name="snsoft.plat">
<level value="DEBUG" />
</logger>
<logger name="snsoft.dx">
<level value="DEBUG" />
</logger>
<!--
<logger name="snsoft.emailsearch" additivity="false">
<level value="INFO" />
<appender-ref ref="ESMongoLog" />
</logger>
-->
<!-- 默认日志级别设置 -->
<root>
<level value="INFO" />
<appender-ref ref="Console" />
</root>
</configuration>
日志内容解析
在开发工具控制台中的输出日志内容可以在配置文件中自定义
<!--控制台输出-->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>logbk|%d|%level|%logger{0}|%msg%n</pattern>
</encoder>
</appender>
以上配置的含义为 日志工具|时间戳 |日志级别 |日志分类/组件 |日志消息|附加信息
示例如下
logbk|2024-09-14 13:57:05,448|DEBUG|ui|stockin=>onDataLoaded
logbk|2024-09-14 13:57:05,448|DEBUG|ui| [01245]UIOptCtrlListener
logbk|2024-09-14 13:57:05,448|DEBUG|ui| [00158]SheetAccodeUIListener
logbk|2024-09-14 13:57:05,448|DEBUG|ui| [00206]UITaskPerformListener
logbk|2024-09-14 13:57:05,501|DEBUG|SqlDatabase|[SQL@0]:select sheetcode from bustatus order by 1
logbk|2024-09-14 13:57:05,520|DEBUG|SqlDatabase|[SQL@0]:select optid,value from opts_sys where value is not null and cuicode='C000000001'
logbk|2024-09-14 13:57:05,528|DEBUG|SqlDatabase|[SQL@0]:select dicticode from dictinfo where cuicode=? order by 1:[C000000001]
logbk|2024-09-14 13:57:05,532|DEBUG|SqlDatabase|[SQL@0]:select dicticode from dictinfo where cuicode=? order by 1:[*]
对以上述日志的解析如下:
- 日志工具/源标识 (
logbk
): 这部分标识了生成这条日志的日志工具或系统的名称。在上述情况下,logbk
是一个自定义的日志框架、工具或库的名称。 - 时间戳 (
2024-09-14 13:57:05,448
): 这部分记录了日志事件发生的具体时间和毫秒数。它对于分析和诊断系统问题在时间线上非常重要。 - 日志级别 (
DEBUG
): 这部分指定了日志消息的严重程度或类型。常见的日志级别包括DEBUG
(调试信息)、INFO
(普通信息)、WARN
(警告信息)、ERROR
(错误信息)等。DEBUG
级别通常用于输出详细的调试信息,帮助开发人员理解程序的执行流程和状态。 - 日志分类/组件 (
ui
,SqlDatabase
): 这部分标识了生成日志的系统组件或分类。在例子中,ui
表示与用户界面相关的操作或组件,而SqlDatabase
则表明日志与数据库操作相关。 - 日志消息 (
stockin=>onDataLoaded
,[01245]UIOptCtrlListener
,[SQL@0]:select sheetcode from bustatus order by 1
等): 这是日志的核心部分,包含了实际的日志信息或描述。根据日志级别的不同,这部分内容可以是从详细的调试信息到简单的错误摘要的任何内容。 - 附加信息 (如SQL查询语句中的参数
[C000000001]
): 在某些情况下,日志消息可能会包含额外的参数或上下文信息,这些信息对于理解和诊断问题非常有用。在例子中,SQL查询语句的参数(如[C000000001]
)就是这样一种附加信息。
日志应用场景
1、系统日志级别设置
1. 开发阶段
DEBUG级别
-
应用场景
-
调试新功能或修复bug时,通过DEBUG日志快速定位问题原因。例如:
DEBUG|2024-09-16 10:00:00|MyClass|Entering method processData with parameters: [1, 2, 3]
-
验证代码逻辑是否正确,特别是复杂算法或业务逻辑的实现。
-
性能调优初期,通过DEBUG日志分析程序的瓶颈所在。
-
TRACE级别(如果支持)
-
应用场景
-
深入跟踪和监控程序的执行路径,特别是在多线程或分布式系统中。
-
极端情况下,当DEBUG级别仍不足以解决问题时,启用TRACE级别以获取更多信息。例如:
TRACE|2024-09-16 10:01:00|Thread-1|Executing SQL query: SELECT * FROM users WHERE id = 1
-
2. 测试阶段
INFO级别
-
用途:记录程序运行过程中的重要信息。
-
应用场景
-
自动化测试时,通过INFO日志验证测试用例的执行结果和预期是否一致。
-
性能测试时,记录关键操作的响应时间、吞吐量等指标。例如:
INFO|2024-09-16 11:00:00|SystemTest|Test case 'loginSuccess' passed, response time: 200ms
-
WARN级别
-
用途:记录程序运行中的警告信息。
-
应用场景
-
测试阶段发现潜在的配置错误、资源不足等警告信息,及时提醒开发人员进行修复。例如:
WARN|2024-09-16 11:30:00|ConfigService|Configuration file 'config.json' not found, using default settings
-
3. 上线阶段
INFO级别(默认)
-
应用场景
-
监控系统的运行状态,如服务的启动、停止、异常重启等。
-
记录业务操作的成功和失败,以便分析业务逻辑的正确性和稳定性。例如:
INFO|2024-09-17 08:00:00|MyAppService|Service started successfully INFO|2024-09-17 09:00:00|OrderService|Order #12345 created successfully
-
ERROR级别
-
用途:记录程序运行中的错误信息。
-
应用场景
-
实时监控系统中的错误日志,快速响应并修复系统故障。
-
定期对错误日志进行分析,找出系统潜在的问题并进行优化。例如:
ERROR|2024-09-17 10:00:00|DatabaseService|Failed to connect to database: Connection refused
-
FATAL级别(如果支持)
-
用途:记录非常严重的错误。
-
应用场景
-
记录导致系统崩溃或无法恢复的严重错误。
-
触发系统的紧急恢复机制,如自动重启服务或发送告警通知。例如:
FATAL|2024-09-17 11:00:00|System|Critical error occurred, system is shutting down.
-
2、自定义日志
1. 业务日志
-
用途:记录与业务逻辑紧密相关的操作和信息。
-
应用场景
-
用户登录、单据创建、单据自动生成、支付成功等。例如:
BUSINESS|2024-09-17 12:00:00|UserService|User 'john_doe' logged in successfully BUSINESS|2024-09-17 12:15:00|OrderService|Order #12345 created by user 'john_doe', total amount: 100.00
-
2. 安全日志
-
用途:记录与系统安全相关的操作和信息。
-
应用场景
-
用户登录失败、权限变更、敏感数据访问等,例如:
SECURITY|2024-09-17 13:00:00|LoginService|Failed login attempt for user 'invalid_user', IP: 192.168.1.100 SECURITY|2024-09-17 14:00:00|PermissionService|User 'admin' changed permissions for user 'john_doe'
一般可以将核心流程操作记录存入数据库中
-
3. 性能日志
-
用途:记录系统的性能数据。
-
应用场景
-
监控系统的性能瓶颈,及时进行性能调优。
-
评估系统的负载能力,为系统扩容提供依据。
-
分析系统在不同时间段的性能表现。例如:
PERFORMANCE|2024-09-17 15:00:00|SystemMonitor|Average response time: 250ms,
-
常见业务自定义日志需求与代码实现
常见业务代码日志的输出格式大多数时候会包括 入参(输入参数)和出参(返回结果)的日志输出,这两个部分对于理解和调试程序行为非常重要;
除此之外还可以通过try.catch函数捕获异常并通过logger.error 输出日志,而不是通过e.printStackTrace() 输出堆栈日志和代码日志并混合到一起。
除了系统框架下的日志输出,常见需要输出入出参日志的业务(包括但不限于:
- 自动生成的下级单据:记录触发自动生成的入参(如源业务ID、目标业务模板、参数配置等),生成过程中的关键步骤和状态,以及生成后业务的出参(如新生成的业务ID、关键属性等)。
- API(三方)接口调用:在调用内部或外部API时,记录API的URL、请求方法(GET/POST等)、请求头、请求体(入参)等;API响应后记录响应状态码、响应体(出参)等。对于异常情况,还需记录异常信息、错误码等。
- 数据导入与导出:在数据导入时,记录导入文件的路径、格式、数据量等入参;处理过程中记录转换逻辑、数据清洗步骤;导出时记录导出文件的路径、内容摘要等出参。
- 用户注册与登录:用户注册时,记录用户提交的注册信息(如用户名、密码、邮箱等)作为入参;注册成功后,记录用户ID、创建时间等出参。用户登录时,记录登录尝试的用户名、密码或令牌等入参,以及登录成功或失败的结果作为出参。
- 支付流程:在支付请求发起时,记录支付金额、支付对象、支付方式等入参;支付过程中记录支付状态变更、与支付平台的交互信息等;支付完成后记录支付结果(成功/失败)、支付凭证等出参。
- 订单处理:订单创建时,记录订单详情(如商品信息、用户信息、优惠信息等)作为入参;订单处理过程中记录库存扣减、支付验证等关键步骤;订单完成后记录物流信息、订单状态变更等出参。
- 消息发送与接收:在发送消息时,记录消息内容、接收者信息、发送时间等入参;消息发送成功后记录发送结果、回执信息等出参。接收消息时,记录接收到的消息内容、来源、接收时间等入参,以及处理结果作为出参。
),以下是具体代码示例。
1、自动生成的下级单据
描述:在拷贝核销流程中,拷贝数据自动生成的单据,可以输出单据主键内外码日志,生成参数,便于后续的单据生成的监听
代码示例:
public String createStockIn(Map<String, BigDecimal> qtcMap) {
if(qtcMap == null || qtcMap.isEmpty()){
try {
throw new InfoException("非法请求!");
} catch (InfoException e) {
throw new RuntimeException(e);
}
}
logger.info("生成入库单参数:{}",qtcMap);
CopyServiceFactory<Map<String,BigDecimal>, List<PurOrderBalView>,StockInList,String> factory = SpringBeanUtils.getBeanByName("SN-PLAT.CopyServiceFactory");
CopyService<Map<String,BigDecimal>, List<PurOrderBalView>,StockInList,String> copyService = factory.newCopyService("SN-STUDY.StockIn");
return DAO.newInstance(StockInList.class).trans(dao ->{
String stockicode = copyService.copy(qtcMap);
if(StringUtil.isBlank(stockicode)){
try {
throw new InfoException("生成入库单内码为空");
} catch (InfoException e) {
throw new RuntimeException(e);
}
}
logger.info("生成入库单内码:{}",stockicode);
return stockicode;
});
}
2、API(三方)接口调用
描述:记录调用三方API的入参、响应以及捕获的异常
代码示例:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
// 调用三方接口服务类
public class ThirdPartyService {
private static final Logger logger = LoggerFactory.getLogger(ThirdPartyService.class);
// 调用第三方接口的方法
public String callThirdPartyAPI(Map<String, String> params) {
try {
// 模拟调用三方接口获取Token
String token = Utils.getToken();
// 日志输出调用接口的入参
logger.info("Calling third-party API with params: {}", params);
// 调用三方接口返回值,入参是params,和token
String response = "This is a mocked response from third-party API";
// 日志输出接口返回的结果
logger.info("Received response from third-party API: {}", response);
// 返回结果(为处理后的数据)
return response;
} catch (Exception e) {
// 捕获异常并记录错误日志
logger.error("Error occurred while calling third-party API", e);
// 抛出运行时异常(或根据需要处理)
throw new RuntimeException("Failed to call third-party API", e);
}
}
}
3. 数据导入与导出
描述:记录数据导入的入参(如文件路径)和导出后的出参(如导出文件路径)。
代码示例(仅导入):
public void importData(String filePath, List<Object> params) {
if(ObjectUtils.isEmpty(params)){
logger.error(“{××业务}导入数据为空”);
return;
}
logger.info("({××业务})开始导入数据,文件路径:{},导入量:{}", filePath,params.size());
try {
ImportUtils.importDataFromFile(filePath);
logger.info("数据导入完成");
} catch (Exception e) {
logger.error("数据导入失败", e);
throw new RuntimeException("数据导入失败", e);
}
}
(导出示例类似,只是方法名和逻辑会有所不同,导入)
4. 用户注册与登录
注册描述:记录用户提交的注册信息作为入参,注册成功后记录用户ID等出参。
注册代码示例:
public String registerUser(Map<String, String> userInfo) {
logger.info("用户注册入参:{}", userInfo);
try {
String userId = UserService.register(userInfo);
logger.info("用户注册成功,用户ID:{}", userId);
return userId;
} catch (Exception e) {
logger.error("用户注册失败", e);
throw new RuntimeException("用户注册失败", e);
}
}
登录描述:记录登录尝试的入参(如用户名和密码),以及登录成功或失败的结果。
登录代码示例(简化):
public boolean loginUser(String username, String password) {
logger.info("用户登录尝试,用户名:{}", username);
try {
boolean success = UserService.login(username, password);
if (success) {
logger.info("用户登录成功");
} else {
logger.info("用户登录失败");
}
return success;
} catch (Exception e) {
logger.error("用户登录过程中发生异常", e);
throw new RuntimeException("用户登录失败", e);
}
}
5. 支付流程
描述:记录支付请求的入参、支付过程中的关键步骤和支付结果。
代码示例:
public PaymentResult pay(PaymentRequest request) {
logger.info("支付请求入参:{}", request);
try {
PaymentResult result = PaymentService.pay(request);
logger.info("支付结果:{}", result);
return result;
} catch (Exception e) {
logger.error("支付失败", e);
throw new RuntimeException("支付失败", e);
}
}
6. 订单处理
描述:记录订单创建的入参、处理过程中的关键步骤和订单完成后的出参。
代码示例:
public OrderResult processOrder(OrderCreateRequest request) {
logger.info("订单创建入参:{}", request);
try {
OrderResult result = OrderService.createAndProcessOrder(request);
logger.info("订单处理结果:{}", result);
return result;
} catch (Exception e) {
logger.error("订单处理失败", e);
throw new RuntimeException("订单处理失败", e);
}
}
7. 消息发送与接收
发送描述:记录消息发送的入参(如消息内容、接收者)和发送结果。
发送代码示例:
public void sendMessage(String content, String receiver) {
logger.info("发送消息,内容:{},接收者:{}", content, receiver);
try {
MessageService.send(content, receiver);
logger.info("消息发送成功");
} catch (Exception e) {
logger.error("消息发送失败", e);
throw new RuntimeException("消息发送失败", e);
}
}
接收描述:记录接收到的消息入参和处理结果。
接收代码示例(假设在另一个服务中处理):
public void receiveMessage(String messageContent) {
logger.info("接收到消息,内容:{}", messageContent);
try {
MessageService.processMessage(messageContent);
logger.info("消息处理完成");
} catch (Exception e) {
logger.error("消息处理失败", e);
// 根据需要处理异常,如重试、记录日志等
}
}
日志打印的建议
1. 选择合适的日志等级
-
建议:根据日志的严重性和用途选择合适的日志等级(error、warn、info、debug)。
-
正例代码:
if (someSevereError) { log.error("发生严重错误: {}", errorDetail); } else if (someWarningCondition) { log.warn("出现警告情况: {}", warningDetail); } else { log.info("系统正常运行: {}", operationDetail); }
2. 打印函数的入参、出参
-
建议:在函数入口和出口打印关键参数和返回值。
-
正例代码:
public String getName(Request req) { log.debug("getName 方法开始,入参: {}", req.getUserId()); String name = "JavaPub"; log.debug("getName 方法结束,返回值: {}", name); return name; }
3. 打印日志对象要做判空处理
-
建议:在打印对象之前进行判空处理,避免空指针异常。
-
反例代码:
public void doSomething(Book book) { log.info("Book 名称: {}", book.getName()); // 如果 book 为 null,会抛出 NullPointerException }
-
正例代码:
public void doSomething(Book book) { if (book != null) { log.info("Book 名称: {}", book.getName()); } else { log.info("Book 对象为空"); } }
4. 使用 Slf4j 而不是具体日志实现
-
建议:使用 Slf4j 作为日志门面,以便解耦具体日志实现。
-
反例代码:直接使用 Log4j 或 Logback 日志实现。
-
正例代码:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(ClassName.class);
5. 对低级别日志进行级别开关判断
-
建议:对于 debug、trace 等低级别日志,进行级别判断以避免不必要的字符串拼接和资源浪费。
-
反例代码(无显式判断):
logger.debug("User info: " + user.getName());
-
正例代码:
if (logger.isDebugEnabled()) { logger.debug("User info: {}", user.getName()); }
6. 避免使用 e.printStackTrace()
-
建议:使用日志框架的 error 方法打印异常信息。
-
反例代码:
} catch (Exception e) { e.printStackTrace(); }
-
正例代码:
} catch (Exception e) { log.error("处理过程中发生异常", e); }
7. 打印全部异常信息
-
建议:在捕获异常时,打印完整的异常信息。
-
反例代码:
log.error("发生了一个异常");
-
正例代码:
log.error("处理请求时发生异常", e);
8. 避免打印重复日志
-
建议:在嵌套逻辑中避免打印重复日志。
-
反例代码
public void doSomething(String s){ log.info("do something and print log: {}", s); doSubSomething(s); } private void doSubSomething(String s){ log.info("do sub something and print log: {}", s); // 写点业务逻辑 ... }
-
正例:在父级或最高级别函数中打印日志,或在子函数中改为 debug 级别。
正例代码 1:在父级函数中打印日志
在这个例子中,我们只在
doSomething
方法中打印日志,而doSubSomething
方法则专注于业务逻辑,不重复打印日志。public void doSomething(String s){ log.info("do something and print log: {}", s); doSubSomething(s); } private void doSubSomething(String s){ // 专注于业务逻辑,不打印日志 // 写点业务逻辑 ... }
正例代码 2:在子函数中改为 debug 级别(如果确实需要子函数中的日志)
如果在某些情况下确实需要在子函数中记录日志(例如,为了调试或记录子函数内部的重要事件),可以将这些日志的级别设置为
DEBUG
,这样它们就不会在常规的运行日志中频繁出现,但可以在需要时启用以获取更详细的调试信息。public void doSomething(String s){ log.info("do something and print log: {}", s); doSubSomething(s); } private void doSubSomething(String s){ log.debug("do sub something and print log (debug level): {}", s); // 写点业务逻辑 ... }
在这种方式下,
doSubSomething
方法中的日志仅在启用了DEBUG级别日志记录时才会输出,这有助于减少正常运行时的日志量,同时保留了在需要时进行详细调试的能力。选择哪种方式取决于具体需求,例如日志的详细程度、是否需要调试信息等。通常,推荐在顶层或入口点打印关键日志,而将子函数中的日志级别设置为DEBUG或更低,以减少日志的冗余和提高日志的可用性。
9. 日志尽量使用英文
-
建议:为了避免编码问题,尽量使用英文打印日志。
-
正例代码:
log.info("User login successfully, userId: {}", userId);
10. 在核心业务逻辑分支首行打印日志
-
建议:在分支条件首行打印日志,以便快速定位问题。且各分支要不都有日志,要不都没有,以免难以定位
-
反例代码
public void processUser(User user) { if (user.isAdmin()) { // 管理员逻辑,但没有日志 performAdminTasks(user); } else if (user.isGuest()) { log.info("Processing guest user: {}", user.getName()); performGuestTasks(user); } else { // 普通用户逻辑,也没有日志 performRegularTasks(user); } }
-
正例代码:
if (user.isVip()) { log.info("VIP 用户开始处理逻辑,userId: {}", userId); // 会员逻辑 } else { log.info("非 VIP 用户开始处理逻辑,userId: {}", userId); // 非会员逻辑 }
11. 日志要携带上下文和链路信息
-
建议:日志应包含足够的上下文信息,如用户ID、请求ID等。
-
反例代码
public void doSomething(){ log.info("××核心业务开始进行"); //代码start //代码end log.info("××核心业务结束"); }
-
正例代码:
log.info("处理用户请求,userId: {}, traceId: {}", userId, traceId);
12. 捕获异常并记录完整的堆栈信息
-
建议:在捕获异常时,应记录完整的堆栈信息,而不是只记录异常消息。
-
反例代码:
try { // 可能会抛出异常的代码 } catch (Exception e) { log.error("处理请求时发生异常: " + e.getMessage()); }
这个例子只记录了异常的消息部分,忽略了堆栈信息,这使得定位问题变得更加困难。
-
正例代码:
try { // 可能会抛出异常的代码 } catch (Exception e) { log.error("处理请求时发生异常", e); }
在这个正例代码中,使用了日志框架的自动堆栈信息记录功能(如Logback或Log4j的
log.error(String msg, Throwable t)
方法),将完整的异常堆栈信息记录到日志中,有助于快速定位问题原因。
13. 避免在日志中输出敏感信息
-
建议:在记录日志时,应避免输出敏感信息,如用户密码、密钥等。
-
反例代码:
log.info("用户登录信息,用户名: {}, 密码: {}", userName, userPassword);
这个例子将用户密码也记录到了日志中,存在严重的安全隐患。
-
正例代码:
log.info("用户登录信息,用户名: {}, 密码已隐藏", userName);
或者,如果需要在日志中记录密码的某些校验或处理结果,可以对密码进行脱敏处理后再记录。
14. 日志级别与业务逻辑相匹配
-
建议:日志的级别(如DEBUG、INFO、WARN、ERROR)应与业务逻辑的严重程度相匹配。
-
反例代码:
// 实际上是一个错误情况,但错误地使用了INFO级别 log.info("用户数据加载失败: {}", e.getMessage());
这个例子应该使用ERROR级别来记录错误信息。
-
正例代码:
try { // 加载用户数据 } catch (Exception e) { log.error("用户数据加载失败", e); }
在这个正例代码中,使用了ERROR级别来记录用户数据加载失败的情况,这更符合业务逻辑的严重程度。
15. 建议使用参数占位{}
,而不是用+
拼接
-
建议:在记录日志时,应使用日志框架提供的参数占位功能(如
{}
),而不是通过+
操作符来拼接字符串。使用占位符不仅可以提高代码的可读性,还可以减少不必要的字符串拼接操作,从而提高性能。 -
反例代码:
String userId = "12345"; String message = "用户ID为" + userId + "的用户进行了登录操作"; log.info(message);
在这个反例代码中,通过
+
操作符将字符串拼接起来,然后再记录日志。这种方式在日志内容较多或字符串较长时,可能会导致性能问题,并且代码的可读性也较差。 -
正例代码:
String userId = "12345"; log.info("用户ID为{}的用户进行了登录操作", userId);
在这个正例代码中,使用了日志框架提供的参数占位功能(
{}
),并将userId
作为参数传递给log.info
方法。日志框架会在记录日志时,自动将占位符替换为对应的参数值。这种方式不仅提高了代码的可读性,还避免了不必要的字符串拼接操作,从而提高了性能。此外,需要注意的是,虽然在一些情况下(如日志级别较低时),日志记录本身可能不会执行(因为被框架过滤掉了),但字符串拼接操作仍然会执行。因此,即使日志最终没有被记录,使用占位符仍然可以避免不必要的性能损耗。
16. 建议使用异步的方式来输出日志
-
建议:为了提高应用程序的性能和响应速度,建议使用异步的方式来输出日志。日志的写入操作通常涉及到磁盘I/O,这是一个相对耗时的操作。如果日志输出采用同步方式,可能会阻塞主线程的执行,影响应用程序的整体性能。通过使用异步日志记录器,可以将日志消息发送到一个独立的线程或线程池中处理,从而避免阻塞主线程。
-
反例代码(假设为同步日志记录):
// 假设log是一个同步的Logger实例 log.info("这是一条同步记录的日志信息"); // 这行代码会阻塞,直到日志消息被完全写入磁盘(或达到某个缓冲区大小)
在这个反例代码中,日志记录操作是同步的,可能会阻塞当前线程的执行。
-
正例代码(使用异步日志记录器):
以Logback为例,你可以通过配置
AsyncAppender
来实现异步日志记录。logback.xml配置示例:
<configuration> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <!-- 引用一个已存在的appender --> <appender-ref ref="FILE" /> <!-- 队列最大容量 --> <queueSize>512</queueSize> <!-- 丢弃策略,默认是BlockingQueueDiscarder --> <!-- <discardingThreshold>0</discardingThreshold> --> <!-- 另一个不常用的选项是,如果队列满了,则立即阻塞 --> <!-- <blocking>true</blocking> --> </appender> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>logs/app.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="ASYNC" /> </root> </configuration>
在这个配置中,
AsyncAppender
被配置为引用另一个FileAppender
,它将日志消息异步地写入到文件中。通过设置queueSize
,你可以控制队列的最大容量,从而避免在极端情况下因队列溢出而导致的日志丢失。在代码中,你仍然像平常一样记录日志,但Logback会在后台异步处理这些日志消息:
// 假设log是一个通过AsyncAppender配置的Logger实例 log.info("这是一条异步记录的日志信息"); // 这行代码会立即返回,不会阻塞当前线程
使用异步日志记录可以显著提高应用程序的性能和响应速度,尤其是在高并发场景下。然而,也需要注意到异步日志记录可能会带来一些额外的复杂性和风险,比如日志消息的顺序可能无法得到保证,或者在极端情况下可能会丢失部分日志消息。因此,在选择使用异步日志记录时,需要根据实际应用场景和需求进行权衡。
17. 日志文件按日期或大小分割
-
建议:为了防止日志文件过大或积累过多,建议根据日期或文件大小自动分割日志文件。
-
正例:使用Logback或Log4j的配置文件设置日志文件滚动策略。
Logback 示例:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/app.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 每天滚动一次日志文件 --> <fileNamePattern>logs/archived/app.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 可选:设置最大历史记录数 --> <maxHistory>30</maxHistory> </rollingPolicy> </appender>
在这个配置中,日志文件每天滚动一次,并保存在带有日期戳的归档目录中。同时,可以设置
maxHistory
来限制归档文件的数量。
18. 合理的日志级别配置
-
建议:根据应用程序的不同环境和阶段(如开发、测试、生产)设置合理的日志级别。
-
正例:在开发环境中,可以将日志级别设置为DEBUG或TRACE,以便详细跟踪和调试;在测试和生产环境中,可以将日志级别设置为INFO或WARN,以减少日志量并专注于关键信息。
-
配置示例(使用环境变量或配置文件来动态设置):
# 生产环境日志级别配置 log.level=INFO
在Java代码中,根据配置动态设置日志级别:
Logger logger = LoggerFactory.getLogger(MyClass.class); // 假设从配置中读取了日志级别 String level = System.getProperty("log.level", "INFO"); // 根据需要调整Logger的级别(这里需要自定义实现或使用特定框架的功能) // 注意:标准的Logger API通常不支持直接通过字符串设置级别,这里只是示意
注意:直接通过字符串设置日志级别在标准Java日志框架中并不直接支持,通常需要通过配置文件或编程方式在初始化时设置。
19. 使用MDC(Mapped Diagnostic Context)传递上下文信息
-
建议:在分布式系统或复杂应用中,使用MDC(Mapped Diagnostic Context)来跨线程或请求传递上下文信息,如用户ID、会话ID等。
-
正例:
在请求进入时,将上下文信息放入MDC:
MDC.put("userId", userId); MDC.put("sessionId", sessionId); // 处理请求...
在日志记录时,无需显式传递这些上下文信息,因为它们已经包含在MDC中:
log.info("处理用户请求");
在请求处理完成后,清理MDC中的信息:
MDC.clear();
注意:MDC是线程局部的,这意味着每个线程都有自己独立的MDC映射。
20. 定期检查和分析日志文件
- 建议:定期对日志文件进行检查和分析,以发现潜在的问题、性能瓶颈或安全漏洞。
- 正例:
- 使用日志分析工具(如Splunk、ELK Stack等)对日志文件进行集中管理和分析。
- 编写自动化脚本或利用CI/CD流程中的步骤来定期检查日志文件中的错误或警告信息。
- 设定日志告警规则,当检测到特定日志模式(如异常堆栈、错误代码等)时,及时通知相关人员。
通过遵循这些建议,可以更有效地管理和利用日志信息,提高应用程序的健壮性、可维护性和性能。
扩展
1. 如何不重启项目可以动态修改日志级别
在不重启项目的情况下动态修改日志级别,可以通过多种方法实现,具体方法取决于你使用的日志框架(如Logback、Log4j2等)以及你的项目是否集成了Spring Boot等框架。以下是一些常见的实现方式:
一、使用Spring Boot Actuator(适用于Spring Boot项目)
如果你的项目是基于Spring Boot的,可以利用Spring Boot Actuator来动态调整日志级别,而无需重启服务。
-
引入依赖:
在项目的pom.xml
文件中添加Spring Boot Actuator的依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
开启端点:
在application.properties
或application.yml
配置文件中开启loggers
端点。management.endpoints.web.exposure.include=loggers
-
发送请求:
通过HTTP请求(如使用curl或Postman)向/actuator/loggers
端点发送POST请求,以修改日志级别。curl -i -X POST 'http://localhost:8080/actuator/loggers/com.example' -H 'Content-Type: application/json' -d '{"configuredLevel":"DEBUG"}'
这里
com.example
是你想要修改日志级别的包名,"DEBUG"
是新的日志级别。
二、使用Logback的MBean(适用于Java项目)
如果你的项目使用了Logback作为日志框架,并且没有集成Spring Boot,你可以通过JMX(Java Management Extensions)的MBean来动态修改日志级别。
-
编写代码:
编写一个工具类,通过JMX的MBean接口来修改日志级别。import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import org.slf4j.LoggerFactory; public class LogLevelChanger { public void changeLogLevel(String loggerName, String level) { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); loggerContext.getLogger(loggerName).setLevel(Level.toLevel(level)); } }
-
调用方法:
在你的应用程序中调用这个工具类的方法来修改日志级别。LogLevelChanger logLevelChanger = new LogLevelChanger(); logLevelChanger.changeLogLevel("com.example.MyClass", "DEBUG");
注意,这种方式需要在应用程序运行时进行调用,因此你可能需要提供一个HTTP接口或其他形式的交互方式来触发这个调用。
三、使用Log4j2的Web接口(适用于Log4j2且支持Web的项目)
如果你的项目使用了Log4j2,并且你的应用是一个Web应用,你可以通过创建一个Web接口来动态修改日志级别。
-
创建Controller:
在你的Spring Boot(或其他Web框架)应用中创建一个Controller,用于接收修改日志级别的请求。@RestController @RequestMapping("/log4j2") public class Log4j2Controller { @PostMapping("/changeLevel") public ResponseEntity<?> changeLogLevel(@RequestParam String loggerName, @RequestParam String level) { // 这里需要调用Log4j2的API来修改日志级别 // 注意:这里的实现细节会根据你的具体配置和需求有所不同 return ResponseEntity.ok("日志级别修改成功"); } }
注意:上面的代码只是一个示例,你需要根据Log4j2的API来实现具体的修改逻辑。
-
调用接口:
通过HTTP请求(如使用curl或Postman)调用你创建的接口来修改日志级别。
总结
不重启项目动态修改日志级别的方法多种多样,具体选择哪种方法取决于你的项目需求、使用的日志框架以及是否集成了Spring Boot等框架。上述方法提供了几种常见的实现方式,你可以根据自己的实际情况选择适合的方法。
2.日志级别的集中管理(ELK)
对于大型分布式系统来说,日志级别的管理可能会变得复杂。因此,一些系统采用了集中管理的方式来简化日志级别的配置和调整。例如:
- 配置中心:使用配置中心(如Spring Cloud Config、nacos、Apollo等)来集中管理所有服务的配置信息,包括日志级别。通过修改配置中心的配置,可以实时更新所有服务的日志级别,而无需逐个服务进行修改。
- 日志管理系统:使用专门的日志管理系统(如ELK Stack、Splunk等)来收集、分析和展示日志信息。这些系统通常支持动态调整日志级别的功能,使得开发者可以更加方便地监控和管理日志。