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

2025-06-10 15:50

本文主要是介绍如何自定义一个log适配器starter,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《如何自定义一个log适配器starter》:本文主要介绍如何自定义一个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>-->
   python     <!--                        </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(javascriptHttpServletRequest 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 不支持其他类型

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

定义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,新增自定义mdcwww.chinasem.cn 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</www.chinasem.cnfieldName>
                    <pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
                    <timeZone>Asia/Shanghai</timeZone>
mrGjsMrWE                </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";
        }
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持China编程(www.chinasem.cn)。

这篇关于如何自定义一个log适配器starter的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154989

相关文章

Druid连接池实现自定义数据库密码加解密功能

《Druid连接池实现自定义数据库密码加解密功能》在现代应用开发中,数据安全是至关重要的,本文将介绍如何在​​Druid​​连接池中实现自定义的数据库密码加解密功能,有需要的小伙伴可以参考一下... 目录1. 环境准备2. 密码加密算法的选择3. 自定义 ​​DruidDataSource​​ 的密码解密3

spring-gateway filters添加自定义过滤器实现流程分析(可插拔)

《spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔)》:本文主要介绍spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔),本文通过实例图... 目录需求背景需求拆解设计流程及作用域逻辑处理代码逻辑需求背景公司要求,通过公司网络代理访问的请求需要做请

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

SQL中redo log 刷⼊磁盘的常见方法

《SQL中redolog刷⼊磁盘的常见方法》本文主要介绍了SQL中redolog刷⼊磁盘的常见方法,将redolog刷入磁盘的方法确保了数据的持久性和一致性,下面就来具体介绍一下,感兴趣的可以了解... 目录Redo Log 刷入磁盘的方法Redo Log 刷入磁盘的过程代码示例(伪代码)在数据库系统中,r

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

golang 日志log与logrus示例详解

《golang日志log与logrus示例详解》log是Go语言标准库中一个简单的日志库,本文给大家介绍golang日志log与logrus示例详解,感兴趣的朋友一起看看吧... 目录一、Go 标准库 log 详解1. 功能特点2. 常用函数3. 示例代码4. 优势和局限二、第三方库 logrus 详解1.

一文详解如何从零构建Spring Boot Starter并实现整合

《一文详解如何从零构建SpringBootStarter并实现整合》SpringBoot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,:本文主要介绍如何从... 目录一、Spring Boot Starter的核心价值二、Starter项目创建全流程2.1 项目初始化(

如何自定义Nginx JSON日志格式配置

《如何自定义NginxJSON日志格式配置》Nginx作为最流行的Web服务器之一,其灵活的日志配置能力允许我们根据需求定制日志格式,本文将详细介绍如何配置Nginx以JSON格式记录访问日志,这种... 目录前言为什么选择jsON格式日志?配置步骤详解1. 安装Nginx服务2. 自定义JSON日志格式各

Android自定义Scrollbar的两种实现方式

《Android自定义Scrollbar的两种实现方式》本文介绍两种实现自定义滚动条的方法,分别通过ItemDecoration方案和独立View方案实现滚动条定制化,文章通过代码示例讲解的非常详细,... 目录方案一:ItemDecoration实现(推荐用于RecyclerView)实现原理完整代码实现

SpringCloud负载均衡spring-cloud-starter-loadbalancer解读

《SpringCloud负载均衡spring-cloud-starter-loadbalancer解读》:本文主要介绍SpringCloud负载均衡spring-cloud-starter-loa... 目录简述主要特点使用负载均衡算法1. 轮询负载均衡策略(Round Robin)2. 随机负载均衡策略(