简单的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日志框架配置进行梳理方便日后复习使用。

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的部分参数

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

RegexReplacement部分常用参数

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

下面是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.FooFoo
%c{2}org.apache.com.te.Foote.Foo
%c{1.}org.apache.com.te.Fooo.a.c.t.Foo
%c{1.1.!}org.apache.com.te.Fooo.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右对齐2030右对齐,不足20个字符则在信息前面用空格补足,超过30个字符则只保留最后30个字符
%-20.30左对齐2030左对齐,不足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

项目源码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值