Bootstrap

Druid数据源指标监控

摘要    

       Druid数据源指标监控分数据采集和指标告警,是指通过开发java代码获取应用的Druid数据源指标并暴露到应用的指标端点,然后通过普米等监控工具拉取该指标,然后通过alert-manager、n9e等告警工具配置域值从而实现告警和监控的能力。


本文亮点

  1、实现了一般的Druid数据源监控,
  2、还额外支持tomcat内置JNDI做为底层数据源的Druid数据源监控
  3、如需实现HikariCP数据源指标采集只需改一下代码中的指标名即可(代码可复用)


标签

JNDI数据源指标监控、Druid指标监控、HikariCP指标监控

一、效果图


二、数据流图/组件架构图

三、代码

1、Druid数据源指标配置类

package person.daizhongde.common.monitor.druid;

import person.daizhongde.datasources.DynamicDataSource;// AbstractRoutingDataSource 的实现类,用于支持Druid代理tomcat JNDI数据源
import com.alibaba.druid.pool.DruidDataSource;
import io.prometheus.client.CollectorRegistry;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.jdbc.DataSourceUnwrapper;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Description:    Druid数据源指标配置类
 * @Author:         bricklayer(飞火流星02027)
 * @CreateDate:     2024/10/23 10:28 AM
 * @Version:        1.0
 */
@Slf4j
@Configuration
@ConditionalOnClass({DruidDataSource.class, CollectorRegistry.class})
@ConditionalOnProperty(name = "monitor.datasource.druid.enabled", havingValue = "true", matchIfMissing = false )
public class DruidMetricsConfiguration {
    private static final Logger LOGGER = LoggerFactory.getLogger(DruidMetricsConfiguration.class);
    private final CollectorRegistry registry;

    public DruidMetricsConfiguration(CollectorRegistry registry) {
        this.registry = registry;
    }

    @Autowired
    public void bindMetricsRegistryToDruidDataSources(Collection<DataSource> dataSources) {
        Map<String, DruidDataSource> druidDataSources = new LinkedHashMap<>();

        for (DataSource dataSource : dataSources) {
            // 如果没有使用动态数据源代理可以注释掉if代码,只保留else中的
            if(dataSource instanceof DynamicDataSource){
                DynamicDataSource dynamicDataSource = (DynamicDataSource)dataSource;

                Map<Object, Object> map = dynamicDataSource.getTargetDataSources();

                //Lambda表达式
                map.forEach((k,v) ->{
                    // key 为 name  , v为 datasource
                    log.info("数据源:{}====={}",k, v);
//                    System.out.println("$$$$$$$$$$$$$$  test5  $$$$$$$$");
                    DataSource crmDataSource = (DataSource) map.get(k);
                    DruidDataSource druidDataSource = DataSourceUnwrapper.unwrap(crmDataSource, DruidDataSource.class);
                    if (druidDataSource != null) {
                        druidDataSources.put(druidDataSource.getName()+"-"+k, druidDataSource);
                    }else{

                        Object obj1 = map.get(k);
                        try {
                            druidDataSource = (DruidDataSource)DruidAopUtils.getProxyTarget(obj1 );
                            druidDataSources.put( druidDataSource.getName()+"-"+k,  druidDataSource );
                        } catch (Exception e) {
                            log.error("DruidAopUtils获取Druid数据源代码时出错!error:{}",e.getLocalizedMessage());
                            throw new RuntimeException(e);
                        }
                    }
                });
            }else{
                DruidDataSource druidDataSource = DataSourceUnwrapper.unwrap(dataSource, DruidDataSource.class);
                if (druidDataSource != null) {
                    druidDataSources.put(druidDataSource.getName(), druidDataSource);
                }
            }
        }
        DruidCollector druidCollector = new DruidCollector( druidDataSources );
        druidCollector.register(registry);
    }
}

2、Druid数据源指标聚合类

package person.daizhongde.common.monitor.druid;

import com.alibaba.druid.pool.DruidDataSource;
import io.prometheus.client.Collector;
import io.prometheus.client.GaugeMetricFamily;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * @Description:    Druid数据源指标聚合类
 * @Author:         bricklayer(飞火流星02027)
 * @CreateDate:     2024/10/23 10:28 AM
 * @Version:        1.0
 */
public class DruidCollector extends Collector {

    private static final List<String> LABEL_NAMES = new ArrayList<String>();
    static {
        LABEL_NAMES.add("pool");
        LABEL_NAMES.add("username");
        LABEL_NAMES.add("url");
    }

