可观测性之Log4j2优雅日志打印

本文涉及的产品
可观测链路 OpenTelemetry 版,每月50GB免费额度
可观测可视化 Grafana 版,10个用户账号 1个月
EMR Serverless StarRocks,5000CU*H 48000GB*H
简介: 可观测性之Log4j2优雅日志打印


简介

对于Log4j2大家应该都不是很陌生,听说最多的应该是2021年年底出现的安全漏洞了,不过最让大家头痛的应该不仅仅是这个安全漏洞的处理,安全漏洞通过升级最新的依赖版本即可快速解决,平时在使用过程中遇到过比较多的问题应该就是日志jar包不知道如何选择?日志jar冲突引起的日志不打印问题,日志配置太过复杂不知道如何配置只能百度CV粘贴一个配置。这些日志配置其实并不复杂,主要是因为日志组件的发展历史比较充满曲折,导致了很多地方不兼容。接下来就来通过日志组件的发展历史来入手,看看Log4j2是从什么背景下产生的。

历史

Log4j2日志出现的这些问题多少与它出现的历史有点关系,接下来就先来了解下Java日志发展史,方便我们后续知道引入哪个依赖组件。

System.out

对于Java日志打印最开始只有大家熟悉的以System开头如System.out.println("hello world")这样的写法,默认的控制台日志打印方式需要有IO操作,性能极其低效(慎用),功能也太过单一只能简简单单的输出日志。

Log4j

再后来就有了软件开发者Ceki Gulcu设计出了一套日志库也就是log4j并捐献给了Apache帮助简化日志打印。相关的依赖包是log4j和适配log4j2的桥接包log4j-1.2-api。

JUL(Java Util Logging)

Java毕竟还是sun公司(后被oracle收购)的Java,sun公司并没有采纳Log4j提供的标准库,而是在jdk1.4自立一套日志标准JUL(Java Util Logging) JUL并不算强大也没得到普及所以现在大家也很少听说了。相关的依赖是jdk和适配log4j2的桥接包log4j-jul

JCL (Jakarta Commons Logging)

为了方便开发者进行选择使用,Apache推出了日志门面JCL(Jakarta Commons Logging)可以在运行时绑定日志组件。 相关的依赖是commons-logging和适配log4j2的桥接包log4j-jcl。

Slf4j

前面的竞争促进了日志组件的发展但也逐渐导致日志依赖与配置越来越复杂,2006年Log4j的作者Ceki Gulcu离开了Apache组织后觉得JCL门面不好用,于是自己开发了一版和其功能相似的Slf4j(Simple Logging Facade for Java)这个也是大家所熟悉的日志门面。相关的依赖是slf4j-api和适配log4j2的桥接包og4j-slf4j-impl或者log4j-slf4j2-impl。

Logback

后来Slf4j作者又写出了Logback日志标准库作为Slf4j接口的默认实现。

Log4j2

到了2012年,Apache可能看到这样下去要被反超了,于是就推出了新项目Log4j2并且不兼容Log4j,全面借鉴Slf4j+Logback。Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了显著的改进,并提供了Logback中可用的许多改进,同时修复了Logback体系结构中的一些固有问题。

了解了日志组件的历史,可以看到最后log4j2集众家之长,那应该如何优雅的使用log4j2日志呢,可以继续往下看。

架构说明

定位

Log4j 2 旨在用作审计日志记录,被设计为可靠、快速和可扩展,易于理解和使用的框架。简单的来说Log4j2就是一个日志框架,用来管理日志的。

特征

之所以要使用Log4j2 主要还是因为Log4j2 为我们提供了足够好用的支持,下面可以来看下Log4j2的一些特征:

  • API分离:  API 与实现是分开的。
  • 改进的性能: 支持Disruptor 异步日志记录。
  • 支持多种API: 提供对 Log4j 1.2、SLF4J、Commons Logging 和 java.util.logging (JUL) API 的支持。
  • 无侵入性: 通过扩展机制自动加载,无需与代码完全耦合,代码中可以使用SLF4J门面
  • 插件架构: 插件化配置, 自动识别插件并在配置引用它们,极高的可扩展性
  • 属性配置支持: 可以在配置中引用属性,Log4j 将直接替换它们,属性来自配置文件中定义的值、系统属性、环境变量、ThreadContext Map 和事件中存在的数据。
  • 无垃圾与低垃圾 :稳态日志记录期间,Log4j 2在独立应用程序中是无垃圾的,Web 应用程序中是低垃圾的。

