本文主要是介绍如何自定义一个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 不支持其他类型
定义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"; } } }
总结
这篇关于如何自定义一个log适配器starter的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!