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