简单的springboot log4j2日志配置

简单的springboot log4j2日志配置

在这里插入图片描述

1.简介

Log4j2 是 Apache Software Foundation 开发的一个日志记录工具,它是 Log4j 的后续版本,并且在多个方面进行了改进。以下是 Log4j2 的一些关键特性:

  • 性能提升:Log4j2 在设计上做了很多优化来提高日志记录的效率。例如,它使用了更高效的查找表来减少日志记录时的开销。

  • 可靠性增强:Log4j2 支持异步日志记录,这有助于提高应用程序的整体性能。异步处理可以避免日志记录阻塞应用程序线程。

  • 灵活性增加:Log4j2 提供了一个可插拔的架构,允许用户根据需要选择不同的布局、appender 和其他组件。这种模块化的设计使得配置更加灵活。

  • 简化配置:Log4j2 使用 XML、JSON 或 YAML 文件进行配置,相较于 Log4j 的 XML 配置,提供了更多的灵活性和易用性。
    丰富的功能集:Log4j2 包含了许多高级功能,如支持多种日志级别、过滤器、布局以及多种输出目的地(比如文件、控制台、数据库等)。

  • 安全性改进:鉴于之前在 Log4j 中发现的安全问题,Log4j2 在设计时考虑到了安全性,尤其是在处理外部输入数据时更为谨慎。

Log4j2 是一个广泛使用的日志框架,在 Java 应用程序中非常受欢迎。然而,值得注意的是,Log4j2 也曾经历过一些严重的安全漏洞,比如著名的 Log4Shell 漏洞(CVE-2021-44228),这是一个远程代码执行漏洞,影响了大量的系统和服务。因此,在使用 Log4j2 时,确保使用最新版本并及时应用安全更新是非常重要的。

在 Java 日志领域还有其他的日志框架,Log4j2、Log4j、Logback 和 SLF4J 这几个框架扮演着不同的角色,相互之间还有些许关联,此处整理一下它们之间的关系如下:

  • Log4j
    Log4j 是 Apache 软件基金会开发的第一个日志框架,它为 Java 应用程序提供了强大的日志功能。由于其稳定性和广泛的使用,Log4j 成为了早期 Java 应用的标准日志解决方案之一。
  • Log4j2
    Log4j2 是 Log4j 的继任者,它在 Log4j 的基础上进行了大量的改进,包括性能优化、新的配置方式(XML、JSON 或 YAML)、异步日志处理等功能。虽然两者名称相似,但是 Log4j2 并不向后兼容 Log4j。
  • Logback
    Logback 是 Log4j 的另一个替代品,由 Log4j 的原始作者 Ceki Gülcü 创建。它旨在作为 Log4j 的一个改进版本,并且与 Log4j 具有较高的兼容性。Logback 同样支持异步日志记录,并且具有更好的性能。
  • SLF4J (Simple Logging Facade for Java)
    SLF4J 不是一个实际的日志实现,而是一个抽象层或门面(Facade)。它的目的是提供一个简单的 API,以便于开发者编写日志代码,同时允许在运行时动态地绑定到不同的日志框架(如 Logback、Log4j、java.util.logging 等)。这样可以在不影响应用代码的情况下更换底层的日志实现。

这些日志框架之间的关系可以总结为:

  • SLF4J 是一个日志门面,它提供了一套统一的日志 API。
  • Logback 默认实现了 SLF4J 接口,可以直接与 SLF4J 一起工作。
  • Log4j 和 Log4j2 可以通过适配器(如 slf4j-log4j12 或 log4j-slf4j-impl)与 SLF4J 一起使用。
  • Log4j 和 Log4j2 是独立的日志框架,直接提供日志功能,不需要通过 SLF4J。

在实际应用中,通常会选择一个具体的日志框架(如 Logback 或 Log4j2),并通过 SLF4J 来编写日志代码,以提高代码的可移植性和灵活性。我们项目使用的是log4j2日志框架进行配置,下面主要对log4j2日志框架配置进行梳理方便日后复习使用。

扫描二维码关注公众号,回复: 17409534 查看本文章

2. 配置简介

2.1 日志级别

log4j2有8个级别 从低到高为 ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF,我们作为web服务器,主要配置level为 INFO 级别

等级 描述
ALL 最低等级,用于打印所有日志记录信息
TRACE 追踪程序运行到哪里
DEBUG 消息颗粒度强调调试层面日志信息,展示详细执行信息
INFO 消息颗粒度突出强调应用正常的执行逻辑日志信息
WARN 输出警告
ERROR 输出错误日志信息
FATAL 输出每个严重错误时间,会导致应用程序退出日志
OFF 最高等级,关闭所有日志记录

2.2 Appenders种类