    private final Map<String, DruidDataSource> dataSources;

    public DruidCollector(Map<String, DruidDataSource> dataSources) {
        this.dataSources = dataSources;
    }

    @Override
    public List<MetricFamilySamples> collect() {
        return Arrays.asList(
                createGauge("druid_active_count", "Active count",
                        druidDataSource -> (double) druidDataSource.getActiveCount()),
                createGauge("druid_active_peak", "Active peak",
                        druidDataSource -> (double) druidDataSource.getActivePeak()),
                createGauge("druid_error_count", "Error count",
                        druidDataSource -> (double) druidDataSource.getErrorCount()),
                createGauge("druid_execute_count", "Execute count",
                        druidDataSource -> (double) druidDataSource.getExecuteCount()),
                createGauge("druid_max_active", "Max active",
                        druidDataSource -> (double) druidDataSource.getMaxActive()),
                createGauge("druid_min_idle", "Min idle",
                        druidDataSource -> (double) druidDataSource.getMinIdle()),
                createGauge("druid_max_wait", "Max wait",
                        druidDataSource -> (double) druidDataSource.getMaxWait()),
                createGauge("druid_max_wait_thread_count", "Max wait thread count",
                        druidDataSource -> (double) druidDataSource.getMaxWaitThreadCount()),
                createGauge("druid_pooling_count", "Pooling count",
                        druidDataSource -> (double) druidDataSource.getPoolingCount()),
                createGauge("druid_pooling_peak", "Pooling peak",
                        druidDataSource -> (double) druidDataSource.getPoolingPeak()),
                createGauge("druid_rollback_count", "Rollback count",
                        druidDataSource -> (double) druidDataSource.getRollbackCount()),
                createGauge("druid_wait_thread_count", "Wait thread count",
                        druidDataSource -> (double) druidDataSource.getWaitThreadCount())

                // 还可添加其他指标
                // .....
        );
    }

    private GaugeMetricFamily createGauge(String metric, String help,
                                          Function<DruidDataSource, Double> metricValueFunction) {
        GaugeMetricFamily metricFamily = new GaugeMetricFamily(metric, help, LABEL_NAMES);
        dataSources.forEach((s, druidDataSource) -> {

            List<String> list = new ArrayList<String>();
            list.add(s);
            list.add(druidDataSource.getUsername());
            String url = druidDataSource.getUrl();
            url = url.length()>60?url.substring(0,60)+"...":url;
            list.add( url );

            metricFamily.addMetric(
                list,
                metricValueFunction.apply(druidDataSource)
            );
        });
        return metricFamily;
    }
}

3、从代理对象中取druid数据源对象的工具类

package person.daizhongde.common.monitor.druid;

import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;

import java.lang.reflect.Field;

/**
 * @Description: 从代理对象中取druid数据源对象的工具类
 * @Author:         bricklayer(飞火流星02027)
 * @CreateDate:     2024/10/23 10:28 AM
 * @Version: 1.0
 */
public class DruidAopUtils {
    public static Object getProxyTarget(Object proxy) throws Exception {
        //判断是否是代理对象
        if(AopUtils.isAopProxy(proxy)){
            //cglib 代理
            if(AopUtils.isCglibProxy(proxy)){
                //通过暴力反射拿到代理对象的拦截器属性,从拦截器获取目标对象
                Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
                h.setAccessible(true);
                Object dynamicAdvisedInterceptor = h.get(proxy);

                Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
                advised.setAccessible(true);
                Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
                //返回目标对象
                return target;
            }
            //jdk代理
            if(AopUtils.isJdkDynamicProxy(proxy)){
                //通过暴力反射拿到代理对象的拦截器属性,从拦截器获取目标对象
                Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
                h.setAccessible(true);
                AopProxy aopProxy = (AopProxy) h.get(proxy);

                Field advised = aopProxy.getClass().getDeclaredField("advised");
                advised.setAccessible(true);

                Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();

                return target;
            }
        }
        return null;
    }
}

四、参考资料(不支持JNDI数据源)

某个github项目,不记得名字了,找到再补上

赠送

Druid官方文档    

https://github.com/alibaba/druid/wiki


Druid配置文档    

https://github.com/alibaba/druid/wiki/DruidDataSource配置属性列表


Druid最佳实践    

https://github.com/alibaba/druid/wiki/DruidDataSource配置
 

;