如何自定義一個(gè)log適配器starter
需求
為了適配現(xiàn)有日志平臺,java項(xiàng)目應(yīng)用日志需要添加自定義字段:
日志關(guān)鍵字段:
- app:應(yīng)用名稱
- host:主機(jī)IP
- env:環(huán)境(DEV、UAT、GRAY、PRO)
- namespace:命名空間(默認(rèn)main,多版本用到)
- message:日志內(nèi)容
- logCategory:日志分類 (HttpServer、HttpClient、DB、Job)
- level:日志等級(Debug、Info、Warn、Error、Fatal)
- error:錯誤明細(xì),可以為錯誤堆棧信息
- createdOn:寫日志時(shí)間,毫秒時(shí)間戳,比如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 項(xiàng)目目錄結(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實(shí)現(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)下的主機(jī)名/IP InetAddress inetAddress = null; try { inetAddress = InetAddress.getLocalHost(); } catch (UnknownHostException e) { return "unknown"; } return inetAddress.getHostAddress(); } }
MDCInterceptor 實(shí)現(xiàn)
MDCInterceptor 用于在每個(gè)請求的生命周期中設(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實(shí)現(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)下的主機(jī)名/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> <!-- 顯式指定毫秒時(shí)間戳的類 --> <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
在其他項(xiàng)目中添加依賴:(需要install本地倉庫或deploy遠(yuǎn)程倉庫)
<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實(shí)現(xiàn)日志自定義字段格式
平臺日志需要日志level 為首字母大寫,時(shí)間createdOn 需要為時(shí)間戳,并且為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 作為日志的一個(gè)字段來寫入 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"/> <!-- 顯式指定毫秒時(shí)間戳的類 --> <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é)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java lambda循環(huán)_使用Java 8 Lambda簡化嵌套循環(huán)操作
這篇文章主要介紹了java lambda循環(huán)_使用Java 8 Lambda簡化嵌套循環(huán)操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09解析ConcurrentHashMap: transfer方法源碼分析(難點(diǎn))
ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu),今天給大家普及java面試常見問題---ConcurrentHashMap知識,一起看看吧2021-06-06Java動態(tài)數(shù)組Arraylist存放自定義數(shù)據(jù)類型方式
這篇文章主要介紹了Java動態(tài)數(shù)組Arraylist存放自定義數(shù)據(jù)類型方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10SpringCloud Webflux過濾器增加header傳遞方式
這篇文章主要介紹了SpringCloud Webflux過濾器增加header傳遞方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02IntelliJ IDEA像Eclipse一樣打開多個(gè)項(xiàng)目的圖文教程
這篇文章主要介紹了IntelliJ IDEA像Eclipse一樣打開多個(gè)項(xiàng)目的方法圖文教程講解,需要的朋友可以參考下2018-03-03java定時(shí)任務(wù)Timer和TimerTask使用詳解
這篇文章主要為大家詳細(xì)介紹了java定時(shí)任務(wù)Timer和TimerTask使用方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02