From 4ca9d5f772f2bd6b46546cf2431f9e2b1cd969de Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 2 Sep 2014 10:11:12 +0100 Subject: [PATCH] Break up hystrix dashboard --- pom.xml | 15 ++ .../netflix/hystrix/HystrixConfiguration.java | 2 +- .../HystrixDashboardConfiguration.java | 168 ++++++++++++++++++ .../HystrixStreamEndpoint.java | 4 +- .../annotations/EnableHystrixDashboard.java | 20 +++ ....java => HystrixDashboardApplication.java} | 14 +- .../hystrix/stream/ProxyStreamServlet.java | 152 ---------------- .../src/main/resources/application.yml | 1 - .../cloud/netflix/zuul/Application.java | 3 +- 9 files changed, 215 insertions(+), 164 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java rename spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/{endpoint => hystrix}/HystrixStreamEndpoint.java (74%) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java rename spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/{Application.java => HystrixDashboardApplication.java} (69%) delete mode 100644 spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/stream/ProxyStreamServlet.java diff --git a/pom.xml b/pom.xml index e55b37a0..c5d055b1 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,21 @@ jersey-server 1.13 + + org.webjars + d3js + 3.4.11 + + + org.webjars + jquery + 2.1.1 + + + org.webjars + bootstrap + 3.2.0 + diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixConfiguration.java index 84bebf5a..afc41eb3 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixConfiguration.java @@ -1,6 +1,7 @@ package org.springframework.cloud.netflix.hystrix; import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,7 +9,6 @@ import org.springframework.context.annotation.ImportAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.cloud.netflix.hystrix.annotations.EnableHystrix; -import org.springframework.cloud.netflix.endpoint.HystrixStreamEndpoint; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java new file mode 100644 index 00000000..3ff80319 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixDashboardConfiguration.java @@ -0,0 +1,168 @@ +package org.springframework.cloud.netflix.hystrix; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Dave Syer + */ +@SuppressWarnings("deprecation") +@Configuration +public class HystrixDashboardConfiguration { + + @Bean + public ServletRegistrationBean proxyStreamServlet() { + return new ServletRegistrationBean(new ProxyStreamServlet(), "/proxy.stream"); + } + + /** + * Proxy an EventStream request (data.stream via proxy.stream) since EventStream does not yet support CORS (https://bugs.webkit.org/show_bug.cgi?id=61862) + * so that a UI can request a stream from a different server. + */ + public static class ProxyStreamServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private static final Logger logger = LoggerFactory.getLogger(ProxyStreamServlet.class); + + public ProxyStreamServlet() { + super(); + } + + /** + * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) + */ + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String origin = request.getParameter("origin"); + if (origin == null) { + response.setStatus(500); + response.getWriter().println("Required parameter 'origin' missing. Example: 107.20.175.135:7001"); + } + origin = origin.trim(); + + HttpGet httpget = null; + InputStream is = null; + boolean hasFirstParameter = false; + StringBuilder url = new StringBuilder(); + if (!origin.startsWith("http")) { + url.append("http://"); + } + url.append(origin); + if (origin.contains("?")) { + hasFirstParameter = true; + } + Map params = request.getParameterMap(); + for (String key : params.keySet()) { + if (!key.equals("origin")) { + String[] values = params.get(key); + String value = values[0].trim(); + if (hasFirstParameter) { + url.append("&"); + } else { + url.append("?"); + hasFirstParameter = true; + } + url.append(key).append("=").append(value); + } + } + String proxyUrl = url.toString(); + logger.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n"); + try { + httpget = new HttpGet(proxyUrl); + HttpClient client = ProxyConnectionManager.httpClient; + HttpResponse httpResponse = client.execute(httpget); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + if (statusCode == HttpStatus.SC_OK) { + // writeTo swallows exceptions and never quits even if outputstream is throwing IOExceptions (such as broken pipe) ... since the inputstream is infinite + // httpResponse.getEntity().writeTo(new OutputStreamWrapper(response.getOutputStream())); + // so I copy it manually ... + is = httpResponse.getEntity().getContent(); + + // set headers + for (Header header : httpResponse.getAllHeaders()) { + response.addHeader(header.getName(), header.getValue()); + } + + // copy data from source to response + OutputStream os = response.getOutputStream(); + int b = -1; + while ((b = is.read()) != -1) { + try { + os.write(b); + if (b == 10 /** flush buffer on line feed */) { + os.flush(); + } + } catch (Exception e) { + if (e.getClass().getSimpleName().equalsIgnoreCase("ClientAbortException")) { + // don't throw an exception as this means the user closed the connection + logger.debug("Connection closed by client. Will stop proxying ..."); + // break out of the while loop + break; + } else { + // received unknown error while writing so throw an exception + throw new RuntimeException(e); + } + } + } + } + } catch (Exception e) { + logger.error("Error proxying request: " + url, e); + } finally { + if (httpget != null) { + try { + httpget.abort(); + } catch (Exception e) { + logger.error("failed aborting proxy connection.", e); + } + } + + // httpget.abort() MUST be called first otherwise is.close() hangs (because data is still streaming?) + if (is != null) { + // this should already be closed by httpget.abort() above + try { + is.close(); + } catch (Exception e) { + // e.printStackTrace(); + } + } + } + } + + private static class ProxyConnectionManager { + private final static PoolingClientConnectionManager threadSafeConnectionManager = new PoolingClientConnectionManager(); + private final static HttpClient httpClient = new DefaultHttpClient(threadSafeConnectionManager); + + static { + logger.debug("Initialize ProxyConnectionManager"); + /* common settings */ + HttpParams httpParams = httpClient.getParams(); + HttpConnectionParams.setConnectionTimeout(httpParams, 5000); + HttpConnectionParams.setSoTimeout(httpParams, 10000); + + /* number of connections to allow */ + threadSafeConnectionManager.setDefaultMaxPerRoute(400); + threadSafeConnectionManager.setMaxTotal(400); + } + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/endpoint/HystrixStreamEndpoint.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixStreamEndpoint.java similarity index 74% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/endpoint/HystrixStreamEndpoint.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixStreamEndpoint.java index ac279c91..9c0aa3ca 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/endpoint/HystrixStreamEndpoint.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/HystrixStreamEndpoint.java @@ -1,4 +1,6 @@ -package org.springframework.cloud.netflix.endpoint; +package org.springframework.cloud.netflix.hystrix; + +import org.springframework.cloud.netflix.endpoint.ServletWrappingEndpoint; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java new file mode 100644 index 00000000..3537346b --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/hystrix/annotations/EnableHystrixDashboard.java @@ -0,0 +1,20 @@ +package org.springframework.cloud.netflix.hystrix.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.cloud.netflix.hystrix.HystrixDashboardConfiguration; +import org.springframework.context.annotation.Import; + +/** + * Created by sgibb on 6/19/14. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(HystrixDashboardConfiguration.class) +public @interface EnableHystrixDashboard { +} \ No newline at end of file diff --git a/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/Application.java b/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/HystrixDashboardApplication.java similarity index 69% rename from spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/Application.java rename to spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/HystrixDashboardApplication.java index 82f21cec..57d0cb51 100644 --- a/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/Application.java +++ b/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/HystrixDashboardApplication.java @@ -1,11 +1,12 @@ package io.spring.cloud.netflix.hystrix; import io.spring.cloud.netflix.hystrix.stream.MockStreamServlet; -import io.spring.cloud.netflix.hystrix.stream.ProxyStreamServlet; + import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.embedded.ServletRegistrationBean; import org.springframework.boot.context.web.SpringBootServletInitializer; +import org.springframework.cloud.netflix.hystrix.annotations.EnableHystrixDashboard; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -15,16 +16,17 @@ import org.springframework.context.annotation.Configuration; */ @Configuration @ComponentScan +@EnableHystrixDashboard @EnableAutoConfiguration -public class Application extends SpringBootServletInitializer { +public class HystrixDashboardApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { - return application.sources(Application.class).web(true); + return application.sources(HystrixDashboardApplication.class).web(true); } public static void main(String[] args) { - new SpringApplicationBuilder(Application.class).web(true).run(args); + new SpringApplicationBuilder(HystrixDashboardApplication.class).web(true).run(args); } @Bean @@ -32,8 +34,4 @@ public class Application extends SpringBootServletInitializer { return new ServletRegistrationBean(new MockStreamServlet(), "/mock.stream"); } - @Bean - public ServletRegistrationBean proxyStreamServlet() { - return new ServletRegistrationBean(new ProxyStreamServlet(), "/proxy.stream"); - } } diff --git a/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/stream/ProxyStreamServlet.java b/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/stream/ProxyStreamServlet.java deleted file mode 100644 index 6bd0e9e2..00000000 --- a/spring-cloud-netflix-hystrix/src/main/java/io/spring/cloud/netflix/hystrix/stream/ProxyStreamServlet.java +++ /dev/null @@ -1,152 +0,0 @@ -package io.spring.cloud.netflix.hystrix.stream; - -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.PoolingClientConnectionManager; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Map; - -/** - * Proxy an EventStream request (data.stream via proxy.stream) since EventStream does not yet support CORS (https://bugs.webkit.org/show_bug.cgi?id=61862) - * so that a UI can request a stream from a different server. - */ -public class ProxyStreamServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - private static final Logger logger = LoggerFactory.getLogger(ProxyStreamServlet.class); - - public ProxyStreamServlet() { - super(); - } - - /** - * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) - */ - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String origin = request.getParameter("origin"); - if (origin == null) { - response.setStatus(500); - response.getWriter().println("Required parameter 'origin' missing. Example: 107.20.175.135:7001"); - } - origin = origin.trim(); - - HttpGet httpget = null; - InputStream is = null; - boolean hasFirstParameter = false; - StringBuilder url = new StringBuilder(); - if (!origin.startsWith("http")) { - url.append("http://"); - } - url.append(origin); - if (origin.contains("?")) { - hasFirstParameter = true; - } - @SuppressWarnings("unchecked") - Map params = request.getParameterMap(); - for (String key : params.keySet()) { - if (!key.equals("origin")) { - String[] values = params.get(key); - String value = values[0].trim(); - if (hasFirstParameter) { - url.append("&"); - } else { - url.append("?"); - hasFirstParameter = true; - } - url.append(key).append("=").append(value); - } - } - String proxyUrl = url.toString(); - logger.info("\n\nProxy opening connection to: " + proxyUrl + "\n\n"); - try { - httpget = new HttpGet(proxyUrl); - HttpClient client = ProxyConnectionManager.httpClient; - HttpResponse httpResponse = client.execute(httpget); - int statusCode = httpResponse.getStatusLine().getStatusCode(); - if (statusCode == HttpStatus.SC_OK) { - // writeTo swallows exceptions and never quits even if outputstream is throwing IOExceptions (such as broken pipe) ... since the inputstream is infinite - // httpResponse.getEntity().writeTo(new OutputStreamWrapper(response.getOutputStream())); - // so I copy it manually ... - is = httpResponse.getEntity().getContent(); - - // set headers - for (Header header : httpResponse.getAllHeaders()) { - response.addHeader(header.getName(), header.getValue()); - } - - // copy data from source to response - OutputStream os = response.getOutputStream(); - int b = -1; - while ((b = is.read()) != -1) { - try { - os.write(b); - if (b == 10 /** flush buffer on line feed */) { - os.flush(); - } - } catch (Exception e) { - if (e.getClass().getSimpleName().equalsIgnoreCase("ClientAbortException")) { - // don't throw an exception as this means the user closed the connection - logger.debug("Connection closed by client. Will stop proxying ..."); - // break out of the while loop - break; - } else { - // received unknown error while writing so throw an exception - throw new RuntimeException(e); - } - } - } - } - } catch (Exception e) { - logger.error("Error proxying request: " + url, e); - } finally { - if (httpget != null) { - try { - httpget.abort(); - } catch (Exception e) { - logger.error("failed aborting proxy connection.", e); - } - } - - // httpget.abort() MUST be called first otherwise is.close() hangs (because data is still streaming?) - if (is != null) { - // this should already be closed by httpget.abort() above - try { - is.close(); - } catch (Exception e) { - // e.printStackTrace(); - } - } - } - } - - private static class ProxyConnectionManager { - private final static PoolingClientConnectionManager threadSafeConnectionManager = new PoolingClientConnectionManager(); - private final static HttpClient httpClient = new DefaultHttpClient(threadSafeConnectionManager); - - static { - logger.debug("Initialize ProxyConnectionManager"); - /* common settings */ - HttpParams httpParams = httpClient.getParams(); - HttpConnectionParams.setConnectionTimeout(httpParams, 5000); - HttpConnectionParams.setSoTimeout(httpParams, 10000); - - /* number of connections to allow */ - threadSafeConnectionManager.setDefaultMaxPerRoute(400); - threadSafeConnectionManager.setMaxTotal(400); - } - } -} diff --git a/spring-cloud-netflix-hystrix/src/main/resources/application.yml b/spring-cloud-netflix-hystrix/src/main/resources/application.yml index 0510b269..a491a9e4 100644 --- a/spring-cloud-netflix-hystrix/src/main/resources/application.yml +++ b/spring-cloud-netflix-hystrix/src/main/resources/application.yml @@ -9,7 +9,6 @@ endpoints: server: port: 7979 - context-path: /hystrix-dashboard logging: level: INFO diff --git a/spring-cloud-netflix-zuul/src/main/java/io/spring/cloud/netflix/zuul/Application.java b/spring-cloud-netflix-zuul/src/main/java/io/spring/cloud/netflix/zuul/Application.java index d796c862..fbea9781 100644 --- a/spring-cloud-netflix-zuul/src/main/java/io/spring/cloud/netflix/zuul/Application.java +++ b/spring-cloud-netflix-zuul/src/main/java/io/spring/cloud/netflix/zuul/Application.java @@ -1,13 +1,14 @@ package io.spring.cloud.netflix.zuul; import com.netflix.zuul.context.ContextLifecycleFilter; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.cloud.netflix.endpoint.HystrixStreamEndpoint; +import org.springframework.cloud.netflix.hystrix.HystrixStreamEndpoint; import org.springframework.cloud.netflix.zuul.Routes; import org.springframework.scheduling.annotation.EnableScheduling;