如何自定义一个log适配器starter

 更新时间:2025年06月10日 11:56:51   作者:FLGB  
这篇文章主要介绍了如何自定义一个log适配器starter的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

需求

为了适配现有日志平台,java项目应用日志需要添加自定义字段:

日志关键字段:

  • app:应用名称
  • host:主机IP
  • env:环境(DEV、UAT、GRAY、PRO)
  • namespace:命名空间(默认main,多版本用到)
  • message:日志内容
  • logCategory:日志分类 (HttpServer、HttpClient、DB、Job)
  • level:日志等级(Debug、Info、Warn、Error、Fatal)
  • error:错误明细,可以为错误堆栈信息
  • createdOn:写日志时间,毫秒时间戳,比如1725961448565

格式需要改编成json

{“app”:“formula”,“namespace”:“main”,“host”:“127.0.0.1”,“env”:“DEV”,“createdOn”:“2025-04-23T13:47:08.726+08:00”,“level”:“INFO”,“message”:“(♥◠‿◠)ノ゙启动成功 ლ(´ڡ`ლ)゙”}

Starter 项目目录结构

logback-starter/
│
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── lf
│ │ │ └── logbackstarter
│ │ │ ├── config
│ │ │ │ ├── MDCInterceptor.java
│ │ │ │ ├── LogInitializer.java
│ │ │ │ └── LogbackInterceptorAutoConfiguration.java
│ │ │ │ └── LogbackProperties
│ │ │ └── LogbackAutoConfiguration.java
│ │ └── Resources
│ │ │ └── logback.xml
│ │ │ └── META-INF
│ │ │ └── spring.factories
└── pom.xml

pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.kayou</groupId>
  <artifactId>java-logs-starter</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>java-logs-starter</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <spring-boot.version>2.6.3</spring-boot.version>
  </properties>

  <!-- 只声明依赖,不引入依赖 -->
  <dependencyManagement>
    <dependencies>
      <!-- 声明springBoot版本 -->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    <!-- Logback Classic -->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.6.3</version>
        <!--                <configuration>-->
        <!--                </configuration>-->
        <!--                <executions>-->
        <!--                    <execution>-->
        <!--                        <goals>-->
        <!--                            <goal>repackage</goal>-->
        <!--                        </goals>-->
        <!--                    </execution>-->
        <!--                </executions>-->
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

LogInitializer实现

import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;

@Configuration
@Order
public class LogInitializer {

    private final LogbackProperties properties;

    public LogInitializer(LogbackProperties properties) {
        this.properties = properties;
    }

    @PostConstruct
    public void init() {
        MDC.put("app", properties.getApp());
        MDC.put("env", properties.getEnv());
        MDC.put("namespace", properties.getNamespace());
        MDC.put("host", resolveLocalHostIp());
    }

    private String resolveLocalHostIp() {

        // 获取 Linux 系统下的主机名/IP
        InetAddress inetAddress = null;
        try {
            inetAddress = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {

            return "unknown";
        }
        return inetAddress.getHostAddress();
    }
}

MDCInterceptor 实现

MDCInterceptor 用于在每个请求的生命周期中设置 MDC。

package com.lf;

import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MDCInterceptor implements HandlerInterceptor {

    private final LogbackProperties properties;

    public MDCInterceptor(LogbackProperties properties) {
        this.properties = properties;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        MDC.put("app", properties.getApp());
        MDC.put("env", properties.getEnv());
        MDC.put("namespace", properties.getNamespace());
        MDC.put("host", properties.getHost());
        return true;
    }
}

LogbackInterceptorAutoConfiguration实现

@Configuration
public class LogbackInterceptorAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(MDCInterceptor.class)
    public MDCInterceptor mdcInterceptor(LogbackProperties properties) {
        return new MDCInterceptor(properties);
    }

    @Bean
    public WebMvcConfigurer logbackWebMvcConfigurer(MDCInterceptor mdcInterceptor) {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(mdcInterceptor).addPathPatterns("/**");
            }
        };
    }
}

LogbackProperties

@ConfigurationProperties(prefix = "log.context")
public class LogbackProperties {
    private String app = "default-app";
    private String env = "default-env";
    private String namespace = "default-namespace";
    private String host = "";

    // Getter & Setter

    public String getApp() {
        return app;
    }

    public void setApp(String app) {
        this.app = app;
    }

    public String getEnv() {
        return env;
    }

    public void setEnv(String env) {
        this.env = env;
    }

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public String getHost() {
        if (host != null && !host.isEmpty()) {
            return host;
        }
        return resolveLocalHostIp();
    }

    public void setHost(String host) {
        this.host = host;
    }

    private String resolveLocalHostIp() {

        // 获取 Linux 系统下的主机名/IP
        InetAddress inetAddress = null;
        try {
            inetAddress = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {

            return "unknown";
        }
        return inetAddress.getHostAddress();

    }
}

LogbackAutoConfiguration

@Configuration
@EnableConfigurationProperties(LogbackProperties.class)
public class LogbackAutoConfiguration {
}

resource

logback.xml

<included>

    <property name="LOG_PATH" value="/home/logs"/>

    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <mdc>
                    <includeMdcKeyName>app</includeMdcKeyName>
                    <includeMdcKeyName>env</includeMdcKeyName>
                    <includeMdcKeyName>namespace</includeMdcKeyName>
                    <includeMdcKeyName>host</includeMdcKeyName>
                    <includeMdcKeyName>createdOn</includeMdcKeyName>
                </mdc>

                <timestamp>
                    <fieldName>timestamp</fieldName>
                    <pattern>UNIX_MILLIS</pattern>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>

                <logLevel fieldName="level"/>
                <message fieldName="message"/>
                <stackTrace fieldName="stack_trace"/>
            </providers>
        </encoder>
    </appender>

    <!-- 文件输出 -->
    <appender name="jsonLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <mdc>
                    <includeMdcKeyName>app</includeMdcKeyName>
                    <includeMdcKeyName>env</includeMdcKeyName>
                    <includeMdcKeyName>namespace</includeMdcKeyName>
                    <includeMdcKeyName>host</includeMdcKeyName>
                    <includeMdcKeyName>createdOn</includeMdcKeyName>
                </mdc>
                <!-- 显式指定毫秒时间戳的类 -->
                <timestamp>
                    <fieldName>timestamp</fieldName>
                    <pattern>UNIX_MILLIS</pattern>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>

                <logLevel fieldName="level"/>
                <message fieldName="message"/>
                <stackTrace fieldName="stack_trace"/>
            </providers>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="console"/>
        <appender-ref ref="jsonLog"/>
    </root>

</included>

META-INF

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.lf.LogbackAutoConfiguration,\
  com.lf.LogbackInterceptorAutoConfiguration,\
  com.lf.LogInitializer

使用starter

引用starter

在其他项目中添加依赖:(需要install本地仓库或deploy远程仓库)

<dependency>
    <groupId>com.kayou</groupId>
    <artifactId>java-logs-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

在resource中添加日志文件logback.xml

<configuration scan="true">
    <!-- 添加自动意logback配置 -->
    <property name="APP_NAME" value="java-demo"/>
    <!-- 引入公共的logback配置 -->
    <include resource="logback-default.xml"/>

</configuration>

启动日志效果

{"app":"java-demo","namespace":"default-namespace","host":"10.2.3.130","env":"dev","createdOn":"2025-04-23T14:41:57.981+08:00","level":"INFO","message":"Exposing 13 endpoint(s) beneath base path '/actuator'"}
{"app":"java-demo","namespace":"default-namespace","host":"10.2.3.130","env":"dev","createdOn":"2025-04-23T14:41:58.014+08:00","level":"INFO","message":"Tomcat started on port(s): 8090 (http) with context path ''"}
{"app":"java-demo","namespace":"default-namespace","host":"10.2.3.130","env":"dev","createdOn":"2025-04-23T14:41:58.125+08:00","level":"INFO","message":"Started Application in 4.303 seconds (JVM running for 5.293)"}

自定义Provider实现日志自定义字段格式

平台日志需要日志level 为首字母大写,时间createdOn 需要为时间戳,并且为Long数字, logback原生 mdc支持String 不支持其他类型

定义Provider

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.fasterxml.jackson.core.JsonGenerator;
import net.logstash.logback.composite.AbstractJsonProvider;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;

@Configuration
public class MdcTypeAwareProvider extends AbstractJsonProvider<ILoggingEvent> {

    private final Set<String> longFields = new HashSet<>();

    public MdcTypeAwareProvider() {
        longFields.add("createdOn"); // 指定需要转成 Long 类型的字段
    }

    @Override
    public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
        Map<String, String> mdcProperties = event.getMDCPropertyMap();
        if (mdcProperties == null || mdcProperties.isEmpty()) {
            return;
        }
        for (Map.Entry<String, String> entry : mdcProperties.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            // 处理 level 字段,将首字母大写
            if ("level".equalsIgnoreCase(key)) {
                value = value.substring(0, 1).toUpperCase() + value.substring(1).toLowerCase();
            }
            if (longFields.contains(key)) {
                try {
                    generator.writeNumberField(key, Long.parseLong(value));
                } catch (NumberFormatException e) {
                    generator.writeStringField(key, value); // fallback
                }
            } else {
                generator.writeStringField(key, value);
            }
        }
        // 将 level 作为日志的一个字段来写入
        String level = event.getLevel().toString();
        level = level.substring(0, 1).toUpperCase() + level.substring(1).toLowerCase();  // 首字母大写
        generator.writeStringField("level", level);
    }


}

spring.factories添加注入类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.kayou.LogbackAutoConfiguration,\
  com.kayou.LogbackInterceptorAutoConfiguration,\
  com.kayou.LogInitializer,\
  com.kayou.MdcTypeAwareProvider

resource logback.xml 改造

去除引用的mdc,新增自定义mdc provider

 <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>

                <provider class="com.kayou.MdcTypeAwareProvider"/>
                <!-- 显式指定毫秒时间戳的类 -->
                <timestamp>
                    <fieldName>createdTime</fieldName>
                    <pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>
                <message fieldName="message"/>
                <stackTrace fieldName="stack_trace"/>
            </providers>
        </encoder>
    </appender>

    <!-- 文件输出 -->
    <appender name="jsonLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <provider class="com.kayou.MdcTypeAwareProvider"/>
                <timestamp>
                    <fieldName>createdTime</fieldName>
                    <pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>
                <message fieldName="message"/>
                <stackTrace fieldName="stack_trace"/>
            </providers>

        </encoder>
    </appender>

启动日志输出结果

{“app”:“java-demo”,“namespace”:“default-namespace”,“host”:“10.2.3.130”,“env”:“dev”,“createdOn”:1745820638113,“level”:“Info”,“createdTime”:“2025-04-28 14:10:38.596”,“message”:“(♥◠‿◠)ノ゙启动成功 ლ(´ڡ`ლ)゙”}

