it编程 > 游戏开发 > ar

如何自定义一个log适配器starter

7人参与 2025-06-10 ar

需求

为了适配现有日志平台,java项目应用日志需要添加自定义字段:

日志关键字段:

格式需要改编成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";
        }
    }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

elasticsearch中的mapping简介(最新整理)

06-11

Elasticsearch 映射 fielddata 工作原理解析

06-11

华为鸿蒙HarmonyOS 5.1官宣7月开启升级! 首批支持名单公布

06-11

14000MB/s超高读取速度! 雷克沙ARES PRO Gen5战神4TB固态硬盘评测

06-13

高能体验背后的真实力! 联想YOGA 27一体机2025款体验测评

06-13

5G网卡+高频内存! 七彩虹CVN X870 ARK FROZEN V14方舟主板评测

06-05

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论