需求
为了适配现有日志平台,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>-->
<!-- </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(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实现
@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,新增自定义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>启动日志输出结果
{“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";
}
}
}总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论