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 com.codahale.metrics.*;
019import com.codahale.metrics.health.HealthCheck;
020import com.codahale.metrics.health.HealthCheckRegistry;
021import com.codahale.metrics.jvm.*;
022import org.codehaus.jackson.map.ObjectMapper;
023import org.kuali.rice.core.api.config.property.ConfigContext;
024import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
025import org.kuali.rice.core.api.util.RiceConstants;
026import org.kuali.rice.core.framework.persistence.platform.DatabasePlatform;
027
028import javax.servlet.ServletException;
029import javax.servlet.http.HttpServlet;
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpServletResponse;
032import javax.sql.DataSource;
033import java.io.IOException;
034import java.lang.management.ManagementFactory;
035import java.lang.management.RuntimeMXBean;
036import java.util.Map;
037
038/**
039 * Implements an endpoint for providing health information for a Kuali Rice server.
040 *
041 * @author Eric Westfall
042 */
043public class HealthServlet extends HttpServlet {
044    
045    private MetricRegistry metricRegistry;
046    private HealthCheckRegistry healthCheckRegistry;
047    private Config config;
048
049    @Override
050    public void init() throws ServletException {
051        this.metricRegistry = new MetricRegistry();
052        this.healthCheckRegistry = new HealthCheckRegistry();
053        this.config = new Config();
054
055        monitorMemoryUsage();
056        monitorThreads();
057        monitorGarbageCollection();
058        monitorBufferPools();
059        monitorClassLoading();
060        monitorFileDescriptors();
061        monitorRuntime();
062        monitorDataSources();
063    }
064
065    @Override
066    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
067        HealthStatus status = checkHealth();
068
069        String includeDetail = req.getParameter("detail");
070        if ("true".equals(includeDetail)) {
071            if (status.isOk()) {
072                resp.setStatus(200);
073            } else {
074                resp.setStatus(503);
075            }
076
077            ObjectMapper mapper = new ObjectMapper();
078            resp.setContentType("application/json");
079            mapper.writeValue(resp.getOutputStream(), status);
080
081        } else {
082            if (status.isOk()) {
083                resp.setStatus(204);
084            } else {
085                resp.setStatus(503);
086            }
087        }
088        resp.getOutputStream().flush();
089    }
090
091    @SuppressWarnings("unchecked")
092    private void monitorMemoryUsage() {
093        // registry memory metrics, we are going to rename this slightly using our format given the format required
094        // by the health detail specification
095        MemoryUsageGaugeSet gaugeSet = new MemoryUsageGaugeSet();
096        Map<String, Metric> metrics = gaugeSet.getMetrics();
097        for (String metricName : metrics.keySet()) {
098            this.metricRegistry.register("memory:" + metricName, metrics.get(metricName));
099        }
100
101        Gauge<Double> heapUsage = this.metricRegistry.getGauges().get("memory:heap.usage");
102        Gauge<Long> heapMaxMemory = this.metricRegistry.getGauges().get("memory:heap.max");
103        if (heapMaxMemory.getValue() != -1) {
104            this.healthCheckRegistry.register("memory:heap.usage", new MemoryUsageHealthCheck(heapUsage, config.heapMemoryUsageThreshold()));
105        }
106
107        Gauge<Double> nonHeapUsage = this.metricRegistry.getGauges().get("memory:non-heap.usage");
108        Gauge<Long> nonHeapMaxMemory = this.metricRegistry.getGauges().get("memory:non-heap.max");
109        if (nonHeapMaxMemory.getValue() != -1) {
110            this.healthCheckRegistry.register("memory:non-heap.usage", new MemoryUsageHealthCheck(nonHeapUsage, config.nonHeapMemoryUsageThreshold()));
111        }
112
113        Gauge<Long> totalUsedMemory = this.metricRegistry.getGauges().get("memory:total.used");
114        Gauge<Long> totalMaxMemory = this.metricRegistry.getGauges().get("memory:total.max");
115        if (totalMaxMemory.getValue() != -1) {
116            MemoryUsageRatio totalMemoryRatio = new MemoryUsageRatio(totalUsedMemory, totalMaxMemory);
117            this.metricRegistry.register("memory:total.usage", totalMemoryRatio);
118            this.healthCheckRegistry.register("memory:total.usage", new MemoryUsageHealthCheck(totalMemoryRatio, config.totalMemoryUsageThreshold()));
119        }
120    }
121
122    @SuppressWarnings("unchecked")
123    private void monitorThreads() {
124        ThreadStatesGaugeSet gaugeSet = new ThreadStatesGaugeSet();
125        Map<String, Metric> metrics = gaugeSet.getMetrics();
126        for (String name : metrics.keySet()) {
127            this.metricRegistry.register("thread:" + name, metrics.get(name));
128        }
129
130        // register health check for deadlock count
131        String deadlockCountName = "thread:deadlock.count";
132        final Gauge<Integer> deadlockCount = this.metricRegistry.getGauges().get(deadlockCountName);
133        this.healthCheckRegistry.register("thread:deadlock.count", new HealthCheck() {
134            @Override
135            protected Result check() throws Exception {
136                int numDeadlocks = deadlockCount.getValue();
137                if (numDeadlocks >= config.deadlockThreshold()) {
138                    return Result.unhealthy("There are " + numDeadlocks + " deadlocked threads which is greater than or equal to the threshold of " + config.deadlockThreshold());
139                }
140                return Result.healthy();
141            }
142        });
143    }
144
145    private void monitorGarbageCollection() {
146        GarbageCollectorMetricSet metricSet = new GarbageCollectorMetricSet();
147        Map<String, Metric> metrics = metricSet.getMetrics();
148        for (String name : metrics.keySet()) {
149            this.metricRegistry.register("garbage-collector:" + name, metrics.get(name));
150        }
151    }
152
153    private void monitorBufferPools() {
154        BufferPoolMetricSet metricSet = new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer());
155        Map<String, Metric> metrics = metricSet.getMetrics();
156        for (String name : metrics.keySet()) {
157            this.metricRegistry.register("buffer-pool:" + name, metrics.get(name));
158        }
159
160    }
161
162    private void monitorClassLoading() {
163        ClassLoadingGaugeSet metricSet = new ClassLoadingGaugeSet();
164        Map<String, Metric> metrics = metricSet.getMetrics();
165        for (String name : metrics.keySet()) {
166            this.metricRegistry.register("classloader:" + name, metrics.get(name));
167        }
168    }
169
170    private void monitorFileDescriptors() {
171        final FileDescriptorRatioGauge gauge = new FileDescriptorRatioGauge();
172        String name = "file-descriptor:usage";
173        this.metricRegistry.register(name, gauge);
174        this.healthCheckRegistry.register(name, new HealthCheck() {
175            @Override
176            protected Result check() throws Exception {
177                double value = gauge.getValue();
178                if (value >= config.fileDescriptorUsageThreshold()) {
179                    return Result.unhealthy("File descriptor usage ratio of " + value + " was greater than or equal to threshold of " + config.fileDescriptorUsageThreshold());
180                }
181                return Result.healthy();
182            }
183        });
184    }
185
186    private void monitorRuntime() {
187        final RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
188        this.metricRegistry.register("runtime:uptime", new Gauge<Long>() {
189            @Override
190            public Long getValue() {
191                return runtime.getUptime();
192            }
193        });
194    }
195
196    private void monitorDataSources() {
197        DataSource dataSource = (DataSource) ConfigContext.getCurrentContextConfig().getObject(RiceConstants.DATASOURCE_OBJ);
198        DataSource nonTransactionalDataSource = (DataSource) ConfigContext.getCurrentContextConfig().getObject(RiceConstants.NON_TRANSACTIONAL_DATASOURCE_OBJ);
199        DataSource serverDataSource = (DataSource) ConfigContext.getCurrentContextConfig().getObject(RiceConstants.SERVER_DATASOURCE_OBJ);
200        DatabasePlatform databasePlatform = GlobalResourceLoader.getService(RiceConstants.DB_PLATFORM);
201        monitorDataSource("database.primary:", dataSource, databasePlatform, config.primaryConnectionPoolUsageThreshold());
202        monitorDataSource("database.non-transactional:", nonTransactionalDataSource, databasePlatform, config.nonTransactionalConnectionPoolUsageThreshold());
203        monitorDataSource("database.server:", serverDataSource, databasePlatform, config.serverConnectionPoolUsageThreshold());
204    }
205
206    @SuppressWarnings("unchecked")
207    private void monitorDataSource(String namePrefix, DataSource dataSource, DatabasePlatform databasePlatform, double threshold) {
208        if (databasePlatform != null && dataSource != null) {
209            // register connection metric
210            String name = namePrefix + "connected";
211            DatabaseConnectionHealthGauge healthGauge = new DatabaseConnectionHealthGauge(dataSource, databasePlatform);
212            this.metricRegistry.register(name, healthGauge);
213            this.healthCheckRegistry.register(name, healthGauge);
214
215            // register pool metrics
216            String poolUsageName = namePrefix + DatabaseConnectionPoolMetricSet.USAGE;
217            DatabaseConnectionPoolMetricSet poolMetrics = new DatabaseConnectionPoolMetricSet(namePrefix, dataSource);
218            this.metricRegistry.registerAll(poolMetrics);
219            Gauge<Double> poolUsage = this.metricRegistry.getGauges().get(poolUsageName);
220            if (poolUsage != null) {
221                this.healthCheckRegistry.register(poolUsageName, new DatabaseConnectionPoolHealthCheck(poolUsage, threshold));
222            }
223        }
224    }
225
226    private HealthStatus checkHealth() {
227        HealthStatus status = new HealthStatus();
228        runHealthChecks(status);
229        reportMetrics(status);
230        return status;
231    }
232
233    private void runHealthChecks(HealthStatus status) {
234        Map<String, HealthCheck.Result> results = this.healthCheckRegistry.runHealthChecks();
235        for (String name : results.keySet()) {
236            HealthCheck.Result result = results.get(name);
237            if (!result.isHealthy()) {
238                status.setStatusCode(HealthStatus.FAILED);
239                status.appendMessage(name, result.getMessage());
240            }
241        }
242    }
243
244    private void reportMetrics(HealthStatus status) {
245        reportGauges(this.metricRegistry.getGauges(), status);
246        reportCounters(metricRegistry.getCounters(), status);
247        reportHistograms(metricRegistry.getHistograms(), status);
248        reportMeters(metricRegistry.getMeters(), status);
249        reportTimers(metricRegistry.getTimers(), status);
250    }
251
252    private void reportGauges(Map<String, Gauge> gaugues, HealthStatus status) {
253        for (String name : gaugues.keySet()) {
254            Gauge gauge = gaugues.get(name);
255            status.getMetrics().add(new HealthMetric(name, gauge.getValue()));
256        }
257    }
258
259    private void reportCounters(Map<String, Counter> counters, HealthStatus status) {
260        for (String name : counters.keySet()) {
261            Counter counter = counters.get(name);
262            status.getMetrics().add(new HealthMetric(name, counter.getCount()));
263        }
264    }
265
266    private void reportHistograms(Map<String, Histogram> histograms, HealthStatus status) {
267        for (String name : histograms.keySet()) {
268            Histogram histogram = histograms.get(name);
269            status.getMetrics().add(new HealthMetric(name, histogram.getCount()));
270        }
271    }
272
273    private void reportMeters(Map<String, Meter> meters, HealthStatus status) {
274        for (String name : meters.keySet()) {
275            Meter meter = meters.get(name);
276            status.getMetrics().add(new HealthMetric(name, meter.getCount()));
277        }
278    }
279
280    private void reportTimers(Map<String, Timer> timers, HealthStatus status) {
281        for (String name : timers.keySet()) {
282            Timer timer = timers.get(name);
283            status.getMetrics().add(new HealthMetric(name, timer.getCount()));
284        }
285    }
286    
287    public static final class Config {
288
289        public static final String HEAP_MEMORY_THRESHOLD_PROPERTY = "rice.health.memory.heap.usageThreshold";
290        public static final String NON_HEAP_MEMORY_THRESHOLD_PROPERTY = "rice.health.memory.nonHeap.usageThreshold";
291        public static final String TOTAL_MEMORY_THRESHOLD_PROPERTY = "rice.health.memory.total.usageThreshold";
292        public static final String DEADLOCK_THRESHOLD_PROPERTY = "rice.health.thread.deadlockThreshold";
293        public static final String FILE_DESCRIPTOR_THRESHOLD_PROPERTY = "rice.health.fileDescriptor.usageThreshold";
294        public static final String PRIMARY_POOL_USAGE_THRESHOLD_PROPERTY = "rice.health.database.primary.connectionPoolUsageThreshold";
295        public static final String NON_TRANSACTIONAL_POOL_USAGE_THRESHOLD_PROPERTY = "rice.health.database.nonTransactional.connectionPoolUsageThreshold";
296        public static final String SERVER_POOL_USAGE_THRESHOLD_PROPERTY = "rice.health.database.server.connectionPoolUsageThreshold";
297
298        private static final double HEAP_MEMORY_THRESHOLD_DEFAULT = 0.95;
299        private static final double NON_HEAP_MEMORY_THRESHOLD_DEFAULT = 0.95;
300        private static final double TOTAL_MEMORY_THRESHOLD_DEFAULT = 0.95;
301        private static final int DEADLOCK_THRESHOLD_DEFAULT = 1;
302        private static final double FILE_DESCRIPTOR_THRESHOLD_DEFAULT = 0.95;
303        private static final double POOL_USAGE_THRESHOLD_DEFAULT = 1.0;
304
305
306        double heapMemoryUsageThreshold() {
307            return getDouble(HEAP_MEMORY_THRESHOLD_PROPERTY, HEAP_MEMORY_THRESHOLD_DEFAULT);
308        }
309
310        double nonHeapMemoryUsageThreshold() {
311            return getDouble(NON_HEAP_MEMORY_THRESHOLD_PROPERTY, NON_HEAP_MEMORY_THRESHOLD_DEFAULT);
312        }
313
314        double totalMemoryUsageThreshold() {
315            return getDouble(TOTAL_MEMORY_THRESHOLD_PROPERTY, TOTAL_MEMORY_THRESHOLD_DEFAULT);
316        }
317
318        int deadlockThreshold() {
319            return getInt(DEADLOCK_THRESHOLD_PROPERTY, DEADLOCK_THRESHOLD_DEFAULT);
320        }
321
322        double fileDescriptorUsageThreshold() {
323            return getDouble(FILE_DESCRIPTOR_THRESHOLD_PROPERTY, FILE_DESCRIPTOR_THRESHOLD_DEFAULT);
324        }
325
326        double primaryConnectionPoolUsageThreshold() {
327            return getDouble(PRIMARY_POOL_USAGE_THRESHOLD_PROPERTY, POOL_USAGE_THRESHOLD_DEFAULT);
328        }
329
330        double nonTransactionalConnectionPoolUsageThreshold() {
331            return getDouble(NON_TRANSACTIONAL_POOL_USAGE_THRESHOLD_PROPERTY, POOL_USAGE_THRESHOLD_DEFAULT);
332        }
333
334        double serverConnectionPoolUsageThreshold() {
335            return getDouble(SERVER_POOL_USAGE_THRESHOLD_PROPERTY, POOL_USAGE_THRESHOLD_DEFAULT);
336        }
337
338        private double getDouble(String propertyName, double defaultValue) {
339            String propertyValue = ConfigContext.getCurrentContextConfig().getProperty(propertyName);
340            if (propertyValue != null) {
341                return Double.parseDouble(propertyValue);
342            }
343            return defaultValue;
344        }
345
346        private int getInt(String propertyName, int defaultValue) {
347            String propertyValue = ConfigContext.getCurrentContextConfig().getProperty(propertyName);
348            if (propertyValue != null) {
349                return Integer.parseInt(propertyValue);
350            }
351            return defaultValue;
352        }
353        
354    }
355
356}