logback的DBAppender写入达梦数据库

young 14 2025-06-23

项目上需要将ERROR级别的日志写入数据库中,以便维护人员在页面上可以快速查看到错误日志,于是考虑采用Logback的DBAppender来实现
https://yhsblog.cn/archives/logback-pei-zhi#dbappender-日志入库

高版本的logback中移除了数据库的相关代码,需要手动进行引入

<dependency>
  <groupId>ch.qos.logback.db</groupId>
  <artifactId>logback-classic-db</artifactId>
  <version>1.2.11.1</version>
</dependency>
 <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
   <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
     <driverClass>dm.jdbc.driver.DmDriver</driverClass>
     <url>jdbc:dm://localhost:5237</url>
     <user>APPLOG</user>
     <password>xxxxxx</password>
   </connectionSource>
</appender>

直接使用ch.qos.logback.classic.db.DBAppender时,项目启动报错,报错信息为找不到getGeneratedKeys方法

查看DBAppender的源码

 static {
        // PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
        Method getGeneratedKeysMethod;
        try {
            // the
            getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
        } catch (Exception ex) {
            getGeneratedKeysMethod = null;
        }
        GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
    }

这里写的是从PreparedStatement中获取getGeneratedKeys方法,查看PreparedStatement源码,发现其继承了Statement,在Statement中声明了getGeneratedKeys方法。

查看PreparedStatement的实现

public class DmdbPreparedStatement extends DmdbStatement implements PreparedStatement

public class DmdbStatement extends Filterable implements Statement 

于是考虑将DBAppender复制出来,通过Statemen获取getGeneratedKeys方法

static {

        // PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
        Method getGeneratedKeysMethod;
        try {
            getGeneratedKeysMethod = Statement.class.getMethod("getGeneratedKeys", (Class[]) null);
            // the
            String driverClassName = LogbackConnectionSource.dataSource.getDriverClassName();
            if (driverClassName.contains("DmDriver")) {
                getGeneratedKeysMethod = Statement.class.getMethod("getGeneratedKeys", (Class[]) null);
            } else {
                getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
            }
        } catch (Exception ex) {
            getGeneratedKeysMethod = null;
        }
        GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
    }

修改logback的配置

 <appender name="DB" class="x.y.z.DmDBAppender">
   <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
     <driverClass>dm.jdbc.driver.DmDriver</driverClass>
     <url>jdbc:dm://localhost:5237</url>
     <user>APPLOG</user>
     <password>xxxxxx</password>
   </connectionSource>
</appender>

然而这样一来,数据库链接就暴露了,项目中也未使用JNDI,因此考虑采用重写数据源链接的方式进行操作

public class DriverManagerConnectionSource extends ConnectionSourceBase 

DriverManagerConnectionSource继承了ConnectionSourceBase 接口

于是我也继承此接口,进行重写,ConnectionSourceBase 需要重写获取数据库链接的方法

public Connection getConnection() throws SQLException 

此处考虑使用数据源连接池,数据库配置从配置文件中或者环境变量中读取,同时,考虑到数据库可能会进行加密操作,因此也需要增加解密操作。

public class LogbackConnectionSource extends ConnectionSourceBase implements ApplicationContextAware {
    private String driverClass = null;
    private String url = null;
    private ApplicationContext applicationContext;
    static volatile HikariDataSource dataSource;
    private String jasyptPwd = "12345";
    private static volatile PooledPBEStringEncryptor encryptor;
    private static volatile HikariConfig hikariConfig;

    public void start() {
        try {
            init();
            if (driverClass != null) {
                Class.forName(driverClass);
                discoverConnectionProperties();
            } else {
                addError("WARNING: No JDBC driver specified for logback DriverManagerConnectionSource.");
            }
        } catch (final ClassNotFoundException cnfe) {
            addError("Could not load JDBC driver class: " + driverClass, cnfe);
        }
    }

