如何自定义一个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 不支持其他类型

定义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)。
加载中,请稍侯......
精彩评论