可以看到Log4j2 核心的机制中考虑到了高性能,可扩展,可配置等需求,有效的解决着我们使用日志的痛点,那接下来就来从整体来了解下Log4j2。

架构

下面可以先整体来了解下UML图,这里我用文字的形式标明了日志类型的作用,可以简单了解下。

如果对UML不是非常熟悉的同学看起来可能会比较费劲,不过不用担心下面就针对比较重要的类型详细来说明下,一方面便于了解日志配置,一方面便于后面我们使用。

  • LoggerContext(日志上下文) :   这个就像是Spring的ApplicationContext 充当着容器的上下文环境,Spring可以同时存在应用上下文,Web上下文,Log4j2应用也可以同时有多个 LoggerContext
  • Configuration (配置): 每个 LoggerContext 都有一个活动的 Configuration。Configuration 包含所有 Appenders、context-wide Filters、LoggerConfigs 并包含对 StrSubstitutor 的引用
  • Logger(记录器):  用于让使用者打印日志使用,可以为每个类创建不同的日志记录器,Logger 本身不执行任何直接操作。它只有一个名称并与 LoggerConfig 相关联由日志实现根据配置来进行打印日志。
  • LoggerConfig(记录器配置):  LoggerConfig对象是在日志记录配置中声明Logger时创建的。LoggerConfig包含一组筛选器Filter,这些筛选器必须允许LogEvent在传递给任何Appender之前通过。它包含对应用于处理事件的一组Appender的引用。
  • Log Levels (日志级别): LoggerConfigs 将被分配一个 Log Level 内置级别集包括 ALL、TRACE、DEBUG、INFO、WARN、ERROR、FATAL 和 OFF。Log4j 2 还支持自定义日志级别 ,下表说明了级别过滤的工作原理。在表中,垂直标题显示 LogEvent 的级别,而水平标题显示与适当的 LoggerConfig 关联的级别。交集标识是否允许 LogEvent 通过以进行进一步处理 (Yes) 或丢弃 (No)。
  • Filter(筛选器): 除了如上一节所述发生的自动日志级别过滤之外,Log4j 还提供了 Filter,可以在控制权传递给任何 LoggerConfig 之前、在控制权传递给 LoggerConfig 之后但在调用任何 Appender 之前、在控制权被执行之后应用。
  • Appender(追加器): Log4j 允许记录请求打印到多个目的地。在 log4j 中,输出目的地称为 Appender。多个 Appender 可以附加到一个 Logger。目前,存在用于控制台、文件、远程套接字服务器等日志的追加
  • Layout(布局): 通常情况下,用户不仅希望自定义输出目标,还希望自定义输出格式。这是通过将 Layout 与 Appender 相关联来实现的。Layout 负责根据用户的意愿格式化 LogEvent,而 appender 负责将格式化的输出发送到其目的地。PatternLayout是标准 log4j 发行版的一部分,它允许用户根据类似于 C 语言printf函数的转换模式指定输出格式。
  • StrSubstitutor 和 StrLookup: StrSubstitutor 类和 StrLookup 接口是从 Apache Commons Lang借来的,然后进行了修改以支持评估 LogEvents。另外 插值器 类是从 Apache Commons Configuration 借来的,以允许 StrSubstitutor 评估来自多个 StrLookups 的变量。它也被修改为支持评估 LogEvents。这些一起提供了一种机制,允许配置引用来自系统属性、配置文件、ThreadContext Map、LogEvent 中的 StructuredData 的变量。如果组件能够处理变量,则可以在处理配置时或在处理每个事件时解析变量。

简单的了解了Log4j2的一些概念之后可能并不是很容易理解一些概念的具体含义,使用起来可能还会比较费劲,那接下来就通过一个简单又完整的入门例子来看下.

开发入门

为了增加一点点的难度,也贴近一下平时开发使用的诉求,这里就以Log4j2绑定Slf4j的案例来说明,使用Slf4j来作为日志门面,使用Log4j2来实现具体的日志配置与打印。

