/* * Copyright 2012-2015, the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.flipkart.phantom.runtime.impl.hystrix; import static org.junit.Assert.*; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.hystrix.HystrixCircuitBreaker; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.hystrix.HystrixCommandMetrics; import com.netflix.hystrix.HystrixCommandMetrics.HealthCounts; import com.netflix.hystrix.HystrixCommandProperties; import com.netflix.hystrix.HystrixThreadPoolKey; import com.netflix.hystrix.HystrixThreadPoolMetrics; import com.netflix.hystrix.util.HystrixRollingNumberEvent; /** * The <code>HystrixMetricsPoller</code> class is a clone of the Hystrix {@link com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsPoller} that additionally * includes the host name in metrics data. This code is based on the release version 1.4.0-RC4 * * Class summary copied from original source: * * Polls Hystrix metrics and output JSON strings for each metric to a MetricsPollerListener. * <p> * Polling can be stopped/started. Use shutdown() to permanently shutdown the poller. * * @author Regunath B * @version 1.0, 11 Feb 2014 */ public class HystrixMetricsPoller { static final Logger logger = LoggerFactory.getLogger(HystrixMetricsPoller.class); private final ScheduledExecutorService executor; private final int delay; private final AtomicBoolean running = new AtomicBoolean(false); private volatile ScheduledFuture<?> scheduledTask = null; private final MetricsAsJsonPollerListener listener; /** The local host name value*/ private static String hostName = ""; static { try { hostName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { logger.warn("Unable to resolve local host name. Will use empty string instead"); } } /** * Allocate resources to begin polling. * <p> * Use <code>start</code> to begin polling. * <p> * Use <code>shutdown</code> to cleanup resources and stop polling. * <p> * Use <code>pause</code> to temporarily stop polling that can be restarted again with <code>start</code>. * * @param MetricsAsJsonPollerListener * for callbacks * @param delay */ public HystrixMetricsPoller(MetricsAsJsonPollerListener listener, int delay) { this.listener = listener; executor = new ScheduledThreadPoolExecutor(1, new MetricsPollerThreadFactory()); this.delay = delay; } /** * Start polling. */ public synchronized void start() { // use compareAndSet to make sure it starts only once and when not running if (running.compareAndSet(false, true)) { logger.info("Starting HystrixMetricsPoller"); scheduledTask = executor.scheduleWithFixedDelay(new MetricsPoller(listener), 0, delay, TimeUnit.MILLISECONDS); } } /** * Pause (stop) polling. Polling can be started again with <code>start</code> as long as <code>shutdown</code> is not called. */ public synchronized void pause() { // use compareAndSet to make sure it stops only once and when running if (running.compareAndSet(true, false)) { logger.info("Stopping the Servo Metrics Poller"); scheduledTask.cancel(true); } } /** * Stops polling and shuts down the ExecutorService. * <p> * This instance can no longer be used after calling shutdown. */ public synchronized void shutdown() { pause(); executor.shutdown(); } public boolean isRunning() { return running.get(); } /** * Used to protect against leaking ExecutorServices and threads if this class is abandoned for GC without shutting down. */ @SuppressWarnings("unused") private final Object finalizerGuardian = new Object() { protected void finalize() throws Throwable { if (!executor.isShutdown()) { logger.warn(HystrixMetricsPoller.class.getSimpleName() + " was not shutdown. Caught in Finalize Guardian and shutting down."); try { shutdown(); } catch (Exception e) { logger.error("Failed to shutdown " + HystrixMetricsPoller.class.getSimpleName(), e); } } }; }; public static interface MetricsAsJsonPollerListener { public void handleJsonMetric(String json); } private class MetricsPoller implements Runnable { private final MetricsAsJsonPollerListener listener; private final JsonFactory jsonFactory = new JsonFactory(); public MetricsPoller(MetricsAsJsonPollerListener listener) { this.listener = listener; } @Override public void run() { try { // command metrics for (HystrixCommandMetrics commandMetrics : HystrixCommandMetrics.getInstances()) { HystrixCommandKey key = commandMetrics.getCommandKey(); HystrixCircuitBreaker circuitBreaker = HystrixCircuitBreaker.Factory.getInstance(key); StringWriter jsonString = new StringWriter(); JsonGenerator json = jsonFactory.createGenerator(jsonString); json.writeStartObject(); json.writeStringField("type", "HystrixCommand"); json.writeStringField("name", key.name()); json.writeStringField("group", commandMetrics.getCommandGroup().name()); json.writeNumberField("currentTime", System.currentTimeMillis()); // circuit breaker if (circuitBreaker == null) { // circuit breaker is disabled and thus never open json.writeBooleanField("isCircuitBreakerOpen", false); } else { json.writeBooleanField("isCircuitBreakerOpen", circuitBreaker.isOpen()); } HealthCounts healthCounts = commandMetrics.getHealthCounts(); json.writeNumberField("errorPercentage", healthCounts.getErrorPercentage()); json.writeNumberField("errorCount", healthCounts.getErrorCount()); json.writeNumberField("requestCount", healthCounts.getTotalRequests()); // rolling counters json.writeNumberField("rollingCountCollapsedRequests", commandMetrics.getRollingCount(HystrixRollingNumberEvent.COLLAPSED)); json.writeNumberField("rollingCountExceptionsThrown", commandMetrics.getRollingCount(HystrixRollingNumberEvent.EXCEPTION_THROWN)); json.writeNumberField("rollingCountFailure", commandMetrics.getRollingCount(HystrixRollingNumberEvent.FAILURE)); json.writeNumberField("rollingCountFallbackFailure", commandMetrics.getRollingCount(HystrixRollingNumberEvent.FALLBACK_FAILURE)); json.writeNumberField("rollingCountFallbackRejection", commandMetrics.getRollingCount(HystrixRollingNumberEvent.FALLBACK_REJECTION)); json.writeNumberField("rollingCountFallbackSuccess", commandMetrics.getRollingCount(HystrixRollingNumberEvent.FALLBACK_SUCCESS)); json.writeNumberField("rollingCountResponsesFromCache", commandMetrics.getRollingCount(HystrixRollingNumberEvent.RESPONSE_FROM_CACHE)); json.writeNumberField("rollingCountSemaphoreRejected", commandMetrics.getRollingCount(HystrixRollingNumberEvent.SEMAPHORE_REJECTED)); json.writeNumberField("rollingCountShortCircuited", commandMetrics.getRollingCount(HystrixRollingNumberEvent.SHORT_CIRCUITED)); json.writeNumberField("rollingCountSuccess", commandMetrics.getRollingCount(HystrixRollingNumberEvent.SUCCESS)); json.writeNumberField("rollingCountThreadPoolRejected", commandMetrics.getRollingCount(HystrixRollingNumberEvent.THREAD_POOL_REJECTED)); json.writeNumberField("rollingCountTimeout", commandMetrics.getRollingCount(HystrixRollingNumberEvent.TIMEOUT)); json.writeNumberField("currentConcurrentExecutionCount", commandMetrics.getCurrentConcurrentExecutionCount()); // latency percentiles json.writeNumberField("latencyExecute_mean", commandMetrics.getExecutionTimeMean()); json.writeObjectFieldStart("latencyExecute"); json.writeNumberField("0", commandMetrics.getExecutionTimePercentile(0)); json.writeNumberField("25", commandMetrics.getExecutionTimePercentile(25)); json.writeNumberField("50", commandMetrics.getExecutionTimePercentile(50)); json.writeNumberField("75", commandMetrics.getExecutionTimePercentile(75)); json.writeNumberField("90", commandMetrics.getExecutionTimePercentile(90)); json.writeNumberField("95", commandMetrics.getExecutionTimePercentile(95)); json.writeNumberField("99", commandMetrics.getExecutionTimePercentile(99)); json.writeNumberField("99.5", commandMetrics.getExecutionTimePercentile(99.5)); json.writeNumberField("100", commandMetrics.getExecutionTimePercentile(100)); json.writeEndObject(); // json.writeNumberField("latencyTotal_mean", commandMetrics.getTotalTimeMean()); json.writeObjectFieldStart("latencyTotal"); json.writeNumberField("0", commandMetrics.getTotalTimePercentile(0)); json.writeNumberField("25", commandMetrics.getTotalTimePercentile(25)); json.writeNumberField("50", commandMetrics.getTotalTimePercentile(50)); json.writeNumberField("75", commandMetrics.getTotalTimePercentile(75)); json.writeNumberField("90", commandMetrics.getTotalTimePercentile(90)); json.writeNumberField("95", commandMetrics.getTotalTimePercentile(95)); json.writeNumberField("99", commandMetrics.getTotalTimePercentile(99)); json.writeNumberField("99.5", commandMetrics.getTotalTimePercentile(99.5)); json.writeNumberField("100", commandMetrics.getTotalTimePercentile(100)); json.writeEndObject(); // property values for reporting what is actually seen by the command rather than what was set somewhere HystrixCommandProperties commandProperties = commandMetrics.getProperties(); json.writeNumberField("propertyValue_circuitBreakerRequestVolumeThreshold", commandProperties.circuitBreakerRequestVolumeThreshold().get()); json.writeNumberField("propertyValue_circuitBreakerSleepWindowInMilliseconds", commandProperties.circuitBreakerSleepWindowInMilliseconds().get()); json.writeNumberField("propertyValue_circuitBreakerErrorThresholdPercentage", commandProperties.circuitBreakerErrorThresholdPercentage().get()); json.writeBooleanField("propertyValue_circuitBreakerForceOpen", commandProperties.circuitBreakerForceOpen().get()); json.writeBooleanField("propertyValue_circuitBreakerForceClosed", commandProperties.circuitBreakerForceClosed().get()); json.writeBooleanField("propertyValue_circuitBreakerEnabled", commandProperties.circuitBreakerEnabled().get()); json.writeStringField("propertyValue_executionIsolationStrategy", commandProperties.executionIsolationStrategy().get().name()); json.writeNumberField("propertyValue_executionIsolationThreadTimeoutInMilliseconds", commandProperties.executionIsolationThreadTimeoutInMilliseconds().get()); json.writeBooleanField("propertyValue_executionIsolationThreadInterruptOnTimeout", commandProperties.executionIsolationThreadInterruptOnTimeout().get()); json.writeStringField("propertyValue_executionIsolationThreadPoolKeyOverride", commandProperties.executionIsolationThreadPoolKeyOverride().get()); json.writeNumberField("propertyValue_executionIsolationSemaphoreMaxConcurrentRequests", commandProperties.executionIsolationSemaphoreMaxConcurrentRequests().get()); json.writeNumberField("propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests", commandProperties.fallbackIsolationSemaphoreMaxConcurrentRequests().get()); /* * The following are commented out as these rarely change and are verbose for streaming for something people don't change. * We could perhaps allow a property or request argument to include these. */ // json.put("propertyValue_metricsRollingPercentileEnabled", commandProperties.metricsRollingPercentileEnabled().get()); // json.put("propertyValue_metricsRollingPercentileBucketSize", commandProperties.metricsRollingPercentileBucketSize().get()); // json.put("propertyValue_metricsRollingPercentileWindow", commandProperties.metricsRollingPercentileWindowInMilliseconds().get()); // json.put("propertyValue_metricsRollingPercentileWindowBuckets", commandProperties.metricsRollingPercentileWindowBuckets().get()); // json.put("propertyValue_metricsRollingStatisticalWindowBuckets", commandProperties.metricsRollingStatisticalWindowBuckets().get()); json.writeNumberField("propertyValue_metricsRollingStatisticalWindowInMilliseconds", commandProperties.metricsRollingStatisticalWindowInMilliseconds().get()); json.writeBooleanField("propertyValue_requestCacheEnabled", commandProperties.requestCacheEnabled().get()); json.writeBooleanField("propertyValue_requestLogEnabled", commandProperties.requestLogEnabled().get()); json.writeNumberField("reportingHosts", 1); // this will get summed across all instances in a cluster if (circuitBreaker != null && circuitBreaker.isOpen()) { json.writeStringField("openCircuitHostNames", hostName); // pass the host name from where open circuit is being reported } else { json.writeStringField("openCircuitHostNames",""); // add an empty string } json.writeEndObject(); json.close(); // output to handler listener.handleJsonMetric(jsonString.getBuffer().toString()); } // thread pool metrics for (HystrixThreadPoolMetrics threadPoolMetrics : HystrixThreadPoolMetrics.getInstances()) { HystrixThreadPoolKey key = threadPoolMetrics.getThreadPoolKey(); StringWriter jsonString = new StringWriter(); JsonGenerator json = jsonFactory.createGenerator(jsonString); json.writeStartObject(); json.writeStringField("type", "HystrixThreadPool"); json.writeStringField("name", key.name()); json.writeNumberField("currentTime", System.currentTimeMillis()); json.writeNumberField("currentActiveCount", threadPoolMetrics.getCurrentActiveCount().intValue()); json.writeNumberField("currentCompletedTaskCount", threadPoolMetrics.getCurrentCompletedTaskCount().longValue()); json.writeNumberField("currentCorePoolSize", threadPoolMetrics.getCurrentCorePoolSize().intValue()); json.writeNumberField("currentLargestPoolSize", threadPoolMetrics.getCurrentLargestPoolSize().intValue()); json.writeNumberField("currentMaximumPoolSize", threadPoolMetrics.getCurrentMaximumPoolSize().intValue()); json.writeNumberField("currentPoolSize", threadPoolMetrics.getCurrentPoolSize().intValue()); json.writeNumberField("currentQueueSize", threadPoolMetrics.getCurrentQueueSize().intValue()); json.writeNumberField("currentTaskCount", threadPoolMetrics.getCurrentTaskCount().longValue()); json.writeNumberField("rollingCountThreadsExecuted", threadPoolMetrics.getRollingCountThreadsExecuted()); json.writeNumberField("rollingMaxActiveThreads", threadPoolMetrics.getRollingMaxActiveThreads()); json.writeNumberField("propertyValue_queueSizeRejectionThreshold", threadPoolMetrics.getProperties().queueSizeRejectionThreshold().get()); json.writeNumberField("propertyValue_metricsRollingStatisticalWindowInMilliseconds", threadPoolMetrics.getProperties().metricsRollingStatisticalWindowInMilliseconds().get()); json.writeNumberField("reportingHosts", 1); // this will get summed across all instances in a cluster json.writeEndObject(); json.close(); // output to stream listener.handleJsonMetric(jsonString.getBuffer().toString()); } } catch (Exception e) { logger.warn("Failed to output metrics as JSON", e); // shutdown pause(); return; } } } private class MetricsPollerThreadFactory implements ThreadFactory { private static final String MetricsThreadName = "HystrixMetricPoller"; private final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); public Thread newThread(Runnable r) { Thread thread = defaultFactory.newThread(r); thread.setName(MetricsThreadName); return thread; } } public static class UnitTest { @Test public void testStartStopStart() { final AtomicInteger metricsCount = new AtomicInteger(); HystrixMetricsPoller poller = new HystrixMetricsPoller(new MetricsAsJsonPollerListener() { @Override public void handleJsonMetric(String json) { System.out.println("Received: " + json); metricsCount.incrementAndGet(); } }, 100); try { HystrixCommand<Boolean> test = new HystrixCommand<Boolean>(HystrixCommandGroupKey.Factory.asKey("HystrixMetricsPollerTest")) { @Override protected Boolean run() { return true; } }; test.execute(); poller.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } int v1 = metricsCount.get(); assertTrue(v1 > 0); poller.pause(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } int v2 = metricsCount.get(); // they should be the same since we were paused assertTrue(v2 == v1); poller.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } int v3 = metricsCount.get(); // we should have more metrics again assertTrue(v3 > v1); } finally { poller.shutdown(); } } } }