前言
写日志是一项具有挑战性的任务,在工作中我们常常面临一些困境,比如:
- 开发人员在编写代码时常常陷入纠结,不确定在何处打印日志才是最有意义的;
- SRE人员在调查生产问题时可能因为缺乏必要的日志信息而束手无策;
- 运维人员在面对处理海量日志时往往需要耗费大量的精力进行维护;
- 项目管理者面对大量的无实际业务价值的日志,往往不愿投入过多人力和财力进行管理。
这些问题导致了许多矛盾的产生。然而,当问题出现时,我们需要依赖日志记录来建立一种**“不在场证明”,**找出哪一方有问题。
正是由于这种需求,我们在开发应用程序时需要遵循良好的实践,选择成熟的日志收集机制和管理方案,从而缓解这些矛盾。
矛盾的起因
- 首先,我们探讨为何需要记录日志以及日志的作用。
实际上,对于大多数开发人员来说,在调试代码问题、解决不同环境的 Bug 时,日志的价值是显而易见的。作为调试的得力助手和生产环境中不可或缺的救星。
通过查询日志,我们能够确定代码的执行过程、API请求的正确性、核心业务数据的准确性,以及是否存在错误的堆栈信息等等操作,这些条件也构成了开发和运维人员判断代码和生产问题的首要手段。在一个复杂庞大的系统中,如果没有记录任何日志,那么在排查生产环境中的 Bug 时将变得极为困难。
- 若每一行代码都记录上下文,是否就能解决所有问题呢?
理论上确实是可行的,但目前仍存在一些无法解决的问题。
首先是日志存储量的问题,典型的中大型系统日志可能达到TB级,而超大型系统的日志规模甚至可能达到PB级。
这对于存储而言是一个巨大的挑战。其次是搜索性能的下降,常见的日志存储方案如Elasticsearch数据库在面对海量日志时,可能导致维护的映射关系急剧增长,即使划分不同的索引、分布式管理不同的ElasticSearch集群,也难以做到搜索性能不随数据量增加而下降。最后,海量日志的生成可能在峰值时拖慢系统性能,增加出现故障的风险。
因此,日志既不能记录过多导致存储和管理困难,也不能因记录过少而导致运维人员无法排查问题。尽管听起来似乎自相矛盾,但这正是关于日志重要所在!在日志记录中,我们需要在**“太多”和“太少”**之间找到平衡点,以确保既能有效排查问题同时又能够高效管理和存储日志。
日志级别
在决定记录日志之前,通常需要考虑选择适当的日志级别。在讨论如何确定日志级别之前,我们先来了解一下日志级别的作用。
- 确定日志信息的优先级: 通过设定不同的日志级别,我们可以对日志信息进行优先级排序,从而有效减少信息噪音和警报疲劳。不同的日志级别对应不同的信息重要性,开发人员可以根据当前需求选择适当的级别,确保在解决问题或分析系统行为时能够集中关注最重要的信息。
- 在查询日志时进行过滤: 添加日志级别的过滤可以在查询日志时更加精准地获取所需的信息。例如,在调试阶段可能需要详细的调试信息,而在生产环境中可能只关心警告和错误级别的日志。通过合理使用日志级别,可以提高日志的可读性和查询效率,同时降低处理冗余信息的成本。
因此,选择适当的日志级别是一项关键决策,它能够在不同场景中平衡信息的详细程度,帮助开发人员和运维人员更好地理解和管理系统的行为。
常见的日志级别有以下几类,并且从高到低的顺序是:致命(FATAL)、错误(ERROR)、警告(WARN)、信息(INFO)、调试(DEBUG)、痕迹(TRACE)和全部(ALL)
| | | | | | | |
---|---|---|---|---|---|---|---|
| 致命 | 错误 | 警告 | 信息 | 调试 | 痕迹 | 全部 |
致命 | X | | | | | | |
错误 | X | X | | | | | |
警告 | X | X | X | | | | |
信息 | X | X | X | X | | | |
调试 | X | X | X | X | X | | |
痕迹 | X | X | X | X | X | X | |
全部 | X | X | X | X | X | X | X |
- **FATAL:**严重错误级别,表示系统无法继续运行。
- **ERROR:**错误级别,用于记录错误信息。
- **WARN:**警告级别,表示潜在的问题,但不影响程序的运行。
- **INFO:**信息级别,用于记录程序的正常运行信息。
- **DEBUG:**调试级别,用于详细记录调试信息。
- **TRACE:**追踪级别,提供比DEBUG更详细的信息。
- **ALL:**最低级别,用于启用所有日志记录。
常见场景
场景
某工程师在调查生产环境中某个创建资源的 API 性能问题时,发现该 API 接口中打印了INFO
级别的日志,导致业务峰期时出现海量日志,耗尽 Buffer
区内存,拖慢主线程,影响服务性能。
后续功能优化中工程师删除了写业务INFO
级别日志的操作以解决性能问题。
然而,由于某天修改了 API 服务调用链路上的某服务代码,导致 API 创建出的对象存在错误。但是在生产环境中缺少了该资源的日志,工程师无法准确排查问题。在这种情况下,工程师可能需要重新修改日志级别,将业务日志重新启用,并重新构建发布上线,
场景
假设将生产环境的日志设置为 ERROR
级别。某一时刻,依赖的下游服务故障,导致请求大量超时。由于业务峰期 QPS
非常高,短时间内集中产生大量错误日志,导致磁盘 IO
****急剧提高,消耗大量 CPU
,导致整个服务瘫痪。
场景
某工程师在排查生产问题时,发现 INFO
级别的日志无法满足排查根本原因。他需要 DEBUG
级别的日志,但生产环境只配置为 INFO
级别。
日志级别规范与动态调整
日志级别的规范和动态调整有助于在开发、调试和生产环境中更有效地管理日志信息。
日志级别规范
- **TRACE:**在开发期间可以使用,但确保不要将它们提交到版本控制系统中,以避免不必要的日志信息混入生产环境。
- **DEBUG:**在进入生产阶段之前,对调试语句进行审查和缩减,只保留最关键、最有意义的调试信息。
- **INFO:**记录用户驱动的事件或系统的特定操作。这可以包括定期计划的任务、用户登录等。保持信息简洁明了,避免过多的冗余信息。
- **WARN:**记录可能成为错误的事件。例如,耗时较长的操作、接近容量的内存缓存等。允许设置自动警报,以及在故障排除期间更好地了解系统在故障之前的行为。
- **ERROR:**记录每个错误条件,包括 API 调用返回的错误或内部错误条件。
- **FATAL:**只用于表示整个服务已经无法工作的情况。通常,FATAL 级别记录表示程序的结束。
动态调整日志级别
配置文件动态调整
使用配置文件(如 logback.xml 或 log4j2.xml)来配置日志级别。这样,可以在不重新启动应用程序的情况下调整日志级别。
- logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<!-- 使用springProperty引用配置文件中的属性 -->
<!-- <property resource="application.properties" /> -->
<!-- 定义一个变量,用于动态设置日志级别 -->
<property name="logLevel" value="INFO" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%-5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 指定日志级别为变量引用的值 -->
<root level="${logLevel}">
<appender-ref ref="console" />
</root>
<!-- 添加一个TurboFilter,用于动态更改日志级别 -->
<turboFilter class="com.example.demo.config.LoggerNameChangeFilter" />
</configuration>
- log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<!-- 定义日志输出的方式,这里使用控制台输出 -->
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<!-- 定义日志输出的格式 -->
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<!-- 配置日志级别 -->
<Loggers>
<!-- 根日志级别设置为info -->
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
<!-- 设设置特定包(com.example.myapp)的日志级别为debug,additivity="false"表示不向父Logger传递日志。 -->
<Logger name="com.example.myapp" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>
</Configuration>
- logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入Spring Boot基础日志配置 -->
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- 自定义Logback配置开始 -->
<!-- 示例:添加一个自定义的控制台输出appender -->
<appender name="customConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 示例:将自定义的控制台appender添加到根logger -->
<root level="INFO">
<appender-ref ref="customConsoleAppender"/>
</root>
<!-- 自定义Logback配置结束 -->
</configuration>
logback-spring.xml
可以通过配置,定时地检测配置修改,但是这会带来额外的资源消耗。
<configuration scan="true" scanPeriod="30 seconds" >
<!-- ... 具体配置 -->
</configuration>
- 使用外部配置中心,例如 Spring Cloud Config,可以通过远程配置动态更改日志级别。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入Spring Boot基础日志配置 -->
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- 使用Spring表达式动态配置根logger的日志级别 -->
<root level="${LOG_LEVEL:-INFO}">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
在这里,${LOG_LEVEL:-INFO}
使用了Spring表达式,它会从Spring Cloud Config中获取名为LOG_LEVEL
的配置,如果未配置则默认为INFO级别。
JMX(Java Management Extensions)
使用 JMX 允许在运行时修改日志级别。
通过 JConsole,VisualVM 或其他 JMX 工具,可以直接管理日志框架的运行时配置。
在 application.properties
或 application.yml
中启用 JMX:
spring.jmx.enabled=true
远程管理工具
使用远程管理工具,例如 Spring Boot Actuator
,可以通过 HTTP 端点或其他远程管理手段动态调整日志级别。
Spring Boot Actuator 为Spring Boot应用程序提供了丰富的监控和管理功能。通过使用HTTP Endpoint(端点)或JMX(Java Management Extensions)来监视和管理应用程序,从而更好地理解其运行状况并进行调整。
确保在pom.xml
中添加了Spring Boot Actuator的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置application.yml
文件
# 设置日志级别为INFO
logging:
level:
root: INFO
# 允许Actuator修改日志配置
management:
endpoints:
web:
exposure:
include: loggers
最后,使用curl
命令查看和修改日志级别:
- 查看日志配置:
curl -X GET http://localhost:8080/actuator/loggers
你会看到根日志记录器的级别是INFO。
- 修改日志级别:
curl -X POST http://localhost:8080/actuator/loggers/com.example -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"}'
这会将com.example
包下的日志级别设置为DEBUG。
条件日志
在关键代码路径中使用条件日志,根据配置的条件来决定是否记录日志。这样可以更灵活地控制日志输出。
在 application.properties 或 application.yml 中添加一个属性,表示是否启用条件日志:
myapp.logging.enabled=true
创建一个类来进行条件日志,使用 @ConditionalOnProperty
注解来根据配置的条件来判断是否创建这个类的 Bean。这个类可以用于条件日志记录。
@Configuration
@ConditionalOnProperty(name = "myapp.logging.enabled", havingValue = "true")
public class ConditionalLoggerConfig
集成监控系统
将日志级别调整集成到监控系统中,例如 Prometheus、Grafana 等,以便在需要时能够通过监控界面进行动态调整。
总结
综合利用这些方法,可以在不同的环境和阶段更好地管理日志级别,既保持足够的信息用于排查问题,又避免在生产环境中过度记录冗余信息。
参考链接:
- Spring Boot Actuator 官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
- Spring Boot Actuator GitHub 仓库:https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-actuator