同时下面的示例会有这样的需求:

  • 错误日志打印: 将error日志级别的日志额外打印到error.log里面方便问题排查。
  • 业务日志打印:  将位于link.elastic包及其子包下的所有日志打印到logger.log日志里面。
  • 非业务日志打印: 如果不满足link.elastic的包的日志则打印到控制台。
  • 日志归档: 所有的日志文件都要具有归档策略比如按日期每天归档,或者文件超过250MB也要归档。
  • 链路追踪Id打印: 详细的日志打印可以在Java代码中设置链路追踪Id TraceId打印日志的时候可以将其打印出来。

下面就来详细看下满足这样5个需求的日志配置是如何实现的吧。

依赖引入

可以先通过如下图来看下Log4j2与Slf4之间的适配需要引入哪些依赖包:

可以看到如果要使用Slf4j门面的话,需要引入一个Slf4j门面依赖包slf4j-api和一个与log4j2绑定slf4j的桥接包log4j-slf4j-impl,下面就来看下我们要引入的依赖:

<dependencyManagement>

   <dependencies>

      <!--通过BOM物料清单来引入log4j2的版本-->

       <dependency>

           <groupId>org.apache.logging.log4j</groupId>

           <artifactId>log4j-bom</artifactId>

           <version>2.19.0</version>

           <scope>import</scope>

           <type>pom</type>

       </dependency>

    </dependencies>

</dependencyManagement>

<dependencies>

   <!--log4j2 API包-->

   <dependency>

       <groupId>org.apache.logging.log4j</groupId>

       <artifactId>log4j-api</artifactId>

   </dependency>

   <!--log4j2核心实现包-->

   <dependency>

       <groupId>org.apache.logging.log4j</groupId>

       <artifactId>log4j-core</artifactId>

   </dependency>

   <!--用于log4j2与slf4j桥接-->

   <dependency>

       <groupId>org.apache.logging.log4j</groupId>

       <artifactId>log4j-slf4j-impl</artifactId>

   </dependency>

   <!-- slf4j门面包-->

   <dependency>

       <groupId>org.slf4j</groupId>

       <artifactId>slf4j-api</artifactId>

       <version>1.7.25</version>

   </dependency>

</dependencies>

入门示例

日志配置log4j2.xml

在Log4j2中日志的配置文件是大部分情况下是通过配置日志的xml文件来生效的,这个配置文件的路径默认是在类的根路径下的log4j2.xml配置文件中,当然也可以通过在JVM参数中指定一个其它位置的日志配置路径,具体参数配置的KEY为log4j.configurationFile,接下来就在maven项目的根目录src/main/resources目录下创建一个log4j2.xml配置文件来让配置默认生效,具体配置内容如下:

<?xmlversion="1.0" encoding="UTF-8"?>

<Configuration>

   <!--属性配置-->

   <Properties>

       <!--日志打印格式-->

       <Propertyname="PATTERN">%date{HH:mm:ss.SSS} [%thread] %X{TraceId} %-5level %logger{36} - %msg%n</Property>

       <!--日志等级-->

       <Propertyname="LEVEL">INFO</Property>

   </Properties>

   <!--日志追加器配置-->

   <Appenders>

       <!--可滚动归档文件的日志追加器,这里配置的是Error级别的日志可以打印到error.log文件中

       同时根据日期(天)和大小(最大250MB)进行文件归档-->

       <RollingFilename="ErrorFile"fileName="error.log"

                    filePattern="$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">

           <!--日志文件打印格式-->

           <PatternLayout>

               <Pattern>${PATTERN}</Pattern>

           </PatternLayout>

           <!--日志文件归档策略-->

           <Policies>

               <!--根据日期格式按时间归档-->

               <TimeBasedTriggeringPolicy/>

               <!--根据文件大小归档超过250MB就归档-->

               <SizeBasedTriggeringPolicysize="250 MB"/>

           </Policies>

           <!--日志过滤器-->

           <Filters>

               <!--阈值过滤器,日志等级大于等于ERROR的接收其他的都拒绝-->

               <ThresholdFilterlevel="ERROR"onMatch="ACCEPT"onMi**atch="DENY"/>

           </Filters>

       </RollingFile>

       <!--日志logger文件可以接收所有级别的日志打印-->

       <RollingFilename="LoggerFile"fileName="logger.log"

                    filePattern="$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">

           <!--日志布局 -->

           <PatternLayout>

               <Pattern>${PATTERN}</Pattern>

           </PatternLayout>

           <!--归档策略-->

           <Policies>

               <!--根据日期格式按时间归档-->

               <TimeBasedTriggeringPolicy/>

               <!--根据文件大小归档超过250MB就归档-->

               <SizeBasedTriggeringPolicysize="250 MB"/>

           </Policies>

       </RollingFile>

       <!--打印日志到控制台的日志追加器-->

       <Consolename="STDOUT"target="SYSTEM_OUT">

           <!--日志布局格式-->

           <PatternLayoutpattern="${PATTERN}"/>

       </Console>

   </Appenders>

   <!--日志记录器配置-->

   <Loggers>

       <!-- 记录器的日志名字,这个日志记录器的名字与我们每个类里面获取的Logger对象对应,

       对应的关系就是通过这个name来匹配的,匹配规则一般是满足Logger配置的name前缀,

       每个logger元素的日志上下文中都存在一个LoggerConfig配置对象来管理配置-->

       <Loggername="link.elastic"additivity="false">

           <!-- LoggerConfig 也可以配置一个或多个 AppenderRef 元素,

           在处理日志记录事件时将调用它们中的每一个-->

           <!--logger.log文件日志追加器-->

           <AppenderRefref="LoggerFile"/>

           <!--error级别的error.log追加器-->

           <AppenderRefref="ErrorFile"/>

       </Logger>

       <!--  每个配置都必须有一个根记录器。前面的Logger日志配置器未匹配到则走默认的根记录器

       如果未配置默认根 LoggerConfig,其级别为 ERROR 并附加了控制台附加程序,将被使用。

       根记录器和其他记录器之间的主要区别是:

       1.根记录器没有名称属性。

       2.根记录器不支持可加性属性,因为它没有父记录器-->

       <Rootlevel="${LEVEL}">

           <!--控制台追加器-->

           <AppenderRefref="STDOUT"/>

       </Root>

   </Loggers>