常用的文件追加器 appenders 信息如下

Appenders名称 具体作用
FlumeAppender 将几个不同源的日志汇集、集中到一处
RewriteAppender 对日志事件进行掩码或注入信息
RollingFileAppender 对日志文件进行封存
RoutingAppender 在输出地之间进行筛选路由
SMTPAppender 将LogEvent发送到指定邮件列表
SocketAppender 将LogEvent以普通格式发送到远程主机
SyslogAppender 将LogEvent以RFC 5424格式发送到远程主机
AsynchAppender 将一个LogEvent异步地写入多个不同输出地
ConsoleAppender 将LogEvent输出到控制台
FailoverAppender 维护一个队列,系统将尝试向队列中的Appender依次输出LogEvent,直到有一个成功为止

2.3 PatternLayout格式详解

PatternLayout 是最重要也是最常用的控制输出内容的节点,包括类名、时间、行号、日志级别、序号等都可以控制,同时还可以指定日志格式,可以使用正则表达式处理输出结果。

PatternLayout中包含的特殊字符包括\t,\n,\r,\f,用\输出单斜线,用%%输出%,下面是PatternLayout的部分参数

参数名称 类型 描述
charset String 输出的字符集。如果没有指定,则使用系统默认的字符集输出
pattern String 详见后面的pattern的表格
replace RegexReplacement 替换部分输出中的String。这将会调用一个与RegexReplacement转换器相同的函数,不同的是这是针对整个消息的
alwaysWriteExceptions boolean 默认为true。总是输出异常,即使没有定义异常对应的pattern,也会被附在所有pattern的最后。设为false则不会输出异常
header String 可选项。包含在每个日志文件的顶部
footer String 可选项。包含在每个日志文件的尾部
noConsoleNoAnsi boolean 默认为false。如果为true,且System.console()是null,则不会输出ANSI转义码

RegexReplacement部分常用参数

参数名称 类型 具体作用描述
regex String 将几个不同源的日志汇集、集中到一处
replacement String 任何匹配正则规则,被正则替换的后值

下面是pattern属性具体描述(这个表格仅包括了我能看懂的部分,还有很多看不懂并且试验也不成功的部分我都没写。另外用于控制输出结果颜色的highlight和style我也没有写,实在是感觉平时意义不大):