    private void init() {
        if (encryptor == null) {
            synchronized (LogbackConnectionSource.class) {
                if (encryptor == null) {
                    encryptor = new PooledPBEStringEncryptor();
                    encryptor.setPassword(jasyptPwd);
                    encryptor.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
                    encryptor.setIvGenerator(new org.jasypt.iv.RandomIvGenerator());
                }
            }

        }
        if (dataSource == null) {
            synchronized (LogbackConnectionSource.class) {
                if (dataSource == null) {
                    if (applicationContext != null) {
                        Environment environment = applicationContext.getBean(Environment.class);
                        HikariConfig hikariConfig = new HikariConfig();
                        hikariConfig.setUsername(environment.getProperty("log.datasource.username"));
                        hikariConfig.setPassword(environment.getProperty("log.datasource.password"));
                        hikariConfig.setDriverClassName(environment.getProperty("log.datasource.driverClassName"));
                        hikariConfig.setJdbcUrl(environment.getProperty("log.datasource.url"));
                        hikariConfig.setMinimumIdle(environment.getProperty("log.datasource.minimumIdle", Integer.class,10));
                        hikariConfig.setMaximumPoolSize(environment.getProperty("log.datasource.maximumPoolSize", Integer.class,100));
                        hikariConfig.setKeepaliveTime(60_000);
                        hikariConfig.setConnectionTimeout(30_000);
                        decode(hikariConfig);
                        setter(hikariConfig);
                        dataSource = new HikariDataSource(hikariConfig);

                    } else {
                        String activeProfile = System.getenv("spring.profiles.active");
                        if (!StringUtils.hasLength(activeProfile)) {
                            activeProfile = "local";
                        }
                        Yaml yaml = new Yaml();
                        try (InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("application-" + activeProfile + ".yml")) {
                            Map<String, Object> load = yaml.load(resourceAsStream);
                            Map<String, Object> log = (Map<String, Object>) load.get("log");
                            Map<String, Object> datasource = (Map<String, Object>) log.get("datasource");
                            HikariConfig hikariConfig = new HikariConfig();
                            hikariConfig.setUsername(String.valueOf(datasource.get("username")));
                            hikariConfig.setPassword(String.valueOf(datasource.get("password")));
                            hikariConfig.setDriverClassName(String.valueOf(datasource.get("driverClassName")));
                            hikariConfig.setJdbcUrl(String.valueOf(datasource.get("url")));
                            hikariConfig.setMinimumIdle(datasource.get("minimumIdle") == null ? 10 : Integer.parseInt(datasource.get("minimumIdle").toString()));
                            hikariConfig.setMaximumPoolSize(datasource.get("maximumPoolSize") == null ? 100 : Integer.parseInt(datasource.get("maximumPoolSize").toString()));
                            hikariConfig.setKeepaliveTime(60_000);
                            hikariConfig.setConnectionTimeout(30_000);
                            decode(hikariConfig);
                            setter(hikariConfig);
                            dataSource = new HikariDataSource(hikariConfig);

                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }
        if (this.driverClass == null) {
            setter(hikariConfig);
        }
    }

    private void setter(HikariConfig hikariConfig) {
        this.setUrl(hikariConfig.getJdbcUrl());
        this.setDriverClass(hikariConfig.getDriverClassName());
        this.setPassword(hikariConfig.getPassword());
        this.setUser(hikariConfig.getUsername());
    }

    private void decode(HikariConfig config) {
        String name = config.getUsername();
        if (name.startsWith("ENC(")) {
            name = encryptor.decrypt(name);
        }
        config.setUsername(name);
        String pwd = config.getPassword();
        if (pwd.startsWith("ENC(")) {
            pwd = encryptor.decrypt(pwd);
        }
        config.setPassword(pwd);
        String jdbcUrl = config.getJdbcUrl();
        if (jdbcUrl.startsWith("ENC(")) {
            jdbcUrl = encryptor.decrypt(jdbcUrl);
        }
        config.setJdbcUrl(jdbcUrl);
        String driverClassName = config.getDriverClassName();
        if (driverClassName.startsWith("ENC(")) {
            driverClassName = encryptor.decrypt(driverClassName);
        }
        config.setDriverClassName(driverClassName);
        hikariConfig = config;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public String getUrl() {
        return url;
    }

    /**
     * Sets the url.
     *
     * @param url The url to set
     */
    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * Returns the name of the driver class.
     *
     * @return String
     */
    public String getDriverClass() {
        return driverClass;
    }

    /**
     * Sets the driver class.
     *
     * @param driverClass The driver class to set
     */
    public void setDriverClass(String driverClass) {
        this.driverClass = driverClass;
    }
}

最后采用异步的方式进行日志的入库操作,避免对业务产生影响

<appender name="DB" class="x.y.z.DMLogbackDBAppender">
        <connectionSource class="x.y.z.LogbackConnectionSource">
        </connectionSource>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
    </appender>

    <appender name="ASYNC-DB" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>256</queueSize>
        <appender-ref ref="DB"/>
    </appender>

官方未提供达梦的初始化SQL,在此记录一下

CREATE TABLE logging_event
(
    timestmp         NUMBER(20) NOT NULL,
    formatted_message  VARCHAR2(4000) NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254) NOT NULL,
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          BIGINT  identity(1, 1)
)storage(initial 1, next 1, minextents 1, fillfactor 0)
;

alter table logging_event add constraint "PK_Logging_event" primary key(event_id);




create table logging_event_property
(
    event_id BIGINT not null ,
    mapped_key VARCHAR2(254) not null ,
    mapped_value VARCHAR2(1024)
)
    storage(initial 1, next 1, minextents 1, fillfactor 0)
;

alter table logging_event_property add constraint primary key(event_id,mapped_key);

alter table logging_event_property add constraint foreign key(event_id) references logging_event(event_id) ;


CREATE TABLE logging_event_exception
(
    event_id         BIGINT NOT NULL,
    i                SMALLINT NOT NULL,
    trace_line       VARCHAR2(254) NOT NULL
);


alter table logging_event_exception add constraint primary key(event_id,i);
alter table logging_event_exception add constraint foreign key(event_id) references logging_event(event_id)  ;

参看资料:https://blog.csdn.net/qq_37003223/article/details/128579835