</Configuration>

为了全面的了解我们前面介绍了一些日志相关的概念,这里在引入日志配置的时候尽可能的关联到更多的元素,引入了日志配置之后,下面可以来看Java代码打印日志的示例,同时看下打印效果方便理解。

Java打印日志示例

位于link.elastic包下包含main函数的启动类源码如下:

packagelink.elastic.biz;

importcom.demo.DemoLog;

importorg.slf4j.Logger;

importorg.slf4j.LoggerFactory;

importorg.slf4j.MDC;

/**

* Hello world!

*/

publicclassApp {

   staticLoggerlogger=LoggerFactory.getLogger(App.class);

   publicstaticvoidmain(String[] args) {

       //设置日志上下文MDC

       MDC.put("TraceId", "123456");

       logger.debug("Hello World!");

       logger.info("Hello World!");

       logger.warn("Hello World!");

       logger.error("Hello World!");

       newDemoLog().log("Hello World!");

       MDC.clear();

   }

}

位于其他(非link.elastic)包下的DemoLog日志打印示例:

packagecom.demo;

importorg.slf4j.Logger;

importorg.slf4j.LoggerFactory;

publicclassDemoLog {

   Loggerlogger=LoggerFactory.getLogger(DemoLog.class);

   publicvoidlog(Strings) {

       logger.debug(s);

       logger.info(s);

       logger.warn(s);

       logger.error(s);

   }

}

日志打印效果

控制台日志(非link.elastic包的日志)

logger.log中的日志(link.elastic包下的日志)

error.log中的日志

可以看到这个例子充分的满足了前面的5大诉求:

  • 错误日志打印:  这里使用了阈值过滤器ThresholdFilter,日志等级大于等于ERROR的接收打印其他的都拒绝
  • 业务日志打印:  这里我们单独配置了日志记录器Logger并将其name属性设置为了link.elastic只要Java代码中的日志记录器满足前缀为link.elastic就会将日志打印到这个文件里面,在Java代码中我们的日志记录器的名字为link.elastic.biz.App 是满足link.elastic的前缀的所以会将日志打印到logger.log里面。
  • 非业务日志打印: 对于不满足link.elastic的包比如这里的包名为com.demo下的日志是无法匹配到前面业务日志打印的日志记录器的就只能走Root这个根日志记录器,这个根日志记录器的追加器配置的是控制台,前面控制台打印的日志就是非link.elastic包下的日志打印。
  • 日志归档: 这里可能没有很明显的展示因为要满足日期格式或者大小,日期归档使用的是TimeBasedTriggeringPolicy 这个策略根据filePattern中的日期来进行归档最小的时间我们设置的是日会再每天0点之后产生新日志的时候进行归档,大小归档设置的是SizeBasedTriggeringPolicy。
  • 链路追踪Id打印: 对于链路追踪系统往往不仅仅会将链路信息输送到第三方链路追踪系统也会将链路信息打印控制台一份, 这里我们使用的是字符串替换器,在日志打印格式中设置获取链路追踪id的获取方式%X{TraceId} ,然后在Java代码中将链路追踪Id放入日志诊断上下文MDC中即可如代码: MDC.put("TraceId", "123456");