参数名 参数意义 详细描述
%c{参数}或%logger{参数} 输出logger的名称,即语句private static final Logger logger = LogManager.getLogger(App.class.getName())中App.class.getName()的值。也可以使用其他字符串 如果不带参数,则输出完整的logger名称;如果参数是整数n(只支持正整数),则先将logger名称依照小数点(.)分割成n段,然后取右侧的n段;如果参数不是整数,则除了最右侧的一段,其他整段字符都用这个字符代替,保留小数点;不管怎么写,最右侧的一段都保持不变。默认不带参数,并输出logger的完整名称。注意:上面的说明写得很烂,但是官方文档写得就这么烂,而且还不完整,看不懂是必然的,还请看官自己多试验,才能有所领会。如果看官自己懒得试验又看不懂,只能建议不要加参数,直接%c输出完整值。
%C{参数}或%class{参数} 输出类名。注意,这个是大写C,上面是小写c 参数规则与%c完全一样,请参见上面的说明。
%d{参数}{时区te{参数}{时区} 输出时间。 第一个大括号数可以是保留关键字,也可以是text.SimpleDateFormat字符拼接而成。保留关键字有:DEFAULT,ABSOLUTE, COMPACT, DATE, ISO8601, ISO8601_BASIC。第二个大括号中的参数是java.util.TimeZone.getTimeZone的值,可以设定时区
输出特殊字符 &, <, >, ”, ’全都要使用实体名称或实体编号替代,即官方文档说pattern删除了\r和\n,但是经我测试可以使用,明显是官方文档的错误
%F %file 输出文件名
highlight{pattern}{style} 高亮显示结果
%l 输出完整的错误位置,如com.future.ourfuture.test.test.App.tt(App.java:13) 注意1:这个是小写的L。注意2:使用该参数会影影响日志输出的性能
%L 输出错误行号,如“13” 注意:使用该参数会影响日志输出的性能
%m或%msg或%message 输出错误信息,即logger.error(String msg)中的msg
%M或%method 输出方法名,如“main”,“getMsg”等字符串
%n 换行符 根据系统自行决定,如Windows是”\r\n”,Linux是”\n”
%level{参数1}{参数2}{参数3} 参数1用来替换日志信息的级别,格式为:{level=label, level=label, …},即使用label代替的字符串替换level。其中level为日志的级别,如WARN/DEBUG/ERROR/TRACE/INFO参数2表示只保留前n个字符。格式为length=n,n为整型。但参数1中指定了label的字符串不受此参数限制参数3表示结果是大写还是小写。参数1指定了label的字符串不受此参数限制
%r或%relative 输出自JVM启动以来到log事件开始时的毫秒数
replace{pattern}{regex}{substitution} 将pattern的输出结果,按照正则表达式regex匹配后,使用substitution字符串替换 例如:"%replace{%logger }{.}{/}就是将所有%logger中的小数点(.)全部替换为斜杠,如果%logger是com.future.ourfuture.test.test.App则输出为com/future/ourfuture/test/test/App。pattern中可以写多个表达式,如%replace{%logger%msg%C}{.}{/}%n
%sn或%sequenceNumber 自增序号,每执行一次log事件,序号+1,是一个static变量。
%t或%thread 创建logging事件的线程名
%u{RANDOM|TIME}或%uuid{RANDOM|TIME} 依照一个随机数或当前机器的MAC和时间戳来随机生成一个UUID

patten表达式

pattern表达式 logger名称 响应结果
%c{1} org.apache.com.te.Foo Foo
%c{2} org.apache.com.te.Foo te.Foo
%c{1.} org.apache.com.te.Foo o.a.c.t.Foo
%c{1.1.!} org.apache.com.te.Foo o.a.!.!.Foo
%c{.} org.apache.com.te.Foo ….Foo

pattern的对齐修饰
对齐修饰,可以指定信息的输出格式,如是否左对齐,是否留空格等。

编写格式为在任何pattern和%之间加入一个小数,可以是正数,也可以是负数。如%10.20c表示对logger的信息进行处理。%-10.20m表示对message进行处理。

整数表示右对齐,负数表示左对齐;整数位表示输出信息的最小10个字符,如果输出信息不够10个字符,将用空格补齐;小数位表示输出信息的最大字符数,如果超过20个字符,则只保留最后20个字符的信息(注意:保留的是后20个字符,而不是前20个字符)。下面是一些示例。

格式 是否左对齐 最小宽度 最大宽度 说明
%20 右对齐 20 右对齐,不足20个字符则在信息前面用空格补足,超过20个字符则保留原信息
%-20 左对齐 20 左对齐,不足20个字符则在信息后面用空格补足,超过20个字符则保留原信息
%.30 不对齐 30 如果信息超过30个字符,则只保留最后30个字符
%20.30 右对齐 20 30 右对齐,不足20个字符则在信息前面用空格补足,超过30个字符则只保留最后30个字符
%-20.30 左对齐 20 30 左对齐,不足20个字符则在信息后面用空格补足,超过30个字符则只保留最后30个字符

我们项目中的log4j2.xml日志格式具体配置日志格式附带格式注释信息如下:

<!-- elk日志格式 -->
<property name="patternLayout">[%d{
    
    yyyy-MM-dd'T'HH:mm:ss.SSSZZ}] [%level{
    
    length=5}] [%traceId] [%logger] [${sys:hostName}] [${sys:ip}] [${sys:applicationName}] [%F,%L,%C,%M] [%m] ## '%ex'%n</property>
  • [%d{yyyy-MM-dd’T’HH:mm:ss.SSSZZ}] 日期 美国时间
  • [%level{length=5}] 日志级别
  • [%traceId] 链路追踪id,skyWalking使用
  • [%logger] 记录日志的类或包的全限定名。这有助于在日志输出中明确标识日志来源
  • [${sys:hostName}] 自定义主机名称,System.setProperty(“hostName”, NetUtil.getLocalHostName());
  • [${sys:ip}] 自定义系统ip信息 ,System.setProperty(“ip”, NetUtil.getLocalIp());
  • [${sys:applicationName}] 应用名称
  • [%F,%L,%C,%M] / [当前执行类, 行号, 全类名, 方法名称]
  • [%m] 日志输出内容
  • ##自己特殊约定
  • '%ex'%n 两个引号将异常包裹,打出异常时候方便解析 如何抛异常 和 换行

log4j2 patternLayOut参考此博文

3.项目中使用

我们整体的使用需要先引入pom文件,将服务ip信息存入到系统变量中,供log4j2配置文件使用, 再进行log4j2配置文件配置,最后在代码中可以使用slf4j日志门脸进行日志调用,添加日志后通过elk 我们可以快速定为到线上的问题,哪个服务在哪个机器上,具体发生了哪些问题,提高生产问题排错效率。

3.1 pom配置

在spring boot项目中,默认使用的日志框架是Logback,所以我们需要排除掉其自身引用的日志框架再引入log4j2日志jar包。引入pom内容如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.4</version>
</dependency>

3.2 ip信息初始化到系统变量中

通过代码获取ip信息,具体工具类NetUtil实现如下

package cn.git.elk.util;

import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketChannel;
import java.net.UnknownHostException;
import java.nio.channels.SocketChannel;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @program: bank-credit-sy
 * @description: $NetUtil 获取ip地址hostname工具类
 * @author: lixuchun
 * @create: 2021-02-04 14:52
 */
public class NetUtil {
    
       
    // 正则表达式模式,用于匹配 IP 地址
    private static Pattern pattern;
    // IP 地址分隔符限制长度
    private static Integer BLOCKS_LIMIT_LENGTH_2 = 2;
    // 默认主机名
    private static String HOST = "0.0.0.0";

    /**
     * 格式化输入的地址字符串,确保其包含主机名和端口号,并默认设置端口为 80。
     * @param address 地址字符串
     * @return 格式化后的地址字符串
     */
    public static String normalizeAddress(String address){
    
    
        // 将地址字符串按冒号分割成数组
        String[] blocks = address.split(":");
        // 检查地址是否有效
        if(blocks.length > BLOCKS_LIMIT_LENGTH_2){
    
    
            throw new IllegalArgumentException(address + " is invalid");
        }
        // 获取主机名
        String host = blocks[0];
        // 默认端口号为 80
        int port = 80;
        // 如果地址包含端口号,则提取端口号
        if(blocks.length > 1){
    
    
            port = Integer.valueOf(blocks[1]);
        } else {
    
    
            // 使用默认端口 80
            address += ":" + port;
        } 
        // 格式化并返回地址
        String serverAddr = String.format("%s:%d", host, port);
        return serverAddr;
    }
    
    /**
     * 如果输入地址中的主机名为“0.0.0.0”,则用本地 IP 地址替换后返回格式化的地址。
     * @param address 地址字符串
     * @return 格式化后的地址字符串
     */
    public static String getLocalAddress(String address){
    
    
        // 将地址字符串按冒号分割成数组
        String[] blocks = address.split(":");
        // 检查地址是否有效
        if(blocks.length != BLOCKS_LIMIT_LENGTH_2){
    
    
            throw new IllegalArgumentException(address + " is invalid address");
        } 
        // 获取主机名
        String host = blocks[0];
        // 获取端口号
        int port = Integer.valueOf(blocks[1]);
        
        // 如果主机名为“0.0.0.0”,则替换为本地 IP 地址
        if(HOST.equals(host)){
    
    
            return String.format("%s:%d", NetUtil.getLocalIp(), port);
        }
        // 否则直接返回原地址
        return address;
    }
    
    /**
     * 检查给定的 IP 是否匹配优先级列表中的前缀,并返回匹配的索引。
     * @param ip IP 地址
     * @param prefix 优先级列表
     * @return 匹配的索引
     */
    private static int matchedIndex(String ip, String[] prefix){
    
    
        // 遍历优先级列表
        for(int i=0; i<prefix.length; i++){
    
    
            String p = prefix[i];
            // 如果前缀为“*”,则检查 IP 是否为内网地址
            if("*".equals(p)){
    
    
                if(ip.startsWith("127.") ||
                   ip.startsWith("10.") ||	
                   ip.startsWith("172.") ||
                   ip.startsWith("192.")){
    
    
                    continue;
                }
                return i;
            } else {
    
    
                // 检查 IP 是否以指定前缀开头
                if(ip.startsWith(p)){
    
    
                    return i;
                }
            } 
        }
        
        // 如果没有匹配,则返回 -1
        return -1;
    }
    
    /**
     * 获取本地 IP 地址,根据优先级选择最优 IP 地址;如果没有指定优先级,则使用默认优先级顺序。
     * @param ipPreference IP 优先级字符串
     * @return 本地 IP 地址
     */
    public static String getLocalIp(String ipPreference) {
    
    
        // 如果未指定优先级,则使用默认优先级
        if(ipPreference == null){
    
    
            ipPreference = "*>10>172>192>127";
        }
        // 分割优先级字符串
        String[] prefix = ipPreference.split("[> ]+");
        try {
    
    
            // 编译正则表达式模式
            pattern = Pattern.compile(PATTEN_COMPARE_RULES);
            // 获取所有网络接口
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            // 初始化最佳匹配 IP 和索引
            String matchedIp = null;
            int matchedIdx = -1;
            // 遍历所有网络接口
            while (interfaces.hasMoreElements()) {
    
    
                NetworkInterface ni = interfaces.nextElement();
                // 获取每个网络接口的所有 IP 地址
                Enumeration<InetAddress> en = ni.getInetAddresses(); 
                // 遍历所有 IP 地址
                while (en.hasMoreElements()) {
    
    
                    InetAddress addr = en.nextElement();
                    String ip = addr.getHostAddress();  
                    // 匹配 IP 地址
                    Matcher matcher = pattern.matcher(ip);
                    if (matcher.matches()) {
    
      
                        // 获取匹配的索引
                        int idx = matchedIndex(ip, prefix);
                        if(idx == -1) {
    
    
                            continue;
                        }
                        // 更新最佳匹配 IP 和索引
                        if(matchedIdx == -1){
    
    
                            matchedIdx = idx;
                            matchedIp = ip;
                        } else {
    
    
                            if(matchedIdx > idx){
    
    
                                matchedIdx = idx;
                                matchedIp = ip;
                            }
                        }
                    } 
                } 
            } 
            // 如果找到最佳匹配 IP,则返回;否则返回“127.0.0.1”
            if(matchedIp != null) {
    
    
                return matchedIp;
            }
            return "127.0.0.1";
        } catch (Exception e) {
    
     
            return "127.0.0.1";
        }
    }
    
    /**
     * 获取本地 IP 地址,默认使用优先级顺序。
     * @return 本地 IP 地址
     */
    public static String getLocalIp() {
    
    
        return getLocalIp("*>10>172>192>127");
    }
    
    /**
     * 返回给定 SocketChannel 对象的远程地址信息。
     * @param channel SocketChannel 对象
     * @return 远程地址信息
     */
    public static String remoteAddress(SocketChannel channel){
    
    
        // 获取远程地址
        SocketAddress addr = channel.socket().getRemoteSocketAddress();
        // 格式化并返回地址信息
        String res = String.format("%s", addr);
        return res;
    }
    
    /**
     * 返回给定 SocketChannel 对象的本地地址信息,去掉可能存在的冒号前缀。
     * @param channel SocketChannel 对象
     * @return 本地地址信息
     */
    public static String localAddress(SocketChannel channel){
    
    
        // 获取本地地址
        SocketAddress addr = channel.socket().getLocalSocketAddress();
        // 格式化并返回地址信息
        String res = String.format("%s", addr);
        // 如果地址不为空,则去掉第一个字符(通常是“/”)
        return addr == null ? res : res.substring(1);
    }
    
    /**
     * 获取当前 Java 进程 ID。
     * @return 进程 ID
     */
    public static String getPid(){
    
    
        // 获取运行时管理对象
        RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        // 获取名称
        String name = runtime.getName();
        // 查找“@”符号的位置
        int index = name.indexOf("@");
        if (index != -1) {
    
    
            // 提取进程 ID
            return name.substring(0, index);
        }
        // 如果未找到,则返回 null
        return null;
    }
    
    /**
     * 获取本地主机名。
     * @return 本地主机名
     */
    public static String getLocalHostName() {
    
    
        try {
    
    
            // 获取本地 IP 地址并提取主机名
            return (InetAddress.getLocalHost()).getHostName();
        } catch (UnknownHostException uhe) {
    
    
            // 处理异常情况
            String host = uhe.getMessage();
            if (host != null) {
    
    
                int colon = host.indexOf(':');
                if (colon > 0) {
    
    
                    // 提取主机名部分
                    return host.substring(0, colon);
                }
            }
            // 如果无法获取主机名,则返回“UnknownHost”
            return "UnknownHost";
        }
    }
}

初始化ip信息到系统变量中代码部分如下

package cn.git.init;

import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/** 
 * @description: 初始化设置ip hostname等信息通用类
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-01-03
 */
@Component
public class InitLogIpHost implements EnvironmentAware {
    
    

    private static Environment environment;


    @PostConstruct
    public void initIpHostEnvInfo() {
    
    
        // 设置 applicationName
        System.setProperty("applicationName", environment.getProperty("spring.application.name"));
        // 设置 ip
        System.setProperty("ip", NetUtil.getLocalIp());
        // 设置 hostname
        System.setProperty("hostName", NetUtil.getLocalHostName());
    }

    /**
     * Set the {@code Environment} that this component runs in.
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
    
    
        InitLogIpHost.environment = environment;
    }
}

3.3 log4j2.xml日志文件配置

<?xml version="1.0" encoding="UTF-8"?>
<Configuration schema="Log4J-V2.0.xsd" monitorInterval="600">
    <!-- 配置全局属性 -->
    <Properties>
        <!-- 日志文件保存的基本路径 -->
        <Property name="LOG_HOME">logs</Property>
        <!-- 日志文件的基础名称 -->
        <property name="FILE_NAME">docker-server</property>
        <!-- 日志输出格式 -->
        <property name="patternLayout">[%d{yyyy-MM-dd'T'HH:mm:ss.SSSZZ}] [%level{length=5}] [%traceId] [%logger] [${sys:hostName}] [${sys:ip}] [${sys:applicationName}] [%F,%L,%C,%M] [%m] ## '%ex'%n</property>
    </Properties>

    <!-- 定义不同的日志输出目的地 -->
    <Appenders>
        <!-- 控制台输出 -->
        <Console name="CONSOLE" target="SYSTEM_OUT">
            <!-- 使用定义的日志格式 -->
            <PatternLayout pattern="${patternLayout}"/>
            <!-- 只允许 info 级别及以上的日志输出到控制台 -->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </Console>

        <!-- 应用程序日志滚动文件 -->
        <RollingRandomAccessFile name="appAppender" fileName="${LOG_HOME}/app-${FILE_NAME}.log" filePattern="${LOG_HOME}/app-${FILE_NAME}-%d{yyyy-MM-dd}-%i.log" >
            <!-- 使用定义的日志格式 -->
            <PatternLayout pattern="${patternLayout}" />
            <!-- 滚动策略 -->
            <Policies>
                <!--
					根据当前filePattern配置"%d{yyyy-MM-dd}",每interval天滚动一次
                    "%d{yyyy-MM-dd HH-mm}" 则为每interval分钟滚动一次
				-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 文件大小超过 500MB 时滚动 -->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy 属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingRandomAccessFile>

        <!-- Druid SQL 日志滚动文件 -->
        <RollingRandomAccessFile name="druidSqlRollingFile" fileName="${LOG_HOME}/druid/app-${FILE_NAME}-druid.log" filePattern="${LOG_HOME}/app-${FILE_NAME}-druid-%d{yyyy-MM-dd}-%i.log" >
            <!-- 使用定义的日志格式 -->
            <PatternLayout pattern="${patternLayout}" />
            <!-- 滚动策略 -->
            <Policies>
                <!-- 每天滚动一次 -->
                <TimeBasedTriggeringPolicy interval="1"/>
                <!-- 文件大小超过 500MB 时滚动 -->
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <!-- 最多保留 20 个旧日志文件 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingRandomAccessFile>

        <!-- skywalking GRPC 日志客户端 Appender -->
        <GRPCLogClientAppender name="grpc-log">
            <!-- 使用简单的日志格式 -->
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </GRPCLogClientAppender>
    </Appenders>

    <!-- 定义不同日志记录器 -->
    <Loggers>
        <!-- 关闭 org.apache.kafka 包下的所有日志输出 -->
        <logger name="org.apache.kafka" level="off"/>

        <!-- 设置 druid 包的日志级别为 error,并关闭继承父 Logger 的行为 -->
        <logger name="druid" level="error" additivity="false">
            <appender-ref ref="druidSqlRollingFile"/>
        </logger>

        <!-- 设置 cn.git.* 包的日志级别为 info,并关闭继承父 Logger 的行为 -->
        <logger name="cn.git.*" level="info" additivity="false">
            <AppenderRef ref="grpc-log"/>
        </logger>

        <!-- 创建一个异步 Logger,用于处理 cn.git.* 包的日志,并指定日志输出到 appAppender -->
        <AsyncLogger name="cn.git.*" level="info" includeLocation="true">
            <AppenderRef ref="appAppender"/>
        </AsyncLogger>

        <!-- 设置根 Logger 的日志级别为 info,并指定日志输出到控制台、appAppender 和 grpc-log -->
        <root level="info">
            <AppenderRef ref="CONSOLE"/>
            <Appender-Ref ref="appAppender"/>
            <AppenderRef ref="grpc-log"/>
        </root>
    </Loggers>
</Configuration>

4. 测试

我们启动服务,然后在定时任务中打印一个简单的日志信息,并且运行时候可能会报错,task代码如下

package cn.git.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/** 
 * @description: 简单定时任务
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-07-10
 */
@Slf4j
@Component
@EnableScheduling
public class TimerTask {
    
    

    /**
     * 每5秒执行一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void timer() {
    
    
        log.info("定时任务执行 : " + System.currentTimeMillis());
        if (System.currentTimeMillis() % 2 == 0) {
    
    
            throw new RuntimeException("异常啦!");
        }
    }
}

我们观察运行结果日志信息如下

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:61667,suspend=y,server=n -Dvisualvm.id=6066284867800 -javaagent:C:\Users\Administrator.DESKTOP-40G9I84\AppData\Local\JetBrains\IdeaIC2020.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\idea_workspace_activiti_change\docker-hello\target\classes;D:\apache-maven-3.6.3\repos\org\projectlombok\lombok\1.18.6\lombok-1.18.6.jar;D:\apache-maven-3.6.3\repos\cn\hutool\hutool-all\5.5.7\hutool-all-5.5.7.jar;D:\apache-maven-3.6.3\repos\com\alibaba\fastjson\1.2.83\fastjson-1.2.83.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-starter-web\2.3.8.RELEASE\spring-boot-starter-web-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-starter\2.3.8.RELEASE\spring-boot-starter-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;D:\apache-maven-3.6.3\repos\org\yaml\snakeyaml\1.26\snakeyaml-1.26.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-starter-json\2.3.8.RELEASE\spring-boot-starter-json-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;D:\apache-maven-3.6.3\repos\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-starter-tomcat\2.3.8.RELEASE\spring-boot-starter-tomcat-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\apache\tomcat\embed\tomcat-embed-core\9.0.41\tomcat-embed-core-9.0.41.jar;D:\apache-maven-3.6.3\repos\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;D:\apache-maven-3.6.3\repos\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.41\tomcat-embed-websocket-9.0.41.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-web\5.2.12.RELEASE\spring-web-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-beans\5.2.12.RELEASE\spring-beans-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-webmvc\5.2.12.RELEASE\spring-webmvc-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-aop\5.2.12.RELEASE\spring-aop-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-context\5.2.12.RELEASE\spring-context-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-expression\5.2.12.RELEASE\spring-expression-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-starter-log4j2\2.3.8.RELEASE\spring-boot-starter-log4j2-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\apache\logging\log4j\log4j-slf4j-impl\2.13.3\log4j-slf4j-impl-2.13.3.jar;D:\apache-maven-3.6.3\repos\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;D:\apache-maven-3.6.3\repos\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;D:\apache-maven-3.6.3\repos\org\apache\logging\log4j\log4j-core\2.13.3\log4j-core-2.13.3.jar;D:\apache-maven-3.6.3\repos\org\apache\logging\log4j\log4j-jul\2.13.3\log4j-jul-2.13.3.jar;D:\apache-maven-3.6.3\repos\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;D:\apache-maven-3.6.3\repos\com\lmax\disruptor\3.3.4\disruptor-3.3.4.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-core\5.2.12.RELEASE\spring-core-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\spring-jcl\5.2.12.RELEASE\spring-jcl-5.2.12.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot-autoconfigure\2.3.8.RELEASE\spring-boot-autoconfigure-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\org\springframework\boot\spring-boot\2.3.8.RELEASE\spring-boot-2.3.8.RELEASE.jar;D:\apache-maven-3.6.3\repos\com\huaban\jieba-analysis\1.0.2\jieba-analysis-1.0.2.jar;D:\apache-maven-3.6.3\repos\org\apache\commons\commons-lang3\3.10\commons-lang3-3.10.jar;D:\apache-maven-3.6.3\repos\com\github\whvcse\easy-captcha\1.6.2\easy-captcha-1.6.2.jar;D:\apache-maven-3.6.3\repos\com\jcraft\jsch\0.1.55\jsch-0.1.55.jar;D:\apache-maven-3.6.3\repos\commons-net\commons-net\3.7\commons-net-3.7.jar;D:\IntelliJ IDEA Community Edition 2020.3.1\lib\idea_rt.jar" cn.git.helloApplication
Connected to the target VM, address: '127.0.0.1:61667', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.8.RELEASE)

[2024-09-09T09:41:36.395+08:00] [INFO] [mainraceId] [cn.git.helloApplication] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [StartupInfoLogger.java,55,org.springframework.boot.StartupInfoLogger,logStarting] [Starting helloApplication on smallBigPower with PID 18696 (D:\idea_workspace_activiti_change\docker-hello\target\classes started by Administrator in D:\idea_workspace_activiti_change\docker-hello)] ## ''
[2024-09-09T09:41:36.401+08:00] [INFO] [mainraceId] [cn.git.helloApplication] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [SpringApplication.java,651,org.springframework.boot.SpringApplication,logStartupProfileInfo] [No active profile set, falling back to default profiles: default] ## ''
[2024-09-09T09:41:37.169+08:00] [INFO] [mainraceId] [org.springframework.boot.web.embedded.tomcat.TomcatWebServer] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [TomcatWebServer.java,108,org.springframework.boot.web.embedded.tomcat.TomcatWebServer,initialize] [Tomcat initialized with port(s): 8088 (http)] ## ''
[2024-09-09T09:41:37.179+08:00] [INFO] [mainraceId] [org.apache.coyote.http11.Http11NioProtocol] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [DirectJDKLog.java,173,org.apache.juli.logging.DirectJDKLog,log] [Initializing ProtocolHandler ["http-nio-8088"]] ## ''
[2024-09-09T09:41:37.180+08:00] [INFO] [mainraceId] [org.apache.catalina.core.StandardService] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [DirectJDKLog.java,173,org.apache.juli.logging.DirectJDKLog,log] [Starting service [Tomcat]] ## ''
[2024-09-09T09:41:37.180+08:00] [INFO] [mainraceId] [org.apache.catalina.core.StandardEngine] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [DirectJDKLog.java,173,org.apache.juli.logging.DirectJDKLog,log] [Starting Servlet engine: [Apache Tomcat/9.0.41]] ## ''
[2024-09-09T09:41:37.229+08:00] [INFO] [mainraceId] [org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/]] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [DirectJDKLog.java,173,org.apache.juli.logging.DirectJDKLog,log] [Initializing Spring embedded WebApplicationContext] ## ''
[2024-09-09T09:41:37.229+08:00] [INFO] [mainraceId] [org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [ServletWebServerApplicationContext.java,285,org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext,prepareWebApplicationContext] [Root WebApplicationContext: initialization completed in 796 ms] ## ''
[2024-09-09T09:41:37.263+08:00] [INFO] [mainraceId] [cn.git.init.AnalyzerInit] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [AnalyzerInit.java,44,cn.git.init.AnalyzerInit,analyzerInit] [开始加载分词词典信息,获取自定义词典路径[/D:/idea_workspace_activiti_change/docker-hello/target/classes/dict/custom.dict]] ## ''
main dict load finished, time elapsed 373 ms
user dict D:\idea_workspace_activiti_change\docker-hello\target\classes\dict\custom.dict load finished, tot words:7839, time elapsed:9ms
[2024-09-09T09:41:37.647+08:00] [INFO] [mainraceId] [cn.git.init.AnalyzerInit] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [AnalyzerInit.java,48,cn.git.init.AnalyzerInit,analyzerInit] [加载自定义词典信息完毕] ## ''
[2024-09-09T09:41:37.785+08:00] [INFO] [mainraceId] [cn.git.init.AnalyzerInit] [smallBigPower] [${sys:ip}] [${sys:applicationName}] [AnalyzerInit.java,61,cn.git.init.AnalyzerInit,analyzerInit] [数据库中敏感分词加载完毕!] ## ''
[2024-09-09T09:41:38.093+08:00] [INFO] [mainraceId] [org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor] [smallBigPower] [192.168.220.1] [docker-hello] [ExecutorConfigurationSupport.java,181,org.springframework.scheduling.concurrent.ExecutorConfigurationSupport,initialize] [Initializing ExecutorService 'applicationTaskExecutor'] ## ''
[2024-09-09T09:41:38.770+08:00] [INFO] [mainraceId] [org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] [smallBigPower] [192.168.220.1] [docker-hello] [ExecutorConfigurationSupport.java,181,org.springframework.scheduling.concurrent.ExecutorConfigurationSupport,initialize] [Initializing ExecutorService 'taskScheduler'] ## ''
[2024-09-09T09:41:38.778+08:00] [INFO] [mainraceId] [org.apache.coyote.http11.Http11NioProtocol] [smallBigPower] [192.168.220.1] [docker-hello] [DirectJDKLog.java,173,org.apache.juli.logging.DirectJDKLog,log] [Starting ProtocolHandler ["http-nio-8088"]] ## ''
[2024-09-09T09:41:38.795+08:00] [INFO] [mainraceId] [org.springframework.boot.web.embedded.tomcat.TomcatWebServer] [smallBigPower] [192.168.220.1] [docker-hello] [TomcatWebServer.java,220,org.springframework.boot.web.embedded.tomcat.TomcatWebServer,start] [Tomcat started on port(s): 8088 (http) with context path ''] ## ''
[2024-09-09T09:41:38.804+08:00] [INFO] [mainraceId] [cn.git.helloApplication] [smallBigPower] [192.168.220.1] [docker-hello] [StartupInfoLogger.java,61,org.springframework.boot.StartupInfoLogger,logStarted] [Started helloApplication in 2.72 seconds (JVM running for 3.189)] ## ''
[2024-09-09T09:41:40.008+08:00] [INFO] [scheduling-1raceId] [cn.git.task.TimerTask] [smallBigPower] [192.168.220.1] [docker-hello] [TimerTask.java,24,cn.git.task.TimerTask,timer] [定时任务执行 : 1725846100008] ## ''
[2024-09-09T09:41:40.009+08:00] [ERROR] [scheduling-1raceId] [org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler] [smallBigPower] [192.168.220.1] [docker-hello] [TaskUtils.java,95,org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler,handleError] [Unexpected error occurred in scheduled task] ## ' java.lang.RuntimeException: 异常啦!
	at cn.git.task.TimerTask.timer(TimerTask.java:26)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)
'
Disconnected from the target VM, address: '127.0.0.1:61667', transport: 'socket'

Process finished with exit code -1

项目源码地址