优化异步线程日志切不到的问题

如过在web请求处理中,使用了异步线程,web线程就直接返回了。后续子线程是不会被intercetor切到的。改成日志格式不匹配

在MdcTypeAwareProvider 去填充这些字段就可以了

@Configuration
public class LogbackPropertiesHolder {

    private static LogbackProperties properties;

    public LogbackPropertiesHolder(LogbackProperties properties) {
        LogbackPropertiesHolder.properties = properties;
    }

    public static LogbackProperties getProperties() {
        return properties;
    }
}
@Configuration
public class MdcTypeAwareProvider extends AbstractJsonProvider<ILoggingEvent> {

    private final Set<String> longFields = new HashSet<>();

    public MdcTypeAwareProvider() {
        longFields.add("createdOn");
    }

    @Override
    public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
        Map<String, String> mdcProperties = event.getMDCPropertyMap();
        LogbackProperties properties = LogbackPropertiesHolder.getProperties();

        ensureMdcProperty(mdcProperties, "app", properties.getApp());
        ensureMdcProperty(mdcProperties, "env", properties.getEnv());
        ensureMdcProperty(mdcProperties, "namespace", properties.getNamespace());
        ensureMdcProperty(mdcProperties, "host", resolveLocalHostIp());
        ensureMdcProperty(mdcProperties, "createdOn", String.valueOf(System.currentTimeMillis()));

