springboot2.7.x 集成log4j2配置写入日志到mysql自定义表格


在阅读之前请先查看

本文暂不考虑抽象等实现方式,只限于展示如何自定义配置log4j2并写入mysql数据库(自定义结构)

先看下log4j2的配置

<?xml version="1.0" encoding="UTF-8"?>   
  
    
        ./logs/demo/log4j2/
    
    
        
        
        
        
        
              
            
            
            
        
        
        

        
            

            

            
                

                

            
            
            
                

                
                    
                    
                    
                    

                

            

        

        
            

            

            
                

                

            

            
                
                    

                    

                

            

             
               
            
            
            
                

                

            
            
                
                    

                    

                

            
             
        
            
            
            
                

                

            
            
                
                    

                    

                

            
             
        
            
            
            
                

                

            
            
                
                    

                    

                

            
        

        
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
        
       
    
        
        

        

        
            

            

            

            

            

            

        
        
            
        

    


注意看如下这段

  
            
            
            
            
            
            
            
            

我们在此处并没有采用官方的jdbc配置方式而是采用了自己的nirvana.core.logger.LogConnectionFactory

nirvana.core.logger.LogConnectionFactory 的代码

package nirvana.core.logger;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.yaml.snakeyaml.Yaml;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

/**
 * ConnectionFactory
 *
 * @author linkanyway
 * @version 1.0
 * @date 2022/05/26 20:05
 */

public class LogConnectionFactory {
    private DataSource dataSource;
    private static String PRE_FIX = "spring.datasource.";


    /**
     * constructor
     */
    private LogConnectionFactory() {
        ClassPathResource ymlResource = new ClassPathResource ("application.yml");
        ClassPathResource propertyResource = new ClassPathResource ("application.properties");

        ClassPathResource resource;

        if (!(resource = new ClassPathResource ("application.yml")).exists ()) {
            resource = new ClassPathResource ("application.properties");
            if (!resource.exists ()) {
                throw new RuntimeException ("no application configuration file found");
            }
        }

        Yaml yaml = new Yaml ();
        YamlPropertySourceLoader loader = new YamlPropertySourceLoader ();
        try {
            List> list = loader.load ("log-datasource", resource);
            PropertySource<?> prop = list.get (0);
            Properties props = new Properties ();
            props.setProperty ("druid.url", Objects.requireNonNull (prop.getProperty (PRE_FIX + "url")).toString ());
            props.setProperty ("druid.username",
                    Objects.requireNonNull (prop.getProperty (PRE_FIX + "username")).toString ());
            props.setProperty ("druid.password",
                    Objects.requireNonNull (prop.getProperty (PRE_FIX + "password")).toString ());
            props.setProperty ("druid.driverClassName",
                    Objects.requireNonNull (prop.getProperty (PRE_FIX + "driver" + "-class-name")).toString ());
            this.dataSource = new DruidDataSource ();
            ((DruidDataSource) this.dataSource).configFromPropety (props);
        } catch (IOException e) {
            throw new RuntimeException (e);
        }


    }

    /**
     * get connection
     *
     * @return
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        return Singleton.INSTANCE.dataSource.getConnection ();
    }


    /**
     * only used inner
     */
    private interface Singleton {
        LogConnectionFactory INSTANCE = new LogConnectionFactory ();
    }
}

此处只是粗糙的码了一个读取properties和yaml文件的类,结合配置文件内制定的factory即实现了log4j2读取mysql connection的目地

建立自定义的表结构

    database sql
    /*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80023
 Source Host           : localhost:3306
 Source Schema         : log4j2

 Target Server Type    : MySQL
 Target Server Version : 80023
 File Encoding         : 65001

 Date: 27/05/2022 11:35:19
*/

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
        -- Table structure for logs