总结

日志也是我们最常用的观测系统健康状况的方式,优雅的日志打印可以在排查问题的时候事半功倍,在Java日志组件中很多地方使用了日志实现自动扫描的扩展机制,如果随意引入不兼容的依赖包之后被扩展机制扫描到,就很容易出现日志不打印的问题,对于Java 日志依赖的引入,我们可以先了解其曲折的发展历史,了解其依赖来源,然后再针对性的配置依赖即可。然后就是log4j2日志的配置,关于日志的配置官网有非常详细的文档,在使用的时候CV了百度下来的日志配置之后可以参考官网详细的配置,尝试自定义各种属性比如日志追加器append针对日志进行指定位置输出,归档、日志记录器logger针对日志进行分层处理等。如果还有其他问题可以关注微信公众号 《中间件源码》 一起交流吧。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
Kubernetes Ubuntu Windows
【Azure K8S | AKS】分享从AKS集群的Node中查看日志的方法(/var/log)
【Azure K8S | AKS】分享从AKS集群的Node中查看日志的方法(/var/log)
|
23天前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
1月前
|
存储 监控 数据可视化
SLS 虽然不是直接使用 OSS 作为底层存储,但它凭借自身独特的存储架构和功能,为用户提供了一种专业、高效的日志服务解决方案。
【9月更文挑战第2天】SLS 虽然不是直接使用 OSS 作为底层存储,但它凭借自身独特的存储架构和功能,为用户提供了一种专业、高效的日志服务解决方案。
66 9
|
2月前
|
开发框架 .NET Docker
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
|
2月前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
68 0
|
2月前
|
C# Windows 监控
WPF应用跨界成长秘籍:深度揭秘如何与Windows服务完美交互,扩展功能无界限!
【8月更文挑战第31天】WPF(Windows Presentation Foundation)是 .NET 框架下的图形界面技术,具有丰富的界面设计和灵活的客户端功能。在某些场景下,WPF 应用需与 Windows 服务交互以实现后台任务处理、系统监控等功能。本文探讨了两者交互的方法,并通过示例代码展示了如何扩展 WPF 应用的功能。首先介绍了 Windows 服务的基础知识,然后阐述了创建 Windows 服务、设计通信接口及 WPF 客户端调用服务的具体步骤。通过合理的交互设计,WPF 应用可获得更强的后台处理能力和系统级操作权限,提升应用的整体性能。
69 0
|
2月前
|
存储 消息中间件 监控
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统ELK、日志收集分析
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统、日志收集分析。日志级别从小到大的关系(优先级从低到高): ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF 低级别的会输出高级别的信息,高级别的不会输出低级别的信息
|
2月前
|
存储
【Azure Log A workspace】Azure上很多应用日志收集到Log A workspace后如何来分别各自的占比呢?
【Azure Log A workspace】Azure上很多应用日志收集到Log A workspace后如何来分别各自的占比呢?
|
2月前
|
API
【Azure 应用服务】当在Azure App Service的门户上 Log Stream 日志无输出,需要如何操作让其输出Application Logs呢?
【Azure 应用服务】当在Azure App Service的门户上 Log Stream 日志无输出,需要如何操作让其输出Application Logs呢?
|
2月前
|
存储 关系型数据库 MySQL
深入MySQL:事务日志redo log详解与实践
【8月更文挑战第24天】在MySQL的InnoDB存储引擎中,为确保事务的持久性和数据一致性,采用了redo log(重做日志)机制。redo log记录了所有数据修改,在系统崩溃后可通过它恢复未完成的事务。它由内存中的redo log buffer和磁盘上的redo log file组成。事务修改先写入buffer,再异步刷新至磁盘,最后提交事务。若系统崩溃,InnoDB通过redo log重放已提交事务并利用undo log回滚未提交事务,确保数据完整。理解redo log工作流程有助于优化数据库性能和确保数据安全。
221 0
下一篇
无影云桌面