├── .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 | [![Build Status](https://travis-ci.org/kennedyoliveira/standalone-hystrix-dashboard.svg?branch=master)](https://travis-ci.org/kennedyoliveira/standalone-hystrix-dashboard) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/ee8e2b298bf24109b642cde4a4df8635)](https://www.codacy.com/app/kennedy-oliveira/standalone-hystrix-dashboard) 5 | [![Dependency Status](https://www.versioneye.com/user/projects/56fcad23905db1003b29956c/badge.svg?style=flat)](https://www.versioneye.com/user/projects/56fcad23905db1003b29956c) 6 | [![Download](https://api.bintray.com/packages/kennedyoliveira/maven/standalone-hystrix-dashboard/images/download.svg) ](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 | ![Demonstration](https://dl.dropboxusercontent.com/u/17155314/standalone-hystrix-dashboard-example.gif) 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 | --------------------------------------------------------------------------------