├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build-docker-dev.sh
├── build.gradle
├── dependencies-version.properties
├── dependencies.lock
├── docker
└── Dockerfile
├── findbugs-exclusions.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
├── java
│ └── com
│ │ └── github
│ │ └── kennedyoliveira
│ │ └── hystrix
│ │ └── contrib
│ │ └── standalone
│ │ └── dashboard
│ │ ├── Configuration.java
│ │ ├── HystrixDashboardLauncher.java
│ │ ├── HystrixDashboardProxyConnectionHandler.java
│ │ ├── HystrixDashboardProxyEurekaAppsListingHandler.java
│ │ ├── HystrixDashboardVerticle.java
│ │ └── StringUtils.java
└── resources
│ └── logback.xml
└── test
├── java
└── com
│ └── github
│ └── kennedyoliveira
│ └── hystrix
│ └── contrib
│ └── standalone
│ └── dashboard
│ ├── HystrixDashboardConfigurationTest.java
│ ├── HystrixDashboardProxyConnectionHandlerTest.java
│ ├── HystrixDashboardProxyEurekaAppsListingHandlerTest.java
│ ├── HystrixDashboardProxyEurekaTest.java
│ └── HystrixDashboardVerticleTest.java
└── resources
├── eureka-data.xml
└── logback-test.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 |
4 | # Ignore Gradle GUI config
5 | gradle-app.setting
6 |
7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
8 | !gradle-wrapper.jar
9 |
10 | # Cache of project
11 | .gradletasknamecache
12 | .idea
13 | .DS_Store
14 | .vertx
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | jdk:
4 | - oraclejdk8
5 |
6 | sudo:
7 | false
8 |
9 | cache:
10 | directories:
11 | - $HOME/.gradle/caches
12 | - $HOME/.gradle/wrapper
13 |
14 | install:
15 | ./gradlew assemble
16 |
17 | script:
18 | ./gradlew clean check --info
19 |
20 | after_script:
21 | ./gradlew -Pversioneye.api_key=$VERSIONEYE_API_KEY versioneye-update
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Kennedy Oliveira
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Standalone Hystrix Dashboard
2 |
3 | [](https://travis-ci.org/kennedyoliveira/standalone-hystrix-dashboard)
4 | [](https://www.codacy.com/app/kennedy-oliveira/standalone-hystrix-dashboard)
5 | [](https://www.versioneye.com/user/projects/56fcad23905db1003b29956c)
6 | [ ](https://bintray.com/kennedyoliveira/maven/standalone-hystrix-dashboard/_latestVersion)
7 |
8 | Standalone *hystrix dashboard* that can be started using a single fatJar and is very lightweight and fast!
9 |
10 | ## Features
11 | - Extremely fast startup (About half a sec)
12 | - Extremely lightweight, the fat jar is around 5 MBs
13 | - Easy to start, just run the JAR
14 | - Doesn't need a servlet container
15 | - Non Blocking
16 | - Implemented with [Vert.x](http://vertx.io/)
17 | - Compression enable (Saves alot of bandwidth)
18 | - Docker Image to easily deploy
19 |
20 |
21 | ## Another hystrix-dashboards web app?
22 | It's the same hystrix-dashboard app as the Netflix one,
23 | that can be found here [netflix-hystrix-dashboard](https://github.com/Netflix/Hystrix/tree/master/hystrix-dashboard),
24 | the only difference is that this one isn't servlet based,
25 | so doesn't need a servlet container, nor any other configuration,
26 | it's just a single jar that you can run and you are read to start monitoring you hystrix enabled services.
27 |
28 |
29 | ## Motivations
30 | When i first tried hystrix and hystrix-dashboard, i had some problems testing the examples, not only me but other people had problems too, i think that hystrix-dashboard is soo awesome that shouldn't take more than a single file run to be able to use it, so i built this little adaptation to provide that, and help people that want to get started using hystrix and it's modules, and help advanced users that just need to run a dashboard more easily.
31 |
32 |
33 | ## Download
34 | The standalone-hystrix-dashboard is available at `Maven Central`, `BinTray`.
35 |
36 | Click on "download" blue badge in the top to go to bintray.
37 |
38 | The maven link will be available once it gets published.
39 |
40 |
41 | ## Run fatJar
42 | Generate the fatJar from source or download it and simple do the following:
43 | ```
44 | java -jar standalone-hystrix-dashboard-{VERSION}-all.jar
45 | ```
46 | it should start the dashboard on default port `7979`.
47 |
48 | ## Run on background
49 | ### Starting the application
50 | Generate the fatJar from source or download it and simple do the following:
51 | ```
52 | java -jar standalone-hystrix-dashboard-{VERSION}-all.jar start
53 | ```
54 | it should start the dashboard on default port `7979` and it will print an UUID.
55 |
56 | ### Stopping the application
57 | After starting it, the startup process will print a UUID that you can use it to stop the application,
58 | if you don't remember the UUID you can check the running instances using the following commands:
59 | ```
60 | java -jar standalone-hystrix-dashboard-{VERSION}-all.jar list
61 | ```
62 |
63 | With the UUID you can stop the running instance with the following command:
64 | ```
65 | java -jar standalone-hystrix-dashboard-{VERSION}-all.jar stop UUDI
66 | ```
67 |
68 | ## Run from source
69 | To run the project from source simple do the following:
70 | ```
71 | git clone https://github.com/kennedyoliveira/standalone-hystrix-dashboard.git
72 | cd standalone-hystrix-dashboard
73 | ./gradlew runDashboard
74 | ```
75 |
76 | it should start the dashboard on default port `7979`.
77 |
78 | ## Generate fatJar from source
79 | To generate the fatJar from source simple do the following:
80 | ```
81 | git clone https://github.com/kennedyoliveira/standalone-hystrix-dashboard.git
82 | cd standalone-hystrix-dashboard
83 | ./gradlew fatJar
84 | ```
85 | and your fatJar should be in `build/libs/standalone-hystrix-dashboard-{VERSION}-all.jar`.
86 |
87 |
88 | ## Docker Image
89 | There is a docker image available that you can run by:
90 | ```
91 | docker run --rm -ti -p 7979:7979 kennedyoliveira/hystrix-dashboard
92 | ```
93 |
94 | You can pass configuration or jvm params by using the `ENV VAR` `JVM_ARGS` as with the example below:
95 | ```
96 | docker run --rm -ti -p 7979:7979 -e JVM_ARGS='-Xmx2048m' kennedyoliveira/hystrix-dashboard
97 | ```
98 |
99 | The docker images will have tags equal to hystrix-dashboard and hystrix-core versions, so if you need a especific version you can check there, starting with 1.5.1+
100 |
101 |
102 | ## Configurations
103 |
104 | You can pass configuration parameters using the `-Dconfiguration=value` parameter, the available configurations are listened in the table below.
105 |
106 | Example:
107 |
108 | ```
109 | java -jar -DserverPort=8080 -DbindAddress=192.168.1.100 standalone-hystrix-dashboard-{VERSION}-all.jar
110 | ```
111 |
112 | | Configuration | Description | Default |
113 | |---------------|-------------|---------|
114 | | `serverPort` | The port that the server will listen to. | `7979` |
115 | | `bindAddress` | The address that the server will bind to. | `0.0.0.0` |
116 | | `disableCompression` | Flag to disable compression support for the metrics stream | `enabled` |
117 |
118 | ## Demonstration
119 | 
120 |
--------------------------------------------------------------------------------
/build-docker-dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | cd docker
4 | docker build --force-rm --no-cache -t kennedyoliveira/hystrix-dashboard:dev .
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories { jcenter() }
3 | dependencies {
4 | classpath 'com.netflix.nebula:nebula-publishing-plugin:4.9.1'
5 | }
6 | }
7 |
8 | plugins {
9 | id 'nebula.dependency-recommender' version '3.6.3'
10 | id 'nebula.dependency-lock' version '4.3.0'
11 | id 'nebula.release' version '4.1.0'
12 | id 'nebula.contacts' version '3.0.1'
13 | id 'nebula.info' version '3.2.1'
14 | id 'nebula.nebula-bintray' version '3.5.0'
15 | id 'nebula.provided-base' version '3.1.0'
16 | id "org.standardout.versioneye" version '1.4.0'
17 | }
18 |
19 | apply plugin: 'java'
20 | apply plugin: 'application'
21 | apply plugin: 'nebula.maven-publish'
22 | apply plugin: 'nebula.nebula-bintray-publishing'
23 | apply plugin: 'nebula.info'
24 | apply plugin: 'nebula.source-jar'
25 | apply plugin: 'nebula.javadoc-jar'
26 |
27 | // QA plugins
28 | apply plugin: 'pmd'
29 | apply plugin: 'findbugs'
30 | apply plugin: 'build-dashboard'
31 |
32 | group 'com.github.kennedyoliveira'
33 | description = 'Standalone Hystrix-Dashboard implementation.'
34 |
35 | contacts {
36 | 'kennedy.oliveira@outlook.com' {
37 | moniker 'Kennedy Oliveira'
38 | github 'kennedyoliveira'
39 | roles 'owner', 'developer', 'notify'
40 | }
41 | }
42 |
43 | task wrapper(type: Wrapper) {
44 | gradleVersion = '3.1'
45 | }
46 |
47 | // for the application plugin
48 | mainClassName = 'com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard.HystrixDashboardLauncher'
49 |
50 | ext {
51 | mainVerticleName = 'com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard.HystrixDashboardVerticle'
52 | }
53 |
54 | distributions {
55 | main {
56 | contents {
57 | exclude '**/*lombok*'
58 | }
59 | }
60 | }
61 |
62 | repositories {
63 | mavenCentral()
64 | }
65 |
66 | def recomendationsFile = rootProject.file('dependencies-version.properties')
67 |
68 | dependencyRecommendations {
69 | propertiesFile file: recomendationsFile
70 | }
71 |
72 | configurations {
73 | hystrixDashboard
74 | }
75 |
76 | dependencies {
77 | hystrixDashboard('com.netflix.hystrix:hystrix-dashboard') {
78 | transitive = false
79 | }
80 |
81 | provided group: 'org.projectlombok', name: 'lombok'
82 |
83 | compile(group: 'io.vertx', name: 'vertx-web') {
84 | // won't use it
85 | exclude module: 'vertx-auth-common'
86 | }
87 |
88 | compile group: 'org.slf4j', name: 'slf4j-api'
89 | compile group: 'ch.qos.logback', name: 'logback-classic'
90 |
91 | testCompile group: 'junit', name: 'junit'
92 | testCompile group: 'io.vertx', name: 'vertx-unit'
93 | testCompile group: 'com.netflix.hystrix', name: 'hystrix-core'
94 | testCompile group: 'com.github.kennedyoliveira', name: 'hystrix-vertx-metrics-stream'
95 | testCompile group: 'org.mockito', name: 'mockito-core'
96 | testCompile group: 'com.github.stefanbirkner', name: 'system-rules'
97 | }
98 |
99 | findbugs {
100 | excludeFilter = file("$rootProject.projectDir/findbugs-exclusions.xml")
101 | }
102 |
103 | pmd {
104 | toolVersion = '5.5.1'
105 | }
106 |
107 | // qa plugin configuration to export html instead of xml
108 | tasks.withType(FindBugs) {
109 | reports {
110 | html.enabled = true
111 | xml.enabled = false
112 | }
113 | }
114 |
115 | tasks.withType(Pmd) {
116 | reports {
117 | html.enabled = true
118 | xml.enabled = false
119 | }
120 | }
121 |
122 | publishing {
123 | publications {
124 | nebula(MavenPublication) {
125 | pom.withXml {
126 | asNode().appendNode('licenses').appendNode('license').with {
127 | appendNode('name', 'MIT License')
128 | appendNode('url', 'https://opensource.org/licenses/MIT')
129 | appendNode('distribution', 'repo')
130 | }
131 | }
132 |
133 | // fat jar to be downloadable from binTray and maven central
134 | artifact fatJar {
135 | classifier 'all'
136 | }
137 | }
138 | }
139 | }
140 |
141 | bintray {
142 | pkg {
143 | repo = 'maven'
144 | userOrg = ''
145 | licenses = ['MIT']
146 | websiteUrl = 'https://github.com/kennedyoliveira/standalone-hystrix-dashboard'
147 | issueTrackerUrl = 'https://github.com/kennedyoliveira/standalone-hystrix-dashboard/issues'
148 | vcsUrl = 'https://github.com/kennedyoliveira/standalone-hystrix-dashboard.git'
149 | labels = ['hyxtrix-dashboard', 'vertx3', 'standalone']
150 | publicDownloadNumbers = true
151 | version {
152 | gpg {
153 | if (project.hasProperty('bintraySignPass'))
154 | passphrase = project.property('bintraySignPass')
155 | }
156 | mavenCentralSync {
157 | sync = false
158 | }
159 | }
160 | }
161 | }
162 |
163 | jar {
164 | manifest {
165 | attributes('Implementation-Title': project.name,
166 | 'Implementation-Version': project.version,
167 | 'Main-Class': project.property('mainClassName'),
168 | 'Main-Verticle': project.property('mainVerticleName'))
169 | }
170 | }
171 |
172 | /**
173 | * Extracts the hystrix-dashboard webapp content, removing WEB-INF and META-INF since it won't be used
174 | */
175 | task extractHystrixDashboard(type: Copy) {
176 | from(configurations.hystrixDashboard.collect { zipTree(it).asFileTree }) {
177 | // remove because it won't be used
178 | exclude { it.relativePath.pathString.startsWith('WEB-INF') }
179 | exclude { it.relativePath.pathString.startsWith('META-INF') }
180 | }
181 |
182 | into "$buildDir/resources/main/webroot"
183 | }
184 |
185 | tasks.processResources.dependsOn extractHystrixDashboard
186 |
187 | task('runDashboard', dependsOn: ['classes'], type: JavaExec) {
188 | classpath = sourceSets.main.runtimeClasspath
189 |
190 | main = project.property('mainClassName')
191 |
192 | jvmArgs = ['-Xmx2048m']
193 | }
194 |
195 | //create a single Jar with all dependencies
196 | task fatJar(type: Jar) {
197 | manifest {
198 | attributes('Implementation-Title': project.name,
199 | 'Implementation-Version': project.version,
200 | 'Main-Class': project.property('mainClassName'),
201 | 'Main-Verticle': project.property('mainVerticleName'))
202 | }
203 | baseName = project.name
204 | from({ configurations.compile.collect { it.isDirectory() ? it : zipTree(it).asFileTree } }) {
205 | // exclude { println it.relativePath }
206 | exclude {
207 | def path = it.relativePath.pathString
208 | path.startsWith('io/vertx/groovy') || // groovy not using
209 | path.startsWith('io/vertx/groovy') || // groovy not using
210 | path.startsWith('io/vertx/rxjava') || // rx java not using
211 | path.startsWith('vertx-web-js') || // js not using
212 | path.startsWith('vertx-java') || // java templates not using
213 | path.startsWith('vertx-web') || // ruby classes not using
214 | path.contains("lombok") // lombok is provided
215 | }
216 | }
217 | with jar
218 | }
--------------------------------------------------------------------------------
/dependencies-version.properties:
--------------------------------------------------------------------------------
1 | # Lombok
2 | org.projectlombok:lombok=1.16.+
3 |
4 | # =============
5 | # Hystrix
6 | # =============
7 | HYSTRIX_VERSION=1.5.+
8 |
9 | # Hystrix Dashboard
10 | com.netflix.hystrix:hystrix-dashboard=$HYSTRIX_VERSION
11 |
12 | # Hystrix Core
13 | com.netflix.hystrix:hystrix-core=$HYSTRIX_VERSION
14 |
15 | # Hystrix Metrics Stream
16 | com.github.kennedyoliveira:hystrix-vertx-metrics-stream=$HYSTRIX_VERSION
17 |
18 | # =============
19 | # END Hystrix
20 | # =============
21 |
22 | # Logging
23 | SLF4J_VERSION=1.7.+
24 | org.slf4j:slf4j-api=$SLF4J_VERSION
25 |
26 | # Log Back
27 | LOG_BACK_VERSION=1.+
28 | ch.qos.logback:logback-core=$LOG_BACK_VERSION
29 | ch.qos.logback:logback-classic=$LOG_BACK_VERSION
30 |
31 | # Vert.x
32 | VERTX_VERSION=3.+
33 | io.vertx:vertx-web=$VERTX_VERSION
34 |
35 | # Test
36 | junit:junit=4.+
37 | io.vertx:vertx-unit=$VERTX_VERSION
38 | org.mockito:mockito-core=1.+
39 | com.github.stefanbirkner:system-rules=1.+
--------------------------------------------------------------------------------
/dependencies.lock:
--------------------------------------------------------------------------------
1 | {
2 | "compile": {
3 | "ch.qos.logback:logback-classic": {
4 | "locked": "1.1.7"
5 | },
6 | "io.vertx:vertx-web": {
7 | "locked": "3.3.3"
8 | },
9 | "org.projectlombok:lombok": {
10 | "locked": "1.16.10"
11 | },
12 | "org.slf4j:slf4j-api": {
13 | "locked": "1.7.21"
14 | }
15 | },
16 | "compileClasspath": {
17 | "ch.qos.logback:logback-classic": {
18 | "locked": "1.1.7"
19 | },
20 | "io.vertx:vertx-web": {
21 | "locked": "3.3.3"
22 | },
23 | "org.projectlombok:lombok": {
24 | "locked": "1.16.10"
25 | },
26 | "org.slf4j:slf4j-api": {
27 | "locked": "1.7.21"
28 | }
29 | },
30 | "compileOnly": {
31 | "ch.qos.logback:logback-classic": {
32 | "locked": "1.1.7"
33 | },
34 | "io.vertx:vertx-web": {
35 | "locked": "3.3.3"
36 | },
37 | "org.projectlombok:lombok": {
38 | "locked": "1.16.10"
39 | },
40 | "org.slf4j:slf4j-api": {
41 | "locked": "1.7.21"
42 | }
43 | },
44 | "default": {
45 | "ch.qos.logback:logback-classic": {
46 | "locked": "1.1.7"
47 | },
48 | "io.vertx:vertx-web": {
49 | "locked": "3.3.3"
50 | },
51 | "org.projectlombok:lombok": {
52 | "project": true
53 | },
54 | "org.slf4j:slf4j-api": {
55 | "locked": "1.7.21"
56 | }
57 | },
58 | "findbugs": {
59 | "com.google.code.findbugs:findbugs": {
60 | "locked": "3.0.1"
61 | }
62 | },
63 | "hystrixDashboard": {
64 | "com.netflix.hystrix:hystrix-dashboard": {
65 | "locked": "1.5.6"
66 | }
67 | },
68 | "pmd": {
69 | "net.sourceforge.pmd:pmd-java": {
70 | "locked": "5.5.0"
71 | }
72 | },
73 | "provided": {
74 | "org.projectlombok:lombok": {
75 | "locked": "1.16.10"
76 | }
77 | },
78 | "runtime": {
79 | "ch.qos.logback:logback-classic": {
80 | "locked": "1.1.7"
81 | },
82 | "io.vertx:vertx-web": {
83 | "locked": "3.3.3"
84 | },
85 | "org.projectlombok:lombok": {
86 | "locked": "1.16.10"
87 | },
88 | "org.slf4j:slf4j-api": {
89 | "locked": "1.7.21"
90 | }
91 | },
92 | "testCompile": {
93 | "ch.qos.logback:logback-classic": {
94 | "locked": "1.1.7"
95 | },
96 | "com.github.kennedyoliveira:hystrix-vertx-metrics-stream": {
97 | "locked": "1.5.6"
98 | },
99 | "com.github.stefanbirkner:system-rules": {
100 | "locked": "1.16.1"
101 | },
102 | "com.netflix.hystrix:hystrix-core": {
103 | "locked": "1.5.6"
104 | },
105 | "io.vertx:vertx-unit": {
106 | "locked": "3.3.3"
107 | },
108 | "io.vertx:vertx-web": {
109 | "locked": "3.3.3"
110 | },
111 | "junit:junit": {
112 | "locked": "4.12"
113 | },
114 | "org.mockito:mockito-core": {
115 | "locked": "1.10.19"
116 | },
117 | "org.projectlombok:lombok": {
118 | "locked": "1.16.10"
119 | },
120 | "org.slf4j:slf4j-api": {
121 | "locked": "1.7.21"
122 | }
123 | },
124 | "testCompileClasspath": {
125 | "ch.qos.logback:logback-classic": {
126 | "locked": "1.1.7"
127 | },
128 | "com.github.kennedyoliveira:hystrix-vertx-metrics-stream": {
129 | "locked": "1.5.6"
130 | },
131 | "com.github.stefanbirkner:system-rules": {
132 | "locked": "1.16.1"
133 | },
134 | "com.netflix.hystrix:hystrix-core": {
135 | "locked": "1.5.6"
136 | },
137 | "io.vertx:vertx-unit": {
138 | "locked": "3.3.3"
139 | },
140 | "io.vertx:vertx-web": {
141 | "locked": "3.3.3"
142 | },
143 | "junit:junit": {
144 | "locked": "4.12"
145 | },
146 | "org.mockito:mockito-core": {
147 | "locked": "1.10.19"
148 | },
149 | "org.projectlombok:lombok": {
150 | "locked": "1.16.10"
151 | },
152 | "org.slf4j:slf4j-api": {
153 | "locked": "1.7.21"
154 | }
155 | },
156 | "testCompileOnly": {
157 | "ch.qos.logback:logback-classic": {
158 | "locked": "1.1.7"
159 | },
160 | "com.github.kennedyoliveira:hystrix-vertx-metrics-stream": {
161 | "locked": "1.5.6"
162 | },
163 | "com.github.stefanbirkner:system-rules": {
164 | "locked": "1.16.1"
165 | },
166 | "com.netflix.hystrix:hystrix-core": {
167 | "locked": "1.5.6"
168 | },
169 | "io.vertx:vertx-unit": {
170 | "locked": "3.3.3"
171 | },
172 | "io.vertx:vertx-web": {
173 | "locked": "3.3.3"
174 | },
175 | "junit:junit": {
176 | "locked": "4.12"
177 | },
178 | "org.mockito:mockito-core": {
179 | "locked": "1.10.19"
180 | },
181 | "org.projectlombok:lombok": {
182 | "locked": "1.16.10"
183 | },
184 | "org.slf4j:slf4j-api": {
185 | "locked": "1.7.21"
186 | }
187 | },
188 | "testRuntime": {
189 | "ch.qos.logback:logback-classic": {
190 | "locked": "1.1.7"
191 | },
192 | "com.github.kennedyoliveira:hystrix-vertx-metrics-stream": {
193 | "locked": "1.5.6"
194 | },
195 | "com.github.stefanbirkner:system-rules": {
196 | "locked": "1.16.1"
197 | },
198 | "com.netflix.hystrix:hystrix-core": {
199 | "locked": "1.5.6"
200 | },
201 | "io.vertx:vertx-unit": {
202 | "locked": "3.3.3"
203 | },
204 | "io.vertx:vertx-web": {
205 | "locked": "3.3.3"
206 | },
207 | "junit:junit": {
208 | "locked": "4.12"
209 | },
210 | "org.mockito:mockito-core": {
211 | "locked": "1.10.19"
212 | },
213 | "org.projectlombok:lombok": {
214 | "locked": "1.16.10"
215 | },
216 | "org.slf4j:slf4j-api": {
217 | "locked": "1.7.21"
218 | }
219 | }
220 | }
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM kennedyoliveira/java8
2 | MAINTAINER Kennedy Oliveira
3 |
4 | ENV STANDALONE_HYSTRIX_VERSION 1.5.6
5 | ENV HYSTRIX_DASHBOARD_HOME /opt/standalone-hystrix-dashboard/
6 |
7 | RUN apk add --update curl
8 | RUN mkdir -p $HYSTRIX_DASHBOARD_HOME && \
9 | cd $HYSTRIX_DASHBOARD_HOME && \
10 | curl -L https://bintray.com/kennedyoliveira/maven/download_file?file_path=com/github/kennedyoliveira/standalone-hystrix-dashboard/${STANDALONE_HYSTRIX_VERSION}/standalone-hystrix-dashboard-${STANDALONE_HYSTRIX_VERSION}-all.jar \
11 | -o standalone-hystrix-dashboard-${STANDALONE_HYSTRIX_VERSION}-all.jar && \
12 | rm -rf /var/cache/apk/*
13 |
14 | WORKDIR $HYSTRIX_DASHBOARD_HOME
15 | EXPOSE 7979
16 | ENTRYPOINT exec java -jar $JVM_ARGS standalone-hystrix-dashboard-$STANDALONE_HYSTRIX_VERSION-all.jar
--------------------------------------------------------------------------------
/findbugs-exclusions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | versioneye.projectid=56fcad23905db1003b29956c
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kennedyoliveira/standalone-hystrix-dashboard/8db0c97e0640dc51e24ea9823489d3f5559520b6/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Oct 03 15:00:44 BRT 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'standalone-hystrix-dashboard'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/Configuration.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | /**
4 | * Simple server configurations.
5 | *
6 | * @author Kennedy Oliveira
7 | */
8 | public final class Configuration {
9 |
10 | /**
11 | * Server port to listen to, the default is {@code 7979}.
12 | */
13 | public static final String SERVER_PORT = "serverPort";
14 |
15 | /**
16 | * Server address to bind, the default is {@code 0.0.0.0}
17 | */
18 | public static final String BIND_ADDRESS = "bindAddress";
19 |
20 | /**
21 | * Flag for disabling compression on the server, the default is {@link Boolean#FALSE}.
22 | */
23 | public static final String DISABLE_COMPRESSION = "disableCompression";
24 |
25 | /**
26 | * Utility class, no instances for you!
27 | */
28 | private Configuration() {}
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardLauncher.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.vertx.core.DeploymentOptions;
4 | import io.vertx.core.Launcher;
5 | import io.vertx.core.Vertx;
6 | import io.vertx.core.logging.SLF4JLogDelegateFactory;
7 | import lombok.extern.slf4j.Slf4j;
8 |
9 | /**
10 | * Custom launcher for HystrixDashboard
11 | *
12 | * @author Kennedy Oliveira
13 | */
14 | @Slf4j
15 | public class HystrixDashboardLauncher extends Launcher {
16 |
17 | /**
18 | * Main Entry-Point, can be runned from IDE too.
19 | *
20 | * @param args command line args.
21 | */
22 | public static void main(String[] args) {
23 | System.setProperty("vertx.logger-delegate-factory-class-name", SLF4JLogDelegateFactory.class.getName());
24 | new HystrixDashboardLauncher().dispatch(args);
25 | }
26 |
27 | @Override
28 | public void handleDeployFailed(Vertx vertx, String mainVerticle, DeploymentOptions deploymentOptions, Throwable cause) {
29 | log.error("Deploying verticle " + mainVerticle, cause);
30 | super.handleDeployFailed(vertx, mainVerticle, deploymentOptions, cause);
31 | }
32 |
33 | /**
34 | * Tries to get the verticle from manifest entry {@code Main-Verticle}, if not found, returns the default one.
35 | * This method is make easy develop.
36 | *
37 | * @return the main verticle to run if no one was specified with a command
38 | */
39 | @Override
40 | protected String getMainVerticle() {
41 | final String manifestVerticle = super.getMainVerticle();
42 | return manifestVerticle != null ? manifestVerticle : HystrixDashboardVerticle.class.getName();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardProxyConnectionHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.vertx.core.Handler;
4 | import io.vertx.core.MultiMap;
5 | import io.vertx.core.Vertx;
6 | import io.vertx.core.VertxException;
7 | import io.vertx.core.buffer.Buffer;
8 | import io.vertx.core.http.*;
9 | import io.vertx.core.streams.Pump;
10 | import io.vertx.ext.web.RoutingContext;
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | import java.net.URI;
14 | import java.nio.charset.StandardCharsets;
15 | import java.util.Optional;
16 |
17 | /**
18 | * This handler will proxy connection to a Hystrix Event Metrics stream using Basic Auth if present, and compressing the response.
19 | *
20 | * @author Kennedy Oliveira
21 | */
22 | @Slf4j
23 | public class HystrixDashboardProxyConnectionHandler implements Handler {
24 |
25 | /**
26 | * Creates a new {@link HystrixDashboardProxyConnectionHandler}, that will handle proxying connection to a Hystrix Event Metrics stream using Basic Auth if present
27 | *
28 | * @return The new {@link HystrixDashboardProxyConnectionHandler}
29 | */
30 | static HystrixDashboardProxyConnectionHandler create() {
31 | return new HystrixDashboardProxyConnectionHandler();
32 | }
33 |
34 | @Override
35 | public void handle(RoutingContext requestCtx) {
36 | getProxyUrl(requestCtx)
37 | .flatMap(proxiedUrl -> createUriFromUrl(proxiedUrl, requestCtx))
38 | .ifPresent(uri -> proxyRequest(uri, requestCtx));
39 | }
40 |
41 | /**
42 | * Extract the url to Proxy.
43 | *
44 | * @param requestCtx Context of the current request.
45 | * @return The url to proxy or null if it wasn't found.
46 | */
47 | Optional getProxyUrl(RoutingContext requestCtx) {
48 | final HttpServerRequest serverRequest = requestCtx.request();
49 | final HttpServerResponse serverResponse = requestCtx.response();
50 |
51 | // origin = metrics stream endpoint
52 | String origin = serverRequest.getParam("origin");
53 | if (origin == null || origin.isEmpty()) {
54 | log.warn("Request without origin");
55 | serverResponse.setStatusCode(500)
56 | .end(Buffer.buffer("Required parameter 'origin' missing. Example: 107.20.175.135:7001".getBytes(StandardCharsets.UTF_8)));
57 | return Optional.empty();
58 | }
59 | origin = origin.trim();
60 |
61 | boolean hasFirstParameter = false;
62 | StringBuilder url = new StringBuilder();
63 | // if there is no http, i add
64 | if (!origin.startsWith("http")) {
65 | url.append("http://");
66 | }
67 | url.append(origin);
68 |
69 | // if contains any query parameter
70 | if (origin.contains("?")) {
71 | hasFirstParameter = true;
72 | }
73 |
74 | // add the request params to the url to proxy, because need to forward Delay and maybe another future param
75 | MultiMap params = serverRequest.params();
76 | for (String key : params.names()) {
77 | if (!"origin".equals(key) && !"authorization".equals(key)) {
78 | String value = params.get(key);
79 |
80 | if (hasFirstParameter) {
81 | url.append("&");
82 | } else {
83 | url.append("?");
84 | hasFirstParameter = true;
85 | }
86 | url.append(key).append("=").append(value);
87 | }
88 | }
89 |
90 | return Optional.of(url.toString());
91 | }
92 |
93 | /**
94 | * Tries to transform the {@code proxiedUrl} in a URI, if fails, return a response to the caller and end the request.
95 | *
96 | * @param proxiedUrl Url to convert
97 | * @param requestCtx Context of the current request.
98 | * @return If succeed, a {@link URI}, otherwise {@code null}.
99 | */
100 | Optional createUriFromUrl(String proxiedUrl, RoutingContext requestCtx) {
101 | try {
102 | return Optional.of(URI.create(proxiedUrl));
103 | } catch (Exception e) {
104 | final String errorMsg = String.format("Failed to parse the url [%s] to proxy.", proxiedUrl);
105 | log.error(errorMsg, e);
106 | requestCtx.response().setStatusCode(500).end(errorMsg);
107 | return Optional.empty();
108 | }
109 | }
110 |
111 | /**
112 | * Proxy the request to url {@code proxiedUrl}.
113 | *
114 | * @param proxiedUrl The url to proxy.
115 | * @param requestCtx Context of the current request.
116 | */
117 | void proxyRequest(URI proxiedUrl, RoutingContext requestCtx) {
118 | final HttpServerRequest serverRequest = requestCtx.request();
119 | final HttpServerResponse serverResponse = requestCtx.response();
120 |
121 | final String host = proxiedUrl.getHost();
122 | final String scheme = proxiedUrl.getScheme();
123 | final String path = proxiedUrl.getPath();
124 | int port = proxiedUrl.getPort();
125 |
126 | if (port == -1) {
127 | // if there are no port, and the scheme is HTTPS, set as 443, default HTTPS port, else set as 80, default HTTP port
128 | if ("https".equalsIgnoreCase(scheme)) {
129 | log.warn("No port specified in the url to proxy [{}], using 443 since it's a HTTPS request.", proxiedUrl);
130 | port = 443;
131 | } else {
132 | log.warn("No port specified in the url to proxy [{}], using 80", proxiedUrl);
133 | port = 80;
134 | }
135 | }
136 |
137 | log.info("Proxing request to {}", proxiedUrl);
138 |
139 | // create a request
140 | final HttpClient httpClient = createHttpClient(requestCtx.vertx());
141 | final HttpClientRequest httpClientRequest = httpClient.get(port, host, path + (proxiedUrl.getQuery() != null ? "?" + proxiedUrl.getQuery() : ""));
142 |
143 | // setup basic auth if present
144 | configureBasicAuth(serverRequest, httpClientRequest);
145 |
146 | // TODO Implement the connection close that is available on vert.x 3.3 instead of closing the client
147 |
148 | // set the serverResponse handler
149 | httpClientRequest.handler(clientResponse -> {
150 | // response success
151 | if (clientResponse.statusCode() == 200) {
152 | serverResponse.setChunked(true);
153 | serverResponse.setStatusCode(200);
154 |
155 | // setup the headers from proxied request, ignoring the transfer encoding
156 | clientResponse.headers().forEach(headerEntry -> {
157 | if (!HttpHeaders.TRANSFER_ENCODING.equals(headerEntry.getKey()))
158 | serverResponse.putHeader(headerEntry.getKey(), headerEntry.getValue());
159 | });
160 |
161 | // transfer response from the proxied to the server response
162 | final Pump pump = Pump.pump(clientResponse, serverResponse);
163 |
164 | // if there are any errors, usually connection closed, stop the pump and close the connection if it still open
165 | clientResponse.exceptionHandler(t -> {
166 | if (t instanceof VertxException && t.getMessage().equalsIgnoreCase("Connection was closed")) {
167 | log.info("Proxied connection stopped.");
168 | } else {
169 | log.error("Proxy response", t);
170 | }
171 | closeQuietlyHttpClient(httpClient);
172 | });
173 |
174 | // start the transferring
175 | pump.start();
176 | } else {
177 | log.error("Connecting to the proxied url: Status Code: {}, Status Message: {}", clientResponse.statusCode(), clientResponse.statusMessage());
178 | serverResponse.setStatusCode(500).end("Fail to connect to client, response code: " + clientResponse.statusCode());
179 | closeQuietlyHttpClient(httpClient);
180 | }
181 | });
182 |
183 | // handle errors on the client side (hystrix-dashboard client)
184 | requestCtx.response().closeHandler(ignored -> {
185 | log.warn("[Client disconnected] Connection closed on client side");
186 | log.info("Stopping the proxying...");
187 | closeQuietlyHttpClient(httpClient);
188 | });
189 |
190 | // handle exception on request
191 | serverRequest.exceptionHandler(t -> {
192 | log.error("On server request", t);
193 | closeQuietlyHttpClient(httpClient);
194 | });
195 |
196 | // handle exceptions on proxied server side
197 | httpClientRequest.exceptionHandler(t -> {
198 | log.error("Proxying request", t);
199 | serverResponse.setStatusCode(500);
200 |
201 | if (t.getMessage() != null)
202 | serverResponse.end(t.getMessage());
203 | else
204 | serverResponse.end();
205 |
206 | closeQuietlyHttpClient(httpClient);
207 | });
208 |
209 | // request timeout
210 | httpClientRequest.setTimeout(5000L);
211 |
212 | // do the request
213 | httpClientRequest.end();
214 | }
215 |
216 | /**
217 | * Configure basic auth for proxied stream
218 | *
219 | * @param serverRequest request
220 | * @param httpClientRequest client that will proxy the request
221 | */
222 | void configureBasicAuth(HttpServerRequest serverRequest, HttpClientRequest httpClientRequest) {
223 | final String authorization = serverRequest.getParam("authorization");
224 | if (authorization != null) {
225 | httpClientRequest.putHeader(HttpHeaders.AUTHORIZATION, authorization);
226 | }
227 | }
228 |
229 | /**
230 | * If the HttpClient is already closed when you try to close it again it throws {@link IllegalStateException}, this method swallow it.
231 | *
232 | * @param client Client to close.
233 | */
234 | @SuppressWarnings({"PMD.EmptyCatchBlock"})
235 | private void closeQuietlyHttpClient(HttpClient client) {
236 | try {
237 | client.close();
238 | } catch (Exception ignored) { }
239 | }
240 |
241 | /**
242 | * Initialize if need and returns the {@link HttpClient} for proxying requests.
243 | *
244 | * @return The {@link HttpClient} for proxying requests.
245 | */
246 | private HttpClient createHttpClient(Vertx vertx) {
247 | final HttpClientOptions httpClientOptions = new HttpClientOptions().setKeepAlive(false)
248 | .setTryUseCompression(true)
249 | .setMaxPoolSize(1); // just 1 because the client will be closed when the request end
250 |
251 | return vertx.createHttpClient(httpClientOptions);
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardProxyEurekaAppsListingHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.netty.handler.codec.http.HttpResponseStatus;
4 | import io.vertx.core.Handler;
5 | import io.vertx.core.Vertx;
6 | import io.vertx.core.http.*;
7 | import io.vertx.ext.web.RoutingContext;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | import java.net.URI;
11 | import java.nio.charset.StandardCharsets;
12 | import java.util.Objects;
13 | import java.util.Optional;
14 |
15 | import static com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard.StringUtils.isNullOrEmpty;
16 |
17 | /**
18 | * Handler for proxying a request to eureka for listing apps.
19 | *
20 | * @author Kennedy Oliveira
21 | * @since 1.5.6
22 | */
23 | @Slf4j
24 | public class HystrixDashboardProxyEurekaAppsListingHandler implements Handler {
25 |
26 | private final HttpClient httpClient;
27 |
28 | HystrixDashboardProxyEurekaAppsListingHandler(Vertx vertx) {
29 | final HttpClientOptions httpClientOptions = new HttpClientOptions().setConnectTimeout(10000)
30 | .setTryUseCompression(true)
31 | .setIdleTimeout(60)
32 | .setKeepAlive(true)
33 | .setMaxPoolSize(10);
34 |
35 | this.httpClient = vertx.createHttpClient(httpClientOptions);
36 | }
37 |
38 | public static HystrixDashboardProxyEurekaAppsListingHandler create(Vertx vertx) {
39 | Objects.requireNonNull(vertx);
40 | return new HystrixDashboardProxyEurekaAppsListingHandler(vertx);
41 | }
42 |
43 | @Override
44 | public void handle(RoutingContext routingContext) {
45 | final HttpServerRequest request = routingContext.request();
46 | final HttpServerResponse response = routingContext.response();
47 |
48 | final String eurekaUrl = request.getParam("url");
49 |
50 | log.debug("Routing URL: [{}] to eureka", eurekaUrl);
51 |
52 | if (isNullOrEmpty(eurekaUrl)) {
53 | log.warn("URL is null for Eureka server...");
54 |
55 | response.setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
56 | .end("Error. You need supply a valid eureka URL.");
57 | } else {
58 | try {
59 | final URI uri = URI.create(eurekaUrl);
60 |
61 | final int port = Optional.ofNullable(uri.getPort()).orElse(80);
62 | final String host = uri.getHost();
63 | final String path = uri.getPath();
64 |
65 | httpClient.get(port, host, path)
66 | .handler(clientResp -> clientResp.bodyHandler(respBuffer -> {
67 | // proxied eureka request OK
68 | if (clientResp.statusCode() == 200) {
69 | response.putHeader(HttpHeaders.CONTENT_TYPE, clientResp.headers().get(HttpHeaders.CONTENT_TYPE))
70 | .end(respBuffer);
71 | } else {
72 | log.error("Fetching eureka apps from url: [{}]\nResponse Status: [{}], Response: [{}]",
73 | eurekaUrl,
74 | clientResp.statusCode(),
75 | respBuffer.toString(StandardCharsets.UTF_8));
76 | internalServerError(response, "Error while fetching eureka apps from url " + eurekaUrl + ".");
77 | }
78 | }))
79 | .exceptionHandler(ex -> {
80 | log.error("Fetching eureka apps from url: [ " + eurekaUrl + "]", ex);
81 | internalServerError(response, "Error while fetching eureka apps from url " + eurekaUrl + ".\n" + ex.getMessage());
82 | })
83 | .end();
84 | } catch (Exception e) {
85 | log.error("Fetching eureka apps", e);
86 | internalServerError(response, "Error while fetching eureka apps from url " + eurekaUrl + ".\n" + e.getMessage());
87 | }
88 | }
89 | }
90 |
91 | private void internalServerError(HttpServerResponse response, String message) {
92 | response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(message);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardVerticle.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.netty.handler.codec.http.HttpResponseStatus;
4 | import io.vertx.core.AbstractVerticle;
5 | import io.vertx.core.Future;
6 | import io.vertx.core.http.HttpHeaders;
7 | import io.vertx.core.http.HttpServerOptions;
8 | import io.vertx.ext.web.Router;
9 | import io.vertx.ext.web.handler.StaticHandler;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | /**
14 | * Verticle for the Dashboard web app.
15 | * Serves static content and proxy the streams
16 | *
17 | * @author Kennedy Oliveira
18 | */
19 | public class HystrixDashboardVerticle extends AbstractVerticle {
20 |
21 | private final static Logger log = LoggerFactory.getLogger(HystrixDashboardVerticle.class);
22 |
23 | @Override
24 | public void start(Future startFuture) throws Exception {
25 | vertx.sharedData().getCounter("instance-count", counterAsync -> {
26 | if (counterAsync.failed()) {
27 | startFuture.fail(counterAsync.cause());
28 | } else {
29 | counterAsync.result()
30 | .incrementAndGet(counter -> {
31 | if (counter.failed()) {
32 | startFuture.fail(counter.cause());
33 | } else {
34 | log.info("Initializing the HystrixDashboardVerticle instance {}", counter.result());
35 | initialize(startFuture);
36 | }
37 | });
38 | }
39 | });
40 | }
41 |
42 | /**
43 | * Initialize the Verticle and setup the Http Server
44 | *
45 | * @param startFuture The start future to pass the initialization result
46 | */
47 | private void initialize(Future startFuture) {
48 | final Router mainRouter = createRoutes();
49 | final HttpServerOptions options = getServerOptions();
50 |
51 | startServer(startFuture, mainRouter, options);
52 | }
53 |
54 | /**
55 | * Start the HTTP server.
56 | *
57 | * @param startFuture The start future to report failure or success for async start.
58 | * @param mainRouter {@link Router} with the routes for the server.
59 | * @param options {@link HttpServerOptions} with the server configuration.
60 | */
61 | private void startServer(Future startFuture, Router mainRouter, HttpServerOptions options) {
62 | vertx.createHttpServer(options)
63 | .requestHandler(mainRouter::accept)
64 | .listen(res -> {
65 | if (res.failed()) {
66 | startFuture.fail(res.cause());
67 | } else {
68 | log.info("Listening on port: {}", options.getPort());
69 | log.info("Access the dashboard in your browser: http://{}:{}/hystrix-dashboard/",
70 | "0.0.0.0".equals(options.getHost()) ? "localhost" : options.getHost(), // NOPMD
71 | res.result().actualPort());
72 | startFuture.complete();
73 | }
74 | });
75 | }
76 |
77 | /**
78 | * Read configuration for different sources and generate a {@link HttpServerOptions} with the configurations provided.
79 | * Currently reading configuration options from:
80 | *
81 | * - Vert.x Config Option
82 | * - Command Line Option passed with {@code -D}
83 | *
84 | *
85 | * The list above is from the lesser priority to the highest, so currently the command line options is the highest priority and will
86 | * override any other configuration option
87 | *
88 | * @return {@link HttpServerOptions} with configuration for the server.
89 | */
90 | private HttpServerOptions getServerOptions() {
91 | final Integer systemServerPort = Integer.getInteger(Configuration.SERVER_PORT);
92 | final String systemBindAddress = System.getProperty(Configuration.BIND_ADDRESS);
93 | final String systemDisableCompression = System.getProperty(Configuration.DISABLE_COMPRESSION);
94 |
95 | final Integer serverPort = systemServerPort != null ? systemServerPort : config().getInteger(Configuration.SERVER_PORT, 7979);
96 | final String bindAddress = systemBindAddress != null ? systemBindAddress : config().getString(Configuration.BIND_ADDRESS, "0.0.0.0"); // NOPMD
97 | final boolean disableCompression = systemDisableCompression != null ? Boolean.valueOf(systemDisableCompression) : config().getBoolean(Configuration.DISABLE_COMPRESSION,
98 | Boolean.FALSE);
99 | final HttpServerOptions options = new HttpServerOptions().setTcpKeepAlive(true)
100 | .setIdleTimeout(10000)
101 | .setPort(serverPort)
102 | .setHost(bindAddress)
103 | .setCompressionSupported(!disableCompression);
104 |
105 | log.info("Compression support enabled: {}", !disableCompression);
106 | return options;
107 | }
108 |
109 | /**
110 | * Create the routes for dashboard app
111 | *
112 | * @return A {@link Router} with all the routes needed for the app.
113 | */
114 | private Router createRoutes() {
115 | final Router hystrixRouter = Router.router(vertx);
116 |
117 | // proxy stream handler
118 | hystrixRouter.get("/proxy.stream").handler(HystrixDashboardProxyConnectionHandler.create());
119 |
120 | // proxy the eureka apps listing
121 | hystrixRouter.get("/eureka").handler(HystrixDashboardProxyEurekaAppsListingHandler.create(vertx));
122 |
123 | hystrixRouter.route("/*")
124 | .handler(StaticHandler.create()
125 | .setCachingEnabled(true)
126 | .setCacheEntryTimeout(1000L * 60 * 60 * 24));
127 |
128 | final Router mainRouter = Router.router(vertx);
129 |
130 | // if send a route without the trailing '/' some problems will occur, so i redirect the guy using the trailing '/'
131 | mainRouter.route("/hystrix-dashboard")
132 | .handler(context -> {
133 | if (context.request().path().endsWith("/")) {
134 | context.next();
135 | } else {
136 | context.response()
137 | .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code())
138 | .putHeader(HttpHeaders.LOCATION, "/hystrix-dashboard/")
139 | .end();
140 | }
141 | });
142 |
143 | mainRouter.mountSubRouter("/hystrix-dashboard", hystrixRouter);
144 | return mainRouter;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/main/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/StringUtils.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | /**
4 | * Utility methods for {@link String}.
5 | *
6 | * @author Kennedy Oliveira
7 | * @since 1.5.6
8 | */
9 | public class StringUtils {
10 |
11 | /**
12 | * Checks whether the {@link String} {@code s} is {@code null} or not.
13 | *
14 | * @param s {@link String} to test.
15 | * @return {@code true} if is {@code null} or empty, {@code false} otherwise.
16 | */
17 | public static boolean isNullOrEmpty(String s) {
18 | return s == null || s.isEmpty();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/test/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardConfigurationTest.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.vertx.core.http.HttpClient;
4 | import io.vertx.core.http.HttpClientOptions;
5 | import io.vertx.core.http.HttpHeaders;
6 | import io.vertx.ext.unit.Async;
7 | import io.vertx.ext.unit.TestContext;
8 | import io.vertx.ext.unit.junit.RunTestOnContext;
9 | import io.vertx.ext.unit.junit.VertxUnitRunner;
10 | import org.junit.After;
11 | import org.junit.Before;
12 | import org.junit.Rule;
13 | import org.junit.Test;
14 | import org.junit.contrib.java.lang.system.RestoreSystemProperties;
15 | import org.junit.runner.RunWith;
16 |
17 | /**
18 | * @author Kennedy Oliveira
19 | */
20 | @RunWith(VertxUnitRunner.class)
21 | public class HystrixDashboardConfigurationTest {
22 |
23 | @SuppressWarnings({"PMD.AvoidUsingHardCodedIP"})
24 | private final static String SERVER_BIND_ADDRESS = "127.0.0.1";
25 | private final static int SERVER_PORT = 9999;
26 | private final static String SERVER_USE_COMPRESSION = "true";
27 |
28 | @Rule
29 | public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
30 |
31 | @Rule
32 | public RunTestOnContext runTestOnContext = new RunTestOnContext();
33 |
34 | @Before
35 | public void setUp(TestContext testContext) throws Exception {
36 | System.setProperty(Configuration.SERVER_PORT, String.valueOf(SERVER_PORT));
37 | System.setProperty(Configuration.DISABLE_COMPRESSION, String.valueOf(!Boolean.valueOf(SERVER_USE_COMPRESSION)));
38 | System.setProperty(Configuration.BIND_ADDRESS, SERVER_BIND_ADDRESS);
39 |
40 | runTestOnContext.vertx().deployVerticle(new HystrixDashboardVerticle(), testContext.asyncAssertSuccess());
41 | }
42 |
43 | @Test
44 | public void testConfigurationOptions(TestContext testContext) throws Exception {
45 | final HttpClientOptions options = new HttpClientOptions().setTryUseCompression(false);
46 |
47 | final HttpClient httpClient = runTestOnContext.vertx().createHttpClient(options);
48 |
49 | final Async asyncOp = testContext.async();
50 |
51 | // issue a request on the custom server bind address and port, testing for compression
52 | httpClient.get(SERVER_PORT, SERVER_BIND_ADDRESS, "/hystrix-dashboard/")
53 | .setChunked(false)
54 | .putHeader(HttpHeaders.ACCEPT_ENCODING, HttpHeaders.DEFLATE_GZIP)
55 | .handler(resp -> {
56 | testContext.assertEquals(200, resp.statusCode(), "Should have fetched the index page with status 200");
57 | testContext.assertEquals("gzip", resp.getHeader(HttpHeaders.CONTENT_ENCODING));
58 | })
59 | .exceptionHandler(testContext::fail)
60 | .endHandler(event -> asyncOp.complete())
61 | .end();
62 | }
63 |
64 | @After
65 | public void tearDown(TestContext testContext) throws Exception {
66 | runTestOnContext.vertx().close();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardProxyConnectionHandlerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.vertx.core.http.*;
4 | import io.vertx.ext.web.RoutingContext;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 |
8 | import java.util.Optional;
9 |
10 | import static org.hamcrest.CoreMatchers.is;
11 | import static org.hamcrest.MatcherAssert.assertThat;
12 | import static org.mockito.Mockito.*;
13 |
14 | /**
15 | * @author Kennedy Oliveira
16 | */
17 | public class HystrixDashboardProxyConnectionHandlerTest {
18 |
19 | private final HystrixDashboardProxyConnectionHandler proxyConnectionHandler = new HystrixDashboardProxyConnectionHandler();
20 | private RoutingContext routingContext;
21 | private HttpServerRequest serverRequest;
22 | private HttpServerResponse serverResponse;
23 |
24 | @Before
25 | public void setUp() throws Exception {
26 | routingContext = mock(RoutingContext.class);
27 | serverRequest = mock(HttpServerRequest.class);
28 | serverResponse = mock(HttpServerResponse.class);
29 |
30 | when(routingContext.request()).thenReturn(serverRequest);
31 | when(routingContext.response()).thenReturn(serverResponse);
32 | }
33 |
34 | @Test
35 | public void testProxyWithoutOriginShouldFail() throws Exception {
36 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
37 | final Optional proxyUrl = proxyConnectionHandler.getProxyUrl(routingContext);
38 |
39 | assertThat(proxyUrl.isPresent(), is(false));
40 | verify(serverResponse, times(1)).setStatusCode(500);
41 | }
42 |
43 | @Test
44 | public void testProxyWithOriginShouldSuccess() throws Exception {
45 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
46 | when(serverRequest.getParam("origin")).thenReturn("http://localhost:8080");
47 | when(serverRequest.params()).thenReturn(new CaseInsensitiveHeaders());
48 |
49 | final Optional proxyUrl = proxyConnectionHandler.getProxyUrl(routingContext);
50 |
51 | assertThat(proxyUrl.isPresent(), is(true));
52 | assertThat(proxyUrl.get(), is("http://localhost:8080"));
53 | verify(serverResponse, never()).setStatusCode(anyInt());
54 | }
55 |
56 | @Test
57 | public void testOriginWithoutSchemaShouldAddHttp() throws Exception {
58 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
59 | when(serverRequest.getParam("origin")).thenReturn("localhost:8080");
60 | when(serverRequest.params()).thenReturn(new CaseInsensitiveHeaders());
61 |
62 | final Optional proxyUrl = proxyConnectionHandler.getProxyUrl(routingContext);
63 |
64 | assertThat(proxyUrl.isPresent(), is(true));
65 | assertThat(proxyUrl.get(), is("http://localhost:8080"));
66 | verify(serverResponse, never()).setStatusCode(anyInt());
67 | }
68 |
69 | @Test
70 | public void testOriginWithQueryParamsShouldAddToProxyUrl() throws Exception {
71 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
72 | when(serverRequest.getParam("origin")).thenReturn("http://localhost:8080");
73 | when(serverRequest.params()).thenReturn(new CaseInsensitiveHeaders().add("queryParam1", "test").add("queryParam2", "test2"));
74 |
75 | final Optional proxyUrl = proxyConnectionHandler.getProxyUrl(routingContext);
76 |
77 | assertThat(proxyUrl.isPresent(), is(true));
78 |
79 | assertThat(proxyUrl.get(), is("http://localhost:8080?queryParam1=test&queryParam2=test2"));
80 | verify(serverResponse, never()).setStatusCode(anyInt());
81 | }
82 |
83 | @Test
84 | public void testProxyQueryParamOriginAndAuthorizationShouldBeRomovedFromFinalUrl() throws Exception {
85 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
86 | String url = "http://localhost:8080";
87 | when(serverRequest.getParam("origin")).thenReturn(url);
88 | when(serverRequest.params()).thenReturn(new CaseInsensitiveHeaders().add("queryParam1", "test")
89 | .add("queryParam2", "test2")
90 | .add("authorization", "blablabla")
91 | .add("origin", "http://localhost:8080?authorization=Basicblablabla&queryParam1=test&queryParam2=test2"));
92 |
93 | final Optional proxyUrl = proxyConnectionHandler.getProxyUrl(routingContext);
94 |
95 | assertThat(proxyUrl.isPresent(), is(true));
96 | assertThat(proxyUrl.get(), is("http://localhost:8080?queryParam1=test&queryParam2=test2"));
97 | verify(serverResponse, never()).setStatusCode(anyInt());
98 | }
99 |
100 | @Test
101 | public void testShouldSetupBasicAuthIfPresent() throws Exception {
102 | final HttpClientRequest httpClient = mock(HttpClientRequest.class);
103 | when(serverRequest.getParam("authorization")).thenReturn("zupao");
104 |
105 | proxyConnectionHandler.configureBasicAuth(serverRequest, httpClient);
106 | verify(httpClient, times(1)).putHeader(HttpHeaders.AUTHORIZATION, "zupao");
107 | }
108 | }
--------------------------------------------------------------------------------
/src/test/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardProxyEurekaAppsListingHandlerTest.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.netty.handler.codec.http.HttpResponseStatus;
4 | import io.vertx.core.Vertx;
5 | import io.vertx.core.http.HttpServerRequest;
6 | import io.vertx.core.http.HttpServerResponse;
7 | import io.vertx.ext.web.RoutingContext;
8 | import org.junit.AfterClass;
9 | import org.junit.BeforeClass;
10 | import org.junit.Test;
11 |
12 | import static org.mockito.Mockito.*;
13 |
14 | /**
15 | * @author Kennedy Oliveira
16 | */
17 | public class HystrixDashboardProxyEurekaAppsListingHandlerTest {
18 |
19 | private static HystrixDashboardProxyEurekaAppsListingHandler proxyEurekaAppsListingHandler;
20 | private static RoutingContext routingContext;
21 | private static HttpServerRequest serverRequest;
22 | private static HttpServerResponse serverResponse;
23 | private static Vertx vertx;
24 |
25 | @BeforeClass
26 | public static void setUp() throws Exception {
27 | vertx = Vertx.vertx();
28 | proxyEurekaAppsListingHandler = HystrixDashboardProxyEurekaAppsListingHandler.create(vertx);
29 | routingContext = mock(RoutingContext.class);
30 | serverRequest = mock(HttpServerRequest.class);
31 | serverResponse = mock(HttpServerResponse.class);
32 |
33 | when(routingContext.request()).thenReturn(serverRequest);
34 | when(routingContext.response()).thenReturn(serverResponse);
35 | }
36 |
37 | @Test
38 | public void testShouldFailOnEmptyEurekaUrl() throws Exception {
39 | when(serverRequest.getParam("url")).thenReturn("");
40 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
41 |
42 | proxyEurekaAppsListingHandler.handle(routingContext);
43 |
44 | verify(serverResponse, times(1)).setStatusCode(HttpResponseStatus.BAD_REQUEST.code());
45 | }
46 |
47 | @Test
48 | public void testShouldFailOnInvalidEurekaUrl() throws Exception {
49 | when(serverRequest.getParam("url")).thenReturn("http:localhost:8080");
50 | when(serverResponse.setStatusCode(anyInt())).thenReturn(serverResponse);
51 |
52 | proxyEurekaAppsListingHandler.handle(routingContext);
53 |
54 | verify(serverResponse, times(1)).setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
55 | }
56 |
57 | @AfterClass
58 | public static void tearDown() throws Exception {
59 | if(vertx != null) {
60 | vertx.close();
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/test/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardProxyEurekaTest.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import io.netty.handler.codec.http.HttpHeaderNames;
4 | import io.netty.handler.codec.http.HttpResponseStatus;
5 | import io.vertx.core.AbstractVerticle;
6 | import io.vertx.core.Future;
7 | import io.vertx.core.Vertx;
8 | import io.vertx.core.http.*;
9 | import io.vertx.ext.unit.Async;
10 | import io.vertx.ext.unit.TestContext;
11 | import io.vertx.ext.unit.junit.VertxUnitRunner;
12 | import io.vertx.ext.web.Router;
13 | import lombok.extern.slf4j.Slf4j;
14 | import org.junit.AfterClass;
15 | import org.junit.BeforeClass;
16 | import org.junit.Test;
17 | import org.junit.runner.RunWith;
18 |
19 | import java.nio.charset.StandardCharsets;
20 |
21 | /**
22 | * @author Kennedy Oliveira
23 | */
24 | @Slf4j
25 | @RunWith(VertxUnitRunner.class)
26 | public class HystrixDashboardProxyEurekaTest {
27 |
28 | private static final int FAKE_EUREKA_SERVER_PORT = 9999;
29 | private static final String DASHBOARD_EUREKA_PROXY_URL = "/hystrix-dashboard/eureka?url=";
30 | private static Vertx vertx;
31 | private static HttpClient httpClient;
32 |
33 | @BeforeClass
34 | public static void setUp(TestContext testContext) throws Exception {
35 | vertx = Vertx.vertx();
36 |
37 | deployFakeEurekaVerticle().compose(i -> deployDashboard())
38 | .compose(i -> initializeHttpClientForDashboard())
39 | .setHandler(testContext.asyncAssertSuccess());
40 | }
41 |
42 | @Test
43 | public void testShouldFetchDataWithHeaders(TestContext testContext) throws Exception {
44 | final String fakeEurekaServerUrl = "http://localhost:" + FAKE_EUREKA_SERVER_PORT + "/eureka/v2/apps";
45 | final String dashboardProxyUrl = DASHBOARD_EUREKA_PROXY_URL + fakeEurekaServerUrl;
46 |
47 | final Async fetchData = testContext.async();
48 |
49 | httpClient.getNow(dashboardProxyUrl, resp -> resp.bodyHandler(buffer -> {
50 | final String responseData = buffer.toString(StandardCharsets.UTF_8);
51 |
52 | if (resp.statusCode() != 200) {
53 | testContext.fail("Response Status => " + resp.statusCode() + "\nResponse: " + responseData);
54 | } else {
55 | testContext.assertTrue("application/xml".equals(resp.getHeader(HttpHeaderNames.CONTENT_TYPE)));
56 |
57 | testContext.assertTrue(responseData.contains("UP_2_"));
58 | testContext.assertTrue(responseData.contains("1472352522224"));
59 |
60 | fetchData.complete();
61 | }
62 | }));
63 |
64 | fetchData.awaitSuccess(5000L);
65 | }
66 |
67 | @Test
68 | public void testShouldFailOnOfflineEureka(TestContext testContext) throws Exception {
69 | final String proxyUrl = DASHBOARD_EUREKA_PROXY_URL + "http://localhost:54321/eureka/v2/apps";
70 | final Async proxyRequest = testContext.async();
71 |
72 | httpClient.getNow(proxyUrl, resp -> {
73 | testContext.assertTrue(resp.statusCode() == HttpResponseStatus.INTERNAL_SERVER_ERROR.code());
74 |
75 | resp.bodyHandler(bodyBuffer -> {
76 | final String body = bodyBuffer.toString(StandardCharsets.UTF_8);
77 | log.info("Response Body: {}", body);
78 | testContext.assertTrue(body.contains("Connection refused"));
79 | proxyRequest.complete();
80 | });
81 | });
82 |
83 | proxyRequest.awaitSuccess(5000L);
84 | }
85 |
86 | private static Future deployFakeEurekaVerticle() {
87 | final Future handler = Future.future();
88 |
89 | vertx.deployVerticle(fakeEurekaVerticle(), handler.completer());
90 |
91 | return handler.map(i -> null);
92 | }
93 |
94 | private static Future deployDashboard() {
95 | final Future future = Future.future();
96 |
97 | vertx.deployVerticle(HystrixDashboardVerticle.class.getName(), future.completer());
98 |
99 | return future.map(i -> null);
100 | }
101 |
102 | private static Future initializeHttpClientForDashboard() {
103 | httpClient = vertx.createHttpClient(new HttpClientOptions()
104 | .setDefaultHost("localhost")
105 | .setDefaultPort(7979));
106 |
107 | return Future.succeededFuture();
108 | }
109 |
110 | private static AbstractVerticle fakeEurekaVerticle() {
111 | return new AbstractVerticle() {
112 |
113 | @Override
114 | public void start(Future startFuture) throws Exception {
115 | final Router router = Router.router(vertx);
116 |
117 | router.get("/eureka/v2/apps").handler(context -> {
118 | final HttpServerResponse response = context.response();
119 |
120 | vertx.fileSystem()
121 | .readFile("eureka-data.xml", res -> {
122 | if (res.succeeded()) {
123 | response.putHeader(HttpHeaders.CONTENT_TYPE, "application/xml")
124 | .end(res.result());
125 | } else {
126 | response.setStatusCode(500)
127 | .end("Error while reading eureka data from classpath");
128 | }
129 | });
130 | });
131 |
132 | vertx.createHttpServer(new HttpServerOptions())
133 | .requestHandler(router::accept)
134 | .listen(FAKE_EUREKA_SERVER_PORT, res -> {
135 | if (res.succeeded()) {
136 | startFuture.complete();
137 | } else {
138 | startFuture.fail(res.cause());
139 | }
140 | });
141 | }
142 | };
143 | }
144 |
145 | @AfterClass
146 | public static void tearDown(TestContext testContext) throws Exception {
147 | if (vertx != null) {
148 | vertx.close(testContext.asyncAssertSuccess());
149 | }
150 | }
151 | }
--------------------------------------------------------------------------------
/src/test/java/com/github/kennedyoliveira/hystrix/contrib/standalone/dashboard/HystrixDashboardVerticleTest.java:
--------------------------------------------------------------------------------
1 | package com.github.kennedyoliveira.hystrix.contrib.standalone.dashboard;
2 |
3 | import com.github.kennedyoliveira.hystrix.contrib.vertx.metricsstream.EventMetricsStreamHelper;
4 | import com.netflix.hystrix.HystrixCommandGroupKey;
5 | import com.netflix.hystrix.HystrixObservableCommand;
6 | import io.vertx.core.http.HttpClient;
7 | import io.vertx.core.http.HttpHeaders;
8 | import io.vertx.ext.unit.Async;
9 | import io.vertx.ext.unit.TestContext;
10 | import io.vertx.ext.unit.junit.RunTestOnContext;
11 | import io.vertx.ext.unit.junit.VertxUnitRunner;
12 | import org.junit.After;
13 | import org.junit.Before;
14 | import org.junit.Rule;
15 | import org.junit.Test;
16 | import org.junit.runner.RunWith;
17 | import rx.Observable;
18 |
19 | /**
20 | * @author Kennedy Oliveira
21 | */
22 | @RunWith(VertxUnitRunner.class)
23 | public class HystrixDashboardVerticleTest {
24 |
25 | @Rule
26 | public RunTestOnContext runTestOnContext = new RunTestOnContext();
27 |
28 | // default port
29 | int serverPort = 7979;
30 |
31 | @Before
32 | public void setUp(TestContext testContext) throws Exception {
33 | EventMetricsStreamHelper.deployStandaloneMetricsStream(runTestOnContext.vertx(), testContext.asyncAssertSuccess());
34 | runTestOnContext.vertx().deployVerticle(HystrixDashboardVerticle.class.getName(), testContext.asyncAssertSuccess());
35 | }
36 |
37 | @Test
38 | public void testDashboard(TestContext testContext) throws Exception {
39 | final Async asyncRedirect = testContext.async();
40 | final HttpClient httpClient = runTestOnContext.vertx().createHttpClient();
41 | httpClient.get(serverPort, "localhost", "/hystrix-dashboard")
42 | .handler(response -> {
43 | testContext.assertEquals(301, response.statusCode()); // redirect due to missing trailing '/' in request
44 | })
45 | .exceptionHandler(testContext::fail)
46 | .endHandler(e -> asyncRedirect.complete())
47 | .end();
48 |
49 | final Async asyncMainPage = testContext.async();
50 | httpClient.get(serverPort, "localhost", "/hystrix-dashboard/")
51 | .handler(response -> testContext.assertEquals(200, response.statusCode()))
52 | .exceptionHandler(testContext::fail)
53 | .endHandler(e -> asyncMainPage.complete())
54 | .end();
55 |
56 | // generate some metrics
57 | new HystrixObservableCommand(HystrixObservableCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("dummy"))) {
58 | @Override
59 | protected Observable construct() {
60 | return Observable.just("dummy");
61 | }
62 | }.observe();
63 |
64 | final Async asyncProxying = testContext.async();
65 | final String url = "/hystrix-dashboard/proxy.stream?origin=http://localhost:8099/hystrix.stream";
66 | httpClient.get(serverPort, "localhost", url)
67 | .handler(response -> {
68 | testContext.assertEquals(200, response.statusCode());
69 | testContext.assertEquals("text/event-stream;charset=UTF-8", response.getHeader(HttpHeaders.CONTENT_TYPE));
70 | response.handler(buffer -> {
71 | testContext.assertTrue(buffer.length() > 0);
72 | testContext.assertTrue(buffer.toString().contains("data:")); // the first buffer will have a data
73 | });
74 | })
75 | .exceptionHandler(testContext::fail)
76 | .endHandler(e -> asyncProxying.complete())
77 | .end();
78 | }
79 |
80 | @After
81 | public void tearDown(TestContext testContext) throws Exception {
82 | runTestOnContext.vertx().close();
83 | }
84 | }
--------------------------------------------------------------------------------
/src/test/resources/eureka-data.xml:
--------------------------------------------------------------------------------
1 |
2 | 1
3 | UP_2_
4 |
5 | PRODUCT-SERVICE
6 |
7 | 192.168.0.102
8 | PRODUCT-SERVICE
9 | 192.168.0.102
10 | UP
11 | UNKNOWN
12 | 60302
13 | 443
14 | 1
15 |
16 | MyOwn
17 |
18 |
19 | 30
20 | 90
21 | 1472352522224
22 | 1472352522224
23 | 0
24 | 1472352492183
25 |
26 |
27 | product-service:e08cfa26248ed632a3d14d1cdfc4ea89
28 |
29 | http://192.168.0.102:60302/
30 | http://192.168.0.102:60302/info
31 | http://192.168.0.102:60302/health
32 | product-service
33 | false
34 | 1472352522224
35 | 1472352500435
36 | ADDED
37 |
38 |
39 | 192.168.0.102
40 | PRODUCT-SERVICE
41 | 192.168.0.102
42 | UP
43 | UNKNOWN
44 | 60301
45 | 443
46 | 1
47 |
48 | MyOwn
49 |
50 |
51 | 30
52 | 90
53 | 1472352521646
54 | 1472352521646
55 | 0
56 | 1472352491606
57 |
58 |
59 | product-service:9dd2c2af2fda1e28f805a0d5c84d8ce9
60 |
61 | http://192.168.0.102:60301/
62 | http://192.168.0.102:60301/info
63 | http://192.168.0.102:60301/health
64 | product-service
65 | false
66 | 1472352521646
67 | 1472352499584
68 | ADDED
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------