-- ----------------------------
    DROP TABLE IF EXISTS `logs`;
    CREATE TABLE `logs` (
            `ID` varchar(50) NOT NULL,
  `TRACE_ID` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `DATE_TIME` timestamp NULL DEFAULT NULL,
  `CLASS` varchar(100) DEFAULT NULL,
  `LEVEL` varchar(10) DEFAULT NULL,
  `MESSAGE` text,
            `EXCEPTION` text,
            `IP` text,
    PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    SET FOREIGN_KEY_CHECKS = 1;

这个表结构是对应的配置文件中的如下内容

   
            
            
            
            
            
            
            

如果想知道表结构的列是如何和日志对上的接着往下看,

核心调用类LoggerManager

package nirvana.core.logger;

import nirvana.core.context.WebContext;
import nirvana.core.utils.NetUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.net.TcpSocketManager;
import org.apache.logging.log4j.message.StringMapMessage;
import org.apache.logging.log4j.util.Strings;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * LogManager
 *
 * @author linkanyway
 * @version 1.0
 * @date 2022/05/27 11:06
 */
public class LoggerManager {


//    database sql
//    /*
// Navicat Premium Data Transfer
//
// Source Server         : localhost
// Source Server Type    : MySQL
// Source Server Version : 80023
// Source Host           : localhost:3306
// Source Schema         : log4j2
//
// Target Server Type    : MySQL
// Target Server Version : 80023
// File Encoding         : 65001
//
// Date: 27/05/2022 11:35:19
//*/
//
//    SET NAMES utf8mb4;
//    SET FOREIGN_KEY_CHECKS = 0;
//
//-- ----------------------------
//        -- Table structure for logs
//-- ----------------------------
//    DROP TABLE IF EXISTS `logs`;
//    CREATE TABLE `logs` (
//            `ID` varchar(50) NOT NULL,
//  `TRACE_ID` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
//  `DATE_TIME` timestamp NULL DEFAULT NULL,
//  `CLASS` varchar(100) DEFAULT NULL,
//  `LEVEL` varchar(10) DEFAULT NULL,
//  `MESSAGE` text,
//            `EXCEPTION` text,
//            `IP` text,
//    PRIMARY KEY (`ID`)
//) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
//
//    SET FOREIGN_KEY_CHECKS = 1;


    /**
     * todo: use normal class
     * logger
     */
    public Logger logger;

    /**
     * get logger
     *
     * @param loggerName
     * @return
     */
    public static LoggerManager getLogger(String loggerName) {
        return new LoggerManager (loggerName);
    }

    private LoggerManager(String loggerName) {
        this.logger = LogManager.getLogger (loggerName);
    }

    /**
     * error
     *
     * @param message error message
     * @param ex      exception instance
     */
    public void error(String message, @NotNull Exception ex) {

        StringMapMessage msg = generate (message, "error", ex);
        logger.error (msg);
    }

    /**
     * info
     *
     * @param message info message
     */
    public void info(String message) {
        StringMapMessage msg = generate (message, "info");
        logger.info (msg);
    }

    /**
     * trace
     *
     * @param message trace message
     */
    public void trace(String message) {
        StringMapMessage msg = generate (message, "info");
        logger.trace (msg);
    }


    /**
     * debug
     *
     * @param message debug message
     */
    public void debug(String message) {
        StringMapMessage msg = generate (message, "debug");
        logger.debug (msg);
    }

    /**
     * warn
     *
     * @param message warn message
     */
    public void warn(String message) {
        StringMapMessage msg = generate (message, "warn");
        logger.warn (msg);
    }

    /**
     * generate map message
     *
     * @param message message content
     * @param level   level
     * @return StringMapMessage instance
     */
    private StringMapMessage generate(String message, String level) {
        return generate (message, level, null);

    }


    /**
     * get caller information
     *
     * @return caller class name
     */
    private String getCallerName() {
        StackTraceElement[] traces = Thread.currentThread ().getStackTrace ();
        for (Integer i = 1; i < traces.length - 1; i++) {
            StackTraceElement element = traces[i];
            if (element.getClassName ().indexOf (this.getClass ().getPackageName ()) != 0) {
                return element.getClassName ();
            }
        }
        return Strings.EMPTY;
    }


    /**
     * generate message
     *
     * @param message message content
     * @param level   level
     * @param ex      exception
     * @return StringMapMessage instance
     */
    private StringMapMessage generate(String message, String level, Exception ex) {
        String sourceClassName = getCallerName ();
        StringMapMessage msg = new StringMapMessage ();
        if (ex != null) {
            // have to deal with the exception to prevent throw again
            try {
                StringWriter sw = new StringWriter ();
                PrintWriter pw = new PrintWriter (sw, true);
                ex.printStackTrace (pw);
                msg.put ("EXCEPTION", sw.getBuffer ().toString ());
            } catch (Exception e) {
                throw e;
            }
        } else {
            msg.put ("EXCEPTION", "");
        }

        msg.put ("LEVEL", level);
        msg.put ("ID", UUID.randomUUID ().toString ());
        msg.put ("TRACE_ID", WebContext.getRequestId ());
        msg.put ("DATE_TIME", LocalDateTime.now ().toString ());
        msg.put ("CLASS", sourceClassName);
        msg.put ("MESSAGE", message);
        msg.put ("IP", NetUtils.getRemoteIp ());
        return msg;
    }

}

注意代码中这段


        msg.put ("LEVEL", level);
        msg.put ("ID", UUID.randomUUID ().toString ());
        msg.put ("TRACE_ID", WebContext.getRequestId ());
        msg.put ("DATE_TIME", LocalDateTime.now ().toString ());
        msg.put ("CLASS", sourceClassName);
        msg.put ("MESSAGE", message);
        msg.put ("IP", NetUtils.getRemoteIp ());

其实我们只是使用了StringMapMessage实现了自定义消息结构

而如下代码则是为了获取调用类,此处只是利用调用堆栈寻取了第一个和当前日志记录类LoggerManagger的package不同包的第一个类,实际对于调用堆栈获取需要的caller还是需要自己按照自己的方式寻找

    /**
     * get caller information
     *
     * @return caller class name
     */
    private String getCallerName() {
        StackTraceElement[] traces = Thread.currentThread ().getStackTrace ();
        for (Integer i = 1; i < traces.length - 1; i++) {
            StackTraceElement element = traces[i];
            if (element.getClassName ().indexOf (this.getClass ().getPackageName ()) != 0) {
                return element.getClassName ();
            }
        }
        return Strings.EMPTY;
    }