001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.web.health;
017
018import bitronix.tm.resource.jdbc.PoolingDataSource;
019import com.codahale.metrics.Gauge;
020import com.codahale.metrics.Metric;
021import com.codahale.metrics.MetricSet;
022import com.codahale.metrics.RatioGauge;
023import org.apache.commons.dbcp.BasicDataSource;
024import org.apache.log4j.Logger;
025import org.enhydra.jdbc.pool.StandardXAPoolDataSource;
026
027import javax.sql.DataSource;
028import java.sql.SQLException;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * A set of metrics that indicate min, max, and active number of connections in the pool as well as the percentage of
034 * connection pool usage.
035 *
036 * Supports XAPool, Bitronix, and DBCP datasources. Will attempt to unwrap the underlying DataSource implementation if
037 * the given DataSource is a wrapper for one of these types. If the given DataSource is of an unknown type, this class
038 * will silently fail and invocations of {@link #getMetrics()} will return an empty map.
039 *
040 * @author Eric Westfall
041 */
042public class DatabaseConnectionPoolMetricSet implements MetricSet {
043
044    private static final Logger LOG = Logger.getLogger(DatabaseConnectionPoolMetricSet.class);
045
046    public static final String ACTIVE = "pool.active";
047    public static final String MIN = "pool.min";
048    public static final String MAX = "pool.max";
049    public static final String USAGE = "pool.usage";
050
051    private final String namePrefix;
052    private final DataSource dataSource;
053
054    /**
055     * Construct a new database connection pool metric set.
056     * @param namePrefix the prefix to use for all metric names, will prepend this to all metric names in this set
057     * @param dataSource the DataSource from which to extract the metrics in this metric set
058     */
059    public DatabaseConnectionPoolMetricSet(String namePrefix, DataSource dataSource) {
060        this.namePrefix = namePrefix;
061        this.dataSource = dataSource;
062    }
063
064    @Override
065    public Map<String, Metric> getMetrics() {
066        Map<String, Metric> metrics = new HashMap<>();
067        boolean success = tryXAPool(metrics) || tryBitronix(metrics) || tryDBCP(metrics);
068        if (!success) {
069            LOG.warn("Failed to identify the type of connection pool with namePrefix: " + namePrefix + " and dataSource class: " + dataSource.getClass());
070        }
071        return metrics;
072    }
073
074    private boolean tryXAPool(Map<String, Metric> metrics) {
075        StandardXAPoolDataSource xaPoolDataSource = tryUnwrap(dataSource, StandardXAPoolDataSource.class);
076        if (xaPoolDataSource != null) {
077            installXAPoolMetrics(xaPoolDataSource, metrics);
078            return true;
079        }
080        return false;
081    }
082
083    private void installXAPoolMetrics(final StandardXAPoolDataSource dataSource, Map<String, Metric> metrics) {
084        metrics.put(namePrefix + ACTIVE, new Gauge<Integer>() {
085            @Override
086            public Integer getValue() {
087                return dataSource.getLockedObjectCount();
088            }
089        });
090        metrics.put(namePrefix + MIN, new Gauge<Integer>() {
091            @Override
092            public Integer getValue() {
093                return dataSource.getMinSize();
094            }
095        });
096        metrics.put(namePrefix + MAX, new Gauge<Integer>() {
097            @Override
098            public Integer getValue() {
099                return dataSource.getMaxSize();
100            }
101        });
102        metrics.put(namePrefix + USAGE, new RatioGauge() {
103            @Override
104            protected Ratio getRatio() {
105                return Ratio.of(dataSource.getLockedObjectCount(), dataSource.getMaxSize());
106            }
107        });
108    }
109
110    private boolean tryBitronix(Map<String, Metric> metrics) {
111        PoolingDataSource poolingDataSource = tryUnwrap(dataSource, PoolingDataSource.class);
112        if (poolingDataSource != null) {
113            installBitronixMetrics(poolingDataSource, metrics);
114            return true;
115        }
116        return false;
117    }
118
119    private void installBitronixMetrics(final PoolingDataSource dataSource, Map<String, Metric> metrics) {
120        metrics.put(namePrefix + ACTIVE, new Gauge<Integer>() {
121            @Override
122            public Integer getValue() {
123                return (int)dataSource.getTotalPoolSize() - (int)dataSource.getInPoolSize();
124            }
125        });
126        metrics.put(namePrefix + MIN, new Gauge<Integer>() {
127            @Override
128            public Integer getValue() {
129                return dataSource.getMinPoolSize();
130            }
131        });
132        metrics.put(namePrefix + MAX, new Gauge<Integer>() {
133            @Override
134            public Integer getValue() {
135                return dataSource.getMaxPoolSize();
136            }
137        });
138        metrics.put(namePrefix + USAGE, new RatioGauge() {
139            @Override
140            protected Ratio getRatio() {
141                return Ratio.of(dataSource.getTotalPoolSize() - dataSource.getInPoolSize(), dataSource.getMaxPoolSize());
142            }
143        });
144    }
145
146    private boolean tryDBCP(Map<String, Metric> metrics) {
147        BasicDataSource basicDataSource = tryUnwrap(dataSource, BasicDataSource.class);
148        if (basicDataSource != null) {
149            installDBCPMetrics(basicDataSource, metrics);
150            return true;
151        }
152        return false;
153    }
154
155    private void installDBCPMetrics(final BasicDataSource dataSource, Map<String, Metric> metrics) {
156        metrics.put(namePrefix + ACTIVE, new Gauge<Integer>() {
157            @Override
158            public Integer getValue() {
159                return dataSource.getNumActive();
160            }
161        });
162        metrics.put(namePrefix + MIN, new Gauge<Integer>() {
163            @Override
164            public Integer getValue() {
165                return dataSource.getMinIdle();
166            }
167        });
168        metrics.put(namePrefix + MAX, new Gauge<Integer>() {
169            @Override
170            public Integer getValue() {
171                return dataSource.getMaxActive();
172            }
173        });
174        metrics.put(namePrefix + USAGE, new RatioGauge() {
175            @Override
176            protected Ratio getRatio() {
177                return Ratio.of(dataSource.getNumActive(), dataSource.getMaxActive());
178            }
179        });
180    }
181
182    private <T> T tryUnwrap(DataSource dataSource, Class<T> targetType) {
183        if (targetType.isInstance(dataSource)) {
184            return targetType.cast(dataSource);
185        }
186        try {
187            if (dataSource.isWrapperFor(targetType)) {
188                return dataSource.unwrap(targetType);
189            }
190        } catch (SQLException e) {
191            LOG.warn("Exception when trying to unwrap datasource as " + targetType, e);
192        }
193        return null;
194    }
195
196}