        for (Map.Entry<String, String> entry : mdcProperties.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();

            if (longFields.contains(key)) {
                try {
                    generator.writeNumberField(key, Long.parseLong(value));
                } catch (NumberFormatException e) {
                    generator.writeStringField(key, value);
                }
            } else {
                generator.writeStringField(key, value);
            }
        }

        String level = event.getLevel().toString();
        generator.writeStringField("level", level.substring(0, 1).toUpperCase() + level.substring(1).toLowerCase());
    }

    private void ensureMdcProperty(Map<String, String> mdcProperties, String key, String defaultValue) {
        if (!mdcProperties.containsKey(key)) {
            MDC.put(key, defaultValue);
        }
    }

    private String resolveLocalHostIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            return "127.0.0.1";
        }
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java lambda循环_使用Java 8 Lambda简化嵌套循环操作

    java lambda循环_使用Java 8 Lambda简化嵌套循环操作

    这篇文章主要介绍了java lambda循环_使用Java 8 Lambda简化嵌套循环操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • 解析ConcurrentHashMap: transfer方法源码分析(难点)

    解析ConcurrentHashMap: transfer方法源码分析(难点)

    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment的结构和HashMap类似,是一种数组和链表结构,今天给大家普及java面试常见问题---ConcurrentHashMap知识,一起看看吧
    2021-06-06
  • Java动态数组Arraylist存放自定义数据类型方式

    Java动态数组Arraylist存放自定义数据类型方式

    这篇文章主要介绍了Java动态数组Arraylist存放自定义数据类型方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • springboot 在linux后台运行的方法

    springboot 在linux后台运行的方法

    这篇文章主要介绍了springboot 在linux后台运行的方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-06-06
  • SpringCloud Webflux过滤器增加header传递方式

    SpringCloud Webflux过滤器增加header传递方式

    这篇文章主要介绍了SpringCloud Webflux过滤器增加header传递方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • IntelliJ IDEA像Eclipse一样打开多个项目的图文教程

    IntelliJ IDEA像Eclipse一样打开多个项目的图文教程

    这篇文章主要介绍了IntelliJ IDEA像Eclipse一样打开多个项目的方法图文教程讲解,需要的朋友可以参考下
    2018-03-03
  • 详解Java Unsafe如何花式操作内存

    详解Java Unsafe如何花式操作内存

    C++可以动态的分类内存,而java并不能这样,是不是java就不能操作内存呢,其实是有其他办法可以操作内存的,下面就一起看看Unsafe是如何花式操作内存的吧
    2023-08-08
  • 详谈Java中的Object、T(泛型)、?区别

    详谈Java中的Object、T(泛型)、?区别

    下面小编就为大家带来一篇详谈Java中的Object、T(泛型)、?区别。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • java定时任务Timer和TimerTask使用详解

    java定时任务Timer和TimerTask使用详解

    这篇文章主要为大家详细介绍了java定时任务Timer和TimerTask使用方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-02-02
  • Springboot核心机制详细介绍

    Springboot核心机制详细介绍

    SpringBoot的核心机制包括自动配置、起步依赖、主类和运行器、以及嵌入式服务器等,通过这些机制,SpringBoot简化了应用开发过程,本文给大家介绍Springboot核心机制,感兴趣的朋友一起看看吧
    2024-11-11

最新评论