开发者

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

目录
  • 需求
  • Starter 项目目录结构
  • pom.XML 配置
  • LogInitializer实现
  • MDCInterceptor 实现
  • LogbackInterceptorAutoConfiguration实现
  • LogbackProperties
  • LogbackAutoConfiguration
  • resource
  • 使用starter
    • 引用starter
    • 在resource中添加日志文件logback.xml
  • 启动日志效果
    • 自定义Provider实现日志自定义字段格式
      • 定义Provider
      • spring.factories添加注入类
      • resource logback.xml 改造
      • 启动日志输出结果
    • 优化异步线程日志切不到的问题
      • 总结

        需求

        为了适配现有日志平台,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.devze.com 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.devze.comfieldName>
                            <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";
                }
            }
        }

        总结

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

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