如何自定義一個log適配器starter
需求
為了適配現(xiàn)有日志平臺,java項目應(yīng)用日志需要添加自定義字段:
日志關(guān)鍵字段:
- app:應(yīng)用名稱
- host:主機IP
- env:環(huán)境(DEV、UAT、GRAY、PRO)
- namespace:命名空間(默認main,多版本用到)
- message:日志內(nèi)容
- 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”,“l(fā)evel”:“INFO”,“message”:“(????)??啟動成功 ?(′?`?)?”}Starter 項目目錄結(jié)構(gòu)
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實現(xiàn)
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 系統(tǒng)下的主機名/IP
InetAddress inetAddress = null;
try {
inetAddress = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
return "unknown";
}
return inetAddress.getHostAddress();
}
}MDCInterceptor 實現(xiàn)
MDCInterceptor 用于在每個請求的生命周期中設(shè)置 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實現(xiàn)
@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 系統(tǒng)下的主機名/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實現(xiàn)日志自定義字段格式
平臺日志需要日志level 為首字母大寫,時間createdOn 需要為時間戳,并且為Long數(shù)字, 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"); // 指定需要轉(zhuǎn)成 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>啟動日志輸出結(jié)果
{“app”:“java-demo”,“namespace”:“default-namespace”,“host”:“10.2.3.130”,“env”:“dev”,“createdOn”:1745820638113,“l(fā)evel”:“Info”,“createdTime”:“2025-04-28 14:10:38.596”,“message”:“(????)??啟動成功 ?(′?`?)?”}優(yōu)化異步線程日志切不到的問題
如過在web請求處理中,使用了異步線程,web線程就直接返回了。后續(xù)子線程是不會被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";
}
}
}總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
idea創(chuàng)建SpringBoot自動創(chuàng)建Lombok無效果的問題解決方案
這篇文章主要介紹了idea創(chuàng)建SpringBoot自動創(chuàng)建Lombok無效果的問題解決方案,感興趣的朋友跟隨小編一起看看吧2024-12-12
Java版本兼容性之JDK 21的SDK在JDK 1.8的使用解讀
文章介紹了Java的字節(jié)碼版本機制以及如何處理UnsupportedClassVersionError錯誤,文章首先解釋了JVM如何通過檢查.class文件的字節(jié)碼版本號來確定是否可以運行該文件,然后給出了三個解決方案:統(tǒng)一環(huán)境、交叉編譯和多版本JAR2025-11-11
SpringBoot中使用攔截器攔截跳轉(zhuǎn)登錄的兩種實現(xiàn)方法
攔截器(Interceptor)是Spring框架提供的一種機制,用于在請求的生命周期中插入自定義邏輯,如身份驗證、日志記錄等,本文將詳細介紹兩種在SpringBoot中使用攔截器來控制用戶登錄并跳轉(zhuǎn)到指定頁面的方法,需要的朋友可以參考下2024-11-11
java計算自然數(shù)中的水仙花數(shù)的方法分享
這篇文章主要介紹了java計算自然數(shù)中的水仙花數(shù)的方法,需要的朋友可以參考下2014-03-03

